diff --git a/client/src/routes/player/PlayerPortalLayout.jsx b/client/src/routes/player/PlayerPortalLayout.jsx
index d5134d9..12e9153 100644
--- a/client/src/routes/player/PlayerPortalLayout.jsx
+++ b/client/src/routes/player/PlayerPortalLayout.jsx
@@ -137,7 +137,13 @@ export default function PlayerPortalLayout() {
borderLeft: `2px solid ${isActive ? 'var(--accent)' : 'transparent'}`,
})}
>
-
+ {/* Guarded, like AdminLayout's. `icon` is optional in the nav
+ contract (§3.3) and every CORE row here has always had one, so
+ an unguarded `` was fine right up until a module
+ registered a row without — and then it was not a missing glyph,
+ it was React error #130 and a blank portal. Found by the §7.7
+ browser smoke; no DOM-less test can see it. */}
+ {n.icon && }
{n.label}
))}
diff --git a/server/src/modules/loader.js b/server/src/modules/loader.js
index f44a547..61d3603 100644
--- a/server/src/modules/loader.js
+++ b/server/src/modules/loader.js
@@ -45,7 +45,14 @@ const { splitStatements } = require('../utils/sqlStatements')
const log = require('../utils/logger')('modules')
const REPO_ROOT = path.join(__dirname, '..', '..', '..')
-const MODULES_DIR = process.env.MODULES_DIR || path.join(REPO_ROOT, 'modules')
+// Resolved absolute, and the `path.resolve` is load-bearing rather than tidy.
+// `resolveClient` compares an absolute `path.resolve(dir, entry)` against this
+// directory to check containment, so a RELATIVE `MODULES_DIR` — which is what
+// anyone following §7.7's smoke recipe from `server/` naturally types — makes
+// that comparison fail for every module, with the thoroughly misleading
+// "client.entry escapes the module directory". Found the first time the smoke
+// was run against a module with a real client half.
+const MODULES_DIR = path.resolve(process.env.MODULES_DIR || path.join(REPO_ROOT, 'modules'))
// One segment, lowercase, no parameters. A module prefix that could contain a
// `/` or a `:` would let a module reach outside the slot it was given.