diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md
index 6c62b14c..81ece0ce 100644
--- a/docs/CODEX_EXECUTION_LOG.md
+++ b/docs/CODEX_EXECUTION_LOG.md
@@ -12091,3 +12091,36 @@ Open:
- The guest screenshots intentionally show governed fixture/demo evidence, not
a claim that the rejected V66 candidate labels or a new detector have been
promoted.
+
+## 2026-08-01 - Live animated area-analysis workflow
+
+### Changed
+
+- Added a reusable `LiveAnalysisJourney` overlay to the real map canvas, using
+ the same dark glass, mint signal and four-stage language as the public
+ interactive project illustration.
+- Bound Select, Sources, Process and Verify to persisted workbench state:
+ bounded AOI selection, source/coverage loading, extraction or image analysis,
+ QA validation, completed evidence and real error responses.
+- Added a processing-only scan line, status pulse, responsive compact layout and
+ a complete `prefers-reduced-motion` fallback. The overlay uses
+ `pointer-events: none`, so drawing, panning and object inspection remain
+ available beneath it.
+- Added a pure state resolver and component coverage for idle, loading,
+ processing, verification, completion and error transitions.
+
+### Tested before deployment
+
+- `npm run test:unit -- --run src/components/map/LiveAnalysisJourney.test.tsx`
+ passed: 3 tests.
+- Complete frontend unit suite passed: 49 tests in 15 files.
+- `npm run build` passed TypeScript compilation and the Vite production build.
+- React review confirmed static step metadata is module-scoped, status is
+ derived during render, icons use direct imports and the overlay has an
+ accessible live region.
+
+### Known limitation and next gate
+
+- The local frontend preview cannot enter the workbench without its API runtime.
+ Tower deployment and browser validation against the real guest/operator data
+ remain the final acceptance gate for this pass.
diff --git a/docs/TODO.md b/docs/TODO.md
index 6ae6c410..cb9a7d6a 100644
--- a/docs/TODO.md
+++ b/docs/TODO.md
@@ -1029,3 +1029,12 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Keep near-gate regional precision/recall stabilization inside a 0.03 sampling guard-band to prevent cross-region seesaw regressions.
- [ ] Open protected test and pure-background evidence only after every calibration gate passes.
- [ ] Queue final representative human sign-off only after all automated gates pass, then promote and redeploy the exact checksummed model.
+
+# Sprint 233 - Live animated gebiedsanalyse
+
+- [x] Vertaal de vier schakels van de welkomstillustratie naar de echte kaartworkflow.
+- [x] Koppel Selecteer, Bronnen, Verwerk en Controleer uitsluitend aan bestaande API- en hookstatussen.
+- [x] Toon verwerking, QA, fout en gereed als onderscheidbare live kaartstatussen zonder fictieve successen.
+- [x] Voeg een responsieve desktop- en mobiele procesrail met reduced-motion fallback toe.
+- [x] Dek de statusresolver en het geverifieerde-bewijslabel af met gerichte componenttests.
+- [ ] Controleer na Tower-redeploy de operator- en gastworkflow visueel op desktop en mobiel.
diff --git a/frontend/src/components/map/LiveAnalysisJourney.test.tsx b/frontend/src/components/map/LiveAnalysisJourney.test.tsx
new file mode 100644
index 00000000..e1e87722
--- /dev/null
+++ b/frontend/src/components/map/LiveAnalysisJourney.test.tsx
@@ -0,0 +1,57 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import { LiveAnalysisJourney, resolveLiveAnalysisJourney } from './LiveAnalysisJourney'
+
+const baseState = {
+ selectionMode: false,
+ hasSelection: false,
+ sourceLoading: false,
+ sourceReady: false,
+ processing: false,
+ validating: false,
+ hasResult: false,
+ verified: false,
+ error: null,
+}
+
+describe('resolveLiveAnalysisJourney', () => {
+ it('follows real workflow state without assuming completion', () => {
+ expect(resolveLiveAnalysisJourney(baseState).currentLabel).toBe('Selecteer')
+ expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceLoading: true }).currentLabel).toBe('Bronnen')
+ expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, processing: true }).currentLabel).toBe('Verwerk')
+ expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, hasResult: true }).currentLabel).toBe('Controleer')
+ expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, hasResult: true, verified: true }).status).toBe('complete')
+ })
+
+ it('marks the reached stage as failed', () => {
+ const view = resolveLiveAnalysisJourney({
+ ...baseState,
+ hasSelection: true,
+ sourceReady: true,
+ processing: true,
+ error: 'Bron tijdelijk niet beschikbaar.',
+ })
+ expect(view.status).toBe('error')
+ expect(view.steps[2]).toBe('error')
+ })
+})
+
+describe('LiveAnalysisJourney', () => {
+ it('announces verified evidence only when verification exists', () => {
+ render(
+ ,
+ )
+ expect(screen.getAllByText('Kwaliteitsbewijs beschikbaar')).toHaveLength(2)
+ expect(screen.getByTestId('live-analysis-journey').getAttribute('data-stage')).toBe('controleer')
+ })
+})
diff --git a/frontend/src/components/map/LiveAnalysisJourney.tsx b/frontend/src/components/map/LiveAnalysisJourney.tsx
new file mode 100644
index 00000000..f1ff2ad0
--- /dev/null
+++ b/frontend/src/components/map/LiveAnalysisJourney.tsx
@@ -0,0 +1,97 @@
+import { BadgeCheck, Database, MapPinned, ScanSearch, ShieldAlert } from 'lucide-react'
+
+export type LiveAnalysisStepStatus = 'waiting' | 'active' | 'complete' | 'error'
+
+export interface LiveAnalysisJourneyState {
+ selectionMode: boolean
+ hasSelection: boolean
+ sourceLoading: boolean
+ sourceReady: boolean
+ processing: boolean
+ validating: boolean
+ hasResult: boolean
+ verified: boolean
+ error: string | null
+}
+
+export interface LiveAnalysisJourneyView {
+ currentIndex: number
+ currentLabel: string
+ status: 'idle' | 'running' | 'complete' | 'error'
+ steps: LiveAnalysisStepStatus[]
+}
+
+const STEP_LABELS = ['Selecteer', 'Bronnen', 'Verwerk', 'Controleer'] as const
+
+export function resolveLiveAnalysisJourney(state: LiveAnalysisJourneyState): LiveAnalysisJourneyView {
+ let currentIndex = 0
+ if (state.validating || state.verified || state.hasResult) currentIndex = 3
+ else if (state.processing) currentIndex = 2
+ else if (state.hasSelection) currentIndex = state.sourceLoading || !state.sourceReady ? 1 : 2
+
+ const status: LiveAnalysisJourneyView['status'] = state.error
+ ? 'error'
+ : state.verified
+ ? 'complete'
+ : state.sourceLoading || state.processing || state.validating
+ ? 'running'
+ : 'idle'
+
+ const steps: LiveAnalysisStepStatus[] = [
+ state.hasSelection ? 'complete' : 'active',
+ state.hasSelection && state.sourceReady ? 'complete' : state.hasSelection ? 'active' : 'waiting',
+ state.hasResult ? 'complete' : state.processing || (state.hasSelection && state.sourceReady) ? 'active' : 'waiting',
+ state.verified ? 'complete' : state.validating || state.hasResult ? 'active' : 'waiting',
+ ]
+
+ if (state.error) steps[currentIndex] = 'error'
+
+ return { currentIndex, currentLabel: STEP_LABELS[currentIndex], status, steps }
+}
+
+interface LiveAnalysisJourneyProps extends LiveAnalysisJourneyState {
+ selectionLabel: string
+ sourceLabel: string
+ statusMessage: string
+ resultLabel: string
+}
+
+const steps = [
+ { label: 'Selecteer', icon: MapPinned },
+ { label: 'Bronnen', icon: Database },
+ { label: 'Verwerk', icon: ScanSearch },
+ { label: 'Controleer', icon: BadgeCheck },
+] as const
+
+export function LiveAnalysisJourney({ selectionLabel, sourceLabel, statusMessage, resultLabel, ...state }: LiveAnalysisJourneyProps): JSX.Element {
+ const view = resolveLiveAnalysisJourney(state)
+ const detail = view.currentIndex === 0
+ ? state.selectionMode ? 'Teken nu op de kaart' : selectionLabel
+ : view.currentIndex === 1
+ ? sourceLabel
+ : view.currentIndex === 3 && state.verified
+ ? resultLabel
+ : statusMessage
+
+ return (
+
+
+ Live analyse
+ {state.error ? 'Analyse onderbroken' : view.currentLabel}
+ {state.error ?? detail}
+
+
+ {steps.map(({ label, icon: Icon }, index) => (
+ -
+
+ {label}
+
+ ))}
+
+
+ {state.error ? : }
+ {state.verified ? resultLabel : state.hasResult ? 'Resultaat wacht op controle' : 'Controleerbaar resultaat'}
+
+
+ )
+}
diff --git a/frontend/src/components/map/MapWorkspace.tsx b/frontend/src/components/map/MapWorkspace.tsx
index b192092e..c8b533fa 100644
--- a/frontend/src/components/map/MapWorkspace.tsx
+++ b/frontend/src/components/map/MapWorkspace.tsx
@@ -8,6 +8,7 @@ import { useTemporalComparison } from '../../hooks/useTemporalComparison'
import { getDatasetDisplayName, getDatasetSourceDisplayName } from '../../lib/datasetDisplay'
import { TemporalTrendChart } from './TemporalTrendChart'
import { MunicipalitySearch } from './MunicipalitySearch'
+import { LiveAnalysisJourney } from './LiveAnalysisJourney'
import { terrainImageUrl } from '../../lib/terrainImage'
import { floodHazardImageUrl } from '../../lib/floodHazardImage'
import { thematicRasterImageUrl, walousRasterImageUrl } from '../../lib/thematicRaster'
@@ -1310,6 +1311,39 @@ export function MapWorkspace({
: onDemandProductMap.get(activeTheme.id) ?? null
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
const analysisOverlayActive = mapContentMode === 'analysis' && analysisLayerAvailable && Boolean(mapFeatureCollection)
+ const liveJourneyError = mapSelectionError
+ ?? themeResultsError
+ ?? temporalComparisonError
+ ?? orthophotoAnalysisError
+ ?? coverageError
+ ?? workspaceError
+ const liveJourneyHasResult = Boolean(
+ mapSelectionResult
+ || themeInsights.length > 0
+ || temporalComparison
+ || orthophotoResult
+ || analysisOverlayActive,
+ )
+ const liveJourneyVerified = Boolean(mapSelectionQaResult || orthophotoAnalysisQuality)
+ const liveJourneyProcessing = Boolean(
+ mapSelectionLoading
+ || themeResultsLoading
+ || temporalComparisonLoading
+ || orthophotoAnalysisRunning,
+ )
+ const liveJourneyValidating = Boolean(
+ mapSelectionQaRunning || orthophotoAnalysisStage === 'validating',
+ )
+ const liveJourneyStatus = orthophotoAnalysisStatus
+ || (temporalComparisonLoading ? 'Officiële meetmomenten vergelijken…' : '')
+ || (themeResultsLoading ? 'Begrensde bronnen verwerken…' : '')
+ || (mapSelectionLoading ? 'Objecten binnen de selectie ophalen…' : '')
+ || (liveJourneyHasResult ? 'Resultaat op de kaart beschikbaar' : 'Klaar om de selectie te verwerken')
+ const liveJourneyResultLabel = liveJourneyVerified
+ ? 'Kwaliteitsbewijs beschikbaar'
+ : latestMapSelectionQualityCheckId
+ ? 'Bewaard kwaliteitsbewijs beschikbaar'
+ : 'Controleerbaar resultaat'
const selectedOrthophotoProduct = orthophotoProducts.find((item) => item.key === selectedOrthophotoProductKey) ?? null
const orthophotoImageOverlay = useMemo(
() => orthophotoResult && orthophotoImageUrl && orthophotoResult.bbox_epsg4326.length === 4
@@ -2609,6 +2643,21 @@ export function MapWorkspace({
onMapBboxSelect={handleMapBboxSelect}
onViewportChange={onMapViewportChange}
/>
+
Werkgebied
{thematicRasterImageOverlays.length > 0 || walousRasterImageOverlays.length > 0 ? (
diff --git a/frontend/src/styles/geointel-system.css b/frontend/src/styles/geointel-system.css
index 05497c8b..c9908bd2 100644
--- a/frontend/src/styles/geointel-system.css
+++ b/frontend/src/styles/geointel-system.css
@@ -1415,3 +1415,235 @@ button.overview-command-card { cursor: pointer; }
transition-duration: .01ms !important;
}
}
+
+/* De marketingillustratie wordt hier een echte productstatus. Alle stappen
+ volgen API- en hookstate uit MapWorkspace; de laag onderschept geen kaartinput. */
+.live-analysis-journey {
+ position: absolute;
+ z-index: 4;
+ inset: 0;
+ overflow: hidden;
+ color: #f5fffc;
+ pointer-events: none;
+}
+
+.live-analysis-status-card,
+.live-analysis-proof,
+.live-analysis-steps {
+ border: 1px solid rgba(190, 244, 232, 0.24);
+ background: rgba(4, 45, 40, 0.86);
+ box-shadow: 0 14px 36px rgba(2, 28, 25, 0.24);
+ backdrop-filter: blur(12px);
+}
+
+.live-analysis-status-card {
+ position: absolute;
+ top: var(--gi-space-4);
+ left: var(--gi-space-4);
+ display: grid;
+ gap: 2px;
+ width: min(14rem, calc(100% - 2rem));
+ border-radius: var(--gi-radius-md);
+ padding: 0.72rem 0.82rem;
+}
+
+.live-analysis-kicker {
+ color: #8ee7d2;
+ font-size: 0.55rem;
+ font-weight: 800;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+}
+
+.live-analysis-kicker i {
+ display: inline-block;
+ width: 0.42rem;
+ height: 0.42rem;
+ margin-right: 0.4rem;
+ border-radius: 50%;
+ background: #77e2ca;
+ box-shadow: 0 0 0 5px rgba(119, 226, 202, 0.12);
+}
+
+.live-analysis-status-card strong {
+ font-family: Manrope, sans-serif;
+ font-size: 0.88rem;
+}
+
+.live-analysis-status-card small {
+ overflow: hidden;
+ color: rgba(245, 255, 252, 0.68);
+ font-size: 0.62rem;
+ line-height: 1.35;
+ text-overflow: ellipsis;
+}
+
+.live-analysis-steps {
+ position: absolute;
+ bottom: var(--gi-space-4);
+ left: var(--gi-space-4);
+ display: grid;
+ grid-template-columns: repeat(4, minmax(3.6rem, 1fr));
+ width: min(25rem, calc(100% - 2rem));
+ margin: 0;
+ border-radius: var(--gi-radius-md);
+ padding: 0.52rem 0.62rem;
+ list-style: none;
+}
+
+.live-analysis-step {
+ position: relative;
+ display: grid;
+ gap: 0.24rem;
+ justify-items: center;
+ color: rgba(245, 255, 252, 0.45);
+}
+
+.live-analysis-step:not(:last-child)::after {
+ position: absolute;
+ top: 0.68rem;
+ left: calc(50% + 0.9rem);
+ width: calc(100% - 1.8rem);
+ border-top: 1px dashed rgba(142, 231, 210, 0.3);
+ content: '';
+}
+
+.live-analysis-step > span {
+ display: grid;
+ z-index: 1;
+ place-items: center;
+ width: 1.45rem;
+ height: 1.45rem;
+ border: 1px solid rgba(190, 244, 232, 0.24);
+ border-radius: 50%;
+ background: #103f3a;
+}
+
+.live-analysis-step svg {
+ width: 0.72rem;
+ height: 0.72rem;
+}
+
+.live-analysis-step small {
+ font-size: 0.54rem;
+ font-weight: 750;
+}
+
+.live-analysis-step-active,
+.live-analysis-step-complete {
+ color: #a8f3e2;
+}
+
+.live-analysis-step-active > span {
+ border-color: #8ee7d2;
+ box-shadow: 0 0 0 5px rgba(119, 226, 202, 0.12);
+}
+
+.live-analysis-step-complete > span {
+ border-color: #66d6bd;
+ background: #176a5c;
+}
+
+.live-analysis-step-error,
+.live-analysis-journey-error .live-analysis-kicker {
+ color: #ffd2c2;
+}
+
+.live-analysis-step-error > span,
+.live-analysis-journey-error .live-analysis-status-card {
+ border-color: rgba(255, 137, 105, 0.7);
+}
+
+.live-analysis-proof {
+ position: absolute;
+ top: var(--gi-space-4);
+ right: var(--gi-space-4);
+ display: flex;
+ gap: 0.46rem;
+ align-items: center;
+ max-width: min(15rem, 42%);
+ border-radius: var(--gi-radius-md);
+ padding: 0.62rem 0.72rem;
+ color: rgba(245, 255, 252, 0.64);
+ font-size: 0.59rem;
+ font-weight: 700;
+}
+
+.live-analysis-proof svg {
+ flex: 0 0 auto;
+ width: 0.88rem;
+ height: 0.88rem;
+ color: #8ee7d2;
+}
+
+.live-analysis-proof-ready {
+ border-color: rgba(142, 231, 210, 0.62);
+ color: #f5fffc;
+}
+
+.live-analysis-journey-running::after {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 16%;
+ width: 2px;
+ background: linear-gradient(180deg, transparent, rgba(174, 255, 237, 0.86), transparent);
+ filter: drop-shadow(0 0 7px #7be6d1);
+ content: '';
+ animation: gi-live-analysis-scan 3.8s ease-in-out infinite;
+}
+
+.live-analysis-journey-running .live-analysis-kicker i {
+ animation: gi-live-analysis-pulse 1.8s ease-out infinite;
+}
+
+@keyframes gi-live-analysis-scan {
+ 0%, 100% { opacity: 0; transform: translateX(0); }
+ 15%, 85% { opacity: 0.72; }
+ 50% { opacity: 0.92; transform: translateX(34vw); }
+}
+
+@keyframes gi-live-analysis-pulse {
+ 50% { box-shadow: 0 0 0 9px rgba(119, 226, 202, 0); }
+}
+
+@media (max-width: 720px) {
+ .live-analysis-status-card {
+ top: var(--gi-space-3);
+ left: var(--gi-space-3);
+ width: min(12rem, calc(100% - 6rem));
+ }
+
+ .live-analysis-proof {
+ top: var(--gi-space-3);
+ right: var(--gi-space-3);
+ width: 2rem;
+ min-height: 2rem;
+ padding: 0;
+ justify-content: center;
+ }
+
+ .live-analysis-proof span {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ }
+
+ .live-analysis-steps {
+ right: var(--gi-space-3);
+ bottom: var(--gi-space-3);
+ left: var(--gi-space-3);
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ width: auto;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .live-analysis-journey *,
+ .live-analysis-journey::after {
+ animation: none !important;
+ transition-duration: 0.01ms !important;
+ }
+}