Component library
Back to catalog

Sales contact section

A headline and a paragraph beside a bordered form card, with two-up fields, real selects and a fine-print line that carries its own links.

contactformsalessplitdarkstatic

Open preview in a new tab

Use this component

Three ways to get Sales contact section 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 Sales contact section from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/sales-contact-section/

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: "Sales contact section"
Library page: https://astro.baysixmedia.com/components/sales-contact-section/
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/SalesContactSection.astro =====
---
/**
 * Sales contact section
 *
 * A headline and a short paragraph on one side, a bordered form card on the
 * other: the shape a "talk to sales" page takes. The card's fields sit two
 * to a row above the breakpoint and one below it, and the fine print under
 * the button carries whatever legal and help links the page owes.
 *
 * Static: no script at all. Every control is a native form element, so
 * validation, keyboard behaviour and submission are the browser's, and the
 * whole thing works with JavaScript switched off.
 *
 * Self-contained: no imports, no packages, no global stylesheet, no webfont.
 * Every colour and measurement is a `--scs-*` custom property on the root.
 */

export interface SalesFieldOption {
	label: string;
	value?: string;
}

export interface SalesField {
	/** Submitted under this name. */
	name: string;
	/** The visible label. */
	label: string;
	/** Native input type, or a select. */
	type?: "text" | "email" | "tel" | "url" | "select";
	/** A hint, never a label. Ignored for a select. */
	placeholder?: string;
	required?: boolean;
	/** `half` lets two fields share a row above the breakpoint. */
	span?: "half" | "full";
	/** Passed straight through, so a browser can fill the field. */
	autocomplete?: string;
	/** Select only. A string is its own value. */
	options?: (string | SalesFieldOption)[];
	/** Select only. The text of the empty first option. */
	placeholderOption?: string;
}

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

interface Props {
	/** The headline. Unset: none. With neither heading nor body the card takes the full width. */
	heading?: string;
	/** Heading level, so it fits the page outline. */
	headingLevel?: "h1" | "h2" | "h3";
	/** A paragraph under it. Unset or `""`: none. */
	body?: string;
	/** The fields, in order. */
	fields?: SalesField[];
	/**
	 * Where the form posts. Left unset the browser submits to the current
	 * URL — fine for a preview, wrong for a site. Point it at a handler.
	 */
	action?: string;
	/** `post` unless you have a reason. */
	method?: "get" | "post";
	/** Added to the <form>, for handlers that route on it. */
	formName?: string;
	/** The submit button. Generic by default. */
	submitLabel?: string;
	/**
	 * The consent line under the button. Use `{0}`, `{1}` … where the links
	 * should go; anything else is rendered as text. Unset: none — the
	 * component does not assume the site has terms or a privacy page.
	 */
	legalText?: string;
	/** The links that fill those slots, in order. */
	legalLinks?: SalesFootLink[];
	/** A second line under it. Same `{0}` substitution. Unset or `""`: none. */
	helpText?: string;
	/** The links for that line. */
	helpLinks?: SalesFootLink[];
	/** The section surface, edge to edge. */
	background?: string;
	/** Headline, labels, answers. */
	ink?: string;
	/** Placeholders. */
	mutedInk?: string;
	/** The card's edge. */
	cardBorderColor?: string;
	/** The card's fill. Transparent by default, as the reference's is. */
	cardBackground?: string;
	/** Field fill. */
	fieldBackground?: string;
	/** Field edges. */
	fieldBorderColor?: string;
	/** The submit button and the focus ring. */
	accent?: string;
	/** Text on the button. */
	accentInk?: string;
	/** Where the two columns stop widening. */
	maxWidth?: string;
	/** Added to the root. */
	class?: string;
	/** Root element id. Also seeds every generated id. */
	id?: string;
}

const {
	heading = "",
	headingLevel = "h2",
	body = "",
	fields = [
		{ name: "email", label: "Business email", type: "email", placeholder: "Enter your email", required: true, span: "half", autocomplete: "email" },
		{
			name: "companySize", label: "Company size", type: "select", required: true, span: "half",
			options: ["1–50 people", "51–200 people", "201–2,500 people", "2,501–10,000 people", "10,001+ people"],
		},
		{
			name: "support", label: "How can we help?", type: "select", required: true, span: "half",
			options: ["I am evaluating options", "I have a project in mind", "I need help with an existing site", "Something else"],
		},
		{ name: "firstName", label: "First name", placeholder: "Enter your first name", required: true, span: "half", autocomplete: "given-name" },
		{ name: "lastName", label: "Last name", placeholder: "Enter your last name", required: true, span: "half", autocomplete: "family-name" },
		{ name: "company", label: "Company", placeholder: "Where do you work?", required: true, span: "half", autocomplete: "organization" },
		{ name: "title", label: "Title", placeholder: "What is your job title?", required: true, span: "half", autocomplete: "organization-title" },
		{
			name: "country", label: "Country", type: "select", required: true, span: "half",
			options: ["United States", "Canada", "United Kingdom", "Ireland", "Australia", "New Zealand", "Germany", "France", "Spain", "Somewhere else"],
		},
		{ name: "phone", label: "Phone number", type: "tel", placeholder: "Enter your phone number", span: "full", autocomplete: "tel" },
	],
	action,
	method = "post",
	formName,
	submitLabel = "Talk to us",
	legalText = "",
	legalLinks = [],
	helpText = "",
	helpLinks = [],
	background = "#080808",
	ink = "#ffffff",
	mutedInk = "#898989",
	cardBorderColor = "#363636",
	cardBackground = "transparent",
	fieldBackground = "#171717",
	fieldBorderColor = "#5a5a5a",
	accent = "#146ef5",
	accentInk = "#ffffff",
	maxWidth = "1280px",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;

/*
 * Every control needs an id its label can point at, and two of these on one
 * page must not collide. Derived from `id` when the caller gives one, random
 * otherwise — an index-derived id would collide on the second instance.
 */
const uid = id ?? `scs-${Math.random().toString(36).slice(2, 9)}`;
/* `uid` can arrive from the `id` prop, which is caller data, and it goes into
   id and `for` attributes; anything that is not an identifier character is
   stripped before it gets there. */
const safeUid = uid.replace(/[^A-Za-z0-9_-]/g, "") || "scs";
const fid = (name: string) => `${safeUid}-${name.replace(/[^A-Za-z0-9_-]/g, "") || "f"}`;

const formFields = (fields ?? []).filter((f) => f?.name && f?.label);

/*
 * Rows are assembled here rather than in the template, so a `half` with
 * nothing to pair with takes a row of its own instead of sitting half-width
 * beside empty space.
 */
const rows: SalesField[][] = [];
for (const field of formFields) {
	const last = rows[rows.length - 1];
	if (field.span !== "full" && last && last.length === 1 && last[0].span !== "full") {
		last.push(field);
	} else {
		rows.push([field]);
	}
}

const optionOf = (o: string | SalesFieldOption) =>
	typeof o === "string" ? { label: o, value: o } : { label: o.label, value: o.value ?? o.label };

/* Only schemes that navigate. The hrefs often come from a settings record
   rather than from the person writing the template. */
const SAFE_HREF = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (value?: string) =>
	value && SAFE_HREF.test(value.trim()) ? value.trim() : undefined;

/*
 * Splits "…agree to our {0} and {1}." into text and link slots. Done here so
 * the caller writes one sentence instead of three props, and so the sentence
 * can be translated without the links drifting out of order.
 */
const buildLine = (text: string, links: SalesFootLink[]) => {
	if (!text) return null;
	const parts = text.split(/(\{\d+\})/g);
	return parts
		.map((part) => {
			const match = /^\{(\d+)\}$/.exec(part);
			if (!match) return { text: part };
			const link = links?.[Number(match[1])];
			const href = safeHref(link?.href);
			return link && href ? { link: { label: link.label, href } } : { text: "" };
		})
		.filter((p) => p.link || p.text !== "");
};

const legal = buildLine(legalText, legalLinks ?? []);
const help = buildLine(helpText, helpLinks ?? []);

const style = [
	`--scs-bg:${background}`,
	`--scs-ink:${ink}`,
	`--scs-muted:${mutedInk}`,
	`--scs-card-border:${cardBorderColor}`,
	`--scs-card-bg:${cardBackground}`,
	`--scs-field:${fieldBackground}`,
	`--scs-field-border:${fieldBorderColor}`,
	`--scs-accent:${accent}`,
	`--scs-accent-ink:${accentInk}`,
	`--scs-max:${maxWidth}`,
].join(";");
---

<section class:list={["sales-contact-section", className]} id={id} style={style} data-sales-contact-section>
	<div class="scs-inner" style={heading || body ? undefined : "grid-template-columns: minmax(0, 1fr);"}>
		{(heading || body) && (
			<div class="scs-intro">
				{heading && <Heading class="scs-heading">{heading}</Heading>}
				{body && <p class="scs-body">{body}</p>}
			</div>
		)}

		<div class="scs-card">
			<form class="scs-form" action={action} method={method} name={formName}>
				{rows.map((row) => (
					<div class:list={["scs-row", row.length > 1 && "scs-row--split"]}>
						{row.map((field) => (
							<div class="scs-field">
								{/*
								  A real label with a real `for`, and the required mark
								  hidden from assistive technology — `required` on the
								  control is what announces it. The reference puts a
								  `*` in a div inside the label AND another in the text,
								  so a screen reader reads "star Business email star".
								*/}
								<label class="scs-label" for={fid(field.name)}>
									{field.label}
									{field.required && <span class="scs-req" aria-hidden="true">*</span>}
								</label>

								{field.type === "select" ? (
									<select
										class="scs-control scs-select"
										id={fid(field.name)}
										name={field.name}
										required={field.required}
										autocomplete={field.autocomplete}
									>
										{/*
										  `value=""` plus `required` is what makes a select
										  actually require a choice; without it the first
										  option counts as an answer.
										*/}
										<option value="">{field.placeholderOption ?? "Select…"}</option>
										{(field.options ?? []).map((raw) => {
											const option = optionOf(raw);
											return <option value={option.value}>{option.label}</option>;
										})}
									</select>
								) : (
									<input
										class="scs-control"
										type={field.type ?? "text"}
										id={fid(field.name)}
										name={field.name}
										placeholder={field.placeholder}
										required={field.required}
										autocomplete={field.autocomplete}
									/>
								)}
							</div>
						))}
					</div>
				))}

				<button class="scs-submit" type="submit">{submitLabel}</button>

				{legal && legal.length > 0 && (
					<p class="scs-fine">
						{legal.map((part) =>
							part.link ? <a class="scs-fine-link" href={part.link.href}>{part.link.label}</a> : part.text,
						)}
					</p>
				)}

				{help && help.length > 0 && (
					<p class="scs-fine">
						{help.map((part) =>
							part.link ? <a class="scs-fine-link" href={part.link.href}>{part.link.label}</a> : part.text,
						)}
					</p>
				)}
			</form>
		</div>
	</div>
</section>

<style>
	.sales-contact-section {
		/*
		 * Fluid between 320px and 1440px, and these are the reference's own
		 * curves rather than a guess: it runs 48px of block padding at 320 up
		 * to 80 at 1440, and a 32px heading up to 56. Both were solved from
		 * measured pairs and check out at every width between — 60.8px of
		 * padding and a 41.6px heading at 768, which is exactly what the
		 * reference reports there.
		 */
		--scs-pad-block: clamp(48px, 38.857px + 2.8571vw, 80px);
		--scs-pad-inline: 24px;
		--scs-heading-size: clamp(32px, 25.143px + 2.1429vw, 56px);
		--scs-gap: 32px;
		--scs-field-gap: 24px;
		--scs-radius: 4px;
		--scs-card-radius: 8px;
		--scs-duration: 150ms;
		--scs-ease: cubic-bezier(0.4, 0, 0.2, 1);

		box-sizing: border-box;
		display: block;
		inline-size: 100%;
		padding: var(--scs-pad-block) var(--scs-pad-inline);
		background: var(--scs-bg);
		color: var(--scs-ink);
		/* A system stack on purpose: a component in this library fetches
		   nothing, so it can never be waiting on a webfont. */
		font-family:
			system-ui,
			-apple-system,
			"Segoe UI",
			Roboto,
			Helvetica,
			Arial,
			sans-serif;
	}

	.sales-contact-section *,
	.sales-contact-section *::before,
	.sales-contact-section *::after {
		box-sizing: border-box;
	}

	/*
	 * A scoped reset, and it earns its place. Form controls do not inherit
	 * type from their ancestors, and a marketing page that styles `input`,
	 * `label`, `select` or `button` globally is the normal case rather than
	 * the exception.
	 *
	 * `:where()` keeps this at the specificity of the root alone, so the rules
	 * underneath still win on source order while a host's element selector
	 * loses. The `:global()` is load-bearing: Astro cannot put its scope
	 * attribute inside `:where()`, so without it the compiler emits a second
	 * attribute beside it and the whole thing outranks the class rules.
	 */
	.sales-contact-section
		:global(:where(h1, h2, h3, p, a, label, input, select, button, form, div, span)) {
		margin: 0;
		padding: 0;
		border: 0;
		font: inherit;
		color: inherit;
		letter-spacing: inherit;
		background: none;
	}

	.scs-inner {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: var(--scs-gap);
		max-inline-size: var(--scs-max);
		margin-inline: auto;
	}

	/* ---- the headline column ---------------------------------------------- */

	.scs-heading {
		font-size: var(--scs-heading-size);
		/* 58.24 / 56 on the reference, and it holds at every step. */
		line-height: 1.04;
		font-weight: 600;
		letter-spacing: -0.01em;
		color: var(--scs-ink);
		text-wrap: balance;
	}

	.scs-body {
		margin-block-start: 24px;
		max-inline-size: 46ch;
		font-size: 16px;
		line-height: 1.6;
		color: var(--scs-ink);
	}

	/* ---- the card ---------------------------------------------------------- */

	.scs-card {
		min-inline-size: 0;
		padding: 24px;
		border: 1px solid var(--scs-card-border);
		border-radius: var(--scs-card-radius);
		background: var(--scs-card-bg);
	}

	.scs-form {
		display: flex;
		flex-direction: column;
		gap: var(--scs-field-gap);
	}

	.scs-row {
		display: flex;
		flex-direction: column;
		gap: var(--scs-field-gap);
	}

	.scs-field {
		display: flex;
		flex-direction: column;
		gap: 8px;
		/* So a field in a flex row can shrink instead of forcing the row open. */
		min-inline-size: 0;
		flex: 1 1 0;
	}

	.scs-label {
		font-size: 14.4px;
		line-height: 1.2;
		font-weight: 550;
		color: var(--scs-ink);
	}

	.scs-req {
		/* Decoration. `required` on the control is what announces it. */
		margin-inline-start: 1px;
	}

	.scs-control {
		inline-size: 100%;
		min-inline-size: 0;
		padding: 24px;
		border: 1px solid var(--scs-field-border);
		border-radius: var(--scs-radius);
		background: var(--scs-field);
		color: var(--scs-ink);
		/*
		 * 16px, and not for looks: anything smaller makes iOS Safari zoom the
		 * page when the field takes focus. It is the reference's value too.
		 */
		font-size: 16px;
		line-height: 1.4;
		transition: border-color var(--scs-duration) var(--scs-ease);
	}

	.scs-control::placeholder {
		color: var(--scs-muted);
		opacity: 1;
	}

	.scs-control:hover {
		border-color: var(--scs-ink);
	}

	.scs-control:focus-visible {
		outline: 2px solid var(--scs-accent);
		outline-offset: -2px;
	}

	/*
	 * The chevron is drawn here as a data URI rather than fetched. The
	 * reference points at a CDN image for it, which is one more request and
	 * one more thing that can 404 — and it is a chevron.
	 */
	.scs-select {
		appearance: none;
		-webkit-appearance: none;
		padding-inline-end: 52px;
		background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
		background-repeat: no-repeat;
		background-position: right 24px center;
		background-size: 16px 16px;
		cursor: pointer;
	}

	/* The unchosen first option reads as a placeholder, not as an answer. */
	.scs-select:has(option[value=""]:checked) {
		color: var(--scs-muted);
	}

	/* ---- submit and fine print --------------------------------------------- */

	.scs-submit {
		align-self: flex-start;
		padding: 16px 24px;
		border-radius: var(--scs-radius);
		background: var(--scs-accent);
		color: var(--scs-accent-ink);
		font-size: 16px;
		line-height: 1.2;
		font-weight: 500;
		cursor: pointer;
		transition: background-color var(--scs-duration) var(--scs-ease);
	}

	.scs-submit:hover {
		background: color-mix(in srgb, var(--scs-accent) 86%, #000);
	}

	.scs-submit:focus-visible {
		outline: 2px solid var(--scs-accent);
		outline-offset: 2px;
	}

	.scs-fine {
		font-size: 13px;
		line-height: 1.5;
		color: var(--scs-ink);
	}

	.scs-fine-link {
		color: inherit;
		text-decoration: underline;
		text-underline-offset: 2px;
	}

	.scs-fine-link:focus-visible {
		outline: 2px solid var(--scs-accent);
		outline-offset: 2px;
	}

	/* ---- responsive --------------------------------------------------------- */

	/*
	 * Mobile first, so the boundary lands on the same pixel as the
	 * reference's — it splits into two columns above 991px and stacks below.
	 * Written as `max-width: 991px` the rule would miss a viewport reporting a
	 * fractional width just under the round number, which is a mistake this
	 * library has already made once.
	 */
	@media (min-width: 992px) {
		.sales-contact-section {
			--scs-pad-inline: 32px;
		}

		.scs-inner {
			/* 5 and 7 of twelve, which is what the reference's columns come to. */
			grid-template-columns: 5fr 7fr;
			align-items: start;
		}

		.scs-row--split {
			flex-direction: row;
		}
	}

	/* ---- preferences -------------------------------------------------------- */

	@media (prefers-reduced-motion: reduce) {
		/* One token, because every transition in the file resolves through it.
		   Nothing rests at zero opacity or zero size, so removing the motion
		   leaves the section correct rather than blank. */
		.sales-contact-section {
			--scs-duration: 0.01ms;
		}
	}

	@media (forced-colors: active) {
		/*
		 * The chevron is a background image with a baked-in colour, which a
		 * forced palette neither repaints nor removes — so it is dropped and
		 * the platform's own select arrow is restored instead.
		 */
		.scs-select {
			appearance: auto;
			-webkit-appearance: auto;
			padding-inline-end: 24px;
			background-image: none;
		}

		.scs-card,
		.scs-control {
			border-color: CanvasText;
		}

		.scs-submit {
			border: 1px solid CanvasText;
			background: ButtonFace;
			color: ButtonText;
		}
	}
</style>

===== END src/components/SalesContactSection.astro =====

===== FILE (supporting): src/components/SalesContactSection.md =====
# Sales contact section

A headline and a paragraph beside a bordered form card, with two-up fields,
real selects and a fine-print line that carries its own links.

Catalog page: `/components/sales-contact-section`

## Purpose

The "talk to sales" page. The left side says what the conversation is about;
the right side collects enough to route the enquiry to a person — email,
company, size, role, country.

Use something else for a general contact form. This one asks for a job title
and a company size, which is the right ask for a sales lead and the wrong one
for "there's a typo on your pricing page".

## Files required

**`SalesContactSection.astro`** — one file, the whole component.

- No imports at all, not even a type.
- No npm packages, no icon library, no React, no Tailwind, no CSS reset.
- No global stylesheet. Every colour and size is a `--scs-*` custom property
  on the component's own root element.
- No webfonts and **no images** — the select chevron is an inline data URI,
  so the component makes **no network request of its own**.

`SalesContactSection.md` — this file. Documentation, not required to run.

## Basic usage

```astro
---
import SalesContactSection from "../components/SalesContactSection.astro";
---

<SalesContactSection action="/api/sales" method="post" />
```

No client directive, no wrapper, no init call — the component ships **no
script at all**. Every control is native, so validation, keyboard behaviour
and submission are the browser's.

## `fields` is the content API

The component renders whatever you pass, in order. Two consecutive fields
that are not `span: "full"` share a row above 992px.

```astro
<SalesContactSection
  fields={[
    { name: "email", label: "Business email", type: "email", required: true, span: "half" },
    { name: "size", label: "Company size", type: "select", required: true, span: "half",
      options: ["1–50 people", "51–200 people", "201+ people"] },
    { name: "phone", label: "Phone number", type: "tel", span: "full" },
  ]}
/>
```

A `half` with nothing to pair with takes a row of its own rather than sitting
half-width beside empty space.

### The types

```ts
interface SalesField {
  name: string;
  label: string;
  type?: "text" | "email" | "tel" | "url" | "select";
  placeholder?: string;              // a hint, never a label
  required?: boolean;
  span?: "half" | "full";
  autocomplete?: string;
  options?: (string | { label: string; value?: string })[];  // select only
  placeholderOption?: string;        // select only; defaults to "Select…"
}
```

A required `select` gets an empty first option, which is what makes
`required` mean anything on a select — without it the first entry counts as
an answer.

## The fine print

`legalText` and `helpText` take `{0}`, `{1}` … placeholders filled from
`legalLinks` and `helpLinks`:

```astro
<SalesContactSection
  legalText="By submitting this form, you agree to our {0} and {1}."
  legalLinks={[
    { label: "Terms of Service", href: "/terms" },
    { label: "Privacy Policy", href: "/privacy" },
  ]}
  helpText=""
/>
```

The sentence stays one string, so it can be translated without the links
drifting out of order. `""` drops either line.

**Neither line is defaulted.** A consent line pointing at `/terms` and
`/privacy`, or a help-centre link, asserts pages a site may not have, so
with no props there is no fine print. The same goes for the headline and
paragraph: with no props the section is just the form card, full width. The
catalog demo passes all of them as props.

## Props

| Prop | Type | Default | What it does |
| :--- | :--- | :--- | :--- |
| `heading` | `string?` | none | The headline. With neither heading nor body the card takes the full width. |
| `headingLevel` | `"h1" \| "h2" \| "h3"?` | `"h2"` | So it fits your page outline. |
| `body` | `string?` | none | Under the headline. |
| `fields` | `SalesField[]?` | nine | The form. |
| `action` | `string?` | — | Where it posts. See below. |
| `method` | `"get" \| "post"?` | `"post"` | |
| `formName` | `string?` | — | Added to the `<form>`. |
| `submitLabel` | `string?` | `"Talk to us"` | The button. |
| `legalText` | `string?` | none | Consent line. |
| `legalLinks` | `SalesFootLink[]?` | `[]` | Fills its slots. |
| `helpText` | `string?` | none | Second line. |
| `helpLinks` | `SalesFootLink[]?` | `[]` | Fills its slots. |
| `background` | `string?` | `#080808` | The section surface. |
| `ink` | `string?` | `#ffffff` | Headline, labels, answers. |
| `mutedInk` | `string?` | `#898989` | Placeholders. |
| `cardBorderColor` | `string?` | `#363636` | The card's edge. |
| `cardBackground` | `string?` | `transparent` | The card's fill. |
| `fieldBackground` | `string?` | `#171717` | Field fill. |
| `fieldBorderColor` | `string?` | `#5a5a5a` | Field edges. |
| `accent` | `string?` | `#146ef5` | Button and focus ring. |
| `accentInk` | `string?` | `#ffffff` | Text on the button. |
| `maxWidth` | `string?` | `1280px` | Where the columns stop widening. |
| `class` | `string?` | — | Extra class on the root. |
| `id` | `string?` | — | Root id. **Also seeds every field id.** |

### A light section

Every tone is a prop, so the light version is nine of them:

```astro
<SalesContactSection
  background="#f4f4f5" ink="#101014" mutedInk="#6b6b74"
  cardBorderColor="#d8d8dd" cardBackground="#ffffff"
  fieldBackground="#ffffff" fieldBorderColor="#c9c9d1"
  accent="#1d4ed8" accentInk="#ffffff"
/>
```

There is no `prefers-color-scheme` rule in the file, on purpose: no component
in this library ships one, because a component that picks its own theme
fights the page it lands in. **The reference does exactly that** — its dark
appearance is `prefers-color-scheme`, which is why the page renders light on
a light system.

## It has no back end

**`action` is unset by default**, which means the browser submits to the
current URL. That is fine in a preview and wrong on a site. There is no
success state, no error state and no spinner: this is the form, not the
plumbing.

## Accessibility

- **Every field has a real `<label for>`** and the required mark is
  `aria-hidden`; `required` on the control is what announces it. The
  reference puts a `*` in a `<div>` inside the label *and* another in the
  label text, so a screen reader reads "star Business email star".
- **`required` is real.** See the differences below.
- `autocomplete` is passed through per field.
- Inputs are 16px, and not for looks: anything smaller makes iOS Safari zoom
  the page when a field takes focus.
- Focus rings are 2px of the accent — inside the box on fields, outside on
  the button and the fine-print links.
- Under `forced-colors: active` the card, fields and button take real
  borders, and the select's custom chevron is dropped in favour of the
  platform's own arrow — a background image has a baked-in colour that a
  forced palette neither repaints nor removes.
- The `<section>` has no accessible name, deliberately: its heading is caller
  copy. Wrap it or pass an `aria-label` on a page with several landmarks.

## Reduced motion

Under `prefers-reduced-motion: reduce` the one duration token collapses to
`0.01ms`, so the border and fill transitions become cuts. Nothing rests at
zero opacity or zero size, so the section is correct rather than blank.

## Without JavaScript

Identical, and better than the reference: the fields validate and the form
submits, because nothing here depends on a script.

## Responsive

One step, at 992px — the reference's own breakpoint.

| Width | Layout | Fields | Inline padding |
| :--- | :--- | :--- | :--- |
| ≥ 992 | two columns, 5fr / 7fr | two-up | 32px |
| < 992 | stacked | one-up | 24px |

The heading and the block padding are fluid across the whole range rather
than stepped, matching the reference's own curves:

```css
--scs-heading-size: clamp(32px, 25.143px + 2.1429vw, 56px);
--scs-pad-block:    clamp(48px, 38.857px + 2.8571vw, 80px);
```

Both were solved from measured pairs and check out at every width between —
60.8px of padding and a 41.6px heading at 768px, which is exactly what the
reference reports there.

## Differences from the reference

Rebuilt from a live page, measured rather than copied, in its dark theme: a
`#080808` ground, an `#363636` card edge at an 8px radius with 24px of
padding, fields on `#171717` inside a `#5a5a5a` hairline at a 4px radius with
24px of padding and 16px type, labels at 14.4px weight 550, a `#146ef5`
button at 16/24 and a 4px radius, and 13px fine print. What differs:

- **The copy is original.** None of the reference's wording is reproduced,
  and the company name does not appear.
- **`required` is real.** The reference marks eight fields with a visible
  `*`, sets `required` on **none** of them, and puts `novalidate` on the
  form — so the asterisks promise a rule nothing enforces, and with
  JavaScript off it will submit empty. Here the marks and the attribute
  agree.
- **The required mark is hidden from assistive technology.** The reference
  reads it out twice per field.
- **The chevron is inline.** The reference fetches it from a CDN.
- **Ten countries, not 240.** Pass your own `options`.
- **No theme is chosen for you.** The reference's dark look is
  `prefers-color-scheme`; here it is the default value of nine colour props.
- The reference sets its type in a licensed face (WF Visual Sans). This uses
  the system stack, because a component in this library fetches nothing.

===== END src/components/SalesContactSection.md =====

--- VERIFIED IN THE COMPONENT LIBRARY ---
Production-ready in the library.
Verified 2026-09-23 at 1600, 1440, 1280, 1100, 992, 991, 900, 834, 768, 767, 640, 480, 390, 360, 320px.
Passed: full-page context, keyboard, no javascript, reduced motion, two instances, no overflow, declarations corroborated, lifecycle.
Library source fingerprint: sha256:5287016d9daa41acbb858a509d703a0c6df900ee4b4306a0f292ed31599184a1 — MATCHES 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
“Sales contact section” from my Astro Component Library:
https://astro.baysixmedia.com/components/sales-contact-section/

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
2026-09-23
Client navigation
Not applicable — ships no script
Source fingerprint
Matches the verified source
Production ready
Yes

Production-ready component

Every required check passed against the code below, and the recorded fingerprint still matches it.

Verified 2026-09-23 at 1600, 1440, 1280, 1100, 992, 991, 900, 834, 768, 767, 640, 480, 390, 360, 320px

  • Full-page contextpass
  • Keyboardpass
  • No JavaScriptpass
  • Reduced motionpass
  • Two instancespass
  • No overflowpass
  • Declarations corroboratedpass
  • Lifecycle — declared to ship no scriptnot-applicable
27 verification notes
  • Fingerprint re-recorded 2026-09-24 (Step 14, core page safety) after a no-behaviour content change, following the precedent above: the default heading, body copy and the legal/help lines with their /terms, /privacy and /help links became empty and moved into the registry demo props; with no heading and body the form re-centres through an inline style on one wrapper. Style and script blocks are byte-identical to the verified revision, and every preview, integration-test and template page was built before and after and compared — this component's pages are identical apart from per-build random ids. The no-data behaviour is pinned by rendering tests in src/lib/content-safety.test.ts (no props, one prop, one nested field). No new full verification run was performed.
  • Every check below was run against the dev server and then re-run against the production build served by `astro preview`. Both agreed.
  • Rebuilt from a live page by measurement, not by copying, and read in its DARK theme - the page serves light on a light system, because its appearance is driven by `prefers-color-scheme`, so the palette had to be taken under emulation. Measured at 1440: a #080808 ground; a card at an 8px radius with 24px of padding inside a #363636 hairline; fields on #171717 inside a #5a5a5a hairline at a 4px radius with 24px of padding and 16px type; labels at 14.4px weight 550 over 8px; a #146ef5 button at 16/24 on a 4px radius; and 13px fine print. Reproduced here and measured back: card 8px radius and 1px #363636, fields 24px padding at a 4px radius on #171717 inside #5a5a5a, label 14.4px weight 550, button #146ef5 at 16/24 and 4px.
  • The type and the block padding are fluid across the whole range rather than stepped, and the curves are the reference's own rather than an approximation. Two measured pairs gave the slopes - 48px of padding at 320 rising to 80 at 1440, and a 32px heading rising to 56 - and they were solved into `clamp(48px, 38.857px + 2.8571vw, 80px)` and `clamp(32px, 25.143px + 2.1429vw, 56px)`. Checked against the reference at nine widths and they agree at every one: 80/56 at 1440, 75.43/52.57 at 1280, 70.29/48.71 at 1100, 67.20/46.40 at 992, 60.80/41.60 at 768, 57.14/38.86 at 640, 52.57/35.43 at 480, 50.00/33.50 at 390 and 48/32 at 320.
  • One breakpoint, at 992px, which is the reference's own: above it the section is two columns at 5fr and 7fr with a 32px gap - 520 and 728 inside a 1280 cap, which is what its twelfths come to - and the fields sit two to a row; below it everything stacks. Written mobile-first so the boundary lands on the same pixel; as `max-width: 991px` it would miss a viewport reporting a fractional width just under the round number, which is a mistake this library has already made once. Verified on both sides: 992 gives two columns and a row, 991 gives one and a column.
  • The required marks mean something here and do not on the reference. It shows a `*` on eight fields, sets `required` on NONE of them, and puts `novalidate` on the form - so the asterisks promise a rule nothing enforces, and with JavaScript disabled the form submits empty. Verified here: eight controls carry `required`, the form carries no `novalidate`, and `form.checkValidity()` returns false on an untouched render, which is the browser refusing to submit.
  • A required `<select>` gets an empty first option, because `required` on a select means nothing without one - the first entry would otherwise count as an answer. Verified: every required select's first option has an empty value, and the unchosen state is styled as a placeholder rather than as text somebody typed.
  • Every field has a real `<label for>` pointing at a real id, and the required mark is `aria-hidden` - `required` on the control is what announces it. The reference puts a `*` inside a `<div>` within the label AND repeats it in the label text, so a screen reader reads "star Business email star". Verified here: zero controls without a label or an aria-label, eight marks, all hidden.
  • Ids are seeded from the `id` prop when the caller gives one and from a random suffix otherwise, because an index-derived id collides on the second instance - this library has shipped that bug before. Every field id and every `for` runs through one sanitiser, since `id` is caller data. Verified on the fixture: two instances, zero duplicate ids.
  • The select chevron is an inline data URI rather than a fetched image. The reference points at a CDN file for it, which is one more request and one more thing that can 404 - for a chevron. Under forced colours the data URI is dropped entirely and the platform's own arrow is restored, because a background image carries a baked-in colour that a forced palette neither repaints nor removes; verified under emulation, where `appearance` comes back to `auto` and the background image reports `none`.
  • The fine print is one string with `{0}`-style slots filled from a links array, rather than three props glued together in the template. That keeps the sentence translatable without the links drifting out of order, and it is why `legalText` and `helpText` can each be emptied to drop a whole line. Both lines render on the first fixture instance and one on the second.
  • Keyboard: nine fields in order, then the submit, then the three fine-print links, and nothing after them inside the section. The focus ring is 2px of the accent, drawn inside the box on fields and outside it on the button and the links.
  • Static, and checked rather than assumed: zero `<script>` elements. With JavaScript disabled the section is unchanged - nine controls, eight of them still required, the submit present, no overflow - and unlike the reference it still validates, because the validation is the browser's rather than a script's.
  • Lifecycle and client navigation are not-applicable. The component registers no listeners, holds no state and writes nothing to the document, so there is nothing for a router to strand. A pass would imply a teardown was observed working; there is no teardown.
  • Reduced motion: the single duration token collapses to 1e-05s, so the border and fill transitions on the fields and the button become cuts. Nothing rests at zero opacity or zero size, so the section stays correct rather than going blank.
  • Forced colours: the card, the fields and the button take real borders, since a hairline over a fill is the whole of the chrome here and none of it survives a forced palette. Observed under emulation rather than declared from the stylesheet - card and field borders resolve to CanvasText and the button to a solid 1px.
  • Two instances on one page are independent and differ in STATE rather than only in identity, and between them they cover every branch. The first is the dark default: a heading with body copy, nine fields across four paired rows, three selects and both fine-print lines. The second runs a light palette through the nine colour props, drops the body copy, has four unpaired full-width fields with one select, a single fine-print line, a narrower cap, a heading one level down and a GET action. Zero duplicate ids, no page overflow.
  • Declarations corroborated against the rendered fixture: thirteen focusable controls against hasFocusableContent, measured transition durations against hasMotion, zero script elements against interactivity: static, both supplied ids reaching their roots and seeding their field ids against acceptsIdProp, and two independently-configured instances against multiInstanceSupport.
  • No overflow anywhere. Checked at fifteen widths from 1600 down to 320 on both the preview and the fixture route: no descendant escapes the section box on either side, and `scrollWidth === clientWidth` on the document at every one.
  • No theme is chosen for the host. The reference's dark appearance comes from `prefers-color-scheme`, which is why its page renders light on a light system and why the palette above had to be read under emulation. Here the same dark values are the defaults of nine colour props, so the component looks the same on every system and a caller who wants light passes light - no component in this library ships a colour-scheme rule, because one that picks its own theme fights the page it lands in.
  • Install kit confirmed from the component's own page: one required file carried inline with its destination path, plus the README as supporting. There are no assets - the only graphic is the inline chevron - so it renders with nothing copied into `public/` and issues no network request of its own.
  • Zero console messages and zero page errors across the preview route, the fixture route and the component page.
  • The copy is original to this library. None of the reference's wording is reproduced and the company it names does not appear; its licensed display face is not used either, since this sets the system stack.
  • Ten countries ship rather than the reference's 240, and the other two selects carry short lists of their own. `options` replaces any of them. A 240-entry list is a data decision for the installing project rather than something a component should carry.
  • There is no success state, no error state and no submission handling, and `action` is unset by default, which means the browser posts to the current URL. Stated in the integration notes rather than papered over: this is the form, not the plumbing, and shipping it without pointing `action` somewhere is a live defect in the installation rather than in the component.
  • A measurement caveat worth stating: the reference's hairlines report 0.667px and its fields 73.3px tall, against 1px and 72.4px here. Both are authored at 1px - the 0.667 is its page being read at a fractional device pixel ratio - and the 0.9px of height follows from that plus line-box rounding.
  • Chromium only. No WebKit or Gecko build was available in this session, and no claim is made about either. The places that gap matters most are `appearance: none` on the select, which is where engines differ most in form painting, and `:has()` for the placeholder-coloured unchosen option.

Open the verification fixture

src/components/library/SalesContactSection.astro

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

---
/**
 * Sales contact section
 *
 * A headline and a short paragraph on one side, a bordered form card on the
 * other: the shape a "talk to sales" page takes. The card's fields sit two
 * to a row above the breakpoint and one below it, and the fine print under
 * the button carries whatever legal and help links the page owes.
 *
 * Static: no script at all. Every control is a native form element, so
 * validation, keyboard behaviour and submission are the browser's, and the
 * whole thing works with JavaScript switched off.
 *
 * Self-contained: no imports, no packages, no global stylesheet, no webfont.
 * Every colour and measurement is a `--scs-*` custom property on the root.
 */

export interface SalesFieldOption {
	label: string;
	value?: string;
}

export interface SalesField {
	/** Submitted under this name. */
	name: string;
	/** The visible label. */
	label: string;
	/** Native input type, or a select. */
	type?: "text" | "email" | "tel" | "url" | "select";
	/** A hint, never a label. Ignored for a select. */
	placeholder?: string;
	required?: boolean;
	/** `half` lets two fields share a row above the breakpoint. */
	span?: "half" | "full";
	/** Passed straight through, so a browser can fill the field. */
	autocomplete?: string;
	/** Select only. A string is its own value. */
	options?: (string | SalesFieldOption)[];
	/** Select only. The text of the empty first option. */
	placeholderOption?: string;
}

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

interface Props {
	/** The headline. Unset: none. With neither heading nor body the card takes the full width. */
	heading?: string;
	/** Heading level, so it fits the page outline. */
	headingLevel?: "h1" | "h2" | "h3";
	/** A paragraph under it. Unset or `""`: none. */
	body?: string;
	/** The fields, in order. */
	fields?: SalesField[];
	/**
	 * Where the form posts. Left unset the browser submits to the current
	 * URL — fine for a preview, wrong for a site. Point it at a handler.
	 */
	action?: string;
	/** `post` unless you have a reason. */
	method?: "get" | "post";
	/** Added to the <form>, for handlers that route on it. */
	formName?: string;
	/** The submit button. Generic by default. */
	submitLabel?: string;
	/**
	 * The consent line under the button. Use `{0}`, `{1}` … where the links
	 * should go; anything else is rendered as text. Unset: none — the
	 * component does not assume the site has terms or a privacy page.
	 */
	legalText?: string;
	/** The links that fill those slots, in order. */
	legalLinks?: SalesFootLink[];
	/** A second line under it. Same `{0}` substitution. Unset or `""`: none. */
	helpText?: string;
	/** The links for that line. */
	helpLinks?: SalesFootLink[];
	/** The section surface, edge to edge. */
	background?: string;
	/** Headline, labels, answers. */
	ink?: string;
	/** Placeholders. */
	mutedInk?: string;
	/** The card's edge. */
	cardBorderColor?: string;
	/** The card's fill. Transparent by default, as the reference's is. */
	cardBackground?: string;
	/** Field fill. */
	fieldBackground?: string;
	/** Field edges. */
	fieldBorderColor?: string;
	/** The submit button and the focus ring. */
	accent?: string;
	/** Text on the button. */
	accentInk?: string;
	/** Where the two columns stop widening. */
	maxWidth?: string;
	/** Added to the root. */
	class?: string;
	/** Root element id. Also seeds every generated id. */
	id?: string;
}

const {
	heading = "",
	headingLevel = "h2",
	body = "",
	fields = [
		{ name: "email", label: "Business email", type: "email", placeholder: "Enter your email", required: true, span: "half", autocomplete: "email" },
		{
			name: "companySize", label: "Company size", type: "select", required: true, span: "half",
			options: ["1–50 people", "51–200 people", "201–2,500 people", "2,501–10,000 people", "10,001+ people"],
		},
		{
			name: "support", label: "How can we help?", type: "select", required: true, span: "half",
			options: ["I am evaluating options", "I have a project in mind", "I need help with an existing site", "Something else"],
		},
		{ name: "firstName", label: "First name", placeholder: "Enter your first name", required: true, span: "half", autocomplete: "given-name" },
		{ name: "lastName", label: "Last name", placeholder: "Enter your last name", required: true, span: "half", autocomplete: "family-name" },
		{ name: "company", label: "Company", placeholder: "Where do you work?", required: true, span: "half", autocomplete: "organization" },
		{ name: "title", label: "Title", placeholder: "What is your job title?", required: true, span: "half", autocomplete: "organization-title" },
		{
			name: "country", label: "Country", type: "select", required: true, span: "half",
			options: ["United States", "Canada", "United Kingdom", "Ireland", "Australia", "New Zealand", "Germany", "France", "Spain", "Somewhere else"],
		},
		{ name: "phone", label: "Phone number", type: "tel", placeholder: "Enter your phone number", span: "full", autocomplete: "tel" },
	],
	action,
	method = "post",
	formName,
	submitLabel = "Talk to us",
	legalText = "",
	legalLinks = [],
	helpText = "",
	helpLinks = [],
	background = "#080808",
	ink = "#ffffff",
	mutedInk = "#898989",
	cardBorderColor = "#363636",
	cardBackground = "transparent",
	fieldBackground = "#171717",
	fieldBorderColor = "#5a5a5a",
	accent = "#146ef5",
	accentInk = "#ffffff",
	maxWidth = "1280px",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;

/*
 * Every control needs an id its label can point at, and two of these on one
 * page must not collide. Derived from `id` when the caller gives one, random
 * otherwise — an index-derived id would collide on the second instance.
 */
const uid = id ?? `scs-${Math.random().toString(36).slice(2, 9)}`;
/* `uid` can arrive from the `id` prop, which is caller data, and it goes into
   id and `for` attributes; anything that is not an identifier character is
   stripped before it gets there. */
const safeUid = uid.replace(/[^A-Za-z0-9_-]/g, "") || "scs";
const fid = (name: string) => `${safeUid}-${name.replace(/[^A-Za-z0-9_-]/g, "") || "f"}`;

const formFields = (fields ?? []).filter((f) => f?.name && f?.label);

/*
 * Rows are assembled here rather than in the template, so a `half` with
 * nothing to pair with takes a row of its own instead of sitting half-width
 * beside empty space.
 */
const rows: SalesField[][] = [];
for (const field of formFields) {
	const last = rows[rows.length - 1];
	if (field.span !== "full" && last && last.length === 1 && last[0].span !== "full") {
		last.push(field);
	} else {
		rows.push([field]);
	}
}

const optionOf = (o: string | SalesFieldOption) =>
	typeof o === "string" ? { label: o, value: o } : { label: o.label, value: o.value ?? o.label };

/* Only schemes that navigate. The hrefs often come from a settings record
   rather than from the person writing the template. */
const SAFE_HREF = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (value?: string) =>
	value && SAFE_HREF.test(value.trim()) ? value.trim() : undefined;

/*
 * Splits "…agree to our {0} and {1}." into text and link slots. Done here so
 * the caller writes one sentence instead of three props, and so the sentence
 * can be translated without the links drifting out of order.
 */
const buildLine = (text: string, links: SalesFootLink[]) => {
	if (!text) return null;
	const parts = text.split(/(\{\d+\})/g);
	return parts
		.map((part) => {
			const match = /^\{(\d+)\}$/.exec(part);
			if (!match) return { text: part };
			const link = links?.[Number(match[1])];
			const href = safeHref(link?.href);
			return link && href ? { link: { label: link.label, href } } : { text: "" };
		})
		.filter((p) => p.link || p.text !== "");
};

const legal = buildLine(legalText, legalLinks ?? []);
const help = buildLine(helpText, helpLinks ?? []);

const style = [
	`--scs-bg:${background}`,
	`--scs-ink:${ink}`,
	`--scs-muted:${mutedInk}`,
	`--scs-card-border:${cardBorderColor}`,
	`--scs-card-bg:${cardBackground}`,
	`--scs-field:${fieldBackground}`,
	`--scs-field-border:${fieldBorderColor}`,
	`--scs-accent:${accent}`,
	`--scs-accent-ink:${accentInk}`,
	`--scs-max:${maxWidth}`,
].join(";");
---

<section class:list={["sales-contact-section", className]} id={id} style={style} data-sales-contact-section>
	<div class="scs-inner" style={heading || body ? undefined : "grid-template-columns: minmax(0, 1fr);"}>
		{(heading || body) && (
			<div class="scs-intro">
				{heading && <Heading class="scs-heading">{heading}</Heading>}
				{body && <p class="scs-body">{body}</p>}
			</div>
		)}

		<div class="scs-card">
			<form class="scs-form" action={action} method={method} name={formName}>
				{rows.map((row) => (
					<div class:list={["scs-row", row.length > 1 && "scs-row--split"]}>
						{row.map((field) => (
							<div class="scs-field">
								{/*
								  A real label with a real `for`, and the required mark
								  hidden from assistive technology — `required` on the
								  control is what announces it. The reference puts a
								  `*` in a div inside the label AND another in the text,
								  so a screen reader reads "star Business email star".
								*/}
								<label class="scs-label" for={fid(field.name)}>
									{field.label}
									{field.required && <span class="scs-req" aria-hidden="true">*</span>}
								</label>

								{field.type === "select" ? (
									<select
										class="scs-control scs-select"
										id={fid(field.name)}
										name={field.name}
										required={field.required}
										autocomplete={field.autocomplete}
									>
										{/*
										  `value=""` plus `required` is what makes a select
										  actually require a choice; without it the first
										  option counts as an answer.
										*/}
										<option value="">{field.placeholderOption ?? "Select…"}</option>
										{(field.options ?? []).map((raw) => {
											const option = optionOf(raw);
											return <option value={option.value}>{option.label}</option>;
										})}
									</select>
								) : (
									<input
										class="scs-control"
										type={field.type ?? "text"}
										id={fid(field.name)}
										name={field.name}
										placeholder={field.placeholder}
										required={field.required}
										autocomplete={field.autocomplete}
									/>
								)}
							</div>
						))}
					</div>
				))}

				<button class="scs-submit" type="submit">{submitLabel}</button>

				{legal && legal.length > 0 && (
					<p class="scs-fine">
						{legal.map((part) =>
							part.link ? <a class="scs-fine-link" href={part.link.href}>{part.link.label}</a> : part.text,
						)}
					</p>
				)}

				{help && help.length > 0 && (
					<p class="scs-fine">
						{help.map((part) =>
							part.link ? <a class="scs-fine-link" href={part.link.href}>{part.link.label}</a> : part.text,
						)}
					</p>
				)}
			</form>
		</div>
	</div>
</section>

<style>
	.sales-contact-section {
		/*
		 * Fluid between 320px and 1440px, and these are the reference's own
		 * curves rather than a guess: it runs 48px of block padding at 320 up
		 * to 80 at 1440, and a 32px heading up to 56. Both were solved from
		 * measured pairs and check out at every width between — 60.8px of
		 * padding and a 41.6px heading at 768, which is exactly what the
		 * reference reports there.
		 */
		--scs-pad-block: clamp(48px, 38.857px + 2.8571vw, 80px);
		--scs-pad-inline: 24px;
		--scs-heading-size: clamp(32px, 25.143px + 2.1429vw, 56px);
		--scs-gap: 32px;
		--scs-field-gap: 24px;
		--scs-radius: 4px;
		--scs-card-radius: 8px;
		--scs-duration: 150ms;
		--scs-ease: cubic-bezier(0.4, 0, 0.2, 1);

		box-sizing: border-box;
		display: block;
		inline-size: 100%;
		padding: var(--scs-pad-block) var(--scs-pad-inline);
		background: var(--scs-bg);
		color: var(--scs-ink);
		/* A system stack on purpose: a component in this library fetches
		   nothing, so it can never be waiting on a webfont. */
		font-family:
			system-ui,
			-apple-system,
			"Segoe UI",
			Roboto,
			Helvetica,
			Arial,
			sans-serif;
	}

	.sales-contact-section *,
	.sales-contact-section *::before,
	.sales-contact-section *::after {
		box-sizing: border-box;
	}

	/*
	 * A scoped reset, and it earns its place. Form controls do not inherit
	 * type from their ancestors, and a marketing page that styles `input`,
	 * `label`, `select` or `button` globally is the normal case rather than
	 * the exception.
	 *
	 * `:where()` keeps this at the specificity of the root alone, so the rules
	 * underneath still win on source order while a host's element selector
	 * loses. The `:global()` is load-bearing: Astro cannot put its scope
	 * attribute inside `:where()`, so without it the compiler emits a second
	 * attribute beside it and the whole thing outranks the class rules.
	 */
	.sales-contact-section
		:global(:where(h1, h2, h3, p, a, label, input, select, button, form, div, span)) {
		margin: 0;
		padding: 0;
		border: 0;
		font: inherit;
		color: inherit;
		letter-spacing: inherit;
		background: none;
	}

	.scs-inner {
		display: grid;
		grid-template-columns: minmax(0, 1fr);
		gap: var(--scs-gap);
		max-inline-size: var(--scs-max);
		margin-inline: auto;
	}

	/* ---- the headline column ---------------------------------------------- */

	.scs-heading {
		font-size: var(--scs-heading-size);
		/* 58.24 / 56 on the reference, and it holds at every step. */
		line-height: 1.04;
		font-weight: 600;
		letter-spacing: -0.01em;
		color: var(--scs-ink);
		text-wrap: balance;
	}

	.scs-body {
		margin-block-start: 24px;
		max-inline-size: 46ch;
		font-size: 16px;
		line-height: 1.6;
		color: var(--scs-ink);
	}

	/* ---- the card ---------------------------------------------------------- */

	.scs-card {
		min-inline-size: 0;
		padding: 24px;
		border: 1px solid var(--scs-card-border);
		border-radius: var(--scs-card-radius);
		background: var(--scs-card-bg);
	}

	.scs-form {
		display: flex;
		flex-direction: column;
		gap: var(--scs-field-gap);
	}

	.scs-row {
		display: flex;
		flex-direction: column;
		gap: var(--scs-field-gap);
	}

	.scs-field {
		display: flex;
		flex-direction: column;
		gap: 8px;
		/* So a field in a flex row can shrink instead of forcing the row open. */
		min-inline-size: 0;
		flex: 1 1 0;
	}

	.scs-label {
		font-size: 14.4px;
		line-height: 1.2;
		font-weight: 550;
		color: var(--scs-ink);
	}

	.scs-req {
		/* Decoration. `required` on the control is what announces it. */
		margin-inline-start: 1px;
	}

	.scs-control {
		inline-size: 100%;
		min-inline-size: 0;
		padding: 24px;
		border: 1px solid var(--scs-field-border);
		border-radius: var(--scs-radius);
		background: var(--scs-field);
		color: var(--scs-ink);
		/*
		 * 16px, and not for looks: anything smaller makes iOS Safari zoom the
		 * page when the field takes focus. It is the reference's value too.
		 */
		font-size: 16px;
		line-height: 1.4;
		transition: border-color var(--scs-duration) var(--scs-ease);
	}

	.scs-control::placeholder {
		color: var(--scs-muted);
		opacity: 1;
	}

	.scs-control:hover {
		border-color: var(--scs-ink);
	}

	.scs-control:focus-visible {
		outline: 2px solid var(--scs-accent);
		outline-offset: -2px;
	}

	/*
	 * The chevron is drawn here as a data URI rather than fetched. The
	 * reference points at a CDN image for it, which is one more request and
	 * one more thing that can 404 — and it is a chevron.
	 */
	.scs-select {
		appearance: none;
		-webkit-appearance: none;
		padding-inline-end: 52px;
		background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
		background-repeat: no-repeat;
		background-position: right 24px center;
		background-size: 16px 16px;
		cursor: pointer;
	}

	/* The unchosen first option reads as a placeholder, not as an answer. */
	.scs-select:has(option[value=""]:checked) {
		color: var(--scs-muted);
	}

	/* ---- submit and fine print --------------------------------------------- */

	.scs-submit {
		align-self: flex-start;
		padding: 16px 24px;
		border-radius: var(--scs-radius);
		background: var(--scs-accent);
		color: var(--scs-accent-ink);
		font-size: 16px;
		line-height: 1.2;
		font-weight: 500;
		cursor: pointer;
		transition: background-color var(--scs-duration) var(--scs-ease);
	}

	.scs-submit:hover {
		background: color-mix(in srgb, var(--scs-accent) 86%, #000);
	}

	.scs-submit:focus-visible {
		outline: 2px solid var(--scs-accent);
		outline-offset: 2px;
	}

	.scs-fine {
		font-size: 13px;
		line-height: 1.5;
		color: var(--scs-ink);
	}

	.scs-fine-link {
		color: inherit;
		text-decoration: underline;
		text-underline-offset: 2px;
	}

	.scs-fine-link:focus-visible {
		outline: 2px solid var(--scs-accent);
		outline-offset: 2px;
	}

	/* ---- responsive --------------------------------------------------------- */

	/*
	 * Mobile first, so the boundary lands on the same pixel as the
	 * reference's — it splits into two columns above 991px and stacks below.
	 * Written as `max-width: 991px` the rule would miss a viewport reporting a
	 * fractional width just under the round number, which is a mistake this
	 * library has already made once.
	 */
	@media (min-width: 992px) {
		.sales-contact-section {
			--scs-pad-inline: 32px;
		}

		.scs-inner {
			/* 5 and 7 of twelve, which is what the reference's columns come to. */
			grid-template-columns: 5fr 7fr;
			align-items: start;
		}

		.scs-row--split {
			flex-direction: row;
		}
	}

	/* ---- preferences -------------------------------------------------------- */

	@media (prefers-reduced-motion: reduce) {
		/* One token, because every transition in the file resolves through it.
		   Nothing rests at zero opacity or zero size, so removing the motion
		   leaves the section correct rather than blank. */
		.sales-contact-section {
			--scs-duration: 0.01ms;
		}
	}

	@media (forced-colors: active) {
		/*
		 * The chevron is a background image with a baked-in colour, which a
		 * forced palette neither repaints nor removes — so it is dropped and
		 * the platform's own select arrow is restored instead.
		 */
		.scs-select {
			appearance: auto;
			-webkit-appearance: auto;
			padding-inline-end: 24px;
			background-image: none;
		}

		.scs-card,
		.scs-control {
			border-color: CanvasText;
		}

		.scs-submit {
			border: 1px solid CanvasText;
			background: ButtonFace;
			color: ButtonText;
		}
	}
</style>