diff --git a/components/reviews-section.tsx b/components/reviews-section.tsx
index a0a5f47a..6b8b410f 100644
--- a/components/reviews-section.tsx
+++ b/components/reviews-section.tsx
@@ -36,7 +36,7 @@ export function ReviewsSection() {
align="center"
eyebrow="Google Reviews"
title="What Our Customers Say"
- description="See what Utah businesses have to say about Rocky Mountain Vending, all in the same clean review shell used across the rest of the site."
+ description="See what Utah businesses have to say about Rocky Mountain Vending and the service they count on every day."
>
diff --git a/components/services-section.tsx b/components/services-section.tsx
index dd124c43..4a503d30 100644
--- a/components/services-section.tsx
+++ b/components/services-section.tsx
@@ -55,7 +55,7 @@ export function ServicesSection() {
Our services and products
- From free placement to manuals and repairs, these sections now share the same visual rhythm as the rest of the public site.
+ From free placement to manuals and repairs, we make it easy to get the machines, service, and support your location needs.
diff --git a/components/vending-machines-page.tsx b/components/vending-machines-page.tsx
index 999453d3..c2c4c82e 100644
--- a/components/vending-machines-page.tsx
+++ b/components/vending-machines-page.tsx
@@ -13,8 +13,8 @@ export function VendingMachinesPage() {
diff --git a/docs/public-copy-guardrails.md b/docs/public-copy-guardrails.md
new file mode 100644
index 00000000..7ca07411
--- /dev/null
+++ b/docs/public-copy-guardrails.md
@@ -0,0 +1,61 @@
+# Rocky Public Copy Guardrails
+
+This guide applies to customer-facing copy in `app/` and `components/`.
+
+## Voice
+
+- Sound like a strong local vending company.
+- Lead with customer outcomes: service, speed, support, placement, reliability.
+- Keep it plainspoken, confident, and easy to scan.
+- Write like Rocky Mountain Vending is talking to a business owner, property manager, school, or office admin.
+
+## Do Say
+
+- What we do
+- Who it is for
+- What happens next
+- How quickly we respond
+- Why businesses trust Rocky
+
+Examples:
+
+- `Tell us what you need and our team will follow up quickly.`
+- `Need service, moving help, or machine sales support? We’re here to help.`
+- `Share your location details and we’ll recommend the right machine mix.`
+
+## Never Say
+
+Do not let customer-facing copy explain the website, UI, or refactor work.
+
+Avoid phrases like:
+
+- `rest of the site`
+- `design system`
+- `presentation`
+- `shell`
+- `dedicated page`
+- `intake`
+- `handoff`
+- `path`
+- `popup`
+- `embed`
+- `this page now`
+- `consistent service experience`
+- `best next step`
+- `fuller request`
+- `fuller intake`
+
+## Before / After
+
+- Bad: `We built this page to feel like the rest of the site: clear, calm, and easy to scan.`
+- Good: `See why Utah businesses trust Rocky Mountain Vending for dependable service and support.`
+
+- Bad: `Open the Rocky placement popup and we’ll follow up within one business day.`
+- Good: `Tell us about your location and we’ll follow up within one business day.`
+
+- Bad: `This page now matches the rest of the site instead of feeling like a separate design system.`
+- Good: `Browse the machines we use, the features we prioritize, and the support that comes with every installation.`
+
+## Review Rule
+
+If a sentence explains how the site is organized instead of why a customer should care, rewrite it.
diff --git a/package.json b/package.json
index c4c5684c..acf61130 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"build": "next build",
+ "copy:check": "node scripts/check-public-copy.mjs",
"deploy:staging:env": "node scripts/deploy-readiness.mjs",
"deploy:staging:preflight": "node scripts/deploy-readiness.mjs --build",
"deploy:staging:smoke": "node scripts/staging-smoke.mjs",
diff --git a/scripts/check-public-copy.mjs b/scripts/check-public-copy.mjs
new file mode 100644
index 00000000..409928e2
--- /dev/null
+++ b/scripts/check-public-copy.mjs
@@ -0,0 +1,98 @@
+import { readFileSync, readdirSync, statSync } from "node:fs"
+import path from "node:path"
+import process from "node:process"
+
+const ROOT = process.cwd()
+const SEARCH_DIRS = ["app", "components"]
+const INCLUDE_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".mdx"])
+const IGNORE_PATTERNS = [
+ ".backup",
+ `${path.sep}style-guide${path.sep}`,
+ `${path.sep}ui${path.sep}`,
+ `${path.sep}api${path.sep}`,
+ `${path.sep}manuals${path.sep}README.md`,
+ `${path.sep}components${path.sep}privacy-policy-page.tsx`,
+ `${path.sep}components${path.sep}terms-and-conditions-page.tsx`,
+ `${path.sep}app${path.sep}seaga-hy900-support${path.sep}`,
+]
+
+const BANNED_PATTERNS = [
+ { label: "rest of the site", regex: /\brest of the site\b/i },
+ { label: "design system", regex: /\bdesign system\b/i },
+ { label: "presentation", regex: /\bpresentation\b/i },
+ { label: "same clean", regex: /\bsame clean\b/i },
+ { label: "same Rocky shell", regex: /\bsame rocky shell\b/i },
+ { label: "dedicated page", regex: /\bdedicated page\b/i },
+ { label: "intake", regex: /\bintake\b/i },
+ { label: "handoff", regex: /\bhandoff\b/i },
+ { label: "popup", regex: /\bpopup\b/i },
+ { label: "embed", regex: /\bembed(?:ded)?\b/i },
+ { label: "this page now", regex: /\bthis page now\b/i },
+ { label: "consistent service experience", regex: /\bconsistent service experience\b/i },
+ { label: "best next step", regex: /\bbest next step\b/i },
+ { label: "fuller request", regex: /\bfuller request\b/i },
+ { label: "fuller intake", regex: /\bfuller intake\b/i },
+]
+
+function shouldIgnore(filePath) {
+ return IGNORE_PATTERNS.some((pattern) => filePath.includes(pattern))
+}
+
+function walk(dir) {
+ const entries = readdirSync(dir, { withFileTypes: true })
+ const results = []
+ for (const entry of entries) {
+ const fullPath = path.join(dir, entry.name)
+ if (shouldIgnore(fullPath)) continue
+ if (entry.isDirectory()) {
+ results.push(...walk(fullPath))
+ continue
+ }
+ if (!entry.isFile()) continue
+ if (INCLUDE_EXTENSIONS.has(path.extname(entry.name))) {
+ results.push(fullPath)
+ }
+ }
+ return results
+}
+
+function findLineNumber(content, index) {
+ return content.slice(0, index).split("\n").length
+}
+
+function main() {
+ const files = SEARCH_DIRS.flatMap((dir) => {
+ const fullDir = path.join(ROOT, dir)
+ if (!statSync(fullDir, { throwIfNoEntry: false })) return []
+ return walk(fullDir)
+ })
+
+ const findings = []
+
+ for (const filePath of files) {
+ const content = readFileSync(filePath, "utf8")
+ for (const rule of BANNED_PATTERNS) {
+ const match = content.match(rule.regex)
+ if (!match || match.index == null) continue
+ findings.push({
+ filePath,
+ line: findLineNumber(content, match.index),
+ label: rule.label,
+ preview: match[0],
+ })
+ }
+ }
+
+ if (findings.length === 0) {
+ console.log("Public copy check passed.")
+ process.exit(0)
+ }
+
+ console.error("Public copy guardrail failed. Rewrite customer-facing copy that talks about the site or UI mechanics:")
+ for (const finding of findings) {
+ console.error(`- ${path.relative(ROOT, finding.filePath)}:${finding.line} [${finding.label}] ${finding.preview}`)
+ }
+ process.exit(1)
+}
+
+main()
diff --git a/scripts/deploy-readiness.mjs b/scripts/deploy-readiness.mjs
index bc0785b5..f8300ce3 100644
--- a/scripts/deploy-readiness.mjs
+++ b/scripts/deploy-readiness.mjs
@@ -223,6 +223,13 @@ function main() {
heading("Build")
+ const copyCheckResult = runShell("pnpm copy:check", { inherit: true })
+ if (copyCheckResult.status !== 0) {
+ failures.push("pnpm copy:check failed.")
+ } else {
+ console.log("pnpm copy:check: ok")
+ }
+
if (args.build) {
const buildResult = runShell("pnpm build", { inherit: true })
if (buildResult.status !== 0) {