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: "Fill-in inquiry"
Library page: https://astro.baysixmedia.com/components/fill-in-inquiry/
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/FillInInquiry.astro =====
---
/**
* FillInInquiry (candidate)
*
* A lead form written as a letter. The fields are not a grid of boxes: they
* are blanks inside a sentence set in the display face — "My name is ____,
* / and you can reply to me at ____." The letter is set one clause per line:
* each line is its lead words and its blank, and the blank's ruled line runs
* on to the letter's right edge with its punctuation, so every line ends on
* the same margin. The real <label> is printed under the rule like a paper
* form's caption; what the visitor types appears in the accent italic, while
* prompts (the empty select, a placeholder) are upright in the text face, so
* the finished request reads back as one letter. A textarea becomes ruled
* writing lines under its lead words. A blank without lead words is led by
* its own label instead ("Property address ____"), so any field set reads
* as a ruled form sheet, one line per field. Under the letter, one send row; under
* that, a postscript band — the direct line set in the display face as a
* closing sentence ("Rather talk it through? Call …"), with any supplied
* details beside it.
*
* Truth rules:
* - The section renders only with a heading. No business fact has a default:
* the phone renders only from a real number (tel: from its digits), the
* email, details, greeting and service select only when passed.
* - The form renders only with a verified `action` (a normal POST form) or a
* non-empty `previewNotice` (PREVIEW MODE: no action attribute; the send is
* disabled and aria-disabled with a padlock; the notice sits beside it as
* role="note"). An action is never guessed; an unsafe one is treated as
* absent. With neither, the section is the heading, the intro and the
* direct-line band.
*
* Accessibility: every blank is a native control with a real <label> (the
* caption); its lead words are linked with aria-describedby, so the sentence
* is heard in forms mode too. Required fields use `required`; optional ones
* are captioned "optional". After the visitor has interacted, an invalid
* blank turns its rule to the error colour and its caption appends the fix
* (":user-invalid"), on top of the browser's own validation on send.
*
* Responsive (container queries on the section's own width):
* - < 520px: the lead words run as text and every blank takes its own full
* line (caption under it), so each control is a full-width touch target.
* - ≥ 520px: each blank follows its lead words and fills the rest of the
* line from a minimum length (short, medium, long); when the words leave
* less than that, the whole blank takes the next line at full width.
* - ≥ 900px: the heading and intro share the top row; the postscript and
* details share the foot.
*
* No script: validation is the browser's own, and a disabled send blocks
* submission. Reduced motion removes every transition.
*
* Hooks read (never declared): --fil-bg, --fil-text, --fil-text-muted,
* --fil-brand, --fil-brand-text, --fil-border, --fil-error, --fil-family,
* --fil-display, --fil-grid-max, --fil-grid-gutter, --fil-heading-size.
*/
export interface FillInField {
/** Submitted under this name. */
name: string;
/** The caption under the blank: the control's accessible label. */
label: string;
type: "text" | "email" | "tel" | "textarea" | "select";
/** The words before the blank. Without them, the label leads the line instead. */
lead?: string;
/** Punctuation straight after the blank ("," or "."). */
after?: string;
required?: boolean;
/** Select options, submitted as written. A select with none is left out. */
options?: string[];
/** A hint, never a label. */
placeholder?: string;
autocomplete?: string;
/** Minimum blank length; the rule grows to the line end. Default: long for email, medium otherwise. */
width?: "short" | "medium" | "long";
}
export interface FillInInquiryProps {
heading: string;
headingLevel?: "h2" | "h3";
eyebrow?: string;
intro?: string;
/** Opening line of the letter ("Hello Cedar Row,"). */
greeting?: string;
/** The direct line. Renders only from a real number; `label` replaces the words before it. */
phone?: { number: string; label?: string } | null;
email?: string;
/** Supporting information from verified facts only (hours, coverage…). */
details?: { label: string; value: string }[];
/** The letter's blanks. Default: name, email, phone (optional), message. */
fields?: FillInField[];
/** The business's own services: adds a "service" blank unless a field is already named "service". */
services?: string[];
serviceLabel?: string;
/** The words before the service blank. */
serviceLead?: string;
submitLabel?: string;
/** The verified form endpoint. Absent: preview mode. */
action?: string;
/** Preview-mode notice text (the build passes it). */
previewNotice?: string;
/** Interface labels; English defaults. */
labels?: {
optional?: string;
choose?: string;
fix?: string;
fixEmail?: string;
postscript?: string;
call?: string;
write?: string;
emailOnly?: string;
callNoForm?: string;
emailNoForm?: string;
notConnected?: string;
};
fontStylesheet?: string;
class?: string;
id?: string;
}
interface Props extends FillInInquiryProps {}
const {
heading,
headingLevel = "h2",
eyebrow = "",
intro = "",
greeting = "",
phone = null,
email = "",
details = [],
fields: fieldsProp,
services = [],
serviceLabel = "Service",
serviceLead = "I'm asking about",
submitLabel = "Send my request",
action: actionProp,
previewNotice = "",
labels = {},
fontStylesheet = "https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..600;1,9..144,300..600&family=Instrument+Sans:wght@400;500;600&display=swap",
class: className,
id,
} = Astro.props as Props;
const L = {
optional: "optional",
choose: "Choose one",
fix: "needs an answer",
fixEmail: "check this address",
postscript: "P.S.",
call: "Rather talk it through? Call",
write: "or write to",
emailOnly: "Rather write directly? Email",
callNoForm: "Call",
emailNoForm: "or email",
notConnected: "Not connected.",
...labels,
};
const SAFE = /^(?:https?:|mailto:|tel:|[#/.?])/i;
const safeHref = (href: string) => (SAFE.test(href.trim()) ? href.trim() : "");
const telHref = (n: string) => {
const digits = n.replace(/\D/g, "");
return digits ? `tel:${n.trim().startsWith("+") ? "+" : ""}${digits}` : "";
};
const number = phone?.number?.trim() ?? "";
const callHref = number ? telHref(number) : "";
/* Shown with no-break spaces, so "(650) 555-0117" never splits; the punctuation after it has no break opportunity either. */
const numberShown = number.replace(/\s+/g, String.fromCharCode(160));
const mail = email.trim();
const mailHref = mail ? safeHref(`mailto:${mail}`) : "";
/* Only an http(s) or relative endpoint; anything else is treated as no endpoint (preview). */
const rawAction = typeof actionProp === "string" ? actionProp.trim() : "";
const action = rawAction && /^(?:https?:|[/.?])/i.test(rawAction) ? rawAction : "";
/* A verified endpoint (live) or the site's preview notice (inert); with neither there is no form. */
const preview = !action && Boolean(previewNotice.trim());
const showForm = Boolean(action) || preview;
const DEFAULT_FIELDS: FillInField[] = [
{ name: "name", label: "Your name", type: "text", lead: "My name is", after: ",", required: true, autocomplete: "name" },
{ name: "email", label: "Email address", type: "email", lead: "and you can reply to me at", after: ".", required: true, autocomplete: "email" },
{ name: "phone", label: "Phone number", type: "tel", lead: "If a call is easier, my number is", after: ".", autocomplete: "tel" },
{ name: "message", label: "Your message", type: "textarea", lead: "Here is what I need:", required: true },
];
const serviceList = (services ?? []).map((s) => (typeof s === "string" ? s.trim() : "")).filter(Boolean);
let list: FillInField[] = (fieldsProp && fieldsProp.length ? fieldsProp : DEFAULT_FIELDS)
.filter((f) => f && typeof f.name === "string" && f.name.trim() && typeof f.label === "string" && f.label.trim())
.map((f) => ({ ...f, name: f.name.trim(), label: f.label.trim(), lead: f.lead?.trim() ?? "", after: f.after?.trim() ?? "", options: (f.options ?? []).map((o) => o.trim()).filter(Boolean) }))
.filter((f) => f.type !== "select" || (f.options?.length ?? 0) > 0);
if (serviceList.length && !list.some((f) => f.name === "service")) {
const at = list.findIndex((f) => f.type === "textarea");
const select: FillInField = { name: "service", label: serviceLabel, type: "select", lead: serviceLead.trim(), after: serviceLead.trim() ? "." : "", options: serviceList };
list = at === -1 ? [...list, select] : [...list.slice(0, at), select, ...list.slice(at)];
}
/* Ids: from the caller's id (identifier characters only) or a random suffix, never from position alone. */
const uid = `${(id ?? "fil").replace(/[^\w-]/g, "") || "fil"}-${Math.random().toString(36).slice(2, 7)}`;
const blanks = list.map((f, i) => ({
...f,
id: `${uid}-f${i}`,
leadId: `${uid}-l${i}`,
width: f.type === "select" || f.type === "textarea" ? "auto" : (f.width ?? (f.type === "email" ? "long" : "medium")),
error: f.type === "email" ? L.fixEmail : L.fix,
}));
const Heading = headingLevel;
const headingId = `${uid}-h`;
const noticeId = `${uid}-note`;
const hasLine = Boolean(callHref || mailHref);
/* A detail value with " · " separators (hours: "Mon–Thu 7am–7pm · Fri 7am–4pm") is set one segment per line, never broken mid-range. */
const detailRows = (details ?? [])
.map((d) => ({ label: d?.label?.trim() ?? "", value: d?.value?.trim() ?? "" }))
.filter((d) => d.label && d.value)
.map((d) => ({ ...d, parts: d.value.split(/\s+·\s+/).filter(Boolean) }));
/* The postscript's words, by what exists: phone and email, phone only, email only; plainer without a form. */
const callWords = phone?.label?.trim() || (showForm ? L.call : L.callNoForm);
const writeWords = callHref ? (showForm ? L.write : L.emailNoForm) : showForm ? L.emailOnly : L.emailNoForm.replace(/^or\s+/i, "").replace(/^\w/, (c) => c.toUpperCase());
const hasFoot = hasLine || detailRows.length > 0;
---
{fontStylesheet && heading?.trim() && <link rel="stylesheet" href={fontStylesheet} />}
{heading?.trim() && (
<section class:list={["fil", { "fil--preview": preview, "fil--noform": !showForm }, className]} id={id}>
<div class="fil-inner">
<header class="fil-head">
<div class="fil-head-main">
{eyebrow && <p class="fil-eyebrow">{eyebrow}</p>}
<Heading class="fil-title" id={headingId}>{heading}</Heading>
</div>
{intro && <p class="fil-intro">{intro}</p>}
</header>
{
showForm && (
<form class="fil-form" method="post" action={action || undefined} aria-labelledby={headingId}>
<div class="fil-letter">
{greeting.trim() && <p class="fil-greeting">{greeting.trim()}</p>}
<div class="fil-prose">
{blanks.map((f) => (
<div class:list={["fil-row", { "fil-row--area": f.type === "textarea", "fil-row--bare": !f.lead }]}>
{f.lead ? (
<span class="fil-lead" id={f.leadId}>{f.lead}</span>
) : (
<label class="fil-lead fil-lead--label" for={f.id}>{f.label}</label>
)}{" "}
<span class:list={["fil-blank", `fil-blank--${f.width}`, { "fil-blank--area": f.type === "textarea" }]}>
{f.type === "textarea" ? (
<textarea class="fil-control fil-control--area" id={f.id} name={f.name} rows="4" required={f.required} placeholder={f.placeholder || undefined} autocomplete={f.autocomplete || undefined} aria-describedby={f.lead ? f.leadId : undefined} />
) : f.type === "select" ? (
<select class="fil-control fil-control--select" id={f.id} name={f.name} required={f.required} autocomplete={f.autocomplete || undefined} aria-describedby={f.lead ? f.leadId : undefined}>
<option value="">{L.choose}</option>
{f.options?.map((o) => (
<option value={o}>{o}</option>
))}
</select>
) : (
<input class="fil-control" id={f.id} name={f.name} type={f.type} size="1" required={f.required} placeholder={f.placeholder || undefined} autocomplete={f.autocomplete || undefined} aria-describedby={f.lead ? f.leadId : undefined} />
)}
{f.after && f.type !== "textarea" && <span class="fil-after" aria-hidden="true">{f.after}</span>}
{f.lead ? (
<label class="fil-cap" for={f.id} data-error={f.error}>
{f.label}
{!f.required && <span class="fil-opt"> · {L.optional}</span>}
</label>
) : (
<span class:list={["fil-cap", { "fil-cap--none": f.required }]} data-error={f.error} aria-hidden="true">
{!f.required && <span class="fil-opt">{L.optional}</span>}
</span>
)}
</span>
</div>
))}
</div>
</div>
<div class="fil-send">
{preview ? (
<button class="fil-submit fil-submit--inert" type="submit" disabled aria-disabled="true" aria-describedby={noticeId}>
<span class="fil-lock" aria-hidden="true" />
<span>{submitLabel}</span>
</button>
) : (
<button class="fil-submit" type="submit">
<span>{submitLabel}</span>
<span class="fil-arrow" aria-hidden="true" />
</button>
)}
{preview && (
<p class="fil-note" role="note" id={noticeId}>
<strong>{L.notConnected}</strong> {previewNotice}
</p>
)}
</div>
</form>
)
}
{
hasFoot && (
<div class:list={["fil-foot", { "fil-foot--details": detailRows.length > 0, "fil-foot--line": hasLine }]}>
{hasLine && (
<p class="fil-ps">
{showForm && <><span class="fil-ps-mark">{L.postscript}</span>{" "}</>}{callHref && <>{callWords}{" "}<a class="fil-number" href={callHref}>{numberShown}</a>{mailHref ? "," : "."}{mailHref ? " " : ""}</>}{mailHref && <>{writeWords}{" "}<a class="fil-mail" href={mailHref}>{mail}</a>.</>}
</p>
)}
{detailRows.length > 0 && (
<dl class="fil-details">
{detailRows.map((d) => (
<div class="fil-detail">
<dt>{d.label}</dt>
<dd>
{d.parts.map((p) => <span class="fil-part">{p}</span>)}
</dd>
</div>
))}
</dl>
)}
</div>
)
}
</div>
</section>
)}
<style>
.fil {
--fil-paper: var(--fil-bg, #f4efe6);
--fil-ink: var(--fil-text, #1b1815);
--fil-soft: var(--fil-text-muted, #544d45);
--fil-accent: var(--fil-brand, #a3341c);
--fil-accent-ink: var(--fil-brand-text, #ffffff);
--fil-line: var(--fil-border, #d8d0c2);
--fil-bad: var(--fil-error, #b3261e);
--fil-font: var(--fil-family, "Instrument Sans", ui-sans-serif, system-ui, sans-serif);
--fil-font-display: var(--fil-display, Fraunces, ui-serif, Georgia, serif);
--fil-rule: color-mix(in srgb, var(--fil-ink) 58%, var(--fil-paper));
--fil-writing: color-mix(in srgb, var(--fil-ink) 28%, var(--fil-paper));
--fil-tint: color-mix(in srgb, var(--fil-accent) 7%, var(--fil-paper));
--fil-field: color-mix(in srgb, var(--fil-ink) 5%, var(--fil-paper));
container: fil / inline-size;
background: var(--fil-paper);
color: var(--fil-ink);
font-family: var(--fil-font);
}
.fil *,
.fil *::before,
.fil *::after {
box-sizing: border-box;
}
.fil-inner {
max-width: var(--fil-grid-max, 1232px);
margin: 0 auto;
padding: 56px 16px 60px;
}
/* ---- the head: eyebrow, heading, intro ---- */
.fil-head {
display: grid;
gap: 16px;
min-width: 0;
}
.fil-head-main {
min-width: 0;
}
.fil-eyebrow {
margin: 0 0 14px;
color: var(--fil-accent);
font: 600 15px/1.4 var(--fil-font);
}
.fil-title {
max-width: 16ch;
margin: 0;
font-family: var(--fil-font-display);
font-size: var(--fil-heading-size, clamp(36px, 1.4rem + 3.4cqi, 66px));
font-weight: 450;
font-optical-sizing: auto;
line-height: 1.02;
letter-spacing: -0.02em;
text-wrap: balance;
overflow-wrap: anywhere;
}
.fil-intro {
max-width: 44ch;
margin: 0;
color: var(--fil-soft);
font: 400 17px/1.6 var(--fil-font);
overflow-wrap: anywhere;
}
/* ---- the letter ---- */
.fil-form {
margin: 40px 0 0;
padding-top: 36px;
border-top: 1px solid var(--fil-ink);
min-width: 0;
}
.fil-letter {
max-width: 25em;
font-family: var(--fil-font-display);
font-size: clamp(22px, 0.75rem + 2cqi, 34px);
font-weight: 350;
font-optical-sizing: auto;
letter-spacing: -0.008em;
min-width: 0;
}
.fil-greeting {
margin: 0 0 0.45em;
font-style: italic;
line-height: 1.3;
overflow-wrap: anywhere;
}
/*
* The letter is set one clause per line: each line is its lead words and its blank, and the blank's
* rule runs on to the letter's right edge with its punctuation, so every line ends on the same
* margin and a full stop can never start a line. Phones: the words, then the blank as a full strip.
*/
.fil-prose {
display: grid;
gap: 16px;
line-height: 1.3;
overflow-wrap: anywhere;
}
.fil-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
column-gap: 0.28em;
min-width: 0;
}
.fil-lead {
flex: 0 1 auto;
min-width: 0;
text-wrap: pretty;
}
.fil-lead--label {
cursor: pointer;
}
.fil-blank {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: end;
flex: 1 1 100%;
min-width: 0;
margin-top: 0.2em;
}
.fil-control {
grid-column: 1;
grid-row: 1;
width: 100%;
min-width: 0;
min-height: 44px;
margin: 0;
padding: 0.06em 0.12em 0.08em;
border: 0;
border-bottom: 2px solid var(--fil-rule);
border-radius: 0;
background: transparent;
color: var(--fil-accent);
font-family: var(--fil-font-display);
font-size: 0.92em;
font-style: italic;
font-weight: 400;
line-height: 1.2;
transition: border-color 0.15s ease, background-color 0.15s ease;
}
.fil-control--select {
appearance: none;
padding-right: 34px;
background-image: linear-gradient(45deg, transparent 50%, var(--fil-ink) 50%), linear-gradient(135deg, var(--fil-ink) 50%, transparent 50%);
background-position: calc(100% - 17px) 55%, calc(100% - 8px) 55%;
background-size: 9px 9px;
background-repeat: no-repeat;
cursor: pointer;
text-overflow: ellipsis;
}
/*
* Prompts are set in the text face, upright, in the ink: they ask. Answers are set in the display
* italic, in the accent: they are the visitor's own words.
*/
.fil-control--select:has(option[value=""]:checked) {
color: var(--fil-ink);
font-family: var(--fil-font);
font-size: 17px;
font-style: normal;
font-weight: 500;
letter-spacing: 0;
}
.fil-control--select option {
color: var(--fil-ink);
font-style: normal;
}
/* The message: ruled writing lines under its lead words. */
.fil-blank--area {
display: grid;
width: 100%;
margin-top: 0.25em;
}
.fil-control--area {
grid-column: 1 / -1;
display: block;
height: calc(1.6em * 4);
min-height: calc(1.6em * 2);
padding: 0 0.12em;
font-size: 0.72em;
line-height: 1.6;
resize: vertical;
background-image: linear-gradient(to bottom, transparent calc(100% - 2px), var(--fil-writing) calc(100% - 2px), var(--fil-writing) calc(100% - 1px), transparent calc(100% - 1px));
background-size: 100% 1.6em;
background-repeat: repeat-y;
background-attachment: local;
background-clip: padding-box;
}
.fil-control::placeholder {
color: var(--fil-soft);
font-family: var(--fil-font);
font-size: 17px;
font-style: normal;
font-weight: 400;
letter-spacing: 0;
opacity: 1;
}
.fil-control:hover {
border-bottom-color: var(--fil-ink);
}
.fil-control:focus-visible {
outline: 3px solid var(--fil-accent);
outline-offset: 3px;
border-bottom-color: var(--fil-accent);
background-color: var(--fil-tint);
}
/* Phones: each blank is a full-width writing strip; its punctuation belongs to the inline sentence only. */
.fil-blank:not(.fil-blank--area) .fil-control {
padding-inline: var(--fil-strip-pad, 0.3em);
background-color: var(--fil-strip, var(--fil-field));
}
.fil-blank:not(.fil-blank--area) .fil-control--select {
padding-right: 34px;
}
.fil-blank .fil-control:focus-visible {
background-color: var(--fil-tint);
}
.fil-after {
display: none;
grid-column: 2;
grid-row: 1;
align-self: end;
padding: 0 0 0.08em 0.02em;
line-height: 1.2;
}
.fil-cap {
grid-column: 1 / -1;
grid-row: 2;
display: block;
padding-top: 6px;
color: var(--fil-ink);
font: 500 15px/1.35 var(--fil-font);
letter-spacing: 0;
}
/* A label-led line has no caption unless it is optional or needs fixing. */
.fil-cap--none {
padding-top: 0;
}
.fil-opt {
color: var(--fil-soft);
font-weight: 400;
}
/* After the visitor has been in a blank: an invalid one says so, in its rule and its caption. */
.fil-blank:has(.fil-control:user-invalid) .fil-control {
border-bottom-color: var(--fil-bad);
}
.fil-blank:has(.fil-control:user-invalid) .fil-cap {
color: var(--fil-bad);
}
.fil-blank:has(.fil-control:user-invalid) .fil-cap {
padding-top: 6px;
}
.fil-blank:has(.fil-control:user-invalid) .fil-cap::after {
content: " — " attr(data-error);
font-weight: 600;
}
.fil-blank:has(.fil-control:user-invalid) .fil-cap--none::after {
content: attr(data-error);
}
/* ---- the send row ---- */
.fil-send {
display: grid;
gap: 14px;
align-items: center;
margin-top: 32px;
min-width: 0;
}
.fil-submit {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 14px;
width: 100%;
min-height: 58px;
padding: 16px 28px;
border: 2px solid var(--fil-ink);
border-radius: 0;
background: var(--fil-ink);
color: var(--fil-paper);
font: 600 17px/1.2 var(--fil-font);
cursor: pointer;
transition: background-color 0.15s ease, border-color 0.15s ease;
}
.fil-submit:not([disabled]):hover {
border-color: var(--fil-accent);
background: var(--fil-accent);
color: var(--fil-accent-ink);
}
.fil-submit:not([disabled]):hover .fil-arrow {
transform: translateX(4px);
}
.fil-submit:focus-visible {
outline: 3px solid var(--fil-accent);
outline-offset: 3px;
}
/* Preview: still the page's strongest shape; the padlock, the note beside it and no pointer say it is not available. */
.fil-submit--inert {
cursor: not-allowed;
}
.fil-note {
margin: 0;
max-width: 46ch;
color: var(--fil-ink);
font: 400 15px/1.5 var(--fil-font);
overflow-wrap: anywhere;
}
.fil-note strong {
color: var(--fil-accent);
font-weight: 600;
}
/* A drawn arrow (no icon font): a shaft and a head. */
.fil-arrow {
position: relative;
flex: none;
width: 18px;
height: 2px;
margin-right: 3px;
background: currentColor;
transition: transform 0.2s ease;
}
.fil-arrow::after {
content: "";
position: absolute;
right: 0;
top: -3.5px;
width: 8px;
height: 8px;
border: solid currentColor;
border-width: 2px 2px 0 0;
transform: rotate(45deg);
}
/* A drawn padlock (a state, not proof): a body and a shackle. */
.fil-lock {
position: relative;
flex: none;
width: 12px;
height: 10px;
margin-top: 5px;
border-radius: 1.5px;
background: currentColor;
}
.fil-lock::before {
content: "";
position: absolute;
left: 2px;
bottom: 9px;
width: 8px;
height: 6px;
border: 2px solid currentColor;
border-bottom: 0;
border-radius: 4px 4px 0 0;
}
/* ---- the foot: the postscript and the details ---- */
.fil-foot {
display: grid;
gap: 28px;
margin-top: 48px;
padding-top: 28px;
border-top: 1px solid var(--fil-line);
min-width: 0;
}
.fil--noform .fil-foot {
margin-top: 36px;
border-top-color: var(--fil-ink);
}
.fil-ps {
margin: 0;
font-family: var(--fil-font-display);
font-size: clamp(22px, 0.9rem + 1.3cqi, 30px);
font-weight: 350;
line-height: 1.45;
overflow-wrap: anywhere;
min-width: 0;
}
.fil-ps-mark {
color: var(--fil-accent);
font-style: italic;
font-weight: 500;
}
.fil-number,
.fil-mail {
color: var(--fil-ink);
text-decoration: underline;
text-decoration-color: var(--fil-accent);
text-decoration-thickness: 2px;
text-underline-offset: 0.18em;
transition: color 0.15s ease;
}
/* Inline, so the punctuation after it stays on its line; the vertical padding makes a 44px+ target without moving the text. */
.fil-number {
padding: 0.3em 0;
font-size: 1.22em;
font-weight: 450;
font-variant-numeric: lining-nums tabular-nums;
}
.fil-mail {
padding: 10px 0;
font-style: italic;
}
.fil-number:hover,
.fil-mail:hover {
color: var(--fil-accent);
}
.fil-number:focus-visible,
.fil-mail:focus-visible {
outline: 3px solid var(--fil-accent);
outline-offset: 3px;
border-radius: 2px;
}
.fil-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 190px), 1fr));
gap: 18px 28px;
align-content: start;
margin: 0;
min-width: 0;
}
.fil-detail {
display: grid;
gap: 3px;
align-content: start;
min-width: 0;
}
.fil-detail dt {
color: var(--fil-soft);
font: 500 14px/1.4 var(--fil-font);
}
.fil-detail dd {
margin: 0;
font: 500 16px/1.45 var(--fil-font);
overflow-wrap: break-word;
}
.fil-part {
display: block;
}
/* ---- wider: each blank finishes its own line ---- */
@container fil (min-width: 520px) {
.fil-inner {
padding: 76px 32px 80px;
}
.fil-prose {
--fil-strip: transparent;
--fil-strip-pad: 0.12em;
gap: 0.3em;
}
/*
* The blank starts at its minimum length and grows to the letter's right edge. When the lead words
* leave less room than that, the whole blank (rule, punctuation and caption together) takes the
* next line at full width; the words never split from their blank mid-clause.
*/
.fil-blank {
flex: 1 1 var(--fil-min, 6em);
margin: 0;
line-height: 1.2;
}
.fil-blank--short {
--fil-min: 4em;
}
.fil-blank--medium {
--fil-min: 6em;
}
.fil-blank--long {
--fil-min: 8.5em;
}
.fil-blank--auto {
flex-basis: auto;
}
.fil-control {
min-height: 0;
height: max(44px, 1.3em);
}
.fil-after {
display: block;
}
.fil-control--select {
max-width: 100%;
}
.fil-cap {
padding-top: 5px;
}
.fil-row--area {
margin-top: 0.25em;
}
.fil-blank--area {
flex-basis: 100%;
margin-top: 0.15em;
}
.fil-blank--area .fil-control {
height: calc(1.6em * 4);
}
.fil-send {
grid-template-columns: auto minmax(0, 1fr);
gap: 16px 28px;
margin-top: 36px;
}
.fil-submit {
width: auto;
min-width: 260px;
}
}
/* ---- desktop: heading beside intro; postscript beside details ---- */
@container fil (min-width: 900px) {
.fil-inner {
padding-block: 104px 108px;
padding-inline: var(--fil-grid-gutter, 48px);
}
.fil-head {
grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
align-items: end;
gap: 56px;
}
.fil-intro {
padding-bottom: 6px;
}
.fil-form {
margin-top: 56px;
padding-top: 48px;
}
.fil-cap {
font-size: 16px;
}
.fil-send {
margin-top: 44px;
}
.fil-foot {
margin-top: 72px;
padding-top: 36px;
}
.fil-foot--line.fil-foot--details {
grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
gap: 56px;
align-items: start;
}
.fil-foot--line.fil-foot--details .fil-details {
grid-template-columns: minmax(0, 1fr);
gap: 16px;
}
.fil-foot--line.fil-foot--details .fil-detail {
grid-template-columns: minmax(0, 7.5em) minmax(0, 1fr);
gap: 16px;
align-items: baseline;
}
}
@media (prefers-reduced-motion: reduce) {
.fil-control,
.fil-submit,
.fil-arrow,
.fil-number,
.fil-mail {
transition: none;
}
}
@media (forced-colors: active) {
.fil-control {
border-bottom-color: CanvasText;
}
.fil-submit {
border: 2px solid ButtonText;
}
.fil-submit[disabled] {
border-color: GrayText;
color: GrayText;
}
.fil-lock {
background: CanvasText;
}
.fil-submit[disabled] .fil-lock {
background: GrayText;
}
}
</style>
===== END src/components/FillInInquiry.astro =====
===== FILE (supporting): src/components/FillInInquiry.md =====
# Fill-in inquiry
A typographic lead form written as a letter: the fields are blanks inside a sentence set large in a display serif, one clause per line with each rule running to the same right margin, the real label printed under it and the visitor's answers in the accent italic; a message becomes ruled writing lines, and a field without lead words is led by its label like a ruled form sheet. One send row under the letter, and the direct line written as a postscript sentence at the foot beside any supplied details. Without a verified endpoint it renders an honest preview mode — full form, disabled send, a readable note.
## Integration
- A section heading (h2 by default); it also names the form.
- Direct line only from a real `phone.number` (tel: from its digits); email, greeting, service blank and details only when passed.
- The form renders only with a verified https or relative `action` (a normal POST form) or with `previewNotice` (preview mode: no action attribute, the send disabled and aria-disabled, the note as role="note"). With neither there is no form — the heading, intro and direct line remain.
- Blanks default to name, email, phone (optional), message, each with English lead words; pass `fields` (with `lead` and `after`) to write your own sentence. A field without `lead` is led by its own label, so a client's field set with no authored words still reads as a ruled form sheet, one line per field. Every blank keeps a real label as its caption; lead words are linked by aria-describedby.
- Container queries: blanks on their own full-width lines below 520px, one clause per line from 520px (the rule fills to the letter's right edge), heading beside intro and postscript beside details from 900px. Reads --fil-* hooks including grid-max, grid-gutter and heading-size.
- No script. Loads Fraunces + Instrument Sans by default — pass fontStylesheet="" when the site loads its fonts.
## Provenance
Staged as candidate `fill-in-inquiry` (Step 28) and admitted through the candidate pipeline. Source type: original. References (direction only): none. Quality history: quality/candidates/records/fill-in-inquiry.json.
===== END src/components/FillInInquiry.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.