Component library
Back to catalog

Credential ledger

Verified authority as a ruled record: a split header (heading; one sentence and a provenance note) over a ledger of the business's own licences and registrations — a sentence-case label over the identifier in large tabular figures, with issuer, a short line and a source (a registry link only when one is supplied). Value-less credentials become statement rows. No seals, badges or icons.

credentialslicencesregistrationsledgertypographiceditoriallight

Open preview in a new tab

Use this component

Three ways to get Credential ledger 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 1 file 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 Credential ledger from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/credential-ledger/

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 1 file 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: "Credential ledger"
Library page: https://astro.baysixmedia.com/components/credential-ledger/
Files: 1 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/CredentialLedger.astro =====
---
/**
 * CredentialLedger (candidate, Step 34)
 *
 * Verified authority set as a record, not a badge wall: a split header
 * (heading on one side; one sentence and an optional note on where the
 * details came from on the other) over a full-width ruled ledger. An entry
 * with a value is a registry line — a quiet sentence-case label over the
 * value set large in tabular figures, with the issuer, a short description
 * and its source beside or under it. An entry with a label and no value
 * ("Bonded for residential work") is a statement row in the same face a clear step
 * below the records, never an empty number slot. A label that already carries its
 * identifier ("USDOT 1234567": no value and a run of 3+ digits, or a label
 * containing its value) is set once, whole and exactly as supplied, in the
 * value style — never split, never printed twice.
 *
 * Truth: every label, value, issuer and source renders exactly as supplied.
 * No default credential, number, issuer, seal, check mark or claim of
 * verification. A source that is an http(s) URL becomes a link named by its
 * host; any other source is shown as plain text. No entries, no section.
 *
 * Responsive (container queries on the section's own width): stacked below
 * 640px; from 640px the header splits over the full-width ledger; from
 * 1100px, 4–6 entries form a two-column register (rows aligned across,
 * value over details in each cell), while 2–3 entries stay one column with
 * details beside each record (from 600px of the ledger's width). Row spacing
 * and value size follow the entry count (2–3 roomy, 4 standard, 5–6 compact).
 *
 * Hooks (read, never declared): --crl-bg, --crl-text, --crl-text-muted,
 * --crl-brand, --crl-brand-text, --crl-border, --crl-family, --crl-display,
 * --crl-grid-max, --crl-grid-gutter, --crl-heading-size. No script.
 */

export interface CredentialLedgerEntry {
	/** The public line exactly as supplied. Without a value: an identifier row if it holds 3+ digits, else a statement row. */
	label: string;
	/** The identifier alone, exactly as supplied. */
	value?: string;
	/** Who issued or holds the record, as supplied. */
	issuer?: string;
	/** One short line about what the record covers. */
	description?: string;
	/** Where it can be checked: a registry URL (linked) or plain text, as supplied. */
	source?: string;
}

export interface CredentialLedgerProps {
	heading: string;
	headingLevel?: "h2" | "h3";
	eyebrow?: string;
	/** One sentence beside the heading. */
	intro?: string;
	/** A short provenance note under the intro — never a claim of verification. */
	note?: string;
	entries?: readonly CredentialLedgerEntry[];
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends CredentialLedgerProps {}

const {
	heading,
	headingLevel = "h2",
	eyebrow = "",
	intro = "",
	note = "",
	entries = [],
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,500&family=Public+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const text = (v: unknown) => (typeof v === "string" ? v.trim() : "");
/* A source links only when it is a web address; anything else is shown as text. */
const webHref = (v: string) => (/^https?:\/\/[^\s]+$/i.test(v) ? v : "");
const hostOf = (href: string) => {
	try {
		return new URL(href).host.replace(/^www\./i, "");
	} catch {
		return "";
	}
};

const rows = (entries ?? [])
	.filter((e) => e && text(e.label))
	.map((e) => {
		const source = text(e.source);
		const href = webHref(source);
		const host = href ? hostOf(href) : "";
		const label = text(e.label);
		const value = text(e.value);
		/*
		 * Presentation only — the text is never split, reworded or reformatted.
		 * "record": a label over its value. "identifier": the label already carries
		 * the number (no value, 3+ digits in the label; or the label contains the
		 * value), so the whole label is set once in the value style. "statement":
		 * no number at all.
		 */
		const kind = value ? (label.includes(value) ? "identifier" : "record") : /\d{3,}/.test(label) ? "identifier" : "statement";
		return {
			kind,
			label,
			value,
			issuer: text(e.issuer),
			description: text(e.description),
			source: href && host ? "" : source,
			href: href && host ? href : "",
			host,
		};
	});

const count = rows.length;
const density = count <= 3 ? "roomy" : count >= 5 ? "compact" : "standard";
const Heading = headingLevel;
---

{count > 0 && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section class:list={["crl", className]} id={id} data-density={density} data-cols={count >= 4 ? "2" : "1"}>
	<div class="crl-inner">
		<div class:list={["crl-lead", { "crl-lead--solo": !intro }]}>
			<div class="crl-head">
				{eyebrow && <p class="crl-eyebrow">{eyebrow}</p>}
				<Heading class="crl-title">{heading}</Heading>
			</div>
			{intro && <p class="crl-intro">{intro}</p>}
			{note && <p class="crl-note">{note}</p>}
		</div>
		<ol class="crl-ledger" role="list">
			{rows.map((r) => {
				const hasMeta = Boolean(r.issuer || r.description || r.source || r.href);
				return (
					<li class:list={["crl-entry", { "crl-entry--statement": r.kind === "statement", "crl-entry--identifier": r.kind === "identifier", "crl-entry--solo": !hasMeta }]}>
						{r.kind === "record" ? (
							<dl class="crl-record">
								<dt class="crl-label">{r.label}</dt>
								<dd class="crl-value">{r.value}</dd>
							</dl>
						) : r.kind === "identifier" ? (
							<p class="crl-value crl-value--id">{r.label}</p>
						) : (
							<p class="crl-statement">{r.label}</p>
						)}
						{hasMeta && (
							<div class="crl-meta">
								{r.issuer && <p class="crl-issuer">{r.issuer}</p>}
								{r.description && <p class="crl-desc">{r.description}</p>}
								{r.href ? (
									<p class="crl-source">
										<span class="crl-source-key">Source</span>
										<a class="crl-source-link" href={r.href} target="_blank" rel="noopener noreferrer">
											{r.host}
											<span class="crl-sr"> (opens in a new tab)</span>
											<svg class="crl-arrow" viewBox="0 0 12 12" width="11" height="11" aria-hidden="true" focusable="false"><path d="M3.5 2.5h6v6M9.5 2.5 2.5 9.5" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="square" /></svg>
										</a>
									</p>
								) : r.source ? (
									<p class="crl-source">
										<span class="crl-source-key">Source</span>
										<span class="crl-source-text">{r.source}</span>
									</p>
								) : null}
							</div>
						)}
					</li>
				);
			})}
		</ol>
	</div>
</section>
</>
)}

<style>
	.crl {
		/* Own default palette: cool near-white ground, deep ink, a cobalt accent strong enough to read on the head rule. */
		--crl-surface: var(--crl-bg, #f7f8fa);
		--crl-ink: var(--crl-text, #0f1724);
		--crl-soft: var(--crl-text-muted, #3d4555);
		--crl-accent: var(--crl-brand, #1a47b8);
		--crl-accent-ink: var(--crl-brand-text, #ffffff);
		--crl-line: var(--crl-border, #d3d8e1);
		--crl-font: var(--crl-family, "Public Sans", ui-sans-serif, system-ui, sans-serif);
		--crl-font-display: var(--crl-display, "Source Serif 4", ui-serif, Georgia, serif);

		/* Density: set by the entry count. cqi resolves against the ledger, where these are used. */
		--crl-row-pad: 30px;
		--crl-value-size: clamp(30px, 6.4cqi, 44px);
		/* Statements sit a clear step below the records: same face, ~55% of the value size. */
		--crl-statement-size: max(19px, calc(var(--crl-value-size) * 0.55));

		container-type: inline-size;
		container-name: crl;
		background: var(--crl-surface);
		color: var(--crl-ink);
		font-family: var(--crl-font);
	}

	.crl[data-density="roomy"] {
		--crl-row-pad: 38px;
		--crl-value-size: clamp(32px, 7.4cqi, 52px);
	}

	.crl[data-density="compact"] {
		--crl-row-pad: 22px;
		--crl-value-size: clamp(28px, 5.4cqi, 38px);
	}

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

	.crl-inner {
		display: grid;
		gap: 40px;
		max-width: var(--crl-grid-max, 1232px);
		margin: 0 auto;
		padding: 64px 20px 72px;
	}

	.crl-lead {
		min-width: 0;
	}

	.crl-head {
		min-width: 0;
	}

	/* Sentence case throughout; no tracked capitals anywhere. */
	.crl-eyebrow {
		margin: 0 0 16px;
		color: var(--crl-accent);
		font: 600 15px/1.35 var(--crl-font);
	}

	.crl-title {
		max-width: 15ch;
		margin: 0;
		color: var(--crl-ink);
		font-family: var(--crl-font-display);
		font-size: var(--crl-heading-size, clamp(34px, 5.2cqi, 48px));
		font-weight: 400;
		line-height: 1.06;
		letter-spacing: -0.02em;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.crl-intro {
		max-width: 38ch;
		margin: 20px 0 0;
		color: var(--crl-soft);
		font: 400 17px/1.6 var(--crl-font);
		text-wrap: pretty;
		overflow-wrap: anywhere;
	}

	.crl-note {
		max-width: 38ch;
		margin: 28px 0 0;
		padding-top: 16px;
		border-top: 1px solid var(--crl-line);
		color: var(--crl-soft);
		font: 400 14px/1.55 var(--crl-font);
		font-style: italic;
		overflow-wrap: anywhere;
	}

	/* The ledger: a heavy head rule, hairlines between records. */
	.crl-ledger {
		container-type: inline-size;
		container-name: crl-ledger;
		position: relative;
		min-width: 0;
		margin: 0;
		padding: 0;
		list-style: none;
		border-top: 2px solid var(--crl-ink);
	}

	.crl-ledger::before {
		content: "";
		position: absolute;
		top: -3px;
		left: 0;
		width: 88px;
		height: 4px;
		background: var(--crl-accent);
	}

	.crl-entry {
		display: grid;
		gap: 14px;
		min-width: 0;
		padding-block: var(--crl-row-pad);
		border-bottom: 1px solid var(--crl-line);
	}

	.crl-record {
		display: grid;
		gap: 10px;
		min-width: 0;
		margin: 0;
	}

	/* Sentence case, as supplied: a quiet caption over the value, not a caps tier. */
	.crl-label {
		margin: 0;
		color: var(--crl-soft);
		font: 500 14.5px/1.4 var(--crl-font);
		overflow-wrap: anywhere;
	}

	.crl-value {
		margin: 0;
		color: var(--crl-ink);
		font-family: var(--crl-font-display);
		font-size: var(--crl-value-size);
		font-weight: 400;
		line-height: 1;
		letter-spacing: -0.01em;
		font-variant-numeric: tabular-nums lining-nums;
		font-feature-settings: "tnum" 1, "lnum" 1;
		overflow-wrap: anywhere;
	}

	/* An identifier row: the whole supplied label in the value style, allowed to wrap cleanly. */
	.crl-value--id {
		line-height: 1.06;
		text-wrap: balance;
	}

	/*
	 * A statement row: the label is the record, set flush on the same left edge
	 * as the values and held by the same hairlines — no marker of its own.
	 */
	.crl-statement {
		max-width: 34ch;
		margin: 0;
		color: var(--crl-ink);
		font-family: var(--crl-font-display);
		font-size: var(--crl-statement-size);
		font-weight: 400;
		line-height: 1.2;
		letter-spacing: -0.01em;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.crl-meta {
		display: grid;
		gap: 6px;
		min-width: 0;
		max-width: 46ch;
	}

	.crl-issuer {
		margin: 0;
		color: var(--crl-ink);
		font: 600 15px/1.45 var(--crl-font);
		overflow-wrap: anywhere;
	}

	.crl-desc {
		margin: 0;
		color: var(--crl-soft);
		font: 400 15px/1.55 var(--crl-font);
		text-wrap: pretty;
		overflow-wrap: anywhere;
	}

	.crl-source {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		column-gap: 10px;
		margin: 2px 0 0;
		color: var(--crl-soft);
		font: 400 13.5px/1.45 var(--crl-font);
		overflow-wrap: anywhere;
	}

	.crl-source-key {
		font-style: italic;
	}

	.crl-source-text {
		min-width: 0;
	}

	/* A 44px target from padding cancelled by margin: the row keeps the same line rhythm as a plain-text source. */
	.crl-source-link {
		display: inline-flex;
		align-items: center;
		gap: 5px;
		min-width: 0;
		padding-block: 12px;
		margin-block: -12px;
		color: var(--crl-accent);
		font-weight: 500;
		text-decoration: underline;
		text-decoration-thickness: 1px;
		text-underline-offset: 3px;
		text-decoration-color: color-mix(in srgb, var(--crl-accent) 45%, transparent);
		overflow-wrap: anywhere;
		transition: text-decoration-color 160ms ease;
	}

	.crl-source-link:hover {
		text-decoration-color: currentColor;
	}

	.crl-source-link:focus-visible {
		outline: 2px solid var(--crl-accent);
		outline-offset: 2px;
		border-radius: 2px;
	}

	.crl-arrow {
		flex: none;
	}

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

	/* Ledger wide enough: the record and its details side by side. */
	@container crl-ledger (min-width: 600px) {
		.crl-entry {
			grid-template-columns: minmax(0, 1.08fr) minmax(0, 1fr);
			column-gap: 40px;
			align-items: start;
		}

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

		.crl-meta {
			padding-top: 2px;
		}

		.crl-entry--statement .crl-meta {
			padding-top: 4px;
		}
	}

	/*
	 * From 640px of the section's width: a split header — eyebrow and heading
	 * left; intro with the note directly under it right, top-aligned — over a
	 * full-width ledger.
	 */
	@container crl (min-width: 640px) {
		.crl-inner {
			gap: 48px;
			padding: 80px 40px 88px;
		}

		.crl-lead {
			display: grid;
			grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
			grid-template-rows: auto 1fr;
			column-gap: 40px;
			align-items: start;
		}

		.crl-head {
			grid-row: 1 / span 2;
		}

		.crl-intro {
			margin-top: 4px;
		}

		.crl-note {
			margin-top: 20px;
		}

		/* No intro: nothing to split beside the heading, so the lead stays one column (see .crl-lead--solo). */
		.crl-lead.crl-lead--solo {
			display: block;
		}
	}

	/*
	 * No intro: the note sits directly under the heading. Its rule exists to separate it from an intro; with
	 * none, it would float alone at the top of an empty second column.
	 */
	.crl-lead--solo .crl-note {
		margin-top: 16px;
		padding-top: 0;
		border-top: 0;
	}

	@container crl (min-width: 960px) {
		.crl-inner {
			gap: 56px;
			padding: 104px var(--crl-grid-gutter, 48px) 112px;
		}

		.crl-lead {
			grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
			column-gap: 56px;
		}

		/* The intro and note start close to the heading and run wider, so no dead gap opens between them. */
		.crl-intro {
			max-width: 46ch;
			margin-top: 8px;
			font-size: 18px;
		}

		.crl-note {
			max-width: 46ch;
		}
	}

	/*
	 * Wide section, 4–6 entries: a two-column register. Rows align across the
	 * columns (one grid), each record stacks its value over its details, and
	 * every cell keeps its own hairline. Two or three entries stay one column,
	 * details beside each record.
	 */
	@container crl (min-width: 1100px) {
		.crl[data-cols="2"] .crl-ledger {
			display: grid;
			grid-template-columns: repeat(2, minmax(0, 1fr));
			column-gap: 64px;
		}

		.crl[data-cols="2"] .crl-entry,
		.crl[data-cols="2"] .crl-entry--solo {
			grid-template-columns: minmax(0, 1fr);
			align-content: start;
			row-gap: 16px;
		}

		.crl[data-cols="2"] .crl-meta {
			padding-top: 0;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.crl-source-link {
			transition: none;
		}
	}

	@media (forced-colors: active) {
		.crl-ledger::before {
			background: CanvasText;
			border-color: CanvasText;
		}

		.crl-source-link {
			color: LinkText;
		}
	}
</style>

===== END src/components/CredentialLedger.astro =====

===== FILE (supporting): src/components/CredentialLedger.md =====
# Credential ledger

Verified authority as a ruled record: a split header (heading; one sentence and a provenance note) over a ledger of the business's own licences and registrations — a sentence-case label over the identifier in large tabular figures, with issuer, a short line and a source (a registry link only when one is supplied). Value-less credentials become statement rows. No seals, badges or icons.

## Integration

- A section heading (h2 by default). No entries, no section; designed for 2–6 entries.
- Every label, value, issuer and source renders exactly as supplied. An entry without a value is an identifier row when its label holds a run of 3+ digits (the whole label set in the value style), otherwise a statement row — never an empty number slot. A label that contains its value is shown once, not twice.
- A source that is an http(s) URL renders as a link named by its host (new tab, announced); any other source renders as plain text.
- Container queries on the section's own width: stacked below 640px; split header over a full-width ledger from 640px; from 1100px, 4–6 entries set as a two-column register, 2–3 entries one column with details beside each record.
- Reads --crl-bg, --crl-text, --crl-text-muted, --crl-brand, --crl-brand-text, --crl-border, --crl-family, --crl-display, --crl-grid-max, --crl-grid-gutter and --crl-heading-size.
- Loads Source Serif 4 and Public Sans from Google Fonts by default. Pass fontStylesheet="" on a site that loads its own fonts.

## Provenance

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

===== END src/components/CredentialLedger.md =====

--- 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: not applicable — this component ships no script.

--- 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
“Credential ledger” from my Astro Component Library:
https://astro.baysixmedia.com/components/credential-ledger/

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
Static
Profile
core only
Installation files
1 required + 1 supporting
Verified
Never
Client navigation
Not applicable — ships no script
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 fixture

src/components/library/CredentialLedger.astro

one fileNo imports, no companion files, no packages. Copy CredentialLedger.astro into any Astro project's components folder and render it.

---
/**
 * CredentialLedger (candidate, Step 34)
 *
 * Verified authority set as a record, not a badge wall: a split header
 * (heading on one side; one sentence and an optional note on where the
 * details came from on the other) over a full-width ruled ledger. An entry
 * with a value is a registry line — a quiet sentence-case label over the
 * value set large in tabular figures, with the issuer, a short description
 * and its source beside or under it. An entry with a label and no value
 * ("Bonded for residential work") is a statement row in the same face a clear step
 * below the records, never an empty number slot. A label that already carries its
 * identifier ("USDOT 1234567": no value and a run of 3+ digits, or a label
 * containing its value) is set once, whole and exactly as supplied, in the
 * value style — never split, never printed twice.
 *
 * Truth: every label, value, issuer and source renders exactly as supplied.
 * No default credential, number, issuer, seal, check mark or claim of
 * verification. A source that is an http(s) URL becomes a link named by its
 * host; any other source is shown as plain text. No entries, no section.
 *
 * Responsive (container queries on the section's own width): stacked below
 * 640px; from 640px the header splits over the full-width ledger; from
 * 1100px, 4–6 entries form a two-column register (rows aligned across,
 * value over details in each cell), while 2–3 entries stay one column with
 * details beside each record (from 600px of the ledger's width). Row spacing
 * and value size follow the entry count (2–3 roomy, 4 standard, 5–6 compact).
 *
 * Hooks (read, never declared): --crl-bg, --crl-text, --crl-text-muted,
 * --crl-brand, --crl-brand-text, --crl-border, --crl-family, --crl-display,
 * --crl-grid-max, --crl-grid-gutter, --crl-heading-size. No script.
 */

export interface CredentialLedgerEntry {
	/** The public line exactly as supplied. Without a value: an identifier row if it holds 3+ digits, else a statement row. */
	label: string;
	/** The identifier alone, exactly as supplied. */
	value?: string;
	/** Who issued or holds the record, as supplied. */
	issuer?: string;
	/** One short line about what the record covers. */
	description?: string;
	/** Where it can be checked: a registry URL (linked) or plain text, as supplied. */
	source?: string;
}

export interface CredentialLedgerProps {
	heading: string;
	headingLevel?: "h2" | "h3";
	eyebrow?: string;
	/** One sentence beside the heading. */
	intro?: string;
	/** A short provenance note under the intro — never a claim of verification. */
	note?: string;
	entries?: readonly CredentialLedgerEntry[];
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

interface Props extends CredentialLedgerProps {}

const {
	heading,
	headingLevel = "h2",
	eyebrow = "",
	intro = "",
	note = "",
	entries = [],
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,400;8..60,500&family=Public+Sans:wght@400;500;600&display=swap",
	class: className,
	id,
} = Astro.props;

const text = (v: unknown) => (typeof v === "string" ? v.trim() : "");
/* A source links only when it is a web address; anything else is shown as text. */
const webHref = (v: string) => (/^https?:\/\/[^\s]+$/i.test(v) ? v : "");
const hostOf = (href: string) => {
	try {
		return new URL(href).host.replace(/^www\./i, "");
	} catch {
		return "";
	}
};

const rows = (entries ?? [])
	.filter((e) => e && text(e.label))
	.map((e) => {
		const source = text(e.source);
		const href = webHref(source);
		const host = href ? hostOf(href) : "";
		const label = text(e.label);
		const value = text(e.value);
		/*
		 * Presentation only — the text is never split, reworded or reformatted.
		 * "record": a label over its value. "identifier": the label already carries
		 * the number (no value, 3+ digits in the label; or the label contains the
		 * value), so the whole label is set once in the value style. "statement":
		 * no number at all.
		 */
		const kind = value ? (label.includes(value) ? "identifier" : "record") : /\d{3,}/.test(label) ? "identifier" : "statement";
		return {
			kind,
			label,
			value,
			issuer: text(e.issuer),
			description: text(e.description),
			source: href && host ? "" : source,
			href: href && host ? href : "",
			host,
		};
	});

const count = rows.length;
const density = count <= 3 ? "roomy" : count >= 5 ? "compact" : "standard";
const Heading = headingLevel;
---

{count > 0 && (
<>
{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
<section class:list={["crl", className]} id={id} data-density={density} data-cols={count >= 4 ? "2" : "1"}>
	<div class="crl-inner">
		<div class:list={["crl-lead", { "crl-lead--solo": !intro }]}>
			<div class="crl-head">
				{eyebrow && <p class="crl-eyebrow">{eyebrow}</p>}
				<Heading class="crl-title">{heading}</Heading>
			</div>
			{intro && <p class="crl-intro">{intro}</p>}
			{note && <p class="crl-note">{note}</p>}
		</div>
		<ol class="crl-ledger" role="list">
			{rows.map((r) => {
				const hasMeta = Boolean(r.issuer || r.description || r.source || r.href);
				return (
					<li class:list={["crl-entry", { "crl-entry--statement": r.kind === "statement", "crl-entry--identifier": r.kind === "identifier", "crl-entry--solo": !hasMeta }]}>
						{r.kind === "record" ? (
							<dl class="crl-record">
								<dt class="crl-label">{r.label}</dt>
								<dd class="crl-value">{r.value}</dd>
							</dl>
						) : r.kind === "identifier" ? (
							<p class="crl-value crl-value--id">{r.label}</p>
						) : (
							<p class="crl-statement">{r.label}</p>
						)}
						{hasMeta && (
							<div class="crl-meta">
								{r.issuer && <p class="crl-issuer">{r.issuer}</p>}
								{r.description && <p class="crl-desc">{r.description}</p>}
								{r.href ? (
									<p class="crl-source">
										<span class="crl-source-key">Source</span>
										<a class="crl-source-link" href={r.href} target="_blank" rel="noopener noreferrer">
											{r.host}
											<span class="crl-sr"> (opens in a new tab)</span>
											<svg class="crl-arrow" viewBox="0 0 12 12" width="11" height="11" aria-hidden="true" focusable="false"><path d="M3.5 2.5h6v6M9.5 2.5 2.5 9.5" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="square" /></svg>
										</a>
									</p>
								) : r.source ? (
									<p class="crl-source">
										<span class="crl-source-key">Source</span>
										<span class="crl-source-text">{r.source}</span>
									</p>
								) : null}
							</div>
						)}
					</li>
				);
			})}
		</ol>
	</div>
</section>
</>
)}

<style>
	.crl {
		/* Own default palette: cool near-white ground, deep ink, a cobalt accent strong enough to read on the head rule. */
		--crl-surface: var(--crl-bg, #f7f8fa);
		--crl-ink: var(--crl-text, #0f1724);
		--crl-soft: var(--crl-text-muted, #3d4555);
		--crl-accent: var(--crl-brand, #1a47b8);
		--crl-accent-ink: var(--crl-brand-text, #ffffff);
		--crl-line: var(--crl-border, #d3d8e1);
		--crl-font: var(--crl-family, "Public Sans", ui-sans-serif, system-ui, sans-serif);
		--crl-font-display: var(--crl-display, "Source Serif 4", ui-serif, Georgia, serif);

		/* Density: set by the entry count. cqi resolves against the ledger, where these are used. */
		--crl-row-pad: 30px;
		--crl-value-size: clamp(30px, 6.4cqi, 44px);
		/* Statements sit a clear step below the records: same face, ~55% of the value size. */
		--crl-statement-size: max(19px, calc(var(--crl-value-size) * 0.55));

		container-type: inline-size;
		container-name: crl;
		background: var(--crl-surface);
		color: var(--crl-ink);
		font-family: var(--crl-font);
	}

	.crl[data-density="roomy"] {
		--crl-row-pad: 38px;
		--crl-value-size: clamp(32px, 7.4cqi, 52px);
	}

	.crl[data-density="compact"] {
		--crl-row-pad: 22px;
		--crl-value-size: clamp(28px, 5.4cqi, 38px);
	}

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

	.crl-inner {
		display: grid;
		gap: 40px;
		max-width: var(--crl-grid-max, 1232px);
		margin: 0 auto;
		padding: 64px 20px 72px;
	}

	.crl-lead {
		min-width: 0;
	}

	.crl-head {
		min-width: 0;
	}

	/* Sentence case throughout; no tracked capitals anywhere. */
	.crl-eyebrow {
		margin: 0 0 16px;
		color: var(--crl-accent);
		font: 600 15px/1.35 var(--crl-font);
	}

	.crl-title {
		max-width: 15ch;
		margin: 0;
		color: var(--crl-ink);
		font-family: var(--crl-font-display);
		font-size: var(--crl-heading-size, clamp(34px, 5.2cqi, 48px));
		font-weight: 400;
		line-height: 1.06;
		letter-spacing: -0.02em;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.crl-intro {
		max-width: 38ch;
		margin: 20px 0 0;
		color: var(--crl-soft);
		font: 400 17px/1.6 var(--crl-font);
		text-wrap: pretty;
		overflow-wrap: anywhere;
	}

	.crl-note {
		max-width: 38ch;
		margin: 28px 0 0;
		padding-top: 16px;
		border-top: 1px solid var(--crl-line);
		color: var(--crl-soft);
		font: 400 14px/1.55 var(--crl-font);
		font-style: italic;
		overflow-wrap: anywhere;
	}

	/* The ledger: a heavy head rule, hairlines between records. */
	.crl-ledger {
		container-type: inline-size;
		container-name: crl-ledger;
		position: relative;
		min-width: 0;
		margin: 0;
		padding: 0;
		list-style: none;
		border-top: 2px solid var(--crl-ink);
	}

	.crl-ledger::before {
		content: "";
		position: absolute;
		top: -3px;
		left: 0;
		width: 88px;
		height: 4px;
		background: var(--crl-accent);
	}

	.crl-entry {
		display: grid;
		gap: 14px;
		min-width: 0;
		padding-block: var(--crl-row-pad);
		border-bottom: 1px solid var(--crl-line);
	}

	.crl-record {
		display: grid;
		gap: 10px;
		min-width: 0;
		margin: 0;
	}

	/* Sentence case, as supplied: a quiet caption over the value, not a caps tier. */
	.crl-label {
		margin: 0;
		color: var(--crl-soft);
		font: 500 14.5px/1.4 var(--crl-font);
		overflow-wrap: anywhere;
	}

	.crl-value {
		margin: 0;
		color: var(--crl-ink);
		font-family: var(--crl-font-display);
		font-size: var(--crl-value-size);
		font-weight: 400;
		line-height: 1;
		letter-spacing: -0.01em;
		font-variant-numeric: tabular-nums lining-nums;
		font-feature-settings: "tnum" 1, "lnum" 1;
		overflow-wrap: anywhere;
	}

	/* An identifier row: the whole supplied label in the value style, allowed to wrap cleanly. */
	.crl-value--id {
		line-height: 1.06;
		text-wrap: balance;
	}

	/*
	 * A statement row: the label is the record, set flush on the same left edge
	 * as the values and held by the same hairlines — no marker of its own.
	 */
	.crl-statement {
		max-width: 34ch;
		margin: 0;
		color: var(--crl-ink);
		font-family: var(--crl-font-display);
		font-size: var(--crl-statement-size);
		font-weight: 400;
		line-height: 1.2;
		letter-spacing: -0.01em;
		text-wrap: balance;
		overflow-wrap: anywhere;
	}

	.crl-meta {
		display: grid;
		gap: 6px;
		min-width: 0;
		max-width: 46ch;
	}

	.crl-issuer {
		margin: 0;
		color: var(--crl-ink);
		font: 600 15px/1.45 var(--crl-font);
		overflow-wrap: anywhere;
	}

	.crl-desc {
		margin: 0;
		color: var(--crl-soft);
		font: 400 15px/1.55 var(--crl-font);
		text-wrap: pretty;
		overflow-wrap: anywhere;
	}

	.crl-source {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		column-gap: 10px;
		margin: 2px 0 0;
		color: var(--crl-soft);
		font: 400 13.5px/1.45 var(--crl-font);
		overflow-wrap: anywhere;
	}

	.crl-source-key {
		font-style: italic;
	}

	.crl-source-text {
		min-width: 0;
	}

	/* A 44px target from padding cancelled by margin: the row keeps the same line rhythm as a plain-text source. */
	.crl-source-link {
		display: inline-flex;
		align-items: center;
		gap: 5px;
		min-width: 0;
		padding-block: 12px;
		margin-block: -12px;
		color: var(--crl-accent);
		font-weight: 500;
		text-decoration: underline;
		text-decoration-thickness: 1px;
		text-underline-offset: 3px;
		text-decoration-color: color-mix(in srgb, var(--crl-accent) 45%, transparent);
		overflow-wrap: anywhere;
		transition: text-decoration-color 160ms ease;
	}

	.crl-source-link:hover {
		text-decoration-color: currentColor;
	}

	.crl-source-link:focus-visible {
		outline: 2px solid var(--crl-accent);
		outline-offset: 2px;
		border-radius: 2px;
	}

	.crl-arrow {
		flex: none;
	}

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

	/* Ledger wide enough: the record and its details side by side. */
	@container crl-ledger (min-width: 600px) {
		.crl-entry {
			grid-template-columns: minmax(0, 1.08fr) minmax(0, 1fr);
			column-gap: 40px;
			align-items: start;
		}

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

		.crl-meta {
			padding-top: 2px;
		}

		.crl-entry--statement .crl-meta {
			padding-top: 4px;
		}
	}

	/*
	 * From 640px of the section's width: a split header — eyebrow and heading
	 * left; intro with the note directly under it right, top-aligned — over a
	 * full-width ledger.
	 */
	@container crl (min-width: 640px) {
		.crl-inner {
			gap: 48px;
			padding: 80px 40px 88px;
		}

		.crl-lead {
			display: grid;
			grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
			grid-template-rows: auto 1fr;
			column-gap: 40px;
			align-items: start;
		}

		.crl-head {
			grid-row: 1 / span 2;
		}

		.crl-intro {
			margin-top: 4px;
		}

		.crl-note {
			margin-top: 20px;
		}

		/* No intro: nothing to split beside the heading, so the lead stays one column (see .crl-lead--solo). */
		.crl-lead.crl-lead--solo {
			display: block;
		}
	}

	/*
	 * No intro: the note sits directly under the heading. Its rule exists to separate it from an intro; with
	 * none, it would float alone at the top of an empty second column.
	 */
	.crl-lead--solo .crl-note {
		margin-top: 16px;
		padding-top: 0;
		border-top: 0;
	}

	@container crl (min-width: 960px) {
		.crl-inner {
			gap: 56px;
			padding: 104px var(--crl-grid-gutter, 48px) 112px;
		}

		.crl-lead {
			grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
			column-gap: 56px;
		}

		/* The intro and note start close to the heading and run wider, so no dead gap opens between them. */
		.crl-intro {
			max-width: 46ch;
			margin-top: 8px;
			font-size: 18px;
		}

		.crl-note {
			max-width: 46ch;
		}
	}

	/*
	 * Wide section, 4–6 entries: a two-column register. Rows align across the
	 * columns (one grid), each record stacks its value over its details, and
	 * every cell keeps its own hairline. Two or three entries stay one column,
	 * details beside each record.
	 */
	@container crl (min-width: 1100px) {
		.crl[data-cols="2"] .crl-ledger {
			display: grid;
			grid-template-columns: repeat(2, minmax(0, 1fr));
			column-gap: 64px;
		}

		.crl[data-cols="2"] .crl-entry,
		.crl[data-cols="2"] .crl-entry--solo {
			grid-template-columns: minmax(0, 1fr);
			align-content: start;
			row-gap: 16px;
		}

		.crl[data-cols="2"] .crl-meta {
			padding-top: 0;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.crl-source-link {
			transition: none;
		}
	}

	@media (forced-colors: active) {
		.crl-ledger::before {
			background: CanvasText;
			border-color: CanvasText;
		}

		.crl-source-link {
			color: LinkText;
		}
	}
</style>