diff --git a/docs/field-velocity-visibility.md b/docs/field-velocity-visibility.md
new file mode 100644
index 00000000..e6458fda
--- /dev/null
+++ b/docs/field-velocity-visibility.md
@@ -0,0 +1,51 @@
+# Field-velocity launch boundary
+
+Two deliberately separate changes:
+
+1. **Hide now:** remove the `AreaFieldVelocity` import and mount from both public focus-area page templates (the generic template and the hardcoded Economies & Governance route). No CSS-only hiding, public preview-discovery links, data loading, or chart-fragment dialogs remain on those pages. The reusable component stays staged in source for restoration.
+2. **Restore later:** a separate draft PR reverses only that public-detail wiring and restores the original positive mounting tests. It does not publish `/impact/`, change navigation, or promote the cross-field preview. Merge the hiding PR first; the restore PR is stacked on its branch and must target `main` after the hiding PR merges. Do not merge the restore PR into the hiding branch.
+
+The cross-field page remains at `/impact-preview-eb61fba1b98e/`, with all four areas, chart deep links, source faces, inflections, methodology and toolkit. Its `noindex,nofollow` metadata and exclusions from navigation, sitemap, search, RSS and robots discovery remain unchanged. The read-only field-velocity API/data are preserved for consumers. Unlisted/noindex is not authentication or access control.
+
+This is distinct from the older draft PR #105, which would promote `/impact/` into navigation. That PR is not changed by either detail-page visibility PR.
+
+## Verification (September 10, 2026)
+
+Application revisions exercised:
+- Hidden: `7e696f9b64aba340c4aa15b374e1c1c23d320ed5` — 79 tests passing.
+- Restored: `e426c490736f055794428e39733b160c31de295b` — 75 tests passing.
+- The restored `src` tree is exactly the pre-hide main/PR153 tree at `cea75883bbed07cb751a3bc17c5b677cfa587b74` (tree `61229e854adeb104781b01b0747df0027898d1c4`). No other source, styling, data, API or site changes.
+
+Both passed pnpm 10 frozen installs, TypeScript and production builds, plus independent isolated code reviews. Test-first regressions failed on all four public routes before hiding; the original mounting tests failed on all four hidden routes before restoring.
+
+Headed local production-build browser checks at 1440, 390 and 320px:
+- All four public detail routes preserve Opportunity Spaces and their existing Insights/Explore content.
+- Hidden state has zero field-velocity panels, tiles, preview-discovery links or old `#fv/...` chart dialogs.
+- Restored state has the field panel, all five tiles, three overview/methodology/toolkit links and working old chart fragments.
+- All four selected cross-field overview tabs retain five tiles and methodology. A real chart URL per field opens its modal; the data/source flip and Escape dismissal work.
+- 24 route/viewport cases per state, 48 across both. Screenshots below are local captures of these real source revisions, not claims about production deployment.
+
+36 HTTP checks across both local production builds cover home, area index, all four detail routes, the exact preview, sitemap, robots, search, RSS, each field API, malformed API area, and missing `/impact/` and `/field-velocity/` routes.
+
+## Existing limitations, not changed by these PRs
+
+- Both states produce exactly the same measured horizontal overflow: 8px on the unchanged cross-field preview at 1440/390px; 15px at a 320px viewport with a native scrollbar (the site's existing 320px body minimum). Public detail pages have no horizontal overflow at 1440/390px. These visibility changes do not attempt a preview layout redesign.
+- Builds report existing provider fallback/cache warnings (including Ma Earth snapshot fallback and indexer GraphQL errors). No new provider integration or data freshness is claimed.
+- The production URL was inspected through HTTP extraction. The authenticated Chrome policy blocked direct `www.plrd.org` navigation; visual evidence uses local production builds, with hosted PR deployment verification reported separately in the PR descriptions.
+
+## Screenshots
+
+### Hidden public detail: desktop
+
+
+### Hidden public detail: mobile
+
+
+### Exact pre-hide detail behavior restored: desktop
+
+
+### Exact pre-hide detail behavior restored: mobile
+
+
+### Cross-field preview retained
+
diff --git a/docs/screenshots/field-velocity-visibility/cross-field-retained-desktop.png b/docs/screenshots/field-velocity-visibility/cross-field-retained-desktop.png
new file mode 100644
index 00000000..4563291d
Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/cross-field-retained-desktop.png differ
diff --git a/docs/screenshots/field-velocity-visibility/detail-hidden-desktop.png b/docs/screenshots/field-velocity-visibility/detail-hidden-desktop.png
new file mode 100644
index 00000000..7164944d
Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/detail-hidden-desktop.png differ
diff --git a/docs/screenshots/field-velocity-visibility/detail-hidden-mobile.png b/docs/screenshots/field-velocity-visibility/detail-hidden-mobile.png
new file mode 100644
index 00000000..3940dc84
Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/detail-hidden-mobile.png differ
diff --git a/docs/screenshots/field-velocity-visibility/detail-restored-desktop.png b/docs/screenshots/field-velocity-visibility/detail-restored-desktop.png
new file mode 100644
index 00000000..9e1c8a0c
Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/detail-restored-desktop.png differ
diff --git a/docs/screenshots/field-velocity-visibility/detail-restored-mobile.png b/docs/screenshots/field-velocity-visibility/detail-restored-mobile.png
new file mode 100644
index 00000000..8fbdc7a8
Binary files /dev/null and b/docs/screenshots/field-velocity-visibility/detail-restored-mobile.png differ
diff --git a/scripts/velocity/field-velocity-panel.test.mjs b/scripts/velocity/field-velocity-panel.test.mjs
index 62fc8732..7909f921 100644
--- a/scripts/velocity/field-velocity-panel.test.mjs
+++ b/scripts/velocity/field-velocity-panel.test.mjs
@@ -45,19 +45,14 @@ function elements(node) {
}
for (const { key, label } of FOCUS_AREAS) {
- test(`${key} overview includes the shared panel, original strategy and selected-area methodology/toolkit links`, async (t) => {
+ test(`${key} staged panel retains charts and selected-area methodology/toolkit links without public route wiring`, async (t) => {
assert.ok(existsSync('src/components/AreaFieldVelocity.tsx'), 'missing reusable area panel')
t.mock.method(globalThis, 'fetch', async () => new Response(null, { status: 503 }))
t.mock.method(console, 'warn', () => {})
const Panel = source('components/AreaFieldVelocity.tsx').default
- const Page = source(key === 'economies-governance' ? 'app/areas/economies-governance/page.tsx' : 'app/areas/[slug]/page.tsx').default
- const tree = await Page({ params: Promise.resolve({ slug: key }) })
- const children = elements(tree)
- const panel = children.find(el => el.type === Panel)
- assert.ok(panel, `${key} page does not mount shared panel`)
- assert.equal(panel.props.area, key)
- assert.ok(children.some(el => el.props.id === 'opportunity-spaces'), 'existing strategy preserved')
- const panelTree = await Panel(panel.props)
+ // The component is retained for a later launch, but no public page mounts it.
+ // Public-route absence is covered separately by preview-only.test.mjs.
+ const panelTree = await Panel({ area: key })
if (key === 'economies-governance') {
assert.deepEqual(elements(panelTree).find(el => el.type === Dashboard).props.liveOutputs, {}, 'area panel must retain the overview live-output channel even when providers are unavailable')
}
@@ -69,7 +64,7 @@ for (const { key, label } of FOCUS_AREAS) {
assert.match(html, /id="field-velocity"/)
assert.ok(html.includes(`${label} field velocity`))
- assert.ok(tree.props.className.includes('area-overview'))
+
assert.ok(panelTree.props.className.includes('area-field-velocity'))
assert.ok(panelTree.props.className.includes(key === 'neurotech' ? 'bg-gray-100' : 'bg-gray-200'))
assert.doesNotMatch(panelTree.props.className, /border-y|px-4|bg-gray-50/)
diff --git a/scripts/velocity/preview-only.test.mjs b/scripts/velocity/preview-only.test.mjs
new file mode 100644
index 00000000..731591cc
--- /dev/null
+++ b/scripts/velocity/preview-only.test.mjs
@@ -0,0 +1,64 @@
+import { test } from 'node:test'
+import assert from 'node:assert/strict'
+import React from 'react'
+import { readFileSync } from 'node:fs'
+import { source } from './test-source-loader.mjs'
+
+const Panel = source('components/AreaFieldVelocity.tsx').default
+const Dashboard = source('components/ImpactDashboardV2.tsx').default
+const Methodology = source('components/MeasuringQuestionsV2.tsx').default
+const { FOCUS_AREAS, FIELD_VELOCITY_OVERVIEW } = source('lib/field-velocity.ts')
+
+function elements(node) {
+ if (!React.isValidElement(node)) return []
+ return [node, ...React.Children.toArray(node.props.children).flatMap(elements)]
+}
+
+function offlineProviders(t) {
+ t.mock.method(globalThis, 'fetch', async () => new Response(null, { status: 503 }))
+ t.mock.method(console, 'warn', () => {})
+}
+
+for (const { key } of FOCUS_AREAS) {
+ test(`${key} public detail keeps existing content but does not mount field velocity or preview links`, async (t) => {
+ offlineProviders(t)
+ const route = key === 'economies-governance' ? 'app/areas/economies-governance/page.tsx' : 'app/areas/[slug]/page.tsx'
+ const Page = source(route).default
+ const nodes = elements(await Page({ params: Promise.resolve({ slug: key }) }))
+ assert.ok(!nodes.some(node => node.type === Panel || node.type === Dashboard || node.type === Methodology), 'unreleased field velocity must not mount on public pages')
+ assert.ok(!nodes.some(node => node.props.id === 'field-velocity'))
+ assert.ok(!nodes.some(node => /impact-preview|#fv\/|#field-velocity|#methodology|#toolkit/.test(node.props.href ?? '')), 'no discovery links to the unlisted preview')
+ assert.ok(nodes.some(node => node.props.id === 'opportunity-spaces'), 'original strategy remains')
+ const existingLink = key === 'economies-governance' ? '/areas/economies-governance/projects/' : '/insights/'
+ assert.ok(nodes.some(node => node.props.href === existingLink), 'original Explore/Insights content remains')
+ assert.doesNotMatch(readFileSync(`src/${route}`, 'utf8'), /from ['"]@\/(?:components\/(?:AreaFieldVelocity|ImpactDashboardV2|MeasuringQuestionsV2)|lib\/field-velocity[^'"]*)['"]/, 'public routes must not load field-velocity modules')
+ })
+}
+
+test('the exact unlisted overview retains all fields, charts, methodology and noindex metadata', async (t) => {
+ offlineProviders(t)
+ assert.equal(FIELD_VELOCITY_OVERVIEW, '/impact-preview-eb61fba1b98e/')
+ const { default: Overview, metadata } = source(`app${FIELD_VELOCITY_OVERVIEW}page.tsx`)
+ assert.deepEqual(metadata.robots, { index: false, follow: false, googleBot: { index: false, follow: false } })
+ for (const { key } of FOCUS_AREAS) {
+ const nodes = elements(await Overview({ searchParams: Promise.resolve({ area: key }) }))
+ const dashboard = nodes.find(node => node.type === Dashboard)
+ assert.ok(dashboard)
+ assert.equal(dashboard.props.initialArea, key)
+ assert.equal(dashboard.props.fixedArea, undefined)
+ assert.deepEqual(Object.keys(dashboard.props.recordsByArea).sort(), FOCUS_AREAS.map(area => area.key).sort())
+ assert.ok(nodes.some(node => node.type === Methodology))
+ assert.ok(nodes.some(node => node.props.id === 'methodology'))
+ }
+})
+
+test('the preview stays absent from public navigation, sitemap, search, RSS and robots discovery', () => {
+ const sitemap = source('app/sitemap.ts').default()
+ const { mainNav, footerNav } = source('lib/site-config.ts')
+ for (const [label, value] of Object.entries({ sitemap, mainNav, footerNav })) {
+ assert.doesNotMatch(JSON.stringify(value), /impact-preview|\/field-velocity\/|"[^" ]*\/impact\/"/, label)
+ }
+ for (const file of ['public/search-index.json', 'public/feed.xml', 'public/robots.txt']) {
+ assert.doesNotMatch(readFileSync(file, 'utf8'), /impact-preview-eb61fba1b98e|\/field-velocity\//, file)
+ }
+})
diff --git a/src/app/areas/[slug]/page.tsx b/src/app/areas/[slug]/page.tsx
index b119b966..a581a5f7 100644
--- a/src/app/areas/[slug]/page.tsx
+++ b/src/app/areas/[slug]/page.tsx
@@ -1,6 +1,4 @@
import type { Metadata } from 'next'
-import AreaFieldVelocity from '@/components/AreaFieldVelocity'
-import { isFocusAreaKey } from '@/lib/field-velocity-data'
import EditPageButton from '@/components/EditPageButton'
import { PageEditHistoryByline } from '@/components/EditHistoryByline'
@@ -289,7 +287,7 @@ export default async function AreaPage({ params }: Props) {
)}
- {isFocusAreaKey(slug) &&