Files
diana/specification/38-lead-capture-spec.md

31 KiB
Raw Blame History

38 — Lead Capture Specification

HER — Home Enhancement and Renovation

Status: Phase 3 — implementation-ready
Implements: FR-1, FR-2, FR-3, FR-4, FR-5, FR-6, FR-7 | AC-1, AC-6 | ADR-002
Authority: This document is the authoritative spec for all form field definitions, validation rules, error states, spam protection, and API integration. 33-frontend-spec.md §5.6 governs page layout and visual styling; where the two documents overlap, this document takes precedence for behavior and copy.


Table of Contents

  1. Form Fields
  2. Validation Rules
  3. Form UX States
  4. UX Copy
  5. AWS SES Integration
  6. Spam Protection
  7. Accessibility
  8. Mobile Considerations
  9. Thank-You / Confirmation
  10. Requirements Traceability

1. Form Fields

1.1 Field Inventory

The form collects five visible required fields plus one hidden honeypot field. All visible fields use the FormField component (see 33-frontend-spec.md §6.7).

# Field name Label HTML type name attr id attr autocomplete Required
1 Full Name Full name text name field-name name Yes
2 Service Address Service address text address field-address off Yes
3 Phone Phone number tel phone field-phone tel Yes
4 Email Email address email email field-email email Yes
5 Project Description Describe the work textarea description field-description off Yes
Honeypot (none — hidden) text website field-website off No

Note on autocomplete="off" for address: Browser autofill for street-address fills a billing/home address, which may differ from the service location. Using off prevents misplaced autofill. This is intentional.

1.2 Field Details

Full Name

  • Label: Full name
  • Placeholder: e.g. Jane Smith
  • Help text: (none)

Service Address

  • Label: Service address
  • Placeholder: e.g. 123 Market St, San Francisco, CA
  • Help text (visible below label, above input): "Where is the work located? City or full address."
  • Style: Help text rendered as <p class="field-hint"> in DM Sans 400, --text-sm, --col-slate. Sits between label and input, --space-1 margin below.

Phone Number

  • Label: Phone number
  • Placeholder: e.g. (415) 555-0123
  • Help text: (none)
  • Input mode: inputmode="tel"

Email Address

  • Label: Email address
  • Placeholder: e.g. jane@example.com
  • Help text: (none)
  • Input mode: inputmode="email"

Project Description

  • Label: Describe the work
  • Placeholder: What needs to be fixed, replaced, or improved? Include any relevant details — size, urgency, or special requirements.
  • Help text: (none; placeholder is the primary guide)
  • Element: <textarea>, resizable vertically only (resize: vertical)
  • Min height: 120px on mobile, 160px on desktop (md and above)

Honeypot (hidden)

  • Label: None — never rendered in the visible DOM
  • Name / ID: name="website", id="field-website"
  • Hidden via CSS (see §6 for exact CSS; do not use type="hidden" — bots fill visible fields, not type="hidden" ones)
  • Expected value at submission: empty string
  • Behavior if non-empty: silently discard on client; Lambda also discards and returns { "ok": true } (see §5)

1.3 Field Order

The canonical field order — used as DOM order, mobile stacking order, and keyboard tab order — is:

Full Name → Service Address → Phone → Email → Project Description → [Submit]

Desktop layout note: 33-frontend-spec.md §5.6 specifies a two-column desktop layout where Name and Phone appear side-by-side in Row 1. This visual grouping is achieved with CSS Grid order properties, not by reordering the DOM. The DOM order defined above must be preserved for correct tab/focus sequence.

Desktop CSS grid layout (at md breakpoint and above):

[ Full Name (50%)     ] [ Phone (50%)          ]   ← visual row 1 (CSS order: Name=1, Phone=3)
[ Service Address (100%)                        ]   ← visual row 2 (CSS order: Address=2)
[ Email Address (100%)                          ]   ← visual row 3 (CSS order: Email=4)
[ Project Description (100%)                    ]   ← visual row 4 (CSS order: Description=5)
[                       Submit →                ]   ← visual row 5

DOM order remains: Name, Address, Phone, Email, Description — so keyboard navigation flows logically even with visual rearrangement.

1.4 Submit Button

Property Value
Label Send My Request
Component Button (primary variant — see 33-frontend-spec.md §6.1)
Mobile width Full-width (100%)
Desktop width Auto, min-width 200px, right-aligned within form container
Disabled state While submitting: aria-disabled="true", shows spinner (see §3)

2. Validation Rules

2.1 Validation Strategy

  • Client-side: HTML5 native attributes (required, minlength, maxlength, pattern) plus JavaScript for on-blur validation and submit-time validation. Do not rely on browser default validation UI — suppress with novalidate on the <form> element and handle all error rendering in JavaScript.
  • Server-side (Lambda): Re-validate all fields independently. Never trust client-only validation.
  • Error display: Inline error messages rendered below each field, inside a <p role="alert"> element that is always present in the DOM (even when empty). Field errors appear on blur (after first touch) and on submit attempt.

2.2 Per-Field Validation Rules

Field Required Min length Max length Pattern Error message
Full Name Yes 2 100 (none beyond length) "Please enter your name."
Service Address Yes 3 200 (none beyond length) "Please enter the service address or city."
Phone Yes US phone (see §2.3) "Please enter a valid phone number."
Email Yes Standard email (see §2.3) "Please enter a valid email address."
Project Description Yes 10 2000 (none beyond length) "Please describe your project (at least 10 characters)."
Honeypot No Must be empty (no error shown — silent discard)

2.3 Pattern Definitions

Phone (name="phone"):

^\+?[\d\s\-().]{7,20}$

Allows: digits, spaces, hyphens, parentheses, plus sign. Minimum 7 characters (shortest valid phone), maximum 20. Accepts US and international formats:

  • (415) 555-0123
  • 415-555-0123
  • 4155550123
  • +1 415 555 0123

Email (name="email"): Use the type="email" browser built-in pattern, supplemented by JS check that the value contains at least one @ and at least one . after the @. Do not write an overly strict regex — the AWS SES delivery failure is the ultimate validator for undeliverable addresses.

2.4 Validation Timing

Trigger Behavior
First focus (untouched field) No validation; field is in default state
Blur (after first touch) Validate field; show error if invalid
Input (after field has been touched and errored) Re-validate on each keystroke; clear error as soon as valid
Submit button click Validate all fields; show all errors simultaneously; move focus to first invalid field
Successful submission No field validation (form is replaced or disabled)

2.5 Character Counter (Description Field)

Display a live character counter below the description textarea showing N / 2000 characters. Counter updates on every keystroke.

  • Style: DM Sans 400, --text-xs, --col-slate, right-aligned below textarea.
  • Warning state (≥ 1800 characters): color changes to #B5622A (--col-copper).
  • Over-limit state (> 2000 characters): color #C0392B (error red), counter reads 2000 / 2000 — the maxlength HTML attribute enforces the hard cap and prevents additional input.

3. Form UX States

The form operates as a client-side state machine with six distinct states.

3.1 State Definitions

Default

  • All fields empty, no errors shown.
  • Submit button enabled, label "Send My Request".
  • Character counter shows 0 / 2000.
  • Form element: aria-busy="false".

Touched / Dirty (Per-Field)

  • Validation runs on blur for each field independently.
  • If invalid: inline error message appears below the field (role="alert"), field border changes to error red (#C0392B), aria-invalid="true" set on the input.
  • If valid: error message cleared, border returns to default (--col-chalk), aria-invalid="false".
  • Other fields are unaffected.

Submitting

  • Triggered when user clicks submit and all fields pass client-side validation.
  • Submit button: spinner replaces button text, aria-disabled="true", visually disabled (opacity 0.7, cursor: not-allowed). Do not use disabled attribute — this removes the element from tab order.
  • All form fields: disabled attribute added (prevents editing during in-flight request).
  • Form element: aria-busy="true".
  • No duplicate submissions possible while in this state.
  • Spinner: 20×20px animated SVG ring, --col-linen color on copper button background.

Success

  • Triggered on HTTP 200 response with { "ok": true }.
  • The form element is hidden (display: none or removed from DOM).
  • The FormFeedback (success) component is rendered in its place (see §4.1 for exact copy).
  • Focus is moved programmatically to the success message heading (<h2> or <h3> within the feedback block).
  • The success component has role="status" and aria-live="polite" (see §7).

Error — Network / Server

  • Triggered on: HTTP non-200 response, network failure, { "ok": false } in response body, or unhandled exception.
  • Form fields are re-enabled (remove disabled).
  • Submit button returns to normal state.
  • Form element: aria-busy="false".
  • FormFeedback (error) banner is rendered above the submit button (see §4.2 for exact copy). The form itself remains visible and filled — user does not lose their input.
  • Focus is moved programmatically to the error banner heading.
  • The error banner has role="alert" and aria-live="assertive" (see §7).

Partial Error — Field-Level from Server

  • Triggered when Lambda returns { "ok": false, "error": "..." } with a field-level message (e.g., a field failed server-side validation).
  • The specific field is highlighted with error styling and aria-invalid="true".
  • The field-level error message is rendered in that field's error slot.
  • The network/server error banner is also shown (degraded trust signal).
  • Focus is moved to the first affected field.

3.2 State Transition Diagram

Default
  │
  ├─(user types/tabs)──→ Touched/Dirty (per field)
  │
  ├─(submit, valid)────→ Submitting
  │                           │
  │                           ├─(200, ok:true)──→ Success  [terminal]
  │                           │
  │                           └─(error)─────────→ Error (Network/Server)
  │                                                    │
  │                                                    └─(user edits & resubmits)──→ Submitting
  │
  └─(submit, invalid)──→ Touched/Dirty (all fields) + focus → first error

4. UX Copy

All copy is final and must be used verbatim. Placeholders in brackets ([phone], [email]) must be replaced with Diana's actual contact details at implementation.

4.1 Page Intro Lead

Rendered as the lead paragraph (<p>) in the /contact/ page hero, below the H1. (See 33-frontend-spec.md §5.6 Section 2.)

"Tell Diana about your project. She responds within 12 business days."

Styling: DM Sans 400, --text-lg, --col-ink.

4.2 Success Message (FormFeedback — success variant)

Shown in place of the form after a successful submission. Component: FormFeedback (success) (33-frontend-spec.md §6.8).

Heading:

"Your request is on its way!"

Body:

"Diana will review your request and get back to you within 12 business days. If your project is urgent, you can also reach her directly at [phone] or [email]."

  • [phone] renders as a <a href="tel:[phone]">[phone]</a> link.
  • [email] renders as a <a href="mailto:[email]">[email]</a> link.
  • Both links use --col-copper color, text-decoration: underline.

Implementation note — contact detail placeholders:

  • Phone: (415) XXX-XXXX (replace at implementation — see §5.6 §4 "Secondary Contact Info" in 33-frontend-spec.md)
  • Email: diana@example.com (replace at implementation)

4.3 Network / Server Error Message (FormFeedback — error variant)

Shown as a banner above the submit button while the form remains visible. Component: FormFeedback (error) (33-frontend-spec.md §6.8).

Heading:

"Something went wrong."

Body:

"We couldn't send your request. Please try again, or contact Diana directly at [phone] or [email]."

  • Same link treatment as §4.2.
  • Form fields remain filled and editable.

4.4 Field-Level Error Copy (reference)

See §2.2 for the authoritative error message text for each field. Reproduced here for convenience:

Field Error message
Full Name "Please enter your name."
Service Address "Please enter the service address or city."
Phone "Please enter a valid phone number."
Email "Please enter a valid email address."
Description "Please describe your project (at least 10 characters)."

5. AWS SES Integration

Implements ADR-002. Resolves OQ-3 (endpoint URL and recipient email are deferred to implementation).

5.1 Endpoint

Property Value
URL TBD — provided at implementation (see ADR-002, OQ-3)
Method POST
Content-Type application/json
Auth None (public endpoint)
CORS API Gateway must allow the production site origin (e.g. https://her-home.com)

Store the endpoint URL as a build-time environment variable or a configuration constant (e.g. FORM_ENDPOINT_URL). Do not hardcode the URL in form markup.

5.2 Request Payload

The client sends a single JSON object. All string values are trimmed of leading/trailing whitespace before sending.

{
  "name": "string",
  "address": "string",
  "phone": "string",
  "email": "string",
  "description": "string",
  "honeypot": "string"
}
Field Type Notes
name string Trimmed, non-empty
address string Trimmed, non-empty
phone string Trimmed, non-empty
email string Trimmed, lowercase, non-empty
description string Trimmed, non-empty, max 2000 chars
honeypot string Always sent; value is "" for legitimate submissions

5.3 Lambda Responsibilities

The Lambda function is the authoritative gatekeeper and must enforce all rules independently of client validation.

Processing order:

  1. Honeypot check: If honeypot field is a non-empty string, log the discarded submission (for monitoring), return HTTP 200 { "ok": true }. Do not signal to the client that the submission was discarded.

  2. Server-side field validation: Verify all five required fields (name, address, phone, email, description) are non-empty strings after trimming. If any fail, return HTTP 200 { "ok": false, "error": "Missing required fields." }.

  3. SES send: Call AWS SES SendEmail with the formatted email (see §5.4).

  4. Success response: On successful SES send, return HTTP 200 { "ok": true }.

  5. SES error: On SES failure, log the error (CloudWatch), return HTTP 200 { "ok": false, "error": "Failed to send email. Please try again." }.

5.4 Email Formatting

Recipient: Diana's inbox (address configured in Lambda environment variable SES_RECIPIENT — set at implementation)
Sender: A verified SES sender identity (e.g. noreply@her-home.com) — configured at implementation
Reply-To: The submitting user's email address (so Diana can reply directly from her email client)

Subject line:

New Quote Request from [name]

Plain-text body:

New quote request received via HER website.

Name:    [name]
Phone:   [phone]
Email:   [email]
Address: [address]

Project Description:
[description]

---
Submitted at: [ISO 8601 timestamp in UTC]

HTML email: Optional enhancement — not required for v1. Plain-text body is sufficient.

5.5 Response Handling (Client)

Scenario Client action
200 { "ok": true } Show success state (§3.1 Success)
200 { "ok": false, "error": "..." } Show error banner (§3.1 Error)
429 Too Many Requests (AWS WAF rate limit) Show error banner with copy: "Too many requests. Please wait a moment and try again."
Network error (no response) Show error banner
Any non-200, non-429 HTTP status Show error banner

5.6 HTTP Status Code Policy

The Lambda always returns HTTP 200 to the client, with success/failure detail in the JSON body. This policy:

  • Prevents browser retry behavior triggered by 5xx responses.
  • Keeps error handling logic entirely in JavaScript, not in HTTP status handling.
  • Makes CORS error handling simpler.

Exception: AWS WAF may return 429 Too Many Requests before the request reaches Lambda. The client must handle this status code explicitly (see §5.5).

5.7 Timeout Handling

  • Client-side fetch timeout: 10 seconds. If no response is received within 10 seconds, abort the request and show the error banner.
  • Implementation: use AbortController with a 10-second timeout signal passed to fetch().

6. Spam Protection

6.1 Honeypot Field (Primary)

The honeypot is the primary spam protection mechanism in v1. It requires no third-party service and adds no friction for real users.

Field definition:

  • name="website", id="field-website", type="text"
  • No <label> element rendered in the DOM.
  • Never autofocused or tab-accessible.

CSS hiding (applied via a utility class, e.g. .visually-trap):

.visually-trap {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  tabindex: -1;        /* set as HTML attribute, not CSS */
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
}

Do not use display: none or visibility: hidden — many bots skip fields with these styles. CSS-based hiding that leaves the element in the normal document flow is more effective.

Client-side behavior: Before submitting, check document.getElementById('field-website').value. If non-empty, do not call the API — immediately transition to success state (silent discard).

Server-side behavior: Lambda checks honeypot !== "" → return { "ok": true } without sending email.

Configure an AWS WAF Web ACL on the API Gateway with a basic rate-based rule:

Parameter Recommended value
Rule type Rate-based
Rate limit 5 requests per IP per 5 minutes
Action Block (returns 429)
Scope API Gateway stage

This is a recommendation for implementation, not a blocking requirement for this spec. The client must handle 429 responses gracefully (see §5.5).

6.3 No CAPTCHA (v1)

No CAPTCHA or challenge-based verification is used in v1, per FR-2 (form must be lightweight). If spam volume becomes a problem post-launch, the recommended escalation path is:

  1. Tighten AWS WAF rate rules.
  2. Add Cloudflare Turnstile (invisible challenge, no user interaction required for most users).
  3. As a last resort, add hCaptcha or reCAPTCHA v3.

7. Accessibility

All form accessibility requirements conform to WCAG 2.1 AA. These requirements supplement 33-frontend-spec.md §9.

7.1 Labels

  • Every visible input and textarea has a corresponding <label> element.
  • Labels are associated via matching for (on label) and id (on input) attributes.
  • Labels are always visible — never hidden, never replaced by placeholder-only patterns.
  • Label text is DM Sans 500, --text-sm, --col-graphite (see FormField component).

7.2 Error Message Regions

  • Each field has a sibling <p> element for its error message (e.g. id="error-name"), present in the DOM at all times.
  • When no error: element is empty ("") and has aria-hidden="true".
  • When error present: element contains the error text, has role="alert", and aria-hidden is removed (or set "false").
  • Error messages are injected — not toggled with CSS display — to ensure screen reader announcement.

7.3 aria-describedby on Fields

Each input/textarea carries aria-describedby pointing to both the help text element (if present) and the error element, even when empty:

<input
  id="field-address"
  name="address"
  type="text"
  aria-describedby="hint-address error-address"
  aria-invalid="false"
  ...
/>
<p id="hint-address" class="field-hint">Where is the work located? City or full address.</p>
<p id="error-address" role="alert" aria-hidden="true"></p>

7.4 aria-invalid

  • Default state: aria-invalid="false" on all fields.
  • On validation failure: aria-invalid="true".
  • On validation pass (after error): aria-invalid="false" restored.

7.5 Submitting State

  • Form element: aria-busy="true" while request is in-flight.
  • Submit button: aria-disabled="true" (not disabled — button stays focusable and announces its state).
  • Button label updated to "Sending…" (visible text) while in submitting state.

7.6 Success State

  • Success message container: role="status", aria-live="polite".
  • On render, focus is programmatically moved to the success heading element.
  • Success heading must be a heading element (<h2> or <h3>) — not a <p> or <div> — so it is discoverable by screen reader heading navigation.

7.7 Error Banner State

  • Error banner container: role="alert", aria-live="assertive".
  • On render, focus is programmatically moved to the error banner heading element.
  • The banner must be in the DOM before the error occurs (with empty content), or be inserted immediately before focus is moved — do not rely on aria-live alone for focus; use explicit focus().

7.8 Focus Management Summary

State transition Focus moves to
Submit with validation errors First invalid field's <input> or <textarea>
Submitting → Success Success message heading
Submitting → Error (network/server) Error banner heading
Submitting → Partial field error (server) First errored field's <input>

7.9 Keyboard Navigation

  • Tab order follows DOM order: Name → Address → Phone → Email → Description → Submit.
  • All interactive elements are reachable via Tab and operable via Enter/Space.
  • No keyboard traps.
  • Submit button is reachable via Tab and activates on Enter.

8. Mobile Considerations

All requirements here supplement 33-frontend-spec.md §3 (breakpoints) and ADR-003 (mobile-first).

8.1 Layout

  • All fields are full-width (100%) on mobile (below md breakpoint).
  • Fields stack vertically in DOM order: Name → Address → Phone → Email → Description → Submit.
  • Submit button is full-width on mobile.
  • Gap between stacked fields: --space-3 (24px).

8.2 inputmode Attributes

Field inputmode value Effect
Phone tel Numeric keypad with +, -, (, ) on iOS and Android
Email email Keyboard with @ and .com shortcut keys
All others (not set — default) Standard text keyboard

8.3 autocomplete Attributes

Field name attr autocomplete attr
Full Name name name
Phone phone tel
Email email email
Service Address address off
Description description off
Honeypot website off

Address autocomplete="off" rationale: Prevents browser/OS autofill from inserting the user's home or billing address into a field intended for the service location. A homeowner in Oakland requesting work at a rental in San Francisco must not have Oakland autofilled. See §1.2.

8.4 Textarea Sizing

Breakpoint Min height
Mobile (default) 120px
Desktop (md and above) 160px

Textarea is resizable vertically only (resize: vertical). Max height is not constrained (user may expand freely).

8.5 Touch Targets

  • All inputs and the submit button have a minimum tap target height of 44px, per 33-frontend-spec.md §9.3.
  • Submit button padding: at minimum --space-2 (16px) top/bottom, --space-4 (32px) left/right.
  • Field labels tap targets include the input below them (clicking label focuses input via for/id association).

8.6 Viewport and Keyboard Behavior

  • The <meta name="viewport" content="width=device-width, initial-scale=1"> tag must be present (standard; prevents double-tap zoom breaking on iOS).
  • On mobile, when a field is focused, the virtual keyboard pushes the viewport up. The form container must not use overflow: hidden in a way that clips the active field below the keyboard.
  • Do not use position: fixed on the form or its container on mobile.

9. Thank-You / Confirmation

9.1 Options

Two approaches are available. The choice is flagged as Open Question OQ-7 (see below).

Option A — In-Page Success State (No Redirect)

After successful submission, the form is hidden and the FormFeedback (success) component renders in its place on /contact/. No navigation occurs.

Pros: Simpler implementation, no extra page to maintain, success copy is already on the page.
Cons: If the user hits browser Back and then Forward, they may see the form in its original empty state. Duplicate submission via Back → Re-submit is prevented by client state (form resets to Success state on remount, or history state is used).

Option B — Redirect to /thank-you/

After { "ok": true }, the client performs window.location.href = '/thank-you/'. The /thank-you/ page renders the success message as a full page.

Pros: Eliminates back-button resubmission (form is gone from history); clean URL for conversion tracking if analytics are added later.
Cons: Requires a separate HTML page; slightly more complex routing.

/thank-you/ page requirements (if Option B is chosen):

  • Full standalone page with nav and footer.
  • H1: "Your request is on its way!"
  • Body copy: same as §4.2 success message.
  • noindex meta tag: <meta name="robots" content="noindex, follow">.
  • Included in sitemap.xml as changefreq="never" with no <priority> set — or excluded entirely if the sitemap is filtered to indexable pages only (see 37-seo-content-spec.md).
  • No form on this page.

9.2 Open Question — OQ-7

OQ-7: Should successful form submission stay on /contact/ (Option A — in-page success state) or redirect to /thank-you/ (Option B — separate page)?

Recommendation: Option A for v1, given that there is no analytics in v1 (ADR-005) and the redirect's primary benefit (conversion tracking) does not apply. Revisit if analytics are added later.

Decision needed from: Diana / project owner.
Blocking: Implementation of 38-lead-capture-spec.md.


10. Requirements Traceability

10.1 Functional Requirements

Requirement Description How this spec satisfies it
FR-1 Quote form accessible within 12 taps from any page Form lives at /contact/; nav CTA "Get a Quote" links there from all pages (persistent nav)
FR-2 Form must be lightweight (no file uploads) Five text fields only; no file input; no CAPTCHA in v1 (§6.3)
FR-3 Form collects Name, Address, Phone, Email, Description All five fields defined in §1.1 with types, labels, and validation
FR-4 Clear confirmation and next-steps after submission Success state (§3.1) and success copy (§4.2) specify exact heading + body with timeline and fallback contact
FR-5 Submissions delivered via existing AWS service API Gateway + Lambda + SES integration fully specified in §5
FR-6 Prominently expose form, email, and phone Form is the primary channel; email and phone appear in success copy (§4.2) and error copy (§4.3)
FR-7 Secondary channels (email, phone) in header/footer and post-submission Phone and email are clickable links in both success and error messages (§4.2, §4.3); header/footer governed by 33-frontend-spec.md

10.2 Acceptance Criteria

Criterion How this spec satisfies it
AC-1 Visitor can reach the quote form in 12 taps from any page
AC-6 Secondary contact channels visible in header/footer and post-form confirmation

10.3 Architecture Decision Records

ADR Decision Relevance to this spec
ADR-001 Static website delivery Form submission requires a dynamic backend; satisfied by API Gateway + Lambda (no server-side rendering required on the static site)
ADR-002 Form backend: AWS SES via API Gateway + Lambda Directly implemented in §5; endpoint URL and SES recipient deferred to implementation per ADR-002 status
ADR-003 Mobile-first, performance-first design §8 specifies mobile layout, inputmode, autocomplete, touch targets; no heavy JS frameworks; fetch API only
ADR-005 No analytics in v1 Influences §9 recommendation: in-page success state (Option A) preferred since redirect's conversion-tracking benefit doesn't apply without analytics

10.4 Open Questions (from 99-open-questions.md)

OQ Status Impact on this spec
OQ-3 Resolved (endpoint URL deferred) API endpoint URL is a build-time constant (§5.1); recipient email is a Lambda env var (§5.4) — both set at implementation
OQ-7 New — raised by this spec Thank-you page vs. in-page success (§9); must be decided before implementation

10.5 Cross-Document References

Document Relationship
33-frontend-spec.md §5.6 Defines /contact/ page layout, hero copy, secondary contact section, and desktop two-column grid; this spec governs behavior and copy within the form
33-frontend-spec.md §6.7 FormField Component used by all visible fields
33-frontend-spec.md §6.8 FormFeedback Component used for success and error states
33-frontend-spec.md §6.1 Button Submit button component
33-frontend-spec.md §9 Accessibility Form accessibility requirements; §7 of this spec extends them
37-seo-content-spec.md noindex handling for /thank-you/ if Option B is chosen
99-open-questions.md OQ-3 (resolved), OQ-7 (new — added by this spec)