diff --git a/web/index.html b/web/index.html
index 6ccda51..e25a163 100644
--- a/web/index.html
+++ b/web/index.html
@@ -19,6 +19,7 @@
"@noble/ciphers/chacha": "/vendor/noble-ciphers-chacha.js",
"@noble/hashes/sha2": "/vendor/noble-hashes-sha2.js",
"@noble/hashes/hmac": "/vendor/noble-hashes-hmac.js",
+ "@noble/hashes/pbkdf2": "/vendor/noble-hashes-pbkdf2.js",
"@noble/hashes/hkdf": "/vendor/noble-hashes-hkdf.js",
"@noble/hashes/utils": "/vendor/noble-hashes-utils.js",
"@xterm/xterm": "/vendor/xterm.js",
diff --git a/web/sw.js b/web/sw.js
index 1ce79ef..3a4219e 100644
--- a/web/sw.js
+++ b/web/sw.js
@@ -53,6 +53,7 @@ const SHELL = [
'/vendor/noble-curves-ed25519.js',
'/vendor/noble-hashes-hkdf.js',
'/vendor/noble-hashes-hmac.js',
+ '/vendor/noble-hashes-pbkdf2.js',
'/vendor/noble-hashes-sha2.js',
'/vendor/noble-hashes-utils.js',
'/vendor/xterm-addon-fit.js',
diff --git a/web/test/importmap.test.js b/web/test/importmap.test.js
new file mode 100644
index 0000000..63034c6
--- /dev/null
+++ b/web/test/importmap.test.js
@@ -0,0 +1,66 @@
+// web/test/importmap.test.js — guards the gap that node tests cannot see: the
+// browser resolves bare module specifiers (`@noble/...`) through the importmap in
+// index.html, NOT node_modules. A specifier imported by app code but missing from
+// the importmap loads fine under `node --test` yet fails at boot in the browser
+// ("Failed to resolve module specifier") — a black screen. This test asserts every
+// bare specifier used under web/src/ is mapped. (Regression guard for the missing
+// @noble/hashes/pbkdf2 mapping that broke the SPA after wallet derivation moved
+// into the sign-in path.)
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync, readdirSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const here = dirname(fileURLToPath(import.meta.url));
+const webRoot = join(here, '..');
+
+function walk(dir) {
+ const out = [];
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
+ const p = join(dir, e.name);
+ if (e.isDirectory()) out.push(...walk(p));
+ else if (e.name.endsWith('.js')) out.push(p);
+ }
+ return out;
+}
+
+// A bare specifier does not start with '.' or '/'. Those are the ones the browser
+// resolves via the importmap; relative paths resolve as URLs directly.
+function bareSpecifiers(src) {
+ const out = new Set();
+ const re = /(?:^|\W)(?:import|export)[^'"]*?from\s*['"]([^'"]+)['"]/g;
+ let m;
+ while ((m = re.exec(src)) !== null) {
+ const spec = m[1];
+ if (!spec.startsWith('.') && !spec.startsWith('/')) out.add(spec);
+ }
+ return out;
+}
+
+test('every bare import under web/src is in the index.html importmap', () => {
+ const html = readFileSync(join(webRoot, 'index.html'), 'utf8');
+ const mapJSON = html.match(/