diff --git a/php-transformer/composer.json b/php-transformer/composer.json
index 93375ebe9..0051bd6d9 100644
--- a/php-transformer/composer.json
+++ b/php-transformer/composer.json
@@ -348,6 +348,7 @@
"php tests/unit/captured-dialog-open-state.php",
"php tests/unit/captured-dialog-dropdown-panel.php",
"php tests/unit/captured-dialog-shared-header.php",
+ "php tests/unit/captured-dialog-ordinal-shared-header.php",
"php tests/unit/captured-dialog-editor-at-rest.php",
"php tests/unit/scoped-mixed-dropdown.php",
"php tests/unit/capture-runtime-ancestor-parity.php",
diff --git a/php-transformer/src/WordPressSitePlan/ShellExtraction.php b/php-transformer/src/WordPressSitePlan/ShellExtraction.php
index d56cd43ee..cf55227e8 100644
--- a/php-transformer/src/WordPressSitePlan/ShellExtraction.php
+++ b/php-transformer/src/WordPressSitePlan/ShellExtraction.php
@@ -2708,7 +2708,9 @@ private static function normalizeNestedChromeMarkup(string $markup): string
$markup = self::withoutScrollStateCarrierIdentity($markup);
$markup = preg_replace('/\s*' . EngineMarker::patternBody() . '/', '', $markup) ?? $markup;
$markup = preg_replace('/\s*(?:' . EngineMarker::specificityPatternBody('specificity-class') . '|blocks-engine-disclosure-summary-[a-f0-9]{6,}(?:-\d+)?)/', '', $markup) ?? $markup;
- // A captured dialog's generated id, and its triggers' ids, carry a per-document hash.
+ // A captured dialog's generated id, and its triggers' ids, carry a per-document hash;
+ // the capture's own panel handle (id, panel attribute, aria-controls) carries a per-document ordinal.
+ $markup = preg_replace('/\b' . preg_quote(CaptureEvidence::DIALOG_CLASS, '/') . '-\d+\b/', CaptureEvidence::DIALOG_CLASS, $markup) ?? $markup;
$markup = preg_replace('/blocks-engine-dialog-trigger-[a-f0-9]+-(\d+)/', 'blocks-engine-dialog-trigger-$1', $markup) ?? $markup;
$markup = preg_replace('/blocks-engine-dialog-[a-f0-9]{16}\b/', 'blocks-engine-dialog', $markup) ?? $markup;
$markup = preg_replace('/\s*be-inline-geometry-[a-f0-9]{16}(?:-[a-f0-9]{16})?/', '', $markup) ?? $markup;
diff --git a/php-transformer/tests/unit/captured-dialog-ordinal-shared-header.php b/php-transformer/tests/unit/captured-dialog-ordinal-shared-header.php
new file mode 100644
index 000000000..f96532266
--- /dev/null
+++ b/php-transformer/tests/unit/captured-dialog-ordinal-shared-header.php
@@ -0,0 +1,68 @@
+
HomeAboutContact';
+$header = static fn(int $n, string $dialogLinks): string => 'Acme' . "\n"
+ . '' . "\n"
+ . '' . $dialogLinks . '
';
+
+$compile = static function (callable $dialogFor) use ($header): array {
+ $files = array();
+ foreach (array('index.html' => 'Home', 'about.html' => 'About', 'contact.html' => 'Contact') as $path => $title) {
+ $n = count($files);
+ $files[] = array('path' => $path, 'content' => '' . $title . '' . $header($n, $dialogFor($n)) . '' . $title . '
' . $title . ' copy.
');
+ }
+ return (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => $files))->toWordPressSitePlanView()['wordpress_site_plan'] ?? array();
+};
+$parts = static fn(array $plan, string $area): array => array_values(array_filter($plan['template_parts'] ?? array(), static fn(array $part): bool => $area === ($part['area'] ?? null)));
+$template = static function (array $plan, string $slug): string {
+ foreach ($plan['templates'] ?? array() as $template) {
+ if ($slug === ($template['slug'] ?? null)) return (string) ($template['content'] ?? $template['canonical_block_markup'] ?? '');
+ }
+ return '';
+};
+
+// Identical headers that differ only by the capture's dialog ordinal.
+$plan = $compile(static fn(int $n): string => $links);
+$diagnostic = (string) json_encode(array_map(static fn(array $part): array => array('area' => $part['area'] ?? null, 'slug' => $part['slug'] ?? null), $plan['template_parts'] ?? array()));
+$assert(3 === count($plan['pages'] ?? array()), 'all three routes compile to pages', (string) json_encode(array_column($plan['pages'] ?? array(), 'source_path')));
+$assert(1 === count($parts($plan, 'header')), 'the header differing only by dialog ordinal becomes one template part', $diagnostic);
+$assert(1 === count($parts($plan, 'footer')), 'the footer becomes one template part', $diagnostic);
+foreach (array('page', 'front-page') as $slug) {
+ $markup = $template($plan, $slug);
+ if ('' === $markup) continue;
+ $assert(str_contains($markup, '"area":"header"') || str_contains($markup, '"slug":"header'), $slug . ' includes the header part', $markup);
+ $assert(str_contains($markup, '"area":"footer"') || str_contains($markup, '"slug":"footer'), $slug . ' includes the footer part', $markup);
+}
+
+// Negative control: dialogs that differ in content keep the header unshared.
+$control = $compile(static fn(int $n): string => '');
+$assert(0 === count($parts($control, 'header')), 'headers whose dialogs differ in content stay unshared', (string) json_encode(array_column($control['template_parts'] ?? array(), 'area')));
+$assert(1 === count($parts($control, 'footer')), 'the control footer is still shared', (string) json_encode(array_column($control['template_parts'] ?? array(), 'area')));
+
+fwrite(STDOUT, "Captured dialog ordinal shared header: {$passes} passed, {$failures} failed\n");
+exit($failures ? 1 : 0);