Component library
Back to catalog

Ruled fascia header

A shopfront-fascia header: one full-bleed band of ruled compartments — a solid nameplate from the left edge, equal page cells, a number cell and a filled action block to the right edge. It measures its own content so the cells never wrap, keeps the number in the band at every width, and moves the pages into a native disclosure sheet below that width.

headernavigationfasciaruled cellsnameplatefull-bleedphonelocal servicetrades

Open preview in a new tab

Use this component

Three ways to get Ruled fascia 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 Ruled fascia header from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/ruled-fascia-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
“Ruled fascia header” from my Astro Component Library:
https://astro.baysixmedia.com/components/ruled-fascia-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/RuledFasciaHeader.astro

Installing this one is more than pasting a section

  • installOne required file, plus supporting files worth taking:
    • src/components/RuledFasciaHeader.astrocomponentrequired
    • src/components/RuledFasciaHeader.mdreadmesupporting
    Use Copy header kit above to get them all, each one labelled with the path it belongs at.
---
/**
 * RuledFasciaHeader (candidate, original, role `navigation`)
 *
 * A header built like a shopfront fascia: one full-bleed band divided into ruled compartments.
 * The name sits on a solid nameplate that runs from the left edge of the screen; the pages are a
 * row of equal ruled cells; the number has its own cell (a small "Call" line over the number set
 * large); the one action is a filled block that runs out to the right edge. Every cell is the full
 * height of the bar, so the rules — not loose gaps — space the controls, at every width.
 *
 * The nameplate text starts, and the action text ends, exactly on the site grid's content edges;
 * only their colour bleeds past them.
 *
 * It never wraps. The frontmatter estimates the one-line width of the name, every label, the
 * number and the action, and picks the narrowest container width at which they fit with a margin;
 * below it the pages move into a menu. The number is written out wherever it fits and is a 44px+
 * call cell everywhere else: it never leaves the bar. A small script then measures the real bar in
 * the face the page loaded and steps down (pages to the menu, then the number to its icon) if the
 * estimate was short; without JavaScript the estimate stands.
 *
 * Truth rules. Pages, number and action render only when passed. The call cell needs a real number
 * (tel: built from its digits). No links, no number, no action: the band is the nameplate only.
 *
 * The menu is a native <details> disclosure (works without JavaScript); the script adds Escape,
 * close on outside click and close on choosing a link. ClientRouter-safe: it runs on
 * astro:page-load, once per header, and releases its listeners on astro:before-swap.
 *
 * Reads, never declares: --rfh-bg, --rfh-text, --rfh-tint, --rfh-rule, --rfh-plate,
 * --rfh-plate-text, --rfh-brand, --rfh-brand-hover, --rfh-brand-text, --rfh-family, --rfh-display,
 * --rfh-grid-max, --rfh-grid-gutter.
 */
export interface RuledFasciaHeaderLink {
	label: string;
	href: string;
}

interface Props {
	brandName?: string;
	brandHref?: string;
	logo?: { src: string; alt: string; width?: number; height?: number } | null;
	items?: readonly RuledFasciaHeaderLink[];
	currentPath?: string;
	phone?: { number: string; label?: string } | null;
	cta?: RuledFasciaHeaderLink | null;
	plate?: "ink" | "paper";
	navLabel?: string;
	menuNavLabel?: string;
	menuLabel?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

const {
	brandName = "",
	brandHref,
	logo = null,
	items = [],
	currentPath,
	phone = null,
	cta = null,
	plate,
	navLabel = "Primary",
	menuNavLabel = "Site menu",
	menuLabel = "Menu",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wdth,wght@12..96,75..100,400..800&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 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 name = brandName.trim();
const home = safeHref(brandHref);
const logoSrc = logo?.src?.trim() ? logo : null;
const norm = (p: string) => p.replace(/\/+$/, "") || "/";
const isCurrent = (href: string) => (currentPath ? norm(currentPath) === norm(href) : false);
/* A supplied logo is usually drawn for a light ground, so the nameplate turns paper unless told otherwise. */
const plateTone = plate ?? (logoSrc ? "paper" : "ink");
/* The wordmark steps down by the name's length so it holds one line on the plate. */
const nameSize = name.length <= 16 ? "l" : name.length <= 24 ? "m" : "s";
const menuText = menuLabel.trim() || "Menu";

/*
 * The one-line budget, in px at the default type, calibrated against the rendered band (Bricolage
 * Grotesque): per-character widths for the wordmark at each step, the 15.5px page labels, the 17px
 * number, the 12.5px call line and the 15.5px action. Cell paddings and 1px rules are added as drawn.
 * A 3% margin covers a site face that sets a little wider; the script corrects anything beyond that.
 */
const K = { brand: { l: 9.6, m: 8.6, s: 7.8 }, link: 7.8, num: 8.9, label: 6.2, cta: 7.3, menu: 7.8 };
const brandW = logoSrc ? Math.min(200, Math.round(((logoSrc.width ?? 160) / (logoSrc.height ?? 40)) * 36)) : Math.max(56, name.length * K.brand[nameSize]);
const plateW = brandW + 32;
const navW = links.reduce((w, l) => w + l.label.length * K.link + 40 + 1, 0);
const callFullW = call ? Math.max(call.label.length * K.label, call.number.length * K.num) + 20 + 12 + 40 + 1 : 0;
const ctaW = action ? action.label.length * K.cta + 24 + 26 : 0;
const menuW = links.length ? menuText.length * K.menu + 18 + 10 + 24 + 1 : 0;
/* The content width a container leaves between the grid's edges: 24px gutters below 960, the site gutter (48) from 960, 1232 max. */
const inner = (w: number) => Math.min(w, 1232) - 2 * (w >= 960 ? 48 : 24);
const fits = (need: number, w: number) => need * 1.03 <= inner(w);
/* Pages inline: nameplate + page cells + the written-out number + the action. */
const INLINE_STEPS = [760, 860, 960, 1080, 1180, 1280, 1400] as const;
const inlineAt = links.length ? (INLINE_STEPS.find((w) => fits(plateW + navW + callFullW + ctaW, w)) ?? "never") : "0";
/* Pages in the menu: the number is written out from the first width at which it fits beside the action and the menu cell. */
const NUMBER_STEPS = [600, 640, 680, 720, 740, 780, 860, 960, 1080, 1180] as const;
const numberAt = call ? (NUMBER_STEPS.find((w) => fits(plateW + callFullW + (action ? ctaW + 24 : 0) + menuW, w)) ?? "never") : "never";
/* Which cell closes the band when the menu is not showing: it takes the grid edge as its right padding. */
const last = action ? "cta" : call ? "call" : links.length ? "nav" : "none";
const panelId = `${id ?? "rfh"}-menu`;
const hasBand = Boolean(name || logoSrc || links.length);
---

{hasBand && (
	<header
		class:list={["rfh", className]}
		id={id}
		data-rfh-root
		data-inline={String(inlineAt)}
		data-number={String(numberAt)}
		data-last={last}
		data-plate={plateTone}
		data-name={nameSize}
		data-links={String(links.length)}
	>
		{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
		<div class="rfh-bar">
			{home ? (
				<a class="rfh-brand" href={home}>
					{logoSrc ? <img class="rfh-logo" src={logoSrc.src} alt={logoSrc.alt || name} width={logoSrc.width} height={logoSrc.height} /> : <span class="rfh-wordmark">{name}</span>}
				</a>
			) : (
				<span class="rfh-brand">
					{logoSrc ? <img class="rfh-logo" src={logoSrc.src} alt={logoSrc.alt || name} width={logoSrc.width} height={logoSrc.height} /> : <span class="rfh-wordmark">{name}</span>}
				</span>
			)}

			{links.length > 0 && (
				<nav class="rfh-nav" aria-label={navLabel}>
					<ul role="list">
						{links.map((l) => (
							<li>
								<a class="rfh-link" href={l.href} aria-current={isCurrent(l.href) ? "page" : undefined}>{l.label}</a>
							</li>
						))}
					</ul>
				</nav>
			)}
			<span class="rfh-spacer" aria-hidden="true"></span>

			{call && (
				<a class="rfh-call" href={call.href}>
					<svg class="rfh-call-icon" aria-hidden="true" viewBox="0 0 20 20" width="20" height="20"><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="rfh-call-text">
						<span class="rfh-call-label">{call.label}</span>{" "}
						<span class="rfh-call-number">{call.number}</span>
					</span>
				</a>
			)}
			{action && (
				<a class="rfh-cta" href={action.href}>
					<span>{action.label}</span>
					<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M2.5 8h10M9 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" /></svg>
				</a>
			)}

			{links.length > 0 && (
				<details class="rfh-menu" data-rfh-menu>
					<summary class="rfh-toggle" aria-controls={panelId}>
						<span class="rfh-toggle-text">{menuText}</span>
						<span class="rfh-toggle-icon" aria-hidden="true"><span></span><span></span></span>
					</summary>
					<div class="rfh-panel" id={panelId}>
						<div class="rfh-panel-inner">
							<nav aria-label={menuNavLabel}>
								<ul role="list" class="rfh-panel-links" data-count={String(links.length)}>
									{links.map((l) => (
										<li>
											<a href={l.href} aria-current={isCurrent(l.href) ? "page" : undefined}>{l.label}</a>
										</li>
									))}
								</ul>
							</nav>
							{(call || action) && (
								<div class="rfh-panel-actions">
									{action && (
										<a class="rfh-panel-cta" href={action.href}>
											<span>{action.label}</span>
											<svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M2.5 8h10M9 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" /></svg>
										</a>
									)}
									{call && (
										<a class="rfh-panel-call" href={call.href}>
											<span class="rfh-call-label">{call.label}</span>{" "}
											<span class="rfh-call-number">{call.number}</span>
										</a>
									)}
								</div>
							)}
						</div>
					</div>
				</details>
			)}
		</div>
	</header>
)}

<style>
	.rfh {
		--rfh-paper: var(--rfh-bg, #f2ede3);
		--rfh-ink: var(--rfh-text, #1a1813);
		--rfh-hover: var(--rfh-tint, #e6decd);
		--rfh-line: var(--rfh-rule, #1a1813);
		--rfh-plate-bg: var(--rfh-plate, #1f3b33);
		--rfh-plate-ink: var(--rfh-plate-text, #f5efe2);
		--rfh-accent: var(--rfh-brand, #a93d17);
		--rfh-accent-deep: var(--rfh-brand-hover, #8f3212);
		--rfh-accent-ink: var(--rfh-brand-text, #fff8ef);
		--rfh-font: var(--rfh-family, "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif);
		--rfh-font-display: var(--rfh-display, "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif);
		--rfh-max: var(--rfh-grid-max, 1232px);
		container-type: inline-size;
		position: relative;
		z-index: 40;
		overflow-x: clip;
		background: var(--rfh-paper);
		color: var(--rfh-ink);
		font-family: var(--rfh-font);
		font-optical-sizing: auto;
	}
	.rfh *,
	.rfh *::before,
	.rfh *::after {
		box-sizing: border-box;
	}
	/* The grid's content edge: the gutter, or the centred max width plus the gutter. */
	.rfh-bar,
	.rfh-panel {
		--rfh-pad: 20px;
		--rfh-edge: max(var(--rfh-pad), calc((100cqw - var(--rfh-max)) / 2 + var(--rfh-pad)));
	}
	.rfh-bar {
		display: flex;
		align-items: stretch;
		min-height: 64px;
		border-bottom: 1px solid var(--rfh-line);
	}

	/* Nameplate: solid, from the screen's left edge; its text starts on the grid edge. */
	.rfh-brand {
		display: flex;
		align-items: center;
		flex: 1 1 auto;
		min-width: 0;
		padding: 8px 20px 8px var(--rfh-edge);
		background: var(--rfh-plate-bg);
		color: var(--rfh-plate-ink);
		text-decoration: none;
	}
	.rfh[data-plate="paper"] .rfh-brand {
		background: var(--rfh-hover);
		color: var(--rfh-ink);
		border-right: 1px solid var(--rfh-line);
	}
	.rfh-wordmark {
		font-family: var(--rfh-font-display);
		font-size: 19px;
		font-weight: 720;
		font-stretch: 90%;
		letter-spacing: -0.018em;
		line-height: 1.1;
		text-wrap: balance;
	}
	.rfh[data-name="m"] .rfh-wordmark {
		font-size: 17px;
	}
	.rfh[data-name="s"] .rfh-wordmark {
		font-size: 16px;
	}
	.rfh-logo {
		display: block;
		height: 30px;
		width: auto;
		max-width: 200px;
		object-fit: contain;
	}
	a.rfh-brand:hover .rfh-wordmark {
		text-decoration: underline;
		text-decoration-thickness: 1.5px;
		text-underline-offset: 5px;
	}

	/* Page cells: equal ruled columns that share the band between the nameplate and the number. */
	.rfh-nav {
		display: none;
		/* Basis 0: the cells share the band; the labels' own widths are the floor. */
		flex: 1 1 0;
	}
	.rfh-nav ul {
		display: grid;
		flex: 1 1 auto;
		grid-auto-flow: column;
		grid-auto-columns: minmax(max-content, 1fr);
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.rfh-nav li {
		display: flex;
		border-left: 1px solid var(--rfh-line);
	}
	.rfh[data-plate="paper"] .rfh-nav li:first-child {
		border-left: 0;
	}
	.rfh-link {
		display: flex;
		flex: 1 1 auto;
		align-items: center;
		padding: 0 20px;
		color: var(--rfh-ink);
		font-size: 15.5px;
		font-weight: 560;
		letter-spacing: -0.006em;
		line-height: 1.2;
		text-decoration: none;
		white-space: nowrap;
		transition: background-color 140ms ease;
	}
	.rfh-link:hover {
		background: var(--rfh-hover);
	}
	.rfh-link[aria-current="page"] {
		background: var(--rfh-hover);
		box-shadow: inset 0 -4px 0 var(--rfh-accent);
		font-weight: 650;
	}
	/* Paper between the nameplate and the right-hand cells; on phones the nameplate takes that room. */
	.rfh-spacer {
		display: none;
		flex: 1 1 auto;
	}

	/* The number's cell: a 44px+ call cell (icon) wherever the written number does not fit. */
	.rfh-call {
		position: relative;
		display: flex;
		flex: 0 0 auto;
		align-items: center;
		justify-content: center;
		gap: 12px;
		min-width: 56px;
		padding: 0 16px;
		border-left: 1px solid var(--rfh-line);
		color: var(--rfh-ink);
		text-decoration: none;
		transition: background-color 140ms ease;
	}
	.rfh-call:hover {
		background: var(--rfh-hover);
	}
	.rfh-call-icon {
		flex: none;
	}
	.rfh-call-text {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}
	.rfh-call-label {
		display: block;
		font-size: 12.5px;
		font-weight: 620;
		line-height: 1.2;
		color: var(--rfh-ink);
		white-space: nowrap;
	}
	.rfh-call-number {
		display: block;
		font-family: var(--rfh-font-display);
		font-size: 17px;
		font-weight: 680;
		font-variant-numeric: tabular-nums;
		letter-spacing: -0.01em;
		line-height: 1.2;
		white-space: nowrap;
	}
	.rfh-call:hover .rfh-call-number {
		text-decoration: underline;
		text-decoration-thickness: 1.5px;
		text-underline-offset: 3px;
	}

	/* The action: a filled block; when it closes the band it runs to the screen's right edge. */
	.rfh-cta {
		display: none;
		flex: 0 0 auto;
		align-items: center;
		gap: 10px;
		padding: 0 24px;
		background: var(--rfh-accent);
		color: var(--rfh-accent-ink);
		font-size: 15.5px;
		font-weight: 660;
		letter-spacing: -0.006em;
		text-decoration: none;
		white-space: nowrap;
		transition: background-color 140ms ease;
	}
	.rfh-cta svg,
	.rfh-panel-cta svg {
		flex: none;
		transition: transform 160ms ease;
	}
	.rfh-cta:hover,
	.rfh-panel-cta:hover {
		background: var(--rfh-accent-deep);
	}
	.rfh-cta:hover svg,
	.rfh-panel-cta:hover svg {
		transform: translateX(3px);
	}

	/* Menu cell: closes the band whenever the pages are in the menu. */
	.rfh-menu {
		display: flex;
		flex: 0 0 auto;
	}
	.rfh-toggle {
		display: flex;
		align-items: center;
		gap: 10px;
		min-width: 56px;
		padding: 0 var(--rfh-edge) 0 16px;
		border-left: 1px solid var(--rfh-line);
		cursor: pointer;
		list-style: none;
		font-size: 15.5px;
		font-weight: 600;
		user-select: none;
		transition: background-color 140ms ease;
	}
	.rfh-toggle::-webkit-details-marker {
		display: none;
	}
	.rfh-toggle:hover,
	.rfh-menu[open] .rfh-toggle {
		background: var(--rfh-hover);
	}
	.rfh-toggle-text {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip-path: inset(50%);
		white-space: nowrap;
	}
	.rfh-toggle-icon {
		position: relative;
		display: inline-block;
		width: 22px;
		height: 22px;
	}
	.rfh-toggle-icon span {
		position: absolute;
		left: 1px;
		right: 1px;
		height: 2px;
		background: currentColor;
		transition: transform 200ms ease, top 200ms ease;
	}
	.rfh-toggle-icon span:first-child {
		top: 6px;
	}
	.rfh-toggle-icon span:last-child {
		top: 14px;
	}
	.rfh-menu[open] .rfh-toggle-icon span:first-child {
		top: 10px;
		transform: rotate(45deg);
	}
	.rfh-menu[open] .rfh-toggle-icon span:last-child {
		top: 10px;
		transform: rotate(-45deg);
	}

	/* The sheet under the band: the pages as ruled cells, then the action and the number. */
	.rfh-panel {
		position: absolute;
		top: 100%;
		left: 0;
		right: 0;
		max-height: calc(100dvh - 64px);
		overflow-y: auto;
		background: var(--rfh-paper);
		border-bottom: 1px solid var(--rfh-line);
		box-shadow: 0 28px 40px -30px rgb(26 24 19 / 0.45);
	}
	.rfh-panel-inner {
		padding: 0 var(--rfh-edge) 24px;
	}
	.rfh-panel-links {
		display: grid;
		grid-template-columns: 1fr;
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.rfh-panel-links li {
		display: flex;
		border-bottom: 1px solid var(--rfh-line);
	}
	.rfh-panel-links a {
		display: flex;
		flex: 1 1 auto;
		align-items: center;
		min-height: 60px;
		padding: 10px 0;
		color: var(--rfh-ink);
		font-family: var(--rfh-font-display);
		font-size: 22px;
		font-weight: 600;
		letter-spacing: -0.015em;
		line-height: 1.2;
		text-decoration: none;
	}
	.rfh-panel-links a:hover {
		text-decoration: underline;
		text-decoration-thickness: 2px;
		text-underline-offset: 6px;
	}
	/* The current page: its rule turns into the 4px accent rule. */
	.rfh-panel-links li:has(a[aria-current="page"]) {
		border-bottom: 4px solid var(--rfh-accent);
	}
	.rfh-panel-links a[aria-current="page"] {
		min-height: 57px;
		font-weight: 700;
	}
	.rfh-panel-actions {
		display: grid;
		gap: 12px;
		padding-top: 20px;
	}
	.rfh-panel-cta {
		display: flex;
		align-items: center;
		justify-content: space-between;
		gap: 12px;
		min-height: 56px;
		padding: 0 20px;
		background: var(--rfh-accent);
		color: var(--rfh-accent-ink);
		font-size: 17px;
		font-weight: 660;
		text-decoration: none;
		transition: background-color 140ms ease;
	}
	.rfh-panel-call {
		display: flex;
		flex-direction: column;
		justify-content: center;
		min-height: 56px;
		padding: 6px 20px;
		border: 1px solid var(--rfh-line);
		color: var(--rfh-ink);
		text-decoration: none;
	}
	.rfh-panel-call:hover {
		background: var(--rfh-hover);
	}

	/* Focus: an inset ring in the cell's own text colour, so it is never clipped by the band. */
	.rfh a:focus-visible,
	.rfh-toggle:focus-visible {
		outline: 2px solid currentColor;
		outline-offset: -6px;
	}

	/* Narrow phones: the menu cell is its icon; the name may take two lines on the plate. */
	@container (min-width: 360px) {
		.rfh-toggle-text {
			position: static;
			width: auto;
			height: auto;
			overflow: visible;
			clip-path: none;
		}
		.rfh-toggle {
			padding-left: 18px;
		}
	}
	@container (min-width: 600px) {
		.rfh-bar {
			--rfh-pad: 24px;
		}
		.rfh-panel {
			--rfh-pad: 24px;
		}
		.rfh-spacer {
			display: block;
		}
		.rfh-brand {
			flex: 0 0 auto;
			padding-right: 32px;
		}
		.rfh[data-name="s"] .rfh-brand {
			flex-shrink: 1;
		}
		.rfh-wordmark {
			font-size: 21px;
		}
		.rfh[data-name="m"] .rfh-wordmark {
			font-size: 19px;
		}
		.rfh[data-name="s"] .rfh-wordmark {
			font-size: 17px;
		}
		.rfh[data-name="l"] .rfh-wordmark,
		.rfh[data-name="m"] .rfh-wordmark {
			white-space: nowrap;
		}
		.rfh-logo {
			height: 36px;
		}
		.rfh-call {
			min-width: 64px;
			padding: 0 20px;
		}
		.rfh-cta {
			display: flex;
		}
		.rfh-panel-links {
			grid-template-columns: 1fr 1fr;
			column-gap: 40px;
		}
		.rfh-panel-actions {
			grid-template-columns: 1fr 1fr;
		}
	}
	@container (min-width: 960px) {
		.rfh-bar,
		.rfh-panel {
			--rfh-pad: var(--rfh-grid-gutter, 48px);
		}
		.rfh-bar {
			min-height: 76px;
		}
		.rfh-panel {
			max-height: calc(100dvh - 76px);
		}
		.rfh-panel-links {
			grid-template-columns: repeat(3, 1fr);
		}
		.rfh-panel-actions {
			grid-template-columns: repeat(3, 1fr);
		}
	}
	/* No pages: no menu; the action shows at every width and closes the band. */
	.rfh[data-links="0"] .rfh-cta {
		display: flex;
	}
	.rfh[data-last="cta"][data-links="0"] .rfh-cta,
	.rfh[data-last="call"][data-links="0"] .rfh-call {
		padding-right: var(--rfh-edge);
	}

	/* The number written out, from the width the frontmatter computed. */
	@container (min-width: 600px) {
		.rfh[data-number="600"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 640px) {
		.rfh[data-number="640"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 680px) {
		.rfh[data-number="680"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 720px) {
		.rfh[data-number="720"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 740px) {
		.rfh[data-number="740"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 780px) {
		.rfh[data-number="780"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 860px) {
		.rfh[data-number="860"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 960px) {
		.rfh[data-number="960"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 1080px) {
		.rfh[data-number="1080"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}
	@container (min-width: 1180px) {
		.rfh[data-number="1180"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
	}

	/* Pages inline, from the width the frontmatter computed: page cells replace the spacer and the menu cell. */
	@container (min-width: 760px) {
		.rfh[data-inline="760"] .rfh-nav { display: flex; }
		.rfh[data-inline="760"] :is(.rfh-spacer, .rfh-menu) { display: none; }
		.rfh[data-inline="760"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
		.rfh[data-inline="760"][data-last="cta"] .rfh-cta,
		.rfh[data-inline="760"][data-last="call"] .rfh-call { padding-right: var(--rfh-edge); }
	}
	@container (min-width: 860px) {
		.rfh[data-inline="860"] .rfh-nav { display: flex; }
		.rfh[data-inline="860"] :is(.rfh-spacer, .rfh-menu) { display: none; }
		.rfh[data-inline="860"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
		.rfh[data-inline="860"][data-last="cta"] .rfh-cta,
		.rfh[data-inline="860"][data-last="call"] .rfh-call { padding-right: var(--rfh-edge); }
	}
	@container (min-width: 960px) {
		.rfh[data-inline="960"] .rfh-nav { display: flex; }
		.rfh[data-inline="960"] :is(.rfh-spacer, .rfh-menu) { display: none; }
		.rfh[data-inline="960"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
		.rfh[data-inline="960"][data-last="cta"] .rfh-cta,
		.rfh[data-inline="960"][data-last="call"] .rfh-call { padding-right: var(--rfh-edge); }
	}
	@container (min-width: 1080px) {
		.rfh[data-inline="1080"] .rfh-nav { display: flex; }
		.rfh[data-inline="1080"] :is(.rfh-spacer, .rfh-menu) { display: none; }
		.rfh[data-inline="1080"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
		.rfh[data-inline="1080"][data-last="cta"] .rfh-cta,
		.rfh[data-inline="1080"][data-last="call"] .rfh-call { padding-right: var(--rfh-edge); }
	}
	@container (min-width: 1180px) {
		.rfh[data-inline="1180"] .rfh-nav { display: flex; }
		.rfh[data-inline="1180"] :is(.rfh-spacer, .rfh-menu) { display: none; }
		.rfh[data-inline="1180"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
		.rfh[data-inline="1180"][data-last="cta"] .rfh-cta,
		.rfh[data-inline="1180"][data-last="call"] .rfh-call { padding-right: var(--rfh-edge); }
	}
	@container (min-width: 1280px) {
		.rfh[data-inline="1280"] .rfh-nav { display: flex; }
		.rfh[data-inline="1280"] :is(.rfh-spacer, .rfh-menu) { display: none; }
		.rfh[data-inline="1280"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
		.rfh[data-inline="1280"][data-last="cta"] .rfh-cta,
		.rfh[data-inline="1280"][data-last="call"] .rfh-call { padding-right: var(--rfh-edge); }
	}
	@container (min-width: 1400px) {
		.rfh[data-inline="1400"] .rfh-nav { display: flex; }
		.rfh[data-inline="1400"] :is(.rfh-spacer, .rfh-menu) { display: none; }
		.rfh[data-inline="1400"] .rfh-call-text { position: static; width: auto; height: auto; overflow: visible; clip-path: none; }
		.rfh[data-inline="1400"][data-last="cta"] .rfh-cta,
		.rfh[data-inline="1400"][data-last="call"] .rfh-call { padding-right: var(--rfh-edge); }
	}

	/*
	 * Measured corrections (the script, only when the estimate was short in the loaded face):
	 * "menu" keeps the pages in the menu; "compact" also folds the number to its call cell.
	 */
	.rfh[data-fit] .rfh-nav {
		display: none;
	}
	@container (min-width: 600px) {
		.rfh[data-fit] .rfh-spacer {
			display: block;
		}
	}
	.rfh[data-fit] .rfh-menu {
		display: flex;
	}
	.rfh[data-fit][data-links]:not([data-links="0"]) .rfh-cta {
		padding-right: 24px;
	}
	.rfh[data-fit][data-links]:not([data-links="0"]) .rfh-call {
		padding-right: 20px;
	}
	.rfh[data-fit="compact"][data-number] .rfh-call-text {
		position: absolute;
		width: 1px;
		height: 1px;
		overflow: hidden;
		clip-path: inset(50%);
	}

	@media (prefers-reduced-motion: reduce) {
		.rfh-link,
		.rfh-call,
		.rfh-cta,
		.rfh-cta svg,
		.rfh-panel-cta svg,
		.rfh-toggle,
		.rfh-toggle-icon span,
		.rfh-panel-links a {
			transition: none;
		}
		.rfh-cta:hover svg,
		.rfh-panel-cta:hover svg {
			transform: none;
		}
	}
	@media (forced-colors: active) {
		.rfh-brand,
		.rfh-cta,
		.rfh-panel-cta {
			border: 1px solid CanvasText;
		}
		.rfh-link[aria-current="page"],
		.rfh-panel-links a[aria-current="page"] {
			text-decoration: underline;
			text-decoration-thickness: 3px;
			text-underline-offset: 6px;
		}
	}
</style>

<script>
	/*
	 * One controller per header, safe to run again after a client-side navigation. It measures the
	 * real band in the loaded face and steps down if it does not fit, and adds Escape / outside click /
	 * link choice to the native disclosure.
	 */
	function initRuledFasciaHeaders() {
		for (const root of document.querySelectorAll<HTMLElement>("[data-rfh-root]:not([data-rfh-ready])")) {
			root.setAttribute("data-rfh-ready", "");
			const bar = root.querySelector<HTMLElement>(".rfh-bar");
			const menu = root.querySelector<HTMLDetailsElement>("[data-rfh-menu]");
			const summary = menu?.querySelector<HTMLElement>("summary") ?? null;
			const ac = new AbortController();
			const { signal } = ac;

			const overflows = () => (bar ? bar.scrollWidth > bar.clientWidth + 1 : false);
			let width = -1;
			const fit = (force = false) => {
				const now = root.clientWidth;
				if (!force && now === width) return;
				width = now;
				delete root.dataset.fit;
				if (overflows() && menu) root.dataset.fit = "menu";
				if (overflows()) root.dataset.fit = "compact";
				/* The pages went inline: an open sheet would only repeat them. */
				if (menu?.open && getComputedStyle(menu).display === "none") menu.open = false;
			};
			fit(true);
			const observer = new ResizeObserver(() => fit());
			observer.observe(root);
			document.fonts?.ready.then(() => fit(true));

			if (menu && summary) {
				root.addEventListener(
					"keydown",
					(event) => {
						if (event.key === "Escape" && menu.open) {
							event.preventDefault();
							menu.open = false;
							summary.focus();
						}
					},
					{ signal },
				);
				menu.querySelector(".rfh-panel")?.addEventListener(
					"click",
					(event) => {
						if ((event.target as HTMLElement).closest("a")) menu.open = false;
					},
					{ signal },
				);
				document.addEventListener(
					"click",
					(event) => {
						if (menu.open && !root.contains(event.target as Node)) menu.open = false;
					},
					{ signal },
				);
			}
			document.addEventListener(
				"astro:before-swap",
				() => {
					observer.disconnect();
					ac.abort();
				},
				{ once: true },
			);
		}
	}
	initRuledFasciaHeaders();
	document.addEventListener("astro:page-load", initRuledFasciaHeaders);
</script>