# Development checklist

Legend: ✅ done and tested · 🟡 schema ready, code in a later phase · ⬜ not started

## Phase 1 — Foundation ✅

| Item | Status | Where |
|---|---|---|
| CodeIgniter 4.7 project, `.env` template, platform config | ✅ | `env`, `app/Config/Platform.php` |
| Full database schema (62 tables at Phase 1; 67 now) as migrations + `database.sql` | ✅ | `app/Database/Migrations/*`, `database/database.sql` |
| Tenant architecture: `TenantContext`, `TenantScopedModel` (fail-closed) | ✅ | `app/Services/TenantContext.php`, `app/Models/TenantScopedModel.php` |
| Registration: tenant + owner + free subscription in one transaction | ✅ | `RegistrationService`, `Auth/RegisterController` |
| Login with lockout, IP ceiling, rate limiting, no user enumeration | ✅ | `AuthService`, `RateLimitFilter` |
| Email verification (split tokens, single use) | ✅ | `TokenService`, `Auth/VerificationController` |
| Forgot / reset password | ✅ | `Auth/PasswordResetController` |
| Forced password change for seeded admin | ✅ | `Auth/ForcedPasswordController` |
| Roles & permissions: 4 roles, 46 permissions, route filter, `can()` | ✅ | `RbacSeeder`, `PermissionFilter` |
| Role permission editor (Super Admin) | ✅ | `Admin/RoleController` |
| Super Admin dashboard with live statistics | ✅ | `Admin/DashboardController` |
| Merchant dashboard: stats, plan usage, onboarding checklist | ✅ | `Dashboard/HomeController` |
| Account settings: profile + change password | ✅ | `AccountController` |
| Activity log (platform-wide and per tenant) | ✅ | `ActivityLogger`, `Admin/ActivityController`, `Dashboard/ActivityController` |
| Layouts: auth, Super Admin, merchant; responsive sidebar | ✅ | `app/Views/layouts/*`, `public/assets/css/app.css` |
| CSRF on all forms, secure headers, no-store on authenticated pages | ✅ | `app/Config/Filters.php`, `AuthFilter::after` |
| Seeders: roles, permissions, plans, settings, super admin, demo merchants | ✅ | `app/Database/Seeds/*` |
| Tests: 28 integration checks + 30 HTTP checks | ✅ | `php spark sitecraft:selftest`, `tests/smoke/phase1.sh` |

## Phase 2 — Admin panel, merchant dashboard, websites, subscriptions ✅

| Item | Status | Where |
|---|---|---|
| Plan-limit enforcement on the server (websites, products, features) | ✅ | `Services/PlanLimitService.php` |
| Subscription lifecycle: one-time trial, grace period, expiry → Free, downgrade (now or at period end), cancel at period end, resume | ✅ | `Services/SubscriptionService.php` |
| Scheduler for due transitions (cron) + "Process now" button | ✅ | `php spark subscriptions:process`, `Admin/SubscriptionController::process` |
| Over-limit websites locked (never deleted) and unlocked automatically | ✅ | `WebsiteService::syncWithPlanLimit` |
| Merchant: My websites, create wizard step 1 (name → category → address), live subdomain check with suggestions, settings, delete with typed confirmation | ✅ | `Dashboard/WebsiteController`, `views/dashboard/websites/*` |
| Merchant: Subscription & billing page (plans, usage, history, trial/downgrade/cancel/resume) | ✅ | `Dashboard/BillingController` |
| Merchant: Team — invite staff by email, resend, revoke, suspend, reactivate | ✅ | `Dashboard/TeamController`, `Services/InvitationService.php` |
| Merchant: Support tickets — open, reply, close, reopen | ✅ | `Dashboard/SupportController`, `Services/SupportService.php` |
| In-app notifications (bell + page) | ✅ | `Services/Notifier.php`, `NotificationController` |
| Admin: Merchants — search/filter, detail page, activate/suspend/deactivate with reason + owner email | ✅ | `Admin/TenantController`, `Services/TenantAdminService.php` |
| Admin: change plan (complimentary period), extend, cancel now | ✅ | `Admin/SubscriptionController::act` |
| Admin: Users — search/filter, suspend/reactivate/deactivate, send reset link, mark verified | ✅ | `Admin/UserController` |
| Admin: Platform staff — invite Platform Staff / Super Admin | ✅ | `Admin/UserController::staff` |
| Admin: Websites — all tenants, suspend with reason / restore | ✅ | `Admin/WebsiteController` |
| Admin: Pricing plans — create/edit/delete (blocked if in use), default plan rules | ✅ | `Admin/PlanController` |
| Admin: Subscriptions — live / history, filters, ending-soon count | ✅ | `Admin/SubscriptionController` |
| Admin: Support desk — filters, priority sort, assign, status, reply (+ email) | ✅ | `Admin/SupportController` |
| Admin: Email templates — edit, sandboxed preview, reset to default; active content stripped | ✅ | `Admin/EmailTemplateController`, `Config/EmailTemplates.php` |
| Admin: Platform settings — branding, sign-ups on/off, legal links, dashboard announcement | ✅ | `Admin/SettingsController`, `Config/PlatformSettings.php` |
| Tests: +37 integration checks (65 total) + 47 HTTP checks | ✅ | `sitecraft:selftest`, `tests/smoke/phase2.sh`, `tests/smoke/run_all.sh` |

**Deferred on purpose (needs Phase 6 payments):** paid upgrades and renewals, prorating. The upgrade buttons say so instead of pretending to work. **Deferred to Phase 3:** create-wizard steps 2–7 (template → design → publish).
**Not built:** admin impersonation of merchants — left out deliberately; "send reset link" and the merchant detail page cover the support need without an admin acting as a merchant.

## Phase 3 — Templates & GrapesJS builder ✅

| Item | Status | Where |
|---|---|---|
| GrapesJS 0.22.16 editor: blocks, layers, pages, images, saved sections, theme (left); canvas; design + settings (right) | ✅ | `Views/builder/editor.php`, `public/assets/js/builder.js` |
| 42 blocks: 10 basic, 3 layout, 19 sections, 11 e-commerce (all requested ones) | ✅ | `Libraries/Builder/BlockCatalog.php`, `SectionLibrary.php` |
| Inline text editing, images, colours, typography (22 Google Fonts), backgrounds, spacing, borders, shadows, layout | ✅ | style manager config in `builder.js` |
| Desktop / tablet / mobile editing with device-specific styles | ✅ | device manager + media rules |
| Duplicate, delete, move, copy/paste, undo/redo, outlines, quick preview | ✅ | GrapesJS commands + toolbar |
| Custom HTML block (sanitised) and site-wide custom CSS | ✅ | `sc-html` component, Custom CSS modal |
| Video (YouTube/Vimeo) and Google Map blocks with settings | ✅ | `sc-video`, `sc-map` components |
| Autosave (debounced), Save, version conflict detection between tabs/users | ✅ | `ProjectService::save` (compare-and-set) |
| Shared header/footer across pages (symbols) | ✅ | `ensureGlobals()` in `builder.js` |
| Pages: add / rename / re-address / delete in editor and dashboard; SEO title, description, canonical, social image, robots, visibility | ✅ | `ProjectService`, `Dashboard/PageController`, `views/dashboard/pages.php` |
| Server renderer + HTML/CSS sanitiser; draft preview with strict CSP | ✅ | `ProjectRenderer`, `HtmlSanitizer`, `CssSanitizer`, `PageDocument` |
| Publish (snapshot + render + store), unpublish, version history, restore | ✅ | `PublishService`, `WebsiteController` |
| Image upload: type/size/quota checks, re-encode, resize, WebP copy, tenant folders | ✅ | `MediaService`, `public/media/.htaccess` |
| Saved sections (per tenant) | ✅ | `website_saved_sections`, builder API |
| 13 templates, one per requested category, with pages, header, footer, product page (stores), SEO-ready structure | ✅ | `TemplateLibrary`, `TemplateSeeder` |
| Template artwork (364 SVGs) and real thumbnails | ✅ | `tools/generate_template_art.py`, `public/assets/templates/` |
| Merchant gallery with category filter, preview, premium gating at the server; create wizard step 2 | ✅ | `Dashboard/TemplateGalleryController` |
| Admin: template list/filters, categories, create (blank or copy), details, status, thumbnail upload, designer, preview, delete | ✅ | `Admin/TemplateController`, `Admin/TemplateDesignController` |
| Builder settings (autosave delay, versions kept) | ✅ | `Config/PlatformSettings.php` |
| Tests: +44 integration (109 total), +53 HTTP, 19 browser checks incl. renderer parity on every page | ✅ | `sitecraft:selftest`, `tests/smoke/phase3.sh`, `tests/e2e/editor.e2e.js` |

**Fixed along the way:** Phase 3 migration rollback failed (index backing a foreign key); storefront "robots" setting could never be saved (`in_list` with commas, Phase 2 bug); plan-limit errors in the editor would have returned HTTP 500; per-request CSRF rotation broke multi-tab editing; Undo button state could lag.
## Phase 4 — Publishing engine, subdomains, custom domains ✅

| Item | Status | Where |
|---|---|---|
| Host-based split: store hosts get only storefront routes (admin/dashboard URLs 404 there) | ✅ | `Config/Routes.php`, `Libraries/Storefront/Host.php` |
| Site resolution: subdomain, verified custom domain, www ↔ apex, main-address 301, plan-gated custom domains | ✅ | `Services/Storefront/SiteResolver.php` |
| Dynamic page rendering from published pages: menus, live widgets, forms, branding, empty states | ✅ | `StorefrontController`, `Libraries/Storefront/StorefrontRenderer.php`, `StoreData` |
| Theme + custom CSS frozen at publish (drafts never leak to the live site) | ✅ | `PublishService`, `website_settings.published_*` |
| SEO head: title/description/robots/canonical/Open Graph/Twitter, JSON-LD (WebSite, Organization, WebPage, BreadcrumbList), favicon | ✅ | `StorefrontController::seo()` |
| Dynamic `sitemap.xml` and `robots.txt` per site; app hosts `Disallow: /` | ✅ | `StorefrontController`, `MetaController` |
| Canonical paths (301 for trailing slash / upper case), `redirects` table honoured, branded 404, unavailable pages (503/404, no reasons shown) | ✅ | `StorefrontController` |
| No sessions/cookies on stores, CSP `script-src 'none'`, no debug toolbar, no app links | ✅ | `Config/Filters.php`, `Config/App.php` |
| Page cache with per-website generations; status checked before cache | ✅ | `Services/Storefront/StorefrontCache.php` |
| Custom domains: add (normalised, IDN), TXT ownership proof, CNAME/A routing check, main address, remove, plan gate, 3 per site | ✅ | `Services/DomainService.php`, `Dashboard/DomainController`, `views/dashboard/domains.php` |
| Domain re-checks: back-off for pending, daily for verified, disconnect after 3 missed TXT checks, notifications, squatting protection (72 h) | ✅ | `php spark domains:verify` |
| On-demand TLS "ask" endpoint | ✅ | `/__sc/tls-check?domain=…` (`TlsCheckController`) |
| Admin: Domains — all tenants, filters, re-check, remove with reason | ✅ | `Admin/DomainController`, `views/admin/domains.php` |
| Navigation menus: header + up to 4 footer columns, import links from the design, pages/URLs/home, one drop-down level, reorder, new tab | ✅ | `Services/MenuService.php`, `Dashboard/MenuController`, `views/dashboard/menus.php` |
| Storefront forms: contact + newsletter, signed token, honeypot, Origin check, rate limit, PRG with result message | ✅ | `Services/Storefront/FormService.php`, `Libraries/Storefront/FormToken.php` |
| Merchant inbox (read/unread, search, delete, email + in-app notification) and subscribers (filter, unsubscribe, CSV export) | ✅ | `Dashboard/InboxController`, `Dashboard/SubscriberController` |
| Dev DNS overrides for testing domains on localhost | ✅ | `writable/dns-overrides.json` (ignored in production) |
| Tests: +53 integration (162 total), +109 HTTP | ✅ | `sitecraft:selftest`, `tests/smoke/phase4.sh` |

**Fixed along the way:** theme/custom-CSS edits went live before publishing (Phase 3); CodeIgniter's HTTPS redirect would send shoppers to the app host; the bundled `public/robots.txt` and CodeIgniter `favicon.ico` would have been served on every store.
**Phase 5 picks up:** product pages (`/products/{slug}` with the product layout), collections, cart, checkout and search — then remove those paths from `StorefrontRenderer::UNAVAILABLE_PATHS`. Site-level SEO settings, redirects UI and analytics are Phase 7.
## Phase 5 — Catalog, inventory, cart, checkout, orders ✅

| Item | Status | Where |
|---|---|---|
| Categories: two levels, image, visibility, order, `/collections/{slug}` | ✅ | `Services/Commerce/CategoryService.php`, `Dashboard/CategoryController` |
| Products: details, rich-text description (sanitised), price/sale/cost, GST rate + HSN, SKU, weight, status, featured, tags, categories, SEO | ✅ | `Services/Commerce/ProductService.php`, `views/dashboard/catalog/product_form.php` |
| Options & variants (up to 3 options / 100 variants) with own price, sale, SKU, stock, on/off | ✅ | `ProductService::saveOptions` |
| Product images: upload (re-encoded), main image, reorder, alt text, remove | ✅ | `ProductService::addImages/updateImage/deleteImage` |
| Product list: search, filters (status, category, stock), bulk status/delete, duplicate, CSV export, plan limit shown | ✅ | `Dashboard/ProductController` |
| Inventory: per product/variant, low/out filters, add/remove/set with reason and note, full history | ✅ | `Services/Commerce/InventoryService.php`, `Dashboard/InventoryController` |
| Storefront: all products, collections (incl. sub-categories), search (name, description, SKU, tags), sort, pagination | ✅ | `Controllers/Storefront/ShopController.php` |
| Product page from the site's own product layout (or a standard one): gallery, options, stock, add to cart, related products, Product JSON-LD | ✅ | `Libraries/Storefront/ProductMarkup.php`, `StorefrontRenderer` |
| Live catalog widgets: "From" prices, sold-out badges; cart/search links enabled | ✅ | `StorefrontRenderer` |
| Cart: cookie token (hashed in DB), stock checks, re-pricing, remove/update, live header count on cached pages | ✅ | `Services/Commerce/CartService.php` |
| Checkout: contact, address (Indian states, PIN validation, 16 countries), billing address, note, COD with limit; field errors without sessions | ✅ | `ShopController::checkout/placeOrder` |
| Atomic order placement: locks, server-side totals, GST (inclusive/exclusive), shipping, order numbers, customer upsert, stock commit | ✅ | `Services/Commerce/OrderService.php`, `Pricing.php` |
| Buyer's order page (private link), confirmation + status emails, merchant email + notification | ✅ | `ShopController::order`, `Config/EmailTemplates.php` |
| Merchant orders: list with filters/date range/30-day stats, detail, workflow, tracking, cancel → restock, COD paid on delivery, private notes, CSV | ✅ | `Dashboard/OrderController`, `views/dashboard/orders/*` |
| GST tax invoice (CGST+SGST / IGST), printable / save as PDF | ✅ | `views/dashboard/orders/invoice.php` |
| Store settings: on/off, currency, GST, GSTIN, legal name, state, shipping fee + free threshold, COD + limit, order prefix, checkout note, contact | ✅ | `Dashboard/StoreSettingsController` |
| Products and collections in `sitemap.xml` | ✅ | `StorefrontController::sitemap` |
| Tests: +43 integration (205 total), +83 HTTP | ✅ | `sitecraft:selftest`, `tests/smoke/phase5.sh` |

**Fixed along the way:** order cancellation and stock changes didn't refresh cached product pages; the "add more than in stock" message now says how many are already in the cart.

## Phase 6 — Razorpay (platform + store), shipping, coupons, customers ✅

| Item | Status | Where |
|---|---|---|
| Payment gateway layer: Razorpay REST client, signature checks, encrypted store keys, development simulator (checkout page + API) | ✅ | `Libraries/Payments/*`, `DevRazorpayController` |
| **Platform billing:** plan checkout with server quote (coupon, proration credit, GST), Razorpay Checkout, verified callback, capture, activation, renewal, receipts/invoices `SC-YYYY-NNNNN`, renewal reminders | ✅ | `Services/Billing/PlatformBillingService.php`, `Dashboard/BillingController`, `BillingCallbackController` |
| Platform coupons (admin CRUD) and payments dashboard (transactions, revenue, webhook deliveries) | ✅ | `Admin/PlatformCouponController`, `Admin/PaymentController` |
| Webhooks for both systems: HMAC over raw body, event de-duplication, idempotent processing, separate secrets/URLs | ✅ | `WebhookController`, `webhook_events` |
| **Store payments:** merchant Razorpay keys (verified, encrypted, unique per store), "Pay online" at checkout, pay page, verified callback, webhook completion, failed-payment retry | ✅ | `Services/Commerce/StorePaymentService.php`, `Dashboard/PaymentController`, `ShopController::pay/payCallback` |
| Stock reserved while paying, released after 30 min by cron, late payments reinstated | ✅ | `OrderService::placeFromCart`, `orders:expire-unpaid` |
| Refunds: partial/full, via Razorpay or recorded for COD, optional restock | ✅ | `StorePaymentService::refund`, order page |
| Merchant payments list with totals and filters | ✅ | `views/dashboard/sales/payments.php` |
| Discount codes: percent (cap) / fixed / free shipping, minimum, limits (total, per customer), dates; cart apply/remove; GST on discounted price; usage released on cancel | ✅ | `Services/Commerce/CouponService.php`, `Dashboard/CouponController`, `ShopController::cartCoupon` |
| Shipping zones (countries, states, PIN rules, priority) and rates (flat, free, weight, order value, bands, delivery time); checkout delivery options; Shiprocket-ready `provider` field | ✅ | `Services/Commerce/ShippingService.php`, `Dashboard/ShippingController` |
| Customer accounts: sign-up, sign-in/out, forgot/reset, email confirmation, profile, password change, order history, signed-in checkout | ✅ | `Services/Commerce/CustomerAccountService.php`, `Controllers/Storefront/AccountController.php` |
| Merchant customers: list/search/filter, detail with orders and spend, block/unblock, CSV export | ✅ | `Dashboard/CustomerController` |
| Tests: +65 integration (270 total), +122 HTTP | ✅ | `sitecraft:selftest`, `tests/smoke/phase6.sh` |

**Not included (needs the merchant's courier account):** live Shiprocket rate quotes and AWB creation. The data model (`shipping_rates.provider`, order tracking fields) is ready for it; merchants enter tracking manually today.

## Phase 7 — SEO, blog, media library, analytics ✅

| Item | Status | Where |
|---|---|---|
| Media library: folders, multi-upload (20), search, missing-alt filter, rename, alt text, move, bulk actions, storage meter, usage check before delete | ✅ | `Dashboard/MediaController`, `MediaService::whereUsed/delete/usage` |
| Image picker for dashboard forms (blog, SEO) and images inside blog posts | ✅ | `public/assets/js/app.js` (`data-media-pick`), `/dashboard/media/picker` |
| Blog: posts (rich text, excerpt, featured image, category, tags, author, draft/published/scheduled, SEO fields), categories | ✅ | `Services/BlogService.php`, `Dashboard/BlogController` |
| Storefront blog: listing, category & tag pages, post page (BlogPosting JSON-LD, Open Graph article, reading time, related posts), RSS feed, sitemap | ✅ | `Controllers/Storefront/BlogController.php` |
| SEO settings: homepage title/description, sharing image, hide-from-search switch, verification tags (Google, Bing, Pinterest, Meta), live search preview & character counters | ✅ | `Dashboard/SeoController`, `views/dashboard/seo.php` |
| robots.txt editor (advanced plans) with validation; built-in rules fixed for existing sites | ✅ | `SeoController::cleanRobots`, Phase 7 migration |
| Redirects: manual, bulk import, automatic on address changes (pages, products, collections, posts, blog categories), loop/chain handling, hit counts, plan limit | ✅ | `Services/RedirectService.php`, `StorefrontController::notFound` |
| SEO audit with score and fix links (advanced plans) | ✅ | `Services/SeoAuditService.php` |
| Optional Google Analytics 4 (public pages only, CSP-scoped) | ✅ | `StorefrontController::html/gtag` |
| Built-in privacy-friendly analytics: visitors, views, top pages, sources, devices, browsers; sales, orders, conversion, best sellers; 7–365 days by plan; CSV | ✅ | `Services/AnalyticsService.php`, `Dashboard/AnalyticsController` |
| Platform reports: MRR, plan revenue, sign-ups, GMV, visitors, plans, top stores, CSV | ✅ | `Admin/ReportController` |
| Tests: +42 integration (312 total), +89 HTTP | ✅ | `sitecraft:selftest`, `tests/smoke/phase7.sh` |

**Fixed along the way:** every site had a placeholder robots.txt (`Allow: /`) that hid the built-in rules keeping cart/checkout/account out of search; the homepage now uses the SEO screen's title/description; a site hidden from search now stays hidden even where a page says "index".

## Phase 8 — Security testing, performance, deployment ✅

| Item | Status | Where |
|---|---|---|
| Tenant-isolation sweep: every private route (GET/POST, valid CSRF) with another tenant's IDs → 403/404 and victim rows byte-identical; admin routes → 403; signed-out → login | ✅ | `tests/smoke/isolation_sweep.py` |
| Platform CSP without `unsafe-inline` scripts: all 37 inline handlers and 4 inline scripts moved to `app.js` (`data-confirm`, `data-nav-select`, modal templates…) | ✅ | `App\Filters\AppSecurityHeaders`, `public/assets/js/app.js` |
| Permissions-Policy, COOP, HSTS over HTTPS, no `X-Powered-By`; secure cookies by default in production | ✅ | `AppSecurityHeaders`, `Config/Events.php`, `Config/Cookie.php` |
| Failed sign-ins logged as `SECURITY failed-login` (production log level includes them) + fail2ban filter/jail | ✅ | `AuthService`, `deploy/fail2ban` |
| Code scans: raw SQL fragments (all cast/escaped), unescaped view output (IDs/constants only) | ✅ | reviewed |
| `.htaccess`: removed the www→apex rewrite (looped with www-primary custom domains), added compression and static caching | ✅ | `public/.htaccess` |
| Times compared with PHP's clock, not MySQL `NOW()` (no server-timezone dependency) | ✅ | `DomainService` |
| `backup:run` (consistent dump + media, rotation, password never on the command line) | ✅ | `app/Commands/Backup.php` |
| `sitecraft:doctor` (PHP, config, folders, DB, migrations, admin password, email, payments, disk, cron heartbeats) | ✅ | `app/Commands/Doctor.php` |
| Cron heartbeats shown on the admin dashboard; `/health` endpoint for uptime monitors | ✅ | `Libraries/Ops/Heartbeat.php`, `HealthController` |
| Performance: indexes for per-website date queries; measured cached store pages ~8 ms, dashboard ~15–20 ms | ✅ | Phase 8 migration |
| Responsive + CSP + accessibility sweep: 54 pages × 3 widths, axe-core WCAG 2.1 AA (fixed: editor toolbar overflow on phones, low-contrast default 404) | ✅ | `tests/e2e/ux.e2e.js` |
| Branded 404 and 500 pages | ✅ | `app/Views/errors/html/` |
| Deployment kit: Caddy (on-demand TLS for every store/domain), Nginx alternative, PHP/FPM, MySQL, cron, logrotate, fail2ban, production `.env`, release script with backup + rollback | ✅ | `deploy/` |
| Guides: VPS deployment, security model + operator checklist, testing + release checklist; README refreshed | ✅ | `docs/DEPLOYMENT.md`, `docs/SECURITY.md`, `docs/TESTING.md` |
| Tests: Phase 8 smoke (30) + isolation sweep (5) + UX sweep (486) | ✅ | `tests/smoke/phase8.sh` |

**Known limits (by design, documented):** live courier rates/labels (Shiprocket) need the merchant's courier account; merchant domains on Nginx need per-domain certificates (Caddy handles them automatically); multi-server setups need Redis and shared media storage (DEPLOYMENT.md §12).

## After 1.0 — Public marketing site ✅

| Item | Status | Where |
|---|---|---|
| Home page: hero, highlights, features, three steps, template showcase, pricing, FAQ, call to action | ✅ | `app/Views/marketing/home.php` |
| Features page (7 groups, all describing shipped functionality) | ✅ | `marketing/features.php` |
| Templates gallery with category and store/website filters; template pages with live preview of every page | ✅ | `MarketingController::templates/template/preview` |
| Pricing: cards with CSS-only monthly/yearly switch, comparison table, FAQ — all generated from the plans table | ✅ | `Libraries/Marketing/Plans.php`, `marketing/pricing.php` |
| SEO: titles, descriptions, canonical, Open Graph image, Organization/SoftwareApplication/FAQPage JSON-LD, `/sitemap.xml`, `robots.txt` that admits only these pages | ✅ | layout, `MetaController`, `MarketingController::sitemap` |
| No JavaScript, no visitor cookies (CSRF skipped on these GET-only paths), strict CSP kept | ✅ | `Config/Filters.php` |
| Plan/template chosen on the site carried through sign-up into the template wizard and Billing | ✅ | `Libraries/Marketing/SignupIntent.php` |
| Sign-up buttons follow *Allow new merchant sign-ups* | ✅ | layout, partials |
| Tests: 44 HTTP checks; marketing pages added to the responsive/accessibility sweep (59 pages × 3 widths) | ✅ | `tests/smoke/marketing.sh`, `tests/e2e/ux.e2e.js` |

