diff --git a/README.md b/README.md
index 85123cb..eb4e0a8 100644
--- a/README.md
+++ b/README.md
@@ -303,7 +303,7 @@ A tool that answers everything is easy to build. This is the other kind.
### Installing it
-**[The add-in's page →](https://peopleworks.github.io/SignsofAI/word)** — what it does, both
+**[The add-in's page →](https://peopleworks.github.io/SignsofAI/word-addin)** — what it does, both
install routes, and what it refuses to do, in English and Spanish.
The short version: on the web it is Home → Add-ins → More Add-ins → My Add-ins → **Upload My Add-in**
diff --git a/src/SignsOfAI.UI/Layout/MainLayout.razor b/src/SignsOfAI.UI/Layout/MainLayout.razor
index 1a28574..6984f0e 100644
--- a/src/SignsOfAI.UI/Layout/MainLayout.razor
+++ b/src/SignsOfAI.UI/Layout/MainLayout.razor
@@ -24,7 +24,7 @@
@L["nav.catalog"]
@* No host condition, unlike the two below: somebody using the desktop app writes in
Word too, and the add-in is useful to them for the same reasons. *@
- @L["nav.word"]
+ @L["nav.word"]
@* Only where there is something to gain by downloading it. The same signal as the
folder link above, and for the same reason: this host cannot open a folder, so a
better host exists for this machine and hiding that would be the lowest-common-
diff --git a/src/SignsOfAI.UI/Pages/Download.razor b/src/SignsOfAI.UI/Pages/Download.razor
index cffcd10..d112919 100644
--- a/src/SignsOfAI.UI/Pages/Download.razor
+++ b/src/SignsOfAI.UI/Pages/Download.razor
@@ -113,7 +113,7 @@ else
@L["dl.word.title"]
@L["dl.word.lede"]
diff --git a/src/SignsOfAI.UI/Pages/WordAddin.razor b/src/SignsOfAI.UI/Pages/WordAddin.razor
index 7f45b16..c15e143 100644
--- a/src/SignsOfAI.UI/Pages/WordAddin.razor
+++ b/src/SignsOfAI.UI/Pages/WordAddin.razor
@@ -1,4 +1,7 @@
-@page "/word"
+@* /word-addin, not /word: the deploy publishes the task pane itself at /word/, and a directory
+ on the host wins over a route in the router. The pane's address is in a manifest people have
+ already installed, so the pane keeps it and the page moves. *@
+@page "/word-addin"
@using SignsOfAI.Core.Calibration
@inherits LocalizedComponent
diff --git a/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs b/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs
new file mode 100644
index 0000000..3bf020e
--- /dev/null
+++ b/tests/SignsOfAI.Core.Tests/DeployedPathTests.cs
@@ -0,0 +1,90 @@
+using System.Text.RegularExpressions;
+using Xunit;
+
+namespace SignsOfAI.Core.Tests;
+
+///
+/// The deploy publishes two applications onto one host: the web app at the site root, and the Word
+/// task pane in a subdirectory of it. A real directory beats a client-side route, so any page whose
+/// @page path matches that subdirectory is unreachable — the visitor gets the task pane,
+/// which outside Word says it has no document to read.
+///
+/// That happened. /word was given to a page while the deploy was already publishing the pane
+/// at /word/, and the navigation link led to the pane for as long as it took to notice. It is
+/// invisible locally, because a dev server has neither directory, and invisible in the diff, because
+/// the two halves live in different files.
+///
+public class DeployedPathTests
+{
+ private static string RepoRoot()
+ {
+ var dir = new DirectoryInfo(AppContext.BaseDirectory);
+ while (dir is not null && !Directory.Exists(Path.Combine(dir.FullName, "src")))
+ dir = dir.Parent;
+
+ Assert.NotNull(dir);
+ return dir!.FullName;
+ }
+
+ /// Every route the router owns, from the @page directives themselves.
+ private static IReadOnlyList Routes()
+ {
+ string pages = Path.Combine(RepoRoot(), "src", "SignsOfAI.UI", "Pages");
+ var routes = new List();
+
+ foreach (string file in Directory.EnumerateFiles(pages, "*.razor"))
+ foreach (Match m in Regex.Matches(File.ReadAllText(file), @"^@page\s+""/([^""]*)""", RegexOptions.Multiline))
+ routes.Add(m.Groups[1].Value.Trim('/'));
+
+ Assert.NotEmpty(routes);
+ return routes;
+ }
+
+ ///
+ /// The directory the deploy copies the pane into, taken from the workflow rather than repeated
+ /// here — the point of this test is that the two are compared, not that both are typed twice.
+ ///
+ private static string PaneDirectory()
+ {
+ string workflow = File.ReadAllText(
+ Path.Combine(RepoRoot(), ".github", "workflows", "deploy-pages.yml"));
+
+ var m = Regex.Match(workflow, @"cp -r publish-word/wwwroot publish/wwwroot/(\S+)");
+ Assert.True(m.Success,
+ "deploy-pages.yml no longer copies the task pane where this test expects. If the step " +
+ "moved or was renamed, update this pattern — do not delete the check.");
+
+ return m.Groups[1].Value.Trim('/');
+ }
+
+ [Fact]
+ public void No_page_route_is_shadowed_by_the_task_pane_directory()
+ {
+ string pane = PaneDirectory();
+ var clash = Routes().FirstOrDefault(r => string.Equals(r, pane, StringComparison.OrdinalIgnoreCase));
+
+ Assert.True(clash is null,
+ $"A page is routed at \"/{clash}\" and the deploy publishes the task pane at \"/{pane}/\". " +
+ "On the host the directory wins and the page is unreachable — visitors get the pane. " +
+ "Move the page's route; the pane's address is in a manifest people have installed.");
+ }
+
+ [Fact]
+ public void The_manifest_points_at_the_directory_the_deploy_publishes()
+ {
+ // The other half of the same seam: the pane's URL lives in the manifest Word loads, and the
+ // deploy decides where the files land. If those two disagree the add-in 404s, which Word
+ // reports as "this add-in may not load properly" — an error a long way from its cause.
+ string manifest = File.ReadAllText(
+ Path.Combine(RepoRoot(), "src", "SignsOfAI.Word", "manifest.xml"));
+
+ var source = Regex.Match(manifest, @"= 2, $"SourceLocation \"{path}\" has no subdirectory to compare.");
+ Assert.Equal(PaneDirectory(), parts[^2]);
+ }
+}