diff --git a/docs/_overrides/main.html b/docs/_overrides/main.html
index 5490c824..5f7a8b75 100644
--- a/docs/_overrides/main.html
+++ b/docs/_overrides/main.html
@@ -33,13 +33,19 @@
: document.querySelector("[data-aaasm-outdated]");
if (!host) return;
- // Derive the current version directory and the versions.json URL from the
- // page location: mike serves each version under ///, with
- // versions.json one level up at //versions.json.
- var path = window.location.pathname.replace(/\/+$/, "");
- var segments = path.split("/");
- var current = segments.pop() || "";
- var versionsUrl = segments.join("/") + "/versions.json";
+ // mike serves each version under ///, with versions.json
+ // one level up at //versions.json. mkdocs renders `base_url` so it
+ // points at the deploy root (the current version directory) from *any*
+ // page depth, so the deploy-root versions.json is reliably one level
+ // above it. Resolving the bare relative `base_url` against `location`
+ // yields an absolute URL; popping only the page's last path segment (the
+ // previous approach) 404s on deep pages.
+ var versionRoot = new URL("{{ base_url }}/", window.location);
+ var versionsUrl = new URL("../versions.json", versionRoot).toString();
+
+ // The current version id is the last path segment of the deploy root.
+ var current =
+ versionRoot.pathname.replace(/\/+$/, "").split("/").pop() || "";
fetch(versionsUrl, { cache: "no-store" })
.then(function (r) { return r.ok ? r.json() : []; })
diff --git a/docs/_overrides/partials/integrations/analytics.html b/docs/_overrides/partials/integrations/analytics.html
new file mode 100644
index 00000000..f9760b86
--- /dev/null
+++ b/docs/_overrides/partials/integrations/analytics.html
@@ -0,0 +1,27 @@
+{#-
+ Override of Material's partials/integrations/analytics.html (AAASM-3558).
+
+ Material's generated partial only calls `__md_analytics()` once the analytics
+ consent cookie is granted (`__md_get("__consent").analytics`). With our opt-in
+ consent (cookies.analytics.checked: false, off by default) that call never
+ fires for ordinary visitors, so gtag.js is never loaded and GA never reports.
+
+ We switch python to *advanced* Consent Mode v2 (matching the other four doc
+ sites): the overridden google.html partial declares `analytics_storage:
+ 'denied'` as the default before loading gtag, so calling `__md_analytics()`
+ unconditionally is GDPR-correct — GA sends cookieless, modelled pings until
+ the visitor grants analytics consent, then full hits. We therefore drop
+ Material's consent gate and always run the loader.
+
+ The native `extra.consent` banner UI is kept; google.html reads the stored
+ consent on each load and updates Consent Mode to 'granted' when the analytics
+ cookie is enabled (the consent form reloads the page on submit, so reading
+ stored consent at load time is sufficient — no live form hook needed).
+-#}
+{% if config.extra.analytics %}
+ {% set provider = config.extra.analytics.provider %}
+{% endif %}
+{% if provider %}
+ {% include "partials/integrations/analytics/" ~ provider ~ ".html" %}
+
+{% endif %}
diff --git a/docs/_overrides/partials/integrations/analytics/google.html b/docs/_overrides/partials/integrations/analytics/google.html
new file mode 100644
index 00000000..239206b2
--- /dev/null
+++ b/docs/_overrides/partials/integrations/analytics/google.html
@@ -0,0 +1,102 @@
+{#-
+ Override of Material's partials/integrations/analytics/google.html (AAASM-3558).
+
+ Material's generated partial defines `__md_analytics()` (the gtag.js loader +
+ `config` + search/feedback wiring) but loads GA with *basic* Consent Mode — it
+ only runs once the analytics consent cookie is granted. With our opt-in consent
+ GA never loaded for ordinary visitors (window.gtag stayed undefined, zero
+ /g/collect hits).
+
+ This override keeps Material's `__md_analytics()` body intact (so the feedback
+ widget is revealed + wired and its single GA `feedback` event still flows
+ through Material's own handler — no custom handler, no double-count) but turns
+ it into *advanced* Consent Mode v2, matching the other four doc sites:
+
+ 1. Before `gtag('config', …)`, declare Consent Mode defaults as DENIED for
+ analytics_storage / ad_storage / ad_user_data / ad_personalization, so
+ gtag.js sends cookieless, modelled pings until the visitor consents.
+ 2. Read Material's stored consent (`__md_get('__consent')`, namespaced to the
+ deploy root) and, if the analytics cookie is enabled, immediately update
+ analytics_storage to 'granted' — upgrading from cookieless to full hits.
+ The consent banner reloads the page on submit, so reading stored consent
+ at load time covers accept/reject without a live form hook.
+
+ `partials/integrations/analytics.html` (also overridden) now calls
+ `__md_analytics()` unconditionally, so this runs on every page load. The
+ Measurement ID + feedback markup still come from `extra.analytics` in
+ mkdocs.yml. Mirrors mkdocs-material 9.7.6; only the Consent Mode lines are added.
+-#}
+{% if config.extra.analytics %}
+ {% set property = config.extra.analytics.property | d("", true) %}
+{% endif %}
+
diff --git a/docs/_overrides/partials/javascripts/outdated.html b/docs/_overrides/partials/javascripts/outdated.html
new file mode 100644
index 00000000..a3062a0d
--- /dev/null
+++ b/docs/_overrides/partials/javascripts/outdated.html
@@ -0,0 +1,14 @@
+{#-
+ Override of Material's partials/javascripts/outdated.html (AAASM-3558, carried
+ over from the closed PR #169 / AAASM-3557).
+
+ Material's generated script calls `new URL("{{ base_url }}")` with no base
+ argument. On many pages `base_url` is a bare relative value such as `.` or
+ `..`, which is not an absolute URL, so `new URL("..")` throws
+ `TypeError: Failed to construct 'URL': Invalid URL` on every page load.
+
+ The only change here is passing `location` as the base so the relative
+ `base_url` resolves against the current page. Behaviour is otherwise identical
+ to Material's generated script.
+-#}
+
diff --git a/scripts/ci/deploy-release-version-documentation.sh b/scripts/ci/deploy-release-version-documentation.sh
index 2eb5428e..b86cf141 100755
--- a/scripts/ci/deploy-release-version-documentation.sh
+++ b/scripts/ci/deploy-release-version-documentation.sh
@@ -153,8 +153,13 @@ fi
# Set the default (root-redirect target) to the most authoritative channel that
# exists, as decided by the resolver: stable, else pre-release, else latest.
+# Use a custom redirect template that carries the GA snippet (advanced Consent
+# Mode v2, cookieless-until-consent) so Google's "Test your website" detects the
+# tag on the bare-root URL too (AAASM-3558).
echo "🎯 Setting default channel (root redirect) to '${DEFAULT_CHANNEL}'"
-mike set-default --push "${DEFAULT_CHANNEL}"
+mike set-default --push \
+ --template "${PROJECT_ROOT}/scripts/ci/templates/mike-redirect-with-analytics.html" \
+ "${DEFAULT_CHANNEL}"
echo "🍻 Release documentation deployed for ${RELEASE_TAG}: stable=${STABLE_VERSION:-(none)}," \
"pre-release=${PRERELEASE_VERSION:-(hidden)}, default=${DEFAULT_CHANNEL}."
diff --git a/scripts/ci/templates/mike-redirect-with-analytics.html b/scripts/ci/templates/mike-redirect-with-analytics.html
new file mode 100644
index 00000000..7d6d4a8d
--- /dev/null
+++ b/scripts/ci/templates/mike-redirect-with-analytics.html
@@ -0,0 +1,42 @@
+
+
+
+
+ Redirecting
+
+
+
+
+
+
+
+ Redirecting to {{href}}...
+
+