diff --git a/CONTENT.md b/CONTENT.md
new file mode 100644
index 0000000..f469630
--- /dev/null
+++ b/CONTENT.md
@@ -0,0 +1,121 @@
+# CONTENT - Copy Standards, Image Rules, and Asset Guidelines
+Author: Andre Cobham / Arising Media
+Updated: 2026-06-09
+
+## Writing Standard
+
+Reading level: 7th to 8th grade for service businesses, 10th to 12th grade for professional/B2B.
+
+Active voice. Short paragraphs (2 to 4 sentences max).
+
+Lead with the customer's problem, not the business's credentials.
+
+One clear CTA per section.
+
+No marketing jargon (synergize, leverage, best-in-class, cutting-edge, state-of-the-art).
+
+No filler phrases (In today's fast-paced world, Look no further, We pride ourselves on, Don't hesitate, Whether you are X or Y).
+
+Specificity beats superlatives. A timeframe, a material, a measurable result beats any adjective.
+
+## What We Never Write
+
+Em dashes or en dashes. Replace with a period, comma, colon, or the word "and" or "to".
+
+Invented numbers (satisfaction rates, years of experience, award claims) without client-verified proof.
+
+"Licensed" for any provisionally licensed or permit-holding clinician. Use "Provisionally Licensed" instead.
+
+Exclamation points (one per page maximum, in a CTA only).
+
+Passive voice as the default sentence structure.
+
+Verification check: `grep -rn '.\|–\|—\|–' . --include="*.html" --include="*.json"`. Result must be zero.
+
+## Tone by Sector
+
+Service businesses (trades, cleaning, home services): plain, direct, neighborhood-familiar.
+
+Healthcare / counseling: warm, clinical accuracy required, never overpromise outcomes.
+
+Professional / B2B / tech: peer-level, systems-oriented, results-focused.
+
+## Healthcare Credential Rules
+
+MHC-LP is Provisionally Licensed, not Licensed. LP stands for Limited Permit.
+
+Any permit holder must display: "Practices under the supervision of [Name], [Credential]" in the footer, about page, and auto-response email.
+
+Use "Provisionally Licensed" everywhere: credential row, why-cards, location page intros, section headers, auto-response emails, meta descriptions.
+
+Run grep check before launch: `grep -r "Licensed Professional" --include="*.html"` . result must be zero.
+
+Supervisor's credential must be accurate to the letter. LMHC not equivalent to LMHC-D. Confirm the current designation before publishing.
+
+When a credential changes (upgrade or advancement), update the source templates first (render.py, copy_library.py), rebuild the Docker container, then re-run verification checks.
+
+## Copy Structure
+
+One h1 per page. h2 for major sections. h3 for cards or items within a section. Never skip levels.
+
+Footer on every page: phone, hours, address, in identical format.
+
+Phone format: (###) ###-####. Hours: Monday to Friday: 8:00 AM to 5:00 PM (no dashes).
+
+Links use relative paths with .html extension for internal pages. External links include target="_blank" rel="noopener". Phone links use tel:+1 format. Email links use mailto:. Map links open in new tab.
+
+## Image Standards
+
+Format: WebP only in production. No JPGs or PNGs in the webroot.
+
+Every image has descriptive alt text or alt="" if decorative.
+
+loading="lazy" on every image except the above-the-fold hero.
+
+width and height attributes on every img tag to prevent layout shift.
+
+No people, no faces in any generated or stock imagery.
+
+Show the result (clean room, finished floor, complete installation), not the process or equipment.
+
+Hero images: unique per page, named hero-{page-slug}.webp.
+
+## Image Generation
+
+Preferred source: local ComfyUI (FLUX.1 Schnell) or Google Imagen API.
+
+Every generated image passes a vision validation check for people/faces before being saved.
+
+Prompt structure: camera angle + lens + subject + foreground detail + background + lighting + no people.
+
+Specify lens focal length and depth of field. Vague room names produce incoherent scenes.
+
+Never generate: people, faces, cleaning equipment, text overlaid on source, before/after states.
+
+No cleaning machines, vacuums, steam equipment, or hoses in any image.
+
+Show upright equipment only, not flat industrial models.
+
+Machines must look functional and modern, not dated or commercial-scale.
+
+## Image Size Targets
+
+Service card / thumbnail: max 900px wide, 78% quality, 30-80KB target.
+
+Hero image (page header): max 1400px wide, 80% quality, 50-180KB target.
+
+OG / social share image: max 1200px wide, 85% quality, under 150KB.
+
+Images over these targets are rejected at deploy time.
+
+## Prompt Engineering
+
+All prompts follow this structure: {camera angle} {lens} {subject description}, {foreground detail} sharp in foreground, {background} receding into bokeh, {lighting description}, no people, ultra-realistic {type} photography.
+
+This pattern produces correct depth, perspective, and scene geometry because it names every surface explicitly.
+
+Fix incoherent objects by naming every part of the frame: background walls, floor material, ceiling (if visible), and what recedes. Avoid vague room names ("office" without detail). Specify plain surfaces (cream painted wall, white drop ceiling) not implied ones.
+
+Inline negative elements in the prompt itself (no people, no machines, no text), not in a separate negative prompt.
+
+Do not use "wide shot" without a camera angle qualifier.
\ No newline at end of file
diff --git a/OPTIMIZATION.md b/OPTIMIZATION.md
new file mode 100644
index 0000000..75ad181
--- /dev/null
+++ b/OPTIMIZATION.md
@@ -0,0 +1,616 @@
+# OPTIMIZATION - Mobile Responsive, SEO, Testing, and Performance
+Author: Andre Cobham / Arising Media
+Updated: 2026-06-09
+
+## Mobile
+
+Mobile-first CSS. Default styles target 320px and up.
+
+Breakpoints: 360px, 480px, 600px, 768px, 900px, 1023px, 1024px.
+
+Switch to mobile nav at max-width: 1023px, not 768px. A typical header does not fit cleanly below 1024px.
+
+Inline grid styles (style="display:grid;grid-template-columns:1fr 1fr") require !important overrides in media queries to collapse on mobile. Include override block at end of main.css.
+
+Always set: html, body { overflow-x: clip; max-width: 100%; }
+
+Form fields (input, select, textarea) require min-width: 0 and box-sizing: border-box on mobile or they push layout wider than viewport.
+
+Touch targets: 44x44px minimum (WCAG, Apple HIG).
+
+### Verification . Before Declaring Done
+
+Always run a Playwright check at multiple viewport widths. Save the script in
+`.planning/mobile_check.py`:
+
+```python
+from playwright.sync_api import sync_playwright
+
+PAGES = ['/', '/about/', '/services/', '/locations/buffalo.html', '/contact/']
+
+with sync_playwright() as p:
+ b = p.firefox.launch(headless=True)
+ for w in [320, 360, 390, 768, 900, 1023, 1024, 1200]:
+ ctx = b.new_context(viewport={'width': w, 'height': 800})
+ page = ctx.new_page()
+ for path in PAGES:
+ page.goto(f'http://localhost:8096{path}', wait_until='networkidle')
+ r = page.evaluate('() => ({sw: document.documentElement.scrollWidth, cw: document.documentElement.clientWidth})')
+ diff = r['sw'] - r['cw']
+ status = 'OK' if diff <= 0 else f'OVERFLOW +{diff}px'
+ print(f' w={w} {path:<35} {status}')
+ ctx.close()
+ b.close()
+```
+
+Result must be zero overflow on every page at every width.
+
+### Animation on Mobile
+
+Scroll-triggered animations (`data-animate="up"` etc.) work the same on mobile.
+But if you take a full-page screenshot for review, force-trigger them first:
+
+```python
+page.evaluate("() => document.querySelectorAll('[data-animate]').forEach(e => e.classList.add('in-view'))")
+page.wait_for_timeout(500)
+```
+
+Otherwise the screenshot shows blank sections that are actually hiding behind
+the IntersectionObserver.
+
+### Touch Targets
+
+- Tap targets must be at least 44x44px (Apple HIG, WCAG)
+- Header menu button: 44px square minimum
+- Form submit buttons: padding 0.875rem vertical minimum
+- Phone-link CTAs: same
+
+### Test Devices
+
+When the site is "done", verify on:
+- Real iPhone (Safari) . test the form actually submits
+- Real Android phone (Chrome) . same
+- Tablet (iPad) . header switches to mobile menu cleanly at 1023px and below
+- Desktop (any browser) . full nav, hover states work
+
+---
+
+## SEO, Meta, and Schema
+
+Every page on every site must include the full set of head tags below.
+No exceptions.
+
+### Required `
` Tags (Every Page)
+
+```html
+
+
+
+
+ {Page-specific title under 60 chars} | {Brand}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### OG Image
+
+One default OG image at `/assets/images/og-default.jpg`:
+- 1200x630px
+- Brand colors
+- Logo + company name + city + one descriptor (e.g., "Hardwood Floor Refinishing . Buffalo, NY")
+- Under 200KB optimized
+
+For pages with their own hero image (location, service detail), use that
+image's webp/jpg version as the OG image instead of the default.
+
+### Schema.org JSON-LD
+
+#### Home Page . LocalBusiness
+
+```json
+{
+ "@context": "https://schema.org",
+ "@type": "LocalBusiness",
+ "@id": "https://{domain}/#business",
+ "name": "{Legal business name}",
+ "url": "https://{domain}",
+ "telephone": "+1{10digits}",
+ "email": "{contact email}",
+ "address": {
+ "@type": "PostalAddress",
+ "streetAddress": "{street}",
+ "addressLocality": "{city}",
+ "addressRegion": "{state}",
+ "postalCode": "{zip}",
+ "addressCountry": "US"
+ },
+ "geo": {
+ "@type": "GeoCoordinates",
+ "latitude": {lat},
+ "longitude": {lng}
+ },
+ "areaServed": [
+ { "@type": "City", "name": "Buffalo" },
+ { "@type": "City", "name": "Amherst" }
+ ],
+ "openingHoursSpecification": [{
+ "@type": "OpeningHoursSpecification",
+ "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
+ "opens": "08:00",
+ "closes": "17:00"
+ }],
+ "aggregateRating": {
+ "@type": "AggregateRating",
+ "ratingValue": "4.9",
+ "reviewCount": "47"
+ }
+}
+```
+
+#### Service Detail Pages . Service
+
+```json
+{
+ "@context": "https://schema.org",
+ "@type": "Service",
+ "serviceType": "{Service name}",
+ "provider": { "@id": "https://{domain}/#business" },
+ "areaServed": { "@type": "AdministrativeArea", "name": "Erie County, NY" },
+ "url": "https://{domain}{path}"
+}
+```
+
+#### Location Pages . LocalBusiness with areaServed Override
+
+Same as the home `LocalBusiness` schema but override `areaServed` to the
+specific city, and include a different `@id` per page.
+
+#### Every Page . BreadcrumbList
+
+```json
+{
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ "itemListElement": [
+ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://{domain}/" },
+ { "@type": "ListItem", "position": 2, "name": "Services", "item": "https://{domain}/services/" },
+ { "@type": "ListItem", "position": 3, "name": "Floor Refinishing" }
+ ]
+}
+```
+
+#### FAQ Pages or Sections . FAQPage
+
+```json
+{
+ "@context": "https://schema.org",
+ "@type": "FAQPage",
+ "mainEntity": [{
+ "@type": "Question",
+ "name": "How long does refinishing take?",
+ "acceptedAnswer": { "@type": "Answer", "text": "Most rooms take 2 to 3 days..." }
+ }]
+}
+```
+
+### robots.txt
+
+Every site needs one at `/robots.txt`:
+
+```
+User-agent: *
+Allow: /
+Disallow: /api/
+
+Sitemap: https://{domain}/sitemap.xml
+```
+
+### sitemap.xml
+
+Generate at the end of every build. Save at `/sitemap.xml`. One `` entry
+per page. Use `` from the file's mtime.
+
+```xml
+
+
+
+ https://{domain}/
+ 2026-05-08
+ 1.0
+
+
+ https://{domain}/services/floor-refinishing.html
+ 2026-05-08
+ 0.8
+
+
+```
+
+After deploy, submit the sitemap to Google Search Console.
+
+### Title and Description Rules
+
+- **Title** under 60 characters. Format: `{Service} | {Brand} . {City}, {State}`
+- **Description** 150-160 characters, action-oriented, include city + service, phone when possible
+- Never use the same title/description on multiple pages
+- City + service name in title for location and service pages (huge SEO impact)
+- No em-dashes in meta tags. Use `|` pipe as brand separator
+- Always include `` . 5-10 comma-separated terms, city + service variants
+
+### Title/Meta Examples (Lahrcarpetcleaning.com Reference)
+
+Homepage:
+```html
+Lahr Carpet Cleaning | Residential & Commercial Carpet Cleaning . Finger Lakes, NY
+
+
+
+```
+
+Service page:
+```html
+Carpet Cleaning | Lahr Carpet Cleaning . Waterloo, NY
+
+
+```
+
+Location page:
+```html
+Carpet Cleaning in Seneca Falls, NY | Lahr Carpet Cleaning
+
+
+```
+
+### Audit Before Launch
+
+```bash
+# All pages have title
+grep -L '' site/**/*.html
+
+# All pages have canonical
+grep -L ' {One-line description of what the site does}
+
+{2-3 sentence description of the business, services, and target audience.}
+
+## Services
+- {Service 1}
+- {Service 2}
+
+## Pages
+- /: Homepage
+- /about/: About
+- /contact/: Contact
+
+## Not for external use
+- /api/: Internal API endpoints
+- /account/: User account pages
+```
+
+**robots.txt must also disallow /api/ and private paths** . llms.txt is the invitation, robots.txt is the boundary.
+
+---
+
+## Testing and Verification
+
+Before declaring a site done, run every check in this document and show the
+output. No exceptions.
+
+### The Law
+
+Before stating ANYTHING works, is configured, is ready, is installed, or is
+complete . run a live test and show the raw output. No output = it was not
+tested. Not for plugins, not for logins, not for deploys. Test it. Show the proof.
+
+### Build Verification
+
+After running build scripts:
+
+```bash
+# Zero unreplaced placeholders
+grep -rn "{{" site/locations/*.html site/services/*.html
+# Result: empty
+```
+
+### Container Health
+
+```bash
+docker compose ps
+# All services "Up" and (healthy) where applicable
+
+docker logs {project}-api-1 2>&1 | tail -20
+# No errors, no stack traces
+
+curl -s -o /dev/null -w "%{http_code}\n" http://localhost:{port}/
+# 200
+```
+
+### URL Surface Check
+
+Every public URL returns 200. Every sensitive URL returns 404.
+
+```bash
+# Public . should all be 200
+for p in "/" "/about/" "/services/" "/locations/" "/locations/buffalo.html" \
+ "/services/floor-refinishing.html" "/contact/" "/reviews/" \
+ "/assets/css/main.css" "/components/header.html" "/api/health"; do
+ curl -s -o /dev/null -w "%{http_code} ${p}\n" http://localhost:{port}${p}
+done
+
+# Sensitive . should all be 404
+for p in "/Dockerfile" "/nginx.conf" "/.env" "/api/.env" "/.git/HEAD" \
+ "/build_locations.py" "/docker-compose.yml" "/README.md"; do
+ curl -s -o /dev/null -w "%{http_code} ${p}\n" http://localhost:{port}${p}
+done
+```
+
+### Container Content Check
+
+Confirm sensitive files are not inside the nginx container:
+
+```bash
+docker exec {project}-web-1 ls /usr/share/nginx/html/
+# Only public folders + index.html. No api/, no Dockerfile, no .env
+
+docker exec {project}-web-1 ls /usr/share/nginx/html/api/ 2>&1
+# Result: "No such file or directory" (api was correctly excluded from web image)
+```
+
+### Mobile Responsive Check (Playwright)
+
+```python
+from playwright.sync_api import sync_playwright
+
+PAGES = ['/', '/about/', '/services/', '/services/floor-refinishing.html',
+ '/locations/', '/locations/buffalo.html', '/contact/', '/reviews/']
+
+with sync_playwright() as p:
+ b = p.firefox.launch(headless=True)
+ fails = 0
+ for w in [320, 360, 390, 768, 900, 1023, 1024, 1200]:
+ ctx = b.new_context(viewport={'width': w, 'height': 800})
+ page = ctx.new_page()
+ for path in PAGES:
+ page.goto(f'http://localhost:{port}{path}', wait_until='networkidle')
+ r = page.evaluate('() => ({sw: document.documentElement.scrollWidth, cw: document.documentElement.clientWidth})')
+ diff = r['sw'] - r['cw']
+ status = 'OK' if diff <= 0 else f'OVERFLOW +{diff}px'
+ print(f' w={w} {path:<40} {status}')
+ if diff > 0: fails += 1
+ ctx.close()
+ print(f'\nfails: {fails}')
+ b.close()
+```
+
+Result must be `fails: 0` across every page at every width.
+
+### Form Submission End-to-End
+
+After Resend domain is verified:
+
+```bash
+# Validation rejection test
+curl -s -X POST http://localhost:{port}/api/estimate \
+ -H "Content-Type: application/json" \
+ -d '{"name":"","email":"bad"}'
+# Expected: {"error":"Validation failed.","fields":[...]}
+
+# Real submission test
+curl -s -X POST http://localhost:{port}/api/estimate \
+ -H "Content-Type: application/json" \
+ -d '{"name":"Test","email":"acobham@arisingmedia.us","phone":"(716) 555-1234","address":"100 Test St","city":"Buffalo","zip":"14201","service":"refinishing","message":"E2E test","token":""}'
+# Expected: {"ok":true}
+
+# Verify the email actually arrived in the destination inbox
+```
+
+### Idempotency Test
+
+Send the same payload twice. Both should return `{"ok":true}`. Inbox should
+receive only ONE email (Resend deduplicates via Idempotency-Key).
+
+### Rate Limit Test
+
+Send 6 requests rapidly from the same IP:
+```bash
+for i in 1 2 3 4 5 6; do
+ curl -s -X POST http://localhost:{port}/api/estimate \
+ -H "Content-Type: application/json" \
+ -d "{\"name\":\"RL$i\",\"email\":\"a@b.co\",\"phone\":\"(716) 555-1234\",\"address\":\"x\",\"service\":\"x\",\"token\":\"\"}"
+ echo ""
+done
+# Requests 1-5: 200 or 422
+# Request 6: 429 Too Many Requests
+```
+
+### SEO Surface
+
+```bash
+# Every page has
+grep -L "" site/**/*.html
+# Result: empty
+
+# Every page has canonical
+grep -L 'rel="canonical"' site/**/*.html
+
+# Every page has og:title
+for f in $(find site -name "*.html"); do
+ grep -q 'property="og:title"' "$f" || echo "MISSING og: $f"
+done
+
+# robots.txt and sitemap.xml exist and are served
+curl -s http://localhost:{port}/robots.txt | head -5
+curl -s http://localhost:{port}/sitemap.xml | head -10
+```
+
+### Dash Check (Content Rule)
+
+```bash
+grep -rn '.\|–\|—\|–' site/ --include="*.html" --include="*.json"
+# Result: empty
+```
+
+### Lighthouse / PageSpeed (Recommended)
+
+Run https://pagespeed.web.dev/ on the live URL after launch. Targets:
+- Performance: 90+
+- Accessibility: 95+
+- Best Practices: 95+
+- SEO: 100
+
+If accessibility is below 95, common fixes:
+- Color contrast on body text (WCAG AA = 4.5:1 for normal text)
+- Form labels associated with inputs (`