--- /** * `/contact/` — build step 8. Spec: docs/01 §`/contact/`, docs/05-backend-spec.md. * * ⚠️ **THE FORM USES NO JAVASCRIPT, AND THAT IS NOT A CONSTRAINT WORKED AROUND — * IT IS THE DESIGN.** A plain `
` to a same-origin path; the * handler answers `303 See Other` to `/contact/received/`. So it works with * script disabled, cannot double-submit on refresh, and never shows the visitor a * raw JSON response. `backend/intake/handler.mjs` carries the reasoning in full. * * Consequences that shape the markup: * - **Validation errors land on `/contact/could-not-send/`**, because a static * page cannot read a query string without script. In practice the browser's * own `required` / `type="email"` / `maxlength` handling catches the real * cases and announces them natively, which is what `docs/05`'s * "errors announced with `role="alert"`" asks for; a server rejection is * almost always a bot, and a bot gets the success page (see the handler). * - **No booking embed — R6.** Parked by Pouya 2026-08-26. `docs/01` asks for a * "reserved slot for an embed", so the slot is `CONTACT.bookingUrl` being * `null`: nothing renders, and when a URL exists the block appears without a * rebuild of this page. **Nothing on this page mentions booking**, because a * page that says "book a call" with no way to book it is worse than one that * says to email. * * ⚠️ **THE RESPONSE-TIME SENTENCE IS A PUBLIC COMMITMENT (§4, Q27) AND MUST READ * IDENTICALLY HERE, IN THE CONFIRMATION EMAIL, AND IN ANY BIO.** It is rendered * from `CONTACT.responseTime`; the handler takes the same string from its * environment. Never retype it, and never soften it to "usually". * * ⚠️ **`NO_RETAINER_NOTICE` AND `CONSENT_TEXT` BOTH SHIP, AND THAT IS NOT * DUPLICATION.** `docs/01` requires the page to carry the notice; `docs/05` * requires the consent the inquirer TICKS to carry it too. One is a statement the * page makes, the other is a thing the inquirer agrees to. Neither is retyped. * * ⚠️ **NO PHONE NUMBER — Q3. §4 verifies "no public phone number".** Render * `CONTACT.phoneFallback` wherever a number would go rather than leaving the slot * visually empty. */ import BaseLayout from '../layouts/BaseLayout.astro'; import Button from '../components/Button.astro'; import ContactBand from '../components/ContactBand.astro'; import Eyebrow from '../components/Eyebrow.astro'; import SectionHeading from '../components/SectionHeading.astro'; import { getImage } from 'astro:assets'; import ogDefault from '../assets/og-portrait.jpg'; import { pageGraph } from '../data/schema'; import { CONTACT, NO_RETAINER_NOTICE } from '../data/site'; import { CONSENT_TEXT, HONEYPOT_FIELD, INTAKE_ACTION, INTAKE_FIELDS, } from '../data/intake'; const ldImage = await getImage({ src: ogDefault, format: 'jpeg', width: 1200, height: 630, }); /* No `Service` node. `/contact/` offers nothing — it is the way in to what the other pages offer, and a `Service` here would duplicate an `@id` that already resolves on `/mediation/`. Person alone, the `/practice/` and `/process/` shape. No `BreadcrumbList`: one hop from the root, no visible trail. */ const graph = pageGraph(new URL(ldImage.src, Astro.site).href); const hintId = (name: string) => `${name}-hint`; --- {/* ---- 1. Hero -------------------------------------------------------- */}
Contact

Start with a confidential call.

The first step is a scheduled call to scope the matter, identify the parties, and run conflicts. Send the form below, or email me directly.

Phone
{ /* Q3: no public number. The fallback fills the slot rather than leaving a labelled row visually empty. */ }
{CONTACT.phoneFallback}
Location
{CONTACT.location}
Response
{CONTACT.responseTime}
{/* ---- 2. What an inquiry does and does not do ------------------------ */}
What an inquiry is, and what it is not.

{NO_RETAINER_NOTICE}

I ask for the other parties and their counsel because I cannot accept an appointment before conflicts are checked, and that check needs names. Please keep the summary short and leave privileged or confidential detail out of it — the call is for that.

What is collected, where it is stored, how long it is kept, and how to have it deleted are set out in the privacy policy.

{/* ---- 3. The intake form -------------------------------------------- */}
Tell me about the matter.
{ /* `novalidate` IS DELIBERATELY ABSENT. The browser's own validation is the only client-side validation on this page, and with no script it is also the only thing that can announce an error inline — which is what `docs/05`'s `role="alert"` item is really asking for. The Lambda re-validates everything regardless; see `src/data/intake.ts`. */ } { INTAKE_FIELDS.map((field) => (
{field.type === 'radio' ? (
{field.label}
{field.options?.map((option) => ( ))}
) : ( <> {field.type === 'select' ? ( ) : field.type === 'textarea' ? (