31 KiB
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
- Form Fields
- Validation Rules
- Form UX States
- UX Copy
- AWS SES Integration
- Spam Protection
- Accessibility
- Mobile Considerations
- Thank-You / Confirmation
- 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 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 forstreet-addressfills a billing/home address, which may differ from the service location. Usingoffprevents 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-1margin 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 (
mdand 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, nottype="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 withnovalidateon 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." |
| 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-0123415-555-01234155550123+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 reads2000 / 2000— themaxlengthHTML 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 usedisabledattribute — this removes the element from tab order. - All form fields:
disabledattribute 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-linencolor on copper button background.
Success
- Triggered on HTTP 200 response with
{ "ok": true }. - The form element is hidden (
display: noneor 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"andaria-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"andaria-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 1–2 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 1–2 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-coppercolor,text-decoration: underline.
Implementation note — contact detail placeholders:
- Phone:
(415) XXX-XXXX(replace at implementation — see §5.6 §4 "Secondary Contact Info" in33-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." |
"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:
-
Honeypot check: If
honeypotfield 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. -
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." }. -
SES send: Call AWS SES
SendEmailwith the formatted email (see §5.4). -
Success response: On successful SES send, return HTTP 200
{ "ok": true }. -
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
AbortControllerwith a 10-second timeout signal passed tofetch().
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: noneorvisibility: 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.
6.2 AWS WAF Rate Limiting (Recommended)
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:
- Tighten AWS WAF rate rules.
- Add Cloudflare Turnstile (invisible challenge, no user interaction required for most users).
- 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) andid(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(seeFormFieldcomponent).
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 hasaria-hidden="true". - When error present: element contains the error text, has
role="alert", andaria-hiddenis 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"(notdisabled— 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-livealone for focus; use explicitfocus().
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
mdbreakpoint). - 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 |
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 |
|
| 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/idassociation).
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: hiddenin a way that clips the active field below the keyboard. - Do not use
position: fixedon 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.
noindexmeta tag:<meta name="robots" content="noindex, follow">.- Included in
sitemap.xmlaschangefreq="never"with no<priority>set — or excluded entirely if the sitemap is filtered to indexable pages only (see37-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 of38-lead-capture-spec.md.
10. Requirements Traceability
10.1 Functional Requirements
| Requirement | Description | How this spec satisfies it |
|---|---|---|
| FR-1 | Quote form accessible within 1–2 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 1–2 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) |