# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Changed
- copy: switch all German UI copy from formal "Sie/Ihr" to informal "Du/Dein" — covers i18n.py (~60 keys), planner wizard step titles/subtitles, export waitlist page, quote wizard steps, quote submitted/verify pages, directory supplier detail, directory results partial, supplier signup step 4, supplier waitlist confirmed page
- copy: replace "Platz-Anbieter" with "Anbieter" in CTAs; "Anlage" → "Padel-Platz" in planner wizard step 1 title/subtitle and planner translations (wiz_venue, sl_budget_target); "Anlageplanung" → "Padelplatz-Planung" in service checklist
- copy: update directory H1 to SEO multi-term "Padelplatz-Hersteller, Platzbauer & Anbieter"; subheading now mentions Hersteller, Platzbauer, schlüsselfertige Lösungen
- copy: fix `mkt_no_results` and `mkt_search_placeholder` — replaced incorrect "Artikel" with "Märkte" in German and English (markets page shows market pages like Miami, not articles)

### Added
- i18n: translate `base.html`, `_cookie_banner.html` — "Manage Cookies", "About" footer links, feedback placeholder via `{{ t.key }}`; cookie banner heading/categories/descriptions/buttons; JS toggle text injected via `tojson` so "Manage"/"Close" states are also translated; `public/routes.py` feedback flash messages use `get_translations(g.lang)` keys
- i18n: expand `i18n.py` with ~300 UI template keys, ~200 planner JS locale strings (`_PLANNER_TRANSLATIONS`), ~35 CAPEX/OPEX item name translations (`_CALC_ITEM_NAMES`), plus `get_planner_translations()` and `get_calc_item_names()` functions

### Fixed
- i18n: `planner.html` used `{% if lang %}...{% block %}` nesting which Jinja2 forbids — restructured to `{% block title %}{% if lang == 'de' %}...{% endif %}{% endblock %}`
- ruff: unsorted import in `planner/routes.py` (new `get_planner_translations` import) — auto-fixed with `ruff --fix`

### Added
- i18n: localize planner — inject `window.__PADELNOMICS_LOCALE__` from server via `get_planner_translations(lang)`, add `const L / tr()` helpers in `planner.js`, replace all hardcoded English strings in TABS, WIZ_STEPS, all `buildInputs()`/`rebuildCapexInputs()`/`rebuildOpexInputs()` slider labels, `renderWith()`, `renderCapex()`, `renderOperating()`, `renderCashflow()`, `renderReturns()`, `renderMetrics()`, `renderSeasonChart()`, `resetToDefaults()`, `saveScenario()`, `renderWizNav()`, and `renderWizPreview()` with `tr('key', 'fallback')` calls
- i18n: localize `planner.html` — add `window.__PADELNOMICS_LOCALE__` script injection, translate wizard step titles/subtitles, toggle labels, chart/section headers, CTA sidebar and inline CTA, signup bar, scenario controls, metrics section headers, and page title/meta via `{% if lang == 'de' %}` and `{{ t.key }}` / `{{ planner_t.key }}`
- i18n: localize all export templates — `export.html`, `export_success.html`, `export_generating.html`, `export_waitlist.html` — all strings via `{{ t.key }}`, feature lists via `{% if lang == 'de' %}` conditionals
- i18n: localize `partials/scenario_list.html` — drawer title, default badge, Load/Delete buttons, updated label, empty state message via `{{ t.scenario_* }}`
- calculator: add `lang: str = "en"` parameter to `calc()`, import `get_calc_item_names`, replace all `ci()`/`oi()` hardcoded English names with `names["key"]` lookups, track `rent_amount` as local variable to replace name-based loop lookup for rentRatio
- routes: pass `lang` and `planner_t` to `planner.html` render context; pass `lang=lang` to `calc()` in both index and `/calculate` endpoints

- i18n: translate directory and leads templates — `directory.html`, `supplier_detail.html`, `partials/results.html`, `partials/enquiry_result.html`, `quote_request.html`, `quote_step_1–9.html`, `quote_submitted.html`, `quote_verify_sent.html` — short strings via `{{ t.key }}`, long paragraphs and context-sensitive text via `{% if lang == 'de' %}` conditionals, title/meta tags conditional per language
- i18n: translate supplier signup flow (`signup.html`, `signup_step_1–4.html`, `signup_success.html`), waitlist pages (`waitlist.html`, `waitlist_confirmed.html`), content templates (`markets.html`, `article_detail.html`, `market_results.html`), and all scenario partials (`scenario_summary`, `scenario_capex`, `scenario_cashflow`, `scenario_operating`, `scenario_returns`) — step labels via `{{ t.key }}`, all other strings via `{% if lang == 'de' %}` conditionals
- i18n: translate `landing.html`, `features.html`, and `about.html` to German — all short strings via `{{ t.key }}`, long paragraphs/FAQ answers via `{% if lang == 'de' %}` conditionals, JSON-LD structured data wrapped per language, title/meta blocks conditional

### Changed
- leads/routes.py: replace hardcoded `QUOTE_STEPS` list with `_get_quote_steps(lang)` function — step titles now use i18n keys so the progress bar shows translated step names; all public-facing `flash()` calls now use `get_translations(g.lang)` keys instead of hardcoded English strings

### Fixed
- Nav: hamburger button was trapped inside `.nav-links--right`; parent `display: none` on mobile hid it too — moved hamburger to be first child of `.nav-inner`; sign-in button added as always-visible mobile slot (`.nav-auth-mobile`) on the right; mobile grid is now `auto 1fr auto` (hamburger | logo | sign-in)
- Nav: align `.nav-inner` width with `container-page` — changed `max-width` from 80rem to 72rem (`--container-6xl`) and matched responsive padding (`1rem` / `1.5rem` / `2rem` at mobile / sm / lg) so nav edges are flush with page content and footer
- Planner: "Get Supplier Quotes" button now navigates to the correct lang-prefixed URL (`/en/leads/quote` etc.) — hardcoded `/leads/quote` caused a 404 on prod; URL is now injected from the server via `window.__PADELNOMICS_QUOTE_URL__`

### Added
- Nav: hamburger menu on screens < 900px — clicking opens a full-width mobile panel with all nav links; overlay click and Escape key close it
- Nav: mobile panel groups links under "Plan", "Explore", and "Account" section headers

### Changed
- Nav: widen container from 72rem (1152px) to 80rem (1280px) — matches Zillow's nav container width, more breathing room for nav items on large monitors
- Nav: collapse breakpoint raised from 768px to 899px — nav links no longer hide until the screen is actually too narrow
- Nav: remove redundant inline `style="display:grid;grid-template-columns:1fr auto 1fr"` on `.nav-inner` (already in CSS)

### Fixed
- i18n: improve German nav labels — "Verzeichnis" → "Anbieterverzeichnis", "Planer" → "Kostenrechner"
- CI: add missing env vars to `.env` heredoc — `WAITLIST_MODE`, `LEADS_EMAIL`, `UMAMI_API_URL`; make Paddle vars optional (`:-`) so they don't break deploys when unset

### Changed
- Legal pages: replaced home address with virtual office address (c/o COCENTER, Koppoldstr. 1, 86551 Aichach) in all four legal templates (`imprint_de`, `imprint_en`, `privacy_de`, `privacy_en`, `terms_de`)

### Fixed
- Litestream: remove local-path replica — v0.5.8 dropped multi-replica support (`"multiple replicas on a single database are no longer supported"`), keeping only the R2 replica
- Litestream: extend retention from 7 days to 1 year (`8760h`) — WAL frames are tiny, R2 storage cost is negligible
- Deploy: gate deployment on litestream health (`kill -0 1`, retries 6×5s after 15s start period) so broken backups fail the deploy loudly
- Deploy: write nginx router config *before* starting containers so the router health check (`nginx -t`) passes on first deploy or after volume wipe
- Deploy: pre-migration DB backup added to `deploy.sh`; on health-check failure the DB is restored to pre-migration state (prevents old slot from running against new schema)
- Migrations: removed all `conn.commit()` and `executescript()` calls from `up()` functions in 0000, 0011, 0012, 0013, 0014, 0015 — restores batch-atomicity guarantee (`executescript` issued implicit COMMITs, breaking rollback on failure)
- Visual tests: server now builds schema via `migrate()` instead of the deleted `schema.sql`; all 12 Playwright tests pass
- Visual tests: updated assertions to match current landing page (text logo replacing img, `.roi-calc` replacing `.teaser-calc`, `hero-dark`/`cta-card` allowed as intentional dark sections, card count ≥ 6, i18n-prefixed logo href, h3 brightness threshold relaxed to 150)
- CSS: removed dead `.nav-logo { line-height: 0 }` rule (was for image logo, collapsed text logo to zero height); removed dead `.nav-logo img` rule
- Ruff: fixed 49 lint errors across `src/` and `tests/` (unused imports, unused variables, unsorted imports, bare f-strings, ambiguous variable name `l`)

### Added
- Litestream: R2 replication config with env-var placeholders (`LITESTREAM_R2_BUCKET` etc.) — fill in GitLab CI variables to enable off-host backup; handles new server, deleted volume, or disaster recovery via auto-restore on container startup
- `.env.example`: Litestream R2 vars documented

### Added
- i18n URL prefixes: all public-facing blueprints (`public`, `planner`, `directory`, `content`, `leads`, `suppliers`) now live under `/<lang>/` (e.g. `/en/`, `/de/`); internal blueprints (`auth`, `dashboard`, `admin`, `billing`) unchanged; root `/` detects language from cookie / Accept-Language header and 301-redirects; legacy URLs (`/terms`, `/privacy`, etc.) redirect to `/en/` equivalents
- German legal pages: full DSGVO-compliant `Datenschutzerklärung` (`/de/privacy`), `AGB` (`/de/terms`), and `Impressum` (`/de/imprint`) per § 5 DDG — populated with Hendrik Dreesmann's details, Kleinunternehmer § 19 UStG, Oldenburg address
- Rewritten English legal pages (`/en/terms`, `/en/privacy`, `/en/imprint`) with expanded GDPR sections, correct controller identity, proper data-processing details (Umami self-hosted, Paddle, Resend with SCCs), and German-law jurisdiction
- Language toggle (`EN | DE`) in footer; hreflang `en`, `de`, and `x-default` tags in `<head>` on all lang-prefixed pages
- `lang` cookie (1-year, SameSite=Lax) persisted on first visit; `lang` and `t` (translation dict) injected into every template context
- `i18n.py`: flat translation dicts for ~20 nav/footer keys in `en` and `de`; `LANG_BLUEPRINTS` and `SUPPORTED_LANGS` constants
- `sitemap.xml` and `robots.txt` moved to app-level root routes (not under `/<lang>`); sitemap now includes both language variants of every SEO page
- Cookie consent banner: fixed bottom bar with "Accept all" and "Manage preferences" (toggle for functional/A/B cookies); consent stored in `cookie_consent` cookie for 1 year; "Manage Cookies" link added to footer Legal section

### Changed
- Defer Paddle.js loading to only the 3 pages that use checkout (export, supplier signup, supplier dashboard) — removed from global `base.html` head; all other pages no longer receive Paddle's third-party cookies
- Gate A/B test cookie (`ab_*`) on functional cookie consent: variant is still picked per-request for rendering, but the cookie is only persisted when the visitor has accepted functional cookies
- Privacy policy section 6 (Cookies): full disclosure of all cookie categories (essential, functional, payment); fix "Plausible" → "Umami" in service providers list

### Changed
- Auto-create Resend audiences per blueprint: `capture_waitlist_email()` now derives the audience name from `request.blueprints[0]` (e.g., `waitlist-auth`, `waitlist-suppliers`) and lazily creates audiences via the Resend API on first use, caching IDs in a new `resend_audiences` table; removes `RESEND_AUDIENCE_WAITLIST` env var — only `RESEND_API_KEY` needed

### Added
- Waitlist mode (lean startup smoke test): `WAITLIST_MODE` config flag intercepts `/auth/signup`, `/suppliers/signup`, and `/planner/export` to capture emails or show "coming soon" messaging before Paddle billing is ready; confirmation emails sent via Resend; optional `RESEND_AUDIENCE_WAITLIST` for bulk launch blast; flip flag to `false` and all flows revert to normal

### Changed
- Redesign padel racket SVG logo/favicon: filled dark silhouette with white punched-through holes (3×3 grid), proper throat tapering to handle at bottom, grip tape lines — replaces outline-only stroke approach; bump favicon cache to v4

### Added
- Simple A/B testing with `@ab_test` decorator and Umami `data-tag` integration
- SEO defaults in `base.html`: canonical, og:url, og:type, og:image (logo fallback), og:title, og:description, twitter:card — every page gets these automatically, child templates override as needed
- `robots.txt` route: disallows `/admin/`, `/auth/`, `/dashboard/`, `/billing/`, `/directory/results`; includes Sitemap reference
- Meta descriptions and OG tags on all public pages: about, terms, privacy, pricing, features, suppliers, directory, supplier detail
- `og:image` on landing and features pages pointing to planner screenshot
- JSON-LD Organization schema on homepage and supplier detail pages
- JSON-LD FAQPage schema on homepage (5 FAQ entries)
- JSON-LD Article schema on article detail pages
- Sitemap supplier slugs: all supplier detail pages now indexed
- Sitemap `<lastmod>` on all entries (static pages: today, articles: `COALESCE(updated_at, published_at)`, suppliers: `created_at`)
- `rel="preconnect"` for Google Fonts to reduce font-load latency
- `X-Robots-Tag: noindex` header on `/directory/results` HTMX partial

### Fixed
- Render-blocking Paddle.js: added `defer` attribute and wrapped `Paddle.Initialize` in `DOMContentLoaded` listener
- Render-blocking Chart.js on planner page: added `defer` attribute
- Broken `og:image` on planner page (`og-planner.png` → `planner-screenshot.png`)
- Homepage meta description trimmed to under 155 characters
- Duplicate `og:url` and `canonical` tags removed from landing, markets, and article detail pages

### Changed
- Rewrote supplier page hero: pain-first headline ("Stop Chasing Cold Leads"), differentiator copy, micro-proof under CTA
- Added "Problem With Finding Padel Clients Today" section with trade show / Google Ads / cold outreach pain points
- Tightened "How It Works" step titles: "Claim Your Listing", "Browse Pre-Qualified Leads", "Win Projects Faster"
- Added Basic tier (€39/mo / €29/mo yearly) to public pricing section on suppliers page
- Pricing section now shows 3-column grid (Basic / Growth / Pro) with CSS-only billing period toggle (monthly / yearly, no JS)
- Added "How We Compare" table: Padelnomics vs trade show / Google Ads / cold directory
- Upgraded social proof section: two testimonial cards with decorative quote mark, stat bar in subtitle
- Updated supplier FAQ: three tiers with correct prices (€39/€199/€499), two new entries (what makes leads different, pricing vs alternatives), removed stale €149/€399 figures
- Strengthened final CTA: "Your Next Client Is Already Building a Business Plan"
- Fixed stale prices in landing page FAQ: Basic €39/mo, Growth €199/mo, Pro €499/mo (was €149/€399)

### Changed
- Redesigned directory cards: image-first 16:9 layout with cover photos, frosted category badge, CSS court/grid placeholders, and logo avatar straddling the media/body border
- 4-tier visual ladder: Free (62% opacity, grey placeholder, unverified chip) → Basic (full opacity, green placeholder, verified chip, description, "View Listing →") → Growth (green placeholder, Growth chip + stats, "Request Quote →") → Pro (green 2.5px top border, CSS court media, full stats, green hover glow)
- Pro card media defaults to CSS court visualization when no cover image; Growth/Basic default to dark-green grid placeholder

### Added
- Cover image upload for suppliers in the dashboard listing form (saves to `static/uploads/covers/`, 16:9 thumbnail preview)
- Migration 0013: `cover_image TEXT` column on suppliers table

### Added
- **Basic subscription tier** — verified directory listing with contact info, services checklist, social links, and enquiry form; no lead credits
- **Monthly + yearly billing** — all paid supplier tiers now offer yearly pricing with annual discount (Basic: €349/yr, Growth: €1,799/yr, Pro: €4,499/yr)
- **Billing period toggle** in supplier signup wizard — monthly/yearly pill switch, defaults to yearly; price cards update in real time via CSS sibling selectors
- **Redesigned supplier detail page** — navy hero section with court-grid CSS pattern, two-column body (main + 320px sidebar), contact card with avatar/role/social links, stats grid, services checklist, enquiry form for Basic+ listings, tier-adaptive CTA strip
- **Supplier enquiry form** on Basic+ listing pages — HTMX-powered inline form, rate-limited at 5 per email per 24 h, email relayed to supplier via worker task
- **New DB columns on suppliers**: `services_offered`, `contact_role`, `linkedin_url`, `instagram_url`, `youtube_url`
- **`supplier_enquiries` table** for tracking incoming enquiries from listing pages
- **Basic card variant** in directory results — shows verified badge, logo, website, short description; sits between growth and free in sort order
- **Dashboard access for Basic tier** — overview + listing tabs; leads/boosts tabs hidden; upgrade CTA in sidebar
- **Listing form: new fields** — services offered (multi-select checkboxes), contact role, LinkedIn/Instagram/YouTube social links
- **Admin: Basic tier support** — tier dropdown updated, new fields in supplier form and detail view, enquiry count shown

### Changed
- Supplier Growth monthly price adjusted to €199/mo (yearly: €150/mo billed at €1,799/yr)
- Supplier Pro monthly price adjusted to €499/mo (yearly: €375/mo billed at €4,499/yr)
- Directory sort order updated: pro → growth → basic → free
- `_supplier_required` decorator now grants access to basic, growth, and pro tiers
- New `_lead_tier_required` decorator restricts lead feed, unlock, and dashboard leads to growth/pro only

### Fixed
- **Supplier detail page: locked quote CTA** — "Request Quote" button is now
  visually disabled (greyed-out) for unverified/free-tier suppliers; clicking
  opens an inline popover explaining the limitation and linking to the general
  quote wizard instead
- **Supplier signup Paddle checkout** — form now intercepts submit, fetches
  checkout config via JS, and opens `Paddle.Checkout.open()` overlay instead
  of displaying raw JSON in the browser
- **Credit balance OOB updates** — sidebar and lead feed header credits now
  update instantly via HTMX OOB swaps after unlocking a lead (no page refresh)
- **Boosts page layout** — capped main content column at 720px on wide screens;
  credit card grid uses `auto-fill` for graceful responsive adaptation

### Added
- **Listing preview live update** — form fields (name, tagline, description,
  website) trigger HTMX `hx-get` with 500ms debounce to update the directory
  card preview in real time; new `/dashboard/listing/preview` endpoint and
  extracted `dashboard_listing_preview.html` partial
- **Lead cards: full qualification data** — unlocked cards now show decision
  process, prior supplier contact, financing help preference, with clear
  section headers (Project, Location & Timeline, Readiness, Contact) and
  human-readable enum labels; includes "View their plan" link to linked scenario
- **Lead feed search bar** — text input with 300ms debounced HTMX filtering on
  country, facility type, and additional info; mirrors directory search pattern
- **Phone number mandatory in quote form** — step 9 now requires phone with
  HTML `required` attribute and server-side validation
- **Supplier-aware dashboard redirect** — `/dashboard/` checks if user has a
  claimed supplier with paid tier and redirects to supplier dashboard
- **Inline SVG logo** — replaced PNG logo with inline SVG padel racket icon +
  Bricolage Grotesque wordmark in navbar and footer

### Changed
- **Planner fonts** — replaced Inter with DM Sans for body text, JetBrains Mono
  with Commit Mono for numeric values, added Bricolage Grotesque for planner
  header and wizard step titles; loaded Commit Mono via fontsource CDN
- **Migration system: single source of truth** — eliminated dual-maintenance
  of `schema.sql` + versioned migrations; all databases (fresh and existing)
  now replay migrations in order starting from `0000_initial_schema.py`;
  removed `schema.sql`, `_is_fresh_db()`, and the fresh-DB fast-path that
  skipped migration execution; `migrate()` accepts an optional `db_path`
  parameter for direct use in tests; test fixtures use cached migration replay
  instead of loading `schema.sql` directly; removed fragile `_old_schema_sql()`
  test helper and `TestMigration0001` class; template repo updated to match
  (deleted `0001_roles_and_billing_customers.py`, projects own their migrations)
- **Design system: Bricolage Grotesque + DM Sans** — replaced Inter with
  Bricolage Grotesque (display headings) and DM Sans (body text); added
  `--font-display` theme variable; headings use display font via
  `font-family: var(--font-display)` in base layer; added `--color-forest`
  (#064E3B) to theme palette
- **Glass navbar** — replaced opaque white navbar with semi-transparent
  `backdrop-filter: blur(14px)` frosted glass effect
- **Landing page: dark hero** — navy background with radial blue glow,
  white text, green badge on dark, white ROI calculator card with stronger
  shadow; hero section is now full-width outside the container
- **Landing page: journey timeline** — replaced 5 left-border cards with
  numbered step track (01-05) with connecting line, active/upcoming states;
  CSS grid 5-col desktop, stacks to horizontal layout on mobile
- **Landing page: dark CTA card** — replaced plain white CTA section with
  rounded navy card with noise texture and white inverted button
- **Directory card tiers** — pro cards get stronger green left border +
  subtle box-shadow glow and 48px logo; featured badges more prominent with
  box-shadow; free/unclaimed cards more visibly muted (lower opacity, lighter
  border)
- **Supplier dashboard sidebar icons** — added inline SVG icons (chart,
  inbox, building, rocket) to sidebar navigation links
- **Supplier dashboard lead cards** — added heat-color left borders
  (red/amber/gray by heat score) on `.lf-card`

### Added
- **Admin sidebar navigation** — new `base_admin.html` template with
  persistent sidebar (Overview, Leads, Suppliers, Users, Content, System
  sections); Heroicons inline SVGs for each nav item; active state via
  `{% set admin_page %}` in child templates; mobile: horizontal scroll nav;
  all 20 admin templates now extend `base_admin.html`
- **Admin dashboard section labels** — stat card groups labeled "Lead
  Funnel" and "Supplier Funnel" with color-coded left borders (blue for
  leads, green for suppliers)

### Fixed
- **Hardcoded Inter on supplier unlock button** — `.lf-unlock-btn` used
  `font-family: 'Inter'`; changed to `inherit` so it picks up DM Sans

### Changed
- **Admin auth: password → RBAC** — replaced `ADMIN_PASSWORD` env var and
  session-based password login with role-based access control; admin access is
  now granted via `ADMIN_EMAILS` env var (comma-separated); on login/dev-login,
  matching emails auto-receive the `admin` role; removed `/admin/login` and
  `/admin/logout` routes, `admin_required` decorator, and `login.html` template;
  all admin routes now use `@role_required("admin")` from `auth/routes.py`
- **Billing: separated billing identity from subscriptions** — new
  `billing_customers` table stores `provider_customer_id` (was on
  `subscriptions.paddle_customer_id`); subscriptions table renamed
  `paddle_subscription_id` → `provider_subscription_id` and dropped `UNIQUE`
  constraint on `user_id` (allows multiple subscriptions per user);
  `upsert_subscription` now finds existing rows by `provider_subscription_id`
  instead of `user_id`; webhook handler calls `upsert_billing_customer()` for
  all subscription events
- **Eager-loaded user context** — `load_user()` now JOINs `billing_customers`,
  `user_roles`, and latest subscription in a single query; adds `g.subscription`
  and `is_admin` template context variable (replaces `session.get('is_admin')`)

### Added
- **RBAC decorators** — `role_required(*roles)`, `subscription_required(plans,
  allowed)`, `grant_role()`, `revoke_role()`, `ensure_admin_role()` in
  `auth/routes.py`
- **`user_roles` table** — stores user-role pairs with `UNIQUE(user_id, role)`
- **`billing_customers` table** — stores provider customer ID per user
- **`ADMIN_EMAILS` config** — parsed from comma-separated env var in `core.py`
- **Migration 0011** — adds `user_roles` and `billing_customers` tables,
  migrates `paddle_customer_id` data, recreates subscriptions table with
  `provider_subscription_id` column and no `UNIQUE` on `user_id`

### Removed
- `ADMIN_PASSWORD` env var and password-based admin authentication
- `/admin/login` and `/admin/logout` routes
- `admin/templates/admin/login.html` template
- `admin_required` decorator (replaced by `role_required("admin")`)
- `subscription_required` from `billing/routes.py` (replaced by version in
  `auth/routes.py` that reads from `g.subscription`)

### Fixed
- **Webhook crash on null `custom_data`** — Paddle sends `"custom_data": null`
  on lifecycle events (e.g. `subscription.updated`); `.get("custom_data", {})`
  returns `None` when the key exists with a null value, causing `AttributeError`
  on the next `.get()` call; switched to `or {}` fallback; also guarded
  `subscription.activated` to skip when `user_id` is missing (was inserting
  `user_id=0` causing FK violation), and applied same `or {}` to
  `current_billing_period`
- **Webhook signature verification uses SDK Verifier** — replaced manual HMAC
  implementation with `paddle_billing.Notifications.Verifier` via a lightweight
  request wrapper; same algorithm, fewer moving parts

### Added
- **Credit system test suite** (`tests/test_credits.py` — 24 tests) — covers
  `get_balance`, `add_credits`, `spend_credits`, `compute_credit_cost`,
  `already_unlocked`, `unlock_lead`, `monthly_credit_refill`, `get_ledger`;
  tests run against real in-memory SQLite with no mocking
- **Supplier webhook test suite** (`tests/test_supplier_webhooks.py` — 10 tests)
  — integration tests POSTing signed payloads to `/billing/webhook/paddle` and
  verifying DB state for credit pack purchases, sticky boosts, supplier
  subscription activation (growth/pro tiers, boost items, noop on missing
  supplier_id), and business plan PDF export

### Removed
- **Dead test** `test_mobile_nav_no_overflow` from `test_visual.py` — evaluated
  JS but never asserted the result; always passed regardless of overflow state

### Fixed
- **Supplier logo missing on public directory** — directory cards and supplier
  detail page only checked `logo_url` (external URL); now checks `logo_file`
  first (uploaded via dashboard), falling back to `logo_url`
- **Admin forward history links to wrong page** — supplier name in lead forward
  history linked to `/directory/` (public index) instead of
  `/admin/suppliers/<id>` (admin detail page)
- **Dashboard lead country filter drops heat/timeline state** — hidden inputs
  used mismatched names (`heat_hidden`/`timeline_hidden` vs `heat`/`timeline`),
  silently resetting other filters when country changed
- **Directory pagination drops region filter** — paginating after filtering by
  region lost the `region` parameter; added to all pagination link templates
- **`boost_card_color` missing from webhook handler** — `BOOST_PRICE_KEYS`
  omitted `boost_card_color`, so purchasing the card color boost via Paddle
  would silently fail to create the `supplier_boosts` record; also removed
  stale `boost_newsletter` entry (replaced by card color boost)
- **Sticky boost DB operations not atomic** — `boost_sticky_week` and
  `boost_sticky_month` handlers issued two bare `execute()` calls (INSERT +
  UPDATE) without a transaction; wrapped in `db_transaction()` to prevent
  partial writes

### Added
- **Clickable admin list rows** — entire row is clickable on leads, suppliers,
  and users admin list pages (`data-href` + JS click handler that skips
  links/buttons/forms); pointer cursor and hover highlight via CSS
- **Supplier impersonation** — "Impersonate Owner" button on admin supplier
  detail page; reuses existing `admin.impersonate` route, shown only when
  the supplier has a `claimed_by` user
- **Seed data: supplier owner accounts** — each claimed supplier now gets its
  own user + active subscription for realistic impersonation testing

### Fixed
- **Insufficient credits UX** — unlock error now shows credit balance, cost,
  and a "Buy Credits" CTA linking to Boost & Upsells tab (was a collapsed
  red error message with no action)
- **Supplier dashboard sidenav** — active tab highlight now updates on click
  (was only correct on initial page load)
- **Boost/credit buy buttons used wrong price IDs** — dashboard boosts tab
  passed internal keys (`boost_logo`, `credits_25`) as Paddle `priceId`
  instead of resolved Paddle price IDs; buttons now show "Not configured"
  when Paddle products aren't set up
- **Listing preview card stretched full width** — constrained to 420px to
  match actual directory card proportions
- **Impersonate redirects to supplier dashboard** — impersonating a user who
  owns a supplier now lands on `/suppliers/dashboard` instead of the generic
  demand-side dashboard

### Added — Programmatic SEO: Content Generation Engine
- **Database migration 0010** — `published_scenarios`, `article_templates`,
  `template_data`, `articles` tables with FTS5 full-text search on articles;
  content-sync triggers for INSERT/UPDATE/DELETE
- **Article template system** — parameterized content recipes with
  `input_schema` (JSON field definitions), `url_pattern`, `title_pattern`,
  `body_template` (Markdown + Jinja2); supports multiple content types
  (only `calculator` built now)
- **Template data rows** — per-city/region input data that feeds the
  generation pipeline; manual add or bulk CSV upload
- **Bulk generation pipeline** — `POST /admin/templates/<id>/generate`
  computes financial scenarios from data rows via `validate_state()` +
  `calc()`, renders Markdown with baked scenario cards, writes static HTML
  to `data/content/_build/`, creates articles with staggered publish dates
- **Published scenarios** — standalone financial widgets with pre-computed
  `calc_json`; embeddable in articles via `[scenario:slug]` markers with
  section variants (`:capex`, `:operating`, `:cashflow`, `:returns`, `:full`)
- **Scenario widget templates** — 6 partial templates (summary, CAPEX
  breakdown, revenue & OPEX, 5-year projection, returns & financing,
  full combined) with dark navy header bar, Commit Mono numbers, responsive
  metric grids, and "Try with your own numbers" CTA
- **Static file rendering** — articles rendered to `data/content/_build/`
  as pre-baked HTML; DB stores metadata only, no body content in DB
- **Content blueprint** — catch-all route serves published articles by
  `url_path`; registered last to avoid path collisions
- **Markets hub** (`/markets`) — public search + country/region filter page
  for discovering articles; HTMX live filtering via FTS5 queries
- **Admin template CRUD** — list, create, edit, delete article templates;
  view/add/upload/delete data rows; bulk generation form with start date
  and articles-per-day controls
- **Admin scenario CRUD** — list, create (with curated calculator form),
  edit + recalculate, delete, preview all widget sections
- **Admin article management** — list with status badges (draft/published/
  scheduled), create manual articles (Markdown textarea), edit, delete,
  publish/unpublish toggle, rebuild single or all articles
- **Rebuild system** — `POST /admin/articles/<id>/rebuild` and
  `/admin/rebuild-all` re-render article HTML from source (template+data
  or markdown file) with fresh scenario `calc_json`
- **Article detail page** — SEO meta tags (description, og:title,
  og:description, og:type=article, og:image, canonical), article
  typography styles, bottom CTA linking to planner
- **Scenario widget CSS** — `.scenario-widget` component styles in
  `input.css` with responsive table scroll and metric grid collapse;
  `.article-body` typography for headings, paragraphs, lists, blockquotes,
  tables, code blocks
- **`slugify()` utility** — added to `core.py` for URL-safe slug generation
- **`mistune` dependency** — Markdown → HTML rendering for articles
- **Sitemap** — `/markets` and all published articles added to
  `/sitemap.xml` (only articles with `published_at <= now`)
- **Footer** — "Markets" link added to Product column
- **Admin dashboard** — Templates, Scenarios, Articles quick-link buttons
- **Path collision prevention** — `RESERVED_PREFIXES` validation rejects
  article `url_path` values that conflict with existing routes

### Added
- **Dev setup script** (`scripts/dev_setup.sh`) — interactive bootstrap that
  checks prerequisites, installs deps, creates `.env` with auto-generated
  `SECRET_KEY`, runs migrations, seeds test data, optionally sets up Paddle
  sandbox products, and builds CSS
- **Dev run script** (`scripts/dev_run.sh`) — resets DB, runs migrations, seeds
  data, builds CSS, then starts app + worker + CSS watcher in parallel with
  colored/labeled output and clean Ctrl-C shutdown; when Paddle is configured
  and ngrok is installed, starts a tunnel and updates the Paddle webhook
  destination automatically for end-to-end checkout testing
- **Paddle webhook auto-setup** — `setup_paddle.py` now creates a notification
  destination in Paddle with the 5 event types the webhook handler processes,
  and writes `PADDLE_WEBHOOK_SECRET` to `.env` automatically
- **Resend test email docs** — documented Resend test addresses
  (`delivered@resend.dev`, `bounced@resend.dev`, etc.) in `.env.example` and
  README for testing email flows without a verified domain

### Fixed
- **Webhook signature verification broken** — `Verifier().verify()` was called
  with raw bytes instead of a request object, causing all signed webhooks to
  fail with 400; replaced with manual HMAC verification matching Paddle's
  `ts=<unix>;h1=<hmac>` format; also added JSON parse error guard (400 instead
  of 500 on malformed payloads)
- **Billing tests stale after SDK migration** — webhook tests used plain
  HMAC instead of Paddle's `ts=...;h1=...` signature format; checkout tests
  expected redirect instead of JSON overlay response; manage/cancel tests
  mocked httpx instead of Paddle SDK; removed stale `PADDLE_PRICES` config
  test (prices now in DB)
- **Quote wizard state loss** — `_accumulated` hidden input used `"` attribute
  delimiters which broke on `tojson` output containing literal `"` characters;
  switched all 8 step templates to single-quote delimiters (`value='...'`)
- **Admin dashboard crash** — `no such column: amount` in credit ledger query;
  corrected to `delta` (the actual column name)
- **Admin supplier detail** — credit ledger table referenced `entry.entry_type`
  and `entry.amount` instead of `entry.event_type` and `entry.delta`
- **Nav bar semi-transparent** — replaced `rgba(255,255,255,0.85)` +
  `backdrop-filter: blur()` with opaque `#ffffff` background
- **Supplier pricing card buttons misaligned** — added flexbox to `.pricing-card`
  with `flex-grow: 1` on feature list and `margin-top: auto` on buttons

### Changed
- **Newsletter boost removed** — replaced with "Custom Card Color" boost
  (€19/mo) across supplier pricing page, `setup_paddle.py`, and supplier
  dashboard; directory cards with active `card_color` boost render a custom
  border color from `supplier_boosts.metadata` JSON
- Removed "Zillow-style" comments from `base.html` and `input.css`

### Added
- **Migration 0009** — adds `metadata TEXT` column to `supplier_boosts` for
  card color boost configuration
- **Admin supplier/lead CRUD** — `GET/POST /admin/suppliers/new` and
  `GET/POST /admin/leads/new` routes with form templates; "New Supplier" and
  "New Lead" buttons on admin list pages
- **Dev seed data script** (`scripts/seed_dev_data.py`) — creates 5 suppliers
  (mix of tiers), 10 leads (mix of heat scores), 1 dev user, credit ledger
  entries, and lead forwards for local testing
- **Playwright quote wizard tests** (`tests/test_quote_wizard.py`) — full
  9-step flow, back-navigation data preservation, and validation error tests

### Added — Phase 2: Scale the Marketplace — Supplier Dashboard + Business Plan PDF

- **Paddle.js overlay checkout** — migrated all checkout flows (billing,
  supplier signup, business plan) from server-side Paddle transaction creation +
  redirect to client-side `Paddle.Checkout.open()` overlay; `PADDLE_CLIENT_TOKEN`
  config; Paddle.js script in `base.html` with sandbox/production toggle
- **Paddle products in DB** — new `paddle_products` table replaces 16
  `PADDLE_PRICE_*` env vars; `get_paddle_price(key)` and
  `get_all_paddle_prices()` async helpers in `core.py`; `setup_paddle.py`
  rewritten to write product/price IDs directly to database
- **Migration 0008** — `paddle_products`, `business_plan_exports`, `feedback`
  tables; `logo_file` and `tagline` columns on `suppliers`
- **Umami analytics** — tracking script in `base.html`; config vars
  `UMAMI_API_URL`, `UMAMI_API_TOKEN`, `UMAMI_WEBSITE_ID`; directory click
  tracking redirect routes (`/<slug>/website`, `/<slug>/quote`)
- **Supplier dashboard** (`/suppliers/dashboard`) — tab-based HTMX dashboard
  with sidebar nav (Overview, Lead Feed, My Listing, Boost & Upsells); each
  tab loads via `hx-get` with `hx-push-url` for deep-linking
  - **Overview tab** — 4 KPI stat cards (profile views, leads unlocked, credit
    balance, directory rank), new leads alert banner, recent activity feed
  - **Lead feed tab** — refactored `_get_lead_feed_data()` shared function
    with bidder count; heat/country/timeline filter pills; region matching
    badges; "No other suppliers yet — be first!" messaging
  - **My Listing tab** — preview card + inline edit form (company info,
    categories, service area, logo upload); `POST /suppliers/dashboard/listing`
    saves changes
  - **Boost & Upsells tab** — current plan, active boosts, available boosts
    with `Paddle.Checkout.open()` purchase buttons, credit packs grid, summary
    sidebar with visibility multiplier
- **Business plan PDF export** — `businessplan.py` with WeasyPrint PDF engine;
  `plan.html` + `plan.css` A4 templates (executive summary, CAPEX, OPEX,
  revenue model, 5-year P&L, 12-month cash flow, sensitivity, key metrics);
  bilingual EN/DE; `generate_business_plan` worker task
- **Business plan routes** — `GET /planner/export` (options page with scenario
  picker + Paddle checkout), `POST /planner/export/checkout`, `GET
  /planner/export/success`, `GET /planner/export/<id>` (download); export CTA
  in planner sidebar
- **Supplier landing page enhanced** — live stats from DB (business plans
  created, avg project value, suppliers listed, monthly leads); real anonymized
  lead preview cards (fallback to example data); credit explainer (hot=35,
  warm=20, cool=8); "Most Popular" badge on Growth plan; expanded FAQ (8
  questions including credits, countries, cancellation); social proof section
- **Admin supplier management** — `GET /admin/suppliers` with tier/country/name
  filters (HTMX search), `GET /admin/suppliers/<id>` detail with profile info,
  credit balance + ledger, active boosts, lead forward history; `POST
  /admin/suppliers/<id>/credits` manual credit adjustment; `POST
  /admin/suppliers/<id>/tier` manual tier change; supplier stats on admin
  dashboard (claimed, growth, pro, credits spent, leads forwarded)
- **Feedback widget** — compact "Feedback" button in navbar opens HTMX popover
  with textarea; `POST /feedback` rate-limited (5/hr per IP), inserts into
  `feedback` table; `GET /admin/feedback` paginated admin view with user email
  + page URL

### Added — Phase 1: Lead Operations + Builder Directory Monetization

- **SDK migration** — replaced raw httpx calls with official `paddle-python-sdk`
  and `resend` SDKs for type safety, built-in webhook verification, and cleaner
  code; `send_email()` now accepts `from_addr` parameter; all Paddle API calls
  use SDK client
- **EMAIL_ADDRESSES dict** — hardcoded `transactional`, `leads`, and `nurture`
  from-addresses sharing the `notification.padelnomics.io` Resend domain
- **Paddle product setup script** — `scripts/setup_paddle.py` creates all 14
  products/prices programmatically via SDK; outputs `.env` snippet for CI
- **Claude Code skills** — `.claude/skills/paddle-integration/SKILL.md` and
  `.claude/skills/resend-emails/SKILL.md` for consistent SDK usage patterns
- **Migration 0007** — `credit_ledger`, `lead_forwards`, `supplier_boosts`
  tables; 12 new columns on `suppliers` (profile, credits); `credit_cost` and
  `unlock_count` on `lead_requests`
- **Credit system** (`credits.py`) — `get_balance`, `add_credits`,
  `spend_credits`, `unlock_lead`, `compute_credit_cost`, `monthly_credit_refill`,
  `get_ledger`; `InsufficientCredits` exception; heat-based pricing (hot=35,
  warm=20, cool=8 credits); `refill_monthly_credits` worker task + scheduler
- **Admin lead management** — `GET /admin/leads` with status/heat/country
  filters (HTMX search), `GET /admin/leads/<id>` detail with project brief +
  forward history, `POST /admin/leads/<id>/status` update, `POST
  /admin/leads/<id>/forward` manual forward (no credit cost); lead funnel stats
  on admin dashboard (planner users → leads → verified → unlocked)
- **Lead forwarding emails** — `send_lead_forward_email` worker task sends full
  project brief + contact details to supplier; `send_lead_matched_notification`
  notifies entrepreneur when a supplier unlocks their lead
- **Credit cost computed on submission** — `credit_cost` set from heat score
  both on verified-user submission and on email verification
- **Supplier signup wizard** (`/suppliers/signup`) — 4-step HTMX wizard: plan
  selection (Growth €149/mo, Pro €399/mo), boost add-ons (logo, highlight,
  verified, newsletter), credit packs (25-250), account details + order summary;
  builds multi-item Paddle transaction; `_accumulated` hidden JSON pattern
- **Supplier claim flow** — `GET /suppliers/claim/<slug>` verifies unclaimed
  and redirects to signup with pre-fill
- **Webhook handlers** — `subscription.activated` with `supplier_*` plan creates
  supplier record with tier, credits, and boosts; `transaction.completed`
  handles credit pack purchases and sticky boost purchases with expiry
- **Supplier profile page** (`/directory/<slug>`) — public profile with logo,
  verified badge, description, service categories as pills, service area, years
  in business, project count, website; "Request Quote" and "Claim This Listing"
  CTAs
- **Directory card links** — all directory cards now link to supplier profile
  pages; paid-tier cards show "Request Quote" mini-CTA
- **Supplier lead feed** (`/suppliers/leads`) — requires login + paid supplier
  tier; shows anonymized lead cards with heat badge, facility type, courts,
  country, timeline, budget range, credit cost, unlock count; `POST
  /suppliers/leads/<id>/unlock` spends credits, creates lead_forward, sends
  emails, returns full-details card via HTMX swap
- **Email nurture via Resend Audiences** — on first scenario save, user is added
  to "Planner Users" audience (triggers 3-email automation); on quote submission,
  user is removed from audience (stops nurture)
- **PADDLE_PRICES expanded** — 13 new price keys for supplier plans, boosts,
  credit packs; `PADDLE_ENVIRONMENT` config for sandbox/production switching

### Changed
- Supplier marketing page CTAs link to `/suppliers/signup` instead of mailto
- `httpx` removed from direct dependencies (transitive via paddle SDK)

### Added
- **Double opt-in email verification for quote requests** — guest quote
  submissions now require email verification before the lead goes live;
  verification click also creates a user account and logs them in
  automatically (GDPR-friendly consent trail)
- `GET /leads/verify` route — validates token, activates lead
  (`pending_verification` → `new`, sets `verified_at`), logs user in, sends
  admin notification and welcome email
- `send_quote_verification` worker task — branded verification email with
  project details and "Verify & Activate Quote" CTA button (DEBUG mode
  prints link to console)
- `quote_verify_sent.html` template — "Check your email" page shown after
  guest quote submission
- Migration 0006 — adds `verified_at TEXT` column to `lead_requests`
- 9 new tests in `TestQuoteVerification` class covering the full
  verification flow, expired tokens, duplicate verification, and user
  creation

### Changed
- **Inline CTA full copy** — mobile/narrow-screen inline quote CTA now matches
  sidebar: "Next Step" label, full title, description, 4 checkmark benefits,
  "Get Supplier Quotes" button, and "Takes ~2 minutes" hint
- **Signup bar simplified** — removed `×` close button from guest signup bar;
  now a non-dismissable nudge (still only shown on results tabs via JS)
- **Investment tab narrower** — CAPEX tab content constrained to 800px max-width
  so 3-column card grid, table, and chart don't stretch across full 1100px on
  wide screens

### Changed
- **Quote form → standalone 9-step HTMX wizard** — extracted "Get Quotes" from
  planner Step 5 into a standalone multi-step wizard at `/leads/quote` using
  server-rendered HTMX partials; each step validates server-side and swaps via
  `hx-post`/`hx-get` with OOB dot progress updates; accumulated state passed
  forward as hidden JSON field (no JS state management)
- **Planner reduced to 4 steps** — removed embedded quote form (Step 5) from
  planner wizard; Step 4 "Get Quotes →" now navigates to `/leads/quote` with
  pre-filled params (venue, courts, glass, lighting, country, budget)
- **Planner sidebar CTA** — "Get Supplier Quotes" button now links to standalone
  quote wizard instead of scrolling to embedded Step 5; sidebar now visible on
  all tabs including assumptions (was results-only)
- **Sticky wizard nav** — planner preview bar (CAPEX/CF/IRR) and back/next
  buttons now stick to the bottom of the viewport so users don't have to scroll
  to navigate between steps
- **Mobile quote CTA** — inline "Get Quotes" card shown below main content on
  screens narrower than 1400px (where the fixed sidebar is hidden)
- **Step 4 → "Show Results"** — final planner wizard step now says "Show Results"
  instead of "Get Quotes" since quote flow is a separate standalone wizard
- **Removed "2-5 suppliers" cap language** — replaced specific supplier count
  promises with "matched suppliers" across landing page, supplier FAQ, planner
  sidebar, and quote form privacy box

### Removed
- Inline quote form from planner (Step 5 HTML, `#wizSuccess`, hidden inputs)
- `populateWizAutoFill()`, `submitQuote()`, `COUNTRY_NAMES` from planner.js
- `__PADELNOMICS_QUOTE_URL__` JS variable from planner template
- Step 5 scoped CSS (~155 lines): `#wizQuoteForm`, `.wiz-autofill-summary`,
  `.wiz-input`, `.wiz-privacy-box`, `.consent-group`, `.wiz-success`,
  `.wiz-signup-nudge`, `.wiz-checkbox-label`

### Added
- **Supplier tier system** — Migration 0005 adds `tier` (free/growth/pro),
  `logo_url`, `is_verified`, `highlight`, `sticky_until`, `sticky_country`
  columns to suppliers table for paid listing support
- **HTMX live search** — directory search input and filters update results
  via `hx-get` with 300ms debounce; new `/directory/results` endpoint returns
  swappable partial
- **Directory card tiers** — three-tier card design: Pro (green border, logo,
  verified badge, website), Growth (description, blue badge), Free (muted,
  unverified, "Is this your company?" CTA); sticky/featured suppliers pinned
  to top with blue border
- **Supplier pricing page** — `/suppliers/` now shows Growth (€149/mo) and
  Pro (€399/mo) plan cards with feature lists, boost add-ons grid (Logo,
  Highlight, Verified Badge, Sticky Top, Newsletter Feature), updated FAQ
- **Mandatory form fields** — country, timeline, and stakeholder_type now
  required on quote request form with server-side 422 validation
- **Validation test** — `test_quote_validation_rejects_missing_fields` verifies
  server returns 422 JSON errors for missing mandatory fields

### Changed
- **Nav redesign** — Zillow-style sticky nav with backdrop-blur: demand-side
  links (Planner, Directory) left, supply-side (For Suppliers, Help) after
  separator, Sign In right; removed "Get Started Free" button
- **CTA text sweep** — "Get Matched" → "Get Quotes" across planner, landing,
  and lead forms; removed all "Free" qualifiers from CTAs and badges
- **ROI calculator fix** — realistic cost model: €35K/court (was €25K),
  staff costs, €8/sqm rent (was €4); payback and ROI now based on total
  investment (was equity only); defaults: €40/hr rate, 35% utilization;
  shows ~3.9yr payback, ~26% ROI (was 0.1yr/1255%)
- **Directory route refactor** — shared `_build_directory_query()` helper
  with tier-based SQL ordering (sticky → pro → growth → free → alphabetical)

### Added
- **Supplier directory** — public searchable directory at `/directory/` with 279
  padel court suppliers across 31 countries; FTS5 full-text search, country and
  category filters, pagination, category-colored badges, unclaimed listing model
- **Supplier landing page** — `/suppliers/` marketing page for suppliers: hero,
  how-it-works steps, example lead preview, FAQ, "Claim Your Listing" CTAs
- **Migration 0004** — creates `suppliers` table with FTS5 virtual table,
  content-sync triggers, and seeds 279 suppliers from PadelDirectory.md
- **Quick ROI calculator** — landing page now features an interactive 3-slider
  calculator (courts, rate, utilization) showing investment, monthly cash flow,
  payback period, and annual ROI in real time
- **Supplier matching section** — landing page "Find the Right Suppliers" section
  with 3-step flow and link to directory
- **FAQ accordion** — landing page FAQ covering planner features, signup
  requirements, supplier matching, directory pricing, and projection accuracy

### Changed
- **Visual refresh** — adopted React prototype color palette and aesthetic
  site-wide: royal blue primary (#1D4ED8), green (#16A34A), gold (#D97706);
  elevated cards with soft shadows, rounder corners (rounded-2xl cards,
  rounded-xl buttons/inputs), frosted-glass planner nav, highlighted CTA
  regions with blue-tinted backgrounds, pill-shaped toggle/filter controls,
  polished buttons with colored shadows, stronger hover lift on directory cards
- **Landing hero redesigned** — two-column layout with headline + CTAs on left
  and interactive Quick ROI calculator on right (matching React prototype);
  green badge pill, feature check bullets, "Open Full Planner" CTA inside
  calculator card; responsive single-column on mobile
- **Landing page redesigned** — replaced screenshot card with Quick ROI
  calculator; added supplier matching section, FAQ, and live supplier stats;
  CTAs renamed "Open the Planner — Free"; Build journey card updated with
  live supplier/country counts
- **Navbar** — Planner and Directory links now visible for all users (not just
  logged-in); footer updated with Directory and For Suppliers links
- **Planner CTAs** — removed sticky "Get Builder Quotes" footer bar; CAPEX and
  Returns tab CTAs now navigate to wizard step 5 (integrated lead qualifier)
  instead of redirecting to standalone `/leads/quote` form
- **Sitemap** — added `/planner/`, `/directory/`, and `/suppliers/` URLs

### Changed
- **Planner wizard** — Assumptions tab reorganized into 5 guided steps
  (Your Venue → Pricing → Costs → Finance → Get Matched) with live preview
  bar and step navigation; reduces cognitive load from 60 sliders to ~6-15
  per step
- **Integrated lead qualifier** — Step 5 "Get Matched" embeds the supplier
  quote form directly in the planner; auto-fills venue, courts, glass,
  lighting, country, budget from planner state; submits inline via fetch
- **JSON quote endpoint** — `POST /leads/quote` now accepts `application/json`
  and returns `{"ok": true, "heat": "..."}` for inline planner submissions;
  standalone HTML form unchanged

### Added — Phase 0 Round 2: Polish & Country-Specific Calculator
- **Country-specific calculator** — `country` selector (DE/ES/IT/FR/NL/SE/UK/Other) and `permitsCompliance` CAPEX item for Indoor Rent and Outdoor scenarios; country presets auto-adjust permit costs
- **Permits & Compliance** — new CAPEX line item for building permits, noise studies, and regulatory compliance (default €12K for Germany); excluded from Indoor Buy where Planning + Permits already covers this
- **Quote form redesign** — elevated white card on gradient background, green gradient CTA buttons, progress labels (Project/Details/Contact), privacy info box, mandatory consent checkbox
- **Project phase** (replaces location_status options) — 7-stage progression: still searching → location found → converting existing → lease signed → permit not filed → permit pending → permit granted; updated heat scoring
- **Stakeholder type** field — "You are..." selector (Entrepreneur, Tennis Club, Municipality, Developer, Operator, Architect) with `stakeholder_type` DB column (migration 0003)
- **Build context** — added "Need Help Finding a Venue / Land" (`venue_search`) option
- **Quote submitted page redesign** — "You're matched!" flow with next-steps timeline, email confirmation box, and signup CTA for guests
- **Migration 0003** — adds `stakeholder_type TEXT` column to `lead_requests`

### Changed
- **Landing page** — replaced teaser calculator with planner screenshot in browser-frame card + "Start Planning — Free" CTA; all CTAs now point to `/planner/` (no signup gate)
- **Heat score** — updated `calculate_heat_score()` for new project phase values (`permit_granted` +4, `lease_signed`/`permit_pending` +3, `converting_existing`/`permit_not_filed` +2, `location_found` +1)
- **Quote URL** — planner now passes `country` parameter to quote form prefill
- **Admin email** — includes stakeholder type and updated field labels

### Added — Phase 0: Ungate & Validate
- **Guest mode planner** — removed auth gate from `/planner/` and `/planner/calculate`; scenarios still require login
- **New calculator variables** — `budgetTarget` (budget vs CAPEX comparison), `glassType` (standard/panoramic, 1.4x multiplier), `lightingType` (LED standard/competition/natural, 1.5x/0x multipliers)
- **Pill select UI component** — reusable `pillSelect()` helper in planner.js with matching `.pill-btn` CSS for multi-option inputs
- **Budget indicator card** — shows over/under budget with variance amount and percentage on the Investment tab
- **3-step "Get Builder Quotes" flow** — `/leads/quote` with project specs, details, and contact steps; no login required
- **Lead heat scoring** — `calculate_heat_score()` rates leads as hot/warm/cool based on timeline, financing, location readiness, and budget signals
- **PDF export CTA** — "Export Business Plan (PDF) — €99" wired to Paddle checkout (`business_plan` price in PADDLE_PRICES)
- **SEO meta tags** — `<meta>` description, og:title, og:description, og:image on planner page
- **Migration 0002** — expands `lead_requests` with 17 new columns for quote qualification flow; makes `user_id` nullable for guest leads
- **Phase 0 test suite** (`tests/test_phase0.py`) — 47 tests covering guest mode, glass/lighting/budget variables, heat scoring, quote submission, schema validation
- Updated Hypothesis strategy in `test_calculator.py` with `budgetTarget`, `glassType`, `lightingType`

### Changed
- Planner CTA links now point to `/leads/quote` with pre-filled calculator state params (venue, courts, glass, lighting, budget)
- Sticky footer bar updated: "Get Builder Quotes" + "Export Business Plan (PDF)" replace old supplier/financing links

### Changed
- Landing page journey section: renamed "From Idea to Operating Hall" → "Your Journey", expanded from 4 cards to 5 (Explore → Plan → Finance → Build → Grow) with "Coming Soon" badges on unreleased stages
- Added `.grid-5` CSS helper for 5-column grid layout

### Changed
- **Pico CSS → Tailwind CSS v4** — full design system migration across all templates (except planner, which keeps its own CSS)
  - Standalone Tailwind CLI binary (no Node.js) with `make css-build` / `make css-watch`
  - Court Tech brand theme: navy/charcoal/electric/accent color palette
  - Component classes (`.btn`, `.card`, `.form-input`, `.table`, `.badge`, `.flash`, etc.) in `input.css` for consistent styling
  - Self-hosted Commit Mono font (replaces JetBrains Mono) for monospace data display
  - Docker multi-stage build: CSS compiled in dedicated stage before Python build
- Landing page teaser calculator restyled with Tailwind utilities and brand colors

### Removed
- Pico CSS CDN dependency
- `custom.css` (replaced by Tailwind `input.css` with `@layer components`)
- JetBrains Mono font (replaced by self-hosted Commit Mono)

### Fixed
- Empty env vars (e.g. `SECRET_KEY=`) now fall back to defaults instead of silently using `""` — fixes 500 on every request when `.env` has blank values

### Added
- Comprehensive migration test suite (`tests/test_migrations.py` — 20 tests) covering fresh DB, existing DB, up-to-date DB, idempotent migration, version discovery, `_is_fresh_db`, migration 0001 correctness, and ordering
- Expanded `migrate.py` module docstring documenting the 8-step algorithm, protocol for adding migrations, and design decisions
- Sequential migration system (`migrations/migrate.py`) — tracks applied versions in `_migrations` table, auto-detects fresh vs existing DBs, runs pending migrations in order
- `migrations/versions/0001_rename_ls_to_paddle.py` — first versioned migration (absorbed from `scripts/migrate_to_paddle.py`)
- Server-side financial calculator (`planner/calculator.py`) — ported JS `calc()`, `pmt()`, `calcIRR()` to Python so the full financial model is no longer exposed in client-side JavaScript
- `POST /planner/calculate` endpoint for server-side computation
- Pre-computed initial data (`window.__PADELNOMICS_INITIAL_D__`) injected on page load for instant first render
- Debounced API fetch pattern in `planner.js` with `AbortController` for in-flight request cancellation
- Computing indicator CSS (`.planner-app--computing`) with subtle "computing..." text
- Comprehensive test suite for calculator (`tests/test_calculator.py` — 227 tests) covering all 4 venue/ownership combos, edge cases, and Hypothesis property-based fuzzing
- Comprehensive billing test suite (371 tests total):
  - `tests/conftest.py` — shared fixtures (DB, app, clients, subscriptions, webhook helpers)
  - `tests/test_billing_helpers.py` — unit tests for SQL helpers, feature/limit access, plan determination (60+ tests + parameterized + Hypothesis)
  - `tests/test_billing_webhooks.py` — integration tests for LemonSqueezy webhooks (signature verification, all lifecycle events, Hypothesis fuzzing)
  - `tests/test_billing_routes.py` — route tests (pricing, checkout, manage, cancel, resume, subscription_required decorator)
  - Added `hypothesis>=6.100.0` and `respx>=0.22.0` to dev dependencies for property-based testing and httpx mocking
  - **Factored into Copier template** — all billing tests now generate as `.jinja` templates with provider-specific conditionals for Stripe, Paddle, and LemonSqueezy
- GitLab CI/CD pipeline (`.gitlab-ci.yml`) — runs pytest + ruff on master/MRs, auto-deploys on master
- Blue-green deployment with Docker Compose profiles (`docker-compose.prod.yml`, `deploy.sh`)
  - nginx router on port 5000 proxies to active blue/green slot
  - Zero-downtime: new slot health-checked before traffic switch
  - Automatic rollback on failed health check

### Removed
- `scripts/migrate_to_paddle.py` — superseded by `versions/0001_rename_ls_to_paddle.py`

### Changed
- `planner.js` no longer contains `calc()`, `pmt()`, or `calcIRR()` functions — computation moved server-side
- `render()` split into `render()` (tab switching + schedule calc) and `renderWith(d)` (DOM updates from data)
- Tab switching now renders from `_lastD` cache (instant, no API call)
- Slider input triggers 200ms debounced server call instead of synchronous client-side calc
