Component library
Back to templates

Page template

Tideway contact page

The Tideway contact page: the shared header and footer around a title banner and a navy contact section with a tall photo, a translucent form panel and address, phone and hours cards.

contactinner pageformlogisticsnavyorangetideway

Open preview in a new tab

Use this template

Two ways to get Tideway contact page 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 8 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 "Tideway contact page" page template in my Astro Component Library.

Library page, for reference only:
https://astro.baysixmedia.com/templates/tideway-contact/

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/tideway-contact.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/tideway-contact.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 8 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: "Tideway contact page"
Library page: https://astro.baysixmedia.com/templates/tideway-contact/
Page file: src/pages/tideway-contact.astro
Files: 8 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/tideway-contact.astro =====
---
/**
 * Tideway contact page — a page template from the Astro Component Library.
 * https://astro.baysixmedia.com/templates/tideway-contact/
 *
 * 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 ScrollPillHeader from "../components/ScrollPillHeader.astro";
import TagTitleBanner from "../components/TagTitleBanner.astro";
import DarkContactPanel from "../components/DarkContactPanel.astro";
import ShipCtaFooter from "../components/ShipCtaFooter.astro";

const scrollPillHeaderProps = {
	"items": [
		{
			"label": "Home",
			"href": "/",
			"children": [
				{
					"label": "Homepage 1",
					"href": "/"
				},
				{
					"label": "Homepage 2",
					"href": "/home-2"
				}
			]
		},
		{
			"label": "About",
			"href": "/about"
		},
		{
			"label": "Services",
			"href": "/services"
		},
		{
			"label": "Contact",
			"href": "/contact"
		}
	],
	"brandName": "Tideway",
	"cta": {
		"label": "Get a Quote",
		"href": "/contact"
	},
	"currentPath": "/contact"
};

const tagTitleBannerProps = {
	"title": "Contact",
	"tag": "Tideway",
	"breadcrumbs": [
		{
			"label": "Home",
			"href": "/"
		},
		{
			"label": "Contact Us"
		}
	],
	"background": {
		"src": "/tag-title-banner/port.webp"
	}
};

const darkContactPanelProps = {
	"heading": "Ready to ship smarter? *Contact* our team",
	"image": {
		"src": "/dark-contact-panel/aerial-ship.webp",
		"alt": "Aerial view of a loaded container ship crossing blue water"
	},
	"info": [
		{
			"title": "Location",
			"icon": "pin",
			"lines": [
				"1200 Harbor Way",
				"Oakland, CA 94607"
			],
			"href": "https://maps.google.com/?q=1200+Harbor+Way+Oakland+CA"
		},
		{
			"title": "Support",
			"icon": "headset",
			"lines": [
				"(555) 014-2290",
				"(555) 014-2291"
			],
			"href": "tel:+15550142290"
		},
		{
			"title": "Opening Hours",
			"icon": "clock",
			"lines": [
				"Monday – Saturday",
				"7:00 AM – 7:00 PM"
			]
		}
	]
};

const shipCtaFooterProps = {
	"heading": "Explore our *real world* freight services",
	"cta": {
		"label": "Get a Quote",
		"href": "/contact"
	},
	"brandName": "Tideway",
	"tagline": "A freight partner that plans every load and answers the phone when it matters.",
	"newsletter": {
		"action": "/subscribe",
		"label": "Email address",
		"placeholder": "you@company.com",
		"button": "Submit"
	},
	"columns": [
		{
			"heading": "Services",
			"links": [
				{
					"label": "Freight Services",
					"href": "/services/freight"
				},
				{
					"label": "Cargo Transportation",
					"href": "/services/cargo"
				},
				{
					"label": "Road Transport",
					"href": "/services/road"
				},
				{
					"label": "Global Shipping",
					"href": "/services/global"
				}
			]
		},
		{
			"heading": "Company",
			"links": [
				{
					"label": "About Us",
					"href": "/about"
				},
				{
					"label": "Contact",
					"href": "/contact"
				},
				{
					"label": "Careers",
					"href": "/careers"
				}
			]
		}
	],
	"contact": [
		{
			"label": "(555) 014-2290",
			"href": "tel:+15550142290"
		},
		{
			"label": "hello@tideway.example",
			"href": "mailto:hello@tideway.example"
		},
		{
			"label": "1200 Harbor Way, Oakland, CA",
			"href": "https://maps.google.com/?q=1200+Harbor+Way+Oakland+CA"
		}
	],
	"legalLinks": [
		{
			"label": "Terms & Conditions",
			"href": "/terms"
		},
		{
			"label": "Privacy Policy",
			"href": "/privacy"
		}
	],
	"social": [
		{
			"network": "facebook",
			"href": "https://facebook.com"
		},
		{
			"network": "x",
			"href": "https://x.com"
		},
		{
			"network": "instagram",
			"href": "https://instagram.com"
		}
	],
	"background": {
		"src": "/ship-cta-footer/ship-at-sea.webp"
	}
};
---

<html lang="en">
	<head>
		<meta charset="utf-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<title>Contact [Business name] — Get a Freight Quote</title>
		<meta name="description" content="[One-sentence invitation to get in touch, with the service area and response time.]" />
		<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>

		<!-- Scroll pill header -->
		<ScrollPillHeader {...scrollPillHeaderProps} />

		<main id="main">
			<!-- Tag title banner -->
			<TagTitleBanner {...tagTitleBannerProps} />

			<!-- Dark contact panel -->
			<DarkContactPanel {...darkContactPanelProps} />
		</main>

		<!-- Ship CTA footer -->
		<ShipCtaFooter {...shipCtaFooterProps} />
	</body>
</html>

===== END src/pages/tideway-contact.astro =====

===== FILE (required): src/components/ScrollPillHeader.astro =====
---
/**
 * ScrollPillHeader
 *
 * A 1350px pill bar fixed 20px from the top of the viewport. Over the hero it
 * is a faint white wash with white type; once the page scrolls past
 * `threshold` it turns solid white with navy type. Logo on the left, links in
 * the middle (an item with `children` opens a small dropdown card), an orange
 * "Get a Quote" pill on the right. Below 1000px it folds to the logo and a
 * round menu button that opens a white card under the bar.
 *
 * Reads the --tw-* tokens from any ancestor, with fallbacks.
 */

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

export interface ScrollPillHeaderItem {
	label: string;
	/** Required for a plain link; ignored on a dropdown trigger. */
	href?: string;
	/** Turns the item into a dropdown trigger. */
	children?: ScrollPillHeaderLink[];
	/**
	 * Mega-menu shaped data from other headers in this library. The links of
	 * every group are flattened into one dropdown list; headings are dropped.
	 */
	groups?: { heading?: string; links: ScrollPillHeaderLink[] }[];
}

export interface ScrollPillHeaderProps {
	items?: ScrollPillHeaderItem[];
	/** Pass Astro.url.pathname. Matching links get aria-current="page". */
	currentPath?: string;
	brandName?: string;
	brandHref?: string;
	/** Replaces the drawn mark and wordmark. Give a light and a dark version. */
	logo?: { src: string; srcOnDark?: string; alt: string; width?: number; height?: number };
	/** The orange pill. `null` drops it. */
	cta?: ScrollPillHeaderLink | null;
	/** Always solid white, for pages whose top is not a dark hero. */
	solid?: boolean;
	/** Scroll distance in px after which the bar turns solid. */
	threshold?: number;
	navLabel?: string;
	menuLabel?: string;
	fontStylesheet?: string;
	id?: string;
}

interface Props extends ScrollPillHeaderProps {}

/* 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: rawItems = [],
	currentPath,
	brandName,
	brandHref = "/",
	logo,
	cta = null,
	solid = false,
	threshold = 8,
	navLabel = "Primary",
	menuLabel = "Menu",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700;800;900&family=Noto+Sans:wght@400;500;600;700&family=Inter:wght@500;600;700&display=swap",
	id,
} = Astro.props;

const items = rawItems.map((item) => ({
	label: item.label,
	href: item.href ?? "#",
	children: item.children ?? item.groups?.flatMap((g) => g.links),
}));

const seed = id ? id : `sph-${Math.random().toString(36).slice(2, 8)}`;
const menuId = `${seed}-menu`;

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

const norm = (p: string) => (p.length > 1 ? p.replace(/\/+$/, "") : p);
const isCurrent = (href?: string) => Boolean(href && currentPath && norm(href) === norm(currentPath));
const hasCurrentChild = (item: { children?: ScrollPillHeaderLink[] }) => (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="sph"
	id={id}
	data-header-root
	data-sph-root
	data-sph-threshold={threshold}
	data-solid={solid ? "" : undefined}
>
	<div class="sph-bar">
		{hasBrand && (
		<a class="sph-brand" href={safeHref(brandHref)} aria-label={logo ? undefined : `${brand} home`}>
			{
				logo ? (
					<>
						<img class="sph-logo sph-logo--light" src={logo.src} alt={logo.alt} width={logo.width ?? 167} height={logo.height ?? 43} />
						{logo.srcOnDark && (
							<img class="sph-logo sph-logo--dark" src={logo.srcOnDark} alt="" width={logo.width ?? 167} height={logo.height ?? 43} />
						)}
					</>
				) : (
					<>
						<svg class="sph-mark" viewBox="0 0 40 40" aria-hidden="true">
							<path d="M6 21a14 14 0 0 1 28 0h-6a8 8 0 0 0-16 0Z" fill="currentColor" />
							<path d="M20 17 11 30h6v7h6v-7h6Z" fill="currentColor" />
						</svg>
						<span class="sph-word">{brand}</span>
					</>
				)
			}
		</a>
		)}

		{hasItems && (
		<nav class="sph-nav" aria-label={navLabel}>
			<ul class="sph-list">
				{
					items.map((item, i) =>
						item.children?.length ? (
							<li class="sph-item sph-item--drop" data-sph-drop>
								<button
									type="button"
									class="sph-link sph-trigger"
									aria-expanded="false"
									aria-controls={`${seed}-drop-${i}`}
									data-sph-trigger
									data-current={hasCurrentChild(item) ? "" : undefined}
								>
									{item.label}
									<svg class="sph-chev" viewBox="0 0 12 12" aria-hidden="true">
										<path d="M2 4.25 6 8l4-3.75" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" />
									</svg>
								</button>
								<ul class="sph-drop" id={`${seed}-drop-${i}`} data-sph-panel>
									{item.children.map((child) => (
										<li>
											<a href={safeHref(child.href)} aria-current={isCurrent(child.href) ? "page" : undefined}>
												{child.label}
											</a>
										</li>
									))}
								</ul>
							</li>
						) : (
							<li class="sph-item">
								<a class="sph-link" href={safeHref(item.href)} aria-current={isCurrent(item.href) ? "page" : undefined}>
									{item.label}
								</a>
							</li>
						),
					)
				}
			</ul>
		</nav>
		)}

		{
			cta && (
				<a class="sph-cta" href={safeHref(cta.href)}>
					<span>{cta.label}</span>
					<span class="sph-cta-ic" aria-hidden="true">
						<svg viewBox="0 0 24 24"><path d="M8 5l10 7-10 7 3-7Z" fill="currentColor" /></svg>
					</span>
				</a>
			)
		}

		{hasMenu && (
		<button type="button" class="sph-burger" aria-expanded="false" aria-controls={menuId} data-sph-burger>
			<span class="sph-sr">{menuLabel}</span>
			<svg viewBox="0 0 24 24" aria-hidden="true">
				<circle cx="12" cy="12" r="9.25" fill="none" stroke="currentColor" stroke-width="1.5" />
				<path d="M12 7.5v9M7.5 12h9" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
			</svg>
		</button>
		)}
	</div>

	{hasMenu && (
	<div class="sph-menu" id={menuId} data-sph-menu>
		{hasItems && (
		<ul>
			{
				items.flatMap((item) =>
					item.children?.length
						? item.children.map((child) => (
								<li>
									<a href={safeHref(child.href)} aria-current={isCurrent(child.href) ? "page" : undefined}>
										{child.label}
									</a>
								</li>
							))
						: [
								<li>
									<a href={safeHref(item.href)} aria-current={isCurrent(item.href) ? "page" : undefined}>
										{item.label}
									</a>
								</li>,
							],
				)
			}
		</ul>
		)}
		{
			cta && (
				<a class="sph-cta sph-cta--menu" href={safeHref(cta.href)}>
					<span>{cta.label}</span>
					<span class="sph-cta-ic" aria-hidden="true">
						<svg viewBox="0 0 24 24"><path d="M8 5l10 7-10 7 3-7Z" fill="currentColor" /></svg>
					</span>
				</a>
			)
		}
	</div>
	)}
</header>
)}

<style>
	.sph {
		--sph-navy: var(--tw-navy, #0b4654);
		--sph-deep: var(--tw-navy-deep, #052931);
		--sph-accent: var(--tw-accent, #ff6d45);
		--sph-body: var(--tw-body, "Noto Sans", ui-sans-serif, system-ui, sans-serif);
		--sph-display: var(--tw-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--sph-ease: cubic-bezier(0.44, 0, 0.56, 1);
		--sph-ink: #fff;

		position: fixed;
		top: 12px;
		left: 50%;
		z-index: 40;
		width: min(1350px, calc(100% - 24px));
		transform: translateX(-50%);
		font-family: var(--sph-body);
	}

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

	.sph[data-scrolled],
	.sph[data-solid],
	.sph[data-sph-open] {
		--sph-ink: var(--sph-navy);
	}

	.sph-bar {
		position: relative;
		display: flex;
		align-items: center;
		gap: 16px;
		height: 64px;
		padding: 0 12px 0 16px;
		border-radius: 999px;
		background: rgb(255 255 255 / 0.1);
		color: var(--sph-ink);
		/* No colour transition here: the links carry their own, and a second
		   one on the parent would chain behind it. */
		transition:
			background-color 0.4s var(--sph-ease),
			box-shadow 0.4s var(--sph-ease);
	}

	.sph[data-scrolled] .sph-bar,
	.sph[data-solid] .sph-bar,
	.sph[data-sph-open] .sph-bar {
		background: #fff;
		box-shadow: 0 0 0 1px rgb(11 70 84 / 0.08);
	}

	.sph-brand {
		display: inline-flex;
		align-items: center;
		gap: 8px;
		margin-right: auto;
		color: inherit;
		text-decoration: none;
		border-radius: 8px;
	}

	.sph-mark {
		width: 32px;
		height: 32px;
		color: var(--sph-accent);
	}

	.sph-word {
		font: 500 26px/1 var(--sph-display);
		letter-spacing: -0.5px;
	}

	.sph-logo {
		display: block;
		width: auto;
		height: 34px;
	}

	.sph-logo--dark {
		display: none;
	}

	.sph:not([data-scrolled]):not([data-solid]):not([data-sph-open]) .sph-logo--dark {
		display: block;
	}

	.sph:not([data-scrolled]):not([data-solid]):not([data-sph-open]) .sph-logo--dark ~ .sph-logo--light,
	.sph:not([data-scrolled]):not([data-solid]):not([data-sph-open]) .sph-logo--light:has(+ .sph-logo--dark) {
		display: none;
	}

	.sph-nav,
	.sph > .sph-bar > .sph-cta {
		display: none;
	}

	.sph-list {
		display: flex;
		align-items: center;
		gap: 35px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.sph-item {
		position: relative;
	}

	.sph-link {
		display: inline-flex;
		align-items: center;
		gap: 4px;
		padding: 8px 0;
		border: 0;
		background: none;
		color: inherit;
		font: 500 16px/25.6px var(--sph-body);
		letter-spacing: -0.32px;
		text-decoration: none;
		cursor: pointer;
		transition: color 0.3s var(--sph-ease);
	}

	.sph-link:hover,
	.sph-item--drop[data-open] .sph-trigger,
	.sph-link[aria-current="page"],
	.sph-trigger[data-current] {
		color: var(--sph-accent);
	}

	.sph-chev {
		width: 14px;
		height: 14px;
		transition: transform 0.3s var(--sph-ease);
	}

	.sph-item--drop[data-open] .sph-chev {
		transform: rotate(180deg);
	}

	.sph-drop {
		position: absolute;
		top: calc(100% + 16px);
		left: 0;
		min-width: 290px;
		margin: 0;
		padding: 6px 10px;
		list-style: none;
		border-radius: 10px;
		background: #fff;
		box-shadow: 0 12px 30px rgb(5 41 49 / 0.14);
		opacity: 0;
		visibility: hidden;
		transform: translateY(6px);
		transition:
			opacity 0.25s var(--sph-ease),
			transform 0.25s var(--sph-ease),
			visibility 0s linear 0.25s;
	}

	/* A bridge over the gap, so moving the pointer from the trigger to the
	   card does not cross empty space and close it. */
	.sph-drop::before {
		content: "";
		position: absolute;
		inset: -18px 0 100% 0;
	}

	.sph-item--drop[data-open] .sph-drop {
		opacity: 1;
		visibility: visible;
		transform: none;
		transition-delay: 0s;
	}

	.sph-drop li + li {
		border-top: 1px solid rgb(11 70 84 / 0.1);
	}

	.sph-drop a {
		display: block;
		padding: 10px 0;
		color: var(--sph-navy);
		font: 500 16px/1.4 var(--sph-body);
		text-transform: uppercase;
		text-decoration: none;
		transition: color 0.3s var(--sph-ease);
	}

	.sph-drop a:hover,
	.sph-drop a[aria-current="page"] {
		color: var(--sph-accent);
	}

	.sph-cta {
		display: inline-flex;
		align-items: center;
		gap: 12px;
		height: 50px;
		padding: 0 8px 0 25px;
		border-radius: 999px;
		background: var(--sph-accent);
		color: #fff;
		font: 400 16px/25.6px var(--sph-body);
		letter-spacing: -0.32px;
		text-decoration: none;
		white-space: nowrap;
		transition: background-color 0.3s var(--sph-ease);
	}

	.sph-cta:hover {
		background: var(--sph-deep);
	}

	.sph-cta-ic {
		display: grid;
		place-items: center;
		width: 34px;
		height: 34px;
		border-radius: 50%;
		background: #fff;
		color: var(--sph-accent);
		transition: transform 0.3s var(--sph-ease);
	}

	.sph-cta-ic svg {
		width: 18px;
		height: 18px;
	}

	.sph-cta:hover .sph-cta-ic {
		transform: rotate(-45deg);
	}

	.sph-burger {
		display: grid;
		place-items: center;
		width: 44px;
		height: 44px;
		padding: 0;
		border: 0;
		border-radius: 50%;
		background: none;
		color: var(--sph-accent);
		cursor: pointer;
	}

	.sph-burger svg {
		width: 26px;
		height: 26px;
		transition: transform 0.3s var(--sph-ease);
	}

	.sph[data-sph-open] .sph-burger svg {
		transform: rotate(45deg);
	}

	.sph-menu {
		position: absolute;
		top: calc(100% + 10px);
		left: 0;
		right: 0;
		display: none;
		padding: 12px 20px 20px;
		border-radius: 20px;
		background: #fff;
		box-shadow: 0 16px 40px rgb(5 41 49 / 0.18);
	}

	.sph[data-sph-open] .sph-menu {
		display: block;
	}

	.sph-menu ul {
		margin: 0 0 16px;
		padding: 0;
		list-style: none;
	}

	.sph-menu li + li {
		border-top: 1px solid rgb(11 70 84 / 0.1);
	}

	.sph-menu a:not(.sph-cta) {
		display: block;
		padding: 12px 0;
		color: var(--sph-navy);
		font: 500 17px/1.4 var(--sph-body);
		text-decoration: none;
	}

	.sph-menu a:not(.sph-cta):hover,
	.sph-menu a[aria-current="page"] {
		color: var(--sph-accent);
	}

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

	.sph a:focus-visible,
	.sph button:focus-visible {
		outline: 2px solid var(--sph-accent);
		outline-offset: 3px;
	}

	/* No JavaScript: no menu button, and the phone card is shown under the bar
	   as a plain list so every link stays reachable. */
	.sph:not([data-sph-ready]) .sph-burger {
		display: none;
	}

	.sph:not([data-sph-ready]) .sph-menu {
		display: block;
	}

	.sph:not([data-sph-ready]) .sph-bar {
		background: #fff;
		--sph-ink: var(--sph-navy);
		color: var(--sph-navy);
	}

	.sph:not([data-sph-ready]) .sph-item--drop:focus-within .sph-drop,
	.sph:not([data-sph-ready]) .sph-item--drop:hover .sph-drop {
		opacity: 1;
		visibility: visible;
		transform: none;
		transition-delay: 0s;
	}

	@media (min-width: 810px) {
		.sph {
			top: 20px;
			width: min(1350px, calc(100% - 60px));
		}

		.sph-bar {
			height: 80px;
			padding: 0 16px 0 22px;
		}
	}

	@media (min-width: 1000px) {
		.sph {
			width: min(1350px, calc(100% - 90px));
		}

		.sph-bar {
			height: 90px;
			padding: 0 20px 0 22px;
		}

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

		.sph > .sph-bar > .sph-cta {
			display: inline-flex;
		}

		.sph-burger,
		.sph:not([data-sph-ready]) .sph-menu,
		.sph .sph-menu {
			display: none !important;
		}

		.sph-mark {
			width: 38px;
			height: 38px;
		}

		.sph-word {
			font-size: 32px;
		}

		.sph-logo {
			height: 43px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.sph-bar,
		.sph-link,
		.sph-chev,
		.sph-drop,
		.sph-cta,
		.sph-cta-ic,
		.sph-burger svg {
			transition: none;
		}

		.sph-cta:hover .sph-cta-ic {
			transform: none;
		}
	}

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

<script>
	/**
	 * ScrollPillHeader controller: the scroll swap, the desktop dropdowns and
	 * the phone menu. One closure per instance, every lookup scoped to its
	 * root, every listener on one AbortController aborted before a client-side
	 * navigation swaps the document. No body scroll lock.
	 */
	function initScrollPillHeader(root: HTMLElement) {
		if (root.dataset.sphReady === "") return;
		const burger = root.querySelector<HTMLButtonElement>("[data-sph-burger]");
		const menu = root.querySelector<HTMLElement>("[data-sph-menu]");

		root.dataset.sphReady = "";
		const controller = new AbortController();
		const { signal } = controller;
		const threshold = Number(root.dataset.sphThreshold) || 8;
		const desktop = window.matchMedia("(min-width: 1000px)");
		const hoverable = window.matchMedia("(hover: hover)");

		/* ---- scroll swap ---- */
		let ticking = false;
		const applyScroll = () => {
			ticking = false;
			root.toggleAttribute("data-scrolled", window.scrollY > threshold);
		};
		applyScroll();
		window.addEventListener(
			"scroll",
			() => {
				if (ticking) return;
				ticking = true;
				requestAnimationFrame(applyScroll);
			},
			{ passive: true, signal },
		);

		/* ---- dropdowns ---- */
		const drops = [...root.querySelectorAll<HTMLElement>("[data-sph-drop]")];
		const setDrop = (drop: HTMLElement, open: boolean, returnFocus = false) => {
			const trigger = drop.querySelector<HTMLButtonElement>("[data-sph-trigger]");
			if (!trigger || drop.hasAttribute("data-open") === open) return;
			drop.toggleAttribute("data-open", open);
			trigger.setAttribute("aria-expanded", String(open));
			if (!open && returnFocus) trigger.focus({ preventScroll: true });
		};
		const closeDrops = (except?: HTMLElement) => drops.forEach((d) => d !== except && setDrop(d, false));

		for (const drop of drops) {
			const trigger = drop.querySelector<HTMLButtonElement>("[data-sph-trigger]");
			if (!trigger) continue;
			trigger.addEventListener(
				"click",
				() => {
					const next = !drop.hasAttribute("data-open");
					closeDrops(drop);
					setDrop(drop, next);
				},
				{ signal },
			);
			drop.addEventListener(
				"pointerenter",
				(e) => {
					if (e.pointerType === "mouse" && hoverable.matches) {
						closeDrops(drop);
						setDrop(drop, true);
					}
				},
				{ signal },
			);
			drop.addEventListener(
				"pointerleave",
				(e) => {
					if (e.pointerType === "mouse" && hoverable.matches) setDrop(drop, false);
				},
				{ signal },
			);
			drop.addEventListener(
				"focusout",
				(e) => {
					const next = e.relatedTarget as Node | null;
					if (next && !drop.contains(next)) setDrop(drop, false);
				},
				{ signal },
			);
		}

		/* ---- phone menu ---- */
		let open = false;
		/* No links and no CTA means no menu button and no card: the scroll
		   swap and dropdowns above still run. */
		const setMenu = (next: boolean, returnFocus = false) => {
			if (!burger || !menu || open === next) return;
			open = next;
			root.toggleAttribute("data-sph-open", next);
			burger.setAttribute("aria-expanded", String(next));
			if (!next && returnFocus) burger.focus({ preventScroll: true });
		};
		burger?.addEventListener("click", () => setMenu(!open), { signal });
		menu?.addEventListener(
			"click",
			(e) => {
				if ((e.target as HTMLElement).closest("a[href]")) setMenu(false);
			},
			{ signal },
		);
		root.addEventListener(
			"focusout",
			(e) => {
				const next = e.relatedTarget as Node | null;
				if (open && next && !root.contains(next)) setMenu(false);
			},
			{ signal },
		);

		/* Escape and outside click on the document: after a client-side
		   navigation focus can sit outside the header. */
		document.addEventListener(
			"keydown",
			(e) => {
				if (e.key !== "Escape") return;
				const openDrop = drops.find((d) => d.hasAttribute("data-open"));
				if (openDrop) {
					e.preventDefault();
					setDrop(openDrop, false, openDrop.contains(document.activeElement));
				} else if (open) {
					e.preventDefault();
					setMenu(false, true);
				}
			},
			{ signal },
		);
		document.addEventListener(
			"pointerdown",
			(e) => {
				if (root.contains(e.target as Node)) return;
				closeDrops();
				setMenu(false);
			},
			{ signal },
		);
		desktop.addEventListener(
			"change",
			() => {
				setMenu(false);
				closeDrops();
			},
			{ signal },
		);

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

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

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

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

===== END src/components/ScrollPillHeader.astro =====

===== FILE (required): src/components/TagTitleBanner.astro =====
---
/**
 * TagTitleBanner
 *
 * An inner-page banner built as a split: the breadcrumb and a heavy
 * uppercase title with a stamped tag on a navy field, and — when a photo is
 * given — a full-height photo panel meeting it at an accent seam. Below
 * 700px of its own width the photo becomes a strip that closes the banner.
 * Leaves room at the top for a fixed pill header.
 *
 * Sizes follow the component's own width (container queries), so it holds
 * the same composition in a sidebar layout or a narrow column. The title
 * rises in on load; the breadcrumb follows. Both end at rest and are off
 * under reduced motion.
 *
 * Reads the --tw-* tokens from any ancestor, with fallbacks.
 */

export interface TagTitleCrumb {
	label: string;
	/** Omit on the last crumb, the current page. */
	href?: string;
}

export interface TagTitleBannerProps {
	title?: string;
	/** The orange box after the title. `""` drops it. */
	tag?: string;
	breadcrumbs?: TagTitleCrumb[];
	/** Background photo. `src: ""` renders the navy band alone. */
	background?: { src?: string };
	animate?: boolean;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends TagTitleBannerProps {}

const {
	title = "",
	tag = "",
	breadcrumbs: rawCrumbs = [],
	background: bgProp,
	animate = true,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700;800;900&family=Noto+Sans:wght@400;500;600;700&family=Inter:wght@500;600;700&display=swap",
	class: className,
	id,
} = Astro.props;

/* No photo by default: the navy band renders alone. No title, no banner. */
const background = { src: "", ...(bgProp ?? {}) };
const breadcrumbs = rawCrumbs.filter((c) => c?.label);
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
---

{title && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section class:list={["ttb", className]} id={id} data-animate={animate ? "" : undefined} data-media={background.src ? "" : undefined}>
	<div class="ttb-grid">
	<div class="ttb-inner">
		{
			breadcrumbs.length > 0 && (
				<nav class="ttb-crumbs" aria-label="Breadcrumb">
					<ol>
						{breadcrumbs.map((c, i) => (
							<li>
								{c.href && i < breadcrumbs.length - 1 ? (
									<a href={safeHref(c.href)}>{c.label}</a>
								) : (
									<span aria-current={i === breadcrumbs.length - 1 ? "page" : undefined}>{c.label}</span>
								)}
							</li>
						))}
					</ol>
				</nav>
			)
		}
		<h1 class="ttb-title">
			<span class="ttb-word">{title}</span>
			{tag && <span class="ttb-tag">{tag}</span>}
		</h1>
	</div>
	{
		background.src && (
			<div class="ttb-media" aria-hidden="true">
				<img src={background.src} alt="" width="1920" height="600" fetchpriority="high" decoding="async" />
			</div>
		)
	}
	</div>
</section>
</>
)}

<style>
	.ttb {
		--ttb-navy: var(--tw-navy, #0b4654);
		--ttb-accent: var(--tw-accent, #ff6d45);
		--ttb-display: var(--tw-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--ttb-body: var(--tw-body, "Noto Sans", ui-sans-serif, system-ui, sans-serif);
		--ttb-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
		--ttb-seam: 6px;

		container-type: inline-size;
		position: relative;
		isolation: isolate;
		overflow: hidden;
		background: var(--ttb-navy);
		color: #fff;
		font-family: var(--ttb-body);
		/* The closing edge: the banner ends on a line, never on open colour. */
		border-bottom: var(--ttb-seam) solid var(--ttb-accent);
	}

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

	.ttb-grid {
		display: grid;
	}

	/* Step 39.2: --tw-grid-max / --tw-grid-gutter put the title on a site grid; the fallbacks are the original 1440 / 64. */
	.ttb-inner {
		--ttb-max: var(--tw-grid-max, 1440px);
		--ttb-gutter: var(--tw-grid-gutter, 64px);
	}

	.ttb-inner {
		display: flex;
		flex-direction: column;
		justify-content: flex-end;
		gap: 18px;
		min-width: 0;
		padding: 124px 20px 40px;
	}

	/* ---- breadcrumb: wayfinding first, above the title ---- */

	.ttb-crumbs ol {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: 0 2px;
		margin: 0;
		padding: 0;
		list-style: none;
		font: 500 15px/1.4 var(--ttb-body);
	}

	.ttb-crumbs li {
		display: flex;
		align-items: center;
		min-width: 0;
	}

	.ttb-crumbs li + li::before {
		content: "/";
		margin: 0 8px 0 6px;
		color: rgb(255 255 255 / 0.4);
	}

	.ttb-crumbs a,
	.ttb-crumbs span {
		display: inline-block;
		padding: 5px 0;
		overflow-wrap: anywhere;
	}

	.ttb-crumbs a {
		color: rgb(255 255 255 / 0.74);
		text-decoration: underline;
		text-decoration-color: rgb(255 255 255 / 0);
		text-underline-offset: 4px;
		transition: color 0.2s var(--ttb-ease), text-decoration-color 0.2s var(--ttb-ease);
	}

	.ttb-crumbs a:hover {
		color: #fff;
		text-decoration-color: rgb(255 255 255 / 0.6);
	}

	.ttb-crumbs a:focus-visible {
		outline: 2px solid #fff;
		outline-offset: 2px;
		border-radius: 2px;
	}

	.ttb-crumbs [aria-current] {
		color: #fff;
		font-weight: 600;
	}

	/* ---- title: one lockup, the tag stamped onto the end of it ---- */

	.ttb-title {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		gap: 0.14em 0.2em;
		margin: 0;
		max-width: 16ch;
		font: 700 clamp(40px, 11.5cqi, 60px) / 0.98 var(--ttb-display);
		letter-spacing: -0.035em;
		text-transform: uppercase;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.ttb-tag {
		display: inline-block;
		padding: 0.1em 0.26em 0.06em;
		border: 2px solid transparent;
		border-radius: 4px;
		background: var(--ttb-accent);
		color: var(--ttb-navy);
		font-size: 0.62em;
		line-height: 1;
		letter-spacing: -0.02em;
		white-space: nowrap;
	}

	/* ---- photo ---- */

	.ttb-media {
		position: relative;
		min-height: 168px;
		border-top: var(--ttb-seam) solid var(--ttb-accent);
	}

	.ttb-media img {
		position: absolute;
		inset: 0;
		width: 100%;
		height: 100%;
		object-fit: cover;
		object-position: 70% center;
	}

	/* A light navy grade so a busy photo sits in the palette instead of fighting it. */
	.ttb-media::after {
		content: "";
		position: absolute;
		inset: 0;
		background: linear-gradient(90deg, rgb(11 70 84 / 0.45), rgb(11 70 84 / 0.12) 55%, rgb(11 70 84 / 0.05));
	}

	/* ---- tablet and up: side by side, the seam runs top to bottom ---- */

	@container (min-width: 700px) {
		.ttb[data-media] .ttb-grid {
			grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr);
		}

		.ttb-inner {
			gap: 22px;
			padding: 150px 40px 56px;
		}

		.ttb-title {
			max-width: 14ch;
			font-size: clamp(52px, 8.4cqi, 104px);
		}

		.ttb:not([data-media]) .ttb-title {
			max-width: 20ch;
		}

		.ttb-media {
			min-height: 0;
			border-top: 0;
			border-left: var(--ttb-seam) solid var(--ttb-accent);
		}
	}

	@container (min-width: 1100px) {
		.ttb-inner {
			padding: 196px max(var(--ttb-gutter), calc((100cqi - var(--ttb-max)) / 2 + var(--ttb-gutter))) 72px;
		}

		.ttb[data-media] .ttb-grid {
			grid-template-columns: minmax(0, 1fr) minmax(0, 0.78fr);
		}
	}

	/* ---- motion: rises in, ends at rest ---- */

	@keyframes ttb-rise {
		from {
			opacity: 0;
			transform: translateY(var(--ttb-from, 18px));
		}
	}

	.ttb[data-animate] .ttb-title {
		animation: ttb-rise 0.7s var(--ttb-ease) both;
	}

	.ttb[data-animate] .ttb-crumbs {
		--ttb-from: 10px;
		animation: ttb-rise 0.6s var(--ttb-ease) 0.12s both;
	}

	@media (prefers-reduced-motion: reduce) {
		.ttb[data-animate] .ttb-title,
		.ttb[data-animate] .ttb-crumbs {
			animation: none;
		}

		.ttb-crumbs a {
			transition: none;
		}
	}

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

		.ttb-media,
		.ttb-tag {
			border-color: CanvasText;
		}
	}
</style>

===== END src/components/TagTitleBanner.astro =====

===== FILE (required): public/tag-title-banner/port.webp =====
[binary asset — download https://astro.baysixmedia.com/tag-title-banner/port.webp and save it to public/tag-title-banner/port.webp]

===== FILE (required): src/components/DarkContactPanel.astro =====
---
/**
 * DarkContactPanel
 *
 * A navy contact section: a tall photo beside a translucent panel holding an
 * outlined eyebrow, a big uppercase heading with orange words and a real
 * contact form (name and phone side by side, email, message, a full-width
 * send button). Underneath, an optional row of info cards: round orange
 * icon, a title and a few lines each (address, phone numbers, opening hours).
 *
 * Nothing about the business is assumed: no heading, photo or info card is
 * shown unless it is passed. With none of them the form still renders under
 * the generic eyebrow.
 *
 * The form posts to `action`. The send button dims until the required fields
 * are valid but is never disabled, so the browser's own validation messages
 * still explain what is missing.
 *
 * Heading syntax: wrap the orange words in asterisks.
 *
 * Reads the --tw-* tokens from any ancestor, with fallbacks.
 */

export type ContactInfoIcon = "pin" | "headset" | "clock" | "mail" | "phone";

export interface ContactInfoCard {
	title: string;
	lines: string[];
	icon?: ContactInfoIcon;
	/** Makes the lines one link, e.g. tel: or a map URL. */
	href?: string;
}

export interface DarkContactPanelProps {
	eyebrow?: string;
	heading?: string;
	headingLevel?: "h2" | "h3";
	action?: string;
	submitLabel?: string;
	/** Unset or `null`: none, and the panel spans the row. `src: ""` renders a labelled placeholder. */
	image?: { src?: string; alt?: string } | null;
	info?: ContactInfoCard[];
	background?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends DarkContactPanelProps {}

const {
	eyebrow = "Contact Request",
	heading = "",
	headingLevel = "h2",
	action = "/contact",
	submitLabel = "Send Message",
	image: imageProp,
	info = [],
	background = "#0b4654",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700;800;900&family=Noto+Sans:wght@400;500;600;700&family=Inter:wght@500;600;700&display=swap",
	class: className,
	id,
} = Astro.props;

/* No photo unless one is passed; a passed photo is taken as given, alt "" (decorative) if unset. */
const image = imageProp ? { src: imageProp.src ?? "", alt: imageProp.alt ?? "" } : null;

const seed = `${id ?? "dcp"}-${Math.random().toString(36).slice(2, 7)}`;
const words = heading
	.split("*")
	.flatMap((seg, i) => seg.split(/\s+/).filter(Boolean).map((w) => ({ w, accent: i % 2 === 1 })));
const Heading = headingLevel;
const Title = headingLevel === "h2" ? "h3" : "h4";
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");

const ICONS: Record<ContactInfoIcon, string> = {
	pin: "M12 21s-7-6.2-7-12a7 7 0 0 1 14 0c0 5.8-7 12-7 12Z M12 11.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z",
	headset: "M4 14v-2a8 8 0 0 1 16 0v2 M4 14h3v5H5a1 1 0 0 1-1-1Z M20 14h-3v5h2a1 1 0 0 0 1-1Z M17 19c0 1.5-2 2-5 2",
	clock: "M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z M12 7v5l3 2",
	mail: "M3 6h18v12H3Z M3 7l9 6 9-6",
	phone: "M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2Z",
};
---

{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section class:list={["dcp", className]} id={id} style={`--dcp-band:${background}`} data-dcp-root>
	<div class="dcp-inner">
		<div class:list={["dcp-row", { "dcp-row--solo": !image }]}>
			{
				image && (
					<div class="dcp-media" data-dcp-reveal="up">
						{image.src ? (
							<img src={image.src} alt={image.alt} width="900" height="1160" loading="lazy" decoding="async" />
						) : (
							<div class="dcp-ph" role="img" aria-label="Image placeholder: portrait">
								<span>Image · portrait · 900 × 1160</span>
							</div>
						)}
					</div>
				)
			}

			<div class="dcp-panel" data-dcp-reveal="up" style="--d:0.1s">
				{eyebrow && <p class="dcp-eyebrow">{eyebrow}</p>}
				{words.length > 0 && (
				<Heading class="dcp-heading" data-dcp-reveal="words">
					{
						words.map((t, i) => (
							<>
								{i > 0 && " "}
								<span class:list={["dcp-w", { "dcp-accent": t.accent }]} style={`--i:${i}`}>
									{t.w}
								</span>
							</>
						))
					}
				</Heading>
				)}

				<form class="dcp-form" action={safeHref(action)} method="post">
					<div class="dcp-field">
						<label for={`${seed}-name`}>Name</label>
						<input id={`${seed}-name`} name="name" type="text" autocomplete="name" required placeholder="Jane Smith" />
					</div>
					<div class="dcp-field">
						<label for={`${seed}-phone`}>Phone</label>
						<input id={`${seed}-phone`} name="phone" type="tel" autocomplete="tel" placeholder="(555) 000-0000" />
					</div>
					<div class="dcp-field dcp-field--wide">
						<label for={`${seed}-email`}>Email</label>
						<input id={`${seed}-email`} name="email" type="email" autocomplete="email" required placeholder="jane@company.com" />
					</div>
					<div class="dcp-field dcp-field--wide">
						<label for={`${seed}-message`}>Message</label>
						<textarea id={`${seed}-message`} name="message" rows="4" required placeholder="Tell us about your shipment…"></textarea>
					</div>
					<button class="dcp-submit dcp-field--wide" type="submit">{submitLabel}</button>
				</form>
			</div>
		</div>

		{
			info.length > 0 && (
				<ul class="dcp-info">
					{info.map((c, i) => (
						<li class="dcp-card" data-dcp-reveal="up" style={`--d:${i * 0.1}s`}>
							<span class="dcp-ic" aria-hidden="true">
								<svg viewBox="0 0 24 24">
									<path d={ICONS[c.icon ?? "pin"]} fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" />
								</svg>
							</span>
							<div>
								<Title class="dcp-card-title">{c.title}</Title>
								{c.href ? (
									<a class="dcp-lines" href={safeHref(c.href)}>
										{c.lines.map((l) => (
											<span>{l}</span>
										))}
									</a>
								) : (
									<p class="dcp-lines">
										{c.lines.map((l) => (
											<span>{l}</span>
										))}
									</p>
								)}
							</div>
						</li>
					))}
				</ul>
			)
		}
	</div>
</section>

<style>
	.dcp {
		--dcp-navy-2: var(--tw-navy-2, #15505e);
		--dcp-accent: var(--tw-accent, #ff6d45);
		--dcp-display: var(--tw-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--dcp-body: var(--tw-body, "Noto Sans", ui-sans-serif, system-ui, sans-serif);
		--dcp-ease: cubic-bezier(0.44, 0, 0.56, 1);

		background: var(--dcp-band);
		color: #fff;
		font-family: var(--dcp-body);
	}

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

	.dcp-inner {
		max-width: 1440px;
		margin: 0 auto;
		padding: 64px 16px;
	}

	.dcp-row {
		display: grid;
		gap: 24px;
	}

	.dcp-media {
		aspect-ratio: 4 / 3;
		overflow: hidden;
		border-radius: 20px;
	}

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

	.dcp-ph {
		display: grid;
		place-items: center;
		background: repeating-linear-gradient(135deg, #154f5c 0 12px, #1a5866 12px 24px);
		font: 500 13px/1.4 var(--dcp-body);
	}

	.dcp-panel {
		padding: 28px 20px;
		border-radius: 15px;
		background: rgb(255 255 255 / 0.1);
	}

	.dcp-eyebrow {
		display: inline-block;
		margin: 0 0 18px;
		padding: 4px 14px;
		border: 1px solid var(--dcp-accent);
		border-radius: 999px;
		color: var(--dcp-accent);
		font: 500 15px/24px var(--dcp-body);
		letter-spacing: -0.3px;
		text-transform: uppercase;
	}

	.dcp-heading {
		max-width: 560px;
		margin: 0;
		font: 700 28px/1.2 var(--dcp-display);
		letter-spacing: -0.56px;
		text-transform: uppercase;
	}

	.dcp-w {
		display: inline-block;
	}

	.dcp-accent {
		color: var(--dcp-accent);
	}

	.dcp-form {
		display: grid;
		gap: 18px 10px;
		margin-top: 30px;
	}

	.dcp-field {
		display: grid;
		gap: 8px;
	}

	.dcp-field label {
		font: 600 14px/22.4px var(--dcp-body);
		letter-spacing: -0.28px;
		text-transform: uppercase;
	}

	.dcp-field input,
	.dcp-field textarea {
		width: 100%;
		min-height: 50px;
		padding: 12px 10px;
		border: 1px solid rgb(255 255 255 / 0.18);
		border-radius: 10px;
		background: rgb(255 255 255 / 0.1);
		color: #fff;
		font: 400 15px/1.4 var(--dcp-body);
		transition: border-color 0.3s var(--dcp-ease);
	}

	.dcp-field textarea {
		min-height: 100px;
		resize: vertical;
	}

	.dcp-field input::placeholder,
	.dcp-field textarea::placeholder {
		color: rgb(255 255 255 / 0.6);
	}

	.dcp-field input:focus-visible,
	.dcp-field textarea:focus-visible {
		outline: 2px solid var(--dcp-accent);
		outline-offset: 1px;
		border-color: transparent;
	}

	.dcp-submit {
		height: 50px;
		margin-top: 4px;
		border: 0;
		border-radius: 25px;
		background: var(--dcp-accent);
		color: #fff;
		font: 400 16px/25.6px var(--dcp-body);
		letter-spacing: -0.32px;
		cursor: pointer;
		transition:
			background-color 0.3s var(--dcp-ease),
			opacity 0.3s var(--dcp-ease);
	}

	/* Dimmed, not disabled, until the required fields are valid. */
	.dcp-form:has(:invalid) .dcp-submit {
		background: #3f646f;
		opacity: 0.6;
	}

	.dcp-submit:hover {
		background: #052931;
		opacity: 1;
	}

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

	.dcp-info {
		display: grid;
		gap: 16px;
		margin: 40px 0 0;
		padding: 0;
		list-style: none;
	}

	.dcp-card {
		display: flex;
		gap: 15px;
		padding: 22px;
		border-radius: 10px;
		background: var(--dcp-navy-2);
	}

	.dcp-ic {
		display: grid;
		flex: none;
		place-items: center;
		width: 45px;
		height: 45px;
		border-radius: 50%;
		background: var(--dcp-accent);
		color: #fff;
	}

	.dcp-ic svg {
		width: 22px;
		height: 22px;
	}

	.dcp-card-title {
		margin: 0;
		font: 600 20px/32px var(--dcp-body);
		letter-spacing: -0.4px;
	}

	.dcp-lines {
		display: grid;
		margin: 4px 0 0;
		color: #fff;
		font: 400 16px/25.6px var(--dcp-body);
		letter-spacing: -0.32px;
		text-decoration: none;
		transition: color 0.3s var(--dcp-ease);
	}

	a.dcp-lines:hover {
		color: var(--dcp-accent);
	}

	a.dcp-lines:focus-visible {
		outline: 2px solid var(--dcp-accent);
		outline-offset: 3px;
		border-radius: 4px;
	}

	[data-dcp-reveal="up"] {
		transition:
			opacity 0.8s var(--dcp-ease) var(--d, 0s),
			transform 0.8s var(--dcp-ease) var(--d, 0s);
	}

	.dcp[data-dcp-ready] [data-dcp-reveal="up"]:not([data-in]) {
		opacity: 0;
		transform: translateY(80px);
	}

	.dcp-w {
		transition:
			opacity 0.6s var(--dcp-ease) calc(var(--i) * 0.06s),
			transform 0.6s var(--dcp-ease) calc(var(--i) * 0.06s);
	}

	.dcp[data-dcp-ready] [data-dcp-reveal="words"]:not([data-in]) .dcp-w {
		opacity: 0;
		transform: translateY(10px);
	}

	@media (min-width: 600px) {
		.dcp-form {
			grid-template-columns: repeat(2, minmax(0, 1fr));
		}

		.dcp-field--wide {
			grid-column: 1 / -1;
		}
	}

	@media (min-width: 810px) {
		.dcp-inner {
			padding: 90px 40px;
		}

		.dcp-panel {
			padding: 40px;
		}

		.dcp-heading {
			font-size: 40px;
			letter-spacing: -0.8px;
		}

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

		.dcp-card {
			padding: 25px;
		}
	}

	@media (min-width: 1200px) {
		.dcp-inner {
			padding: 100px 65px 110px;
		}

		.dcp-row {
			grid-template-columns: 590px minmax(0, 1fr);
			gap: 50px;
		}

		.dcp-row--solo {
			grid-template-columns: minmax(0, 1fr);
		}

		.dcp-media {
			aspect-ratio: auto;
			min-height: 761px;
		}

		.dcp-panel {
			padding: 30px 30px 30px 30px;
		}

		.dcp-panel .dcp-heading,
		.dcp-panel .dcp-eyebrow,
		.dcp-panel .dcp-form {
			margin-left: 15px;
			margin-right: 15px;
		}

		.dcp-heading {
			font-size: 50px;
			line-height: 60px;
			letter-spacing: -1px;
		}

		.dcp-eyebrow {
			margin-top: 12px;
		}

		.dcp-info {
			gap: 20px;
			margin-top: 67px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		[data-dcp-reveal="up"],
		.dcp-w,
		.dcp-submit,
		.dcp-lines,
		.dcp-field input,
		.dcp-field textarea {
			transition: none;
		}
	}
</style>

<script>
	/* Appear-on-scroll; one observer per instance, torn down on astro:before-swap. */
	function initDarkContactPanel(root: HTMLElement) {
		if (root.dataset.dcpReady === "") return;
		if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || !("IntersectionObserver" in window)) return;
		const controller = new AbortController();
		root.dataset.dcpReady = "";
		const io = new IntersectionObserver(
			(entries) => {
				for (const entry of entries) {
					if (!entry.isIntersecting) continue;
					(entry.target as HTMLElement).dataset.in = "";
					io.unobserve(entry.target);
				}
			},
			{ threshold: 0.1, rootMargin: "0px 0px -5% 0px" },
		);
		root.querySelectorAll<HTMLElement>("[data-dcp-reveal]").forEach((t) => io.observe(t));
		controller.signal.addEventListener("abort", () => io.disconnect());
		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				delete root.dataset.dcpReady;
			},
			{ once: true, signal: controller.signal },
		);
	}

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

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

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

===== END src/components/DarkContactPanel.astro =====

===== FILE (required): public/dark-contact-panel/aerial-ship.webp =====
[binary asset — download https://astro.baysixmedia.com/dark-contact-panel/aerial-ship.webp and save it to public/dark-contact-panel/aerial-ship.webp]

===== FILE (required): src/components/ShipCtaFooter.astro =====
---
/**
 * ShipCtaFooter
 *
 * A tall dark footer over a photo washed navy-to-black. On top, a big
 * uppercase call to action with orange words and an orange pill; under a
 * hairline, the brand, a tagline and an email sign-up on the left, and
 * Services, Company and Contact columns split by hairlines on the right; at
 * the foot, copyright, two legal links and round social buttons.
 *
 * The sign-up is a real <form> posting to `newsletter.action`; the button
 * dims until the email field is valid, but it is never disabled.
 *
 * Heading syntax: wrap the orange words in asterisks.
 *
 * Nothing about the business is assumed: no name, heading, pill, link,
 * contact line, sign-up form, copyright or photo is shown unless it is
 * passed, a block with nothing in it is not drawn, and with no content at
 * all nothing renders.
 *
 * Reads the --tw-* tokens from any ancestor, with fallbacks.
 */

export type ShipFooterSocial = "facebook" | "x" | "instagram" | "linkedin" | "youtube";

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

export interface ShipCtaFooterProps {
	heading?: string;
	/** Heading level for the call to action. */
	headingLevel?: "h2" | "p";
	cta?: ShipFooterLink | null;
	brandName?: string;
	brandHref?: string;
	/** Replaces the drawn mark and wordmark. Use a light version. */
	logo?: { src: string; alt: string; width?: number; height?: number };
	tagline?: string;
	/** The sign-up form. Off by default; pass your handler as `action`. */
	newsletter?: { action: string; label?: string; placeholder?: string; button?: string } | null;
	columns?: { heading: string; links: ShipFooterLink[] }[];
	contactHeading?: string;
	contact?: ShipFooterLink[];
	copyright?: string;
	legalLinks?: ShipFooterLink[];
	social?: { network: ShipFooterSocial; href: string; label?: string }[];
	/** Background photo. None by default: the navy gradient alone. */
	background?: { src?: string };
	navLabel?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends ShipCtaFooterProps {}

const {
	heading = "",
	headingLevel = "h2",
	cta = null,
	brandName = "",
	brandHref = "/",
	logo,
	tagline = "",
	newsletter = null,
	columns = [],
	contactHeading = "Contact",
	contact = [],
	copyright,
	legalLinks = [],
	social = [],
	background: bgProp,
	navLabel = "Footer",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Outfit:wght@500;600;700;800;900&family=Noto+Sans:wght@400;500;600;700&family=Inter:wght@500;600;700&display=swap",
	class: className,
	id,
} = Astro.props;

const background = { src: "", ...(bgProp ?? {}) };
const year = new Date().getFullYear();
/* No name, no copyright line: never "© 2026 . All rights reserved." */
const copy = copyright ?? (brandName ? `© ${year} ${brandName}. All rights reserved.` : "");

const words = heading
	.split("*")
	.flatMap((seg, i) => seg.split(/\s+/).filter(Boolean).map((w) => ({ w, accent: i % 2 === 1 })));
const hasCta = words.length > 0 || Boolean(cta);
const hasMark = Boolean(logo || brandName);
const hasCols = columns.length > 0 || contact.length > 0;
const hasMain = hasMark || Boolean(tagline) || Boolean(newsletter) || hasCols;
const hasBottom = Boolean(copy) || legalLinks.length > 0 || social.length > 0;
const hasContent = hasCta || hasMain || hasBottom;
const Heading = headingLevel;
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "#");
const external = (href: string) => /^https?:/i.test(href);
const inputId = `${id ?? "scf"}-email-${Math.random().toString(36).slice(2, 7)}`;

const SOCIAL: Record<ShipFooterSocial, { label: string; path: string }> = {
	facebook: { label: "Facebook", path: "M14 8h3V4h-3a4 4 0 0 0-4 4v2H8v4h2v6h4v-6h3l1-4h-4V8Z" },
	x: { label: "X", path: "M5 4h4l10 16h-4Z M19 4l-6 7 M5 20l6-7" },
	instagram: { label: "Instagram", path: "M8 4h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z M17 7h.01" },
	linkedin: { label: "LinkedIn", path: "M5 9v10 M5 5v.01 M10 19v-6a3 3 0 0 1 6 0v6 M10 9v10" },
	youtube: { label: "YouTube", path: "M3 12s0-4 1-5 8-1 8-1 7 0 8 1 1 5 1 5 0 4-1 5-8 1-8 1-7 0-8-1-1-5-1-5Z M10 9l5 3-5 3Z" },
};
---

{hasContent && fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
{hasContent && (
<footer class:list={["scf", className]} id={id} data-scf-root>
	{background.src && <img class="scf-bg" src={background.src} alt="" width="1600" height="960" loading="lazy" decoding="async" />}
	<div class="scf-wash" aria-hidden="true"></div>

	<div class="scf-inner">
		{hasCta && (
		<div class="scf-cta">
			{words.length > 0 && (
			<Heading class="scf-heading" data-scf-reveal="words">
				{
					words.map((t, i) => (
						<>
							{i > 0 && " "}
							<span class:list={["scf-w", { "scf-accent": t.accent }]} style={`--i:${i}`}>
								{t.w}
							</span>
						</>
					))
				}
			</Heading>
			)}
			{
				cta && (
					<a class="scf-btn" href={safeHref(cta.href)}>
						<span>{cta.label}</span>
						<span class="scf-btn-ic" aria-hidden="true">
							<svg viewBox="0 0 24 24"><path d="M8 5l10 7-10 7 3-7Z" fill="currentColor" /></svg>
						</span>
					</a>
				)
			}
		</div>
		)}

		{hasMain && (
		<div class="scf-main" data-scf-reveal="up">
			<div class="scf-brand">
				{hasMark && (
				<a class="scf-logo" href={safeHref(brandHref)} aria-label={logo ? undefined : `${brandName} home`}>
					{
						logo ? (
							<img src={logo.src} alt={logo.alt} width={logo.width ?? 124} height={logo.height ?? 32} />
						) : (
							<>
								<svg viewBox="0 0 40 40" aria-hidden="true">
									<path d="M6 21a14 14 0 0 1 28 0h-6a8 8 0 0 0-16 0Z" fill="currentColor" />
									<path d="M20 17 11 30h6v7h6v-7h6Z" fill="currentColor" />
								</svg>
								<span>{brandName}</span>
							</>
						)
					}
				</a>
				)}
				{tagline && <p class="scf-tagline">{tagline}</p>}
				{
					newsletter && (
						<form class="scf-form" action={safeHref(newsletter.action)} method="post">
							<label class="scf-sr" for={inputId}>
								{newsletter.label ?? "Email address"}
							</label>
							<input id={inputId} type="email" name="email" required autocomplete="email" placeholder={newsletter.placeholder ?? "you@company.com"} />
							<button type="submit">{newsletter.button ?? "Submit"}</button>
						</form>
					)
				}
			</div>

			{hasCols && (
			<nav class="scf-cols" aria-label={navLabel}>
				{
					columns.map((col) => (
						<div class="scf-col">
							<h2 class="scf-col-title">{col.heading}</h2>
							<ul>
								{col.links.map((l) => (
									<li>
										<a href={safeHref(l.href)}>{l.label}</a>
									</li>
								))}
							</ul>
						</div>
					))
				}
				{
					contact.length > 0 && (
						<div class="scf-col scf-col--contact">
							<h2 class="scf-col-title">{contactHeading}</h2>
							<ul>
								{contact.map((l) => (
									<li>
										<a href={safeHref(l.href)} {...(external(l.href) ? { rel: "noopener" } : {})}>
											<svg viewBox="0 0 16 16" aria-hidden="true">
												<path d="M2 2l12 6-12 6 3-6Z" fill="currentColor" />
											</svg>
											{l.label}
										</a>
									</li>
								))}
							</ul>
						</div>
					)
				}
			</nav>
			)}
		</div>
		)}

		{hasBottom && (
		<div class="scf-bottom">
			{copy && <p class="scf-copy">{copy}</p>}
			{
				legalLinks.length > 0 && (
					<ul class="scf-legal">
						{legalLinks.map((l) => (
							<li>
								<a href={safeHref(l.href)}>{l.label}</a>
							</li>
						))}
					</ul>
				)
			}
			{
				social.length > 0 && (
					<ul class="scf-social">
						{social.map((s) => (
							<li>
								<a href={safeHref(s.href)} rel="noopener" aria-label={s.label ?? SOCIAL[s.network].label}>
									<svg viewBox="0 0 24 24" aria-hidden="true">
										<path d={SOCIAL[s.network].path} fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
									</svg>
								</a>
							</li>
						))}
					</ul>
				)
			}
		</div>
		)}
	</div>
</footer>
)}

<style>
	.scf {
		--scf-navy: var(--tw-navy, #0b4654);
		--scf-accent: var(--tw-accent, #ff6d45);
		--scf-display: var(--tw-display, Outfit, ui-sans-serif, system-ui, sans-serif);
		--scf-body: var(--tw-body, "Noto Sans", ui-sans-serif, system-ui, sans-serif);
		--scf-ease: cubic-bezier(0.44, 0, 0.56, 1);
		--scf-line: rgb(255 255 255 / 0.1);

		position: relative;
		isolation: isolate;
		overflow: hidden;
		background: #07252c;
		color: #fff;
		font-family: var(--scf-body);
	}

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

	.scf-bg,
	.scf-wash {
		position: absolute;
		inset: 0;
		z-index: -1;
		width: 100%;
		height: 100%;
	}

	.scf-bg {
		object-fit: cover;
	}

	.scf-wash {
		background: linear-gradient(180deg, rgb(11 70 84 / 0.88) 0%, rgb(8 45 54 / 0.9) 55%, rgb(2 10 12 / 0.97) 100%);
	}

	.scf-inner {
		max-width: 1440px;
		margin: 0 auto;
		padding: 64px 16px 32px;
	}

	.scf-cta {
		display: flex;
		flex-direction: column;
		align-items: flex-start;
		gap: 28px;
		padding-bottom: 48px;
		border-bottom: 1px solid var(--scf-line);
	}

	.scf-heading {
		max-width: 680px;
		margin: 0;
		font: 900 30px/1.2 var(--scf-display);
		letter-spacing: -0.6px;
		text-transform: uppercase;
	}

	.scf-w {
		display: inline-block;
	}

	.scf-accent {
		color: var(--scf-accent);
	}

	.scf-btn {
		display: inline-flex;
		flex: none;
		align-items: center;
		gap: 12px;
		height: 50px;
		padding: 0 8px 0 25px;
		border-radius: 999px;
		background: var(--scf-accent);
		color: #fff;
		font: 400 16px/25.6px var(--scf-body);
		letter-spacing: -0.32px;
		text-decoration: none;
		transition: background-color 0.3s var(--scf-ease);
	}

	.scf-btn:hover {
		background: #052931;
	}

	.scf-btn-ic {
		display: grid;
		place-items: center;
		width: 34px;
		height: 34px;
		border-radius: 50%;
		background: #fff;
		color: var(--scf-accent);
		transition: transform 0.3s var(--scf-ease);
	}

	.scf-btn-ic svg {
		width: 18px;
		height: 18px;
	}

	.scf-btn:hover .scf-btn-ic {
		transform: rotate(-45deg);
	}

	.scf-main {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: 48px;
		padding: 56px 0;
	}

	.scf-logo {
		display: inline-flex;
		align-items: center;
		gap: 8px;
		color: #fff;
		font: 500 26px/1 var(--scf-display);
		letter-spacing: -0.5px;
		text-decoration: none;
	}

	.scf-logo svg {
		width: 30px;
		height: 30px;
		color: var(--scf-accent);
	}

	.scf-tagline {
		max-width: 300px;
		margin: 36px 0 0;
		font: 400 16px/25.6px var(--scf-body);
		letter-spacing: -0.32px;
	}

	.scf-form {
		display: flex;
		gap: 10px;
		max-width: 400px;
		margin-top: 40px;
	}

	.scf-form input {
		flex: 1 1 auto;
		min-width: 0;
		height: 54px;
		padding: 0 14px;
		border: 0;
		border-radius: 10px;
		background: #fff;
		color: var(--scf-navy);
		font: 400 15px/1 var(--scf-body);
	}

	.scf-form button {
		flex: none;
		width: 115px;
		height: 54px;
		border: 0;
		border-radius: 10px;
		background: var(--scf-accent);
		color: #fff;
		font: 500 16px/1 var(--scf-body);
		cursor: pointer;
		transition:
			opacity 0.3s var(--scf-ease),
			background-color 0.3s var(--scf-ease);
	}

	.scf-form:has(input:invalid) button {
		opacity: 0.5;
	}

	.scf-form button:hover {
		background: #052931;
		opacity: 1;
	}

	.scf-cols {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 36px 24px;
	}

	.scf-col--contact {
		grid-column: 1 / -1;
	}

	.scf-col-title {
		margin: 0 0 14px;
		color: rgb(255 255 255 / 0.7);
		font: 600 20px/32px var(--scf-body);
		letter-spacing: -0.4px;
		text-transform: uppercase;
	}

	.scf-col ul,
	.scf-legal,
	.scf-social {
		margin: 0;
		padding: 0;
		list-style: none;
	}

	.scf-col li + li {
		margin-top: 15px;
	}

	.scf-col a,
	.scf-legal a {
		display: inline-flex;
		align-items: center;
		gap: 10px;
		color: #fff;
		font: 500 16px/25.6px var(--scf-body);
		letter-spacing: -0.32px;
		text-decoration: none;
		transition: color 0.3s var(--scf-ease);
	}

	.scf-col a svg {
		flex: none;
		width: 16px;
		height: 16px;
		color: var(--scf-accent);
	}

	.scf-col a:hover,
	.scf-legal a:hover {
		color: var(--scf-accent);
	}

	.scf-bottom {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: space-between;
		gap: 16px 24px;
		padding-top: 28px;
		border-top: 1px solid var(--scf-line);
	}

	.scf-copy {
		margin: 0;
		color: rgb(255 255 255 / 0.7);
		font: 400 15px/24px var(--scf-body);
	}

	.scf-legal {
		display: flex;
		flex-wrap: wrap;
		gap: 12px 40px;
	}

	.scf-legal a {
		color: rgb(255 255 255 / 0.7);
	}

	.scf-social {
		display: flex;
		gap: 10px;
	}

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

	.scf-social a:hover {
		background: var(--scf-accent);
	}

	.scf-social svg {
		width: 18px;
		height: 18px;
	}

	.scf a:focus-visible,
	.scf button:focus-visible,
	.scf input:focus-visible {
		outline: 2px solid var(--scf-accent);
		outline-offset: 3px;
	}

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

	[data-scf-reveal="up"] {
		transition:
			opacity 0.8s var(--scf-ease),
			transform 0.8s var(--scf-ease);
	}

	.scf[data-scf-ready] [data-scf-reveal="up"]:not([data-in]) {
		opacity: 0;
		transform: translateY(80px);
	}

	.scf-w {
		transition:
			opacity 0.6s var(--scf-ease) calc(var(--i) * 0.06s),
			transform 0.6s var(--scf-ease) calc(var(--i) * 0.06s);
	}

	.scf[data-scf-ready] [data-scf-reveal="words"]:not([data-in]) .scf-w {
		opacity: 0;
		transform: translateY(10px);
	}

	@media (min-width: 810px) {
		.scf-inner {
			padding: 90px 40px 36px;
		}

		.scf-cta {
			flex-direction: row;
			align-items: center;
			justify-content: space-between;
			padding-bottom: 56px;
		}

		.scf-heading {
			font-size: 40px;
			letter-spacing: -0.8px;
		}

		.scf-cols {
			grid-template-columns: repeat(3, auto);
			justify-content: start;
			gap: 0;
		}

		.scf-col {
			padding: 0 40px;
			border-left: 1px solid var(--scf-line);
		}

		.scf-col:first-child {
			padding-left: 0;
			border-left: 0;
		}

		.scf-col--contact {
			grid-column: auto;
		}
	}

	@media (min-width: 1200px) {
		.scf-inner {
			padding: 100px 65px 36px;
		}

		.scf-heading {
			font-size: 50px;
			line-height: 60px;
			letter-spacing: -1px;
		}

		.scf-main {
			grid-template-columns: 485px minmax(0, 1fr);
			gap: 0;
			padding: 100px 0 90px;
		}

		.scf-cols {
			justify-content: space-between;
			padding-top: 20px;
		}

		.scf-col {
			padding: 0 50px;
		}

		.scf-col:first-child {
			padding-left: 50px;
			border-left: 1px solid var(--scf-line);
		}

		.scf-col--contact {
			padding-right: 0;
		}

		.scf-social a {
			width: 35px;
			height: 35px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		[data-scf-reveal="up"],
		.scf-w,
		.scf-btn,
		.scf-btn-ic,
		.scf-col a,
		.scf-legal a,
		.scf-social a,
		.scf-form button {
			transition: none;
		}

		.scf-btn:hover .scf-btn-ic {
			transform: none;
		}
	}
</style>

<script>
	/* Appear-on-scroll; one observer per instance, torn down on astro:before-swap. */
	function initShipCtaFooter(root: HTMLElement) {
		if (root.dataset.scfReady === "") return;
		if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || !("IntersectionObserver" in window)) return;
		const controller = new AbortController();
		root.dataset.scfReady = "";
		const io = new IntersectionObserver(
			(entries) => {
				for (const entry of entries) {
					if (!entry.isIntersecting) continue;
					(entry.target as HTMLElement).dataset.in = "";
					io.unobserve(entry.target);
				}
			},
			{ threshold: 0.1, rootMargin: "0px 0px -5% 0px" },
		);
		root.querySelectorAll<HTMLElement>("[data-scf-reveal]").forEach((t) => io.observe(t));
		controller.signal.addEventListener("abort", () => io.disconnect());
		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				delete root.dataset.scfReady;
			},
			{ once: true, signal: controller.signal },
		);
	}

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

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

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

===== END src/components/ShipCtaFooter.astro =====

===== FILE (required): public/ship-cta-footer/ship-at-sea.webp =====
[binary asset — download https://astro.baysixmedia.com/ship-cta-footer/ship-at-sea.webp and save it to public/ship-cta-footer/ship-at-sea.webp]

--- SECTIONS ---
- Scroll pill header (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/scroll-pill-header/
- Tag title banner (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/tag-title-banner/
- Dark contact panel (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/dark-contact-panel/
- Ship CTA footer (enrolled, not yet production-ready): https://astro.baysixmedia.com/components/ship-cta-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

4 sections, top to bottom. Each is an ordinary library component — open one for its props, its own preview and its verification record.

  1. HeaderScroll pill headerin verification
  2. SectionTag title bannerin verification
  3. SectionDark contact panelin verification
  4. FooterShip CTA footerin verification

8 required files · page file src/pages/tideway-contact.astro · no npm packages