Component library
Back to catalog

Client plates

Client proof as a hierarchy: a heading over one sentence, the first few supplied clients on large stone plates cut apart by slanted gutters with their marks shown as supplied (sized by area from the plate width, so every mark fills its plate with the same weight), then the rest as a roll call of names in running type split by the same slant. Plates always fill their row and the roll wraps as text, so no count leaves a stray cell. No logos, no section.

logosclientssocial proofplatesslantedroll calltypographiclight

Open preview in a new tab

Use this component

Three ways to get Client plates into one of your own projects. The first two carry the component's files with them, so nothing has to fetch this page.

Start here

Placement prompt

The whole thing: what to do, where it goes, what must not change, and every one of the component's 7 files appended underneath with the path each belongs at. Paste it into Claude Code inside the destination project — it fetches nothing and needs nothing else.

Read it first — the files are added underneath when you copy
Add Client plates from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/client-plates/

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

Position:
[ENTER WHERE IT SHOULD APPEAR]

Every file you need is appended below, each fenced with the path it belongs at. Write them there, relative to the project root. 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.

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.

Install kit

The 7 files this component needs and nothing else, each fenced with the path it belongs at, followed by what the library's own verification covered and what it did not. No instructions attached — for placing it yourself, or handing it to something that has its own.

Read it first
Install kit: "Client plates"
Library page: https://astro.baysixmedia.com/components/client-plates/
Files: 7 required, 1 supporting

Write each block below to the path named in its header, relative to the project root. A block marked (required) is one the component does not work without; (supporting) is documentation. Nothing here needs downloading except a block that says so in as many words.

===== FILE (required): src/components/ClientPlates.astro =====
---
/**
 * ClientPlates (candidate)
 *
 * Client proof in two tiers, set as one list. The first few supplied clients
 * are set large, each mark on its own stone plate exactly as supplied (its
 * own colours, never recoloured); everyone after them follows as a roll call
 * of names in running type. The plates in a row are cut apart by the same
 * slanted stroke that splits the names in the roll call, so the big tier and
 * the small tier read as the same line of clients at two sizes.
 *
 * Requires real clients: with no logos it renders nothing, and it never
 * supplies a name, logo, count or claim of its own. How many clients lead is
 * a layout decision (`lead`, 1–4); the order is the order supplied. A client
 * with a link is a link; names are always the accessible text. Linked and
 * unlinked clients look the same at rest, so a mixed set reads as one set;
 * a link shows itself on hover and focus.
 *
 * Plate size follows plate width, and each mark is sized from that width by
 * area: a small script reads the mark's proportions once it loads, so a
 * compact mark with a tagline and a long single-line wordmark both fill their
 * plate with the same visual weight. Without the script every mark falls
 * back to one height. ClientRouter-safe and idempotent.
 *
 * Marks are multiplied onto the plate, so a mark delivered on a white box
 * sits on the stone instead of showing a white rectangle.
 *
 * The plate faces are drawn on two layers behind the mark: an edge layer and
 * a face layer inset 3px from it. At rest they match; on focus the edge turns
 * the accent colour, which draws a 3px ring that follows the slanted cut.
 *
 * Plates in one row from 640px of the section's own width; below that, two
 * columns with the first plate spanning both when the count is odd. Reads
 * --cpl-* tokens from any ancestor.
 */

export interface ClientPlatesLogo {
	name: string;
	src?: string;
	href?: string;
}

export interface ClientPlatesProps {
	heading: string;
	headingLevel?: "h2" | "h3";
	body?: string;
	logos?: readonly ClientPlatesLogo[];
	/** How many clients take a plate (1–4). Default: all of them up to four; three when there are five or more. */
	lead?: number;
	/** The word that leads into the roll call of the remaining clients. */
	moreLabel?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends ClientPlatesProps {}

const {
	heading,
	headingLevel = "h2",
	body = "",
	logos = [],
	lead,
	moreLabel = "Alongside",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&display=swap",
	class: className,
	id,
} = Astro.props;

const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "");
const marks = (logos ?? [])
	.filter((l) => l && typeof l.name === "string" && l.name.trim())
	.map((l) => ({
		name: l.name.trim(),
		src: typeof l.src === "string" ? l.src.trim() : "",
		href: typeof l.href === "string" ? safeHref(l.href) : "",
	}));

const n = marks.length;
const requested = typeof lead === "number" && Number.isFinite(lead) ? Math.round(lead) : 0;
const leadCount = requested > 0 ? Math.min(Math.max(requested, 1), 4, n) : n <= 4 ? n : 3;
const plates = marks.slice(0, leadCount);
const roll = marks.slice(leadCount);
const Heading = headingLevel;
const rollLabel = (moreLabel ?? "").trim();
---

{n > 0 && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section class:list={["cpl", className]} id={id} data-cpl-root data-lead={leadCount} style={`--cpl-lead: ${leadCount};`}>
	<div class="cpl-inner">
		<div class="cpl-head">
			<Heading class="cpl-title">{heading}</Heading>
			{body && <p class="cpl-body">{body}</p>}
		</div>

		<ul class="cpl-plates" role="list">
			{plates.map((m) => {
				const Tag = m.href ? "a" : "div";
				return (
					<li class="cpl-slot">
						<Tag class:list={["cpl-plate", { "cpl-plate--link": m.href }]} href={m.href || undefined}>
							{m.src ? (
								<img class="cpl-mark" src={m.src} alt={m.name} loading="lazy" decoding="async" data-cpl-mark />
							) : (
								<span class:list={["cpl-word", { "cpl-word--long": m.name.length > 16 }]}>{m.name}</span>
							)}
						</Tag>
					</li>
				);
			})}
		</ul>

		{roll.length > 0 && (
			<div class="cpl-roll">
				<ul class="cpl-names" role="list" aria-label={rollLabel || undefined}>
					{rollLabel && <li class="cpl-lead-in" aria-hidden="true">{rollLabel}</li>}
					{roll.map((m) => (
						<li class="cpl-name">
							{m.href ? <a class="cpl-name-text cpl-name-link" href={m.href}>{m.name}</a> : <span class="cpl-name-text">{m.name}</span>}
						</li>
					))}
				</ul>
			</div>
		)}
	</div>
</section>
</>
)}

<style>
	.cpl {
		--cpl-surface: var(--cpl-bg, #ffffff);
		--cpl-ink: var(--cpl-text, #121821);
		--cpl-soft: var(--cpl-text-muted, #454d5b);
		--cpl-slab: var(--cpl-plate, #efece5);
		--cpl-slab-hover: var(--cpl-plate-hover, #e4dfd3);
		--cpl-on-slab: var(--cpl-plate-text, #121821);
		--cpl-accent: var(--cpl-brand, #2747d4);
		--cpl-divider: var(--cpl-stroke, #9aa2b0);
		--cpl-font: var(--cpl-family, "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif);

		container: cpl-section / inline-size;
		background: var(--cpl-surface);
		color: var(--cpl-ink);
		font-family: var(--cpl-font);
	}

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

	.cpl-inner {
		max-width: 1240px;
		margin: 0 auto;
		padding: 60px 20px;
	}

	/* ---- statement: heading, then its sentence directly beneath ---- */

	.cpl-head {
		display: grid;
		gap: 12px;
		margin-bottom: 30px;
	}

	.cpl-title {
		max-width: 22ch;
		margin: 0;
		font: 640 clamp(30px, 5.4cqi, 54px) / 1.06 var(--cpl-font);
		font-optical-sizing: auto;
		letter-spacing: -0.02em;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.cpl-body {
		max-width: 46ch;
		margin: 0;
		color: var(--cpl-soft);
		font: 450 18px/1.5 var(--cpl-font);
		text-wrap: pretty;
		overflow-wrap: anywhere;
	}

	/* ---- the lead clients: plates cut apart by a slanted stroke ---- */

	.cpl-plates {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 10px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	/*
	 * --cpl-l / --cpl-r: 1 where a plate meets a neighbour in its row (that
	 * side is cut on the slant), 0 on the row's outer edges. Two columns here:
	 * left plates cut on the right, right plates cut on the left.
	 */
	.cpl-slot {
		--cpl-l: 0;
		--cpl-r: 1;
		/* Plates and marks size to their own slot. */
		container: cpl-slot / inline-size;
		display: flex;
		min-width: 0;
	}

	.cpl-slot:nth-child(even) {
		--cpl-l: 1;
		--cpl-r: 0;
	}

	/* One plate, or an odd count: the first plate takes the whole row and is uncut. */
	.cpl[data-lead="1"] .cpl-slot:first-child,
	.cpl[data-lead="3"] .cpl-slot:first-child {
		--cpl-l: 0;
		--cpl-r: 0;
		grid-column: 1 / -1;
	}

	/* Three plates: the two under the full-width one pair up. */
	.cpl[data-lead="3"] .cpl-slot:nth-child(2) {
		--cpl-l: 0;
		--cpl-r: 1;
	}

	.cpl[data-lead="3"] .cpl-slot:nth-child(3) {
		--cpl-l: 1;
		--cpl-r: 0;
	}

	.cpl-plate {
		/* Plate height follows plate width; the cut leans 18° (tan 18° ≈ 0.325). */
		--cpl-ph: clamp(88px, 44cqi, 184px);
		--cpl-s: calc(var(--cpl-ph) * 0.325);
		--cpl-pad: max(14px, 6cqi);
		/* Mark scale: area constant and widest share of the slot. */
		--cpl-k: 35cqi;
		--cpl-wmax: 76cqi;
		--cpl-fill: var(--cpl-slab);
		--cpl-edge: var(--cpl-fill);
		position: relative;
		isolation: isolate;
		display: grid;
		flex: 1;
		place-items: center;
		min-width: 0;
		min-height: var(--cpl-ph);
		/* A cut side reaches half the cut into the gap, so neighbouring cuts run parallel at the grid gap. */
		margin-inline: calc(var(--cpl-l) * var(--cpl-s) * -0.5) calc(var(--cpl-r) * var(--cpl-s) * -0.5);
		padding-block: 12px;
		padding-inline: calc(var(--cpl-pad) + var(--cpl-l) * var(--cpl-s) * 0.5) calc(var(--cpl-pad) + var(--cpl-r) * var(--cpl-s) * 0.5);
		color: var(--cpl-on-slab);
		text-decoration: none;
		/* Invisible at rest; forced-colours mode paints it, where the drawn ring is dropped. */
		outline: 3px solid transparent;
		outline-offset: 2px;
	}

	.cpl-plate::before,
	.cpl-plate::after {
		content: "";
		position: absolute;
		z-index: -1;
		pointer-events: none;
		transition: background-color 0.2s ease;
	}

	/* Edge layer: the plate's full cut shape. */
	.cpl-plate::before {
		inset: 0;
		background: var(--cpl-edge);
		clip-path: polygon(calc(var(--cpl-l) * var(--cpl-s)) 0, 100% 0, calc(100% - var(--cpl-r) * var(--cpl-s)) 100%, 0 100%);
	}

	/* Face layer: the same shape 3px in, its cut edges moved in by the 3px ring measured square to the slant. */
	.cpl-plate::after {
		inset: 3px;
		background: var(--cpl-fill);
		clip-path: polygon(
			calc(var(--cpl-l) * (var(--cpl-s) - 0.83px)) 0,
			calc(100% - var(--cpl-r) * 1.13px) 0,
			calc(100% - var(--cpl-r) * (var(--cpl-s) - 0.83px)) 100%,
			calc(var(--cpl-l) * 1.13px) 100%
		);
	}

	.cpl-mark {
		display: block;
		width: auto;
		max-width: 100%;
		height: 19cqi;
		max-height: calc(var(--cpl-ph) * 0.62);
		object-fit: contain;
		/* The supplied mark keeps its colours; a white backing drops into the plate tone. */
		mix-blend-mode: multiply;
		transition: transform 0.25s ease;
	}

	/*
	 * Sized by area from the slot width once the script has read the mark's
	 * proportions (--cpl-ar): a 3:1 mark spans about 60% of its plate, a
	 * compact mark is set taller, a long wordmark wider, none past 76%.
	 */
	@supports (height: calc(1px * pow(2, 1))) {
		.cpl-mark {
			height: min(calc(var(--cpl-k) * pow(var(--cpl-ar, 3), -0.5)), calc(var(--cpl-wmax) / var(--cpl-ar, 3)), calc(var(--cpl-ph) * 0.62));
			max-height: none;
		}
	}

	/* A narrow plate (two to a phone row) gives its mark a larger share. */
	@container cpl-slot (max-width: 260px) {
		.cpl-plate {
			--cpl-pad: 10px;
			--cpl-k: 41cqi;
			--cpl-wmax: 84cqi;
		}
	}

	/* A lead client supplied by name alone: set as a wordmark sized to its plate. */
	.cpl-word {
		max-width: 100%;
		font: 680 clamp(17px, 9cqi, 52px) / 1.08 var(--cpl-font);
		letter-spacing: -0.018em;
		text-align: center;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.cpl-word--long {
		font-size: clamp(15px, 7cqi, 40px);
	}

	.cpl-plate--link:hover {
		--cpl-fill: var(--cpl-slab-hover);
	}

	.cpl-plate--link:hover .cpl-mark {
		transform: scale(1.04);
	}

	.cpl-plate--link:hover .cpl-word {
		text-decoration: underline;
		text-decoration-thickness: 2px;
		text-underline-offset: 0.16em;
	}

	.cpl-plate--link:focus-visible {
		--cpl-fill: var(--cpl-slab-hover);
		--cpl-edge: var(--cpl-accent);
	}

	/* ---- everyone else: a roll call in running type ---- */

	/*
	 * The lead-in word opens the first line; each name carries its stroke in
	 * front of it. The list is pulled one stroke-width left and clipped, so
	 * the stroke in front of whichever name starts a wrapped line falls in the
	 * clipped strip: no line starts or ends on a stroke. The 8px of unclipped
	 * room keeps focus rings whole.
	 */
	.cpl-roll {
		min-width: 0;
		margin-top: 26px;
		margin-inline-start: -8px;
		padding-inline-start: 8px;
		overflow-x: clip;
	}

	.cpl-names {
		--cpl-sep: 1.1em;
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		row-gap: 12px;
		margin: 0;
		margin-inline-start: calc(-1 * var(--cpl-sep));
		padding: 0;
		list-style: none;
		font: 620 21px/1.2 var(--cpl-font);
		letter-spacing: -0.012em;
	}

	.cpl-lead-in {
		/* Pulls the first name to a word space away; if that name wraps, its own blank stroke keeps it clear of the clipped strip. */
		margin-inline: var(--cpl-sep) calc(0.42em - var(--cpl-sep));
		color: var(--cpl-soft);
		font-weight: 440;
	}

	.cpl-name {
		display: inline-flex;
		align-items: center;
		min-width: 0;
		max-width: 100%;
	}

	.cpl-name::before {
		content: "";
		flex: none;
		align-self: center;
		width: var(--cpl-sep);
		height: 0.9em;
		background: linear-gradient(to right, transparent calc(50% - 0.75px), var(--cpl-divider) 0 calc(50% + 0.75px), transparent 0);
		transform: skewX(-18deg);
	}

	/* The lead-in is followed by space, not a stroke. */
	.cpl-lead-in + .cpl-name::before {
		background: none;
	}

	.cpl-name-text {
		min-width: 0;
		overflow-wrap: anywhere;
	}

	/* Linked names reach 45px tall without spreading the lines apart. */
	.cpl-name-link {
		--cpl-hit: max(0px, calc((45px - 1.2em) / 2));
		display: inline-block;
		padding-block: var(--cpl-hit);
		margin-block: calc(-1 * var(--cpl-hit));
		color: inherit;
		text-decoration: underline;
		text-decoration-color: transparent;
		text-decoration-thickness: 2px;
		text-underline-offset: 0.2em;
		transition: color 0.2s ease, text-decoration-color 0.2s ease;
	}

	.cpl-name-link:hover {
		color: var(--cpl-accent);
		text-decoration-color: currentColor;
	}

	.cpl-name-link:focus-visible {
		outline: 3px solid var(--cpl-accent);
		outline-offset: 2px;
		border-radius: 3px;
		text-decoration-color: currentColor;
	}

	/* ---- wider: every plate in one row ---- */

	@container cpl-section (min-width: 640px) {
		.cpl-inner {
			padding: 84px 32px;
		}

		.cpl-head {
			gap: 14px;
			margin-bottom: 40px;
		}

		.cpl-body {
			font-size: 19px;
		}

		.cpl-plates {
			grid-template-columns: repeat(var(--cpl-lead), minmax(0, 1fr));
			gap: 12px;
		}

		/* One row: every join is cut; the row's two outer edges are square. */
		.cpl[data-lead] .cpl-plates > .cpl-slot {
			--cpl-l: 1;
			--cpl-r: 1;
		}

		.cpl[data-lead] .cpl-plates > .cpl-slot:first-child {
			--cpl-l: 0;
		}

		.cpl[data-lead] .cpl-plates > .cpl-slot:last-child {
			--cpl-r: 0;
		}

		.cpl[data-lead="1"] .cpl-plates {
			max-width: 560px;
		}

		.cpl[data-lead="3"] .cpl-slot:first-child {
			grid-column: auto;
		}

		.cpl-roll {
			margin-top: 30px;
		}

		.cpl-names {
			font-size: 23px;
		}
	}

	@container cpl-section (min-width: 960px) {
		.cpl-inner {
			padding: 104px 48px;
		}

		.cpl-body {
			font-size: 20px;
		}

		.cpl-plates {
			gap: 14px;
		}

		.cpl-roll {
			margin-top: 34px;
		}

		.cpl-names {
			row-gap: 10px;
			font-size: 26px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.cpl-plate::before,
		.cpl-plate::after,
		.cpl-mark,
		.cpl-name-link {
			transition: none;
		}

		.cpl-plate--link:hover .cpl-mark {
			transform: none;
		}
	}

	@media (forced-colors: active) {
		.cpl-plate {
			border: 2px solid CanvasText;
		}

		.cpl-plate--link:focus-visible {
			outline-color: Highlight;
		}

		.cpl-mark {
			mix-blend-mode: normal;
		}

		.cpl-name::before {
			background: linear-gradient(to right, transparent calc(50% - 0.75px), CanvasText 0 calc(50% + 0.75px), transparent 0);
			forced-color-adjust: none;
		}

		.cpl-lead-in + .cpl-name::before {
			background: none;
		}
	}
</style>

<script>
	/* Reads each plate mark's proportions once it has loaded and hands them to CSS as --cpl-ar. */
	function sizeMark(img: HTMLImageElement) {
		const w = img.naturalWidth;
		const h = img.naturalHeight;
		if (!w || !h) return;
		img.style.setProperty("--cpl-ar", String(Math.min(Math.max(w / h, 0.5), 10).toFixed(3)));
	}

	function initClientPlates(root: HTMLElement) {
		if (root.dataset.cplReady === "") return;
		root.dataset.cplReady = "";
		const controller = new AbortController();
		root.querySelectorAll<HTMLImageElement>("img[data-cpl-mark]").forEach((img) => {
			if (img.complete && img.naturalWidth) sizeMark(img);
			else img.addEventListener("load", () => sizeMark(img), { once: true, signal: controller.signal });
		});
		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				delete root.dataset.cplReady;
			},
			{ once: true, signal: controller.signal },
		);
	}

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

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

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

===== END src/components/ClientPlates.astro =====

===== FILE (supporting): src/components/ClientPlates.md =====
# Client plates

Client proof as a hierarchy: a heading over one sentence, the first few supplied clients on large stone plates cut apart by slanted gutters with their marks shown as supplied (sized by area from the plate width, so every mark fills its plate with the same weight), then the rest as a roll call of names in running type split by the same slant. Plates always fill their row and the roll wraps as text, so no count leaves a stray cell. No logos, no section.

## Integration

- A section heading (h2 by default).
- No logos, no section. The first clients supplied take plates (all of them up to four, three when there are five or more, or `lead` 1–4); the rest are listed by name in the order supplied. Names are always the accessible text.
- Plate marks are shown exactly as supplied, in their own colours; they are multiplied onto the plate, so a mark on a white background loses the white box. Keep --cpl-plate light for that reason.
- A small script reads each plate mark's proportions after it loads and sizes it by area; without it (or before it runs) every mark uses one height. ClientRouter-safe.
- Plates in one row from 640px of the section's own width, two columns below (an odd count spans the first plate); plate height and mark size follow each plate's width (container queries). Neighbouring plates are cut on an 18-degree slant drawn with clip-path on two pseudo-element layers behind the mark.
- Loads Bricolage Grotesque from Google Fonts by default. Pass fontStylesheet="" on a site that already loads it; theme with --cpl-bg, --cpl-text, --cpl-text-muted, --cpl-plate, --cpl-plate-hover, --cpl-brand, --cpl-stroke and --cpl-family on an ancestor.

## Provenance

Staged as candidate `client-plates` (Step 28) and admitted through the candidate pipeline. Source type: original. References (direction only): none. Quality history: quality/candidates/records/client-plates.json.

===== END src/components/ClientPlates.md =====

===== FILE (required): public/client-plates/meridian-dental.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 0 102 40" role="img" aria-label="Meridian Dental"><path d="M18 6c-8 0-12 6-12 12 0 8 6 16 12 16s12-8 12-16c0-6-4-12-12-12z" fill="#0e7490"/><text x="38" y="21" font-family="Arial,sans-serif" font-size="16" font-weight="700" fill="#164e63">Meridian</text><text x="38" y="35" font-family="Arial,sans-serif" font-size="11" letter-spacing="3" fill="#0e7490">DENTAL</text></svg>

===== END public/client-plates/meridian-dental.svg =====

===== FILE (required): public/client-plates/alder-cove.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 0 143 40" role="img" aria-label="Alder Cove"><path d="M6 32 18 8l12 24z" fill="#2d6a4f"/><text x="38" y="27" font-family="Georgia,serif" font-size="19" font-weight="700" fill="#1b4332">Alder Cove</text></svg>

===== END public/client-plates/alder-cove.svg =====

===== FILE (required): public/client-plates/keelwright.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="4 0 174 40" role="img" aria-label="Keelwright"><circle cx="18" cy="20" r="12" fill="none" stroke="#1d4ed8" stroke-width="4"/><path d="M8 22h20" stroke="#1d4ed8" stroke-width="4"/><text x="38" y="27" font-family="Arial,sans-serif" font-size="19" font-weight="800" letter-spacing="-0.5" fill="#1e3a8a">KEELWRIGHT</text></svg>

===== END public/client-plates/keelwright.svg =====

===== FILE (required): public/client-plates/brickhaus.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="2 0 137 40" role="img" aria-label="Brickhaus"><rect x="4" y="8" width="11" height="11" fill="#b91c1c"/><rect x="17" y="8" width="11" height="11" fill="#b91c1c"/><rect x="10" y="21" width="11" height="11" fill="#7f1d1d"/><text x="38" y="28" font-family="Arial Black,Arial,sans-serif" font-size="18" font-weight="900" fill="#450a0a">Brickhaus</text></svg>

===== END public/client-plates/brickhaus.svg =====

===== FILE (required): public/client-plates/sunvale.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 115 40" role="img" aria-label="Sunvale"><circle cx="16" cy="22" r="9" fill="#f59e0b"/><path d="M2 32h28" stroke="#b45309" stroke-width="3"/><text x="38" y="28" font-family="Trebuchet MS,sans-serif" font-size="21" font-weight="700" fill="#92400e">sunvale</text></svg>

===== END public/client-plates/sunvale.svg =====

===== FILE (required): public/client-plates/pellham-roofing.svg =====
<svg xmlns="http://www.w3.org/2000/svg" viewBox="2 0 188 40" role="img" aria-label="Pellham Roofing"><path d="M4 22 18 8l14 14" fill="none" stroke="#374151" stroke-width="4" stroke-linejoin="round"/><rect x="10" y="22" width="16" height="10" fill="#374151"/><text x="40" y="27" font-family="Verdana,sans-serif" font-size="16" font-weight="700" fill="#111827">Pellham Roofing</text></svg>

===== END public/client-plates/pellham-roofing.svg =====

--- VERIFIED IN THE COMPONENT LIBRARY ---
Enrolled for verification but never run.
No verification run has been recorded.
No checks have passed yet.
No usable source fingerprint, so these results cannot be tied to the files below.

--- 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
“Client plates” from my Astro Component Library:
https://astro.baysixmedia.com/components/client-plates/

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.

Component readiness

Judged on recorded evidence, not on how it looks in the frame above. A description of a component is not a result.

Interactivity
Ships script
Profile
core only
Installation files
7 required + 1 supporting
Verified
Never
Client navigation
Event-tested only — not certified against a real router
Source fingerprint
No fingerprint recorded
Production ready
Not yet

Not yet verified

Enrolled for verification, but never run. Silence is reported as silence rather than assumed to be fine.

Open the verification fixtureOpen the ClientRouter fixture

src/components/library/ClientPlates.astro

This component is not one file on its own

  • assetsCopy these across too — they are referenced by URL, so nothing in the source above points at them: public/client-plates/meridian-dental.svg, public/client-plates/alder-cove.svg, public/client-plates/keelwright.svg, public/client-plates/brickhaus.svg, public/client-plates/sunvale.svg, public/client-plates/pellham-roofing.svg. Leave them behind and those URLs resolve to nothing on the other end. What each component does without them is in its README.
---
/**
 * ClientPlates (candidate)
 *
 * Client proof in two tiers, set as one list. The first few supplied clients
 * are set large, each mark on its own stone plate exactly as supplied (its
 * own colours, never recoloured); everyone after them follows as a roll call
 * of names in running type. The plates in a row are cut apart by the same
 * slanted stroke that splits the names in the roll call, so the big tier and
 * the small tier read as the same line of clients at two sizes.
 *
 * Requires real clients: with no logos it renders nothing, and it never
 * supplies a name, logo, count or claim of its own. How many clients lead is
 * a layout decision (`lead`, 1–4); the order is the order supplied. A client
 * with a link is a link; names are always the accessible text. Linked and
 * unlinked clients look the same at rest, so a mixed set reads as one set;
 * a link shows itself on hover and focus.
 *
 * Plate size follows plate width, and each mark is sized from that width by
 * area: a small script reads the mark's proportions once it loads, so a
 * compact mark with a tagline and a long single-line wordmark both fill their
 * plate with the same visual weight. Without the script every mark falls
 * back to one height. ClientRouter-safe and idempotent.
 *
 * Marks are multiplied onto the plate, so a mark delivered on a white box
 * sits on the stone instead of showing a white rectangle.
 *
 * The plate faces are drawn on two layers behind the mark: an edge layer and
 * a face layer inset 3px from it. At rest they match; on focus the edge turns
 * the accent colour, which draws a 3px ring that follows the slanted cut.
 *
 * Plates in one row from 640px of the section's own width; below that, two
 * columns with the first plate spanning both when the count is odd. Reads
 * --cpl-* tokens from any ancestor.
 */

export interface ClientPlatesLogo {
	name: string;
	src?: string;
	href?: string;
}

export interface ClientPlatesProps {
	heading: string;
	headingLevel?: "h2" | "h3";
	body?: string;
	logos?: readonly ClientPlatesLogo[];
	/** How many clients take a plate (1–4). Default: all of them up to four; three when there are five or more. */
	lead?: number;
	/** The word that leads into the roll call of the remaining clients. */
	moreLabel?: string;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends ClientPlatesProps {}

const {
	heading,
	headingLevel = "h2",
	body = "",
	logos = [],
	lead,
	moreLabel = "Alongside",
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&display=swap",
	class: className,
	id,
} = Astro.props;

const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "");
const marks = (logos ?? [])
	.filter((l) => l && typeof l.name === "string" && l.name.trim())
	.map((l) => ({
		name: l.name.trim(),
		src: typeof l.src === "string" ? l.src.trim() : "",
		href: typeof l.href === "string" ? safeHref(l.href) : "",
	}));

const n = marks.length;
const requested = typeof lead === "number" && Number.isFinite(lead) ? Math.round(lead) : 0;
const leadCount = requested > 0 ? Math.min(Math.max(requested, 1), 4, n) : n <= 4 ? n : 3;
const plates = marks.slice(0, leadCount);
const roll = marks.slice(leadCount);
const Heading = headingLevel;
const rollLabel = (moreLabel ?? "").trim();
---

{n > 0 && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section class:list={["cpl", className]} id={id} data-cpl-root data-lead={leadCount} style={`--cpl-lead: ${leadCount};`}>
	<div class="cpl-inner">
		<div class="cpl-head">
			<Heading class="cpl-title">{heading}</Heading>
			{body && <p class="cpl-body">{body}</p>}
		</div>

		<ul class="cpl-plates" role="list">
			{plates.map((m) => {
				const Tag = m.href ? "a" : "div";
				return (
					<li class="cpl-slot">
						<Tag class:list={["cpl-plate", { "cpl-plate--link": m.href }]} href={m.href || undefined}>
							{m.src ? (
								<img class="cpl-mark" src={m.src} alt={m.name} loading="lazy" decoding="async" data-cpl-mark />
							) : (
								<span class:list={["cpl-word", { "cpl-word--long": m.name.length > 16 }]}>{m.name}</span>
							)}
						</Tag>
					</li>
				);
			})}
		</ul>

		{roll.length > 0 && (
			<div class="cpl-roll">
				<ul class="cpl-names" role="list" aria-label={rollLabel || undefined}>
					{rollLabel && <li class="cpl-lead-in" aria-hidden="true">{rollLabel}</li>}
					{roll.map((m) => (
						<li class="cpl-name">
							{m.href ? <a class="cpl-name-text cpl-name-link" href={m.href}>{m.name}</a> : <span class="cpl-name-text">{m.name}</span>}
						</li>
					))}
				</ul>
			</div>
		)}
	</div>
</section>
</>
)}

<style>
	.cpl {
		--cpl-surface: var(--cpl-bg, #ffffff);
		--cpl-ink: var(--cpl-text, #121821);
		--cpl-soft: var(--cpl-text-muted, #454d5b);
		--cpl-slab: var(--cpl-plate, #efece5);
		--cpl-slab-hover: var(--cpl-plate-hover, #e4dfd3);
		--cpl-on-slab: var(--cpl-plate-text, #121821);
		--cpl-accent: var(--cpl-brand, #2747d4);
		--cpl-divider: var(--cpl-stroke, #9aa2b0);
		--cpl-font: var(--cpl-family, "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif);

		container: cpl-section / inline-size;
		background: var(--cpl-surface);
		color: var(--cpl-ink);
		font-family: var(--cpl-font);
	}

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

	.cpl-inner {
		max-width: 1240px;
		margin: 0 auto;
		padding: 60px 20px;
	}

	/* ---- statement: heading, then its sentence directly beneath ---- */

	.cpl-head {
		display: grid;
		gap: 12px;
		margin-bottom: 30px;
	}

	.cpl-title {
		max-width: 22ch;
		margin: 0;
		font: 640 clamp(30px, 5.4cqi, 54px) / 1.06 var(--cpl-font);
		font-optical-sizing: auto;
		letter-spacing: -0.02em;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.cpl-body {
		max-width: 46ch;
		margin: 0;
		color: var(--cpl-soft);
		font: 450 18px/1.5 var(--cpl-font);
		text-wrap: pretty;
		overflow-wrap: anywhere;
	}

	/* ---- the lead clients: plates cut apart by a slanted stroke ---- */

	.cpl-plates {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 10px;
		margin: 0;
		padding: 0;
		list-style: none;
	}

	/*
	 * --cpl-l / --cpl-r: 1 where a plate meets a neighbour in its row (that
	 * side is cut on the slant), 0 on the row's outer edges. Two columns here:
	 * left plates cut on the right, right plates cut on the left.
	 */
	.cpl-slot {
		--cpl-l: 0;
		--cpl-r: 1;
		/* Plates and marks size to their own slot. */
		container: cpl-slot / inline-size;
		display: flex;
		min-width: 0;
	}

	.cpl-slot:nth-child(even) {
		--cpl-l: 1;
		--cpl-r: 0;
	}

	/* One plate, or an odd count: the first plate takes the whole row and is uncut. */
	.cpl[data-lead="1"] .cpl-slot:first-child,
	.cpl[data-lead="3"] .cpl-slot:first-child {
		--cpl-l: 0;
		--cpl-r: 0;
		grid-column: 1 / -1;
	}

	/* Three plates: the two under the full-width one pair up. */
	.cpl[data-lead="3"] .cpl-slot:nth-child(2) {
		--cpl-l: 0;
		--cpl-r: 1;
	}

	.cpl[data-lead="3"] .cpl-slot:nth-child(3) {
		--cpl-l: 1;
		--cpl-r: 0;
	}

	.cpl-plate {
		/* Plate height follows plate width; the cut leans 18° (tan 18° ≈ 0.325). */
		--cpl-ph: clamp(88px, 44cqi, 184px);
		--cpl-s: calc(var(--cpl-ph) * 0.325);
		--cpl-pad: max(14px, 6cqi);
		/* Mark scale: area constant and widest share of the slot. */
		--cpl-k: 35cqi;
		--cpl-wmax: 76cqi;
		--cpl-fill: var(--cpl-slab);
		--cpl-edge: var(--cpl-fill);
		position: relative;
		isolation: isolate;
		display: grid;
		flex: 1;
		place-items: center;
		min-width: 0;
		min-height: var(--cpl-ph);
		/* A cut side reaches half the cut into the gap, so neighbouring cuts run parallel at the grid gap. */
		margin-inline: calc(var(--cpl-l) * var(--cpl-s) * -0.5) calc(var(--cpl-r) * var(--cpl-s) * -0.5);
		padding-block: 12px;
		padding-inline: calc(var(--cpl-pad) + var(--cpl-l) * var(--cpl-s) * 0.5) calc(var(--cpl-pad) + var(--cpl-r) * var(--cpl-s) * 0.5);
		color: var(--cpl-on-slab);
		text-decoration: none;
		/* Invisible at rest; forced-colours mode paints it, where the drawn ring is dropped. */
		outline: 3px solid transparent;
		outline-offset: 2px;
	}

	.cpl-plate::before,
	.cpl-plate::after {
		content: "";
		position: absolute;
		z-index: -1;
		pointer-events: none;
		transition: background-color 0.2s ease;
	}

	/* Edge layer: the plate's full cut shape. */
	.cpl-plate::before {
		inset: 0;
		background: var(--cpl-edge);
		clip-path: polygon(calc(var(--cpl-l) * var(--cpl-s)) 0, 100% 0, calc(100% - var(--cpl-r) * var(--cpl-s)) 100%, 0 100%);
	}

	/* Face layer: the same shape 3px in, its cut edges moved in by the 3px ring measured square to the slant. */
	.cpl-plate::after {
		inset: 3px;
		background: var(--cpl-fill);
		clip-path: polygon(
			calc(var(--cpl-l) * (var(--cpl-s) - 0.83px)) 0,
			calc(100% - var(--cpl-r) * 1.13px) 0,
			calc(100% - var(--cpl-r) * (var(--cpl-s) - 0.83px)) 100%,
			calc(var(--cpl-l) * 1.13px) 100%
		);
	}

	.cpl-mark {
		display: block;
		width: auto;
		max-width: 100%;
		height: 19cqi;
		max-height: calc(var(--cpl-ph) * 0.62);
		object-fit: contain;
		/* The supplied mark keeps its colours; a white backing drops into the plate tone. */
		mix-blend-mode: multiply;
		transition: transform 0.25s ease;
	}

	/*
	 * Sized by area from the slot width once the script has read the mark's
	 * proportions (--cpl-ar): a 3:1 mark spans about 60% of its plate, a
	 * compact mark is set taller, a long wordmark wider, none past 76%.
	 */
	@supports (height: calc(1px * pow(2, 1))) {
		.cpl-mark {
			height: min(calc(var(--cpl-k) * pow(var(--cpl-ar, 3), -0.5)), calc(var(--cpl-wmax) / var(--cpl-ar, 3)), calc(var(--cpl-ph) * 0.62));
			max-height: none;
		}
	}

	/* A narrow plate (two to a phone row) gives its mark a larger share. */
	@container cpl-slot (max-width: 260px) {
		.cpl-plate {
			--cpl-pad: 10px;
			--cpl-k: 41cqi;
			--cpl-wmax: 84cqi;
		}
	}

	/* A lead client supplied by name alone: set as a wordmark sized to its plate. */
	.cpl-word {
		max-width: 100%;
		font: 680 clamp(17px, 9cqi, 52px) / 1.08 var(--cpl-font);
		letter-spacing: -0.018em;
		text-align: center;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.cpl-word--long {
		font-size: clamp(15px, 7cqi, 40px);
	}

	.cpl-plate--link:hover {
		--cpl-fill: var(--cpl-slab-hover);
	}

	.cpl-plate--link:hover .cpl-mark {
		transform: scale(1.04);
	}

	.cpl-plate--link:hover .cpl-word {
		text-decoration: underline;
		text-decoration-thickness: 2px;
		text-underline-offset: 0.16em;
	}

	.cpl-plate--link:focus-visible {
		--cpl-fill: var(--cpl-slab-hover);
		--cpl-edge: var(--cpl-accent);
	}

	/* ---- everyone else: a roll call in running type ---- */

	/*
	 * The lead-in word opens the first line; each name carries its stroke in
	 * front of it. The list is pulled one stroke-width left and clipped, so
	 * the stroke in front of whichever name starts a wrapped line falls in the
	 * clipped strip: no line starts or ends on a stroke. The 8px of unclipped
	 * room keeps focus rings whole.
	 */
	.cpl-roll {
		min-width: 0;
		margin-top: 26px;
		margin-inline-start: -8px;
		padding-inline-start: 8px;
		overflow-x: clip;
	}

	.cpl-names {
		--cpl-sep: 1.1em;
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		row-gap: 12px;
		margin: 0;
		margin-inline-start: calc(-1 * var(--cpl-sep));
		padding: 0;
		list-style: none;
		font: 620 21px/1.2 var(--cpl-font);
		letter-spacing: -0.012em;
	}

	.cpl-lead-in {
		/* Pulls the first name to a word space away; if that name wraps, its own blank stroke keeps it clear of the clipped strip. */
		margin-inline: var(--cpl-sep) calc(0.42em - var(--cpl-sep));
		color: var(--cpl-soft);
		font-weight: 440;
	}

	.cpl-name {
		display: inline-flex;
		align-items: center;
		min-width: 0;
		max-width: 100%;
	}

	.cpl-name::before {
		content: "";
		flex: none;
		align-self: center;
		width: var(--cpl-sep);
		height: 0.9em;
		background: linear-gradient(to right, transparent calc(50% - 0.75px), var(--cpl-divider) 0 calc(50% + 0.75px), transparent 0);
		transform: skewX(-18deg);
	}

	/* The lead-in is followed by space, not a stroke. */
	.cpl-lead-in + .cpl-name::before {
		background: none;
	}

	.cpl-name-text {
		min-width: 0;
		overflow-wrap: anywhere;
	}

	/* Linked names reach 45px tall without spreading the lines apart. */
	.cpl-name-link {
		--cpl-hit: max(0px, calc((45px - 1.2em) / 2));
		display: inline-block;
		padding-block: var(--cpl-hit);
		margin-block: calc(-1 * var(--cpl-hit));
		color: inherit;
		text-decoration: underline;
		text-decoration-color: transparent;
		text-decoration-thickness: 2px;
		text-underline-offset: 0.2em;
		transition: color 0.2s ease, text-decoration-color 0.2s ease;
	}

	.cpl-name-link:hover {
		color: var(--cpl-accent);
		text-decoration-color: currentColor;
	}

	.cpl-name-link:focus-visible {
		outline: 3px solid var(--cpl-accent);
		outline-offset: 2px;
		border-radius: 3px;
		text-decoration-color: currentColor;
	}

	/* ---- wider: every plate in one row ---- */

	@container cpl-section (min-width: 640px) {
		.cpl-inner {
			padding: 84px 32px;
		}

		.cpl-head {
			gap: 14px;
			margin-bottom: 40px;
		}

		.cpl-body {
			font-size: 19px;
		}

		.cpl-plates {
			grid-template-columns: repeat(var(--cpl-lead), minmax(0, 1fr));
			gap: 12px;
		}

		/* One row: every join is cut; the row's two outer edges are square. */
		.cpl[data-lead] .cpl-plates > .cpl-slot {
			--cpl-l: 1;
			--cpl-r: 1;
		}

		.cpl[data-lead] .cpl-plates > .cpl-slot:first-child {
			--cpl-l: 0;
		}

		.cpl[data-lead] .cpl-plates > .cpl-slot:last-child {
			--cpl-r: 0;
		}

		.cpl[data-lead="1"] .cpl-plates {
			max-width: 560px;
		}

		.cpl[data-lead="3"] .cpl-slot:first-child {
			grid-column: auto;
		}

		.cpl-roll {
			margin-top: 30px;
		}

		.cpl-names {
			font-size: 23px;
		}
	}

	@container cpl-section (min-width: 960px) {
		.cpl-inner {
			padding: 104px 48px;
		}

		.cpl-body {
			font-size: 20px;
		}

		.cpl-plates {
			gap: 14px;
		}

		.cpl-roll {
			margin-top: 34px;
		}

		.cpl-names {
			row-gap: 10px;
			font-size: 26px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.cpl-plate::before,
		.cpl-plate::after,
		.cpl-mark,
		.cpl-name-link {
			transition: none;
		}

		.cpl-plate--link:hover .cpl-mark {
			transform: none;
		}
	}

	@media (forced-colors: active) {
		.cpl-plate {
			border: 2px solid CanvasText;
		}

		.cpl-plate--link:focus-visible {
			outline-color: Highlight;
		}

		.cpl-mark {
			mix-blend-mode: normal;
		}

		.cpl-name::before {
			background: linear-gradient(to right, transparent calc(50% - 0.75px), CanvasText 0 calc(50% + 0.75px), transparent 0);
			forced-color-adjust: none;
		}

		.cpl-lead-in + .cpl-name::before {
			background: none;
		}
	}
</style>

<script>
	/* Reads each plate mark's proportions once it has loaded and hands them to CSS as --cpl-ar. */
	function sizeMark(img: HTMLImageElement) {
		const w = img.naturalWidth;
		const h = img.naturalHeight;
		if (!w || !h) return;
		img.style.setProperty("--cpl-ar", String(Math.min(Math.max(w / h, 0.5), 10).toFixed(3)));
	}

	function initClientPlates(root: HTMLElement) {
		if (root.dataset.cplReady === "") return;
		root.dataset.cplReady = "";
		const controller = new AbortController();
		root.querySelectorAll<HTMLImageElement>("img[data-cpl-mark]").forEach((img) => {
			if (img.complete && img.naturalWidth) sizeMark(img);
			else img.addEventListener("load", () => sizeMark(img), { once: true, signal: controller.signal });
		});
		document.addEventListener(
			"astro:before-swap",
			() => {
				controller.abort();
				delete root.dataset.cplReady;
			},
			{ once: true, signal: controller.signal },
		);
	}

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

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

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