diff --git a/frontend/scripts/check-runtime-bindings.mjs b/frontend/scripts/check-runtime-bindings.mjs
new file mode 100644
--- /dev/null
+++ b/frontend/scripts/check-runtime-bindings.mjs
@@ -0,0 +1,106 @@
+import { dirname, join, resolve, sep } from "node:path";
+import { fileURLToPath } from "node:url";
+import ts from "typescript";
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
+const configPath = join(root, "apps", "artcraft", "tsconfig.json");
+const galleryPath = join(
+  root,
+  "libs",
+  "components",
+  "gallery-modal",
+  "src",
+  "lib",
+  "gallery-modal.tsx",
+);
+const topBarPath = join(
+  root,
+  "apps",
+  "artcraft",
+  "app",
+  "src",
+  "components",
+  "signaled",
+  "TopBar",
+  "TopBar.tsx",
+);
+const normalizePath = (value) =>
+  resolve(value).split(sep).join("/").toLowerCase();
+const sourceRoots = [
+  join(root, "apps", "artcraft", "app"),
+  join(root, "libs", "components", "gallery-modal"),
+].map((path) => normalizePath(path) + "/");
+const selfTest = process.argv.includes("--self-test");
+
+const { config, error } = ts.readConfigFile(configPath, ts.sys.readFile);
+if (error) {
+  throw new Error(ts.flattenDiagnosticMessageText(error.messageText, "\n"));
+}
+const parsed = ts.parseJsonConfigFileContent(
+  config,
+  ts.sys,
+  dirname(configPath),
+);
+if (parsed.errors.length > 0) {
+  throw new Error(
+    parsed.errors
+      .map((item) => ts.flattenDiagnosticMessageText(item.messageText, "\n"))
+      .join("\n"),
+  );
+}
+
+const host = ts.createCompilerHost(parsed.options);
+if (selfTest) {
+  const readFile = host.readFile.bind(host);
+  host.readFile = (filename) => {
+    const source = readFile(filename);
+    return source !== undefined &&
+      normalizePath(filename) === normalizePath(topBarPath)
+      ? `${source}\nvoid __artcraft_probe_missing__;`
+      : source;
+  };
+}
+
+const program = ts.createProgram({
+  rootNames: [...parsed.fileNames, galleryPath],
+  options: parsed.options,
+  projectReferences: parsed.projectReferences,
+  host,
+});
+const unbound = ts
+  .getPreEmitDiagnostics(program)
+  .filter(
+    (item) =>
+      (item.code === 2304 || item.code === 2552) &&
+      item.file &&
+      sourceRoots.some((sourceRoot) =>
+        normalizePath(item.file.fileName).startsWith(sourceRoot),
+      ),
+  );
+
+if (selfTest) {
+  if (
+    !unbound.some((item) =>
+      ts
+        .flattenDiagnosticMessageText(item.messageText, " ")
+        .includes("__artcraft_probe_missing__"),
+    )
+  ) {
+    throw new Error(
+      "The runtime binding check did not catch a missing identifier",
+    );
+  }
+  console.log("Runtime binding check self-test passed");
+} else if (unbound.length > 0) {
+  for (const item of unbound) {
+    const { line } = item.file.getLineAndCharacterOfPosition(item.start);
+    console.error(
+      `${item.file.fileName}:${line + 1}: TS${item.code} ${ts.flattenDiagnosticMessageText(item.messageText, " ")}`,
+    );
+  }
+  process.exitCode = 1;
+} else {
+  console.log(
+    "ArtCraft runtime bindings verified (full typecheck is separate)",
+  );
+}
diff --git a/script/artcraft/verify_embedded_frontend.mjs b/script/artcraft/verify_embedded_frontend.mjs
new file mode 100644
--- /dev/null
+++ b/script/artcraft/verify_embedded_frontend.mjs
@@ -0,0 +1,54 @@
+import { existsSync, readFileSync, readdirSync } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+import { brotliDecompressSync } from "node:zlib";
+
+// Usage: node verify_embedded_frontend.mjs [exe] [frontend-dist]
+const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
+const distRoot = process.argv[3]
+  ? resolve(process.argv[3])
+  : join(repoRoot, "frontend", "apps", "artcraft", "dist");
+const buildRoot = join(repoRoot, "target", "release", "build");
+const exePath = process.argv[2]
+  ? resolve(process.argv[2])
+  : join(repoRoot, "target", "release", "artcraft.exe");
+
+try {
+  const index = readFileSync(join(distRoot, "index.html"), "utf8");
+  const entry = index.match(/\/assets\/index-[^"'\s]+\.js/)?.[0];
+  if (!entry || !existsSync(join(distRoot, entry.slice(1)))) {
+    throw new Error("Built frontend entry is missing from dist");
+  }
+
+  const exe = readFileSync(exePath);
+  for (const build of readdirSync(buildRoot, { withFileTypes: true })) {
+    if (!build.isDirectory() || !build.name.startsWith("artcraft-")) continue;
+    const assets = join(buildRoot, build.name, "out", "tauri-codegen-assets");
+    if (!existsSync(assets)) continue;
+
+    for (const asset of readdirSync(assets)) {
+      if (!asset.endsWith(".html")) continue;
+      const compressed = readFileSync(join(assets, asset));
+      try {
+        if (
+          !brotliDecompressSync(compressed).toString("utf8").includes(entry)
+        ) {
+          continue;
+        }
+      } catch {
+        continue;
+      }
+      if (exe.includes(compressed)) {
+        console.log(`Verified embedded frontend: ${entry}`);
+        process.exit(0);
+      }
+    }
+  }
+
+  throw new Error(
+    "Release EXE does not embed the current frontend; use cargo tauri build",
+  );
+} catch (error) {
+  console.error(`Embedded frontend verification failed: ${error.message}`);
+  process.exitCode = 1;
+}
diff --git a/script/artcraft/windows_build.ps1 b/script/artcraft/windows_build.ps1
--- a/script/artcraft/windows_build.ps1
+++ b/script/artcraft/windows_build.ps1
@@ -3,6 +3,8 @@
 Write-Host "Building production Artcraft..."
 Write-Host ""
 
+Get-Command node, npm, cargo -ErrorAction Stop | Out-Null
+
 try
 {
     Push-Location -Path ".\frontend"
@@ -13,6 +15,9 @@ try
     Write-Host ""
 
     npm install --verbose
+    if ($LASTEXITCODE -ne 0) {
+        throw "Frontend dependency install failed"
+    }
 }
 finally
 {
@@ -26,8 +31,26 @@ $env:SQLX_OFFLINE="true"
 $env:TAURI_FRONTEND_PATH=".\frontend"
 $env:TAURI_APP_PATH=".\crates\desktop\artcraft"
 
+node ".\frontend\scripts\check-runtime-bindings.mjs"
+if ($LASTEXITCODE -ne 0) {
+    throw "ArtCraft frontend has unresolved runtime identifiers"
+}
+
 # The config file tells Tauri more instructions for the frontend build.
+# Force a fresh artcraft codegen so the embedded frontend cannot be stale.
+cargo clean --release -p artcraft
+if ($LASTEXITCODE -ne 0) {
+    throw "cargo clean failed"
+}
 cargo tauri build --config ".\crates\desktop\artcraft\tauri.conf.json"
+if ($LASTEXITCODE -ne 0) {
+    throw "ArtCraft Tauri build failed"
+}
+
+node ".\script\artcraft\verify_embedded_frontend.mjs"
+if ($LASTEXITCODE -ne 0) {
+    throw "ArtCraft release EXE does not contain the current frontend"
+}
 
 Write-Host ""
 Write-Host "Production Build Done!"  -ForegroundColor green -BackgroundColor white
