Component library
Back to catalog

Contents masthead header

A two-tier editorial masthead: the name centred in a display serif between an outlined call pill and one solid action, with every page on a ruled serif contents line beneath. Narrower, it keeps both tiers — the pages or a labelled Menu share the contents line with the action — and Menu opens a large serif index beside a contact card. Computed switch widths; a small script adds Escape and outside-click close.

headernavigationeditorialmastheadtypographicseriflightcontents rail

Open preview in a new tab

Use this component

Three ways to get Contents masthead header into one of your own projects. All pre-filled with this component's name and library page.

Or, for the design alone

Placement prompt

What to do, where it goes and what must not change. Paste it into Claude Code inside the destination project.

Read it first
Add Contents masthead header from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/contents-masthead-header/

Place it in:
[ENTER PAGE, FILE, OR ROUTE]

Position:
[ENTER WHERE IT SHOULD APPEAR]

First read the component source, README, documented props, dependencies, and required assets from the library page. Copy the component and every required companion asset locally into this project. Do not import files from the deployed library at runtime.

Adapt its demo content, links, colors, typography, and images to this project through documented props where possible. Preserve its responsive behavior, accessibility, interaction logic, reduced-motion behavior, and support for multiple instances. Avoid changing unrelated project code.

Test it at desktop, tablet, and mobile widths. Run the project's available check and build commands. Report the files changed and any differences from the library version. Do not commit or deploy.

--- VERIFIED IN THE COMPONENT LIBRARY ---
Enrolled for verification but never run.
No verification run has been recorded.
No checks have passed yet.
This record pre-dates source fingerprinting, so it is not tied to a specific revision.

--- NOT PROVEN ---
- ClientRouter: event-tested only. Handlers respond to dispatched lifecycle events; this has NOT been certified against a real <ClientRouter /> transition.

--- WHAT THIS DOES NOT CERTIFY ---
Verification was performed on the unmodified component, in an isolated fixture, in the
library. It says nothing about this project's CSS, routes, layout or data, and nothing
about the component once you adapt it. A verified component is a good starting point,
not a guarantee about the installation you are about to build.

--- TEST IN THIS PROJECT AFTER INSTALLING OR CUSTOMISING ---
- Horizontal overflow at 1440, 1280, 1024, 834, 390 and 320px.
- Keyboard operation end to end, including visible focus.
- The no-JavaScript render.
- prefers-reduced-motion: reduce.
- Two instances on one page, if this project renders more than one.
- If this project uses <ClientRouter />: teardown and reinitialisation across a real navigation.

Component reference

A fragment for a request you write yourself, e.g. “I want to add [paste] to Section 3 of my homepage.” It names the component, links its page, and fixes what must not change — the design build and its effects — while leaving branding and content open. It does not carry the code.

Read it first
“Contents masthead header” from my Astro Component Library:
https://astro.baysixmedia.com/components/contents-masthead-header/

Use the existing library component as the implementation source. Preserve its design structure, layout, proportions, spacing system, responsive behavior, visual effects, animations, transitions, hover states, interactions, and accessibility.

The copy, colors, typography, images, and other content may be adapted to match the destination website. Do not redesign, simplify, or reinterpret the component’s underlying design build or effects.

src/components/library/ContentsMastheadHeader.astro

Installing this one is more than pasting a section

  • installOne required file, plus supporting files worth taking:
    • src/components/ContentsMastheadHeader.astrocomponentrequired
    • src/components/ContentsMastheadHeader.mdreadmesupporting
    Use Copy header kit above to get them all, each one labelled with the path it belongs at.
---
/**
 * ContentsMastheadHeader (candidate, role `navigation`, original)
 *
 * An editorial masthead in two tiers, at every width.
 *
 * Where the pages fit, the business's name is set centred in a display serif between two pills of
 * equal height — the number (outlined, with a call glyph) on the left and the one action (solid) on
 * the right — and under a full-bleed hairline runs the contents rail: every page on one line in the
 * serif, hairline dividers between them, the current page italic on an accent rule.
 *
 * Where they do not fit, the two tiers stay. The top tier carries the name and the number (a 44px
 * call button on the narrowest bars, the number itself from 380px, an outlined pill from 600px). The
 * lower tier is the same ruled contents line in short form: a labelled Menu control, the current page
 * italic on its accent rule where there is room, and the action at the end — so booking is always on
 * the first screen. Menu opens the contents as a large serif index beside a contact card (number,
 * email, hours, address and the action) that stretches to the index's height.
 *
 * The switch is computed, not guessed: the frontmatter estimates the width the rail and the centred
 * masthead need at the default type and picks the narrowest container width where both fit with a
 * margin. Long names and seven long labels therefore never wrap or collide; they keep the index.
 *
 * Truth rules. Links, phone, email, hours, address and the action each render only when passed;
 * the contact card renders only when it has something in it. tel: and mailto: are built from the
 * supplied values; a maps link is used only when supplied. The menu is a native <details>
 * disclosure, so it works without script; a small script adds Escape, outside-click and link-choice
 * close. It boots on astro:page-load, once per header, and releases its listeners on
 * astro:before-swap. In the flow; reserves its own height.
 *
 * Reads, never declares: --cmh-bg, --cmh-text, --cmh-text-muted, --cmh-brand, --cmh-brand-text,
 * --cmh-border, --cmh-well, --cmh-family, --cmh-display, --cmh-grid-max, --cmh-grid-gutter.
 */
export interface ContentsMastheadHeaderLink {
	label: string;
	href: string;
}

interface Props {
	brandName?: string;
	brandHref?: string;
	logo?: { src: string; alt: string; width?: number; height?: number } | null;
	items?: readonly ContentsMastheadHeaderLink[];
	currentPath?: string;
	phone?: { number: string; label?: string } | null;
	email?: string;
	hours?: string | readonly string[];
	address?: string | readonly string[];
	addressHref?: string;
	cta?: ContentsMastheadHeaderLink | null;
	/** Heading of the contact card in the open menu; "Contact" by default. */
	contactLabel?: string;
	emailLabel?: string;
	hoursLabel?: string;
	addressLabel?: string;
	navLabel?: string;
	menuLabel?: string;
	closeLabel?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

const {
	brandName = "",
	brandHref,
	logo = null,
	items = [],
	currentPath,
	phone = null,
	email = "",
	hours,
	address,
	addressHref,
	cta = null,
	contactLabel = "Contact",
	emailLabel = "Email",
	hoursLabel = "Hours",
	addressLabel = "Visit",
	navLabel = "Primary",
	menuLabel = "Menu",
	closeLabel = "Close",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400&family=Inter+Tight:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const safeHref = (href: string | undefined) => {
	const h = (href ?? "").trim();
	return /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i.test(h) ? h : null;
};
const lines = (v: string | readonly string[] | undefined) => (Array.isArray(v) ? v : typeof v === "string" ? [v] : []).map((s) => String(s ?? "").trim()).filter(Boolean);

const links = items
	.map((l) => ({ label: (l?.label ?? "").trim(), href: safeHref(l?.href) }))
	.filter((l): l is { label: string; href: string } => Boolean(l.label && l.href));
const action = cta && cta.label?.trim() && safeHref(cta.href) ? { label: cta.label.trim(), href: safeHref(cta.href)! } : null;
const digits = (phone?.number ?? "").replace(/[^\d+]/g, "");
const call = phone && phone.number?.trim() && digits.replace(/\D/g, "").length >= 7 ? { number: phone.number.trim(), label: (phone.label ?? "Call").trim() || "Call", href: `tel:${digits}` } : null;
const mail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()) ? email.trim() : "";
const hourLines = lines(hours);
const addressLines = lines(address);
const mapHref = addressLines.length ? safeHref(addressHref) : null;
const name = brandName.trim();
const home = safeHref(brandHref);
const norm = (p: string) => p.replace(/\/+$/, "") || "/";
const isCurrent = (href: string) => (currentPath ? norm(currentPath) === norm(href) : false);
const here = links.find((l) => isCurrent(l.href))?.label ?? "";
const hasContact = Boolean(call || mail || hourLines.length || addressLines.length || action);
const hasMenu = links.length > 0 || hasContact;
const toggleText = menuLabel.trim() || "Menu";
const closeText = closeLabel.trim() || "Close";

/*
 * The masthead budget, in px at the default faces, calibrated against the rendered rail: Newsreader
 * sets about 8.5px a character at 19px and 7.6px at 17px; each page adds its side padding and divider.
 * The centred name needs equal room on both sides for the wider of the two pills. A 5% margin covers
 * a site face that sets a little wider.
 */
const chars = links.reduce((n, l) => n + l.label.length, 0);
const railW = (size: "lg" | "md") => (size === "lg" ? chars * 8.5 + links.length * 45 : chars * 7.6 + links.length * 34);
const markSize = name.length <= 18 ? 34 : name.length <= 26 ? 30 : name.length <= 34 ? 27 : 24;
const brandW = logo?.src ? Math.min(240, Math.round(((logo.width ?? 160) / (logo.height ?? 40)) * 44)) : name.length * markSize * 0.49;
const callW = call ? call.number.length * 9.2 + 72 : 0;
const ctaW = action ? action.label.length * 8.1 + 54 : 0;
const mastW = brandW + 2 * (Math.max(callW, ctaW) + 28);
const STEPS = [900, 1024, 1180, 1280, 1400] as const;
const pad = (w: number) => (w >= 960 ? 96 : w >= 600 ? 64 : 40);
const barInner = (w: number) => Math.min(w, 1232) - pad(w);
const railInner = (w: number) => Math.min(w, 1440) - pad(w);
const fits = (w: number, size: "lg" | "md") => railInner(w) >= railW(size) * 1.05 && barInner(w) >= mastW * 1.05;
const railAt = links.length ? (STEPS.find((w) => fits(w, "md")) ?? "never") : "never";
const lgAt = links.length ? (STEPS.find((w) => fits(w, "lg")) ?? "never") : "never";

/*
 * The short contents line (lower tier of the bar): Menu, the current page where it fits, the action.
 * The current page shows from the narrowest width where all three sit on one line; on phones where
 * Menu and the action do not share a line, the action takes its own full-width row instead.
 */
const toggleW = toggleText.length * 8.6 + 46;
const hereW = here ? here.length * 7 + 34 : 0;
const pillW = (w: number) => (action ? (w >= 600 ? action.label.length * 7.9 + 50 : action.label.length * 7.4 + 40) : 0);
const HERE_STEPS = [480, 600, 720, 840] as const;
const hereAt = here && hasMenu ? (HERE_STEPS.find((w) => barInner(w) >= (toggleW + hereW + pillW(w) + 28) * 1.05) ?? "never") : "never";
/* Where the whole rail and the action share the lower tier, the pages themselves replace Menu. */
const STRIP_STEPS = [600, 720, 840] as const;
const stripAt = links.length ? (STRIP_STEPS.find((w) => barInner(w) >= (railW("md") - 16 + pillW(w) + 28) * 1.05) ?? "never") : "never";
const strapFits = (w: number) => barInner(w) >= (toggleW + pillW(w) + 14) * 1.04;
const strapStack = !action || !hasMenu ? undefined : !strapFits(380) ? "480" : !strapFits(320) ? "380" : undefined;

/* The compact wordmark: long names set a step smaller and may take two balanced lines on phones. */
const nameSize = name.length <= 14 ? "s" : name.length <= 22 ? "m" : "l";
/* Few pages set larger in the open index. */
const scale = links.length <= 4 ? "lg" : links.length <= 6 ? "md" : "sm";
const uid = id ?? `cmh-${Math.random().toString(36).slice(2, 8)}`;
const panelId = `${uid}-menu`;
const titleId = `${uid}-contact`;
const callName = call ? `${call.label} ${call.number}` : "";
---

{(name || logo?.src) && (
	<header class:list={["cmh", className]} id={id} data-cmh-root data-rail={String(railAt)} data-lg={String(lgAt)} data-name={nameSize} data-here={String(hereAt)} data-strip={String(stripAt)}>
		{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}

		{/* Where the pages fit: the centred masthead and the contents rail. */}
		{links.length > 0 && (
			<div class="cmh-masthead" style={`--cmh-mark:${markSize}px`}>
				<div class="cmh-mast-row">
					<div class="cmh-mast-start">
						{call && (
							<a class="cmh-call cmh-call--mast" href={call.href} aria-label={callName}>
								<svg class="cmh-glyph" aria-hidden="true" viewBox="0 0 20 20" width="18" height="18"><path d="M6.2 2.8 8 6.6 6.3 8.2c.9 2 2.5 3.6 4.5 4.5l1.6-1.7 3.8 1.8-.6 3.2c-.1.5-.5.8-1 .8C8.4 16.6 3.4 11.6 2.2 5.4c-.1-.5.3-.9.8-1Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" /></svg>
								<span class="cmh-call-num">{call.number}</span>
							</a>
						)}
					</div>
					{home ? (
						<a class="cmh-brand cmh-brand--mast" href={home}>
							{logo?.src ? <img class="cmh-logo" src={logo.src} alt={logo.alt || name} width={logo.width} height={logo.height} /> : <span class="cmh-mark">{name}</span>}
						</a>
					) : (
						<span class="cmh-brand cmh-brand--mast">
							{logo?.src ? <img class="cmh-logo" src={logo.src} alt={logo.alt || name} width={logo.width} height={logo.height} /> : <span class="cmh-mark">{name}</span>}
						</span>
					)}
					<div class="cmh-mast-end">{action && <a class="cmh-action" href={action.href}>{action.label}</a>}</div>
				</div>
				<nav class="cmh-rail" aria-label={navLabel}>
					<ul role="list">
						{links.map((l) => (
							<li>
								<a href={l.href} aria-current={isCurrent(l.href) ? "page" : undefined}>{l.label}</a>
							</li>
						))}
					</ul>
				</nav>
			</div>
		)}

		{/* Where they do not: the name and number above, the short contents line below. */}
		<div class="cmh-compact">
			<div class="cmh-bar">
				{home ? (
					<a class="cmh-brand" href={home}>
						{logo?.src ? <img class="cmh-logo" src={logo.src} alt={logo.alt || name} width={logo.width} height={logo.height} /> : <span class="cmh-wordmark">{name}</span>}
					</a>
				) : (
					<span class="cmh-brand">
						{logo?.src ? <img class="cmh-logo" src={logo.src} alt={logo.alt || name} width={logo.width} height={logo.height} /> : <span class="cmh-wordmark">{name}</span>}
					</span>
				)}
				{call && (
					<a class="cmh-call cmh-call--bar" href={call.href} aria-label={callName}>
						<svg class="cmh-glyph" aria-hidden="true" viewBox="0 0 20 20" width="18" height="18"><path d="M6.2 2.8 8 6.6 6.3 8.2c.9 2 2.5 3.6 4.5 4.5l1.6-1.7 3.8 1.8-.6 3.2c-.1.5-.5.8-1 .8C8.4 16.6 3.4 11.6 2.2 5.4c-.1-.5.3-.9.8-1Z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" /></svg>
						<span class="cmh-call-num">{call.number}</span>
					</a>
				)}
			</div>
			{(hasMenu || action) && (
				<div class="cmh-strap" data-stack={strapStack}>
					<div class="cmh-strap-inner">
						{hasMenu && (
							<details class="cmh-menu" data-cmh-menu>
								<summary class="cmh-toggle" aria-controls={panelId}>
									<span class="cmh-toggle-mark" aria-hidden="true"></span>
									<span class="cmh-toggle-open">{toggleText}</span>
									<span class="cmh-toggle-close">{closeText}</span>
								</summary>
								<div class="cmh-panel" id={panelId}>
									<div class="cmh-panel-inner" data-scale={scale} data-solo={links.length === 0 ? "" : undefined}>
										{links.length > 0 && (
											<nav class="cmh-index" aria-label={navLabel}>
												<ul role="list">
													{links.map((l) => (
														<li>
															<a href={l.href} aria-current={isCurrent(l.href) ? "page" : undefined}>
																<span class="cmh-index-label">{l.label}</span>
															</a>
														</li>
													))}
												</ul>
											</nav>
										)}
										{hasContact && (
											<section class="cmh-card" aria-labelledby={titleId}>
												<h2 class="cmh-card-title" id={titleId}>{contactLabel}</h2>
												{(call || mail || hourLines.length > 0 || addressLines.length > 0) && (
													<dl class="cmh-rows">
														{call && (
															<div class="cmh-row">
																<dt>{call.label}</dt>
																<dd><a class="cmh-value cmh-value--num" href={call.href}>{call.number}</a></dd>
															</div>
														)}
														{mail && (
															<div class="cmh-row">
																<dt>{emailLabel}</dt>
																<dd><a class="cmh-value" href={`mailto:${mail}`}>{mail}</a></dd>
															</div>
														)}
														{hourLines.length > 0 && (
															<div class="cmh-row">
																<dt>{hoursLabel}</dt>
																<dd class="cmh-lines">{hourLines.map((h) => <span>{h}</span>)}</dd>
															</div>
														)}
														{addressLines.length > 0 && (
															<div class="cmh-row">
																<dt>{addressLabel}</dt>
																<dd class="cmh-lines">
																	{mapHref ? (
																		<a class="cmh-value cmh-value--plain" href={mapHref}>{addressLines.map((a) => <span>{a}</span>)}</a>
																	) : (
																		addressLines.map((a) => <span>{a}</span>)
																	)}
																</dd>
															</div>
														)}
													</dl>
												)}
												{action && <a class="cmh-card-cta" href={action.href}>{action.label}</a>}
											</section>
										)}
									</div>
								</div>
							</details>
						)}
						{stripAt !== "never" && (
							<nav class="cmh-pages" aria-label={navLabel}>
								<ul role="list">
									{links.map((l) => (
										<li>
											<a href={l.href} aria-current={isCurrent(l.href) ? "page" : undefined}>{l.label}</a>
										</li>
									))}
								</ul>
							</nav>
						)}
						{here && hasMenu && <span class="cmh-here" aria-hidden="true"><span class="cmh-here-text">{here}</span></span>}
						{action && <a class="cmh-action cmh-action--strap" href={action.href}>{action.label}</a>}
					</div>
				</div>
			)}
		</div>
	</header>
)}

<style>
	.cmh {
		--cmh-surface: var(--cmh-bg, #f5f3ee);
		--cmh-ink: var(--cmh-text, #16171a);
		--cmh-soft: var(--cmh-text-muted, #46484d);
		--cmh-accent: var(--cmh-brand, #8a3a24);
		--cmh-accent-ink: var(--cmh-brand-text, #ffffff);
		--cmh-line-c: var(--cmh-border, #d6d0c3);
		--cmh-card-bg: var(--cmh-well, #ebe6db);
		--cmh-font: var(--cmh-family, "Inter Tight", ui-sans-serif, system-ui, sans-serif);
		--cmh-serif: var(--cmh-display, "Newsreader", ui-serif, Georgia, serif);
		--cmh-max: var(--cmh-grid-max, 1232px);
		--cmh-pad: 20px;
		container-type: inline-size;
		position: relative;
		z-index: 40;
		background: var(--cmh-surface);
		color: var(--cmh-ink);
		font-family: var(--cmh-font);
		border-bottom: 1px solid var(--cmh-line-c);
	}
	.cmh *,
	.cmh *::before,
	.cmh *::after {
		box-sizing: border-box;
	}
	.cmh ul,
	.cmh dl,
	.cmh dd {
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.cmh a:focus-visible,
	.cmh-toggle:focus-visible {
		outline: 2px solid var(--cmh-accent);
		outline-offset: 3px;
		border-radius: 3px;
	}
	.cmh .cmh-rail a:focus-visible,
	.cmh .cmh-pages a:focus-visible {
		outline-offset: -4px;
	}
	.cmh .cmh-action:focus-visible,
	.cmh .cmh-card-cta:focus-visible,
	.cmh .cmh-call:focus-visible {
		border-radius: 999px;
	}

	/* Shared pieces */
	.cmh-brand {
		display: inline-flex;
		align-items: center;
		min-height: 44px;
		min-width: 0;
		color: inherit;
		text-decoration: none;
	}
	.cmh-logo {
		display: block;
		height: 34px;
		width: auto;
		max-width: 200px;
		object-fit: contain;
	}

	/* The number: an outlined pill the same height as the action, so the two flanks weigh the same. */
	.cmh-call {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		gap: 9px;
		flex: none;
		min-height: 48px;
		padding: 0 22px 0 18px;
		border: 1.5px solid var(--cmh-ink);
		border-radius: 999px;
		color: var(--cmh-ink);
		text-decoration: none;
		white-space: nowrap;
		transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease;
	}
	.cmh-glyph {
		flex: none;
		color: var(--cmh-accent);
		transition: color 180ms ease;
	}
	.cmh-call-num {
		font-size: 16px;
		font-weight: 600;
		line-height: 1.2;
		letter-spacing: 0.005em;
		font-variant-numeric: tabular-nums;
	}
	.cmh-call:hover {
		background: var(--cmh-ink);
		color: var(--cmh-surface);
	}
	.cmh-call:hover .cmh-glyph {
		color: currentColor;
	}

	.cmh-action {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		flex: none;
		min-height: 48px;
		padding: 0 24px;
		border: 1.5px solid var(--cmh-ink);
		border-radius: 999px;
		background: var(--cmh-ink);
		color: var(--cmh-surface);
		font-size: 15px;
		font-weight: 600;
		line-height: 1.2;
		text-align: center;
		text-decoration: none;
		white-space: nowrap;
		transition: background-color 180ms ease, border-color 180ms ease, color 180ms ease;
	}
	.cmh-action:hover {
		background: var(--cmh-accent);
		border-color: var(--cmh-accent);
		color: var(--cmh-accent-ink);
	}

	/* ---------- The masthead (shown only where the computed width allows) ---------- */
	.cmh-masthead {
		display: none;
		padding-inline: var(--cmh-pad);
	}
	.cmh-mast-row {
		display: grid;
		grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
		align-items: center;
		gap: 28px;
		max-width: calc(var(--cmh-max) - 2 * var(--cmh-pad));
		min-height: 100px;
		margin: 0 auto;
	}
	.cmh-mast-start {
		justify-self: start;
	}
	.cmh-mast-end {
		justify-self: end;
	}
	.cmh-brand--mast {
		justify-self: center;
		text-align: center;
	}
	.cmh-brand--mast .cmh-logo {
		height: 44px;
		max-width: 240px;
	}
	.cmh-mark {
		font-family: var(--cmh-serif);
		font-size: var(--cmh-mark, 30px);
		font-weight: 500;
		letter-spacing: -0.02em;
		line-height: 1;
		white-space: nowrap;
		font-variation-settings: "opsz" 60;
		/* Centre the capitals, not the line box: the serif's descender space otherwise lifts the name. */
		text-box: trim-both cap alphabetic;
	}

	/* The contents rail: every page on one ruled line under the name. */
	.cmh-rail {
		border-top: 1px solid var(--cmh-line-c);
		margin-inline: calc(-1 * var(--cmh-pad));
	}
	.cmh-rail ul {
		display: flex;
		justify-content: center;
		align-items: stretch;
		max-width: 1440px;
		margin: 0 auto;
		padding-inline: var(--cmh-pad);
	}
	.cmh-rail li {
		position: relative;
		display: flex;
	}
	.cmh-rail li + li::before,
	.cmh-pages li + li::before {
		content: "";
		position: absolute;
		left: 0;
		top: 50%;
		width: 1px;
		height: 20px;
		margin-top: -10px;
		background: var(--cmh-line-c);
	}
	.cmh-rail a,
	.cmh-pages a {
		position: relative;
		display: inline-flex;
		align-items: center;
		min-height: 56px;
		padding: 0 16px;
		color: var(--cmh-ink);
		font-family: var(--cmh-serif);
		font-size: 17px;
		line-height: 1.25;
		white-space: nowrap;
		text-decoration: none;
		transition: color 180ms ease;
	}
	.cmh-rail a::after,
	.cmh-pages a::after {
		content: "";
		position: absolute;
		left: 16px;
		right: 16px;
		bottom: -1px;
		height: 2px;
		background: var(--cmh-accent);
		transform: scaleX(0);
		transform-origin: left center;
		transition: transform 240ms cubic-bezier(0.2, 0.7, 0.2, 1);
	}
	.cmh-rail a:hover,
	.cmh-pages a:hover {
		color: var(--cmh-accent);
	}
	.cmh-rail a:hover::after,
	.cmh-rail a[aria-current="page"]::after,
	.cmh-pages a:hover::after,
	.cmh-pages a[aria-current="page"]::after {
		transform: scaleX(1);
	}
	.cmh-rail a[aria-current="page"],
	.cmh-pages a[aria-current="page"] {
		font-style: italic;
		color: var(--cmh-accent);
	}

	/* ---------- The compact masthead: two tiers ---------- */
	.cmh-bar {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 12px;
		max-width: var(--cmh-max);
		min-height: 72px;
		margin: 0 auto;
		padding-block: 10px;
		padding-inline: var(--cmh-pad);
	}
	.cmh-wordmark {
		display: block;
		font-family: var(--cmh-serif);
		font-size: 24px;
		font-weight: 500;
		letter-spacing: -0.02em;
		line-height: 1.04;
		text-wrap: balance;
		overflow-wrap: anywhere;
		font-variation-settings: "opsz" 48;
		text-box: trim-both cap alphabetic;
	}
	/* Phones: a long name takes two balanced lines rather than shrinking out of scale. */
	.cmh[data-name="m"] .cmh-wordmark,
	.cmh[data-name="l"] .cmh-wordmark {
		font-size: 22px;
	}

	/* The narrowest bars: the number as a 44px call button. */
	.cmh-call--bar {
		width: 44px;
		min-height: 44px;
		height: 44px;
		padding: 0;
		border-width: 1px;
		border-color: var(--cmh-line-c);
	}
	.cmh-call--bar .cmh-call-num {
		display: none;
	}

	/* The short contents line: the rail's rule, Menu, the current page and the action. */
	.cmh-strap {
		border-top: 1px solid var(--cmh-line-c);
	}
	.cmh-strap-inner {
		display: flex;
		align-items: center;
		flex-wrap: wrap;
		column-gap: 14px;
		row-gap: 10px;
		max-width: var(--cmh-max);
		min-height: 60px;
		margin: 0 auto;
		padding-inline: var(--cmh-pad);
	}
	.cmh-action--strap {
		margin-inline-start: auto;
		min-height: 44px;
		padding: 0 18px;
		font-size: 14.5px;
	}
	.cmh-here {
		position: relative;
		display: none;
		align-items: center;
		align-self: stretch;
		min-width: 0;
		padding-inline: 14px 2px;
		font-family: var(--cmh-serif);
		font-size: 18px;
		font-style: italic;
		line-height: 1.2;
		white-space: nowrap;
		color: var(--cmh-accent);
	}
	.cmh-here-text {
		display: block;
		/* Centre on the letters, as the Menu label beside it is. */
		text-box: trim-both cap alphabetic;
	}
	.cmh-here::before {
		content: "";
		position: absolute;
		left: 0;
		top: 50%;
		width: 1px;
		height: 20px;
		margin-top: -10px;
		background: var(--cmh-line-c);
	}
	.cmh-here::after {
		content: "";
		position: absolute;
		left: 14px;
		right: 2px;
		bottom: -1px;
		height: 2px;
		background: var(--cmh-accent);
	}

	/* The pages on the lower tier: the rail, set from the start edge under the name. */
	.cmh-pages {
		display: none;
		align-self: stretch;
		min-width: 0;
	}
	.cmh-pages ul {
		display: flex;
		align-items: stretch;
		height: 100%;
	}
	.cmh-pages li {
		position: relative;
		display: flex;
	}
	.cmh-pages li:first-child a {
		padding-inline-start: 0;
	}
	.cmh-pages li:first-child a::after {
		left: 0;
	}

	.cmh-menu {
		align-self: stretch;
		display: flex;
	}
	.cmh-toggle {
		display: inline-flex;
		align-items: center;
		gap: 10px;
		min-height: 44px;
		padding: 0 6px 0 0;
		cursor: pointer;
		list-style: none;
		font-size: 16px;
		font-weight: 600;
		user-select: none;
	}
	.cmh-toggle::-webkit-details-marker {
		display: none;
	}
	.cmh-toggle-close {
		display: none;
	}
	.cmh-menu[open] .cmh-toggle-open {
		display: none;
	}
	.cmh-menu[open] .cmh-toggle-close {
		display: inline;
	}
	.cmh-toggle-mark {
		position: relative;
		flex: none;
		width: 22px;
		height: 12px;
	}
	.cmh-toggle-mark::before,
	.cmh-toggle-mark::after {
		content: "";
		position: absolute;
		left: 0;
		right: 0;
		height: 1.5px;
		background: currentColor;
		transition: transform 220ms ease, top 220ms ease;
	}
	.cmh-toggle-mark::before {
		top: 2px;
	}
	.cmh-toggle-mark::after {
		top: 9px;
	}
	.cmh-menu[open] .cmh-toggle-mark::before {
		top: 5.5px;
		transform: rotate(35deg);
	}
	.cmh-menu[open] .cmh-toggle-mark::after {
		top: 5.5px;
		transform: rotate(-35deg);
	}

	/* The index */
	.cmh-panel {
		position: absolute;
		left: 0;
		right: 0;
		top: 100%;
		max-height: calc(100dvh - 132px);
		overflow-y: auto;
		overscroll-behavior: contain;
		background: var(--cmh-surface);
		border-top: 1px solid var(--cmh-line-c);
		border-bottom: 1px solid var(--cmh-line-c);
		box-shadow: 0 36px 48px -40px rgb(22 23 26 / 0.4);
	}
	.cmh-panel-inner {
		display: grid;
		gap: 28px;
		max-width: var(--cmh-max);
		margin: 0 auto;
		padding: 8px var(--cmh-pad) 28px;
	}
	.cmh-index li {
		border-bottom: 1px solid var(--cmh-line-c);
	}
	.cmh-index a {
		display: flex;
		align-items: baseline;
		min-height: 60px;
		padding-block: 14px 12px;
		color: var(--cmh-ink);
		font-family: var(--cmh-serif);
		font-size: 28px;
		font-weight: 400;
		letter-spacing: -0.02em;
		line-height: 1.1;
		text-decoration: none;
		transition: color 180ms ease;
	}
	.cmh-panel-inner[data-scale="sm"] .cmh-index a {
		font-size: 25px;
		min-height: 54px;
		padding-block: 12px 10px;
	}
	.cmh-index-label {
		transition: transform 240ms cubic-bezier(0.2, 0.7, 0.2, 1);
	}
	.cmh-index a:hover {
		color: var(--cmh-accent);
	}
	.cmh-index a:hover .cmh-index-label {
		transform: translateX(6px);
	}
	.cmh-index a[aria-current="page"] {
		color: var(--cmh-accent);
	}
	.cmh-index a[aria-current="page"] .cmh-index-label {
		font-style: italic;
	}

	/* The contact card: the same ruled rows as the index, on a quiet well. */
	.cmh-card {
		display: flex;
		flex-direction: column;
		padding: 22px 20px 20px;
		border-radius: 4px;
		background: var(--cmh-card-bg);
	}
	.cmh-card-title {
		margin: 0 0 12px;
		font-family: var(--cmh-serif);
		font-size: 24px;
		font-weight: 500;
		letter-spacing: -0.015em;
		line-height: 1.1;
	}
	.cmh-row {
		display: grid;
		grid-template-columns: 62px minmax(0, 1fr);
		align-items: baseline;
		gap: 12px;
		padding-block: 4px;
		border-bottom: 1px solid var(--cmh-line-c);
	}
	.cmh-row dd.cmh-lines {
		padding-block: 9px;
	}
	.cmh-row dt {
		font-family: var(--cmh-serif);
		font-style: italic;
		font-size: 17px;
		color: var(--cmh-soft);
	}
	.cmh-row dd {
		font-size: 17px;
		font-weight: 500;
		line-height: 1.4;
		overflow-wrap: break-word;
	}
	.cmh-value {
		display: inline-flex;
		align-items: center;
		min-height: 44px;
		color: var(--cmh-ink);
		text-decoration: underline;
		text-decoration-color: var(--cmh-line-c);
		text-decoration-thickness: 1px;
		text-underline-offset: 5px;
		transition: text-decoration-color 180ms ease;
	}
	.cmh-value:hover {
		text-decoration-color: currentColor;
	}
	.cmh-value--num {
		white-space: nowrap;
		font-size: 19px;
		font-weight: 600;
		font-variant-numeric: tabular-nums;
	}
	.cmh-lines,
	.cmh-value--plain {
		display: grid;
	}
	.cmh-value--plain {
		align-items: start;
	}
	.cmh-lines {
		font-weight: 500;
	}
	.cmh-card-cta {
		display: flex;
		align-items: center;
		justify-content: center;
		min-height: 52px;
		margin-top: 20px;
		padding: 0 24px;
		border-radius: 999px;
		background: var(--cmh-ink);
		color: var(--cmh-surface);
		font-size: 16px;
		font-weight: 600;
		text-align: center;
		text-decoration: none;
		transition: background-color 180ms ease, color 180ms ease;
	}
	.cmh-card-cta:hover {
		background: var(--cmh-accent);
		color: var(--cmh-accent-ink);
	}

	@container (max-width: 379px) {
		.cmh-bar {
			gap: 10px;
		}
		.cmh-wordmark,
		.cmh[data-name] .cmh-wordmark {
			font-size: 20px;
		}
	}
	/* Phones: an action too long to share the line with Menu takes its own full-width row. */
	@container (max-width: 379px) {
		.cmh-strap[data-stack] .cmh-strap-inner {
			padding-block: 4px 12px;
		}
		.cmh-strap[data-stack] .cmh-action--strap {
			flex: 1 0 100%;
			margin-inline-start: 0;
		}
	}
	@container (max-width: 479px) {
		.cmh-strap[data-stack="480"] .cmh-strap-inner {
			padding-block: 4px 12px;
		}
		.cmh-strap[data-stack="480"] .cmh-action--strap {
			flex: 1 0 100%;
			margin-inline-start: 0;
		}
	}
	/* From 380px the number itself sits beside the name. */
	@container (min-width: 380px) {
		.cmh-call--bar {
			width: auto;
			padding: 0 2px 0 4px;
			border: 0;
		}
		.cmh-call--bar .cmh-call-num {
			display: inline;
			font-size: 16.5px;
		}
		.cmh-call--bar:hover {
			background: none;
			color: var(--cmh-ink);
		}
		.cmh-call--bar:hover .cmh-call-num {
			text-decoration: underline;
			text-decoration-thickness: 1.5px;
			text-underline-offset: 5px;
		}
		.cmh-call--bar:hover .cmh-glyph {
			color: var(--cmh-accent);
		}
	}
	@container (min-width: 600px) {
		.cmh {
			--cmh-pad: 32px;
		}
		.cmh-bar {
			min-height: 84px;
			gap: 20px;
		}
		.cmh-wordmark,
		.cmh[data-name] .cmh-wordmark {
			font-size: 28px;
			line-height: 1.02;
		}
		.cmh[data-name="l"] .cmh-wordmark {
			font-size: 25px;
		}
		.cmh-logo {
			height: 38px;
		}
		/* The number becomes the same outlined pill as on the masthead. */
		.cmh-call--bar,
		.cmh-call--bar:hover {
			padding: 0 20px 0 16px;
			border: 1.5px solid var(--cmh-ink);
		}
		.cmh-call--bar .cmh-call-num {
			font-size: 16px;
		}
		.cmh-call--bar:hover {
			background: var(--cmh-ink);
			color: var(--cmh-surface);
		}
		.cmh-call--bar:hover .cmh-call-num {
			text-decoration: none;
		}
		.cmh-call--bar:hover .cmh-glyph {
			color: currentColor;
		}
		.cmh-strap-inner {
			min-height: 64px;
			column-gap: 18px;
		}
		.cmh-here {
			padding-inline-start: 18px;
		}
		.cmh-here::after {
			left: 18px;
		}
		.cmh-action--strap {
			min-height: 46px;
			padding: 0 22px;
			font-size: 15px;
		}
		.cmh-panel {
			max-height: calc(100dvh - 150px);
		}
	}
	@container (min-width: 720px) {
		.cmh-panel-inner {
			grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
			column-gap: 48px;
			padding-block: 20px 40px;
		}
		.cmh-panel-inner[data-solo] {
			grid-template-columns: minmax(0, 1fr);
			max-width: 640px;
			margin-inline: auto;
		}
		.cmh-index a {
			font-size: 38px;
			min-height: 72px;
			padding-block: 18px 14px;
		}
		.cmh-panel-inner[data-scale="lg"] .cmh-index a {
			font-size: 46px;
			min-height: 84px;
		}
		.cmh-panel-inner[data-scale="sm"] .cmh-index a {
			font-size: 32px;
			min-height: 62px;
			padding-block: 14px 12px;
		}
		.cmh-card {
			align-self: stretch;
			margin-top: 12px;
			padding: 28px 28px 28px;
		}
		.cmh-card-cta {
			margin-top: auto;
		}
		.cmh .cmh-rows {
			margin-bottom: 28px;
		}
		/* The card's column is narrow here: each label sits above its value. */
		.cmh-row {
			grid-template-columns: minmax(0, 1fr);
			gap: 0;
			padding-block: 10px 4px;
		}
		.cmh-row dd.cmh-lines {
			padding-block: 2px 8px;
		}
		/* The number leads the card in the display face. */
		.cmh-value--num {
			font-family: var(--cmh-serif);
			font-size: 27px;
			font-weight: 500;
			letter-spacing: -0.01em;
			font-variant-numeric: lining-nums;
		}
	}
	@container (min-width: 960px) {
		.cmh {
			--cmh-pad: var(--cmh-grid-gutter, 48px);
		}
	}
	@container (min-width: 1024px) {
		.cmh-row {
			grid-template-columns: 76px minmax(0, 1fr);
			gap: 12px;
			padding-block: 4px;
		}
		.cmh-row dd.cmh-lines {
			padding-block: 9px;
		}
		.cmh-value--num {
			font-size: 30px;
		}
	}

	/* The current page joins the short contents line from the width the frontmatter computed. */
	@container (min-width: 480px) {
		.cmh[data-here="480"] .cmh-here { display: inline-flex; }
	}
	@container (min-width: 600px) {
		.cmh[data-here="600"] .cmh-here { display: inline-flex; }
	}
	@container (min-width: 720px) {
		.cmh[data-here="720"] .cmh-here { display: inline-flex; }
	}
	@container (min-width: 840px) {
		.cmh[data-here="840"] .cmh-here { display: inline-flex; }
	}

	/* The pages replace Menu on the lower tier from the width the frontmatter computed. */
	@container (min-width: 600px) {
		.cmh[data-strip="600"] .cmh-pages { display: block; }
		.cmh[data-strip="600"] .cmh-menu,
		.cmh[data-strip="600"] .cmh-here { display: none; }
	}
	@container (min-width: 720px) {
		.cmh[data-strip="720"] .cmh-pages { display: block; }
		.cmh[data-strip="720"] .cmh-menu,
		.cmh[data-strip="720"] .cmh-here { display: none; }
	}
	@container (min-width: 840px) {
		.cmh[data-strip="840"] .cmh-pages { display: block; }
		.cmh[data-strip="840"] .cmh-menu,
		.cmh[data-strip="840"] .cmh-here { display: none; }
	}

	/* The masthead replaces the two-tier bar from the width the frontmatter computed. */
	@container (min-width: 900px) {
		.cmh[data-rail="900"] .cmh-masthead { display: block; }
		.cmh[data-rail="900"] .cmh-compact { display: none; }
		.cmh[data-lg="900"] .cmh-rail a { font-size: 19px; padding-inline: 22px; }
		.cmh[data-lg="900"] .cmh-rail a::after { left: 22px; right: 22px; }
	}
	@container (min-width: 1024px) {
		.cmh[data-rail="1024"] .cmh-masthead { display: block; }
		.cmh[data-rail="1024"] .cmh-compact { display: none; }
		.cmh[data-lg="1024"] .cmh-rail a { font-size: 19px; padding-inline: 22px; }
		.cmh[data-lg="1024"] .cmh-rail a::after { left: 22px; right: 22px; }
	}
	@container (min-width: 1180px) {
		.cmh[data-rail="1180"] .cmh-masthead { display: block; }
		.cmh[data-rail="1180"] .cmh-compact { display: none; }
		.cmh[data-lg="1180"] .cmh-rail a { font-size: 19px; padding-inline: 22px; }
		.cmh[data-lg="1180"] .cmh-rail a::after { left: 22px; right: 22px; }
	}
	@container (min-width: 1280px) {
		.cmh[data-rail="1280"] .cmh-masthead { display: block; }
		.cmh[data-rail="1280"] .cmh-compact { display: none; }
		.cmh[data-lg="1280"] .cmh-rail a { font-size: 19px; padding-inline: 22px; }
		.cmh[data-lg="1280"] .cmh-rail a::after { left: 22px; right: 22px; }
	}
	@container (min-width: 1400px) {
		.cmh[data-rail="1400"] .cmh-masthead { display: block; }
		.cmh[data-rail="1400"] .cmh-compact { display: none; }
		.cmh[data-lg="1400"] .cmh-rail a { font-size: 19px; padding-inline: 22px; }
		.cmh[data-lg="1400"] .cmh-rail a::after { left: 22px; right: 22px; }
	}

	@media (prefers-reduced-motion: reduce) {
		.cmh-action,
		.cmh-call,
		.cmh-glyph,
		.cmh-card-cta,
		.cmh-toggle-mark::before,
		.cmh-toggle-mark::after,
		.cmh-rail a,
		.cmh-rail a::after,
		.cmh-pages a,
		.cmh-pages a::after,
		.cmh-index a,
		.cmh-index-label,
		.cmh-value {
			transition: none;
		}
		.cmh-index a:hover .cmh-index-label {
			transform: none;
		}
	}
	@media (forced-colors: active) {
		.cmh-action,
		.cmh-call,
		.cmh-card-cta {
			border: 2px solid ButtonText;
		}
		.cmh-toggle-mark::before,
		.cmh-toggle-mark::after,
		.cmh-rail a::after,
		.cmh-rail li + li::before,
		.cmh-pages a::after,
		.cmh-pages li + li::before,
		.cmh-here::before,
		.cmh-here::after {
			background: CanvasText;
		}
		.cmh-card {
			border: 1px solid CanvasText;
		}
	}
</style>

<script>
	/*
	 * Adds Escape, outside-click and link-choice close to the native disclosure. One controller per
	 * header, safe to run again after a client-side navigation; listeners are released on swap.
	 */
	function initContentsMastheads() {
		for (const root of document.querySelectorAll<HTMLElement>("[data-cmh-root]:not([data-cmh-ready])")) {
			const menu = root.querySelector<HTMLDetailsElement>("[data-cmh-menu]");
			const summary = menu?.querySelector<HTMLElement>("summary");
			if (!menu || !summary) continue;
			root.setAttribute("data-cmh-ready", "");
			const ac = new AbortController();
			const { signal } = ac;

			document.addEventListener(
				"keydown",
				(event) => {
					if (event.key !== "Escape" || !menu.open) return;
					event.preventDefault();
					menu.open = false;
					summary.focus();
				},
				{ signal },
			);
			document.addEventListener(
				"pointerdown",
				(event) => {
					if (menu.open && !menu.contains(event.target as Node)) menu.open = false;
				},
				{ signal },
			);
			/* Keyboard users tabbing out of the open index close it too. */
			menu.addEventListener(
				"focusout",
				(event) => {
					const next = event.relatedTarget as Node | null;
					if (menu.open && next && !menu.contains(next)) menu.open = false;
				},
				{ signal },
			);
			menu.querySelector(".cmh-panel")?.addEventListener(
				"click",
				(event) => {
					if ((event.target as HTMLElement).closest("a")) menu.open = false;
				},
				{ signal },
			);
			document.addEventListener(
				"astro:before-swap",
				() => {
					ac.abort();
					root.removeAttribute("data-cmh-ready");
				},
				{ once: true },
			);
		}
	}
	initContentsMastheads();
	document.addEventListener("astro:page-load", initContentsMastheads);
</script>