diff --git a/.gitignore b/.gitignore
index 8581be00..aba273e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,4 +89,10 @@ tools/tla/states/
# bun + TypeScript tooling — post-setup scripting surface per
# docs/DECISIONS/2026-04-20-tools-scripting-language.md. The
# bun.lock file IS committed; node_modules is not.
-node_modules/
\ No newline at end of file
+node_modules/
+
+# playwright-mcp artifacts — per-session browser state
+# (screenshots / console / page-dump YAMLs). Regenerated per
+# use; not needed in-repo. Parallel to drop/ staging per
+# PR #265 Otto-90.
+.playwright-mcp/
diff --git a/src/Core/Core.fsproj b/src/Core/Core.fsproj
index 7073ae30..e43b4c64 100644
--- a/src/Core/Core.fsproj
+++ b/src/Core/Core.fsproj
@@ -40,6 +40,7 @@
+
diff --git a/src/Core/TemporalCoordinationDetection.fs b/src/Core/TemporalCoordinationDetection.fs
new file mode 100644
index 00000000..fb250f57
--- /dev/null
+++ b/src/Core/TemporalCoordinationDetection.fs
@@ -0,0 +1,96 @@
+namespace Zeta.Core
+
+open System
+
+
+/// **Temporal Coordination Detection — foundational primitives.**
+///
+/// This module hosts the single-node-shippable detection primitives
+/// from Aaron's *differentiable firefly network + trivial cartel
+/// detect* design, as formalized by Amara in the 11th courier
+/// ferry (`docs/aurora/2026-04-24-amara-temporal-coordination-
+/// detection-cartel-graph-influence-surface-11th-ferry.md`). Full
+/// multi-node architecture awaits Zeta's multi-node foundation;
+/// these pure-function primitives ship incrementally per the
+/// Otto-105 graduation-cadence.
+///
+/// **Attribution.** The underlying concepts (temporal-coordination
+/// detection as a primary defence surface, the firefly-synchronization
+/// metaphor, trivial-cartel-detect as the first-order-signal tier)
+/// are Aaron's design. Amara's contribution is the technical
+/// vocabulary and specific formulations (phase-locking value, cross-
+/// correlation, modularity spikes, eigenvector centrality drift, …).
+/// This module implements Amara's formalizations; the design intent
+/// behind them is Aaron's.
+///
+/// **Scope of this first graduation.** Only `crossCorrelation` and
+/// `crossCorrelationProfile` ship here — the single most portable
+/// primitive for detecting timing coordination between two event
+/// streams. Downstream primitives (phase-locking value, burst-
+/// alignment clusters, modularity-spike detectors, eigenvector-
+/// centrality drift) are separate graduation candidates that
+/// compose over this one.
+///
+/// **Why it matters for cartel detection.** Honest distributed
+/// actors produce noisy, partially-independent event streams;
+/// coordinated actors produce phase-aligned streams. Pearson cross-
+/// correlation at lag τ quantifies how similarly two series move
+/// at a time offset, yielding the core signal for the "obvious
+/// coordinated timing" (trivial-cartel) case. Subtler coordination
+/// requires the harder primitives added in later graduations.
+[]
+module TemporalCoordinationDetection =
+
+ /// Pearson cross-correlation of two series at lag `tau`. The
+ /// value is normalized to `[-1.0, 1.0]` when defined; returns
+ /// `None` when either series has fewer than two elements that
+ /// overlap at the given lag, or when either overlap window is
+ /// constant (standard deviation = 0, undefined denominator).
+ ///
+ /// Lag semantics: positive `tau` aligns `ys[i + tau]` with
+ /// `xs[i]`; negative `tau` aligns `ys[i]` with `xs[i - tau]`.
+ /// A detector asking "does `ys` lead `xs` by `k` steps?" passes
+ /// `tau = k`.
+ let crossCorrelation (xs: double seq) (ys: double seq) (tau: int) : double option =
+ let xArr = Seq.toArray xs
+ let yArr = Seq.toArray ys
+ let startX, startY =
+ if tau >= 0 then 0, tau
+ else -tau, 0
+ let overlap = min (xArr.Length - startX) (yArr.Length - startY)
+ if overlap < 2 then None
+ else
+ let mutable meanX = 0.0
+ let mutable meanY = 0.0
+ for i in 0 .. overlap - 1 do
+ meanX <- meanX + xArr.[startX + i]
+ meanY <- meanY + yArr.[startY + i]
+ let n = double overlap
+ meanX <- meanX / n
+ meanY <- meanY / n
+ let mutable cov = 0.0
+ let mutable varX = 0.0
+ let mutable varY = 0.0
+ for i in 0 .. overlap - 1 do
+ let dx = xArr.[startX + i] - meanX
+ let dy = yArr.[startY + i] - meanY
+ cov <- cov + dx * dy
+ varX <- varX + dx * dx
+ varY <- varY + dy * dy
+ if varX = 0.0 || varY = 0.0 then None
+ else Some (cov / sqrt (varX * varY))
+
+ /// Cross-correlation across the full lag range `[-maxLag, maxLag]`.
+ /// Returns one entry per lag; `None` entries indicate lags where
+ /// the overlap window was too short or degenerate (flat input)
+ /// to compute a correlation.
+ ///
+ /// Intended downstream use: feed to a burst-alignment / cluster
+ /// detector that flags lags where `|corr|` is unusually high
+ /// versus a baseline, the operational "firefly detection" case
+ /// from the 11th ferry's signal model.
+ let crossCorrelationProfile (xs: double seq) (ys: double seq) (maxLag: int) : (int * double option) array =
+ if maxLag < 0 then [||]
+ else
+ [| for tau in -maxLag .. maxLag ->
+ tau, crossCorrelation xs ys tau |]
diff --git a/tests/Tests.FSharp/Algebra/TemporalCoordinationDetection.Tests.fs b/tests/Tests.FSharp/Algebra/TemporalCoordinationDetection.Tests.fs
new file mode 100644
index 00000000..f8acce01
--- /dev/null
+++ b/tests/Tests.FSharp/Algebra/TemporalCoordinationDetection.Tests.fs
@@ -0,0 +1,101 @@
+module Zeta.Tests.Algebra.TemporalCoordinationDetectionTests
+
+open System
+open FsUnit.Xunit
+open global.Xunit
+open Zeta.Core
+
+
+// ─── crossCorrelation at lag 0 ─────────
+
+[]
+let ``crossCorrelation of identical series is 1 at lag 0`` () =
+ let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0 ]
+ TemporalCoordinationDetection.crossCorrelation xs xs 0
+ |> Option.map (fun v -> Math.Round(v, 9))
+ |> should equal (Some 1.0)
+
+[]
+let ``crossCorrelation of negated series is -1 at lag 0`` () =
+ let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0 ]
+ let ys = xs |> List.map (fun v -> -v)
+ TemporalCoordinationDetection.crossCorrelation xs ys 0
+ |> Option.map (fun v -> Math.Round(v, 9))
+ |> should equal (Some -1.0)
+
+[]
+let ``crossCorrelation of constant series is None (undefined variance)`` () =
+ // A flat series has zero variance; Pearson correlation is
+ // undefined. Detectors must get None, not NaN or 0.0.
+ let xs = [ 5.0; 5.0; 5.0; 5.0 ]
+ let ys = [ 1.0; 2.0; 3.0; 4.0 ]
+ TemporalCoordinationDetection.crossCorrelation xs ys 0 |> should equal (None: double option)
+
+
+// ─── crossCorrelation at nonzero lag ─────────
+
+[]
+let ``crossCorrelation detects a one-step lag alignment`` () =
+ // ys is xs shifted right by 1. At tau=1, the aligned windows
+ // are the same shape, so correlation should be 1.
+ let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0 ]
+ let ys = [ 0.0; 1.0; 2.0; 3.0; 4.0 ]
+ TemporalCoordinationDetection.crossCorrelation xs ys 1
+ |> Option.map (fun v -> Math.Round(v, 9))
+ |> should equal (Some 1.0)
+
+[]
+let ``crossCorrelation at negative lag aligns x ahead of y`` () =
+ // Same series, probed at tau=-1: the aligned windows are
+ // xs[1..] vs ys[..], both length 4, identical shape within
+ // their respective slices when xs = ys.
+ let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0 ]
+ TemporalCoordinationDetection.crossCorrelation xs xs -1
+ |> Option.map (fun v -> Math.Round(v, 9))
+ |> should equal (Some 1.0)
+
+
+// ─── crossCorrelation edge cases ─────────
+
+[]
+let ``crossCorrelation with single-element overlap is None`` () =
+ // Overlap of 1 is below the 2-sample minimum; must return None.
+ let xs = [ 1.0; 2.0 ]
+ let ys = [ 1.0; 2.0 ]
+ TemporalCoordinationDetection.crossCorrelation xs ys 1 |> should equal (None: double option)
+
+[]
+let ``crossCorrelation with lag larger than series returns None`` () =
+ let xs = [ 1.0; 2.0; 3.0 ]
+ let ys = [ 1.0; 2.0; 3.0 ]
+ TemporalCoordinationDetection.crossCorrelation xs ys 10 |> should equal (None: double option)
+
+
+// ─── crossCorrelationProfile ─────────
+
+[]
+let ``crossCorrelationProfile returns 2 maxLag + 1 entries`` () =
+ let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0; 6.0 ]
+ let ys = [ 1.0; 2.0; 3.0; 4.0; 5.0; 6.0 ]
+ let profile = TemporalCoordinationDetection.crossCorrelationProfile xs ys 2
+ profile.Length |> should equal 5
+ profile |> Array.map fst |> should equal [| -2; -1; 0; 1; 2 |]
+
+[]
+let ``crossCorrelationProfile identical series peaks at lag 0`` () =
+ // Zero-lag correlation of a series with itself is 1.0.
+ let xs = [ 1.0; 2.0; 3.0; 4.0; 5.0; 6.0; 7.0; 8.0 ]
+ let profile = TemporalCoordinationDetection.crossCorrelationProfile xs xs 3
+ let zeroLagCorr =
+ profile
+ |> Array.find (fun (lag, _) -> lag = 0)
+ |> snd
+ zeroLagCorr
+ |> Option.map (fun v -> Math.Round(v, 9))
+ |> should equal (Some 1.0)
+
+[]
+let ``crossCorrelationProfile with negative maxLag returns empty array`` () =
+ let xs = [ 1.0; 2.0; 3.0 ]
+ let ys = [ 1.0; 2.0; 3.0 ]
+ TemporalCoordinationDetection.crossCorrelationProfile xs ys -1 |> should equal ([||]: (int * double option) array)
diff --git a/tests/Tests.FSharp/Tests.FSharp.fsproj b/tests/Tests.FSharp/Tests.FSharp.fsproj
index b65af635..f9e64744 100644
--- a/tests/Tests.FSharp/Tests.FSharp.fsproj
+++ b/tests/Tests.FSharp/Tests.FSharp.fsproj
@@ -18,6 +18,7 @@
+