diff --git a/.gitignore b/.gitignore
index b00d68c6..3d0a15e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,4 +48,6 @@ references/*/vendor
# bind mount, so git ends up unable to modify tracked files the user does not own
# ("unable to unlink old ... Permission denied"). Untracking it removes that
# whole class of failure.
-.aws-sam/
\ No newline at end of file
+.aws-sam/
+# Wrangler local state (docs site at repo root, and www/)
+.wrangler/
diff --git a/design/marketing/web/DEPLOY.md b/design/marketing/web/DEPLOY.md
deleted file mode 100644
index 4262a57c..00000000
--- a/design/marketing/web/DEPLOY.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# Marketing Site Deployment
-
-## Quick Deploy
-
-This is a static site. Deploy the `html/` folder to any static host:
-- **S3 + CloudFront** — Upload contents of `html/` to an S3 bucket, configure CloudFront
-- **Netlify** — Connect repo, set publish directory to `design/marketing/web/html`
-- **Vercel** — Same as Netlify
-- **GitHub Pages** — Point to `design/marketing/web/html`
-- **Cloudflare Pages** — Connect repo, set build output to `design/marketing/web/html`
-
-## URLs
-
-| Link | URL |
-|------|-----|
-| App | `https://app.teem.nz` |
-| Contact | `mailto:hello@teem.nz` |
-
-Docs link has been removed for now. To add it back, add `https://docs.teem.nz` (or similar) to the nav in index.html, features.html, and pricing.html.
-
-## File Structure
-
-```
-html/
-├── index.html # Homepage
-├── features.html # Features page
-├── pricing.html # Pricing page
-└── logo.png # Brand logo — run design/brand/build-teemops-logo.py (transparent PNG)
-```
-
-## SEO (Optional)
-
-- Add `sitemap.xml` listing all pages
-- Add `robots.txt`
-- Add Open Graph meta tags for social sharing
-- Add JSON-LD schema (Organization, Product)
diff --git a/design/marketing/web/content-plan.md b/design/marketing/web/content-plan.md
deleted file mode 100644
index 0412067c..00000000
--- a/design/marketing/web/content-plan.md
+++ /dev/null
@@ -1,288 +0,0 @@
-# Teemops Marketing Website - Content Plan
-
-## Overview
-
-This document outlines all content required for the Teemops marketing website, targeting CTOs and DevOps engineers at SMBs (20-200 employees) in SaaS, Fintech, and Healthtech industries.
-
----
-
-## Site Structure
-
-```
-teemops.com/
-├── / (Homepage)
-├── /features
-├── /pricing
-├── /security
-├── /docs (links to documentation)
-├── /blog (future)
-├── /about
-├── /contact
-├── /login (→ app)
-├── /signup (→ app)
-│
-├── /use-cases/
-│ ├── /saas-companies
-│ ├── /fintech
-│ ├── /healthtech
-│ └── /agencies
-│
-├── /compliance/
-│ ├── /cis-benchmarks
-│ ├── /pci-dss
-│ └── /soc2
-│
-└── /resources/ (future)
- ├── /case-studies
- ├── /whitepapers
- └── /webinars
-```
-
----
-
-## Page Requirements
-
-### 1. Homepage (/)
-
-**Purpose:** Convert visitors to signups, communicate core value proposition
-
-**Sections:**
-1. Hero - Main value prop + CTA
-2. Problem/Pain - Why cloud security is hard
-3. Solution - How Teemops helps
-4. Features overview - Key capabilities
-5. How it works - 3-step process
-6. Social proof - Logos, testimonials
-7. Pricing teaser - Starting price + CTA
-8. Final CTA - Get started
-
-**Content File:** `pages/homepage.md`
-**Mockup:** `html/index.html`
-
----
-
-### 2. Features Page (/features)
-
-**Purpose:** Detail all product capabilities for evaluators
-
-**Sections:**
-1. Hero - Features overview
-2. AWS Security Scanning
-3. Compliance Monitoring (CIS, PCI, SOC2)
-4. Multi-Account Management
-5. Real-time Alerts
-6. Remediation Guidance
-7. Reporting & Exports
-8. Integrations (future)
-9. CTA
-
-**Content File:** `pages/features.md`
-**Mockup:** `html/features.html`
-
----
-
-### 3. Pricing Page (/pricing)
-
-**Purpose:** Clear pricing, drive signup decisions
-
-**Sections:**
-1. Pricing tiers (Free, Starter, Pro, Business)
-2. Feature comparison table
-3. FAQ
-4. Enterprise contact CTA
-
-**Content File:** `pages/pricing.md`
-**Mockup:** `html/pricing.html`
-
----
-
-### 4. Security Page (/security)
-
-**Purpose:** Build trust, show we practice what we preach
-
-**Sections:**
-1. Our security commitment
-2. How we protect your data
-3. Compliance certifications (future)
-4. Responsible disclosure
-5. FAQ
-
-**Content File:** `pages/security.md`
-**Mockup:** `html/security.html`
-
----
-
-### 5. Use Case Pages (/use-cases/*)
-
-**Purpose:** Industry-specific messaging and SEO
-
-**Pages:**
-- SaaS Companies
-- Fintech & Payments
-- Healthtech
-- Digital Agencies
-
-**Content File:** `pages/use-cases.md`
-**Mockup:** `html/use-case.html` (template)
-
----
-
-### 6. Compliance Pages (/compliance/*)
-
-**Purpose:** Compliance-specific messaging and SEO
-
-**Pages:**
-- CIS AWS Benchmarks
-- PCI-DSS
-- SOC 2
-
-**Content File:** `pages/compliance.md`
-**Mockup:** `html/compliance.html` (template)
-
----
-
-### 7. About Page (/about)
-
-**Purpose:** Build trust, tell our story
-
-**Sections:**
-1. Our mission
-2. The team (future)
-3. Why we built Teemops
-4. Contact info
-
-**Content File:** `pages/about.md`
-
----
-
-### 8. Contact Page (/contact)
-
-**Purpose:** Lead capture, support inquiries
-
-**Sections:**
-1. Contact form
-2. Email addresses
-3. Social links
-
-**Content File:** `pages/contact.md`
-
----
-
-## Content Inventory
-
-### Core Messaging
-
-| Element | Content |
-|---------|---------|
-| **Tagline** | "AWS Security Scanning Made Simple" |
-| **Subheadline** | "Find misconfigurations before attackers do. Get actionable fixes in minutes, not weeks." |
-| **Value Prop 1** | See all your AWS security gaps in one dashboard |
-| **Value Prop 2** | Continuous CIS, PCI-DSS, SOC 2 compliance monitoring |
-| **Value Prop 3** | One-click remediation guidance for every finding |
-| **CTA Primary** | "Start Free Scan" |
-| **CTA Secondary** | "Book a Demo" |
-
-### SEO Keywords (Primary)
-
-| Keyword | Search Volume | Difficulty | Priority |
-|---------|---------------|------------|----------|
-| AWS security scanning | Medium | Medium | High |
-| AWS misconfiguration scanner | Low | Low | High |
-| Cloud security posture management | Medium | High | Medium |
-| AWS CIS benchmark tool | Low | Low | High |
-| AWS compliance monitoring | Medium | Medium | High |
-| S3 bucket security checker | Low | Low | High |
-
-### SEO Keywords (NZ-Specific)
-
-| Keyword | Priority |
-|---------|----------|
-| AWS security NZ | High |
-| Cloud security New Zealand | High |
-| NZISM compliance | Medium |
-| NZ Privacy Act compliance | Medium |
-
----
-
-## Content Assets Required
-
-### Images/Graphics
-
-| Asset | Purpose | Status |
-|-------|---------|--------|
-| Hero illustration | Homepage hero | Needed |
-| Feature icons (8x) | Features page | Needed |
-| How it works diagram | Homepage | Needed |
-| Dashboard screenshot | Social proof | Needed |
-| Scan results screenshot | Features | Needed |
-| Compliance badges | Trust signals | Needed |
-| Team photos | About page | Future |
-| Customer logos | Social proof | Future |
-
-### Videos (Future)
-
-| Video | Purpose | Priority |
-|-------|---------|----------|
-| Product demo (2 min) | Homepage, features | High |
-| Getting started tutorial | Onboarding | Medium |
-| Customer testimonials | Social proof | Medium |
-
----
-
-## Page Priority for MVP
-
-| Priority | Page | Reason |
-|----------|------|--------|
-| 1 | Homepage | Core conversion page |
-| 2 | Pricing | Required for purchase decisions |
-| 3 | Features | Evaluator research |
-| 4 | Security | Trust building |
-| 5 | Use Cases (SaaS) | Target market alignment |
-| 6 | Compliance (CIS) | SEO + credibility |
-| 7 | About | Trust building |
-| 8 | Contact | Lead capture |
-
----
-
-## Technical Requirements
-
-### Performance
-- Page load < 3 seconds
-- Mobile-first responsive design
-- Core Web Vitals optimized
-
-### SEO
-- Meta titles/descriptions for all pages
-- Open Graph tags
-- Schema markup (Organization, Product, FAQ)
-- Sitemap.xml
-- robots.txt
-
-### Analytics
-- Google Analytics 4
-- Conversion tracking (signup, demo request)
-- Heatmaps (Hotjar/similar)
-
-### Integration
-- CTA buttons link to app signup
-- Contact form to CRM/email
-- Chat widget (future - Intercom/Crisp)
-
----
-
-## Content Timeline
-
-| Week | Deliverable |
-|------|-------------|
-| 1 | Homepage copy + mockup |
-| 1 | Pricing page copy + mockup |
-| 2 | Features page copy + mockup |
-| 2 | Security page copy |
-| 3 | Use case pages (2) |
-| 3 | Compliance pages (1) |
-| 4 | About, Contact pages |
-| 4 | Final review + polish |
-
----
-
-*Last updated: January 2026*
diff --git a/design/marketing/web/html/features.html b/design/marketing/web/html/features.html
deleted file mode 100644
index fa2e62a7..00000000
--- a/design/marketing/web/html/features.html
+++ /dev/null
@@ -1,745 +0,0 @@
-
-
-
-
-
-
Complete AWS Security Visibility
-
- Built in New Zealand. Scan your entire AWS environment—find misconfigurations and fix them fast.
-
-
- Start Free Scan →
-
-
-
-
-
-
-
-
See All Your AWS Security Gaps
-
- Start your first scan in under 5 minutes. Free tier available.
-
-
-
-
-
-
-
-
-
-
Built in New Zealand for Growing Tech Teams
-
Join teams across NZ and beyond securing their AWS environments—SaaS, fintech, and healthtech.
-
-
-
-
4
-
AWS services scanned
-
-
-
25+
-
Security checks
-
-
-
-
< 5 min
-
Time to first scan
-
-
-
-
What teams securing AWS are saying
-
-
-
- "We knew we had security gaps but didn't know where to start. Teemops showed us exactly what to fix first. Saved us from hiring a consultant."
-
-
-
-
-
CTO
-
NZ SaaS Company (50 employees)
-
-
-
-
-
- "The compliance reporting alone is worth it. Our auditors love the CIS benchmark reports."
-
-
-
-
-
VP Engineering
-
Fintech Startup
-
-
-
-
-
-
-
-
-
-
-
Need More?
-
- For organizations with complex requirements, custom integrations, or regulatory needs—let's talk.
-
-
- • Unlimited AWS accounts
- • Custom compliance frameworks
- • Dedicated support
- • Volume discounts
-
-
- Contact Sales
-
-
-
-
-
-
-
-
-
- §1 · Hero + jump nav
- Everything TOPS does
- 74 checks across 11 AWS services. Self-hosted. Apache-2.0.
-
-
- Findings · Scanning · Filtering · Remediation ·
- Insights · Teams
-
-
- Note 1 — this page is scanned, not read
- The reader has decided we're interesting and is now checking whether we do the one specific
- thing they need. Optimise for someone using ⌘F. Sticky jump nav under the main nav on
- lg+; drop it below md rather than stacking two sticky bars.
-
-
- Note 2 — reorganised, not edited
- The current page (../pages/features.md, 333 lines) is a generic CSPM feature list:
- scanning, compliance, multi-account, alerts, remediation, reporting, integrations. Three of
- those sections describe things that don't exist or are now wrong. This is organised around what
- we actually ship.
-
-
-
-
-
- §2 · Durable findings
- Findings that persist
- One record per resource and rule — not one row per scan.
-
-
-
-
- - Mark resolved or ignored — it stays that way through the next scan
- - First-seen and last-seen on every finding
- - Reappears automatically if the misconfiguration comes back
-
-
-
- Note 3 — the anatomy figure
- Ships #81 and #90. Every element on this card is a separate capability, labelled once; the
- sections below refer back to it rather than re-explaining.
-
-
- Note 4 — "51 days open" is the number that sells this
- It is only computable because a finding is a durable record (#81). A per-scan row cannot
- produce it.
-
-
-
-
-
- §3 · Scanning
- Scan what you need, when you need it
-
-
-
Whole benchmark
-
CIS AWS Foundations, or the general "basic" profile.
-
-
-
One service
-
Changed one S3 policy? Scan S3 and get an answer in a
- fraction of the time.
-
-
- Regions run in parallel. A ten-region account isn't ten times the wait.
-
- Coverage
-
- | Service | Checks | Service | Checks |
- | S3 | n | IAM | n |
- | EC2 | n | RDS | n |
- | CloudTrail | n | KMS | n |
- | Lambda | n | DynamoDB | n |
- | ELBv2 | n | SNS | n |
- | SQS | n | 11 services · 74 checks |
-
- See every check → docs
-
-
- Note 5 — the coverage table is the ⌘F target
- Highest-value block on the page for an evaluator. Per-service counts must be
- generated from the rule JSON via scan:validate-rules, never typed.
- A stale count here is exactly the small inaccuracy that loses a technical buyer.
-
-
- Note 6 — link out, don't inline
- Full listing of all 74 checks belongs in the docs, not on this page.
-
-
-
-
-
- §4 · Filter and slice
- Cut 400 findings down to the 8 you're working on
-
-
-
-
- Account: prod ▾
- Service: S3 ▾
- Benchmark: CIS 2.x ▾
- Severity: Critical ▾
- Status: Open ▾
- 12 results
-
-
-
-
-
- /findings?service=s3&benchmark=cis&status=open
-
-
- ↑ every filter is in the URL — bookmark it, paste it in Slack, send it to the person who owns S3
-
-
-
- Note 7 — render the URL as visible monospace, not a caption
- Ships #85, #87, #83. The URL is the section; it should not be styled as an aside.
-
-
- Note 8 — one sentence on why benchmark filtering works
- Findings record which benchmark raised them (a74a6c3), so a compliance question has
- a direct answer.
-
-
-
-
-
- §5 · Remediation
- Every finding tells you how to fix it
-
-
-
In the list
-
-
One line. No click needed.
-
-
-
On the finding
-
- - Open the RDS console
- - Modify → Connectivity
- - Public access → No
- - Apply immediately
-
-
AWS docs →
-
-
- Step-by-step guidance on every critical and high finding.
-
-
- Note 9 — the split is the real product behaviour
- Ships #82. A one-liner everywhere, full steps on critical and high. Do not
- imply step-by-step exists on all 74 checks — the README is precise about this and the site must
- match it.
-
-
-
-
-
- §6 · Insights
- Where the problems actually are
-
-
-
By service
-
With a severity breakdown per service, so "42 in S3" becomes
- "12 critical in S3".
-
-
-
By benchmark
-
Which controls are failing, and how badly.
-
-
-
- Also: findings trend over a period, severity distribution, remediation rate.
-
-
- Note 10 — the "also" line is table stakes and gets one line
- Ships #88 and #89. Trend / distribution / remediation-rate already existed and must not get
- equal billing with the two groupings.
-
-
- Note 11 — no score, no grade, no single health number anywhere on this page
-
-
-
-
-
- §7 · Teams and isolation
- Built multi-tenant from the schema up
- Organisations · team roles · every query scoped by organisation.
- Run one instance for several clients, or several teams, without one seeing another's findings.
-
- Note 12 — short, and aimed at agencies and MSPs
- A real segment for a self-hosted scanner. It is genuinely enforced rather than aspirational, so
- we can state it flatly.
-
-
-
-
-
- §8 · What we don't do
- What TOPS doesn't do (yet)
-
- - Azure or GCP — AWS only
- - Auto-remediation — we tell you the fix, you apply it
- - Real-time alerting / webhooks — scans are on demand
- - Agent-based runtime monitoring — this is config scanning
-
- Roadmap → github.com/teemops/tops
-
- Note 13 — recommend keeping this section
- It disqualifies bad-fit visitors before they install and bounce, it is unusual enough to be
- memorable, and on an open-source project it reads as confidence. It also lets us delete the
- current page's "Real-time Alerts" and "Integrations (future)" sections honestly instead of
- leaving them as vapour.
-
-
- Note 14 — only an asset while it's accurate
- Verify every line against the roadmap before shipping.
-
-
-
-
-
- §9 · CTA
-
-
- $ bash <(curl -fsSL https://raw.githubusercontent.com/teemops/tops/develop/install.sh)
- ⧉ Copy
-
-
Read the script first → ★ Star on GitHub
-
-
-
Note 15 — no paid alternative offered, here or anywhere on this page
- Same block as
homepage §9. The only commercial mention on the site is
- the footer line in homepage §10.
-
-
-
-
-
- Removed from the current features page
-
-
Deleted
-
- - Three "Score" tiles (
../html/features.html:285, 303, 321) — feature removed
- in a246323
- - "Real-time Alerts" section — does not exist; now one line in §8
- - "Integrations (future)" section — does not exist; now one line in §8
- - "Reporting & Exports" as a headline section — demote to a line unless we can
- screenshot it
- - All "Start Free Scan" CTAs
-
-
-
-
-
-
-
-
-
-
diff --git a/design/marketing/web/wireframes/html/index.html b/design/marketing/web/wireframes/html/index.html
deleted file mode 100644
index a2d0b3e2..00000000
--- a/design/marketing/web/wireframes/html/index.html
+++ /dev/null
@@ -1,413 +0,0 @@
-
-
-
-
-
-
-
- §1 · Nav
-
-
▣ TOPS
-
Features How it works Docs
-
★ Star on GitHub 1.2k
-
-
- Note 1 — what's missing is the point
- No Pricing item and no Login / Sign up. There is nothing to sign up for, and a
- nav link named "Pricing" on a free project invites the "so what's the catch" reflex we have
- nothing to answer with. The GitHub button is the persistent right-hand action instead.
- Sticky on scroll; collapses to hamburger + GitHub button below md.
-
-
- Note 2 — star count
- Live from the GitHub API, cached. Render the button without a count if the call fails rather
- than blocking paint.
-
-
-
-
-
- §2 · Hero
-
-
-
Find what's wrong in your AWS account. Then fix it.
-
- Open-source AWS security scanning you run yourself. 74 checks, every finding carries the
- fix, and the ones you've triaged stay triaged.
-
-
- $ bash <(curl -fsSL https://raw.githubusercontent.com/teemops/tops/develop/install.sh)
- ⧉ Copy
-
-
Docker is the whole list.
-
- ★ Star on GitHub
- Docs →
-
-
-
-
-
Findings
-
-
- All accounts ▾
- S3 ▾
- CIS ▾
-
-
-
-
-
-
-
-
-
- Note 3 — this panel replaces the security score
- The current hero is built around a 78/100 Security Score
- (../html/index.html:104). That feature was deleted in a246323
- (issue #91 — pinned at 0, could not improve). The replacement shows what we built
- instead of it: a findings register where every row has a severity, a remediation, a
- benchmark tag, and a status that persists.
-
-
- Note 4 — three different statuses, deliberately
- Open / Ignored / Resolved on screen at once is the entire pitch for issue #90 in one
- glance. Do not let this become three Opens. The cis-* tags are issue #87 made
- visible; use real rule IDs.
-
-
- Note 5 — the filter chips must match the real UI
- They are decorative here, but this panel becomes a real screenshot as soon as one is captured,
- and it must not look like a downgrade when it does.
-
-
- Note 6 — the copy button is the conversion event
- Click-to-copy with a "Copied" toast. Instrument it. Below lg the panel drops under
- the copy and the command wraps to two lines — do not hide it on mobile, people screenshot it.
-
-
-
-
-
- §3 · Proof strip
-
- 74 checks · 11 AWS services · CIS AWS Foundations ·
- Apache-2.0 · Docker Compose, nothing else
-
-
- Note 7 — all five are verifiable
- Thin band, single row, no icons. Numbers come from the rule set, not from a copywriter —
- scan:validate-rules is the source and they must be regenerated when rules change.
-
-
-
-
-
- §3b · Free. Forever. — decision pending
-
-
Free. Forever.
-
- Apache-2.0. Every check, unlimited AWS accounts, unlimited users. No trial, no seats, no
- edition above this one, and no feature we've held back to sell you later.
-
-
Read the licence
-
-
- Note 8 — this is the open question
- /pricing 301s here (#free). Whether the band earns homepage space is
- the last structural decision in this set — see ../pricing.md. Recommendation: keep
- it, keep it small. Every visitor arriving from a commercial CSPM carries the assumption that
- there is a catch, and one unambiguous paragraph is cheaper than losing them to that suspicion.
- It must read as a statement of fact, not a sales counter-argument: no comparison table, no
- "unlike other vendors", no exclamation marks.
-
-
-
-
-
- §4 · The problem
- Most scanners hand you a PDF. Then another one.
-
-
Every scan starts from zero
-
You triaged it last week. It's back.
-
400 findings, no idea which 3 matter
-
-
- Note 9 — keep the ordering aligned
- §5 answers the first two cards, §8 answers the third. If the cards get reordered in copy, the
- sections move with them.
-
-
-
-
-
- §5 · Durable findings — the differentiator
- A finding is a record, not a row in a report
-
-
Diagram — same finding across two scans,
status intact. Custom illustration.
-
-
Scan on Monday
- HIGH · Root account has no MFA
- → you mark it Ignored: "root is break-glass only"
-
Scan on Friday
- HIGH · Root account has no MFA
- → still Ignored
-
Your triage survives. So does the first-seen date.
-
-
-
- Note 10 — the one claim a competitor's free tier can't make
- Ships issues #81 and #90. This is the section to get right. A diagram, not a screenshot — a
- screenshot cannot show time.
-
-
- Note 11 — copy constraint
- Must not say "we track findings over time"; every vendor says that. Say what breaks without it:
- you re-triage the same thing every week. Keep the first-seen / last-seen line — "how
- long has this been open" is the question a durable record answers and a per-scan row cannot.
-
-
-
-
-
- §6 · How it works
-
-
-
① Install
-
$ bash <(curl …)
Docker Compose. No PHP, no database.
-
-
-
② Connect an account
-
A CloudFormation stack grants TOPS a read-only audit role.
-
-
-
③ Scan
-
Findings appear as they are produced.
-
-
- You don't need an AWS account to try it.
-
- Note 12 — "read-only" goes in the step, not the small print
- It is the first objection a CTO has, and answering it early is worth more than a trust badge.
-
-
- Note 13 — no total time claimed here
- 7494959 instrumented scan phases to baseline where the time goes. Do not put a
- number on this page until the instrumentation supports one.
-
-
-
-
-
- §7 · Scope a scan
- Scan one service, not the whole benchmark
-
-
-
New scan
-
-
Account
-
prod-1234 ▾
-
Profile
-
CIS AWS Foundations ▾
-
Services
-
- ✓ S3IAMEC2
- RDS+7 more
-
-
Start scan
-
-
-
-
Chasing one S3 change? Scan S3.
-
Regions run in parallel, so more regions doesn't mean a longer wait.
-
-
-
- Note 14 — one sentence is the right budget for multi-region
- Ships #84 and #92. Parallel regions is an implementation win, not a buying reason. Becomes a
- screenshot of the real New Scan modal once captured.
-
-
-
-
-
- §8 · Insights
- Know which three things matter
-
-
-
Findings by service
-
-
…each with a severity breakdown
-
-
-
Findings by benchmark
-
- - CIS 1.x18
- - CIS 2.x40
- - CIS 3.x11
-
-
-
- Click through to a filtered, shareable list →
-
- Note 15 — no total, no score, no headline metric
- Ships #88, #89 and — via the caption — #83 and #85/#87. Resist adding a single health number
- here; that instinct is what produced the security score in the first place.
-
-
- Note 16 — say "shareable", not "URL state"
- Shareable is the user-facing value of filters living in the URL.
-
-
-
-
-
- §9 · Final CTA
-
-
One command. Docker is the only prerequisite.
-
- $ bash <(curl -fsSL https://raw.githubusercontent.com/teemops/tops/develop/install.sh)
- ⧉ Copy
-
-
Read the script first → ★ Star on GitHub
-
-
- Note 17 — "read the script first" is a trust signal that costs nothing
- Links to the curl -o install.sh variant in the docs. Lands with exactly the
- audience we want.
-
-
-
-
-
- §10 · Footer — and the one Teem link
-
-
Product
Features
How it works
Insights
-
Docs
Install
Scanning AWS
Configuration
-
Project
GitHub
Issues
Releases
-
Legal
Licence
Trademark
Privacy
-
-
-
-
-
- Teem is the company behind the open source software.
- For AWS security auditing, view their website →
- teem.cloud
-
-
-
-
-
- TOPS is free and open source under Apache-2.0. Apache-2.0
-
-
-
- Note 18 — this band is the only commercial mention on the entire site
- Not a card, not a banner, not a CTA — a sentence and a link, below the fold of every page, in
- body-text weight. Anyone who needs a vendor will find it; nobody who just wants the software
- has to step around it.
-
-
- Note 19 — treat these as constraints, not preferences
- One placement, site-wide — footer only, never nav/hero/mid-page/banner.
- No commercial verbs — no "get support", "talk to sales", "enterprise",
- "pricing", "book a demo". rel="noopener", new tab — a visitor
- leaving for teem.cloud must not lose the install command they were about to copy.
- Wording is fixed (product-owner supplied); copywriting may fix punctuation and
- nothing else. Apache-2.0 sits below it, so the last thing on every page is the
- free-software statement rather than the company.
-
-
- Note 20 — the Red Hat split
- Forever-free software here; a company behind it for customers who need stability or help with
- setup and management. Everything about that lives on teem.cloud and is not explained
- here. Red Hat works because fedoraproject.org does not sell RHEL — the moment teemops.com
- describes support offerings, every free claim on the site reads as a funnel. The old "Company"
- footer column is renamed Project for the same reason.
-
-
-
-
-
- Removed from the current homepage
-
-
Deleted
-
- - Security Score panel (
../html/index.html:104) — the feature no longer exists
- - All "Start Free Scan" CTAs — nothing to sign up for
- - "No credit card required. Free tier available forever." — implies a paid tier above it
- - Social proof / customer logo section — no customers to name; a row of grey placeholder
- logos reads worse than no section at all
- - Pricing teaser with $/mo, and the "Pricing" nav item — there is no pricing
- - The three-card "who runs it" block from the first draft of this wireframe — replaced by
- the single footer line in §10
-
-
-
-
-
-
- Assets still needed
-
- | Asset | Blocking? | Note |
- | Findings list screenshot | §2 — yes | Needs realistic data with all three statuses visible |
- | Rescan diagram | §5 — yes | Custom illustration; nothing existing works |
- | New Scan modal screenshot | §7 — no | Wireframe box is adequate for review |
- | Insights screenshot | §8 — no | Same |
- | Live GitHub star count | §1 — no | Degrade to a plain button |
-
-
-
-
-
-
-
-
-
diff --git a/design/marketing/web/wireframes/html/pricing.html b/design/marketing/web/wireframes/html/pricing.html
deleted file mode 100644
index 40ff4482..00000000
--- a/design/marketing/web/wireframes/html/pricing.html
+++ /dev/null
@@ -1,155 +0,0 @@
-
-
-
-
-
-
- Recommendation
- Delete the pricing page. Keep the URL.
-
- teemops.com sells nothing. No tiers, no managed-hosting cards, no service offers, no
- "talk to us". The software is Apache-2.0 and stays that way, and the company behind it gets
- exactly one sentence in the footer of every page.
-
-
- This page exists only because /pricing is currently in the nav and the sitemap,
- and "deleted the page" is not a complete answer for a URL people will still type, search for
- and link to.
-
-
-
-
- What replaces it
- teemops.com/pricing → 301 → teemops.com/#free
-
- A permanent redirect to a short "Free. Forever." band on the homepage, sitting
- between the proof strip (§3) and the problem section (§4).
-
-
-
-
-
Free. Forever.
-
- Apache-2.0. Every check, unlimited AWS accounts, unlimited users. No trial, no seats, no
- edition above this one, and no feature we've held back to sell you later.
-
-
Read the licence
-
-
-
-
- Note 1 — four lines and a link
- That is the whole answer. Giving it a dedicated page would make it look like a longer answer
- than it is.
-
-
- Note 2 — why keep the route at all
- "is X free" and "X pricing" are the highest-intent sceptical queries an open-source project
- gets, and they are exactly the ones a commercial competitor's comparison page will try to own.
- Landing them on a two-sentence "yes, actually free" costs one redirect rule.
-
-
- Note 3 — the open judgement call
- Does this band belong on the homepage at all? The hero already says open-source and
- self-hosted, and §3 already shows the Apache-2.0 badge; an argument exists that a whole band
- restating it protests too much. Recommendation: keep it, keep it small. If it
- is cut, point the redirect at the features-page FAQ instead of #free, so the URL
- still lands somewhere that answers the question.
-
-
-
-
- The Red Hat framing — where each half is said
- Product owner, 2026-08-02:
-
- We provide open source, forever free software, but we have a company behind this so if a
- customer needs stability and a different level of support during setup or management of the
- software, Teem will support.
-
-
-
- | Claim | Said on |
- | The software is free, forever, no held-back features | teemops.com — loudly, repeatedly |
- | A company stands behind it | teemops.com — one footer sentence, once |
- | What that company will do for you, and what it costs | teem.cloud only |
-
-
-
- Note 4 — the discipline this needs
- Red Hat works because fedoraproject.org does not sell RHEL. The moment teemops.com starts
- describing support tiers, response times or engagement models, every free claim on the site
- reads as a funnel. The footer link is load-bearing because it is the only one — it is
- a door, not a pitch.
-
-
- Note 5 — so, explicitly none of these
- No support page, no enterprise page, no contact-sales form, no "need help?" callout, no
- comparison table with a paid column. People looking to spend money are good at finding where to
- spend it.
-
-
-
-
- Consequences for the rest of the site
-
- | File / element | Action |
- | Nav "Pricing & support" item | Remove → Features · How it works · Docs + GitHub button |
- | Homepage three-card paid block | Remove (was in the first draft of this set) |
- ../pages/pricing.md — 230 lines | Delete, not rewrite |
- ../html/pricing.html — 588 lines | Delete |
- ../pricing-strategy.md — 431 lines | Move, don't delete — only written-down SMB segment analysis; plausible input for teem.cloud |
- ../content-plan.md site structure | Drop /pricing; drop /contact on the same logic |
- "free tier" wording across ../pages/*.md | Reword — "free tier" implies a paid tier above it. The word is just free |
-
-
- Note 6 — /contact is a sales channel wearing a different hat
- GitHub issues is the contact channel for a project; teem.cloud can own the rest. Flagged, not
- actioned in this set.
-
-
-
-
- Removed from the current pricing page
-
-
Deleted
-
- - Free / Starter ($149) / Pro ($449) / Business tiers — priced a SaaS we are not shipping
- - "20% off annual" — no subscription product to discount
- - Per-tier limits on AWS accounts, scan frequency, check count, history, users — none of
- these limits exist in the software, and inventing them contradicts the licence
- - "Not competing on price with DIY/open source" positioning — we are the open
- source
- - The managed-hosting and support-services cards drafted earlier in this wireframe set —
- those belong on teem.cloud, which is out of scope for
-
design/marketing/web/
-
-
-
-
-
-
-
-
-
-
diff --git a/design/marketing/web/wireframes/html/wireframe.css b/design/marketing/web/wireframes/html/wireframe.css
deleted file mode 100644
index c92bcde1..00000000
--- a/design/marketing/web/wireframes/html/wireframe.css
+++ /dev/null
@@ -1,222 +0,0 @@
-/* Low-fidelity wireframe styling for the teemops.com redraw.
- Greyscale on purpose — this is structure and message, not visual design.
- Self-contained: no CDN, no fonts, no network. Opens offline from the filesystem. */
-
-:root {
- --ink: #1a1a1a;
- --mid: #6b6b6b;
- --faint: #9a9a9a;
- --line: #c2c2c2;
- --fill: #f1f1f1;
- --fill-2: #e4e4e4;
- --paper: #fff;
- --note: #0b57d0;
- --note-bg: #eef3fd;
- --note-line: #b9cdf3;
- --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
- --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
-}
-
-* { box-sizing: border-box; }
-
-body {
- margin: 0;
- padding: 56px 0 80px;
- background: #d9d9d9;
- color: var(--ink);
- font: 400 15px/1.55 var(--sans);
-}
-
-/* ---------- wireframe chrome (not part of the design) ---------- */
-
-.wf-bar {
- position: fixed; inset: 0 0 auto 0; z-index: 100;
- display: flex; align-items: center; gap: 4px; flex-wrap: wrap;
- padding: 8px 14px;
- background: var(--ink); color: #fff;
- font: 500 12px/1 var(--mono);
-}
-.wf-bar strong { font-weight: 700; margin-right: 10px; letter-spacing: .04em; }
-.wf-bar a {
- color: #fff; text-decoration: none; opacity: .62;
- padding: 5px 9px; border-radius: 4px;
-}
-.wf-bar a:hover { opacity: 1; background: #ffffff1f; }
-.wf-bar a[aria-current="page"] { opacity: 1; background: #ffffff2e; }
-.wf-bar button {
- margin-left: auto; cursor: pointer;
- padding: 5px 10px; border: 1px solid #ffffff4d; border-radius: 4px;
- background: none; color: #fff; font: 500 12px/1 var(--mono);
-}
-.wf-bar button:hover { background: #ffffff1f; }
-
-.page {
- max-width: 1080px; margin: 0 auto; padding: 0 16px;
-}
-
-/* Each section gets a labelled frame so reviewers can cite "§5" */
-.sec {
- position: relative;
- margin: 26px 0;
- padding: 40px 34px 34px;
- background: var(--paper);
- border: 1px solid var(--line);
-}
-.sec > .tag {
- position: absolute; top: 0; left: 0;
- padding: 4px 10px;
- background: var(--ink); color: #fff;
- font: 600 10px/1.4 var(--mono); letter-spacing: .08em; text-transform: uppercase;
-}
-.sec.tight { padding-top: 34px; }
-
-/* ---------- generic low-fi primitives ---------- */
-
-h1, h2, h3, h4 { margin: 0 0 12px; line-height: 1.2; font-weight: 700; }
-h1 { font-size: 34px; }
-h2 { font-size: 25px; }
-h3 { font-size: 17px; }
-h4 { font-size: 14px; text-transform: uppercase; letter-spacing: .06em; color: var(--mid); }
-p { margin: 0 0 12px; }
-p.lede { font-size: 17px; color: var(--mid); }
-.center { text-align: center; }
-.muted { color: var(--mid); }
-.small { font-size: 13px; }
-.tiny { font-size: 11.5px; }
-
-.row { display: flex; gap: 18px; flex-wrap: wrap; }
-.row > * { flex: 1 1 220px; min-width: 0; }
-.split { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; align-items: center; }
-.cols-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
-.cols-2 { display: grid; grid-template-columns: repeat(2, 1fr); gap: 18px; }
-@media (max-width: 860px) {
- .split, .cols-3, .cols-2 { grid-template-columns: 1fr; }
- .sec { padding: 38px 18px 24px; }
- h1 { font-size: 27px; }
-}
-
-.box {
- padding: 16px;
- border: 1px solid var(--line);
- background: var(--paper);
-}
-.box.fill { background: var(--fill); }
-.box.dash { border-style: dashed; }
-
-/* placeholder for imagery we don't have yet */
-.ph {
- display: flex; align-items: center; justify-content: center;
- min-height: 150px; padding: 16px; text-align: center;
- border: 1px dashed var(--faint);
- color: var(--faint);
- font: 500 12px/1.5 var(--mono);
- background:
- linear-gradient(to top right, transparent calc(50% - .5px), var(--faint) 50%, transparent calc(50% + .5px)),
- linear-gradient(to bottom right, transparent calc(50% - .5px), var(--faint) 50%, transparent calc(50% + .5px)),
- var(--fill);
-}
-.ph span { padding: 4px 8px; background: var(--fill); }
-
-.btn {
- display: inline-block; padding: 11px 20px;
- border: 1.5px solid var(--ink); background: var(--ink); color: #fff;
- font: 600 14px/1 var(--sans); text-decoration: none;
-}
-.btn.ghost { background: none; color: var(--ink); }
-.btn.sm { padding: 7px 12px; font-size: 12.5px; }
-
-/* the install command — the primary conversion element on the site */
-.cmd {
- display: flex; align-items: center; gap: 10px;
- padding: 13px 14px;
- border: 1.5px solid var(--ink); background: var(--fill);
- font: 500 13px/1.3 var(--mono);
- overflow: hidden;
-}
-.cmd code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.cmd .copy {
- flex: none; padding: 4px 9px;
- border: 1px solid var(--ink); background: var(--paper);
- font: 600 11px/1.3 var(--mono);
-}
-@media (max-width: 860px) { .cmd code { white-space: normal; } }
-
-/* app-UI chrome mimic, used in hero + product panels */
-.app {
- border: 1px solid var(--ink); background: var(--paper);
-}
-.app > .bar {
- display: flex; align-items: center; gap: 6px;
- padding: 8px 10px; border-bottom: 1px solid var(--line); background: var(--fill);
- font: 600 11px/1 var(--mono); color: var(--mid);
-}
-.app > .bar i { width: 9px; height: 9px; border: 1px solid var(--faint); border-radius: 50%; }
-.app > .bar em { margin-left: auto; font-style: normal; }
-.app > .body { padding: 12px; }
-
-.chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
-.chip {
- padding: 4px 9px; border: 1px solid var(--line); background: var(--fill);
- font: 500 11px/1.3 var(--mono); color: var(--mid); white-space: nowrap;
-}
-.chip.on { border-color: var(--ink); color: var(--ink); background: var(--paper); }
-
-/* a findings row — severity is a weight, never a colour, in a wireframe */
-.finding {
- display: flex; gap: 10px;
- padding: 10px 0; border-top: 1px solid var(--line);
-}
-.finding:first-child { border-top: 0; }
-.finding .sev {
- flex: none; width: 62px;
- font: 700 10px/1.6 var(--mono); letter-spacing: .04em;
- border-left: 4px solid var(--ink); padding-left: 7px;
-}
-.finding .sev.s2 { border-left-color: var(--mid); }
-.finding .sev.s3 { border-left-color: var(--line); }
-.finding .meta { flex: 1; min-width: 0; }
-.finding .title { font-weight: 600; font-size: 13.5px; }
-.finding .fix { font-size: 12.5px; color: var(--mid); }
-.finding .tags { margin-top: 4px; display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
-.status { font: 600 11px/1.4 var(--mono); }
-
-/* simple bar chart for the insights blocks */
-.bars { margin: 0; padding: 0; list-style: none; font: 500 12px/1 var(--mono); }
-.bars li { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
-.bars .k { width: 74px; flex: none; color: var(--mid); }
-.bars .b { height: 12px; background: var(--fill-2); border: 1px solid var(--line); }
-.bars .n { color: var(--mid); }
-
-table.wf { width: 100%; border-collapse: collapse; font-size: 13.5px; }
-table.wf th, table.wf td { padding: 9px 10px; border-bottom: 1px solid var(--line); text-align: left; }
-table.wf th { font: 600 11px/1.4 var(--mono); text-transform: uppercase; letter-spacing: .05em; color: var(--mid); }
-table.wf td.c, table.wf th.c { text-align: center; }
-
-ul.ticks { margin: 0; padding: 0; list-style: none; }
-ul.ticks li { padding: 4px 0 4px 22px; position: relative; font-size: 13.5px; }
-ul.ticks li::before { content: "✓"; position: absolute; left: 0; color: var(--mid); }
-ul.ticks li.no::before { content: "✗"; }
-
-/* ---------- annotations (reviewer notes, toggleable) ---------- */
-
-.note {
- margin: 14px 0 0;
- padding: 11px 13px;
- border-left: 3px solid var(--note);
- background: var(--note-bg);
- color: #17335f;
- font-size: 13px;
-}
-.note b { color: var(--note); font: 700 10.5px/1.6 var(--mono); letter-spacing: .07em; text-transform: uppercase; display: block; }
-.note a { color: var(--note); }
-.note code { font: 500 12px/1.4 var(--mono); background: #ffffffb3; padding: 1px 4px; }
-body.no-notes .note { display: none; }
-
-.gone {
- margin: 14px 0 0; padding: 11px 13px;
- border: 1px dashed var(--faint); background: repeating-linear-gradient(
- 135deg, transparent 0 8px, #00000008 8px 16px);
- color: var(--mid); font-size: 13px;
-}
-.gone b { display: block; font: 700 10.5px/1.6 var(--mono); letter-spacing: .07em; text-transform: uppercase; }
-body.no-notes .gone { display: none; }
diff --git a/design/marketing/web/wireframes/pricing.md b/design/marketing/web/wireframes/pricing.md
deleted file mode 100644
index 26a85417..00000000
--- a/design/marketing/web/wireframes/pricing.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# Wireframe — the pricing page, and why there isn't one
-
-**Recommendation: delete the pricing page. Keep the URL.**
-
-teemops.com sells nothing. There are no tiers, no managed hosting cards, no service
-offers, no "talk to us". The software is free under Apache-2.0 and stays that way, and
-the company behind it gets exactly one sentence in the footer of every page — specified
-in [homepage.md](homepage.md) §10.
-
-This file exists because `/pricing` is currently in the nav and in the sitemap, and
-"deleted the page" is not a complete answer for a URL that people will still type,
-search for, and link to.
-
----
-
-## What replaces it
-
-```
-teemops.com/pricing → 301 → teemops.com/#free
-```
-
-A permanent redirect to a short **"Free. Forever."** band on the homepage, sitting
-between the proof strip (§3) and the problem section (§4).
-
-```
-┌──────────────────────────────────────────────────────────────────────┐
-│ │
-│ Free. Forever. │
-│ │
-│ Apache-2.0. Every check, unlimited AWS accounts, unlimited users. │
-│ No trial, no seats, no edition above this one, and no feature │
-│ we've held back to sell you later. │
-│ │
-│ [ Read the licence ] │
-└──────────────────────────────────────────────────────────────────────┘
-```
-
-Four lines and a link. That is the whole answer, and giving it a dedicated page would
-make it look like a longer answer than it is.
-
-Why keep the route at all: "is X free" and "X pricing" are the highest-intent sceptical
-queries an open-source project gets, and they are the ones a commercial competitor's
-comparison page will try to own. Landing them on a two-sentence "yes, actually free"
-costs one redirect rule.
-
-**Add this band to the homepage wireframe when this file is signed off.** It is not
-drawn into [homepage.md](homepage.md) yet, because whether it earns above-the-fold-
-adjacent space is the one open judgement call here — see below.
-
----
-
-## The Red Hat framing, and where it lives
-
-Product owner, 2026-08-02:
-
-> We provide open source, forever free software, but we have a company behind this so if
-> a customer needs stability and a different level of support during setup or management
-> of the software, Teem will support.
-
-That is the right model and the wireframes adopt it. The part worth being disciplined
-about is **where each half of it is said**:
-
-| Claim | Said on |
-| --- | --- |
-| The software is free, forever, no held-back features | teemops.com — loudly, repeatedly |
-| A company stands behind it | teemops.com — one footer sentence, once |
-| What that company will do for you, and what it costs | **teem.cloud only** |
-
-Red Hat works because fedoraproject.org does not sell RHEL. The moment teemops.com
-starts describing support tiers, response times or engagement models, every free claim
-on the site reads as a funnel. The footer link is load-bearing precisely *because* it is
-the only one — it is a door, not a pitch.
-
-So: no support page, no enterprise page, no contact-sales form, no "need help?" callout,
-no comparison table with a paid column. If a visitor needs a vendor, one footer link is
-enough — people looking to spend money are good at finding where to spend it.
-
----
-
-## Consequences for the rest of the site
-
-- **Nav loses "Pricing & support"** → `Features · How it works · Docs` + GitHub button.
- Reflected in [homepage.md](homepage.md) §1.
-- **Homepage loses the three-card paid block** from the first draft. Reflected in
- [homepage.md](homepage.md).
-- **`../pages/pricing.md`** (230 lines of Free/Starter/Pro/Business) is deleted, not
- rewritten.
-- **`../html/pricing.html`** (588 lines) is deleted.
-- **`../pricing-strategy.md`** (431 lines) is obsolete on this site. It may still be
- useful input to teem.cloud, so **move it rather than delete it** — it is the only place
- the SMB segment analysis is written down, and it should not be lost to a `git rm`.
-- **`../content-plan.md`** site structure loses `/pricing`; `/contact` should go too, on
- the same logic — a contact form on the project site is a sales channel by another name.
- GitHub issues is the contact channel for a project.
-- **Anything in `../pages/*.md` promising a "free tier"** needs rewording. "Free tier"
- implies a paid tier; the word for what we have is just *free*.
-
----
-
-## The one open judgement call
-
-Does the "Free. Forever." band belong on the homepage at all, or is it redundant?
-
-The hero already says open-source and self-hosted, and §3 already shows the Apache-2.0
-badge. An argument exists that a whole band restating it protests too much.
-
-**My recommendation: keep it, and keep it small.** Every visitor arriving from a
-commercial CSPM is carrying the assumption that there is a catch, and one unambiguous
-paragraph is cheaper than losing them to that suspicion. But it should read as a
-statement of fact, not a sales counter-argument — no comparison table, no "unlike other
-vendors", no exclamation marks.
-
-If it is cut, the `/pricing` redirect should point at the FAQ answer on the features
-page instead of `#free`, so the URL still lands somewhere that answers the question.
-
----
-
-## Deleted from the current pricing page
-
-- Free / Starter ($149) / Pro ($449) / Business tiers — priced a SaaS we are not shipping
-- "20% off annual" — no subscription product to discount
-- Per-tier limits on AWS accounts, scan frequency, check count, history, users — none of
- these limits exist in the software, and inventing them contradicts the licence
-- "Not competing on price with DIY/open source" positioning — **we are the open source**
-- The managed-hosting and support-services cards drafted earlier in this wireframe set —
- those belong on teem.cloud, which is out of scope for `design/marketing/web/`
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 5f9542b0..478b92da 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -100,6 +100,7 @@ Decisions already made, so we don't relitigate them. Each has a trigger for revi
| **D-10** | **User documentation lives in a top-level `user-docs/` directory, not under `docs/`, and deploys to `docs.teemops.com`.** One site for technical and non-technical readers — no separate tracks. | 2026-07-31 | See below. | The content outgrows plain Markdown, or a contributor proposes a better home. |
| **D-12** | **No security score.** Removed rather than recalibrated. Severity counts and the breakdown are what we show; a score can come back later if a design partner asks for one and we know what it should mean. | 2026-08-02 | See below. Closes [#91](https://github.com/teemops/tops/issues/91). | A design partner asks for a single headline number — and then design it so it can *move*. |
| **D-11** | **A finding is a durable record keyed by AWS account + resource + rule, and findings are current state. No per-scan history, no observations table.** The most recent scan that examined a resource is authoritative for its content; status belongs to the user. | 2026-08-01 | See below. Shipped as [#81](https://github.com/teemops/tops/issues/81). | A design partner asks for trends over time — and then treat it as a new data model, not an addition to this one. |
+| **D-14** | **`teemops.com` is a single page whose only job is recruiting design partners, not a product marketing site.** Built fresh in `www/`; the pre-pivot `design/marketing/web/` is deleted. | 2026-08-06 | See below. | Design partners exist and the constraint moves from recruitment to something else. |
| **D-13** | **Docs renderer: MkDocs + Material theme**, deployed to `docs.teemops.com` via Cloudflare Pages (`mkdocs build`, output `site/`). Fills in the choice D-10 deliberately deferred. | 2026-08-03 | Clears the "MkDocs- or Docsify-class" bar D-10 set, now that section count and search actually bite (four pages against an eight-section, 34-page IA). Material gives sidebar nav, on-page TOC and client-side search for free — nothing here needed building by hand. Chosen over Docsify because it renders real static HTML per page rather than client-side, which matters for the evaluator audience D-10's own design work identified as arriving via search or a vendor-review link rather than already inside the app. Adds no new language to the repo: `python3` already backs the link-checker and SVG-validator scripts next to it in `user-docs/README.md`. | Content needs something outside MkDocs' plugin ecosystem, or the Python build step becomes a maintenance burden of its own. |
> **Naming collision, flagged 2026-08-01.** [`durable-findings.md`](./features/durable-findings.md)
@@ -109,6 +110,56 @@ Decisions already made, so we don't relitigate them. Each has a trigger for revi
> renames it. **Worth renaming the story's ID to `DF-1`** — deferred rather than done
> unilaterally, because it touches three docs and a settled wireframe.
+### D-14 in full: what teemops.com is for
+
+**The milestone is limited by recruitment, not engineering** — this document has said so
+since 2026-08-01 — and until 2026-08-06 there was no public page to send anyone to.
+`teemops.com` served nothing.
+
+**What was there was worse than nothing.** `design/marketing/web/` held a complete,
+well-built three-page site for a **hosted commercial SaaS**: Free/Starter/Pro/Business
+pricing tiers, "Start Free" CTAs pointing at `app.teem.nz/register` — a domain that now
+redirects to an unrelated property — and no mention anywhere that TOPS is open source. It
+contradicted D-1 and D-6 on its face. It is deleted rather than kept as reference, because
+a second, contradictory description of the product in the repo is a trap for whoever opens
+it next. Git history has it.
+
+**The reframe that shaped the build.** A brand-new page with no traffic, no backlinks and no
+search history will not generate inbound leads inside the milestone window. The realistic
+sequence is outbound — a post, a message, an email — followed by the recipient looking us
+up. So the page is a **conversion asset for outbound**, not a lead engine. That is why it is
+one page and not eight: what it needs is a clear statement of what TOPS is, proof it is real
+(the install one-liner, the GitHub repo, live docs, a version number), an honest account of
+what it does *not* do, and one specific ask. Use-case pages, compliance landing pages and
+SEO keyword targeting — all specified in the old content plan — are premature at zero
+traffic and were dropped.
+
+**The "what it isn't" section is the load-bearing one.** It says AWS-only, not runtime
+protection, not a compliance guarantee, no scheduled scans, no report export, PCI empty —
+and that nobody outside the project is running it. For an audience of sceptical engineers,
+volunteering the limits is what makes the rest of the page credible. It is also lifted
+directly from `user-docs/start-here/what-tops-is.md`, so the two cannot drift into telling
+different stories.
+
+**Every number on the page is checked against the code**, not against intent: 74 rules
+(52 basic + 22 CIS), 11 services, all 74 carrying a remediation, all 28 critical/high
+carrying step-by-step guidance. `www/README.md` records where each is verified, because
+overstating any of them to a design partner costs more than it buys.
+
+**Lead capture is a Cloudflare Worker writing to D1**, protected by a honeypot field.
+**Turnstile was built, tested and then removed before launch** — at five-partner volume a
+few junk rows are cheaper to skim than a widget is to run, the endpoint sends no email and
+publishes nothing so spam has no amplification path, and a third-party challenge script
+that fails to load makes the form unsubmittable and loses a real lead silently. That last
+risk is the one that decided it. Bot Fight Mode and a rate-limiting rule are the first two
+responses if spam arrives; Turnstile is the third, and `www/README.md` records how.
+
+Deliberately no email notification either: reading leads is one command, and building
+notification before a single lead exists is work with no evidence behind it. Deployment
+reuses D-13's pattern — Workers static assets — so this adds no new category of
+infrastructure, only a second project in the same account. **The result is that deploying
+needs one CLI command and one dashboard step, with no secrets to manage at all.**
+
### D-12 in full: why the security score is gone rather than fixed
The score was `100 − (critical×10 + high×5 + medium×2 + low×1)`, floored at zero.
@@ -324,14 +375,16 @@ reader gets what they need from the top of a page, a technical reader keeps scro
**`docs.teemops.com`, not `.teem.nz`.** `teemops.com` is the domain already wired into the
live app — `MAIL_FROM_ADDRESS` defaults to `help@teemops.com`
(`app/config/mail.php:114`), and `docs/architecture.md:117` names `app.teemops.com`.
-`teem.nz` only appears in `design/marketing/web/` — an unbuilt, pre-pivot marketing mockup
-that still has a paid "Start Free" pricing page, which contradicts D-6. That directory is
-stale and out of scope here; it's noted so the domain choice isn't relitigated by whoever
-next opens it.
+`teem.nz` only appeared in `design/marketing/web/` — an unbuilt, pre-pivot marketing mockup
+that still had a paid "Start Free" pricing page, which contradicts D-6. **That directory was
+deleted on 2026-08-06** when `www/` replaced it; see D-14. The note is kept so the domain
+choice isn't relitigated by whoever next goes looking for it in git history.
**Deployment: Cloudflare Pages**, connected to this repo, publishing `user-docs/`. This is
-not a new category of infrastructure — `design/marketing/web/DEPLOY.md` already proposes
-Cloudflare Pages for the marketing site; this applies the same plan to a second directory.
+not a new category of infrastructure — the deleted marketing mockup's `DEPLOY.md` already
+proposed Cloudflare Pages; this applies the same plan to a second directory. (In the event
+D-13 landed on Cloudflare **Workers** static assets rather than Pages, and `www/` follows
+it.)
**Format: plain Markdown, tooling decided later.** Per the product practices — ship the
smallest working version, don't reach for a framework before there's content to render —
@@ -410,6 +463,7 @@ fixes are what made that run succeed, which is the sequencing argument justifyin
| # | Feature | Why it's here | Size |
| --- | --- | --- | :---: |
+| **N-12** | **`teemops.com` — the design-partner recruitment page** | The milestone has been recruitment-limited since 2026-08-01 and there was nowhere to send anyone: `teemops.com` served nothing, and what was in the repo sold a hosted product with a pricing page. Built 2026-08-06 as one page in `www/`. See **D-14**. **Not yet deployed** — needs `npm run db:init` and the custom domain attached in the dashboard; no secrets. Steps in `www/README.md`. | S |
| ~~**N-11**~~ | ~~Lock down the account-linking SNS topic~~ | ✅ **Done** 2026-08-03 — consumer-side validation ([#100](https://github.com/teemops/tops/issues/100)) and an install-scoped filter secret ([#101](https://github.com/teemops/tops/issues/101)), verified end to end on a real account: a correct install id links as before, a wrong one is filtered to quarantine and never reaches `teemops_main`. The live run also caught `aws:link-rejections` reporting "nothing rejected" while a message sat in quarantine — the silent failure moved one layer out, and is now fixed. [Feature doc](features/sns-topic-publish-authorization.md). | S + M |
| ~~**N-1**~~ | ~~Fix the clean-checkout build~~ | ✅ **Done** 2026-07-29 — `@vitejs/plugin-vue` on `^6`, `npm ci` clean, frontend CI job added. | — |
| ~~**N-2**~~ | ~~Choose and add a licence~~ | ✅ **Done** 2026-07-29 — Apache-2.0, trademark held separately, DCO for contributions. See D-7. | — |
@@ -462,18 +516,14 @@ detail becomes a link into Findings, filtered. The per-finding list comes **off*
#### Open — UI, in dependency order
-**The P0/P1 run is complete.** #81, #82, #83, #85 and #86 have all landed; what remains is P2
-and below. **#88 (F-5, Insights by service) is the cheapest next thing** — S-1 built the
-breakdown as a shared service and component precisely so Insights is a re-key rather than a
-rewrite, and there is already a test proving the organization-wide path works.
+**Nothing is left in this table. The UI workstream is closed** — verified against GitHub on
+2026-08-06: #81, #82, #83, #85, #86, #87, #88, #89 and #91 are all closed. Every row that
+used to sit here is in the Shipped table above.
-| # | Item | Depends on | Size | Priority |
-| --- | --- | --- | :---: | :---: |
-| **#89** | F-6 · Insights by benchmark — **the last item in the milestone**; F-4 unblocked it | ✅ all met | XS | P3 |
-| **#87** | F-4 · Filter Findings by compliance benchmark — the one genuine data-model change left; nothing records a finding's benchmark | #81 | M | P2 |
-| **#88** | F-5 · Insights by service with severity breakdown | #86, #81 | S | P2 |
-| **#91** | B-2 · Security score pinned at 0 and cannot improve | — | XS–S | P2 · bug |
-| **#89** | F-6 · Insights by benchmark | #87, #88 | XS | P3 |
+*The table that was here listed #87, #88, #89 and #91 as open for four days after they
+landed, which is the failure mode this document is most prone to: the Shipped table gets
+updated and the Open table does not. If you close something here, delete its row in the same
+commit.*
**One architectural instruction, worth repeating from the wireframes:** S-1's grouped,
severity-stacked, drillable breakdown must be built as a **reusable component**. F-5 and F-6
@@ -534,11 +584,14 @@ Keep it to the P0/P1 items until a partner is actually watching.
| # | Feature | Why it's here | Size |
| --- | --- | --- | :---: |
-| **X-1** | New-device email OTP | Wanted soon. Code prompt only on an unrecognised browser. Generator already exists — mostly extraction. | M |
+| **X-10** | Trim the child IAM role to what TOPS actually uses | Fell out of N-11 and was never written down here. [#111](https://github.com/teemops/tops/issues/111) | S |
+| **X-11** | `teemops_main` queue policy grants `SQS:ReceiveMessage` to `Principal: "*"` | Dead permission — the `aws:SourceArn` condition can never match a direct call — but it is a `Principal: "*"` in a public repo. Also from N-11. [#109](https://github.com/teemops/tops/issues/109) | XS |
+| **X-1** | New-device email OTP | **Parked 2026-08-06, not dropped.** No design partner has asked, and the milestone is recruitment-limited; building the largest open item ahead of evidence is the anti-pattern this document names. Two findings from the discovery are worth keeping: the generator is welded to a Firebase ID token and keyed on `firebase_uid`, so it is a rewrite rather than the extraction this row claimed; and the default install cannot send email to a real inbox at all (see below). | M |
| **X-3** | Prove the self-hosted path in CI | Nothing asserts the app boots with `FIREBASE_USER_AUTH=false` — the default config. | S |
| **X-4** | Delete or route the dead OAuth controller | 99 lines, zero routes, unused dependency. Could give native-auth users OAuth without Firebase. | S |
| **X-6** | Enforce DCO sign-off in CI | Sign-off is required in writing but unchecked. D-7's guarantee depends on provenance. **The repo is public, so an external PR can now arrive at any time.** | XS |
| **X-5** | Reconcile member permissions | Code, comments and the plan doc disagree on who can manage members. | XS |
+| **X-12** | A self-hoster cannot point TOPS at their own SMTP | `docker-compose.yml` hardcodes `MAIL_*` in `environment:`, which overrides `env_file:`. Verification and invitation email goes to a catcher on an unauthenticated port. Affects shipped features, not just X-1. | XS |
| **X-8** | Publish a SHA256 for `install.sh` | N-3 documents the download-and-read form but cannot document a checksum, because the release workflow does not emit one. Small addition to `release.yml`. | XS |
| **X-7** | Clear the remaining npm audit backlog | 17 → **5**, criticals at 0 and gated in CI. What's left needs a `firebase` major bump; nothing reaches a running instance. | XS |
@@ -591,10 +644,10 @@ as a first slice plus continuous growth rather than a checklist.
AWS account, run your first scan, what the IAM role can do, reading a finding,
resolving a finding, the security model, reporting a vulnerability. Scan profiles
and member management are priority-2, still ahead, per the IA's writing order
-- [ ] Cloudflare Pages is connected and `docs.teemops.com` resolves to it — **D-13** picked
- the renderer (MkDocs + Material) and the repo now builds; the Pages project itself
- still needs connecting in the Cloudflare dashboard, which isn't something a repo
- commit can do
+- [x] `docs.teemops.com` resolves and serves the built site — confirmed live 2026-08-06.
+ **D-13** picked the renderer (MkDocs + Material); it deploys as Cloudflare **Workers**
+ static assets (`wrangler.jsonc` at the repo root) rather than Pages as originally
+ written
- [ ] `README.md`'s setup section trims to a summary linking into `user-docs/`, so
installation instructions have exactly one canonical copy
@@ -660,10 +713,10 @@ Full research, the four options considered and why three were rejected, the open
the proposed design, and acceptance criteria for both phases:
**[docs/features/sns-topic-publish-authorization.md](features/sns-topic-publish-authorization.md)**.
-- [ ] Phase 1 — consumer-side validation ships with tests — [#100](https://github.com/teemops/tops/issues/100)
-- [ ] Phase 2 — install-scoped filter secret, verified end to end against a real AWS account — [#101](https://github.com/teemops/tops/issues/101)
-- [ ] Payload filtering confirmed to work against a real CloudFormation message before it is relied on
-- [ ] The architecture diagram's "unlimited child accounts" claim still holds after the change
+- [x] Phase 1 — consumer-side validation ships with tests — [#100](https://github.com/teemops/tops/issues/100)
+- [x] Phase 2 — install-scoped filter secret, verified end to end against a real AWS account — [#101](https://github.com/teemops/tops/issues/101) — **shipped and verified; the issue is still open on GitHub and wants closing**
+- [x] Payload filtering confirmed to work against a real CloudFormation message before it is relied on — 2026-08-03
+- [x] The architecture diagram's "unlimited child accounts" claim still holds after the change
---
@@ -1299,9 +1352,34 @@ the code worked fine — so only a test that reads what actually ships would hav
Queued behind the design-partner milestone. Not started, not forgotten.
-### X-1 · New-device email OTP
+### X-1 · New-device email OTP — **parked 2026-08-06**
-**User story**
+**Why it is parked.** Picked up on 2026-08-06, taken through Discovery, and stopped before a
+user story was written. Nobody has asked for it, the milestone is limited by recruiting
+design partners rather than by shipping features, and it is the largest open item on the
+board. The counter-argument — the repo is public, this is a security product, and
+password-only access to a map of someone's AWS weaknesses is a poor look — is real, and is
+why this is parked rather than moved to *Not Doing*. **D-5 still stands**: when it is built,
+it is email OTP, not TOTP.
+
+**Two findings from the discovery, both worth keeping:**
+
+1. **This is a rewrite, not the extraction the summary below claims.**
+ `FirebaseAuthController::requestEmailOtp()` identifies the user by verifying a Firebase
+ **ID token** and caches under `mfa_email_otp:{firebase_uid}` — a column that is null for
+ every native-auth user. Both the input and the cache key have to be replaced. What is
+ genuinely reusable is about six lines plus `maskEmail()`; the valuable inheritance is the
+ *shape* (cache-backed, 6-digit, 10-minute expiry), not the code. It also uses `Mail::raw`
+ where the repo's pattern is a queued `Notification`.
+
+2. **The default install cannot send email to a real inbox, and `.env` cannot fix it.** See
+ **X-12** below. This is a prerequisite, and shipping OTP without it would deliver the
+ second factor to an unauthenticated mail catcher on the same host — the appearance of MFA
+ without the substance, which is worse than not shipping it.
+
+---
+
+**User story** *(retained as written; not yet agreed)*
> As a self-hosting user, I want to be asked for an emailed code only when I sign in from
> a browser I haven't used before, so that a stolen password alone isn't enough to reach
> my account — without adding friction to everyday logins.
@@ -1330,6 +1408,32 @@ Queued behind the design-partner milestone. Not started, not forgotten.
---
+### X-12 · A self-hoster cannot point TOPS at their own SMTP
+
+*Bug. Found 2026-08-06 during X-1's discovery, but it is not an MFA problem — it affects
+mail that ships today.*
+
+`docker-compose.yml` sets `MAIL_MAILER`, `MAIL_HOST` and `MAIL_PORT` in the `environment:`
+block, hardcoded to the bundled `maildev` catcher. The comment directly above them, added
+for the queue settings, explains exactly why this is a bug: **`environment:` overrides
+`env_file:`**, so a value in the operator's `.env` is ignored. Mail got the treatment the
+queue settings were deliberately spared.
+
+The consequences exist now, without MFA:
+
+- **Email verification** (`MustVerifyEmail` is live) and **organisation invitations** both
+ send to a catcher, so an invited colleague never receives anything. Today the operator
+ has to know to open maildev's web UI on port `8090`.
+- That UI is published on **all interfaces with no authentication** — fine for a local mail
+ catcher, not fine as the place account email lands.
+- Changing it means editing `docker-compose.yml`, a file `install.sh` owns and an upgrade
+ may replace.
+
+**The fix is XS**: move the three `MAIL_*` keys out of `environment:` so `.env` layering
+wins, keep `maildev` as the default for anyone who has configured nothing, and document the
+real-SMTP variables in `.env.example`. **Blocks X-1**, and should be done regardless of
+whether X-1 is ever built.
+
### X-3 · Prove the self-hosted path in CI
No test asserts the app boots and authenticates with `FIREBASE_USER_AUTH=false` — which
@@ -1450,6 +1554,25 @@ Blocking nothing today, but each one shapes the plan:
## Changelog
+- **2026-08-06** — **The plan turned from shipping to recruiting, and this document caught
+ up with itself.** X-1 (email OTP) was picked up, taken through Discovery and **parked**
+ before a user story existed: nobody has asked for it, and it is the largest open item on a
+ board whose stated constraint is finding five operators. Its discovery is kept, because it
+ found two things worth more than the feature would have been — the "generator already
+ exists, mostly extraction" claim is wrong (it is keyed on a Firebase ID token and
+ `firebase_uid`, so it is a rewrite), and **the default install cannot send email to a real
+ inbox at all**, now filed as **X-12**. That second one is a live bug affecting email
+ verification and organisation invitations today, not a hypothetical MFA prerequisite.
+ In its place, **N-12**: `teemops.com` built as a single design-partner recruitment page
+ (**D-14**), with the pre-pivot `design/marketing/web/` mockup deleted — it sold a hosted
+ product with Free/Starter/Pro/Business pricing and CTAs pointing at a domain that now
+ redirects elsewhere, contradicting D-1 and D-6 on its face. **A review of this document
+ against GitHub also found it stale in four places**, all now corrected: N-11's checkboxes
+ were unticked though it shipped on 2026-08-03; the workstream's "Open — UI" table still
+ listed #87, #88, #89 and #91 four days after they closed; `docs.teemops.com` was recorded
+ as not yet connected when it has been live and serving; and [#109](https://github.com/teemops/tops/issues/109)
+ and [#111](https://github.com/teemops/tops/issues/111), both filed on 2026-08-03 out of
+ N-11, appeared nowhere at all — now **X-11** and **X-10**.
- **2026-08-02** — **S-1 landed, and the P0/P1 run of this workstream is complete.** Scan
detail summarises and dispatches: run facts, severity totals, "fix these first" ranked by
severity weight rather than raw count, and a drillable breakdown whose every row links into
diff --git a/www/.gitignore b/www/.gitignore
new file mode 100644
index 00000000..5620f5ea
--- /dev/null
+++ b/www/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+.wrangler/
+
+# Generated by `wrangler types` (npm run typecheck regenerates it).
+# 14k lines of derived declarations do not belong in review diffs.
+worker-configuration.d.ts
diff --git a/www/README.md b/www/README.md
new file mode 100644
index 00000000..f8c9d25a
--- /dev/null
+++ b/www/README.md
@@ -0,0 +1,164 @@
+# teemops.com
+
+The marketing site and its design-partner signup form. One page, deployed to Cloudflare
+Workers as static assets plus a single API route.
+
+**This is a separate Workers project from the docs site.** The repo root's
+`wrangler.jsonc` is `docs.teemops.com` (built by `mkdocs.yml`); this directory's is
+`teemops.com`. Run every command below from inside `www/`, or you will deploy the wrong
+one.
+
+## What's here
+
+```
+www/
+├── wrangler.jsonc # Worker config — assets, D1 binding, observability
+├── schema.sql # the design_partner_leads table
+├── vitest.config.ts # runs the tests inside workerd, against a real local D1
+├── build-assets.py # regenerates public/og.png and public/favicon.ico
+├── src/
+│ └── index.ts # POST /api/design-partner — the only server-side code
+├── test/
+│ └── signup.test.ts # 16 tests over that endpoint
+└── public/ # everything else is static
+ ├── index.html # the page, with its CSS and JS inline
+ ├── 404.html
+ ├── og.png # generated — do not edit by hand
+ ├── favicon.ico # generated — do not edit by hand
+ ├── logo.png # the source both are derived from
+ ├── robots.txt
+ └── sitemap.xml
+```
+
+## First-time setup
+
+Two CLI commands and one dashboard step. There are no secrets to set.
+
+### 1. Create the leads table
+
+The D1 database `tops-www-leads` already exists. Create its schema in the remote copy:
+
+```bash
+npm run db:init
+```
+
+### 2. Deploy
+
+```bash
+npm run deploy
+```
+
+### 3. Point teemops.com at it
+
+In **Workers & Pages → tops-www → Settings → Domains & Routes**, add `teemops.com` and
+`www.teemops.com` as custom domains. This is dashboard-only — a repo commit cannot do it.
+
+## Reading the leads
+
+```bash
+npm run leads
+```
+
+That prints every signup, newest first. For the message someone left as well:
+
+```bash
+npx wrangler d1 execute tops-www-leads --remote \
+ --command="SELECT created_at, name, email, company, aws_scale, notes FROM design_partner_leads ORDER BY created_at DESC LIMIT 20"
+```
+
+**Nothing emails you when a lead arrives.** You have to run this. That is a deliberate
+starting point rather than an oversight — it is one command, and adding notification
+before there is a single lead to notify about is work with no evidence behind it. When
+checking manually gets annoying, that is the signal to add it, and the account already has
+`email_sending` available for exactly that.
+
+## Spam protection, and what to do if it stops working
+
+The form is protected by a **honeypot**: a hidden `website` field that people never see and
+bots tend to fill. If it arrives non-empty the submission is dropped and the endpoint
+answers `201` anyway, so a bot gets no signal about which field gave it away. The name is
+defined once in `src/index.ts` as `HONEYPOT_FIELD` and must match the input in
+`public/index.html`.
+
+**There is deliberately no CAPTCHA.** Turnstile was built and then removed before launch,
+for three reasons: at five-design-partner volume a handful of junk rows is easier to skim
+past than a widget is to maintain; the endpoint sends no email and publishes nothing, so
+spam has no amplification path; and a third-party challenge script that fails to load —
+corporate network, privacy extension — makes the form unsubmittable and loses a real lead
+with no signal that it happened.
+
+**If you start getting spam**, in increasing order of effort:
+
+1. Turn on **Bot Fight Mode** for the zone. No code, no deploy.
+2. Add a **rate-limiting rule** on `/api/design-partner`. Still no code.
+3. Add [Turnstile](https://developers.cloudflare.com/turnstile/) — a widget in the
+ dashboard, its site key in the page, `wrangler secret put TURNSTILE_SECRET_KEY`, and a
+ `siteverify` call in `handleSignup`. Note the stored OAuth token lacks
+ `challenge-widgets.write`, so creating the widget from the CLI needs `wrangler login`
+ first. If you do this, never ship Cloudflare's test key (`1x00000000000000000000AA`) —
+ it always passes, so the form would look protected and be wide open.
+
+Reach for 3 only if 1 and 2 have not held.
+
+## Tests
+
+```bash
+npm test
+```
+
+16 tests covering `POST /api/design-partner`, run inside `workerd` against a real local D1
+via `@cloudflare/vitest-pool-workers` — so the SQL is genuinely executed rather than
+mocked. They cover the happy path, field trimming and length caps, every validation
+rejection, the honeypot (including that a trapped request is byte-for-byte
+indistinguishable from a genuine one), and the method and path guards.
+
+Each assertion checks the database as well as the status code, because the failure that
+matters is a submission that answers `201` and stores nothing.
+
+Note `@cloudflare/vitest-pool-workers` 0.20 removed the `/config` subpath and the
+`defineWorkersConfig` helper that most examples online still use. On vitest 4 the pool
+options go to a `cloudflareTest()` **plugin** — see the comment in `vitest.config.ts`.
+
+## Local development
+
+```bash
+npm install
+npm run db:init:local
+npm run dev
+```
+
+Then open the printed URL. There are no secrets and no `.dev.vars` to set up.
+
+Inspect what local submissions wrote:
+
+```bash
+npx wrangler d1 execute tops-www-leads --local --command="SELECT * FROM design_partner_leads"
+```
+
+## Changing the page
+
+The CSS and JS are inline in `public/index.html` on purpose — the page is one file, has no
+build step, and loads no third-party assets at all. Keep it that way
+unless there is a reason not to.
+
+After changing the headline, regenerate the derived assets so the link-preview card and
+the page agree:
+
+```bash
+python3 build-assets.py
+```
+
+## Claims made on this page
+
+The page states specific numbers. They were true at v0.5.0 and they are checkable — if you
+change the rules, check them again:
+
+| Claim | Source |
+| --- | --- |
+| 74 checks | `app/rules/rulesets/basic.json` (52) + `cis.json` (22) |
+| 11 AWS services | directories under `app/rules/tasks/` |
+| 28 with step-by-step guidance | `app/rules/recommendations/tips.json` |
+| Install needs only Docker | `install.sh`, and D-8 in `docs/roadmap.md` |
+
+Overstating any of these to a design partner costs more than it buys — they will install it
+and find out.
diff --git a/www/build-assets.py b/www/build-assets.py
new file mode 100644
index 00000000..691f0d63
--- /dev/null
+++ b/www/build-assets.py
@@ -0,0 +1,121 @@
+#!/usr/bin/env python3
+"""Generate the two derived brand assets in public/.
+
+- og.png the 1200x630 card shown when teemops.com is shared. Link previews
+ are the first thing most visitors see, because the site is reached
+ from a message someone sent them rather than from search.
+- favicon.ico browsers request /favicon.ico whether or not a