# Phase 2: Marketing Site - Research

**Researched:** 2026-03-19
**Domain:** Next.js 16 App Router — public marketing page, Nodemailer SMTP contact form, carousel UI
**Confidence:** HIGH

---

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions

**Hero section**
- Layout: Split panel — photo on the left, text content on the right (mirrors the login page feel)
- Tone: Professional and polished — credentials, experience, trust-building (not casual)
- CTA button: "Get in Touch" — smooth-scrolls to the contact form section
- Copy: Claude writes the headline, subheading, and bio (professional Utah real estate agent framing). Wrap text in clearly commented placeholders so Teressa can swap in her own words before launch

**Testimonials**
- Display: Auto-scrolling carousel — rotates every 5 seconds, pauses on hover
- Navigation: Arrow controls and dot indicators so visitors can also advance manually
- Content: Claude writes 4–5 sample testimonials with realistic Utah buyer/seller scenarios and placeholder full names (e.g., "Sarah Mitchell"). Clearly commented so real ones can be swapped in
- Attribution: Quote + full name only (no city, no photo)

**Contact form**
- Delivery: Email notification to Teressa via Nodemailer + SMTP (no database storage in this phase)
- SMTP credentials: `CONTACT_EMAIL_USER` and `CONTACT_EMAIL_PASS` environment variables — never hardcoded
- Success state: Inline — the form is replaced by a thank-you message ("Thanks! Teressa will be in touch soon.") when submission succeeds
- Spam protection: Honeypot hidden field — bots fill it in, humans never see it; submissions with the honeypot filled are silently discarded
- Validation: Required fields are name, email, phone, and message — validated server-side in the form action

**Page structure**
- Section order (top to bottom): Sticky nav → Hero → Testimonials → Listings placeholder → Contact form → Footer
- Sticky nav content: Teressa's name as wordmark logo (left), anchor links on the right — Home, About, Listings, Contact — smooth-scroll to each section
- Listings placeholder: A tasteful "Listings Coming Soon" section with a brief description — no real data, just visual presence
- Agent portal: No link anywhere on the public site — Teressa navigates to `/agent/login` directly via URL
- Footer: Name + Utah real estate license number (`14196185-SA00` — NOTE: verify the last two characters, may be letters O or zeros) + copyright year + repeated nav links

**Placeholder assets**
- Hero photo: reuse `public/red.jpg` from Phase 1 (already in the project)
- Listings placeholder: use a tasteful stock-style image or a solid brand-colored background with an icon — no real property photos needed
- Background section breaks: subtle texture or light gradient using brand cream/navy — no photography required for non-hero sections
- Icons: use Lucide React (already likely available) for any UI icons (phone, email, map pin for contact section)

### Claude's Discretion
- Exact hero photo crop/focal point and overlay gradient (if any)
- Mobile nav pattern (hamburger menu vs collapsed links)
- Exact card styling, shadow depth, border radius
- Animation timing beyond the 5-second carousel interval
- Exact color weights within the brand palette already established (navy `#1B2B4B`, gold `#C9A84C`, cream `#FAF9F7`)

### Deferred Ideas (OUT OF SCOPE)
- None — discussion stayed within phase scope
</user_constraints>

---

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| MKTG-01 | Visitor sees a hero section with Teressa's professional photo and warm introductory bio | Split-panel layout pattern, Next.js Image component, `public/red.jpg` already in project |
| MKTG-02 | Visitor sees a "listings coming soon" placeholder section (full WFRMLS integration deferred to v2) | Static section component, brand color background + Lucide icon |
| MKTG-03 | Visitor can submit a contact form with name, email, phone, and message | Next.js Server Action, Nodemailer + SMTP, honeypot pattern, server-side Zod validation |
| MKTG-04 | Visitor sees a testimonials section with client reviews on the homepage | Pure React carousel (useState + useEffect interval), no external carousel library needed |
</phase_requirements>

---

## Summary

This phase builds a single public-facing homepage for Teressa Copeland Homes on top of the Next.js 16 + Tailwind CSS 4 stack established in Phase 1. The page is a single route (`app/page.tsx`) composed of full-width section components: sticky nav, hero, testimonials carousel, listings placeholder, contact form, and footer. No new major dependencies are required — Nodemailer is the only addition for contact form email delivery.

The carousel can be built with zero external libraries using React `useState` / `useEffect` — a 5-second auto-advance with hover-pause, arrow buttons, and dot indicators is straightforward in ~60 lines of client component code. Reaching for a carousel library like Embla or Swiper would add unnecessary bundle weight for a single simple use case.

The contact form uses a Next.js Server Action (no API route needed) with Zod validation already in the project, Nodemailer for SMTP delivery, and a honeypot hidden field for bot protection. No database writes occur in this phase — the form is fire-and-forget email only.

**Primary recommendation:** Build everything as Next.js App Router server components with minimal client component islands (carousel, mobile nav toggle, form state). Keep the public page route at `app/page.tsx` and colocate section components under `app/_components/` or `components/marketing/`.

---

## Standard Stack

### Core (already in project — no new installs for UI)

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| Next.js | 16.2.0 | App Router, Server Actions, Image optimization | Already installed; Phase 1 foundation |
| React | 19.2.4 | Component model | Already installed |
| Tailwind CSS | ^4 | Utility-first styling | Already installed; brand colors via inline styles or CSS vars |
| Zod | ^4.3.6 | Server-side form validation | Already installed; used in Phase 1 auth |

### New Dependency — Contact Form Email

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| nodemailer | ^6.9.x | SMTP email delivery from Server Action | De facto Node.js SMTP library; well-maintained; zero vendor lock-in |
| @types/nodemailer | ^6.4.x | TypeScript types | Required for TS strict mode |

### Supporting (already available)

| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| lucide-react | latest | Icon set (phone, mail, map-pin, chevron, etc.) | Confirmed in CONTEXT.md; check if installed or add |

**Check if lucide-react is already installed:**
```bash
grep lucide /Users/ccopeland/temp/red/teressa-copeland-homes/package.json
```
If absent, add it.

### Alternatives Considered

| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| Hand-rolled carousel | Embla Carousel or Swiper | Overkill for a single 4-5 item testimonials carousel; adds bundle weight |
| Nodemailer | Resend SDK, SendGrid | Adds vendor dependency; Nodemailer works with any SMTP provider already owned by Teressa |
| Server Action form | API Route (`app/api/contact/route.ts`) | Server Actions are simpler, no fetch boilerplate; use progressive enhancement |

**Installation (new packages only):**
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes
npm install nodemailer
npm install --save-dev @types/nodemailer
# If lucide-react not present:
npm install lucide-react
```

---

## Architecture Patterns

### Recommended Project Structure

```
teressa-copeland-homes/
├── app/
│   ├── page.tsx                    # Public homepage — server component, composes sections
│   ├── layout.tsx                  # Root layout (already exists from Phase 1)
│   └── _components/                # Marketing page section components (colocated)
│       ├── SiteNav.tsx             # Sticky nav — client component (mobile toggle state)
│       ├── HeroSection.tsx         # Server component
│       ├── TestimonialsSection.tsx # Client component (carousel state + interval)
│       ├── ListingsPlaceholder.tsx # Server component
│       ├── ContactSection.tsx      # Client component (form state, submission)
│       └── SiteFooter.tsx          # Server component
├── lib/
│   ├── contact-mailer.ts           # Nodemailer transporter + sendContactEmail()
│   └── contact-action.ts           # Server Action — validates, calls mailer, returns state
└── public/
    └── red.jpg                     # Hero photo (already exists)
```

**Why `app/_components/`:** Next.js treats files prefixed with `_` as private — they're not treated as routes. Keeps section components colocated with the page without polluting the route tree.

**Alternative:** `components/marketing/` at project root. Either works; pick one and be consistent.

### Pattern 1: Server Action for Contact Form (No API Route)

**What:** Form submits to a Server Action; action validates with Zod, sends email via Nodemailer, returns typed state.
**When to use:** Any form in Next.js App Router that doesn't need a public REST endpoint.

```typescript
// lib/contact-action.ts
'use server'

import { z } from 'zod'
import { sendContactEmail } from './contact-mailer'

const ContactSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Valid email required'),
  phone: z.string().min(7, 'Phone is required'),
  message: z.string().min(10, 'Message is required'),
  // Honeypot — must be empty
  website: z.string().max(0),
})

export type ContactState = { status: 'idle' | 'success' | 'error'; message?: string }

export async function submitContact(
  _prev: ContactState,
  formData: FormData,
): Promise<ContactState> {
  const raw = {
    name: formData.get('name'),
    email: formData.get('email'),
    phone: formData.get('phone'),
    message: formData.get('message'),
    website: formData.get('website'), // honeypot
  }

  const parsed = ContactSchema.safeParse(raw)
  if (!parsed.success) {
    return { status: 'error', message: 'Please fill in all required fields.' }
  }

  // Honeypot filled — silently succeed (don't tell the bot it failed)
  if (parsed.data.website !== '') {
    return { status: 'success' }
  }

  try {
    await sendContactEmail(parsed.data)
    return { status: 'success' }
  } catch {
    return { status: 'error', message: 'Something went wrong. Please try again.' }
  }
}
```

### Pattern 2: Nodemailer SMTP Transporter

**What:** Singleton transporter initialized from env vars; called from Server Action.
**When to use:** Any server-side email send in Node.js/Next.js.

```typescript
// lib/contact-mailer.ts
import nodemailer from 'nodemailer'

const transporter = nodemailer.createTransport({
  host: process.env.CONTACT_SMTP_HOST,      // e.g., smtp.gmail.com
  port: Number(process.env.CONTACT_SMTP_PORT ?? 587),
  secure: false,                             // true for 465, false for 587 (STARTTLS)
  auth: {
    user: process.env.CONTACT_EMAIL_USER,
    pass: process.env.CONTACT_EMAIL_PASS,
  },
})

export async function sendContactEmail(data: {
  name: string
  email: string
  phone: string
  message: string
}) {
  await transporter.sendMail({
    from: `"${data.name}" <${process.env.CONTACT_EMAIL_USER}>`,
    replyTo: data.email,
    to: process.env.CONTACT_EMAIL_USER,     // Teressa's inbox
    subject: `New contact from ${data.name} — Teressa Copeland Homes`,
    text: `Name: ${data.name}\nEmail: ${data.email}\nPhone: ${data.phone}\n\n${data.message}`,
    html: `<p><strong>Name:</strong> ${data.name}</p>
           <p><strong>Email:</strong> ${data.email}</p>
           <p><strong>Phone:</strong> ${data.phone}</p>
           <p><strong>Message:</strong><br/>${data.message.replace(/\n/g, '<br/>')}</p>`,
  })
}
```

### Pattern 3: Pure React Testimonials Carousel

**What:** Client component with `useState` (activeIndex), `useEffect` (5-second interval), pause on hover.
**When to use:** Simple auto-advance carousel with ≤ 10 items — no library needed.

```typescript
// app/_components/TestimonialsSection.tsx
'use client'
import { useState, useEffect, useRef } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'

const TESTIMONIALS = [
  // PLACEHOLDER — replace with real client testimonials before launch
  { quote: "Working with Teressa made buying our first home in Salt Lake County seamless...", name: "Sarah Mitchell" },
  { quote: "Teressa sold our Provo home in under a week above asking price...", name: "James & Karen Olsen" },
  // add 3-5 total
]

export function TestimonialsSection() {
  const [active, setActive] = useState(0)
  const [paused, setPaused] = useState(false)
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)

  useEffect(() => {
    if (paused) return
    intervalRef.current = setInterval(() => {
      setActive(i => (i + 1) % TESTIMONIALS.length)
    }, 5000)
    return () => { if (intervalRef.current) clearInterval(intervalRef.current) }
  }, [paused])

  const prev = () => setActive(i => (i - 1 + TESTIMONIALS.length) % TESTIMONIALS.length)
  const next = () => setActive(i => (i + 1) % TESTIMONIALS.length)

  return (
    <section
      id="about"
      onMouseEnter={() => setPaused(true)}
      onMouseLeave={() => setPaused(false)}
    >
      {/* carousel content */}
      <blockquote>"{TESTIMONIALS[active].quote}"</blockquote>
      <p>— {TESTIMONIALS[active].name}</p>
      <button onClick={prev}><ChevronLeft /></button>
      <button onClick={next}><ChevronRight /></button>
      <div>
        {TESTIMONIALS.map((_, i) => (
          <button key={i} onClick={() => setActive(i)} aria-label={`Testimonial ${i + 1}`}
            style={{ opacity: i === active ? 1 : 0.3 }} />
        ))}
      </div>
    </section>
  )
}
```

### Pattern 4: Smooth-Scroll Anchor Navigation

**What:** Pure CSS + HTML `id` anchors; no JavaScript scroll library needed.
**When to use:** Single-page anchor navigation.

```typescript
// CSS approach — add to globals.css
// html { scroll-behavior: smooth; }

// Nav links
<a href="#hero">Home</a>
<a href="#about">About</a>
<a href="#listings">Listings</a>
<a href="#contact">Contact</a>

// Section targets
<section id="hero"> ... </section>
<section id="about"> ... </section>  // testimonials anchors here per section order
<section id="listings"> ... </section>
<section id="contact"> ... </section>
```

### Pattern 5: Contact Form with useActionState (React 19)

**What:** React 19's `useActionState` replaces `useFormState` from react-dom/server. Works with Server Actions in Next.js App Router.

```typescript
'use client'
import { useActionState } from 'react'
import { submitContact, type ContactState } from '@/lib/contact-action'

const initialState: ContactState = { status: 'idle' }

export function ContactForm() {
  const [state, action, pending] = useActionState(submitContact, initialState)

  if (state.status === 'success') {
    return <p>Thanks! Teressa will be in touch soon.</p>
  }

  return (
    <form action={action}>
      {/* Honeypot — visually hidden, bots fill it, humans ignore it */}
      <input name="website" type="text" style={{ display: 'none' }} tabIndex={-1} autoComplete="off" />
      <input name="name" type="text" required placeholder="Your name" />
      <input name="email" type="email" required placeholder="Email address" />
      <input name="phone" type="tel" required placeholder="Phone number" />
      <textarea name="message" required placeholder="How can Teressa help?" />
      {state.status === 'error' && <p role="alert">{state.message}</p>}
      <button type="submit" disabled={pending}>
        {pending ? 'Sending...' : 'Send Message'}
      </button>
    </form>
  )
}
```

### Anti-Patterns to Avoid

- **Using `useFormState` from `react-dom`:** Removed in React 19. Use `useActionState` from `react` instead.
- **Storing contact form submissions in DB in this phase:** CONTEXT.md locked to email-only delivery. DB storage is out of scope.
- **Importing carousel library (Swiper, Embla):** Adds unnecessary bundle for 4-5 static testimonials. Hand-roll with useState/useEffect.
- **Putting a link to `/agent/login` on the public site:** Explicitly locked out of scope — agent navigates there directly via URL.
- **Hardcoding SMTP credentials:** Always read from `CONTACT_EMAIL_USER` / `CONTACT_EMAIL_PASS` env vars.
- **Using `router.push` for smooth scroll:** Use CSS `scroll-behavior: smooth` + `<a href="#id">` anchors instead.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| SMTP email delivery | Custom fetch to mail API | `nodemailer` | Handles STARTTLS, auth, connection pooling, error codes |
| Form validation | Manual regex/string checks | `zod` (already installed) | Already in project; handles type coercion, error messages |
| Image optimization | Raw `<img>` tags | `next/image` | Automatic WebP conversion, lazy load, responsive srcset |
| Smooth scroll | `window.scrollTo()` JS | CSS `scroll-behavior: smooth` | Native browser feature; no JS required |

**Key insight:** The SMTP connection lifecycle (STARTTLS negotiation, auth failures, timeouts, connection pooling) is non-trivial. Nodemailer handles all of it — never reimplement it.

---

## Common Pitfalls

### Pitfall 1: `useFormState` vs `useActionState` (React 19 breaking change)

**What goes wrong:** Developer uses `useFormState` from `react-dom/server` — module not found or deprecated warning.
**Why it happens:** Most tutorials pre-date React 19. The hook moved.
**How to avoid:** Import `useActionState` from `react` (not `react-dom`). React 19.2.4 is in the project.
**Warning signs:** TypeScript error "useFormState is not exported from react-dom".

### Pitfall 2: Sticky Nav Overlapping Section Anchors

**What goes wrong:** Clicking "Contact" scrolls so the section heading is hidden behind the sticky nav.
**Why it happens:** Browser positions the anchor at `top: 0`, but sticky nav covers it.
**How to avoid:** Add `scroll-margin-top` to each section equal to nav height.

```css
section[id] { scroll-margin-top: 72px; } /* match nav height */
```

### Pitfall 3: Carousel `setInterval` Memory Leak

**What goes wrong:** Multiple intervals accumulate on re-renders; carousel skips or fires multiple times per second.
**Why it happens:** `useEffect` cleanup not returning `clearInterval`.
**How to avoid:** Always clear the previous interval in the `useEffect` cleanup function. Use a `ref` to track the interval ID (shown in Pattern 3 above).

### Pitfall 4: Honeypot Field Autofilled by Browser Password Manager

**What goes wrong:** Browser autofills the honeypot field, causing legitimate submissions to be silently discarded.
**Why it happens:** The hidden field has a name that triggers autofill (e.g., `name="email2"`).
**How to avoid:** Name the honeypot something bots seek but browsers won't autofill (e.g., `name="website"` or `name="url"`). Add `autoComplete="off"` and `tabIndex={-1}`. Use `style={{ display: 'none' }}` rather than `type="hidden"` (bots fill visible-in-DOM fields more reliably).

### Pitfall 5: Nodemailer `from` Address Rejected by SMTP Server

**What goes wrong:** Email silently fails or bounces because `from` address doesn't match authenticated user.
**Why it happens:** Gmail/SMTP providers require `from` to match the authenticated account.
**How to avoid:** Set `from` to `process.env.CONTACT_EMAIL_USER` (Teressa's SMTP account) and use `replyTo` for the visitor's email. The pattern in Pattern 2 above handles this correctly.

### Pitfall 6: Next.js `Image` Component with Local File

**What goes wrong:** `<Image src="/red.jpg" />` throws "Image is missing required 'width' or 'height'" or renders distorted.
**Why it happens:** Next.js Image requires explicit dimensions OR `fill` + positioned parent.
**How to avoid:** For the hero split-panel, use `fill` with `objectFit="cover"` on a positioned container.

```tsx
// Hero photo container
<div style={{ position: 'relative', height: '100%', minHeight: '500px' }}>
  <Image src="/red.jpg" alt="Teressa Copeland" fill style={{ objectFit: 'cover', objectPosition: 'center top' }} priority />
</div>
```

---

## Code Examples

### Environment Variables Required

Add to `.env.local` (never commit):
```bash
# Contact form SMTP
CONTACT_EMAIL_USER=teressa@teressacopelandhomes.com
CONTACT_EMAIL_PASS=your_app_password
CONTACT_SMTP_HOST=smtp.gmail.com
CONTACT_SMTP_PORT=587
```

Note: Add `CONTACT_SMTP_HOST` and `CONTACT_SMTP_PORT` in addition to the CONTEXT.md variables — the host/port pair is required by Nodemailer but not mentioned in the user decisions. Use Gmail app password or equivalent.

### Sticky Nav with Mobile Toggle

```tsx
// app/_components/SiteNav.tsx
'use client'
import { useState } from 'react'
import { Menu, X } from 'lucide-react'

const NAV_LINKS = [
  { label: 'Home', href: '#hero' },
  { label: 'About', href: '#about' },
  { label: 'Listings', href: '#listings' },
  { label: 'Contact', href: '#contact' },
]

export function SiteNav() {
  const [open, setOpen] = useState(false)
  return (
    <nav style={{
      position: 'sticky', top: 0, zIndex: 50,
      backgroundColor: '#1B2B4B', color: '#FAF9F7',
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '1rem 2rem' }}>
        {/* Wordmark */}
        <span style={{ fontWeight: 700, letterSpacing: '0.05em' }}>Teressa Copeland</span>
        {/* Desktop nav links */}
        <ul className="hidden md:flex gap-8">
          {NAV_LINKS.map(l => <li key={l.href}><a href={l.href}>{l.label}</a></li>)}
        </ul>
        {/* Mobile hamburger */}
        <button className="md:hidden" onClick={() => setOpen(o => !o)}>
          {open ? <X /> : <Menu />}
        </button>
      </div>
      {/* Mobile drawer */}
      {open && (
        <ul style={{ backgroundColor: '#1B2B4B', padding: '1rem 2rem' }}>
          {NAV_LINKS.map(l => (
            <li key={l.href}><a href={l.href} onClick={() => setOpen(false)}>{l.label}</a></li>
          ))}
        </ul>
      )}
    </nav>
  )
}
```

### Footer with License Number

```tsx
// app/_components/SiteFooter.tsx
export function SiteFooter() {
  return (
    <footer style={{ backgroundColor: '#1B2B4B', color: '#FAF9F7', padding: '2rem' }}>
      <p>Teressa Copeland</p>
      {/* TODO: Verify license number before launch — last two chars may be "00" (zeros) or "OO" (letters O) */}
      <p>Utah Real Estate License: 14196185-SA00</p>
      <p>&copy; {new Date().getFullYear()} Teressa Copeland Homes. All rights reserved.</p>
    </footer>
  )
}
```

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| `useFormState` from `react-dom` | `useActionState` from `react` | React 19 (2024) | Old import throws; use new hook |
| API Routes for form submit | Server Actions with `action={serverAction}` | Next.js 14+ | No `fetch`, no `/api/` endpoint needed |
| `pages/` router | App Router (`app/`) | Next.js 13+ | Project already uses App Router from Phase 1 |
| `<img>` tags | `next/image` | Next.js 10+ | Auto-optimization, lazy loading, responsive |

**Deprecated/outdated:**
- `useFormState` from `react-dom/server`: Removed in React 19. Use `useActionState` from `react`.
- Pages Router API routes for forms: Superseded by Server Actions in App Router.

---

## Open Questions

1. **SMTP provider / Gmail App Password**
   - What we know: CONTEXT.md locked `CONTACT_EMAIL_USER` and `CONTACT_EMAIL_PASS` as env vars
   - What's unclear: Whether Teressa uses Gmail (app password), Namecheap hosting email, or another provider — affects `host`/`port` config
   - Recommendation: Add `CONTACT_SMTP_HOST` and `CONTACT_SMTP_PORT` env vars so the planner does not hardcode a provider. Document Gmail setup as the example (host: smtp.gmail.com, port: 587, secure: false, requires App Password with 2FA enabled).

2. **Lucide React installation status**
   - What we know: CONTEXT.md says "use Lucide React (already likely available)" but it does not appear in package.json dependencies
   - What's unclear: Whether it was installed but not listed, or needs to be added
   - Recommendation: Plan task should `npm install lucide-react` as a safe no-op if already present.

3. **License number last two characters**
   - What we know: `14196185-SA00` — user flagged uncertainty whether final chars are `00` (zeros) or `OO` (letters)
   - What's unclear: The correct legal license number
   - Recommendation: Add a `// TODO: VERIFY LICENSE NUMBER` comment in the footer code. Do not block implementation on this — it's a content swap before launch.

---

## Sources

### Primary (HIGH confidence)
- Next.js 16 App Router — Server Actions, `useActionState`, `next/image` patterns: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations
- React 19 `useActionState` API: https://react.dev/reference/react/useActionState
- Nodemailer official docs: https://nodemailer.com/about/
- Project package.json (confirmed versions): Next.js 16.2.0, React 19.2.4, Zod ^4.3.6, Tailwind ^4

### Secondary (MEDIUM confidence)
- CSS `scroll-behavior: smooth` + `scroll-margin-top` MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-margin-top
- Honeypot spam protection pattern — multiple community sources agree on `display:none` + `tabIndex={-1}` + `autoComplete="off"` approach

### Tertiary (LOW confidence — validate before implementation)
- Gmail SMTP settings (host, port, app password requirement) — widely documented but Teressa's actual provider unknown until confirmed

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — package.json confirms exact versions; no guessing
- Architecture: HIGH — Next.js App Router patterns are stable and well-documented
- Carousel pattern: HIGH — pure React useState/useEffect; no library dependency
- Server Action + Nodemailer: HIGH — Next.js official docs confirm pattern; Nodemailer is stable
- SMTP provider specifics: LOW — depends on Teressa's email provider, unknown at research time

**Research date:** 2026-03-19
**Valid until:** 2026-04-19 (30 days — Next.js 16 and Nodemailer are stable; Tailwind 4 and React 19 patterns are settling but unlikely to break)
