diff --git a/.gitignore b/.gitignore
index 0362e7ec2..6365e9967 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,15 @@ tmp/
 .tmp/
 apps/zync-www/.astro/
 .rb-origin
+
+# Invariantum scan products are transient evidence, never source or remote-build input.
+.invariantum/artifacts/
+.invariantum/logs/
+.invariantum/reports/
+.invariantum/runs/
+.invariantum/triage/
+.invariantum/runner/packages/
+.invariantum/runner/pnpm-lock.yaml
+.invariantum/runner/pnpm-workspace.yaml
+.invariantum/runner/run.sh
+%h/
diff --git a/.invariantum/adapters/zync-auth.mjs b/.invariantum/adapters/zync-auth.mjs
new file mode 100644
index 000000000..84d1af7d3
--- /dev/null
+++ b/.invariantum/adapters/zync-auth.mjs
@@ -0,0 +1,135 @@
+const allPermissions = [
+  'tasks:read', 'tasks:write', 'tasks:delete', 'tasks:assign',
+  'projects:read', 'projects:write', 'projects:delete',
+  'customers:read', 'customers:write', 'customers:delete',
+  'invoices:read', 'invoices:write', 'invoices:delete', 'invoices:send', 'invoices:void',
+  'inventory:read', 'inventory:write', 'inventory:manage',
+  'contracts:read', 'contracts:write', 'contracts:delete',
+  'expenses:read', 'expenses:write', 'expenses:delete', 'expenses:approve',
+  'vendors:read', 'vendors:write', 'permissions:write',
+  'billing:read', 'billing:manage',
+  'time:read', 'time:read_all', 'time:write', 'time:write_all', 'time:track', 'time:manage',
+  'tickets:read', 'tickets:write', 'tickets:delete', 'tickets:assign', 'tickets:resolve',
+  'kb:read', 'kb:write', 'kb:delete', 'kb:share', 'kb:publish',
+  'marketing:read', 'marketing:write',
+  'reports:read', 'reports:write', 'reports:export', 'reports:export_external',
+  'accountant:export',
+  'settings:read', 'settings:write', 'settings:security:read', 'settings:security:write',
+  'settings:modules:read', 'settings:modules:write',
+  'users:read', 'users:invite', 'users:manage', 'users:freeze',
+  'payouts:read', 'payouts:write', 'payouts:manage',
+  'calendar:read', 'calendar:write', 'calendar:connect',
+  'webhooks:read', 'webhooks:manage', 'audit:read',
+]
+
+const permissionsByRole = {
+  OWNER: allPermissions,
+  ADMIN: allPermissions.filter((permission) => !['billing:manage', 'users:manage'].includes(permission)),
+  MEMBER: [
+    'tasks:read', 'tasks:write', 'projects:read', 'contracts:read', 'contracts:write',
+    'time:read', 'time:write', 'time:track', 'kb:read', 'tickets:read', 'tickets:write',
+  ],
+  VIEWER: allPermissions.filter((permission) => permission.endsWith(':read')),
+  CONTRACTOR: ['time:read', 'time:write', 'time:track', 'tasks:read'],
+  ACCOUNTANT: ['reports:read', 'reports:export', 'accountant:export', 'inventory:read'],
+}
+
+const moduleIds = [
+  'tasks', 'projects', 'time_management', 'calendar', 'customers', 'crm', 'marketing',
+  'invoices', 'billing', 'expenses', 'contractor_payouts', 'reports', 'kb', 'ai_assistant',
+]
+
+function fulfillJson(route, body, status = 200) {
+  return route.fulfill({ status, contentType: 'application/json', body: JSON.stringify(body) })
+}
+
+function scanSessionCookies() {
+  const baseUrl = process.env.ZYNC_SCAN_BASE_URL
+  const cookieHeader = process.env.ZYNC_SCAN_SESSION_COOKIE
+  if (!baseUrl || !cookieHeader) throw new Error('ZYNC_SCAN_BASE_URL and ZYNC_SCAN_SESSION_COOKIE are required')
+  const { hostname } = new URL(baseUrl)
+  return cookieHeader.split(/;\s*/).map((part) => {
+    const separator = part.indexOf('=')
+    if (separator < 1) throw new Error('ZYNC_SCAN_SESSION_COOKIE must contain name=value pairs')
+    return { name: part.slice(0, separator), value: part.slice(separator + 1), domain: hostname, path: '/' }
+  })
+}
+
+function createAdapter(role) {
+  return {
+    id: `zync-${role.toLowerCase()}`,
+    async setup(context, cell) {
+      await context.addCookies(scanSessionCookies())
+      await context.route('**/api/auth/me', (route) => fulfillJson(route, {
+        id: `invariantum-${role.toLowerCase()}`,
+        email: `${role.toLowerCase()}@invariantum.invalid`,
+        name: `${role} Invariantum`,
+        avatarUrl: null,
+        tenantId: 'invariantum-tenant',
+        tenantSlug: 'invariantum',
+        tenantName: 'Invariantum Tenant',
+        role,
+        tier: 'enterprise',
+        permissions: permissionsByRole[role],
+        locale: cell.locale,
+        timezone: 'UTC',
+        countryCode: 'US',
+        defaultCurrency: 'USD',
+        onboarding_completed: true,
+        onboarding_step: 5,
+        emailVerified: true,
+        twoFactorEnabled: false,
+      }))
+      await context.route('**/api/auth/memberships', (route) => fulfillJson(route, []))
+      await context.route('**/api/settings/modules', (route) => fulfillJson(route, {
+        modules: moduleIds.map((id) => ({ id, enabled: true })),
+      }))
+      await context.route('**/api/settings/time-tracking', (route) => fulfillJson(route, {
+        time_idle_threshold_minutes: 10,
+        time_rounding: 'none',
+      }))
+      await context.route('**/api/invoices/approvals/count', (route) => fulfillJson(route, { total: 0 }))
+      await context.route('**/api/shell/layout?*', (route) => fulfillJson(route, {
+        ui_shell: null,
+        force_shell: null,
+        payload: { data: { windows: [] } },
+        version: 1,
+      }))
+      await context.route('**/api/zync-subscription', (route) => fulfillJson(route, {
+        id: 'invariantum-subscription',
+        tenantId: 'invariantum-tenant',
+        tier: 'enterprise',
+        status: 'active',
+        period: 'monthly',
+        adapter: 'invariantum',
+        adapterSubscriptionId: null,
+        adapterCustomerId: null,
+        currentPeriodStart: null,
+        currentPeriodEnd: null,
+        trialEndsAt: null,
+        canceledAt: null,
+        gracePeriodStartedAt: null,
+        createdAt: '2026-08-07T00:00:00.000Z',
+        billingPortalUrl: null,
+        usage: { teamMembers: { used: 1, limit: 10 }, activeModules: { used: 14 } },
+        storage: { usedBytes: 0, limitBytes: 1_000_000, pct: 0 },
+      }))
+      return {
+        role,
+        evidence: {
+          source: 'zync-ui-matrix-contracts',
+          tenantId: 'invariantum-tenant',
+          permissionCount: permissionsByRole[role].length,
+          locale: cell.locale,
+        },
+      }
+    },
+  }
+}
+
+export const ownerAuth = createAdapter('OWNER')
+export const adminAuth = createAdapter('ADMIN')
+export const memberAuth = createAdapter('MEMBER')
+export const viewerAuth = createAdapter('VIEWER')
+export const contractorAuth = createAdapter('CONTRACTOR')
+export const accountantAuth = createAdapter('ACCOUNTANT')
diff --git a/.invariantum/config/classic-desktop.config.mjs b/.invariantum/config/classic-desktop.config.mjs
new file mode 100644
index 000000000..43d01a726
--- /dev/null
+++ b/.invariantum/config/classic-desktop.config.mjs
@@ -0,0 +1,11 @@
+import { detectorFamilies, locales, roles, routesByRole } from './coverage-contract.mjs'
+
+export default {
+  id: 'classic-desktop',
+  shell: 'classic',
+  viewport: { name: 'desktop', width: 1440, height: 900 },
+  roles,
+  locales,
+  detectorFamilies,
+  routesByRole,
+}
diff --git a/.invariantum/config/coverage-contract.mjs b/.invariantum/config/coverage-contract.mjs
new file mode 100644
index 000000000..887cca55f
--- /dev/null
+++ b/.invariantum/config/coverage-contract.mjs
@@ -0,0 +1,53 @@
+export const roles = ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER', 'CONTRACTOR', 'ACCOUNTANT']
+
+export const locales = ['en', 'he']
+
+export const detectorFamilies = [
+  'assets',
+  'consistency',
+  'geometry',
+  'interaction',
+  'layout',
+  'relations',
+  'rendered',
+  'sweeps',
+]
+
+export const routesByRole = {
+  OWNER: [
+    '/dashboard', '/projects', '/tasks', '/time-track', '/calendar', '/customers',
+    '/marketing', '/marketing/pipeline', '/marketing/proposals', '/marketing/campaigns',
+    '/marketing/catalogs', '/crm/support', '/invoices', '/invoices/approvals',
+    '/invoices/reconcile', '/receipts', '/invoices/drafts', '/invoices/recurring',
+    '/expenses', '/vendors', '/inventory', '/contractors', '/payouts', '/kb', '/reports',
+    '/reports/analytics', '/reports/vat', '/reports/pnl', '/reports/cashflow',
+    '/reports/advance-tax', '/reports/withholding', '/reports/bituach-leumi',
+    '/reports/uniform-format', '/settings', '/profile',
+  ],
+  ADMIN: [
+    '/dashboard', '/projects', '/tasks', '/time-track', '/calendar', '/customers',
+    '/marketing', '/marketing/pipeline', '/marketing/proposals', '/marketing/campaigns',
+    '/marketing/catalogs', '/crm/support', '/invoices', '/invoices/approvals',
+    '/invoices/reconcile', '/receipts', '/invoices/drafts', '/invoices/recurring',
+    '/expenses', '/vendors', '/inventory', '/contractors', '/payouts', '/kb', '/reports',
+    '/reports/analytics', '/reports/vat', '/reports/pnl', '/reports/cashflow',
+    '/reports/advance-tax', '/reports/withholding', '/reports/bituach-leumi',
+    '/reports/uniform-format', '/settings', '/profile',
+  ],
+  MEMBER: ['/my-work', '/dashboard', '/projects', '/tasks', '/time-track', '/crm/support', '/kb', '/profile'],
+  VIEWER: [
+    '/dashboard', '/projects', '/tasks', '/time-track', '/calendar', '/customers',
+    '/marketing', '/marketing/pipeline', '/marketing/proposals', '/marketing/campaigns',
+    '/marketing/catalogs', '/crm/support', '/invoices', '/receipts', '/invoices/drafts',
+    '/invoices/recurring', '/expenses', '/vendors', '/inventory', '/contractors', '/payouts',
+    '/kb', '/reports', '/reports/analytics', '/reports/vat', '/reports/pnl',
+    '/reports/cashflow', '/reports/advance-tax', '/reports/withholding',
+    '/reports/bituach-leumi', '/reports/uniform-format', '/profile',
+  ],
+  CONTRACTOR: ['/my-work', '/time-track', '/kb', '/profile'],
+  ACCOUNTANT: [
+    '/inventory', '/reports', '/reports/analytics', '/reports/vat', '/reports/pnl',
+    '/reports/cashflow', '/reports/advance-tax', '/reports/withholding',
+    '/reports/bituach-leumi', '/reports/uniform-format', '/profile',
+  ],
+}
diff --git a/.invariantum/config/os-desktop.config.mjs b/.invariantum/config/os-desktop.config.mjs
new file mode 100644
index 000000000..861df4bf4
--- /dev/null
+++ b/.invariantum/config/os-desktop.config.mjs
@@ -0,0 +1,11 @@
+import { detectorFamilies, locales, roles, routesByRole } from './coverage-contract.mjs'
+
+export default {
+  id: 'os-desktop',
+  shell: 'os',
+  viewport: { name: 'desktop', width: 1440, height: 900 },
+  roles,
+  locales,
+  detectorFamilies,
+  routesByRole,
+}
diff --git a/.invariantum/config/os-mobile.config.mjs b/.invariantum/config/os-mobile.config.mjs
new file mode 100644
index 000000000..4a3137c39
--- /dev/null
+++ b/.invariantum/config/os-mobile.config.mjs
@@ -0,0 +1,11 @@
+import { detectorFamilies, locales, roles, routesByRole } from './coverage-contract.mjs'
+
+export default {
+  id: 'os-mobile',
+  shell: 'os',
+  viewport: { name: 'mobile', width: 390, height: 844 },
+  roles,
+  locales,
+  detectorFamilies,
+  routesByRole,
+}
diff --git a/.invariantum/runner/package.json b/.invariantum/runner/package.json
new file mode 100644
index 000000000..44281172e
--- /dev/null
+++ b/.invariantum/runner/package.json
@@ -0,0 +1,22 @@
+{
+  "name": "zync-invariantum-rc-runner",
+  "private": true,
+  "type": "module",
+  "packageManager": "pnpm@10.33.0",
+  "scripts": {
+    "validate": "node ./validate-configs.mjs",
+    "test": "node --test ./run.test.mjs",
+    "scan": "node ./run.mjs"
+  },
+  "dependencies": {
+    "@invariantum/cli": "file:packages/invariantum-cli-0.1.0.tgz",
+    "@invariantum/core": "file:packages/invariantum-core-0.1.0.tgz",
+    "@invariantum/corpus": "file:packages/invariantum-corpus-0.1.0.tgz",
+    "@invariantum/feature": "file:packages/invariantum-feature-0.1.0.tgz",
+    "@invariantum/playwright": "file:packages/invariantum-playwright-0.1.0.tgz",
+    "@invariantum/report": "file:packages/invariantum-report-0.1.0.tgz",
+    "@invariantum/schema": "file:packages/invariantum-schema-0.1.0.tgz",
+    "@invariantum/ui-detectors": "file:packages/invariantum-ui-detectors-0.1.0.tgz",
+    "@invariantum/universal": "file:packages/invariantum-universal-0.1.0.tgz"
+  }
+}
diff --git a/.invariantum/runner/run.mjs b/.invariantum/runner/run.mjs
new file mode 100644
index 000000000..c4b1eef96
--- /dev/null
+++ b/.invariantum/runner/run.mjs
@@ -0,0 +1,491 @@
+import { createHash } from 'node:crypto'
+import { spawn } from 'node:child_process'
+import { mkdir, open, readFile, readdir, rename, stat, writeFile } from 'node:fs/promises'
+import { dirname, relative, resolve } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+
+const here = dirname(fileURLToPath(import.meta.url))
+const root = resolve(here, '../..')
+const reportableStatuses = new Set([0, 1, 4])
+const manifestSchemaVersion = 'zync-invariantum-manifest/v3'
+const ignoredSourceDirectories = new Set([
+  'coverage',
+  'dist',
+  'node_modules',
+  'playwright-report',
+  'test-results',
+  '.turbo',
+])
+const targetSourcePaths = [
+  '.platform/packages',
+  'apps/zync-app',
+  'packages',
+  'package.json',
+  'pnpm-lock.yaml',
+  'pnpm-workspace.yaml',
+  'tsconfig.base.json',
+  'tsconfig.json',
+  'turbo.json',
+]
+
+const configPaths = [
+  resolve(root, '.invariantum/config/classic-desktop.config.mjs'),
+  resolve(root, '.invariantum/config/os-desktop.config.mjs'),
+  resolve(root, '.invariantum/config/os-mobile.config.mjs'),
+]
+const authExports = {
+  OWNER: 'ownerAuth',
+  ADMIN: 'adminAuth',
+  MEMBER: 'memberAuth',
+  VIEWER: 'viewerAuth',
+  CONTRACTOR: 'contractorAuth',
+  ACCOUNTANT: 'accountantAuth',
+}
+const adapterModule = pathToFileURL(resolve(root, '.invariantum/adapters/zync-auth.mjs')).href
+
+function sha256(content) {
+  return createHash('sha256').update(content).digest('hex')
+}
+
+async function hashFiles(paths) {
+  const hash = createHash('sha256')
+  for (const path of [...paths].sort()) {
+    hash.update(path.slice(path.lastIndexOf('/') + 1))
+    hash.update('\0')
+    hash.update(await readFile(path))
+    hash.update('\0')
+  }
+  return hash.digest('hex')
+}
+
+async function collectSourceFiles(path) {
+  let entries
+  try {
+    entries = await readdir(path, { withFileTypes: true })
+  } catch (error) {
+    if (error.code === 'ENOTDIR') return [path]
+    if (error.code === 'ENOENT') return []
+    throw error
+  }
+  const files = await Promise.all(entries
+    .filter((entry) => !entry.isDirectory() || !ignoredSourceDirectories.has(entry.name))
+    .map((entry) => collectSourceFiles(resolve(path, entry.name))))
+  return files.flat()
+}
+
+async function hashTargetSource(targetRoot) {
+  const files = (await Promise.all(
+    targetSourcePaths.map((path) => collectSourceFiles(resolve(targetRoot, path))),
+  )).flat().sort()
+  const hash = createHash('sha256')
+  for (const path of files) {
+    hash.update(relative(targetRoot, path).replaceAll('\\', '/'))
+    hash.update('\0')
+    hash.update(await readFile(path))
+    hash.update('\0')
+  }
+  return hash.digest('hex')
+}
+
+function validateTargetSourceHash(value) {
+  if (!/^[0-9a-f]{64}$/.test(value)) {
+    throw new Error('targetSourceHash must be a lowercase SHA-256 digest')
+  }
+  return value
+}
+
+function withShell(route, shell) {
+  return `${route}${route.includes('?') ? '&' : '?'}shell=${shell}`
+}
+
+function parseMachineOutput(stdout) {
+  let result
+  try {
+    result = JSON.parse(stdout)
+  } catch (error) {
+    throw new Error(`invalid machine JSON: ${error.message}`)
+  }
+  if (result === null || typeof result !== 'object' || Array.isArray(result)) {
+    throw new Error('invalid machine JSON: expected object')
+  }
+  if (typeof result.schemaVersion !== 'string' || result.schemaVersion.length === 0) {
+    throw new Error('invalid machine JSON: schemaVersion is required')
+  }
+  if (typeof result.runId !== 'string' || result.runId.length === 0) {
+    throw new Error('invalid machine JSON: runId is required')
+  }
+  if (!Number.isInteger(result.exitStatus)) {
+    throw new Error('invalid machine JSON: numeric exitStatus is required')
+  }
+  return result
+}
+
+async function validateCompletedRun({ processCode, signal, stdout, reportRoot }) {
+  if (signal !== null) throw new Error(`Invariantum terminated by signal ${signal}`)
+  if (!Number.isInteger(processCode)) throw new Error('Invariantum exited without a numeric status')
+  const result = parseMachineOutput(stdout)
+  if (result.exitStatus !== processCode) {
+    throw new Error(`machine exitStatus ${result.exitStatus} does not match process status ${processCode}`)
+  }
+  if (!reportableStatuses.has(processCode)) {
+    throw new Error(`Invariantum failed with non-reportable status ${processCode}`)
+  }
+  const reportPath = resolve(reportRoot, result.runId, 'index.html')
+  const reportDataPath = resolve(reportRoot, result.runId, 'data/report.json')
+  await Promise.all([stat(reportPath), stat(reportDataPath)]).catch(() => {
+    throw new Error(`missing static report artifacts for run ${result.runId}`)
+  })
+  return { result, reportPath, reportDataPath }
+}
+
+async function spawnCaptured(command, args, { stdoutPath, stderrPath }) {
+  const [stdoutFile, stderrFile] = await Promise.all([
+    open(stdoutPath, 'w'),
+    open(stderrPath, 'w'),
+  ])
+  return new Promise((resolveRun, reject) => {
+    const child = spawn(command, args, {
+      cwd: root,
+      env: { ...process.env, CI: '1', NO_COLOR: '1' },
+      stdio: ['ignore', stdoutFile.fd, stderrFile.fd],
+    })
+    let settled = false
+    const closeFiles = () => Promise.all([stdoutFile.close(), stderrFile.close()])
+    child.once('error', async (error) => {
+      if (settled) return
+      settled = true
+      await closeFiles()
+      reject(error)
+    })
+    child.once('exit', async (code, signal) => {
+      if (settled) return
+      settled = true
+      await closeFiles()
+      resolveRun({ code, signal })
+    })
+  })
+}
+
+async function readReusableEntry(entry, expected) {
+  if (
+    !entry ||
+    entry.configHash !== expected.configHash ||
+    entry.rcPackageHash !== expected.rcPackageHash ||
+    entry.targetSourceHash !== expected.targetSourceHash
+  ) return null
+  try {
+    const stdout = await readFile(entry.resultJson, 'utf8')
+    const validated = await validateCompletedRun({
+      processCode: entry.exitStatus,
+      signal: null,
+      stdout,
+      reportRoot: expected.reportRoot,
+    })
+    if (validated.result.runId !== entry.runId || validated.reportPath !== entry.reportPath) return null
+    await stat(entry.stderrLog)
+    return entry
+  } catch {
+    return null
+  }
+}
+
+async function atomicWriteJson(path, value) {
+  const temporaryPath = `${path}.tmp`
+  await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`)
+  await rename(temporaryPath, path)
+}
+
+async function preflightTopology({ baseUrl, apiBaseUrl, outputRoot, fetchImpl = fetch, headers = {} }) {
+  const probes = [
+    '/api/auth/me',
+    '/api/settings/modules',
+    '/api/calendar/events?start=2026-01-01T00%3A00%3A00.000Z&end=2026-01-02T00%3A00%3A00.000Z',
+  ]
+  const targets = [
+    ['api', apiBaseUrl],
+    ['frontend', baseUrl],
+  ]
+  const results = await Promise.all(probes.flatMap((path) => targets.map(async ([target, origin]) => {
+    const url = new URL(path, origin).href
+    try {
+      const response = await fetchImpl(url, { headers })
+      return { target, path, status: response.status, body: await response.text() }
+    } catch (error) {
+      return { target, path, status: 0, body: String(error) }
+    }
+  })))
+  const failures = results.filter(({ status }) => status < 200 || status >= 400).map(({ target, path, status, body }) => ({ target, path, status, body }))
+  const retainedPath = resolve(outputRoot, '.invariantum/artifacts/results/topology-preflight.json')
+  await mkdir(dirname(retainedPath), { recursive: true })
+  await atomicWriteJson(retainedPath, { probes: results, failures })
+  if (failures.length > 0) throw new Error(`API topology preflight failed; evidence: ${retainedPath}`)
+  return { retainedPath, probes: results }
+}
+
+async function waitForTopology(options, deadline = Date.now() + 30_000) {
+  let failure
+  while (Date.now() < deadline) {
+    try {
+      return await preflightTopology(options)
+    } catch (error) {
+      failure = error
+      await new Promise((resolveWait) => setTimeout(resolveWait, 250))
+    }
+  }
+  throw failure
+}
+
+const topologyShutdownTimeoutMs = 5_000
+
+function waitFor(delayMs) {
+  return new Promise((resolveWait) => setTimeout(resolveWait, delayMs))
+}
+
+async function waitForProcessGroupExit(pid, deadline = Date.now() + topologyShutdownTimeoutMs) {
+  while (Date.now() < deadline) {
+    try {
+      process.kill(-pid, 0)
+    } catch (error) {
+      if (error.code === 'ESRCH') return
+      throw error
+    }
+    await waitFor(50)
+  }
+  throw new Error(`topology process group ${pid} did not exit`)
+}
+
+async function startTopologyService(command, stderrPath) {
+  const stderr = await open(stderrPath, 'w')
+  const child = spawn(command, {
+    cwd: root,
+    detached: true,
+    env: { ...process.env, CI: '1', NO_COLOR: '1' },
+    shell: true,
+    stdio: ['ignore', 'ignore', stderr.fd],
+  })
+  let settled = false
+  let resolveExit
+  const exited = new Promise((resolveExited) => { resolveExit = resolveExited })
+  const settle = (result) => {
+    if (settled) return
+    settled = true
+    stderr.close().catch(() => {}).finally(() => resolveExit(result))
+  }
+  child.once('exit', (code, signal) => settle({ code, signal }))
+  child.once('error', (error) => settle({ error }))
+  return { child, exited }
+}
+
+async function stopTopologyService(service) {
+  if (!service) return
+  const { child, exited } = service
+  if (!Number.isInteger(child.pid)) throw new Error('topology service has no process group')
+  const signalGroup = (signal) => {
+    try {
+      process.kill(-child.pid, signal)
+    } catch (error) {
+      if (error.code !== 'ESRCH') throw error
+    }
+  }
+  signalGroup('SIGTERM')
+  try {
+    await waitForProcessGroupExit(child.pid)
+  } catch (error) {
+    if (!/did not exit/.test(error.message)) throw error
+    signalGroup('SIGKILL')
+    await waitForProcessGroupExit(child.pid)
+  }
+  await exited
+}
+
+async function stopTopology(topology) {
+  const stopped = await Promise.allSettled([
+    stopTopologyService(topology?.api),
+    stopTopologyService(topology?.vite),
+  ])
+  const failure = stopped.find(({ status }) => status === 'rejected')
+  if (failure) throw failure.reason
+}
+
+async function startTopology({ baseUrl, apiBaseUrl, outputRoot, sessionCookie }) {
+  const apiCommand = process.env.ZYNC_SCAN_API_COMMAND
+  const viteCommand = process.env.ZYNC_SCAN_VITE_COMMAND
+  if (!apiCommand || !viteCommand || !sessionCookie) {
+    throw new Error('ZYNC_SCAN_API_COMMAND, ZYNC_SCAN_VITE_COMMAND, and ZYNC_SCAN_SESSION_COOKIE are required')
+  }
+  const logDir = resolve(outputRoot, '.invariantum/logs')
+  await mkdir(logDir, { recursive: true })
+  const api = await startTopologyService(apiCommand, resolve(logDir, 'api.stderr.log'))
+  const vite = await startTopologyService(viteCommand, resolve(logDir, 'vite.stderr.log'))
+  try {
+    await waitForTopology({
+      baseUrl,
+      apiBaseUrl,
+      outputRoot,
+      headers: { cookie: sessionCookie },
+    })
+  } catch (error) {
+    await stopTopology({ api, vite })
+    throw error
+  }
+  return { api, vite }
+}
+
+export async function executeScan({
+  baseUrl,
+  outputRoot = root,
+  spawnRun = spawnCaptured,
+  targetSourceHash,
+  rcPackageHash: suppliedRcPackageHash,
+} = {}) {
+  if (!baseUrl) throw new Error('BASE_URL is required')
+  new URL(baseUrl)
+  if (targetSourceHash !== undefined) validateTargetSourceHash(targetSourceHash)
+
+  const generatedDir = resolve(outputRoot, '.invariantum/artifacts/generated-configs')
+  const resultDir = resolve(outputRoot, '.invariantum/artifacts/results')
+  const logDir = resolve(outputRoot, '.invariantum/logs')
+  const reportRoot = resolve(outputRoot, '.invariantum/reports')
+  const manifestPath = resolve(resultDir, 'manifest.json')
+  const checkpointPath = resolve(resultDir, 'checkpoint.json')
+  await Promise.all([
+    mkdir(generatedDir, { recursive: true }),
+    mkdir(resultDir, { recursive: true }),
+    mkdir(logDir, { recursive: true }),
+  ])
+
+  const validationLog = resolve(logDir, 'validation.log')
+  const validationStdout = resolve(resultDir, 'validation.stdout')
+  const validation = await spawnRun(process.execPath, [resolve(here, 'validate-configs.mjs')], {
+    stdoutPath: validationStdout,
+    stderrPath: validationLog,
+  })
+  if (validation.signal !== null || validation.code !== 0) {
+    throw new Error(`config validation failed with ${validation.signal ?? validation.code}; log: ${validationLog}`)
+  }
+
+  let rcPackageHash = suppliedRcPackageHash
+  if (rcPackageHash === undefined) {
+    let packageNames
+    try {
+      packageNames = await readdir(resolve(here, 'packages'))
+    } catch (error) {
+      if (error?.code === 'ENOENT') throw new Error('Invariantum RC packages are not staged in .invariantum/runner/packages')
+      throw error
+    }
+    const packagePaths = packageNames.filter((name) => name.endsWith('.tgz')).map((name) => resolve(here, 'packages', name))
+    if (packagePaths.length === 0) throw new Error('Invariantum RC packages are not staged in .invariantum/runner/packages')
+    rcPackageHash = await hashFiles(packagePaths)
+  }
+  const sourceHash = targetSourceHash ?? await hashTargetSource(root)
+  let previousEntries = new Map()
+  for (const previousPath of [manifestPath, checkpointPath]) {
+    try {
+      const previous = JSON.parse(await readFile(previousPath, 'utf8'))
+      if (previous.schemaVersion === manifestSchemaVersion && Array.isArray(previous.runs)) {
+        for (const entry of previous.runs) previousEntries.set(`${entry.surface}:${entry.role}`, entry)
+      }
+    } catch {}
+  }
+
+  const runs = []
+  for (const configPath of configPaths) {
+    const surface = (await import(`${pathToFileURL(configPath).href}?scan=${Date.now()}`)).default
+    for (const role of surface.roles) {
+      const slug = `${surface.id}-${role.toLowerCase()}`
+      const generatedPath = resolve(generatedDir, `${slug}.json`)
+      const config = {
+        schemaVersion: 'config/v1',
+        seed: `zync-rc-${slug}`,
+        surfaces: [],
+        ui: {
+          baseUrl,
+          routes: surface.routesByRole[role].map((route) => ({ pattern: withShell(route, surface.shell) })),
+          roles: [role],
+          auth: { adapters: { [role]: { module: adapterModule, export: authExports[role] } } },
+          locales: surface.locales,
+          viewports: [surface.viewport],
+          families: surface.detectorFamilies,
+          timeouts: { navigateMs: 30000, settleMs: 5000 },
+        },
+        report: { mode: 'static', autoOpen: false },
+        acceptedFindings: { store: '.invariantum/decisions' },
+      }
+      const configContent = `${JSON.stringify(config, null, 2)}\n`
+      const configHash = sha256(configContent)
+      await writeFile(generatedPath, configContent)
+      const stdoutPath = resolve(resultDir, `${slug}.json`)
+      const stderrLog = resolve(logDir, `${slug}.log`)
+      const expected = { configHash, rcPackageHash, targetSourceHash: sourceHash, reportRoot }
+      const reusable = await readReusableEntry(previousEntries.get(`${surface.id}:${role}`), expected)
+      if (reusable) {
+        runs.push(reusable)
+        await atomicWriteJson(checkpointPath, { schemaVersion: manifestSchemaVersion, rcPackageHash, targetSourceHash: sourceHash, runs })
+        continue
+      }
+
+      const processResult = await spawnRun(resolve(here, 'node_modules/.bin/invariantum'), [
+        'verify',
+        '--scope=ui',
+        `--config=${generatedPath}`,
+        '--report',
+        '--report-mode=static',
+        '--no-open',
+        '--output=json',
+      ], { stdoutPath, stderrPath: stderrLog })
+      const stdout = await readFile(stdoutPath, 'utf8')
+      const validated = await validateCompletedRun({
+        processCode: processResult.code,
+        signal: processResult.signal,
+        stdout,
+        reportRoot,
+      })
+      runs.push({
+        surface: surface.id,
+        role,
+        config: generatedPath,
+        configHash,
+        rcPackageHash,
+        targetSourceHash: sourceHash,
+        runId: validated.result.runId,
+        exitStatus: validated.result.exitStatus,
+        resultJson: stdoutPath,
+        reportPath: validated.reportPath,
+        stderrLog,
+      })
+      await atomicWriteJson(checkpointPath, { schemaVersion: manifestSchemaVersion, rcPackageHash, targetSourceHash: sourceHash, runs })
+    }
+  }
+  if (runs.length !== 18) throw new Error(`expected 18 runs, received ${runs.length}`)
+  const manifest = { schemaVersion: manifestSchemaVersion, rcPackageHash, targetSourceHash: sourceHash, runs }
+  await atomicWriteJson(manifestPath, manifest)
+  return { manifestPath, manifest }
+}
+
+export const internals = {
+  hashTargetSource,
+  manifestSchemaVersion,
+  parseMachineOutput,
+  preflightTopology,
+  readReusableEntry,
+  startTopology,
+  stopTopology,
+  validateCompletedRun,
+  validateTargetSourceHash,
+}
+
+if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+  const baseUrl = process.env.BASE_URL
+  const apiBaseUrl = process.env.ZYNC_SCAN_API_URL
+  const sessionCookie = process.env.ZYNC_SCAN_SESSION_COOKIE
+  if (!baseUrl || !apiBaseUrl || !sessionCookie) {
+    throw new Error('BASE_URL, ZYNC_SCAN_API_URL, and ZYNC_SCAN_SESSION_COOKIE are required')
+  }
+  process.env.ZYNC_SCAN_BASE_URL = baseUrl
+  const topology = await startTopology({ baseUrl, apiBaseUrl, outputRoot: root, sessionCookie })
+  try {
+    await executeScan({ baseUrl, targetSourceHash: process.env.TARGET_SOURCE_HASH })
+  } finally {
+    await stopTopology(topology)
+  }
+}
diff --git a/.invariantum/runner/run.test.mjs b/.invariantum/runner/run.test.mjs
new file mode 100644
index 000000000..7f66e63b6
--- /dev/null
+++ b/.invariantum/runner/run.test.mjs
@@ -0,0 +1,403 @@
+import assert from 'node:assert/strict'
+import { once } from 'node:events'
+import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
+import { createServer } from 'node:http'
+import { tmpdir } from 'node:os'
+import { dirname, resolve } from 'node:path'
+import test from 'node:test'
+import { executeScan, internals } from './run.mjs'
+
+const tempDirs = []
+
+async function tempRoot() {
+  const path = await mkdtemp(resolve(tmpdir(), 'zync-invariantum-runner-'))
+  tempDirs.push(path)
+  return path
+}
+
+test.afterEach(async () => {
+  await Promise.all(tempDirs.splice(0).map((path) => rm(path, { recursive: true, force: true })))
+})
+
+async function reportFixture(root, runId) {
+  const reportDir = resolve(root, runId)
+  await mkdir(resolve(reportDir, 'data'), { recursive: true })
+  await Promise.all([
+    writeFile(resolve(reportDir, 'index.html'), '<title>report</title>'),
+    writeFile(resolve(reportDir, 'data/report.json'), '{}\n'),
+  ])
+}
+
+function machineResult(runId, exitStatus) {
+  return `${JSON.stringify({ schemaVersion: 'cli-result/v1', runId, exitStatus })}\n`
+}
+
+test('accepts reportable statuses 0, 1, and 4', async () => {
+  const root = await tempRoot()
+  for (const status of [0, 1, 4]) {
+    const runId = `run-${status}`
+    await reportFixture(root, runId)
+    const validated = await internals.validateCompletedRun({
+      processCode: status,
+      signal: null,
+      stdout: machineResult(runId, status),
+      reportRoot: root,
+    })
+    assert.equal(validated.result.exitStatus, status)
+  }
+})
+
+test('rejects config, harness, and review failures 2, 3, and 5', async () => {
+  const root = await tempRoot()
+  for (const status of [2, 3, 5]) {
+    const runId = `run-${status}`
+    await reportFixture(root, runId)
+    await assert.rejects(
+      internals.validateCompletedRun({
+        processCode: status,
+        signal: null,
+        stdout: machineResult(runId, status),
+        reportRoot: root,
+      }),
+      new RegExp(`non-reportable status ${status}`),
+    )
+  }
+})
+
+test('rejects signals and missing numeric process status', async () => {
+  const root = await tempRoot()
+  await assert.rejects(
+    internals.validateCompletedRun({ processCode: null, signal: 'SIGTERM', stdout: '', reportRoot: root }),
+    /terminated by signal SIGTERM/,
+  )
+  await assert.rejects(
+    internals.validateCompletedRun({ processCode: null, signal: null, stdout: '', reportRoot: root }),
+    /without a numeric status/,
+  )
+})
+
+test('rejects malformed and incomplete machine output', () => {
+  assert.throws(() => internals.parseMachineOutput('progress\n{}'), /invalid machine JSON/)
+  assert.throws(() => internals.parseMachineOutput('{}'), /schemaVersion is required/)
+  assert.throws(() => internals.parseMachineOutput('{"schemaVersion":"cli-result/v1"}'), /runId is required/)
+  assert.throws(
+    () => internals.parseMachineOutput('{"schemaVersion":"cli-result/v1","runId":"run","exitStatus":"1"}'),
+    /numeric exitStatus is required/,
+  )
+})
+
+test('rejects machine and process status mismatch', async () => {
+  const root = await tempRoot()
+  await reportFixture(root, 'run-mismatch')
+  await assert.rejects(
+    internals.validateCompletedRun({
+      processCode: 1,
+      signal: null,
+      stdout: machineResult('run-mismatch', 4),
+      reportRoot: root,
+    }),
+    /does not match process status/,
+  )
+})
+
+test('rejects missing static report artifacts', async () => {
+  const root = await tempRoot()
+  await assert.rejects(
+    internals.validateCompletedRun({
+      processCode: 0,
+      signal: null,
+      stdout: machineResult('run-missing', 0),
+      reportRoot: root,
+    }),
+    /missing static report artifacts/,
+  )
+})
+
+test('rejects malformed supplied target source hashes', () => {
+  assert.throws(
+    () => internals.validateTargetSourceHash('not-a-sha256'),
+    /lowercase SHA-256 digest/,
+  )
+})
+
+test('fails closed and retains all failed API topology probe bodies', async () => {
+  const outputRoot = await tempRoot()
+  const responses = new Map([
+    ['http://127.0.0.1:8787/api/auth/me', { status: 200, body: '{"id":"user"}' }],
+    ['http://127.0.0.1:4173/api/auth/me', { status: 200, body: '{"id":"user"}' }],
+    ['http://127.0.0.1:8787/api/settings/modules', { status: 200, body: '{"modules":[]}' }],
+    ['http://127.0.0.1:4173/api/settings/modules', { status: 502, body: 'upstream refused connection' }],
+    ['http://127.0.0.1:8787/api/calendar/events?start=2026-01-01T00%3A00%3A00.000Z&end=2026-01-02T00%3A00%3A00.000Z', { status: 200, body: '{"events":[]}' }],
+    ['http://127.0.0.1:4173/api/calendar/events?start=2026-01-01T00%3A00%3A00.000Z&end=2026-01-02T00%3A00%3A00.000Z', { status: 200, body: '{"events":[]}' }],
+  ])
+  const fetchImpl = async (url) => {
+    const response = responses.get(url)
+    assert.ok(response, `unexpected topology probe: ${url}`)
+    return { ok: response.status >= 200 && response.status < 300, status: response.status, text: async () => response.body }
+  }
+
+  await assert.rejects(
+    internals.preflightTopology({
+      baseUrl: 'http://127.0.0.1:4173',
+      apiBaseUrl: 'http://127.0.0.1:8787',
+      outputRoot,
+      fetchImpl,
+    }),
+    /API topology preflight failed/,
+  )
+
+  const retained = JSON.parse(await readFile(resolve(outputRoot, '.invariantum/artifacts/results/topology-preflight.json'), 'utf8'))
+  assert.equal(retained.probes.length, 6)
+  assert.deepEqual(retained.failures, [{
+    target: 'frontend',
+    path: '/api/settings/modules',
+    status: 502,
+    body: 'upstream refused connection',
+  }])
+})
+
+async function availablePort() {
+  const server = createServer()
+  server.listen(0, '127.0.0.1')
+  await once(server, 'listening')
+  const { port } = server.address()
+  await new Promise((resolveClose, reject) => server.close((error) => error ? reject(error) : resolveClose()))
+  return port
+}
+
+async function assertPortReleased(port) {
+  const server = createServer()
+  server.listen(port, '127.0.0.1')
+  await once(server, 'listening')
+  await new Promise((resolveClose, reject) => server.close((error) => error ? reject(error) : resolveClose()))
+}
+
+test('starts healthy API and proxy, retains diagnostics, then reaps both process trees', async () => {
+  const outputRoot = await tempRoot()
+  const apiPort = await availablePort()
+  const vitePort = await availablePort()
+  const fixture = resolve(outputRoot, 'topology-service.mjs')
+  await writeFile(fixture, `
+import { createServer } from 'node:http'
+const [role, port, upstream] = process.argv.slice(2)
+const respond = (request, response, body) => {
+  if (request.headers.cookie !== 'scan=session') return response.writeHead(401).end('missing session')
+  if (request.headers['x-topology-fail'] === '1') return response.writeHead(502).end(role + ' forced failure')
+  response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ role, path: request.url, cookie: request.headers.cookie }))
+}
+const server = createServer(async (request, response) => {
+  if (role === 'api') return respond(request, response)
+  const upstreamResponse = await fetch(upstream + request.url, { headers: request.headers })
+  response.writeHead(upstreamResponse.status, Object.fromEntries(upstreamResponse.headers))
+  response.end(await upstreamResponse.text())
+})
+server.listen(Number(port), '127.0.0.1', () => console.error(role + ' stderr retained'))
+`)
+  const prior = {
+    ZYNC_SCAN_API_COMMAND: process.env.ZYNC_SCAN_API_COMMAND,
+    ZYNC_SCAN_VITE_COMMAND: process.env.ZYNC_SCAN_VITE_COMMAND,
+    ZYNC_SCAN_SESSION_COOKIE: process.env.ZYNC_SCAN_SESSION_COOKIE,
+  }
+  const apiBaseUrl = `http://127.0.0.1:${apiPort}`
+  const baseUrl = `http://127.0.0.1:${vitePort}`
+  process.env.ZYNC_SCAN_API_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(fixture)} api ${apiPort}`
+  process.env.ZYNC_SCAN_VITE_COMMAND = `${JSON.stringify(process.execPath)} ${JSON.stringify(fixture)} vite ${vitePort} ${apiBaseUrl}`
+  process.env.ZYNC_SCAN_SESSION_COOKIE = 'scan=session'
+  let topology
+  try {
+    topology = await internals.startTopology({ baseUrl, apiBaseUrl, outputRoot, sessionCookie: 'scan=session' })
+    const healthy = JSON.parse(await readFile(resolve(outputRoot, '.invariantum/artifacts/results/topology-preflight.json'), 'utf8'))
+    assert.equal(healthy.failures.length, 0)
+    assert.equal(healthy.probes.length, 6)
+    assert.ok(healthy.probes.every(({ body }) => body.includes('scan=session')))
+    await assert.rejects(
+      internals.preflightTopology({ baseUrl, apiBaseUrl, outputRoot, headers: { cookie: 'scan=session', 'x-topology-fail': '1' } }),
+      /API topology preflight failed/,
+    )
+    const failed = JSON.parse(await readFile(resolve(outputRoot, '.invariantum/artifacts/results/topology-preflight.json'), 'utf8'))
+    assert.equal(failed.failures.length, 6)
+    assert.ok(failed.failures.every(({ status, body }) => status === 502 && body.includes('forced failure')))
+    assert.match(await readFile(resolve(outputRoot, '.invariantum/logs/api.stderr.log'), 'utf8'), /api stderr retained/)
+    assert.match(await readFile(resolve(outputRoot, '.invariantum/logs/vite.stderr.log'), 'utf8'), /vite stderr retained/)
+  } finally {
+    if (topology) await internals.stopTopology(topology)
+    for (const [key, value] of Object.entries(prior)) {
+      if (value === undefined) delete process.env[key]
+      else process.env[key] = value
+    }
+  }
+  await Promise.all([assertPortReleased(apiPort), assertPortReleased(vitePort)])
+})
+
+test('target source hash changes when application source changes', async () => {
+  const root = await tempRoot()
+  const sourcePath = resolve(root, 'apps/zync-app/src/App.tsx')
+  await mkdir(dirname(sourcePath), { recursive: true })
+  await writeFile(sourcePath, 'export const version = 1\n')
+  const before = await internals.hashTargetSource(root)
+  await writeFile(sourcePath, 'export const version = 2\n')
+  const after = await internals.hashTargetSource(root)
+  assert.notEqual(after, before)
+})
+
+test('target source hash ignores generated Turbo cache files', async () => {
+  const root = await tempRoot()
+  const sourcePath = resolve(root, 'apps/zync-app/src/App.tsx')
+  const turboPath = resolve(root, 'apps/zync-app/.turbo/turbo-typecheck.log')
+  await mkdir(dirname(sourcePath), { recursive: true })
+  await mkdir(dirname(turboPath), { recursive: true })
+  await writeFile(sourcePath, 'export const version = 1\n')
+  const before = await internals.hashTargetSource(root)
+  await writeFile(turboPath, 'machine-specific build cache\n')
+  const after = await internals.hashTargetSource(root)
+  assert.equal(after, before)
+})
+
+test('reuse requires complete outputs with matching config, RC, and target hashes', async () => {
+  const root = await tempRoot()
+  const reportRoot = resolve(root, 'reports')
+  const runId = 'run-reuse'
+  const resultJson = resolve(root, 'result.json')
+  const stderrLog = resolve(root, 'stderr.log')
+  await reportFixture(reportRoot, runId)
+  await Promise.all([writeFile(resultJson, machineResult(runId, 1)), writeFile(stderrLog, 'progress\n')])
+  const entry = {
+    configHash: 'config-a',
+    rcPackageHash: 'rc-a',
+    targetSourceHash: 'target-a',
+    runId,
+    exitStatus: 1,
+    resultJson,
+    reportPath: resolve(reportRoot, runId, 'index.html'),
+    stderrLog,
+  }
+  const matching = { configHash: 'config-a', rcPackageHash: 'rc-a', targetSourceHash: 'target-a', reportRoot }
+  assert.equal(await internals.readReusableEntry(entry, matching), entry)
+  assert.equal(
+    await internals.readReusableEntry(entry, { ...matching, targetSourceHash: 'target-b' }),
+    null,
+  )
+  assert.equal(
+    await internals.readReusableEntry(entry, { ...matching, configHash: 'config-b' }),
+    null,
+  )
+  assert.equal(
+    await internals.readReusableEntry(entry, { ...matching, rcPackageHash: 'rc-b' }),
+    null,
+  )
+  await rm(entry.reportPath)
+  assert.equal(await internals.readReusableEntry(entry, matching), null)
+})
+
+test('fails closed when RC package artifacts are not staged', async () => {
+  const outputRoot = await tempRoot()
+  const fakeSpawn = async (_command, args, paths) => {
+    await Promise.all([mkdir(dirname(paths.stdoutPath), { recursive: true }), mkdir(dirname(paths.stderrPath), { recursive: true })])
+    if (args[0].endsWith('validate-configs.mjs')) {
+      await Promise.all([writeFile(paths.stdoutPath, ''), writeFile(paths.stderrPath, '')])
+      return { code: 0, signal: null }
+    }
+    throw new Error('scan process must not start without staged RC packages')
+  }
+  await assert.rejects(
+    executeScan({ baseUrl: 'https://example.test', outputRoot, spawnRun: fakeSpawn }),
+    /RC packages are not staged/,
+  )
+})
+
+test('checkpoints validated runs but never reuses a failed run', async () => {
+  const outputRoot = await tempRoot()
+  const attempts = new Map()
+  let failSecondRun = true
+  const fakeSpawn = async (_command, args, paths) => {
+    await Promise.all([mkdir(dirname(paths.stdoutPath), { recursive: true }), mkdir(dirname(paths.stderrPath), { recursive: true })])
+    if (args[0].endsWith('validate-configs.mjs')) {
+      await Promise.all([writeFile(paths.stdoutPath, ''), writeFile(paths.stderrPath, '')])
+      return { code: 0, signal: null }
+    }
+    const configPath = args.find((arg) => arg.startsWith('--config=')).slice('--config='.length)
+    const config = JSON.parse(await readFile(configPath, 'utf8'))
+    const slug = config.seed.slice('zync-rc-'.length)
+    attempts.set(slug, (attempts.get(slug) ?? 0) + 1)
+    const runId = `run-${slug}`
+    const status = failSecondRun && attempts.size === 2 ? 3 : 0
+    await reportFixture(resolve(outputRoot, '.invariantum/reports'), runId)
+    await Promise.all([
+      writeFile(paths.stdoutPath, machineResult(runId, status)),
+      writeFile(paths.stderrPath, `progress ${slug}\n`),
+    ])
+    return { code: status, signal: null }
+  }
+
+  await assert.rejects(
+    executeScan({ baseUrl: 'https://example.test', outputRoot, spawnRun: fakeSpawn, rcPackageHash: 'test-rc-package-hash' }),
+    /non-reportable status 3/,
+  )
+  const [firstSlug, failedSlug] = attempts.keys()
+  assert.equal(attempts.get(firstSlug), 1)
+  assert.equal(attempts.get(failedSlug), 1)
+
+  failSecondRun = false
+  await executeScan({ baseUrl: 'https://example.test', outputRoot, spawnRun: fakeSpawn, rcPackageHash: 'test-rc-package-hash' })
+  assert.equal(attempts.get(firstSlug), 1)
+  assert.equal(attempts.get(failedSlug), 2)
+})
+
+test('writes deterministic complete 18-run manifest and reuses validated runs', async () => {
+  const outputRoot = await tempRoot()
+  let scanCalls = 0
+  const fakeSpawn = async (_command, args, paths) => {
+    await Promise.all([mkdir(dirname(paths.stdoutPath), { recursive: true }), mkdir(dirname(paths.stderrPath), { recursive: true })])
+    if (args[0].endsWith('validate-configs.mjs')) {
+      await Promise.all([writeFile(paths.stdoutPath, ''), writeFile(paths.stderrPath, '')])
+      return { code: 0, signal: null }
+    }
+    scanCalls += 1
+    const configPath = args.find((arg) => arg.startsWith('--config=')).slice('--config='.length)
+    const config = JSON.parse(await readFile(configPath, 'utf8'))
+    const slug = config.seed.slice('zync-rc-'.length)
+    const runId = `run-${slug}`
+    await reportFixture(resolve(outputRoot, '.invariantum/reports'), runId)
+    await Promise.all([
+      writeFile(paths.stdoutPath, machineResult(runId, scanCalls % 3 === 0 ? 4 : scanCalls % 2)),
+      writeFile(paths.stderrPath, `progress ${slug}\n`),
+    ])
+    return { code: scanCalls % 3 === 0 ? 4 : scanCalls % 2, signal: null }
+  }
+
+  const targetSourceHash = 'a'.repeat(64)
+  const first = await executeScan({ baseUrl: 'https://example.test', outputRoot, spawnRun: fakeSpawn, targetSourceHash, rcPackageHash: 'test-rc-package-hash' })
+  assert.equal(first.manifest.runs.length, 18)
+  assert.deepEqual(
+    first.manifest.runs.map(({ surface, role }) => `${surface}:${role}`),
+    [...first.manifest.runs.map(({ surface, role }) => `${surface}:${role}`)].sort((a, b) => {
+      const surfaceOrder = ['classic-desktop', 'os-desktop', 'os-mobile']
+      const [surfaceA] = a.split(':')
+      const [surfaceB] = b.split(':')
+      return surfaceOrder.indexOf(surfaceA) - surfaceOrder.indexOf(surfaceB)
+    }),
+  )
+  assert.equal(first.manifest.targetSourceHash, targetSourceHash)
+  const checkpoint = JSON.parse(await readFile(resolve(outputRoot, '.invariantum/artifacts/results/checkpoint.json'), 'utf8'))
+  assert.equal(checkpoint.targetSourceHash, targetSourceHash)
+  for (const entry of first.manifest.runs) {
+    for (const key of ['surface', 'role', 'config', 'configHash', 'rcPackageHash', 'targetSourceHash', 'runId', 'exitStatus', 'resultJson', 'reportPath', 'stderrLog']) {
+      assert.ok(Object.hasOwn(entry, key), `${key} missing`)
+    }
+    await Promise.all([stat(entry.resultJson), stat(entry.reportPath), stat(entry.stderrLog)])
+  }
+  assert.equal(scanCalls, 18)
+
+  const second = await executeScan({ baseUrl: 'https://example.test', outputRoot, spawnRun: fakeSpawn, targetSourceHash, rcPackageHash: 'test-rc-package-hash' })
+  assert.equal(scanCalls, 18)
+  assert.deepEqual(second.manifest, first.manifest)
+
+  await executeScan({
+    baseUrl: 'https://example.test',
+    outputRoot,
+    spawnRun: fakeSpawn,
+    targetSourceHash: 'b'.repeat(64),
+    rcPackageHash: 'test-rc-package-hash',
+  })
+  assert.equal(scanCalls, 36)
+})
diff --git a/.invariantum/runner/validate-configs.mjs b/.invariantum/runner/validate-configs.mjs
new file mode 100644
index 000000000..dc4adc516
--- /dev/null
+++ b/.invariantum/runner/validate-configs.mjs
@@ -0,0 +1,82 @@
+import assert from 'node:assert/strict'
+import { readFile, readdir } from 'node:fs/promises'
+import { dirname, resolve } from 'node:path'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+
+const here = dirname(fileURLToPath(import.meta.url))
+const root = resolve(here, '../..')
+const configDir = resolve(root, '.invariantum/config')
+const expectedRoles = ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER', 'CONTRACTOR', 'ACCOUNTANT']
+const expectedLocales = ['en', 'he']
+const expectedSurfaces = ['classic-desktop', 'os-desktop', 'os-mobile']
+const expectedFamilies = ['assets', 'consistency', 'geometry', 'interaction', 'layout', 'relations', 'rendered', 'sweeps']
+const configFiles = (await readdir(configDir))
+  .filter((name) => name.endsWith('.config.mjs'))
+  .sort()
+
+assert.equal(configFiles.length, 3, 'exactly three surface configs required')
+const configs = await Promise.all(configFiles.map(async (name) =>
+  (await import(pathToFileURL(resolve(configDir, name)).href)).default))
+assert.deepEqual(configs.map(({ id }) => id).sort(), expectedSurfaces)
+
+const contractsSource = await readFile(resolve(root, 'apps/zync-app/tests/ui-matrix/contracts.ts'), 'utf8')
+const quotedRoutes = (source) => [...source.matchAll(/'((?:\/[a-z0-9-]+)+)'/g)].map((match) => match[1])
+const canonicalMatch = contractsSource.match(/CANONICAL_NAV_ROUTES = \[([\s\S]*?)\] as const/)
+assert.ok(canonicalMatch, 'canonical navigation routes contract missing')
+const canonicalRoutes = quotedRoutes(canonicalMatch[1])
+const roleBlock = (role, nextRole) => {
+  const end = nextRole === null ? '\\] as const' : `role: '${nextRole}'`
+  const match = contractsSource.match(new RegExp(`role: '${role}'([\\s\\S]*?)${end}`))
+  assert.ok(match, `${role}: navigation contract missing`)
+  const routesMatch = match[1].match(/expectedRoutes: \[([\s\S]*?)\]/)
+  assert.ok(routesMatch, `${role}: literal expectedRoutes missing`)
+  return quotedRoutes(routesMatch[1])
+}
+const ownerRoutes = [...canonicalRoutes, '/settings', '/profile']
+const authority = {
+  OWNER: ownerRoutes,
+  ADMIN: ownerRoutes,
+  MEMBER: roleBlock('MEMBER', 'VIEWER'),
+  VIEWER: ownerRoutes.filter((route) => !['/invoices/approvals', '/invoices/reconcile', '/settings'].includes(route)),
+  CONTRACTOR: roleBlock('CONTRACTOR', 'ACCOUNTANT'),
+  ACCOUNTANT: roleBlock('ACCOUNTANT', null),
+}
+const canonical = new Set([...canonicalRoutes, '/my-work', '/settings', '/profile'])
+let routeRolePairsPerSurface = null
+
+for (const config of configs) {
+  assert.deepEqual(config.roles, expectedRoles, `${config.id}: roles`)
+  assert.deepEqual(config.locales, expectedLocales, `${config.id}: locales`)
+  assert.deepEqual([...config.detectorFamilies].sort(), expectedFamilies, `${config.id}: detector families`)
+  assert.equal(config.shell, config.id === 'classic-desktop' ? 'classic' : 'os', `${config.id}: shell`)
+  assert.equal(config.viewport.name, config.id === 'os-mobile' ? 'mobile' : 'desktop', `${config.id}: viewport`)
+
+  let pairs = 0
+  for (const role of expectedRoles) {
+    const routes = config.routesByRole[role]
+    assert.ok(Array.isArray(routes) && routes.length > 0, `${config.id}/${role}: routes required`)
+    assert.equal(new Set(routes).size, routes.length, `${config.id}/${role}: duplicate route`)
+    assert.deepEqual(routes, authority[role], `${config.id}/${role}: applicable routes differ from UI matrix authority`)
+    for (const route of routes) assert.ok(canonical.has(route), `${config.id}/${role}: noncanonical route ${route}`)
+    pairs += routes.length
+  }
+  routeRolePairsPerSurface ??= pairs
+  assert.equal(pairs, routeRolePairsPerSurface, `${config.id}: route-role pair count drift`)
+}
+
+const packageJson = JSON.parse(await readFile(resolve(here, 'package.json'), 'utf8'))
+const tarballs = (await readdir(resolve(here, 'packages'))).filter((name) => name.endsWith('.tgz')).sort()
+const localDependencies = Object.values(packageJson.dependencies)
+assert.equal(localDependencies.length, tarballs.length, 'every staged tarball must be installed')
+assert.ok(localDependencies.every((value) => value.startsWith('file:packages/')), 'all Invariantum dependencies must be local tarballs')
+for (const tarball of tarballs) assert.ok(localDependencies.includes(`file:packages/${tarball}`), `missing local tarball dependency: ${tarball}`)
+
+const cells = routeRolePairsPerSurface * expectedLocales.length * configs.length
+process.stdout.write(JSON.stringify({
+  surfaces: configs.length,
+  rolesPerSurface: expectedRoles.length,
+  localesPerSurface: expectedLocales.length,
+  detectorFamilies: expectedFamilies.length,
+  routeRolePairsPerSurface,
+  totalCells: cells,
+}) + '\n')
diff --git a/apps/zync-app/package.json b/apps/zync-app/package.json
index 4c0fa00c7..9c73a6a05 100644
--- a/apps/zync-app/package.json
+++ b/apps/zync-app/package.json
@@ -54,7 +54,7 @@
     "@zync/calendar": "workspace:*",
     "@zync/expenses": "workspace:*",
     "@zync/modules": "workspace:*",
-    "@zync/os-shell": "file:../../packages/os-shell",
+    "@zync/os-shell": "workspace:*",
     "@zync/payments": "workspace:*",
     "@zync/public-api": "workspace:*",
     "@zync/realtime": "workspace:*",
@@ -104,6 +104,7 @@
     "vite-plugin-pwa": "^1.3.0",
     "wrangler": "^4.0.0",
     "otplib": "^13.4.1",
-    "smol-toml": "^1.7.1"
+    "smol-toml": "^1.7.1",
+    "jsdom": "^26.1.0"
   }
 }
diff --git a/apps/zync-app/src/features/expenses/ExpensesPage.tsx b/apps/zync-app/src/features/expenses/ExpensesPage.tsx
index 1aa18c60f..cf8b8303a 100644
--- a/apps/zync-app/src/features/expenses/ExpensesPage.tsx
+++ b/apps/zync-app/src/features/expenses/ExpensesPage.tsx
@@ -156,7 +156,7 @@ export function ExpensesPage() {
           {/* Bulk action toolbar */}
           <div className="flex items-center gap-4 px-6 py-2 border-b border-line bg-surface">
             <Button size="sm" variant="outline" onClick={handleEvaluateAllPending}>
-              {t('expenses.bulk.evaluateAllPending')}
+              {t('expenses.bulk.evaluateAll')}
             </Button>
           </div>
 
diff --git a/apps/zync-app/src/features/invoices/InvoiceApprovalSortControl.tsx b/apps/zync-app/src/features/invoices/InvoiceApprovalSortControl.tsx
new file mode 100644
index 000000000..64d147583
--- /dev/null
+++ b/apps/zync-app/src/features/invoices/InvoiceApprovalSortControl.tsx
@@ -0,0 +1,29 @@
+import * as React from 'react'
+import { Select } from '@zync/ui'
+import { useTranslation } from 'react-i18next'
+
+export type InvoiceApprovalSort = 'oldest' | 'newest' | 'amount' | 'customer'
+
+interface InvoiceApprovalSortControlProps {
+  value: InvoiceApprovalSort
+  onValueChange: (value: InvoiceApprovalSort) => void
+}
+
+export function InvoiceApprovalSortControl({ value, onValueChange }: InvoiceApprovalSortControlProps) {
+  const { t } = useTranslation()
+  const options = [
+    { value: 'oldest', label: t('invoices.approvals.sort.oldest') },
+    { value: 'newest', label: t('invoices.approvals.sort.newest') },
+    { value: 'amount', label: t('invoices.approvals.sort.amount') },
+    { value: 'customer', label: t('invoices.approvals.sort.customer') },
+  ]
+
+  return (
+    <Select
+      value={value}
+      onValueChange={(nextValue) => onValueChange(nextValue as InvoiceApprovalSort)}
+      options={options}
+      aria-label={t('invoices.approvals.sortLabel')}
+    />
+  )
+}
diff --git a/apps/zync-app/src/lib/realtime/client.ts b/apps/zync-app/src/lib/realtime/client.ts
index d3dbc05e6..5e5912e2e 100644
--- a/apps/zync-app/src/lib/realtime/client.ts
+++ b/apps/zync-app/src/lib/realtime/client.ts
@@ -11,7 +11,7 @@
  *   secondary tabs receive events forwarded by the primary via the channel.
  *   Primary re-elected on beforeunload or if heartbeat times out (5s).
  *
- * Connection URL: wss://{host}/api/realtime/connect — same-origin, so the HttpOnly
+ * Connection URL: ws(s)://{host}/api/realtime/connect — same-origin, so the HttpOnly
  * session cookie is sent automatically on the WebSocket handshake (no token in
  * query). The connect route authenticates the cookie before proxying to the DO.
  */
@@ -24,6 +24,10 @@ const CHANNEL_NAME = 'zync:ws'
 const HEARTBEAT_INTERVAL_MS = 4_000
 const HEARTBEAT_TIMEOUT_MS = 5_000
 
+export function realtimeWebSocketUrl({ protocol, host }: Pick<Location, 'protocol' | 'host'>): string {
+  return `${protocol === 'https:' ? 'wss' : 'ws'}://${host}/api/realtime/connect`
+}
+
 export class RealtimeClient {
   private ws: WebSocket | null = null
   private handlers = new Map<string, Set<Handler>>()
@@ -87,8 +91,7 @@ export class RealtimeClient {
   }
 
   private openWebSocket(): void {
-    const url = `wss://${window.location.host}/api/realtime/connect`
-    this.ws = new WebSocket(url)
+    this.ws = new WebSocket(realtimeWebSocketUrl(window.location))
 
     this.ws.addEventListener('message', (e: MessageEvent) => {
       const event = JSON.parse(e.data as string) as RealtimeEvent
diff --git a/apps/zync-app/src/modules/marketing.test.ts b/apps/zync-app/src/modules/marketing.test.ts
new file mode 100644
index 000000000..3281bb7da
--- /dev/null
+++ b/apps/zync-app/src/modules/marketing.test.ts
@@ -0,0 +1,15 @@
+import { describe, expect, it } from 'vitest'
+import { marketingOverviewRedirect } from './marketing'
+
+describe('marketing index redirect', () => {
+  it('preserves the complete query string when canonicalizing to overview', () => {
+    expect(marketingOverviewRedirect('?shell=classic&redirectProbe=marketing')).toBe(
+      'overview?shell=classic&redirectProbe=marketing',
+    )
+  })
+
+  it('does not synthesize an empty query marker', () => {
+    expect(marketingOverviewRedirect('')).toBe('overview')
+    expect(marketingOverviewRedirect('?')).toBe('overview')
+  })
+})
diff --git a/apps/zync-app/src/modules/marketing.tsx b/apps/zync-app/src/modules/marketing.tsx
index 2a1d8c1ff..cd8447565 100644
--- a/apps/zync-app/src/modules/marketing.tsx
+++ b/apps/zync-app/src/modules/marketing.tsx
@@ -5,7 +5,7 @@
  * email-marketing-sequences (wave-10 leaf6): email sequences + sequence detail.
  */
 import { lazy, Suspense } from 'react'
-import { Routes, Route, Navigate } from 'react-router'
+import { Routes, Route, Navigate, useLocation } from 'react-router'
 import { ModuleLoadingSkeleton } from '../components/ModuleLoadingSkeleton'
 
 const OverviewPage = lazy(() => import('../routes/marketing/overview'))
@@ -23,10 +23,19 @@ const CampaignsPage      = lazy(() => import('../routes/marketing/campaigns'))
 const SequencesPage      = lazy(() => import('../routes/marketing/sequences'))
 const SequenceDetailPage = lazy(() => import('../routes/marketing/sequences.$id'))
 
+export function marketingOverviewRedirect(search: string): string {
+  return search && search !== '?' ? `overview${search}` : 'overview'
+}
+
+export function MarketingOverviewRedirect() {
+  const location = useLocation()
+  return <Navigate to={marketingOverviewRedirect(location.search)} replace />
+}
+
 export default function Module() {
   return (
     <Routes>
-      <Route index element={<Navigate to="overview" replace />} />
+      <Route index element={<MarketingOverviewRedirect />} />
       <Route
         path="overview"
         element={
diff --git a/apps/zync-app/src/os/__tests__/desktop-mounted-components.test.tsx b/apps/zync-app/src/os/__tests__/desktop-mounted-components.test.tsx
index c0c744c47..2dfcc5fbf 100644
--- a/apps/zync-app/src/os/__tests__/desktop-mounted-components.test.tsx
+++ b/apps/zync-app/src/os/__tests__/desktop-mounted-components.test.tsx
@@ -23,6 +23,29 @@ afterEach(() => {
   }
 })
 
+describe('mounted desktop icons', () => {
+  it.each(['ltr', 'rtl'] as const)('wraps long labels within the production cell in %s', async (dir) => {
+    const container = document.createElement('div')
+    const root = createRoot(container)
+    mounts.push({ root, container })
+
+    await act(async () => root.render(
+      <Desktop
+        apps={[{ id: 'tasks', displayName: 'Notifications' }]}
+        desktopIcons={[{ moduleId: 'tasks', cell: 0 }]}
+        dir={dir}
+        onOpen={vi.fn()}
+        onMove={vi.fn()}
+      />,
+    ))
+
+    const label = container.querySelector<HTMLElement>('[data-desktop-icon="Notifications"] > span:last-child')
+    expect(label?.className).toContain('min-w-0')
+    expect(label?.className).toContain('w-full')
+    expect(label?.className).toContain('break-words')
+  })
+})
+
 describe('mounted desktop context menus', () => {
   it('mounts a shared blank-desktop menu with real open actions', async () => {
     const onOpen = vi.fn()
@@ -80,8 +103,12 @@ describe('mounted desktop context menus', () => {
 
     await act(async () => {
       icon?.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0, clientX: 100, clientY: 100 }))
+      icon?.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, button: 0, clientX: 100, clientY: 100 }))
     })
+    expect(onMove).not.toHaveBeenCalled()
+
     await act(async () => {
+      icon?.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true, button: 0, clientX: 96, clientY: 96 }))
       icon?.dispatchEvent(new MouseEvent('pointerup', { bubbles: true, button: 0, clientX: 100, clientY: 100 }))
     })
     expect(onMove).toHaveBeenCalledWith('tasks', 11)
diff --git a/apps/zync-app/src/os/__tests__/os-shell-store.test.ts b/apps/zync-app/src/os/__tests__/os-shell-store.test.ts
index 04b5665b1..4a124c0da 100644
--- a/apps/zync-app/src/os/__tests__/os-shell-store.test.ts
+++ b/apps/zync-app/src/os/__tests__/os-shell-store.test.ts
@@ -63,6 +63,17 @@ describe('OS shell store', () => {
     expect(state.focusedId).toBe(state.windows.at(-1)?.instanceId)
   })
 
+  it('preserves a single-instance deep route when reopened without an explicit location', () => {
+    store.getState().openWindow('tasks', { pathname: '/tasks/42', search: '?tab=notes', hash: '#item' })
+    const instanceId = store.getState().windows[0]!.instanceId
+
+    store.getState().openWindow('tasks')
+
+    expect(store.getState().windows).toEqual([
+      expect.objectContaining({ instanceId, location: { pathname: '/tasks/42', search: '?tab=notes', hash: '#item' } }),
+    ])
+  })
+
   it('allows multiple windows only for modules declared multi-instance', () => {
     store.getState().openWindow('customers')
     store.getState().openWindow('customers')
@@ -338,7 +349,7 @@ describe('OS shell store', () => {
 describe('manifest-driven window sizing', () => {
   it('uses the manifest route and minimum dimensions for a new window', () => {
     expect(getWindowMinSize('customers')).toEqual({ w: 720, h: 480 })
-    expect(getInitialWindowRect('customers', { w: 1280, h: 800 })).toEqual({ x: 96, y: 72, w: 800, h: 560 })
+    expect(getInitialWindowRect('customers', { w: 1280, h: 800 })).toEqual({ x: 104, y: 72, w: 800, h: 560 })
 
     const sizedStore = createOsShellStore({
       supportsMultipleInstances: () => false,
@@ -347,7 +358,7 @@ describe('manifest-driven window sizing', () => {
     sizedStore.getState().openWindow('today')
     expect(sizedStore.getState().windows[0]).toMatchObject({
       location: { pathname: '/dashboard' },
-      rect: { x: 96, y: 72, w: 800, h: 560 },
+      rect: { x: 104, y: 72, w: 800, h: 560 },
     })
   })
 
diff --git a/apps/zync-app/src/os/__tests__/registry-selectors.test.tsx b/apps/zync-app/src/os/__tests__/registry-selectors.test.tsx
new file mode 100644
index 000000000..ecc1cbf9f
--- /dev/null
+++ b/apps/zync-app/src/os/__tests__/registry-selectors.test.tsx
@@ -0,0 +1,38 @@
+// @vitest-environment jsdom
+import * as React from 'react'
+import { act } from 'react'
+import { createRoot } from 'react-dom/client'
+import { afterEach, expect, it } from 'vitest'
+import { useOsApps, type OsApp } from '../registry-selectors'
+
+declare global { var IS_REACT_ACT_ENVIRONMENT: boolean | undefined }
+
+globalThis.IS_REACT_ACT_ENVIRONMENT = true
+
+let root: ReturnType<typeof createRoot> | undefined
+let node: HTMLDivElement | undefined
+
+afterEach(() => {
+  act(() => root?.unmount())
+  node?.remove()
+  root = undefined
+  node = undefined
+})
+
+it('keeps selected OS apps stable across parent renders with unchanged permissions', () => {
+  const observed: Array<readonly OsApp[]> = []
+
+  function Probe({ tick }: { tick: number }) {
+    observed.push(useOsApps(new Set(['tasks:read']), 'OWNER'))
+    return <output>{tick}</output>
+  }
+
+  node = document.createElement('div')
+  document.body.appendChild(node)
+  root = createRoot(node)
+  act(() => root!.render(<Probe tick={1} />))
+  act(() => root!.render(<Probe tick={2} />))
+
+  expect(observed).toHaveLength(2)
+  expect(observed[1]).toBe(observed[0])
+})
diff --git a/apps/zync-app/src/os/__tests__/window-frame.test.tsx b/apps/zync-app/src/os/__tests__/window-frame.test.tsx
index 6bfb2e3e6..bfba78cef 100644
--- a/apps/zync-app/src/os/__tests__/window-frame.test.tsx
+++ b/apps/zync-app/src/os/__tests__/window-frame.test.tsx
@@ -257,10 +257,46 @@ describe('WindowFrame close confirmation', () => {
 })
 
 describe('WindowFrame interaction handles', () => {
-  it('renders all eight resize handles', () => {
+  it('gives every resize handle a 24px target and keeps edge zones between corners', () => {
     const { container } = renderFrame()
+    const handles = [...container.querySelectorAll<HTMLElement>('[data-window-resize-handle]')]
+
+    expect(handles).toHaveLength(8)
+    for (const handle of handles) {
+      expect(handle.className).toContain('z-20')
+      expect(handle.className).toContain('pointer-events-auto')
+      expect(handle.tagName).toBe('BUTTON')
+      expect(handle.getAttribute('aria-hidden')).toBeNull()
+      expect(handle.getAttribute('aria-label')).toMatch(/^Resize window /)
+      expect(handle.getAttribute('tabindex')).toBeNull()
+    }
+    expect(container.querySelector('[aria-label="Close window"]')?.parentElement?.className).toContain('z-30')
+
+    const getHandle = (handle: string) => container.querySelector<HTMLElement>(`[data-window-resize-handle="${handle}"]`)!
+    expect(getHandle('n').style.cssText).toContain('top: 0px')
+    expect(getHandle('n').style.cssText).toContain('left: 24px')
+    expect(getHandle('n').style.cssText).toContain('right: 24px')
+    expect(getHandle('n').style.cssText).toContain('height: 24px')
+    expect(getHandle('s').style.cssText).toContain('bottom: 0px')
+    expect(getHandle('e').style.cssText).toContain('right: 0px')
+    expect(getHandle('e').style.cssText).toContain('width: 24px')
+    expect(getHandle('w').style.cssText).toContain('left: 0px')
+    expect(getHandle('w').style.cssText).toContain('width: 24px')
+    for (const corner of ['ne', 'nw', 'se', 'sw']) {
+      expect(getHandle(corner).style.width).toBe('24px')
+      expect(getHandle(corner).style.height).toBe('24px')
+    }
+  })
+
+  it('supports keyboard resizing from an accessible edge handle', () => {
+    const { container, onResize, onCommit } = renderFrame()
+    const handle = container.querySelector<HTMLButtonElement>('[data-window-resize-handle="e"]')
+    if (!handle) throw new Error('Missing east resize handle')
 
-    expect(container.querySelectorAll('[data-window-resize-handle]')).toHaveLength(8)
+    act(() => handle.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true })))
+
+    expect(onResize).toHaveBeenCalledWith('tasks-1', expect.objectContaining({ x: 0, y: 0, w: 816, h: 600 }))
+    expect(onCommit).toHaveBeenCalledOnce()
   })
 
   it('routes live left and right drag snaps through the geometry seam', () => {
diff --git a/apps/zync-app/src/os/__tests__/wm-geometry.test.ts b/apps/zync-app/src/os/__tests__/wm-geometry.test.ts
index f0dd733ee..1555cd91f 100644
--- a/apps/zync-app/src/os/__tests__/wm-geometry.test.ts
+++ b/apps/zync-app/src/os/__tests__/wm-geometry.test.ts
@@ -1,6 +1,7 @@
 import { describe, expect, it } from 'vitest'
 import {
   clampWindowRect,
+  DESKTOP_CELL_WIDTH,
   DESKTOP_TASKBAR_INSET_PX,
   dragWindowRect,
   getDesktopViewport,
@@ -93,6 +94,13 @@ describe('window interaction geometry', () => {
       .toEqual({ x: 280, y: 100, w: 320, h: 400 })
   })
 
+  it('keeps initial normal windows clear of first-column desktop icons at 1440x900', () => {
+    const viewport = getDesktopViewport({ w: 1440, h: 900 })
+    const rect = responsiveWindowRect('customers', viewport)
+
+    expect(rect.x).toBeGreaterThanOrEqual(16 + DESKTOP_CELL_WIDTH)
+  })
+
   it('derives responsive initial and minimum sizes from the module manifest', () => {
     expect(windowMinSizeForModule('tasks')).toEqual({ w: 640, h: 480 })
     expect(windowMinSizeForModule('customers')).toEqual({ w: 720, h: 480 })
diff --git a/apps/zync-app/src/os/mobile/MobileHome.test.tsx b/apps/zync-app/src/os/mobile/MobileHome.test.tsx
new file mode 100644
index 000000000..f67e8b5ab
--- /dev/null
+++ b/apps/zync-app/src/os/mobile/MobileHome.test.tsx
@@ -0,0 +1,30 @@
+import { renderToStaticMarkup } from 'react-dom/server'
+import { describe, expect, it } from 'vitest'
+import { MobileHome, type ShellAppDescriptor } from '@zync/os-shell'
+
+const apps = [
+  { appId: 'crm', label: 'Support Center', icon: 'Headphones', navGroup: 'workspace', routing: { routePrefixes: ['/crm'], defaultRoute: '/crm' } },
+  { appId: 'time_management', label: 'Time Tracking', icon: 'Clock', navGroup: 'workspace', routing: { routePrefixes: ['/time-track'], defaultRoute: '/time-track' } },
+  { appId: 'contractor_payouts', label: 'Contractor Payouts', icon: 'Wallet', navGroup: 'workspace', routing: { routePrefixes: ['/contractor-payouts'], defaultRoute: '/contractor-payouts' } },
+  { appId: 'kb', label: 'Knowledge Base', icon: 'BookOpen', navGroup: 'workspace', routing: { routePrefixes: ['/kb'], defaultRoute: '/kb' } },
+] as const satisfies readonly ShellAppDescriptor[]
+
+describe('MobileHome', () => {
+  it('constrains every launcher button and label to its grid track', () => {
+    const markup = renderToStaticMarkup(
+      <MobileHome
+        apps={apps}
+        dock={[]}
+        tenantName="Zync"
+        onOpen={() => {}}
+        onOpenDrawer={() => {}}
+        onOpenCommandCenter={() => {}}
+        onOpenNotifications={() => {}}
+      />,
+    )
+
+    expect(markup.match(/role="gridcell" class="min-w-0"/g)).toHaveLength(apps.length)
+    expect(markup.match(/class="relative flex w-full min-h-11 min-w-0 max-w-full/g)).toHaveLength(apps.length)
+    expect(markup.match(/class="min-w-0 max-w-full truncate"/g)).toHaveLength(apps.length)
+  })
+})
diff --git a/apps/zync-app/src/os/mobile/mobile-surfaces.test.tsx b/apps/zync-app/src/os/mobile/mobile-surfaces.test.tsx
index 04679b519..d0b384e61 100644
--- a/apps/zync-app/src/os/mobile/mobile-surfaces.test.tsx
+++ b/apps/zync-app/src/os/mobile/mobile-surfaces.test.tsx
@@ -4,7 +4,7 @@ import { act } from 'react'
 import { createRoot, type Root } from 'react-dom/client'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import type { ShellAppDescriptor } from '@zync/os-shell'
-import { MobileNavigationBar, MobileShell, MobileTopStrip, NotificationShade, RecentApps } from '@zync/os-shell'
+import { enforceFrameMainOwnership, MobileAppFrame, MobileNavigationBar, MobileShell, MobileTopStrip, NotificationShade, RecentApps } from '@zync/os-shell'
 
 declare global { var IS_REACT_ACT_ENVIRONMENT: boolean | undefined }
 globalThis.IS_REACT_ACT_ENVIRONMENT = true
@@ -13,16 +13,34 @@ const mounted: Array<{ root: Root; node: HTMLDivElement }> = []
 afterEach(() => { while (mounted.length) { const entry = mounted.pop(); if (entry) act(() => entry.root.unmount()) }; document.body.replaceChildren(); vi.clearAllMocks() })
 function render() { const node = document.createElement('div'); const root = createRoot(node); const onOpen = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<MobileShell apps={apps} dock={apps} tenantName="Acme" onOpen={onOpen} onHome={vi.fn()} onBack={vi.fn()} onRecents={vi.fn()} onOpenCommandCenter={vi.fn()} onOpenNotifications={vi.fn()} />)); return { node, onOpen } }
 function click(node: HTMLElement, label: string) { const button = [...node.querySelectorAll('button')].find((item) => item.textContent === label || item.getAttribute('aria-label') === label); if (!button) throw new Error(`Missing ${label}`); act(() => button.click()) }
+function pointerEvent(type: 'pointerdown' | 'pointerup', clientY: number, pointerId: number): Event { const event = new Event(type, { bubbles: true }); Object.defineProperties(event, { clientY: { value: clientY }, pointerId: { value: pointerId } }); return event }
 describe('mobile shell behavioral probes', () => {
   it('opens apps from home icon and dock', () => { const { node, onOpen } = render(); click(node, 'Open Tasks'); click(node, 'Open Customers'); expect(onOpen).toHaveBeenCalledTimes(2); expect(onOpen.mock.calls.map(([app]) => app.appId)).toEqual(['tasks', 'customers']) })
   it('filters drawer entries and opens the selected result', () => { const { node, onOpen } = render(); click(node, 'All apps'); const input = node.querySelector<HTMLInputElement>('input[aria-label="Search apps"]'); expect(input).not.toBeNull(); act(() => input!.value = 'customer'); act(() => input!.dispatchEvent(new Event('input', { bubbles: true }))); expect(node.textContent).toContain('Customers'); click(node, 'Customers'); expect(onOpen).toHaveBeenCalledWith(apps[1], expect.objectContaining({ x: expect.any(Number), y: expect.any(Number) })) })
   it('exposes Back, Home, and Recents as 48px-equivalent button twins', () => { const node = document.createElement('div'); const root = createRoot(node); const onBack = vi.fn(); const onHome = vi.fn(); const onRecents = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<MobileNavigationBar onBack={onBack} onHome={onHome} onRecents={onRecents} />)); click(node, 'Back'); click(node, 'Home'); click(node, 'Recents'); expect(onBack).toHaveBeenCalledOnce(); expect(onHome).toHaveBeenCalledOnce(); expect(onRecents).toHaveBeenCalledOnce(); expect(node.querySelector('[role="toolbar"]')).not.toBeNull(); expect([...node.querySelectorAll('button')].every((button) => button.className.includes('min-h-12'))).toBe(true) })
-  it('dismisses a recent card only after its upward swipe crosses the threshold', () => { const node = document.createElement('div'); const root = createRoot(node); const onDismiss = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<RecentApps open apps={[{ instanceId: 'tasks', moduleId: 'tasks', title: 'Tasks', suspendedAt: null }]} canDiscard={() => ({ ok: true })} onClose={vi.fn()} onOpen={vi.fn()} onDismiss={onDismiss} />)); const card = node.querySelector<HTMLElement>('[data-recent-app="tasks"]')!; Object.defineProperty(card, 'getBoundingClientRect', { value: () => ({ height: 200 }) }); act(() => card.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientY: 180, pointerId: 1 }))); act(() => card.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientY: 130, pointerId: 1 }))); expect(onDismiss).not.toHaveBeenCalled(); act(() => card.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientY: 180, pointerId: 2 }))); act(() => card.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientY: 80, pointerId: 2 }))); expect(onDismiss).toHaveBeenCalledWith('tasks') })
+  it('neutralizes native and explicit descendant main landmarks, including late route content', async () => {
+    const owner = document.createElement('main'); owner.id = 'main-content'; document.body.appendChild(owner)
+    const disconnect = enforceFrameMainOwnership(owner)
+    const nativeMain = document.createElement('main'); nativeMain.id = 'main-content'; nativeMain.textContent = 'Native routed content'
+    const explicitMain = document.createElement('div'); explicitMain.id = 'main-content'; explicitMain.setAttribute('role', 'main'); explicitMain.textContent = 'ARIA routed content'
+    owner.appendChild(nativeMain); owner.appendChild(explicitMain)
+    await new Promise<void>((resolve) => window.setTimeout(resolve, 0))
+    expect(owner.id).toBe('main-content'); expect(nativeMain.id).toBe(''); expect(explicitMain.id).toBe('')
+    expect(nativeMain.getAttribute('role')).toBe('none'); expect(explicitMain.getAttribute('role')).toBe('none'); expect(owner.querySelectorAll('[role="main"]')).toHaveLength(0)
+    explicitMain.id = 'main-content'; await new Promise<void>((resolve) => window.setTimeout(resolve, 0)); expect(explicitMain.id).toBe(''); disconnect()
+  })
+  it('keeps the frame main as the sole accessible main landmark', async () => {
+    const node = document.createElement('div'); const root = createRoot(node); mounted.push({ root, node }); document.body.appendChild(node)
+    act(() => root.render(<MobileAppFrame app={{ instanceId: 'tasks', appId: 'tasks', location: { pathname: '/tasks', search: '', hash: '' }, title: 'Tasks' }} onNavigate={vi.fn()} onBack={vi.fn()} onHome={vi.fn()} onRecents={vi.fn()}><main id="main-content">Native routed content</main><div id="main-content" role="main">ARIA routed content</div></MobileAppFrame>))
+    await new Promise<void>((resolve) => window.setTimeout(resolve, 0))
+    expect(node.querySelectorAll('main#main-content, [role="main"]')).toHaveLength(1); expect(node.querySelector('[data-mobile-frame] > main#main-content')).not.toBeNull()
+  })
+  it('dismisses a recent card only after its upward swipe crosses the threshold', () => { const node = document.createElement('div'); const root = createRoot(node); const onDismiss = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<RecentApps open apps={[{ instanceId: 'tasks', moduleId: 'tasks', title: 'Tasks', suspendedAt: null }]} canDiscard={() => ({ ok: true })} onClose={vi.fn()} onOpen={vi.fn()} onDismiss={onDismiss} />)); const card = node.querySelector<HTMLElement>('[data-recent-app="tasks"]')!; Object.defineProperty(card, 'getBoundingClientRect', { value: () => ({ height: 200 }) }); act(() => card.dispatchEvent(pointerEvent('pointerdown', 180, 1))); act(() => card.dispatchEvent(pointerEvent('pointerup', 130, 1))); expect(onDismiss).not.toHaveBeenCalled(); act(() => card.dispatchEvent(pointerEvent('pointerdown', 180, 2))); act(() => card.dispatchEvent(pointerEvent('pointerup', 80, 2))); expect(onDismiss).toHaveBeenCalledWith('tasks') })
   it('uses a guarded confirmation sheet instead of silently dismissing a blocked recent app', () => { const node = document.createElement('div'); const root = createRoot(node); const onDismiss = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<RecentApps open apps={[{ instanceId: 'tasks', moduleId: 'tasks', title: 'Tasks', suspendedAt: null }]} canDiscard={() => ({ ok: false, reason: 'dirty-form' })} onClose={vi.fn()} onOpen={vi.fn()} onDismiss={onDismiss} />)); click(node, 'Close Tasks'); expect(node.querySelector('[role="dialog"]')?.textContent).toContain('Discard unsaved changes?'); click(node, 'Discard'); expect(onDismiss).toHaveBeenCalledWith('tasks') })
   it('never force-discards a mutation-in-flight app during close-all', () => { const node = document.createElement('div'); const root = createRoot(node); const onDismiss = vi.fn(); const recentApps = [{ instanceId: 'tasks', moduleId: 'tasks', title: 'Tasks', suspendedAt: null }, { instanceId: 'customers', moduleId: 'customers', title: 'Customers', suspendedAt: null }]; mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<RecentApps open apps={recentApps} canDiscard={(instanceId) => instanceId === 'tasks' ? ({ ok: false, reason: 'dirty-form' }) : ({ ok: false, reason: 'mutation-in-flight' })} onClose={vi.fn()} onOpen={vi.fn()} onDismiss={onDismiss} />)); click(node, 'Close all'); click(node, 'Discard'); expect(onDismiss).toHaveBeenCalledWith('tasks'); expect(onDismiss).not.toHaveBeenCalledWith('customers'); expect(node.querySelector('[role="alertdialog"]')?.textContent).toContain('still saving') })
   it('opens and marks shade notifications as read', async () => { const node = document.createElement('div'); const root = createRoot(node); const markAll = vi.fn().mockResolvedValue(undefined); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<NotificationShade open notifications={[{ id: 'n1', title: 'Invoice paid', app: 'Invoices', unread: true }]} onClose={vi.fn()} onMarkAllRead={markAll} onOpenNotification={vi.fn()} />)); expect(node.textContent).not.toContain('Quick settings'); const button = [...node.querySelectorAll('button')].find((item) => item.textContent === 'Mark all read'); await act(async () => { button?.click() }); expect(markAll).toHaveBeenCalledOnce() })
   it('surfaces a mark-read failure instead of creating an unhandled rejection', async () => { const node = document.createElement('div'); const root = createRoot(node); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<NotificationShade open notifications={[{ id: 'n1', title: 'Invoice paid', app: 'Invoices', unread: true }]} onClose={vi.fn()} onMarkAllRead={vi.fn().mockRejectedValue(new Error('offline'))} onOpenNotification={vi.fn()} />)); const button = [...node.querySelectorAll('button')].find((item) => item.textContent === 'Mark all read'); await act(async () => { button?.click() }); expect(node.querySelector('[role="alert"]')?.textContent).toContain('Could not mark notifications as read') })
-  it('opens the shade when a downward pull leaves the top-strip bounds', () => { const node = document.createElement('div'); const root = createRoot(node); const onShade = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<MobileTopStrip tenantName="Acme" onOpenShade={onShade} onOpenCommandCenter={vi.fn()} />)); const strip = node.querySelector<HTMLElement>('[data-mobile-surface="top-strip"]')!; Object.defineProperty(strip, 'setPointerCapture', { value: vi.fn() }); act(() => strip.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientY: 8, pointerId: 3 }))); act(() => strip.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, clientY: 96, pointerId: 3 }))); expect(onShade).toHaveBeenCalledOnce() })
+  it('opens the shade when a downward pull leaves the top-strip bounds', () => { const node = document.createElement('div'); const root = createRoot(node); const onShade = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<MobileTopStrip tenantName="Acme" onOpenShade={onShade} onOpenCommandCenter={vi.fn()} />)); const strip = node.querySelector<HTMLElement>('[data-mobile-surface="top-strip"]')!; Object.defineProperty(strip, 'setPointerCapture', { value: vi.fn() }); act(() => strip.dispatchEvent(pointerEvent('pointerdown', 8, 3))); act(() => strip.dispatchEvent(pointerEvent('pointerup', 96, 3))); expect(onShade).toHaveBeenCalledOnce() })
   it('opens the shade from the top strip and exposes search and notifications button twins', () => { const node = document.createElement('div'); const root = createRoot(node); const onShade = vi.fn(); const onCommand = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<MobileTopStrip tenantName="Acme" notificationCount={2} onOpenShade={onShade} onOpenCommandCenter={onCommand} />)); click(node, 'Search'); click(node, 'Open notifications'); expect(onCommand).toHaveBeenCalledOnce(); expect(onShade).toHaveBeenCalledOnce() })
   it('mounts the production top strip and opens its notification shade', () => { const { node } = render(); expect(node.querySelector('[data-mobile-surface="top-strip"]')).not.toBeNull(); click(node, 'Open notifications'); expect(node.querySelector('[data-mobile-surface="shade"]')).not.toBeNull() })
   it('forwards notification-row activation to the production shell callback', () => { const node = document.createElement('div'); const root = createRoot(node); const onOpenNotification = vi.fn(); mounted.push({ root, node }); document.body.appendChild(node); act(() => root.render(<MobileShell apps={apps} dock={apps} tenantName="Acme" notifications={[{ id: 'n1', title: 'Invoice paid', app: 'Invoices', unread: true }]} onOpen={vi.fn()} onHome={vi.fn()} onBack={vi.fn()} onRecents={vi.fn()} onOpenCommandCenter={vi.fn()} onOpenNotifications={vi.fn()} onOpenNotification={onOpenNotification} />)); click(node, 'Open notifications'); click(node, 'Invoice paid'); expect(onOpenNotification).toHaveBeenCalledWith(expect.objectContaining({ id: 'n1' })) })
diff --git a/apps/zync-app/src/os/registry-os.ts b/apps/zync-app/src/os/registry-os.ts
index 4cae4e269..fc3aacb1d 100644
--- a/apps/zync-app/src/os/registry-os.ts
+++ b/apps/zync-app/src/os/registry-os.ts
@@ -19,6 +19,21 @@ export type OsModuleId = ModuleId | 'module_manager'
 
 export { UNOWNED_MODULE_IDS }
 
+export const UNOWNED_ROUTE_PREFIXES: readonly string[] = [
+  '/auth',
+  '/onboarding',
+  '/portal',
+  '/contractor-portal',
+  '/admin',
+  '/design-system',
+  '/search',
+  '/offline.html',
+]
+
+export const OS_HOST_ROUTE_PREFIXES: ReadonlyArray<readonly [OsModuleId, string]> = [
+  ['invoices', '/inventory'],
+]
+
 export const OS_OWNED_MODULE_IDS = MODULE_MANIFEST
   .filter((module) => module.os)
   .map((module) => module.id)
diff --git a/apps/zync-app/src/os/registry-selectors.ts b/apps/zync-app/src/os/registry-selectors.ts
index 87fae9f17..25bf3baec 100644
--- a/apps/zync-app/src/os/registry-selectors.ts
+++ b/apps/zync-app/src/os/registry-selectors.ts
@@ -1,6 +1,11 @@
+import * as React from 'react'
 import { MODULE_MANIFEST, MODULE_BY_ID, type ModuleId } from '@zync/modules'
 import { useModuleStore } from '../stores/module-store'
-import { OS_MODULE_BINDINGS, type OsModuleId } from './registry-os'
+import {
+  OS_HOST_ROUTE_PREFIXES,
+  OS_MODULE_BINDINGS,
+  type OsModuleId,
+} from './registry-os'
 
 export interface OsAppSurfaceContext {
   enabledModules: ReadonlySet<ModuleId>
@@ -23,9 +28,28 @@ const REQUIRED_PERMISSIONS: Partial<Record<OsModuleId, string>> = {
 
 const OS_APP_METADATA: Readonly<Record<string, Pick<OsApp, 'displayName' | 'category'>>> = { module_manager: { displayName: 'Module Manager', category: 'global' } }
 
-const ROUTE_PREFIXES: ReadonlyArray<readonly [OsModuleId, string]> = MODULE_MANIFEST.flatMap(
-  (module) => module.os?.routing.routePrefixes.map((prefix) => [module.id, prefix] as const) ?? [],
-)
+export type RoutePrefixOwner = readonly [OsModuleId, string]
+
+export function validateRoutePrefixOwners(prefixes: ReadonlyArray<RoutePrefixOwner>): ReadonlyArray<RoutePrefixOwner> {
+  const owners = new Map<string, OsModuleId>()
+  for (const [moduleId, prefix] of prefixes) {
+    const existing = owners.get(prefix)
+    if (existing !== undefined) throw new Error(`OS route prefix ${prefix} has multiple declarations: ${existing}, ${moduleId}`)
+    owners.set(prefix, moduleId)
+  }
+  const inventoryOwner = owners.get('/inventory')
+  if (inventoryOwner !== 'invoices') throw new Error(`OS route prefix /inventory must have exactly the invoices owner; received ${inventoryOwner ?? 'none'}`)
+  return [...owners].map(([prefix, moduleId]) => [moduleId, prefix] as const)
+}
+
+export function collectRoutePrefixes(): ReadonlyArray<RoutePrefixOwner> {
+  return validateRoutePrefixOwners([
+    ...MODULE_MANIFEST.flatMap((module) => module.os?.routing.routePrefixes.map((prefix) => [module.id, prefix] as const) ?? []),
+    ...OS_HOST_ROUTE_PREFIXES,
+  ])
+}
+
+const ROUTE_PREFIXES = collectRoutePrefixes()
 
 function isEnabled(moduleId: OsModuleId, enabledModules: ReadonlySet<ModuleId>): boolean {
   if (moduleId === 'module_manager') return true
@@ -60,8 +84,17 @@ export function selectOsApps(context: OsAppSurfaceContext): readonly OsApp[] {
 
 export function useOsApps(permissions: ReadonlySet<string>, role?: string): readonly OsApp[] {
   const modules = useModuleStore((state) => state.modules)
-  const enabledModules = new Set(Object.keys(MODULE_BY_ID).filter((moduleId) => moduleId === 'system' || modules[moduleId as ModuleId]?.enabled !== false).map((moduleId) => moduleId as ModuleId))
-  return selectOsApps({ enabledModules, permissions, role })
+  const permissionKey = JSON.stringify([...permissions].sort())
+  const enabledModules = React.useMemo(() => new Set(
+    Object.keys(MODULE_BY_ID)
+      .filter((moduleId) => moduleId === 'system' || modules[moduleId as ModuleId]?.enabled !== false)
+      .map((moduleId) => moduleId as ModuleId),
+  ), [modules])
+  const stablePermissions = React.useMemo(() => new Set(JSON.parse(permissionKey) as string[]), [permissionKey])
+  return React.useMemo(
+    () => selectOsApps({ enabledModules, permissions: stablePermissions, role }),
+    [enabledModules, stablePermissions, role],
+  )
 }
 
 export function useOsAppsByCategory(
diff --git a/apps/zync-app/src/os/wm-geometry.ts b/apps/zync-app/src/os/wm-geometry.ts
index 0e9d1996a..8bc407a0a 100644
--- a/apps/zync-app/src/os/wm-geometry.ts
+++ b/apps/zync-app/src/os/wm-geometry.ts
@@ -10,6 +10,9 @@ import {
 export {
   clampWindowRect,
   DEFAULT_WINDOW_MIN_SIZE,
+  DESKTOP_CELL_HEIGHT,
+  DESKTOP_CELL_WIDTH,
+  DESKTOP_GRID_INSET_PX,
   DESKTOP_TASKBAR_INSET_PX,
   dragWindowRect,
   getDesktopViewport,
diff --git a/apps/zync-app/src/pages/invoices/InvoiceDetailPage.tsx b/apps/zync-app/src/pages/invoices/InvoiceDetailPage.tsx
index d70b6778b..eeb802597 100644
--- a/apps/zync-app/src/pages/invoices/InvoiceDetailPage.tsx
+++ b/apps/zync-app/src/pages/invoices/InvoiceDetailPage.tsx
@@ -181,19 +181,19 @@ export function InvoiceDetailPage(): React.ReactElement {
 
   if (isLoading) {
     return (
-      <main id="main-content" className="p-4">
+      <section className="p-4">
         <Stack gap={4}>
           {[1, 2, 3].map((n) => (
             <Stack key={n} className="h-12 animate-pulse rounded bg-hover" />
           ))}
         </Stack>
-      </main>
+      </section>
     )
   }
 
   if (isError || !invoice) {
     return (
-      <main id="main-content" className="p-4">
+      <section className="p-4">
         <Stack gap={4}>
           <Button variant="ghost" size="sm" onClick={() => navigate('/invoices')}>
             <ArrowLeft className="me-2 h-4 w-4" aria-hidden="true" />
@@ -206,7 +206,7 @@ export function InvoiceDetailPage(): React.ReactElement {
             Invoice not found
           </Stack>
         </Stack>
-      </main>
+      </section>
     )
   }
 
@@ -238,7 +238,7 @@ export function InvoiceDetailPage(): React.ReactElement {
     invoice.proformaNumber ?? invoice.invoiceNumber ?? invoice.id
 
   return (
-    <main id="main-content" className="flex flex-col gap-4 p-4 max-w-4xl">
+    <section className="flex flex-col gap-4 p-4 max-w-4xl">
       {/* Toast */}
       {toast && (
         <Stack
@@ -810,6 +810,6 @@ export function InvoiceDetailPage(): React.ReactElement {
           defaultRecipients={resendDefaultRecipients}
         />
       )}
-    </main>
+    </section>
   )
 }
diff --git a/apps/zync-app/src/pages/invoices/InvoiceNewPage.tsx b/apps/zync-app/src/pages/invoices/InvoiceNewPage.tsx
index da082a6a6..650bd7f27 100644
--- a/apps/zync-app/src/pages/invoices/InvoiceNewPage.tsx
+++ b/apps/zync-app/src/pages/invoices/InvoiceNewPage.tsx
@@ -334,20 +334,20 @@ export function InvoiceNewPage(): React.ReactElement {
 
   if (error && hasPrefill) {
     return (
-      <main id="main-content" className="p-4">
+      <section className="p-4">
         <Stack
           role="alert"
           className="rounded border border-danger bg-surface px-4 py-3 text-body-2 text-danger"
         >
           {error}
         </Stack>
-      </main>
+      </section>
     )
   }
 
   if (!hasPrefill && !seed) {
     return (
-      <main id="main-content" className="p-4">
+      <section className="p-4">
         <Stack gap={6} className="max-w-2xl">
           <Stack role="heading" aria-level={1} className="text-title-1 font-medium text-ink">
             Start from
@@ -436,7 +436,7 @@ export function InvoiceNewPage(): React.ReactElement {
             </Button>
           </Stack>
         </Stack>
-      </main>
+      </section>
     )
   }
 
diff --git a/apps/zync-app/src/pages/invoices/InvoicePage.tsx b/apps/zync-app/src/pages/invoices/InvoicePage.tsx
index d8621d83e..614965f09 100644
--- a/apps/zync-app/src/pages/invoices/InvoicePage.tsx
+++ b/apps/zync-app/src/pages/invoices/InvoicePage.tsx
@@ -233,7 +233,7 @@ export function InvoicePage(): React.ReactElement {
   ])
 
   return (
-    <main id="main-content" className="flex flex-col gap-4 p-4">
+    <section className="flex flex-col gap-4 p-4">
       {/* Toast */}
       {toast && (
         <Stack
@@ -459,7 +459,7 @@ export function InvoicePage(): React.ReactElement {
 
       {/* Create / Edit sheet */}
       <InvoiceFormSheet open={sheetOpen} onOpenChange={setSheetOpen} />
-    </main>
+    </section>
   )
 }
 
diff --git a/apps/zync-app/src/pages/reports/TimeReportPage.tsx b/apps/zync-app/src/pages/reports/TimeReportPage.tsx
index 1de082868..cb4cc48f5 100644
--- a/apps/zync-app/src/pages/reports/TimeReportPage.tsx
+++ b/apps/zync-app/src/pages/reports/TimeReportPage.tsx
@@ -69,7 +69,7 @@ export function TimeReportPage({ role }: TimeReportPageProps): React.ReactElemen
   const totals = data?.totals ?? { totalSeconds: 0, billableSeconds: 0, entryCount: 0 }
 
   return (
-    <main id="main-content" className="flex flex-col gap-6 p-4 max-w-6xl mx-auto">
+    <section className="flex flex-col gap-6 p-4 max-w-6xl mx-auto">
       {/* Filter bar */}
       <TimeReportFilters
         filter={filter}
@@ -130,6 +130,6 @@ export function TimeReportPage({ role }: TimeReportPageProps): React.ReactElemen
           </Stack>
         </Tabs.Content>
       </Tabs>
-    </main>
+    </section>
   )
 }
diff --git a/apps/zync-app/src/pages/time/TeamOverviewPage.tsx b/apps/zync-app/src/pages/time/TeamOverviewPage.tsx
index 620535a91..da732a457 100644
--- a/apps/zync-app/src/pages/time/TeamOverviewPage.tsx
+++ b/apps/zync-app/src/pages/time/TeamOverviewPage.tsx
@@ -261,7 +261,7 @@ export function TeamOverviewPage(): React.ReactElement {
   // The overview grid shows member totals; clicking a row opens the detail dialog.
 
   return (
-    <main id="main-content" className="flex flex-col gap-4 p-4">
+    <section className="flex flex-col gap-4 p-4">
       {/* Toolbar */}
       <Stack direction="horizontal" align="center" gap={4} className="flex-wrap">
         <Stack className="text-lg font-semibold text-ink">
@@ -425,6 +425,6 @@ export function TeamOverviewPage(): React.ReactElement {
           onOpenChange={setDialogOpen}
         />
       )}
-    </main>
+    </section>
   )
 }
diff --git a/apps/zync-app/src/pages/time/TimePage.tsx b/apps/zync-app/src/pages/time/TimePage.tsx
index 54950473a..53bbbe592 100644
--- a/apps/zync-app/src/pages/time/TimePage.tsx
+++ b/apps/zync-app/src/pages/time/TimePage.tsx
@@ -143,7 +143,7 @@ export function TimePage(): React.ReactElement {
   }
 
   return (
-    <main id="main-content" className="flex flex-col gap-4 p-4">
+    <section className="flex flex-col gap-4 p-4">
       {/* Toast */}
       {toast && (
         <Stack
@@ -280,6 +280,6 @@ export function TimePage(): React.ReactElement {
         projects={projects}
         tasks={tasks}
       />
-    </main>
+    </section>
   )
 }
diff --git a/apps/zync-app/src/push/usePushOptIn.ts b/apps/zync-app/src/push/usePushOptIn.ts
index 29ae4ff04..b996e3301 100644
--- a/apps/zync-app/src/push/usePushOptIn.ts
+++ b/apps/zync-app/src/push/usePushOptIn.ts
@@ -45,17 +45,18 @@ export function usePushOptIn(): UsePushOptInReturn {
   })
 
   const isSupported = permState !== 'unsupported'
+  const canRegisterServiceWorker = isSupported && !import.meta.env.DEV
 
   // Register service worker on mount
   useEffect(() => {
-    if (!isSupported) return
+    if (!canRegisterServiceWorker) return
     navigator.serviceWorker
       .register(SW_PATH)
       .catch((err) => console.error('[Zync] SW registration failed:', err))
-  }, [isSupported])
+  }, [canRegisterServiceWorker])
 
   const requestPermission = useCallback(async () => {
-    if (!isSupported || permState === 'denied') return
+    if (!canRegisterServiceWorker || permState === 'denied') return
 
     try {
       // 1. Register SW if not already registered
@@ -101,7 +102,7 @@ export function usePushOptIn(): UsePushOptInReturn {
     } catch (err) {
       console.error('[Zync] Push opt-in failed:', err)
     }
-  }, [isSupported, permState])
+  }, [canRegisterServiceWorker, permState])
 
   return { permState, isSupported, requestPermission }
 }
diff --git a/apps/zync-app/src/routes/index.tsx b/apps/zync-app/src/routes/index.tsx
index 7e8fafec9..c5757f07b 100644
--- a/apps/zync-app/src/routes/index.tsx
+++ b/apps/zync-app/src/routes/index.tsx
@@ -100,10 +100,13 @@ const VendorsModule     = lazy(() => import('../modules/vendors'))
 // invoice-receipt-document (wave-12)
 const ReceiptsModule    = lazy(() => import('../modules/receipts'))
 
-/** Redirect /time → /time-track preserving query string (magic-link, notifications). */
-function TimeRedirect() {
+export function preserveRedirectSearch(to: string, search: string): string {
+  return search && search !== '?' ? `${to}${search}` : to
+}
+
+export function PreserveSearchRedirect({ to }: { to: string }) {
   const location = useLocation()
-  return <Navigate to={`/time-track${location.search}`} replace />
+  return <Navigate to={preserveRedirectSearch(to, location.search)} replace />
 }
 
 function withSuspense(Module: React.ComponentType) {
@@ -123,8 +126,8 @@ function withModuleGuard(moduleId: ModuleId, Module: React.ComponentType) {
 }
 
 export const moduleRoutes = [
-  // home-dashboard (P053) — keep /dashboard renderable inside OS window routers; / remains the classic canonical home.
-  { path: 'dashboard',       element: withSuspense(HomePage) },
+  // Keep /dashboard renderable inside OS window routers; / remains the classic canonical home.
+  { path: 'dashboard', element: withSuspense(HomePage) },
   { path: '',                element: withSuspense(HomePage) },
   { path: 'invoices/*',     element: withModuleGuard('invoices', InvoicesModule) },
   // invoice-receipt-document (wave-12)
@@ -155,6 +158,7 @@ export const moduleRoutes = [
   // kb-module
   { path: 'kb/*', element: withModuleGuard('kb', KbModule) },
   // marketing-leads-pipeline (wave-8 leaf5)
+  { path: 'marketing', element: <ModuleGuard moduleId="marketing"><PreserveSearchRedirect to="/marketing/overview" /></ModuleGuard> },
   { path: 'marketing/*', element: withModuleGuard('marketing', MarketingModule) },
   // proposal-editor (wave-11 leaf-C, spec 130) — top-level /proposals/* routes
   { path: 'proposals/*', element: withModuleGuard('marketing', ProposalsModule) },
@@ -170,11 +174,11 @@ export const moduleRoutes = [
   // time-approval-workflow (wave-10 leaf10): specific paths BEFORE time-track/*
   { path: 'time/approvals',        element: withModuleGuard('time_management', TimeApprovalsPage) },
   // time-management: canonical /time alias → /time-track
-  { path: 'time',                  element: <TimeRedirect /> },
+  { path: 'time',                  element: <PreserveSearchRedirect to="/time-track" /> },
   // auth-2fa: forced enrollment (tenant enforce_2fa + user not enrolled)
   { path: 'auth/2fa/setup-required', element: withSuspense(ForcedSetupPage) },
   // notification-center
-  { path: 'profile/notifications', element: <Navigate to="/settings/notifications" replace /> },
+  { path: 'profile/notifications', element: <PreserveSearchRedirect to="/settings/notifications" /> },
   { path: 'notifications', element: withSuspense(NotificationsPage) },
   { path: 'search', element: withSuspense(SearchPage) },
   { path: 'inventory/:itemId', element: withModuleGuard('invoices', InventoryItemHistoryPage) },
diff --git a/apps/zync-app/src/routes/invoices/approvals.tsx b/apps/zync-app/src/routes/invoices/approvals.tsx
index 6c5c8d8ab..ae036f470 100644
--- a/apps/zync-app/src/routes/invoices/approvals.tsx
+++ b/apps/zync-app/src/routes/invoices/approvals.tsx
@@ -19,7 +19,6 @@ import {
   Button,
   Checkbox,
   Input,
-  Select,
   Dialog,
   Badge,
   Spinner,
@@ -34,6 +33,7 @@ import {
 import { ApproveDialog } from '../../features/invoices/ApproveDialog'
 import { RejectSheet } from '../../features/invoices/RejectSheet'
 import { BulkRejectSheet } from '../../features/invoices/BulkRejectSheet'
+import { InvoiceApprovalSortControl } from '../../features/invoices/InvoiceApprovalSortControl'
 
 // ── API helpers ───────────────────────────────────────────────────────────────
 
@@ -194,13 +194,6 @@ export default function InvoiceApprovalsPage() {
     })
   }
 
-  const sortOptions = [
-    { value: 'oldest', label: t('invoices.approvals.sort.oldest') },
-    { value: 'newest', label: t('invoices.approvals.sort.newest') },
-    { value: 'amount', label: t('invoices.approvals.sort.amount') },
-    { value: 'customer', label: t('invoices.approvals.sort.customer') },
-  ]
-
   return (
     <Stack gap={8} className="ps-6 pe-6 pt-8 pb-8 max-w-4xl">
       <Stack direction="horizontal" align="center" gap={4}>
@@ -226,14 +219,7 @@ export default function InvoiceApprovalsPage() {
           aria-label={t('invoices.approvals.searchPlaceholder')}
           className="max-w-xs"
         />
-        <Select
-          value={sort}
-          onValueChange={(v) =>
-            setSort(v as 'oldest' | 'newest' | 'amount' | 'customer')
-          }
-          options={sortOptions}
-          aria-label={t('invoices.approvals.sortLabel')}
-        />
+        <InvoiceApprovalSortControl value={sort} onValueChange={setSort} />
       </Stack>
 
       {selectedIds.size > 0 && (
diff --git a/apps/zync-app/src/shell/Shell.tsx b/apps/zync-app/src/shell/Shell.tsx
index dbaac523a..c1a90c5b7 100644
--- a/apps/zync-app/src/shell/Shell.tsx
+++ b/apps/zync-app/src/shell/Shell.tsx
@@ -95,12 +95,28 @@ export function useShell(): ShellContextValue {
   return ctx
 }
 
+export function getClassicShellGridLayout(dir: 'ltr' | 'rtl', collapsed: boolean): {
+  gridTemplateColumns: string
+  sidebarColumn: 1 | 2
+  mainColumn: 1 | 2
+} {
+  const sidebarWidth = collapsed ? '3.5rem' : '15rem'
+  const sidebarColumn = dir === 'rtl' ? 2 : 1
+
+  return {
+    gridTemplateColumns: dir === 'rtl' ? `1fr ${sidebarWidth}` : `${sidebarWidth} 1fr`,
+    sidebarColumn,
+    mainColumn: sidebarColumn === 1 ? 2 : 1,
+  }
+}
+
 // ── Shell ─────────────────────────────────────────────────────────────────────
 
 export function Shell(): React.ReactElement {
   const location = useLocation()
   const { collapsed, toggleCollapsed } = useSidebarState()
   const { dir } = useLocale()
+  const layout = getClassicShellGridLayout(dir, collapsed)
   const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false)
   // keyboard-shortcuts: activate global shortcuts for protected routes
   useGlobalShortcuts()
@@ -198,9 +214,7 @@ export function Shell(): React.ReactElement {
         className="shell-grid grid h-screen overflow-hidden"
         style={{
           gridTemplateRows: '3.5rem 1fr',
-          gridTemplateColumns: collapsed
-            ? dir === 'rtl' ? '1fr 3.5rem' : '3.5rem 1fr'
-            : dir === 'rtl' ? '1fr 15rem' : '15rem 1fr',
+          gridTemplateColumns: layout.gridTemplateColumns,
         }}
       >
         <SkipLink />
@@ -211,7 +225,7 @@ export function Shell(): React.ReactElement {
         </div>
 
         {/* Sidebar */}
-        <div className="shell-sidebar">
+        <div className="shell-sidebar" style={{ gridRow: 2, gridColumn: layout.sidebarColumn }}>
           <Sidebar />
         </div>
 
@@ -219,6 +233,7 @@ export function Shell(): React.ReactElement {
         <main
           id="main-content"
           className="overflow-auto bg-bg"
+          style={{ gridRow: 2, gridColumn: layout.mainColumn }}
           tabIndex={-1}
         >
           <Suspense fallback={<ModuleSkeleton />}>
diff --git a/apps/zync-app/src/shell/Sidebar.tsx b/apps/zync-app/src/shell/Sidebar.tsx
index fceebaebd..fbe91d273 100644
--- a/apps/zync-app/src/shell/Sidebar.tsx
+++ b/apps/zync-app/src/shell/Sidebar.tsx
@@ -110,7 +110,7 @@ function SidebarNavItem({ item, collapsed, depth = 0, badgeCount }: NavItemProps
   // Hidden: no permission or module disabled
   if (!permOk || !moduleEnabled) return null
 
-  const label = t(item.label, { defaultValue: item.label })
+  const label = t(item.label)
   const isActive = location.pathname === item.to ||
     (item.to !== '/' && item.to !== '/dashboard' && location.pathname.startsWith(item.to))
 
@@ -209,7 +209,7 @@ function InvoicesSubTree({ item, collapsed, expanded, onToggle, pendingApprovals
 
   if (!permOk || !moduleEnabled) return null
 
-  const label = t(item.label, { defaultValue: item.label })
+  const label = t(item.label)
 
   const headerEl = (
     <button
@@ -279,7 +279,7 @@ interface NavGroupProps {
 
 function NavGroup({ group, collapsed, groupExpanded, onToggleGroup, invoicesExpanded, onToggleInvoices, pendingApprovalsCount }: NavGroupProps) {
   const { t } = useTranslation()
-  const label = t(group.label, { defaultValue: group.label })
+  const label = t(group.label)
 
   return (
     <div className="flex flex-col gap-0.5">
diff --git a/apps/zync-app/src/shell/nav-model.test.ts b/apps/zync-app/src/shell/nav-model.test.ts
new file mode 100644
index 000000000..5aeb4ba7c
--- /dev/null
+++ b/apps/zync-app/src/shell/nav-model.test.ts
@@ -0,0 +1,78 @@
+import { readFileSync } from 'node:fs'
+import { fileURLToPath } from 'node:url'
+import { createElement, type ReactNode } from 'react'
+import { renderToStaticMarkup } from 'react-dom/server'
+import i18next from 'i18next'
+import { I18nextProvider } from 'react-i18next'
+import { MemoryRouter } from 'react-router'
+import { describe, expect, it, vi } from 'vitest'
+import enCatalog from '../../../../packages/ui/src/i18n/en.json'
+import heCatalog from '../../../../packages/ui/src/i18n/he.json'
+import { Sidebar } from './Sidebar'
+import { NAV_MODEL, type NavItem } from './nav-model'
+
+vi.mock('../components/ModuleGuard', () => ({ useModuleEnabled: () => true }))
+vi.mock('../hooks/use-tier-gate', () => ({ useTierGate: () => ({ allowed: true, upgrade: () => {} }) }))
+vi.mock('../features/invoices/hooks/usePendingApprovalsCount', () => ({ usePendingApprovalsCount: () => ({ data: undefined }) }))
+vi.mock('./Shell', () => ({
+  useShell: () => ({
+    sidebarCollapsed: false,
+    setSidebarCollapsed: () => {},
+    session: { role: 'MEMBER', permissions: [], tenantSlug: 'test' },
+  }),
+}))
+vi.mock('./use-sidebar-state', () => ({ useSidebarState: () => ({ expandedGroups: {}, toggleGroup: () => {} }) }))
+vi.mock('./use-memberships', () => ({ useMemberships: () => ({ memberships: [] }) }))
+vi.mock('./TenantSwitcher', () => ({ TenantSwitcher: ({ children }: { children: ReactNode }) => children }))
+
+function labels(items: readonly NavItem[]): string[] {
+  return items.flatMap((item) => [item.label, ...labels(item.children ?? [])])
+}
+
+async function renderExpandedSidebar(catalog: Readonly<Record<string, unknown>>): Promise<string> {
+  const i18n = i18next.createInstance()
+  await i18n.init({
+    resources: { test: { translation: catalog } },
+    lng: 'test',
+    fallbackLng: false,
+  })
+
+  return renderToStaticMarkup(
+    createElement(
+      I18nextProvider,
+      { i18n },
+      createElement(MemoryRouter, null, createElement(Sidebar)),
+    ),
+  )
+}
+
+describe('NAV_MODEL locale catalogs', () => {
+  it('does not expose navigation keys as visible fallback labels', () => {
+    const sidebarSource = readFileSync(fileURLToPath(new URL('./Sidebar.tsx', import.meta.url)), 'utf8')
+
+    expect(sidebarSource).not.toContain('defaultValue: item.label')
+    expect(sidebarSource).not.toContain('defaultValue: group.label')
+  })
+
+  it.each([
+    ['en', enCatalog],
+    ['he', heCatalog],
+  ])('renders the expanded Workspace heading in %s', async (_locale, catalog) => {
+    const rendered = await renderExpandedSidebar(catalog)
+
+    expect(rendered).toContain(catalog['nav.group.workspace'])
+    expect(rendered).not.toContain('nav.group.workspace')
+  })
+
+  it.each([
+    ['en', enCatalog],
+    ['he', heCatalog],
+  ])('resolves every group and item label in %s', (_locale, catalog) => {
+    const navLabels = [...NAV_MODEL.map((group) => group.label), ...NAV_MODEL.flatMap((group) => labels(group.items))]
+
+    for (const label of navLabels) {
+      expect(catalog[label as keyof typeof catalog]).toBeTruthy()
+      expect(catalog[label as keyof typeof catalog]).not.toBe(label)
+    }
+  })
+})
diff --git a/apps/zync-app/src/shell/nav-model.ts b/apps/zync-app/src/shell/nav-model.ts
index 69bfca966..3a412023d 100644
--- a/apps/zync-app/src/shell/nav-model.ts
+++ b/apps/zync-app/src/shell/nav-model.ts
@@ -196,7 +196,7 @@ export const NAV_MODEL: NavGroup[] = [
         permission: 'expenses:read',
       },
       {
-        label: 'Inventory',
+        label: 'nav.inventory',
         to: '/inventory',
         icon: 'Table',
         moduleId: 'invoices',
diff --git a/apps/zync-app/src/shell/shell-layout.test.tsx b/apps/zync-app/src/shell/shell-layout.test.tsx
new file mode 100644
index 000000000..01f71a9e0
--- /dev/null
+++ b/apps/zync-app/src/shell/shell-layout.test.tsx
@@ -0,0 +1,95 @@
+// @vitest-environment jsdom
+import * as React from 'react'
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { MemoryRouter, Route, Routes } from 'react-router'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+const shellState = vi.hoisted(() => ({ collapsed: false, dir: 'rtl' as 'rtl' | 'ltr' }))
+
+vi.mock('@tanstack/react-query', () => ({ useQuery: () => ({ data: { id: 'user-1', email: 'member@example.com', name: 'Member', avatarUrl: null, tenantId: 'tenant-1', tenantSlug: 'tenant', tenantName: 'Tenant', role: 'MEMBER', tier: 'PRO', permissions: [], onboarding_completed: true, onboarding_step: 0, emailVerified: true, twoFactorEnabled: false }, isLoading: false }) }))
+vi.mock('@zync/ui', () => ({ SkipLink: () => null, Spinner: () => null }))
+vi.mock('@zync/ui/i18n', () => ({ useLocale: () => ({ dir: shellState.dir }) }))
+vi.mock('./Sidebar', () => ({ Sidebar: () => <nav aria-label="Sidebar" /> }))
+vi.mock('./Header', () => ({ Header: () => <header>Header</header> }))
+vi.mock('./AppSkeleton', () => ({ ModuleSkeleton: () => null }))
+vi.mock('./use-sidebar-state', () => ({ useSidebarState: () => ({ collapsed: shellState.collapsed, toggleCollapsed: vi.fn() }) }))
+vi.mock('../hooks/useGlobalShortcuts', () => ({ useGlobalShortcuts: () => undefined }))
+vi.mock('../components/ImpersonationBanner', () => ({ ImpersonationBanner: () => null }))
+vi.mock('../features/onboarding/OnboardingResumeBanner', () => ({ OnboardingResumeBanner: () => null }))
+vi.mock('../features/onboarding/guard', () => ({ useOnboardingGuardRedirect: () => null }))
+vi.mock('../stores/tenantSettings', () => ({ useTenantSettingsStore: { getState: () => ({ load: () => Promise.resolve() }) } }))
+vi.mock('../components/pwa/MobileSidebarOverlay', () => ({ MobileSidebarOverlay: () => null }))
+vi.mock('../components/pwa/OfflineIndicator', () => ({ OfflineIndicator: () => null }))
+vi.mock('../components/pwa/PwaInstallBanner', () => ({ PwaInstallBanner: () => null }))
+vi.mock('../components/pwa/install-prompt', () => ({ incrementLoginCount: () => undefined, usePwaInstallPrompt: () => ({ showBanner: false, showIosInstructions: false, dismiss: () => undefined, install: () => Promise.resolve() }) }))
+vi.mock('../push/usePushOptIn', () => ({ usePushOptIn: () => ({ requestPermission: () => Promise.resolve() }) }))
+
+import { Shell } from './Shell'
+
+declare global { var IS_REACT_ACT_ENVIRONMENT: boolean | undefined }
+globalThis.IS_REACT_ACT_ENVIRONMENT = true
+
+const mounted: Array<{ root: Root; node: HTMLDivElement }> = []
+afterEach(() => {
+  while (mounted.length) {
+    const entry = mounted.pop()
+    if (entry) act(() => entry.root.unmount())
+  }
+  document.body.replaceChildren()
+  vi.clearAllMocks()
+})
+
+function renderClassicRoute(route: string) {
+  const node = document.createElement('div')
+  const root = createRoot(node)
+  mounted.push({ root, node })
+  document.body.appendChild(node)
+  act(() => root.render(
+    <MemoryRouter initialEntries={[`${route}?shell=classic`]}>
+      <Routes>
+        <Route path="/" element={<Shell />}>
+          <Route path={route.slice(1)} element={<button data-representative-action>{route}</button>} />
+        </Route>
+      </Routes>
+    </MemoryRouter>,
+  ))
+  return node
+}
+
+function layoutRects(grid: HTMLElement, main: HTMLElement, sidebar: HTMLElement) {
+  const columns = grid.style.gridTemplateColumns.split(' ')
+  const sidebarWidth = Number.parseFloat(columns[1] ?? '0') * 16
+  const mainWidth = 1440 - sidebarWidth
+  const sidebarRect = { left: mainWidth, right: 1440, width: sidebarWidth, top: 56, bottom: 900, height: 844 }
+  const mainRect = { left: 0, right: mainWidth, width: mainWidth, top: 56, bottom: 900, height: 844 }
+  Object.defineProperty(sidebar, 'getBoundingClientRect', { value: () => sidebarRect })
+  Object.defineProperty(main, 'getBoundingClientRect', { value: () => mainRect })
+  return { mainRect, sidebarRect }
+}
+
+describe('classic Shell RTL geometry', () => {
+  it.each([false, true] as const)('keeps collapsed=%s sidebar and representative actions in their tracks', (collapsed) => {
+    shellState.dir = 'rtl'
+    shellState.collapsed = collapsed
+
+    for (const route of ['/calendar', '/invoices/drafts', '/reports/cashflow', '/tasks', '/my-work']) {
+      const node = renderClassicRoute(route)
+      const grid = node.querySelector<HTMLElement>('.shell-grid')!
+      const sidebar = node.querySelector<HTMLElement>('.shell-sidebar')!
+      const main = node.querySelector<HTMLElement>('#main-content')!
+      const action = node.querySelector<HTMLElement>('[data-representative-action]')!
+      const { mainRect, sidebarRect } = layoutRects(grid, main, sidebar)
+      Object.defineProperty(action, 'getBoundingClientRect', { value: () => ({ left: 24, right: 224, width: 200, top: 80, bottom: 120, height: 40 }) })
+
+      expect(grid.style.gridTemplateColumns).toBe(collapsed ? '1fr 3.5rem' : '1fr 15rem')
+      expect(sidebar.style.gridColumn).toBe('2')
+      expect(main.style.gridColumn).toBe('1')
+      expect(sidebarRect).toMatchObject({ right: 1440, width: collapsed ? 56 : 240 })
+      expect(mainRect).toMatchObject({ left: 0, right: collapsed ? 1384 : 1200, width: collapsed ? 1384 : 1200 })
+      const actionRect = action.getBoundingClientRect()
+      expect(actionRect.left).toBeGreaterThanOrEqual(mainRect.left)
+      expect(actionRect.right).toBeLessThanOrEqual(mainRect.right)
+    }
+  })
+})
diff --git a/apps/zync-app/src/worker.ts b/apps/zync-app/src/worker.ts
index e5925585e..0c976f21c 100644
--- a/apps/zync-app/src/worker.ts
+++ b/apps/zync-app/src/worker.ts
@@ -69,7 +69,8 @@ export default {
 
       // Prefer service binding (env.API) — same-network, no HTTP overhead
       if (env.API) {
-        return env.API.fetch(proxyReq)
+        const apiResponse = await env.API.fetch(proxyReq)
+        return withPwaAssetHeaders(url.pathname, apiResponse)
       }
 
       // Fallback: HTTP proxy to the API worker URL
@@ -81,7 +82,8 @@ export default {
         body: ['GET', 'HEAD'].includes(request.method) ? undefined : request.body,
         redirect: 'manual',
       })
-      return fetch(httpReq)
+      const apiResponse = await fetch(httpReq)
+      return withPwaAssetHeaders(url.pathname, apiResponse)
     }
 
     // Try to serve the static asset
diff --git a/apps/zync-app/test/app-shell-sidebar.test.tsx b/apps/zync-app/test/app-shell-sidebar.test.tsx
index 184652208..54be4b9cc 100644
--- a/apps/zync-app/test/app-shell-sidebar.test.tsx
+++ b/apps/zync-app/test/app-shell-sidebar.test.tsx
@@ -3,6 +3,8 @@ import { renderToStaticMarkup } from 'react-dom/server'
 import { MemoryRouter } from 'react-router'
 import { beforeEach, describe, expect, it, vi } from 'vitest'
 import { Sidebar } from '../src/shell/Sidebar'
+import en from '../../../packages/ui/src/i18n/en.json'
+import he from '../../../packages/ui/src/i18n/he.json'
 
 const membershipState = vi.hoisted(() => ({
   memberships: [] as Array<{
@@ -25,9 +27,13 @@ const shellSessionState = vi.hoisted(() => ({
   role: 'OWNER',
 }))
 
+const translationState = vi.hoisted(() => ({
+  catalog: {} as Record<string, string>,
+}))
+
 vi.mock('react-i18next', () => ({
   useTranslation: () => ({
-    t: (_key: string, options?: { defaultValue?: string }) => options?.defaultValue ?? _key,
+    t: (key: string, options?: { defaultValue?: string }) => translationState.catalog[key] ?? options?.defaultValue ?? key,
   }),
 }))
 
@@ -79,6 +85,7 @@ describe('Sidebar shell affordances', () => {
     membershipState.isLoading = false
     shellSessionState.permissions = ['reports:read']
     shellSessionState.role = 'OWNER'
+    translationState.catalog = en
   })
 
   it('shows the create workspace link for single-tenant users', () => {
@@ -136,6 +143,26 @@ describe('Sidebar shell affordances', () => {
     expect(markup).toContain('Switch workspace')
   })
 
+  it.each([
+    ['MEMBER', en, 'My Work'],
+    ['MEMBER', he, 'העבודה שלי'],
+    ['CONTRACTOR', en, 'My Work'],
+    ['CONTRACTOR', he, 'העבודה שלי'],
+  ])('localizes My Work for %s sidebars', (role, catalog, label) => {
+    shellSessionState.role = role
+    shellSessionState.permissions = ['tasks:read', 'time:track']
+    translationState.catalog = catalog
+
+    const markup = renderToStaticMarkup(
+      <MemoryRouter>
+        <Sidebar />
+      </MemoryRouter>,
+    )
+
+    expect(markup).toContain(label)
+    expect(markup).not.toContain('nav.myWork')
+  })
+
   it('injects My Work for members and keeps Settings visible', () => {
     shellSessionState.role = 'MEMBER'
     shellSessionState.permissions = ['tasks:read', 'time:track', 'settings:read']
diff --git a/apps/zync-app/test/classic-main-landmark-source.test.ts b/apps/zync-app/test/classic-main-landmark-source.test.ts
new file mode 100644
index 000000000..e913d89a1
--- /dev/null
+++ b/apps/zync-app/test/classic-main-landmark-source.test.ts
@@ -0,0 +1,21 @@
+import { readFileSync } from 'node:fs'
+import { describe, expect, it } from 'vitest'
+
+const routedPageSources = [
+  '../src/pages/time/TeamOverviewPage.tsx',
+  '../src/pages/time/TimePage.tsx',
+  '../src/pages/reports/TimeReportPage.tsx',
+  '../src/pages/invoices/InvoiceNewPage.tsx',
+  '../src/pages/invoices/InvoicePage.tsx',
+  '../src/pages/invoices/InvoiceDetailPage.tsx',
+] as const
+
+describe('classic shell main landmark ownership', () => {
+  it('keeps #main-content owned by Shell instead of routed page content', () => {
+    for (const relativePath of routedPageSources) {
+      const source = readFileSync(new URL(relativePath, import.meta.url), 'utf8')
+      expect(source, relativePath).not.toContain('id=\"main-content\"')
+      expect(source, relativePath).not.toMatch(/<main(?:\s|>)/)
+    }
+  })
+})
diff --git a/apps/zync-app/test/expense-filter-catalog.test.tsx b/apps/zync-app/test/expense-filter-catalog.test.tsx
new file mode 100644
index 000000000..815f4575a
--- /dev/null
+++ b/apps/zync-app/test/expense-filter-catalog.test.tsx
@@ -0,0 +1,68 @@
+import * as React from 'react'
+import { readFileSync } from 'node:fs'
+import { renderToStaticMarkup } from 'react-dom/server'
+import { describe, expect, it, vi } from 'vitest'
+
+const { state } = vi.hoisted(() => ({ state: { locale: 'en' as 'en' | 'he' } }))
+
+vi.mock('@zync/types', () => ({
+  EXPENSE_CATEGORIES: [{ id: 'travel', en: 'Travel', he: 'נסיעות' }],
+}))
+
+vi.mock('@zync/ui', () => ({
+  Select: ({ id, options }: { id: string; options: Array<{ label: string; value: string }> }) => (
+    <select id={id}>{options.map((option) => <option key={option.value}>{option.label}</option>)}</select>
+  ),
+}))
+
+vi.mock('@zync/ui/i18n', () => ({
+  useLocale: () => ({ locale: state.locale }),
+}))
+
+vi.mock('react-i18next', () => ({
+  useTranslation: () => ({ t: (key: string) => `${state.locale}:${key}` }),
+}))
+
+vi.mock('react-router', () => ({
+  useSearchParams: () => [new URLSearchParams(), vi.fn()],
+}))
+
+import { ExpenseFilterBar } from '../src/features/expenses/ExpenseFilterBar'
+
+const source = readFileSync(new URL('../src/features/expenses/ExpenseFilterBar.tsx', import.meta.url), 'utf8')
+const expensesPageSource = readFileSync(new URL('../src/features/expenses/ExpensesPage.tsx', import.meta.url), 'utf8')
+const catalogs = ['en', 'he'].map((locale) => ({
+  locale,
+  messages: JSON.parse(readFileSync(new URL(`../../../packages/ui/src/i18n/${locale}.json`, import.meta.url), 'utf8')) as Record<string, string>,
+}))
+
+const filterKeys = [...source.matchAll(/t\('(expenses\.filter\.[^']+)'\)/g)].map((match) => match[1])
+
+describe('ExpenseFilterBar translations', () => {
+  it('renders localized category and empty option labels in English and Hebrew', () => {
+    for (const locale of ['en', 'he'] as const) {
+      state.locale = locale
+      const markup = renderToStaticMarkup(<ExpenseFilterBar />)
+      expect(markup).toContain(`${locale}:expenses.filter.category`)
+      expect(markup).toContain(`${locale}:expenses.filter.all`)
+      expect(markup).not.toContain('>expenses.filter.')
+    }
+  })
+
+  it('uses the canonical evaluate-all key available in both catalogs', () => {
+    expect(expensesPageSource).toContain("t('expenses.bulk.evaluateAll')")
+    expect(expensesPageSource).not.toContain("t('expenses.bulk.evaluateAllPending')")
+    for (const { locale, messages } of catalogs) {
+      expect(messages['expenses.bulk.evaluateAll'], `${locale} missing expenses.bulk.evaluateAll`).toEqual(expect.any(String))
+    }
+  })
+
+  it('has every literal expense filter key in both catalogs', () => {
+    expect(filterKeys).not.toHaveLength(0)
+    for (const { locale, messages } of catalogs) {
+      for (const key of filterKeys) {
+        expect(messages[key], `${locale} missing ${key}`).toEqual(expect.any(String))
+      }
+    }
+  })
+})
diff --git a/apps/zync-app/test/inventory-routing.test.tsx b/apps/zync-app/test/inventory-routing.test.tsx
index 1468db689..2505e1465 100644
--- a/apps/zync-app/test/inventory-routing.test.tsx
+++ b/apps/zync-app/test/inventory-routing.test.tsx
@@ -2,33 +2,21 @@ import * as React from 'react'
 import { renderToStaticMarkup } from 'react-dom/server'
 import { MemoryRouter } from 'react-router'
 import { describe, expect, it, vi } from 'vitest'
-import { moduleRoutes } from '../src/routes'
+import { moduleRoutes, PreserveSearchRedirect, preserveRedirectSearch } from '../src/routes'
 import { ModuleGuard } from '../src/components/ModuleGuard'
+import { collectRoutePrefixes, resolveRouteOwner, validateRoutePrefixOwners } from '../src/os/registry-selectors'
+import { OS_HOST_ROUTE_PREFIXES, UNOWNED_ROUTE_PREFIXES } from '../src/os/registry-os'
 import { InventoryLocationsContent, InventoryStockListContent } from '../src/routes/inventory'
 import { InventoryReportsContent } from '../src/routes/inventory/reports'
 
 vi.mock('react-i18next', () => ({
   useTranslation: () => ({
-    t: (_key: string, options?: { defaultValue?: string }) => options?.defaultValue ?? _key,
+    t: (key: string, options?: { defaultValue?: string }) => (
+      key === 'nav.inventory' ? 'Inventory' : options?.defaultValue ?? key
+    ),
   }),
 }))
 
-vi.mock('../src/routes/inventory', async (importOriginal) => {
-  const actual = await importOriginal<typeof import('../src/routes/inventory')>()
-  return {
-    ...actual,
-    InventoryPage: () => React.createElement('div', null, 'Inventory page'),
-  }
-})
-
-vi.mock('../src/routes/inventory/reports', async (importOriginal) => {
-  const actual = await importOriginal<typeof import('../src/routes/inventory/reports')>()
-  return {
-    ...actual,
-    default: () => React.createElement('div', null, 'Inventory reports page'),
-  }
-})
-
 vi.mock('../src/shell/Shell', () => ({
   useShell: () => ({
     sidebarCollapsed: false,
@@ -105,6 +93,71 @@ describe('inventory routing', () => {
     expect(moduleRoutes.some((route) => route.path === 'inventory/reports')).toBe(true)
   })
 
+  it('preserves redirect query values when canonicalizing shell routes', () => {
+    expect(preserveRedirectSearch('/', '?shell=classic&redirectProbe=dashboard')).toBe(
+      '/?shell=classic&redirectProbe=dashboard',
+    )
+
+    const dashboard = moduleRoutes.find((route) => route.path === 'dashboard')
+    const marketing = moduleRoutes.find((route) => route.path === 'marketing')
+    const time = moduleRoutes.find((route) => route.path === 'time')
+    const notifications = moduleRoutes.find((route) => route.path === 'profile/notifications')
+    expect(dashboard?.element.type).not.toBe(PreserveSearchRedirect)
+    expect(marketing?.element.type).toBe(ModuleGuard)
+    expect(marketing?.element.props.moduleId).toBe('marketing')
+    expect(marketing?.element.props.children.type).toBe(PreserveSearchRedirect)
+    expect(marketing?.element.props.children.props.to).toBe('/marketing/overview')
+    expect(time?.element.type).toBe(PreserveSearchRedirect)
+    expect(time?.element.props.to).toBe('/time-track')
+    expect(notifications?.element.type).toBe(PreserveSearchRedirect)
+    expect(notifications?.element.props.to).toBe('/settings/notifications')
+  })
+
+  it.each([
+    '/inventory',
+    '/inventory/locations',
+    '/inventory/items/stock-1?shell=os#history',
+  ])('assigns %s to the invoices OS application owner', (location) => {
+    expect(resolveRouteOwner(location)).toBe('invoices')
+  })
+
+  it.each([
+    'inventory/locations',
+    'inventory/*',
+    'inventory/reports',
+    'inventory/:itemId',
+  ])('preserves the invoices module guard for %s', (path) => {
+    const route = moduleRoutes.find((candidate) => candidate.path === path)
+
+    expect(route?.element.type).toBe(ModuleGuard)
+    expect(route?.element.props.moduleId).toBe('invoices')
+  })
+
+  it('uses the canonical registry as the single /inventory owner source', () => {
+    expect(OS_HOST_ROUTE_PREFIXES.filter(([, prefix]) => prefix === '/inventory')).toEqual([
+      ['invoices', '/inventory'],
+    ])
+    expect(UNOWNED_ROUTE_PREFIXES).not.toContain('/inventory')
+    expect(collectRoutePrefixes().filter(([, prefix]) => prefix === '/inventory')).toEqual([
+      ['invoices', '/inventory'],
+    ])
+  })
+
+  it('fails closed when /inventory is missing, wrongly owned, or declared twice', () => {
+    expect(() => validateRoutePrefixOwners([])).toThrow('/inventory must have exactly the invoices owner')
+    expect(() => validateRoutePrefixOwners([['tasks', '/inventory']])).toThrow(
+      '/inventory must have exactly the invoices owner',
+    )
+    expect(() => validateRoutePrefixOwners([
+      ['invoices', '/inventory'],
+      ['invoices', '/inventory'],
+    ])).toThrow('multiple declarations')
+    expect(() => validateRoutePrefixOwners([
+      ['invoices', '/inventory'],
+      ['tasks', '/inventory'],
+    ])).toThrow('multiple declarations')
+  })
+
   it('renders inventory list content with totals and locations', () => {
     const markup = renderToStaticMarkup(
       <MemoryRouter>
diff --git a/apps/zync-app/test/invoice-approval-sort-control.test.tsx b/apps/zync-app/test/invoice-approval-sort-control.test.tsx
new file mode 100644
index 000000000..b25265bb7
--- /dev/null
+++ b/apps/zync-app/test/invoice-approval-sort-control.test.tsx
@@ -0,0 +1,88 @@
+// @vitest-environment jsdom
+import * as React from 'react'
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+import en from '../../../packages/ui/src/i18n/en.json'
+import he from '../../../packages/ui/src/i18n/he.json'
+import { InvoiceApprovalSortControl } from '../src/features/invoices/InvoiceApprovalSortControl'
+
+const mockUseTranslation = vi.fn()
+
+vi.mock('react-i18next', () => ({ useTranslation: () => mockUseTranslation() }))
+
+;(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
+
+class TestPointerEvent extends MouseEvent {
+  readonly pointerId: number
+  readonly pointerType: string
+  readonly isPrimary: boolean
+
+  constructor(type: string, init: PointerEventInit = {}) {
+    super(type, init)
+    this.pointerId = init.pointerId ?? 1
+    this.pointerType = init.pointerType ?? 'mouse'
+    this.isPrimary = init.isPrimary ?? true
+  }
+}
+
+Object.defineProperty(globalThis, 'PointerEvent', { configurable: true, value: TestPointerEvent })
+
+Object.defineProperties(HTMLElement.prototype, {
+  hasPointerCapture: { configurable: true, value: () => false },
+  setPointerCapture: { configurable: true, value: () => undefined },
+  releasePointerCapture: { configurable: true, value: () => undefined },
+  scrollIntoView: { configurable: true, value: () => undefined },
+})
+
+const renderedSortControls = {
+  en: {
+    ariaLabel: 'Sort approvals',
+    options: ['Oldest first', 'Newest first', 'Amount', 'Customer'],
+  },
+  he: {
+    ariaLabel: 'מיון אישורים',
+    options: ['הישן ביותר תחילה', 'החדש ביותר תחילה', 'סכום', 'לקוח'],
+  },
+}
+
+const catalogs = { en, he }
+let root: Root | undefined
+
+function translationFor(locale: keyof typeof catalogs) {
+  return (key: string) => catalogs[locale][key as keyof typeof catalogs.en] ?? key
+}
+
+afterEach(async () => {
+  await act(async () => root?.unmount())
+  root = undefined
+  document.body.replaceChildren()
+  mockUseTranslation.mockReset()
+})
+
+describe('InvoiceApprovalSortControl', () => {
+  for (const locale of ['en', 'he'] as const) {
+    it(`renders translated sort control in ${locale}`, async () => {
+      mockUseTranslation.mockReturnValue({ t: translationFor(locale) })
+      const container = document.createElement('div')
+      document.body.append(container)
+      root = createRoot(container)
+
+      await act(async () => {
+        root?.render(<InvoiceApprovalSortControl value="oldest" onValueChange={vi.fn()} />)
+      })
+
+      const expected = renderedSortControls[locale]
+      const trigger = document.querySelector(`[aria-label="${expected.ariaLabel}"]`)
+      expect(trigger).not.toBeNull()
+
+      await act(async () => {
+        trigger?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0, pointerType: 'mouse' }))
+      })
+
+      expect(trigger?.getAttribute('aria-expanded')).toBe('true')
+      expect([...document.querySelectorAll('[role="option"]')].map((option) => option.textContent)).toEqual(expected.options)
+    })
+  }
+})
diff --git a/apps/zync-app/test/invoice-drafts-catalog.test.ts b/apps/zync-app/test/invoice-drafts-catalog.test.ts
new file mode 100644
index 000000000..53ac7ffdc
--- /dev/null
+++ b/apps/zync-app/test/invoice-drafts-catalog.test.ts
@@ -0,0 +1,34 @@
+import { readFileSync } from 'node:fs'
+import { describe, expect, it } from 'vitest'
+
+const source = readFileSync(new URL('../src/routes/invoices/drafts.tsx', import.meta.url), 'utf8')
+const catalogs = ['en', 'he'].map((locale) => ({
+  locale,
+  messages: JSON.parse(
+    readFileSync(new URL(`../../../packages/ui/src/i18n/${locale}.json`, import.meta.url), 'utf8'),
+  ) as Record<string, unknown>,
+}))
+const draftKeys = [...source.matchAll(/\bt\(\s*['"](invoices\.drafts\.[^'"]+)['"]/g)].map((match) => match[1])
+
+describe('invoice drafts translations', () => {
+  it('has every static draft key in both catalogs', () => {
+    expect(draftKeys).not.toHaveLength(0)
+    for (const { locale, messages } of catalogs) {
+      for (const key of draftKeys) {
+        const message = messages[key]
+        expect(message, `${locale} missing ${key}`).toBeDefined()
+        if (typeof message === 'string') {
+          expect(message.length, `${locale} empty ${key}`).toBeGreaterThan(0)
+          continue
+        }
+        expect(message, `${locale} invalid plural message ${key}`).toMatchObject({
+          zero: expect.any(String),
+          one: expect.any(String),
+          two: expect.any(String),
+          many: expect.any(String),
+          other: expect.any(String),
+        })
+      }
+    }
+  })
+})
diff --git a/apps/zync-app/test/push-service-worker-registration.test.tsx b/apps/zync-app/test/push-service-worker-registration.test.tsx
new file mode 100644
index 000000000..126ef023f
--- /dev/null
+++ b/apps/zync-app/test/push-service-worker-registration.test.tsx
@@ -0,0 +1,88 @@
+// @vitest-environment jsdom
+import { act } from 'react'
+import { createRoot, type Root } from 'react-dom/client'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { usePushOptIn } from '../src/push/usePushOptIn'
+
+declare global {
+  var IS_REACT_ACT_ENVIRONMENT: boolean | undefined
+}
+
+globalThis.IS_REACT_ACT_ENVIRONMENT = true
+
+const mounted: Array<{ root: Root; container: HTMLDivElement }> = []
+
+function configurePushSupport(register = vi.fn().mockResolvedValue(undefined)) {
+  Object.defineProperty(navigator, 'serviceWorker', {
+    configurable: true,
+    value: { register, ready: Promise.resolve(undefined) },
+  })
+  Object.defineProperty(window, 'Notification', {
+    configurable: true,
+    value: { permission: 'default', requestPermission: vi.fn().mockResolvedValue('denied') },
+  })
+  vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
+    ok: true,
+    json: vi.fn().mockResolvedValue({ publicKey: 'test-key' }),
+  }))
+  return register
+}
+
+function renderPushOptIn() {
+  let requestPermission: (() => Promise<void>) | undefined
+  const container = document.createElement('div')
+  const root = createRoot(container)
+  mounted.push({ root, container })
+  document.body.appendChild(container)
+
+  function Probe() {
+    requestPermission = usePushOptIn().requestPermission
+    return null
+  }
+
+  act(() => root.render(<Probe />))
+  return () => requestPermission?.()
+}
+
+afterEach(() => {
+  while (mounted.length) {
+    const mount = mounted.pop()
+    if (mount) act(() => mount.root.unmount())
+    mount?.container.remove()
+  }
+  vi.restoreAllMocks()
+  vi.unstubAllEnvs()
+  localStorage.clear()
+})
+
+describe('usePushOptIn service worker behavior', () => {
+  it('does not register /sw.js while mounted in development', () => {
+    vi.stubEnv('DEV', true)
+    const register = configurePushSupport()
+
+    renderPushOptIn()
+
+    expect(register).not.toHaveBeenCalled()
+  })
+
+  it('does not register /sw.js when permission is requested in development', async () => {
+    vi.stubEnv('DEV', true)
+    const register = configurePushSupport()
+    const requestPermission = renderPushOptIn()
+
+    await act(async () => {
+      await requestPermission()
+    })
+
+    expect(register).not.toHaveBeenCalled()
+  })
+
+  it('registers /sw.js while mounted in production', () => {
+    vi.stubEnv('DEV', false)
+    const register = configurePushSupport()
+
+    renderPushOptIn()
+
+    expect(register).toHaveBeenCalledWith('/sw.js')
+  })
+})
diff --git a/apps/zync-app/test/realtime-client.test.ts b/apps/zync-app/test/realtime-client.test.ts
new file mode 100644
index 000000000..aa858256f
--- /dev/null
+++ b/apps/zync-app/test/realtime-client.test.ts
@@ -0,0 +1,11 @@
+import { describe, expect, it } from 'vitest'
+import { realtimeWebSocketUrl } from '../src/lib/realtime/client'
+
+describe('realtimeWebSocketUrl', () => {
+  it.each([
+    ['http:', 'localhost:4173', 'ws://localhost:4173/api/realtime/connect'],
+    ['https:', 'app.zync.is', 'wss://app.zync.is/api/realtime/connect'],
+  ])('uses the matching WebSocket scheme for %s origins', (protocol, host, expected) => {
+    expect(realtimeWebSocketUrl({ protocol, host })).toBe(expected)
+  })
+})
diff --git a/apps/zync-app/tests/e2e/os-shell/09-url-contract.spec.ts b/apps/zync-app/tests/e2e/os-shell/09-url-contract.spec.ts
index 17ca69aaf..a68d6095b 100644
--- a/apps/zync-app/tests/e2e/os-shell/09-url-contract.spec.ts
+++ b/apps/zync-app/tests/e2e/os-shell/09-url-contract.spec.ts
@@ -1,11 +1,51 @@
-import { expect, test } from '@playwright/test'
+import { expect, test, type Page } from '@playwright/test'
 import { createUrlWindowEngine, type UrlWindow } from '@zync/os-shell'
 
+async function authenticate(page: Page): Promise<void> {
+  await page.route('**/api/auth/me', (route) => route.fulfill({
+    json: {
+      id: 'e2e-owner',
+      email: 'owner@example.test',
+      name: 'E2E Owner',
+      avatarUrl: null,
+      tenantId: 'e2e-tenant',
+      tenantSlug: 'e2e',
+      tenantName: 'E2E',
+      role: 'OWNER',
+      tier: 'enterprise',
+      permissions: ['settings:read', 'tasks:read', 'customers:read', 'inventory:read'],
+      onboarding_completed: true,
+      onboarding_step: 0,
+      emailVerified: true,
+      twoFactorEnabled: false,
+      exp: Math.floor(Date.now() / 1_000) + 3_600,
+    },
+  }))
+}
+
 const windows: UrlWindow[] = [
   { instanceId: 'customer-1', appId: 'customers', location: '/customers/acme' },
   { instanceId: 'customer-2', appId: 'customers', location: '/customers/globex' },
 ]
 
+test('route aliases preserve query params', async ({ page }) => {
+  await authenticate(page)
+  const aliases = [
+    ['/marketing', '/marketing/overview'],
+    ['/time', '/time-track'],
+    ['/profile/notifications', '/settings/notifications'],
+  ] as const
+
+  for (const [source, target] of aliases) {
+    const query = `shell=classic&redirectProbe=${encodeURIComponent(source.slice(1))}`
+    await page.goto(`${source}?${query}`)
+    await expect.poll(() => {
+      const url = new URL(page.url())
+      return `${url.pathname}?${url.searchParams.toString()}`
+    }).toBe(`${target}?${query}`)
+  }
+})
+
 test('URL contract keeps history identity and resolves cold record links', () => {
   const events: string[] = []
   const engine = createUrlWindowEngine({
diff --git a/apps/zync-app/tests/e2e/os-shell/22-mobile-baselines.mobile.spec.ts b/apps/zync-app/tests/e2e/os-shell/22-mobile-baselines.mobile.spec.ts
index 7465be5c6..15571a4c7 100644
--- a/apps/zync-app/tests/e2e/os-shell/22-mobile-baselines.mobile.spec.ts
+++ b/apps/zync-app/tests/e2e/os-shell/22-mobile-baselines.mobile.spec.ts
@@ -1,6 +1,14 @@
 import { expect, test } from '@playwright/test'
 import { mountAuthenticatedMobile } from './helpers/mobile-app'
 
+const EXPECTED_OWNER_APP_IDS = [
+  'tasks', 'projects', 'time_management', 'calendar', 'customers', 'crm', 'marketing', 'invoices',
+  'billing', 'expenses', 'contractor_payouts', 'reports', 'kb', 'today', 'notifications', 'settings', 'module_manager',
+] as const
+const EXPECTED_DOCK_IDS = ['tasks', 'projects', 'time_management', 'calendar'] as const
+const EXPECTED_MOBILE_TARGET_COUNT = EXPECTED_OWNER_APP_IDS.length + EXPECTED_DOCK_IDS.length
+
+
 test.beforeEach(async ({ page }, testInfo) => {
   await page.emulateMedia({ reducedMotion: testInfo.project.name.includes('reduced') ? 'reduce' : 'no-preference' })
   await mountAuthenticatedMobile(page)
@@ -8,12 +16,24 @@ test.beforeEach(async ({ page }, testInfo) => {
 
 test('mounted mobile home clears the safe-area strips and keeps touch targets usable', async ({ page }) => {
   await expect(page.locator('[data-mobile-surface="home"]')).toBeVisible()
-  const targetSizes = await page.locator('[data-mobile-surface="home"] button').evaluateAll((buttons) => buttons.map((button) => {
-    const rect = button.getBoundingClientRect()
-    return { width: rect.width, height: rect.height }
-  }))
-  expect(targetSizes.length).toBeGreaterThan(0)
-  expect(targetSizes.every(({ width, height }) => width >= 44 && height >= 44)).toBe(true)
+  const targets = page.locator('[data-mobile-surface="home"] [data-fx="app-open"]')
+  let stableSamples = 0
+  let settledTargets: Array<{ id: string | null; width: number; height: number }> = []
+  await expect.poll(async () => {
+    const targetInfo = await targets.evaluateAll((buttons) => buttons.map((button) => { const rect = button.getBoundingClientRect(); return { id: button.getAttribute('data-app-id'), width: rect.width, height: rect.height } }))
+    const counts = new Map<string, number>(); for (const { id } of targetInfo) if (id) counts.set(id, (counts.get(id) ?? 0) + 1)
+    const identitiesMatch = EXPECTED_OWNER_APP_IDS.every((id) => counts.get(id) === (EXPECTED_DOCK_IDS.includes(id as typeof EXPECTED_DOCK_IDS[number]) ? 2 : 1)) && [...counts.keys()].every((id) => (EXPECTED_OWNER_APP_IDS as readonly string[]).includes(id))
+    const complete = targetInfo.length === EXPECTED_MOBILE_TARGET_COUNT && identitiesMatch && targetInfo.every(({ width, height }) => width >= 44 && height >= 44)
+    stableSamples = complete ? stableSamples + 1 : 0; if (stableSamples >= 5) settledTargets = targetInfo; return stableSamples
+  }, { intervals: [100], timeout: 5_000 }).toBe(5)
+  expect(settledTargets).toHaveLength(EXPECTED_MOBILE_TARGET_COUNT)
+})
+
+test('mobile launcher keeps every app target within its grid track', async ({ page }) => {
+  const home = page.locator('[data-mobile-surface="home"]')
+  for (const id of ['crm', 'time_management', 'contractor_payouts', 'kb']) await expect(home.getByRole('grid', { name: 'Apps' }).locator(`[data-app-id="${id}"]`)).toHaveCount(1)
+  const geometry = await home.evaluate((element) => { const buttons = Array.from(element.querySelectorAll<HTMLElement>('[data-app-id]')); const viewportWidth = document.documentElement.clientWidth; const rects = buttons.map((button) => button.getBoundingClientRect()); return { scrollWidth: document.documentElement.scrollWidth, viewportWidth, contained: rects.every(({ left, right }) => left >= 0 && right <= viewportWidth), overlaps: rects.some((rect, index) => rects.slice(index + 1).some((other) => Math.max(rect.left, other.left) < Math.min(rect.right, other.right) && Math.max(rect.top, other.top) < Math.min(rect.bottom, other.bottom))) } })
+  expect(geometry.scrollWidth).toBe(geometry.viewportWidth); expect(geometry.contained).toBe(true); expect(geometry.overlaps).toBe(false)
 })
 
 test('mounted mobile drawer is keyboard-searchable in the active direction', async ({ page }) => {
@@ -34,6 +54,12 @@ test('mounted mobile app frame keeps navigation controls inside the production f
   await expect(page.locator('[data-mobile-surface="home"]')).toBeVisible()
 })
 
+test('mobile app frame owns the sole main landmark without changing classic My Work', async ({ page }) => {
+  const home = page.locator('[data-mobile-surface="home"]'); await home.getByRole('grid', { name: 'Apps' }).getByRole('button', { name: 'Open Tasks', exact: true }).tap()
+  const frame = page.locator('[data-mobile-frame]'); await expect(frame).toBeVisible(); await expect(frame.locator(':scope > main#main-content[data-module-content]')).toHaveCount(1); await expect(page.getByRole('main')).toHaveCount(1)
+  await page.goto('/my-work?shell=classic'); await expect(page.locator('[data-mobile-frame]')).toHaveCount(0); await expect(page.getByRole('main')).toHaveCount(1)
+})
+
 test('mounted mobile recents and shade are real production surfaces', async ({ page }) => {
   await page.getByRole('grid', { name: 'Apps' }).getByRole('button', { name: 'Open Tasks', exact: true }).tap()
   await page.getByRole('button', { name: 'Recents' }).tap()
diff --git a/apps/zync-app/tests/e2e/os-shell/23-desktop-geometry.spec.ts b/apps/zync-app/tests/e2e/os-shell/23-desktop-geometry.spec.ts
new file mode 100644
index 000000000..753f0a97e
--- /dev/null
+++ b/apps/zync-app/tests/e2e/os-shell/23-desktop-geometry.spec.ts
@@ -0,0 +1,200 @@
+import { expect, test, type Page } from '@playwright/test'
+import { installMatrixSession } from '../../ui-matrix/fixtures'
+
+const TASKBAR_INSET = 48
+const DESKTOP_VIEWPORTS = [
+  { width: 1440, height: 900 },
+  { width: 1280, height: 800 },
+  { width: 1024, height: 768 },
+] as const
+
+function overlaps(a: { x: number; y: number; width: number; height: number }, b: { x: number; y: number; width: number; height: number }) {
+  return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y
+}
+
+async function installDesktopLayout(page: Page): Promise<void> {
+  let version = 1
+  let payload = {
+    v: 2 as const,
+    writer: 'matrix',
+    committedAt: '2026-08-22T00:00:00.000Z',
+    data: {
+      desktops: [{ id: 'desktop-1', name: 'Desktop 1', windows: [] }],
+      activeDesktopId: 'desktop-1',
+      taskbar: { pinned: ['tasks', 'customers', 'today'], position: 'bottom' as const, autoHide: false },
+      icons: (['tasks', 'customers', 'today', 'notifications', 'settings', 'ai_assistant'] as const).map((moduleId, cell) => ({ moduleId, cell })),
+      widgets: [],
+      folders: [],
+      onboarding: { coachMarksCompleted: true },
+    },
+  }
+  await page.route(/\/api\/shell\/layout\?device=desktop$/, async (route) => {
+    if (route.request().method() === 'GET') {
+      await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ payload, version }) })
+      return
+    }
+    if (route.request().method() === 'PUT') {
+      const body = route.request().postDataJSON() as { payload?: typeof payload } | null
+      if (body?.payload) payload = body.payload
+      version += 1
+      await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ payload, version }) })
+      return
+    }
+    await route.fulfill({ status: 405, contentType: 'application/json', body: JSON.stringify({ error: 'method_not_allowed' }) })
+  })
+}
+
+for (const viewport of DESKTOP_VIEWPORTS) {
+  test(`live desktop window geometry preserves drag resize snap hit targets and bounds at ${viewport.width}x${viewport.height}`, async ({ page }, testInfo) => {
+    const dir = testInfo.project.metadata.dir === 'rtl' ? 'rtl' : 'ltr'
+    const locale = dir === 'rtl' ? 'he' : 'en'
+    await page.setViewportSize(viewport)
+    await page.addInitScript(({ locale: initialLocale }) => localStorage.setItem('zync_locale', initialLocale), { locale })
+    await installMatrixSession(page, { locale })
+    await installDesktopLayout(page)
+    await page.goto('/desktop?shell=os')
+    await expect(page.locator('html')).toHaveAttribute('dir', dir)
+    await expect(page.locator('[data-desktop]')).toBeVisible()
+    await expect(page.locator('[data-coach-marks]')).toHaveCount(0)
+
+    const icon = page.locator('[data-module-id="customers"]')
+    const iconBox = await icon.boundingBox()
+    expect(iconBox).not.toBeNull()
+    const frames = page.locator('[data-window-id]')
+    await icon.dblclick()
+    await expect.poll(() => frames.count()).toBeGreaterThan(0)
+
+    // Single-instance modules may focus an already-restored window instead of
+    // creating a new one. The OS store moves the focused window to the end.
+    const frame = frames.last()
+    await expect(frame).toBeVisible()
+    const initial = await frame.boundingBox()
+    expect(initial).not.toBeNull()
+    expect(overlaps(initial!, iconBox!)).toBe(false)
+    expect(initial!.x).toBeGreaterThanOrEqual(0)
+    expect(initial!.y).toBeGreaterThanOrEqual(0)
+    expect(initial!.x + initial!.width).toBeLessThanOrEqual(viewport.width + 1)
+    expect(initial!.y + initial!.height).toBeLessThanOrEqual(viewport.height - TASKBAR_INSET + 1)
+
+    const titlebar = frame.locator(':scope > header')
+    const titleBox = await titlebar.boundingBox()
+    expect(titleBox).not.toBeNull()
+    const titleGrabX = titleBox!.x + titleBox!.width / 2
+    await page.mouse.move(titleGrabX, titleBox!.y + 24)
+    await page.mouse.down()
+    await page.mouse.move(titleGrabX + 40, titleBox!.y + 64, { steps: 5 })
+    await page.mouse.up()
+    const dragged = await frame.boundingBox()
+    expect(dragged).not.toBeNull()
+    expect(dragged!.x).toBeGreaterThan(initial!.x + 20)
+    expect(dragged!.y).toBeGreaterThan(initial!.y + 10)
+    expect(dragged!.x + dragged!.width).toBeLessThanOrEqual(viewport.width + 1)
+    expect(dragged!.y + dragged!.height).toBeLessThanOrEqual(viewport.height - TASKBAR_INSET + 1)
+
+    const closeButton = frame.getByRole('button', { name: 'Close window' })
+    const closeBox = await closeButton.boundingBox()
+    expect(closeBox).not.toBeNull()
+    const closeHit = await page.evaluate(({ x, y }) => {
+      const raw = document.elementFromPoint(x, y) as HTMLElement | null
+      return {
+        label: raw?.closest<HTMLElement>('button')?.getAttribute('aria-label') ?? null,
+        handle: raw?.closest<HTMLElement>('[data-window-resize-handle]')?.getAttribute('data-window-resize-handle') ?? null,
+      }
+    }, { x: closeBox!.x + closeBox!.width - 4, y: closeBox!.y + 4 })
+    expect(closeHit).toEqual({ label: 'Close window', handle: null })
+
+    const east = frame.locator('[data-window-resize-handle="e"]')
+    const eastBox = await east.boundingBox()
+    expect(eastBox).not.toBeNull()
+    expect(eastBox!.width).toBeCloseTo(24, 0)
+    expect(Math.abs((eastBox!.x + eastBox!.width) - (dragged!.x + dragged!.width))).toBeLessThanOrEqual(1)
+    const eastHit = await page.evaluate(({ x, y }) => {
+      const raw = document.elementFromPoint(x, y) as HTMLElement | null
+      return raw?.closest<HTMLElement>('[data-window-resize-handle]')?.getAttribute('data-window-resize-handle') ?? null
+    }, { x: eastBox!.x + eastBox!.width / 2, y: eastBox!.y + eastBox!.height / 2 })
+    expect(eastHit).toBe('e')
+    await page.mouse.move(eastBox!.x + eastBox!.width / 2, eastBox!.y + eastBox!.height / 2)
+    await page.mouse.down()
+    await page.mouse.move(eastBox!.x + eastBox!.width / 2 - 30, eastBox!.y + eastBox!.height / 2, { steps: 5 })
+    await page.mouse.up()
+    const eastResized = await frame.boundingBox()
+    expect(eastResized).not.toBeNull()
+    expect(eastResized!.x).toBeCloseTo(dragged!.x, 0)
+    expect(eastResized!.width).toBeLessThan(dragged!.width - 10)
+
+    const west = frame.locator('[data-window-resize-handle="w"]')
+    const westBox = await west.boundingBox()
+    expect(westBox).not.toBeNull()
+    expect(westBox!.width).toBeCloseTo(24, 0)
+    expect(Math.abs(westBox!.x - eastResized!.x)).toBeLessThanOrEqual(1)
+    const westHit = await page.evaluate(({ x, y }) => {
+      const raw = document.elementFromPoint(x, y) as HTMLElement | null
+      return raw?.closest<HTMLElement>('[data-window-resize-handle]')?.getAttribute('data-window-resize-handle') ?? null
+    }, { x: westBox!.x + westBox!.width / 2, y: westBox!.y + westBox!.height / 2 })
+    expect(westHit).toBe('w')
+    const eastResizedRight = eastResized!.x + eastResized!.width
+    await page.mouse.move(westBox!.x + westBox!.width / 2, westBox!.y + westBox!.height / 2)
+    await page.mouse.down()
+    await page.mouse.move(westBox!.x + westBox!.width / 2 + 30, westBox!.y + westBox!.height / 2, { steps: 5 })
+    await page.mouse.up()
+    const westResized = await frame.boundingBox()
+    expect(westResized).not.toBeNull()
+    expect(westResized!.x).toBeGreaterThan(eastResized!.x + 10)
+    expect(westResized!.x + westResized!.width).toBeCloseTo(eastResizedRight, 0)
+    expect(westResized!.width).toBeLessThan(eastResized!.width - 10)
+
+    const se = frame.locator('[data-window-resize-handle="se"]')
+    let seBox = await se.boundingBox()
+    expect(seBox).not.toBeNull()
+    expect(seBox!.width).toBeCloseTo(24, 0)
+    expect(seBox!.height).toBeCloseTo(24, 0)
+    await expect.poll(async () => {
+      seBox = await se.boundingBox()
+      if (!seBox) return { handle: null, pointerEvents: null }
+      return page.evaluate(({ x, y }) => {
+        const raw = document.elementFromPoint(x, y) as HTMLElement | null
+        const handle = raw?.closest<HTMLElement>('[data-window-resize-handle]') ?? null
+        return {
+          handle: handle?.getAttribute('data-window-resize-handle') ?? null,
+          pointerEvents: handle ? getComputedStyle(handle).pointerEvents : null,
+        }
+      }, { x: seBox.x + seBox.width / 2, y: seBox.y + seBox.height / 2 })
+    }).toEqual({ handle: 'se', pointerEvents: 'auto' })
+
+    await page.mouse.move(seBox!.x + seBox!.width / 2, seBox!.y + seBox!.height / 2)
+    await page.mouse.down()
+    await page.mouse.move(
+      Math.min(viewport.width - 2, seBox!.x + seBox!.width / 2 + 60),
+      Math.min(viewport.height - TASKBAR_INSET - 2, seBox!.y + seBox!.height / 2 + 40),
+      { steps: 5 },
+    )
+    await page.mouse.up()
+    const resized = await frame.boundingBox()
+    expect(resized).not.toBeNull()
+    expect(resized!.width).toBeGreaterThanOrEqual(westResized!.width)
+    expect(resized!.height).toBeGreaterThanOrEqual(westResized!.height)
+    expect(resized!.x + resized!.width).toBeLessThanOrEqual(viewport.width + 1)
+    expect(resized!.y + resized!.height).toBeLessThanOrEqual(viewport.height - TASKBAR_INSET + 1)
+
+    const snapTitle = await titlebar.boundingBox()
+    expect(snapTitle).not.toBeNull()
+    await page.mouse.move(snapTitle!.x + snapTitle!.width / 2, snapTitle!.y + 24)
+    await page.mouse.down()
+    await page.mouse.move(2, Math.floor(viewport.height / 3), { steps: 8 })
+    await page.mouse.up()
+    const snapped = await frame.boundingBox()
+    expect(snapped).not.toBeNull()
+    expect(snapped!.x).toBeLessThanOrEqual(1)
+    expect(snapped!.y).toBeLessThanOrEqual(1)
+    expect(snapped!.width).toBeCloseTo(viewport.width / 2, 0)
+    expect(snapped!.height).toBeCloseTo(viewport.height - TASKBAR_INSET, 0)
+
+    expect(await frame.evaluate((node) => getComputedStyle(node).overflow)).toBe('hidden')
+    expect(await page.evaluate(() => ({
+      sw: document.documentElement.scrollWidth,
+      sh: document.documentElement.scrollHeight,
+      iw: innerWidth,
+      ih: innerHeight,
+    }))).toEqual({ sw: viewport.width, sh: viewport.height, iw: viewport.width, ih: viewport.height })
+  })
+}
diff --git a/apps/zync-app/tests/e2e/os-shell/24-classic-main-landmarks.spec.ts b/apps/zync-app/tests/e2e/os-shell/24-classic-main-landmarks.spec.ts
new file mode 100644
index 000000000..1b11e47d2
--- /dev/null
+++ b/apps/zync-app/tests/e2e/os-shell/24-classic-main-landmarks.spec.ts
@@ -0,0 +1,27 @@
+import { expect, test } from '@playwright/test'
+import { installMatrixSession } from '../../ui-matrix/fixtures'
+
+const AFFECTED_CLASSIC_ROUTES = [
+  '/time-track',
+  '/time-track/team',
+  '/reports/time',
+  '/invoices',
+  '/invoices/new',
+  '/invoices/invoice-1',
+] as const
+
+test.beforeEach(async ({ page }) => {
+  await installMatrixSession(page)
+})
+
+for (const route of AFFECTED_CLASSIC_ROUTES) {
+  test(`classic shell owns the single main landmark on ${route}`, async ({ page }) => {
+    await page.goto(`${route}?shell=classic`)
+    const shellMain = page.locator('#main-content')
+    await expect(shellMain).toHaveCount(1)
+    await expect(shellMain).toBeVisible()
+    await expect(page.locator('main')).toHaveCount(1)
+    await expect(page.locator('main#main-content')).toHaveCount(1)
+    await expect(page.locator('[role="main"]')).toHaveCount(0)
+  })
+}
diff --git a/apps/zync-app/tests/invoice-approvals-catalog.spec.mjs b/apps/zync-app/tests/invoice-approvals-catalog.spec.mjs
new file mode 100644
index 000000000..5f7ebb9dd
--- /dev/null
+++ b/apps/zync-app/tests/invoice-approvals-catalog.spec.mjs
@@ -0,0 +1,38 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import test from 'node:test'
+
+const sortControlSource = readFileSync(
+  new URL('../src/features/invoices/InvoiceApprovalSortControl.tsx', import.meta.url),
+  'utf8',
+)
+const sources = [
+  readFileSync(new URL('../src/routes/invoices/approvals.tsx', import.meta.url), 'utf8'),
+  sortControlSource,
+]
+const sortOptionKeys = [
+  'invoices.approvals.sort.oldest',
+  'invoices.approvals.sort.newest',
+  'invoices.approvals.sort.amount',
+  'invoices.approvals.sort.customer',
+]
+const approvalKeys = sources.flatMap((source) =>
+  [...source.matchAll(/\bt\(\s*['"](invoices\.approvals\.[^'"]+)['"]/g)].map((match) => match[1]),
+)
+
+test('invoice approval calls resolve in every catalog', () => {
+  assert.ok(approvalKeys.length > 0, 'expected invoice approval translation calls')
+  for (const key of sortOptionKeys) {
+    assert.match(sortControlSource, new RegExp(`t\\(['"]${key}['"]\\)`))
+  }
+
+  for (const locale of ['en', 'he']) {
+    const messages = JSON.parse(
+      readFileSync(new URL(`../../../packages/ui/src/i18n/${locale}.json`, import.meta.url), 'utf8'),
+    )
+    for (const key of approvalKeys) {
+      assert.equal(typeof messages[key], 'string', `${locale} missing ${key}`)
+      assert.notEqual(messages[key], key, `${locale} unresolved ${key}`)
+    }
+  }
+})
diff --git a/docs/plans/2026-08-09-zync-invariantum-gpt-workflow.md b/docs/plans/2026-08-09-zync-invariantum-gpt-workflow.md
new file mode 100644
index 000000000..bed1d8238
--- /dev/null
+++ b/docs/plans/2026-08-09-zync-invariantum-gpt-workflow.md
@@ -0,0 +1,123 @@
+# Zync Invariantum GPT Workflow
+
+audience: AI coding agents first.
+
+## Outcome
+
+Ship all 21 confirmed Invariantum root-cause repairs across classic desktop, OS desktop, OS mobile, six roles, English/LTR, Hebrew/RTL. Preserve strict focused RED→GREEN evidence and independent immutable review. Run broad regression suite exactly once after every focused lane is approved. Land, deploy, arm, and verify owner-visible behavior.
+
+## Status
+
+ACTIVE — coordinator preparing GPT Workflow packages. Task #3 in progress. Task #4 blocked by Task #3.
+
+## Source request
+
+Complete every remaining Invariantum repair end-to-end through `/gpt-orchestrator`. Preserve immutable packages, active ask-gpt conversations/processes, approved roots, correction/review receipts, maximum eight concurrent workers, and one global 55–75-second send queue. Do NOT duplicate active remote work. Run broad regression suite exactly once only after every focused repair and independent review is clean; then land, deploy, arm, and verify owner-visible behavior.
+
+## Preserved WIP and identities
+
+- Zync worktree: `/home/user/Projects/zync.is/.worktrees/invariantum-zync-full`
+- Zync HEAD: `1b6c107cfba9f360d29e95eb3c016c6773eff57c`
+- Zync tree: `04c2bf4677686cf5fa90b99eeea68e61a7524f95`
+- Invariantum worktree: `/home/user/Projects/invariantum/.worktrees/fix-pack-finalize`
+- Invariantum HEAD: `080258b857342ef6c681a42efd89329a4441b5f6`
+- Invariantum tree: `9398dc57f59fe23c8ce1154a315116264c437c5b`
+- Authoritative ledger: `.invariantum/triage/problem-ledger.json`
+- Prior dynamic workflow: `wf_700daa14-b53`
+- Prior approved count: 14/21.
+- Root 15 WIP commit exists: `1b6c107cf fix desktop icon label wrapping`; review/evidence incomplete because prior workflow process died.
+- Generated Invariantum scan artifacts remain untracked. NEVER package or commit generated reports, logs, runs, config output, or secrets.
+
+## Approved roots
+
+1. `dev-service-worker-registration`
+2. `http-origin-websocket-scheme`
+3. `mobile-os-apps-identity-render-loop`
+4. `scan-api-proxy-topology`
+5. `my-work-nav-catalog-gap`
+6. `nav-model-catalog-drift`
+7. `expense-filter-catalog-gap`
+8. `expense-evaluate-action-key-mismatch`
+9. `invoice-drafts-catalog-gap`
+10. `invoice-approvals-sort-catalog-gap`
+11. `mobile-launcher-intrinsic-width-overflow`
+12. `scrollable-content-clipping-detector`
+13. `rtl-classic-shell-grid-placement`
+14. `rtl-select-value-overflow`
+
+## Cluster ledger
+
+### GPT-ZI-1 — OS desktop geometry
+
+- State: CORRECTION_REQUIRED
+- Acceptance: finish/review `desktop-icon-label-width`; fix `default-window-desktop-icon-collision`; fix `window-resize-handle-hitboxes`; strict focused RED→GREEN per root; preserve keyboard, drag, resize, hit-test behavior.
+- Allowed files: exact desktop/window/geometry source and focused tests named by ledger or proven imports. No routes, catalogs, API, generated audit data, or unrelated config.
+- Read-only files: ledger, applicable specs, project instructions, prior commit history.
+- Dependencies: approved roots 1–14; existing root-15 WIP must be preserved and audited.
+- Baseline commit: `1b6c107cfba9f360d29e95eb3c016c6773eff57c`
+- Baseline tree: `04c2bf4677686cf5fa90b99eeea68e61a7524f95`
+- Package: `/tmp/claude-1000/-home-user-Projects-zync-is/e8a74fd9-6ae0-429e-be02-0285a2ba7941/gpt-workflow/os-desktop-geometry/os-desktop-geometry-package.zip`; SHA-256 `c99dddda94ea3d6d5f55c9350a6fdf0c6c2e6ea4270b421b1aab99de8398fdba`; 57 entries; 1,079,049 expanded bytes; inventory in sibling `package-receipt.json`; CRC clean.
+- Implementation prompt SHA-256: `5732e4aa6f4bb5eb200eda9807a224019b3feafae596f28583f3c68a73a1fff7`
+- Worker: GPT Pro conversation `6a7b42b3-e168-83ec-94a8-eea351258eec` via Luna transport.
+- Current receipt: corrected downloader recovered and structurally validated implementation ZIP SHA-256 `8595804db2aa7a8ea90a712efcf6333b99ff77fba5c94e52565d5d505ca4bf7c`. Conversation exposes duplicate attachment detections; apply script and instructions fail `ambiguous-button`. Bounded correction package `os-desktop-geometry-correction-2-context.zip` SHA-256 `344b4be070be11110de4f3f5032579386124649d66065f6399f80f923932de6d` dispatched through the active global send queue to the original conversation.
+- Next executable action: tracked transport owns generation/recovery; on completion hostile-validate single outer ZIP and build immutable review package.
+
+### GPT-ZI-2 — shell routing and mobile semantics
+
+- State: VALIDATING
+- Acceptance: fix `redirect-query-shell-override-loss`, `inventory-missing-os-route-owner`, `mobile-app-frame-main-landmark`; focused RED→GREEN per root; preserve shell choice, route ownership, authorization, and landmark semantics.
+- Allowed files: exact route redirect, OS registry/ownership, mobile app frame source and focused tests named by ledger or proven imports. No desktop geometry, detector source, generated audit data, API, or unrelated config.
+- Read-only files: ledger, applicable specs, project instructions.
+- Dependencies: approved roots 1–14.
+- Baseline commit: `1b6c107cfba9f360d29e95eb3c016c6773eff57c`
+- Baseline tree: `04c2bf4677686cf5fa90b99eeea68e61a7524f95`
+- Package: `/tmp/claude-1000/-home-user-Projects-zync-is/e8a74fd9-6ae0-429e-be02-0285a2ba7941/gpt-workflow/shell-routing-mobile-semantics/shell-routing-mobile-semantics-package.zip`; SHA-256 `dd89844331fbd591552e0bd1828e9b1be05dc6eb24cbee9f2bb520de42b3ca1b`; 70 entries; 1,168,757 expanded bytes; inventory in sibling `package-receipt.json`; CRC clean.
+- Implementation prompt SHA-256: `c0f49143408406cba2095d1712e0110bb37593286906f7e08cc128ea98111486`
+- Worker: GPT Pro conversation `6a7b41d7-3078-83ec-8d6c-f299a8b6afef` via Luna transport.
+- Current receipt: correction-3 outer ZIP SHA-256 `119e7989a1719958091b4337558a0b11b880a9f450ca5615137c3fc1d87ba36a`; inner ZIP SHA-256 `79ebab5e93ead5cd83b7bb9641cd0a15292c058c8c32e0184ef0bc1f726505c3`; hostile validation and complete apply-script inspection passed; no bytes applied. Ten complete candidate postimages produce candidate tree `21e4fb018c077e3b160dc884e0bace3ecfc3464e`, binary diff SHA-256 `f18f3e997aacde5aef9d8b1db6b6fd5f736f5c58eb1f04c79ecb1ffed6ccfc94`, changed-path SHA-256 `d8efb96cf73ba13a03f970df4aeef1f3faa59e2b58cdfce49022992bfa3d19ed`.
+- Next executable action: replacement fresh review prompt was confirmed exactly once; harness-tracked ask-gpt process `bz47c0n28` remains active. Do not poll or duplicate-send. On completion, retrieve and hostile-validate the required ZIP verdict bundle, then bind verdict identities.
+
+### GPT-ZI-3 — locale sweep detector
+
+- State: CORRECTION_REQUIRED
+- Acceptance: fix `visually-hidden-locale-sweep-detection`; browser-backed or detector-bound strict RED proves visually hidden text is excluded without hiding visible untranslated text; focused GREEN; no suppression.
+- Allowed files: exact Invariantum locale sweep detector/probe source and focused tests named by ledger or proven imports.
+- Read-only files: Zync ledger/evidence, Invariantum instructions/specs, related detector contracts.
+- Dependencies: approved detector root 12.
+- Baseline commit: `080258b857342ef6c681a42efd89329a4441b5f6`
+- Baseline tree: `9398dc57f59fe23c8ce1154a315116264c437c5b`
+- Package: `/tmp/claude-1000/-home-user-Projects-zync-is/e8a74fd9-6ae0-429e-be02-0285a2ba7941/gpt-workflow/visually-hidden-locale-sweep/visually-hidden-locale-sweep-package.zip`; SHA-256 `7bd54d793c2a5c9f5c1827680f47e72e79aa9c1a84db1f14b3689d42c7a042ea`; 23 entries; 277,680 expanded bytes; inventory in sibling `package-receipt.json`; CRC clean; generated JS/maps/declarations excluded.
+- Implementation prompt SHA-256: `6981ccadd2cec105c901d197b831cd2bd6422652cbbbae7984bc258559e0be88`
+- Worker: GPT Pro conversation `6a7b4220-b0f4-83ec-a73f-33830a8af884` via Luna transport.
+- Current receipt: second correction package SHA-256 `14320657169e73e99bbca0a6cce4e4527886a9e78fa23c4b72e42fad1ebb5efc` verified. Prior transport exited before remote receipt and conversation log recorded no new turn, proving one corrected dispatch safe. It is queued second in the active global send dispatcher, exactly 65 seconds after GPT-ZI-1.
+- Next executable action: tracked transport owns exact original-thread dispatch/recovery; on completion hostile-validate registered attachments and build immutable review package.
+
+## Execution contract
+
+1. Main coordinator builds three bounded packages and records hashes/inventories.
+2. One main-owned send queue starts GPT Pro prompts 55–75 seconds apart. Maximum three implementation transports active now; absolute cap eight.
+3. Luna transports only send/download/validate transport identity. They NEVER design, edit, review, or apply.
+4. Main validates hostile artifacts and constructs immutable baseline→candidate review packages.
+5. Fresh GPT Pro reviewer conversation reviews each candidate. Blocking finding resumes original implementation conversation with coordinator-authored correction package.
+6. Deterministic focused verification runs before integration. Every warning/error/notice/security-gate line is fixed or explicitly proven benign.
+7. Terra/medium integrates approved deltas sequentially where files overlap. Any semantic delta requires fresh review.
+8. After all 21 roots are focused-green and approved, run broad regression suite exactly once.
+9. Land through repository canonical mechanism. Push Invariantum detector fixes to `origin/fix-pack-finalize`. Land/deploy Zync through project canonical path. Verify live owner-visible behavior.
+
+## Acceptance criteria
+
+- Exactly 21/21 root causes approved with immutable receipts.
+- Focused causal RED and clean GREEN recorded for every repair.
+- No unreviewed artifact bytes applied.
+- Diagnostic issues in changed tests/config resolved; no ignored signals.
+- Broad regression suite executed exactly once after focused convergence and passes cleanly.
+- Invariantum branch pushed; Zync landed/deployed/armed; live verification recorded.
+- Generated audit data and secrets excluded from commits/packages.
+
+## Current receipt
+
+2026-08-11: GPT Workflow selected. `ask-gpt --help` verified current syntax. Three immutable packages built, CRC/hash validated, and dispatched through separate GPT-5.6 Luna/max transports with one ask-gpt Pro conversation each. Starts were separated by the main-owned 65-second queue. Active transport workflows: GPT-ZI-1 `wf_5967573a-452`; GPT-ZI-2 `wf_89a76a9d-fa5`; GPT-ZI-3 `wf_6c857905-dd0`. Package-builder manifests list payload entries and omit their own `context/INVENTORY.json`; exact one-entry self-manifest delta is recorded and validated. No duplicate sends. Broad gate not run.
+
+## Next executable action
+
+Build, inventory, CRC-check, and hash all three implementation packages; update each task envelope before dispatch.
diff --git a/docs/plans/2026-08-22-zync-invariantum-handoff.md b/docs/plans/2026-08-22-zync-invariantum-handoff.md
new file mode 100644
index 000000000..ac8f88dc2
--- /dev/null
+++ b/docs/plans/2026-08-22-zync-invariantum-handoff.md
@@ -0,0 +1,28 @@
+# Zync Invariantum recovery — final handoff
+
+Date: 2026-08-22
+
+Reviewed Zync source SHA: `cc7cd55e1e89b51b0f4e0d253e5351c1b36fa40a`
+
+Separate Invariantum detector repository commit: `b18d927b` (`Harden nested-scroll occlusion reachability`)
+
+Baseline used for recovery comparison: `5a785a3e7b3d1bb2eb1a3029f9b5b0850ae44a3b`
+
+## Source review status
+
+The final Zync application recovery patch has an independent read-only review verdict of **PASS — no blocking finding remains for cross-stream reconciliation**. The subsequent scan-runner `.turbo` source-hash exclusion and checkpoint `targetSourceHash` fixes also received no-blocker integrity reviews.
+
+The separate detector commit `b18d927b` incorporates the reviewer-requested descendant-scroller ancestry, outer clipping, sticky-ancestor and transparent-only fringe-suppression fixes. Exact buildbox verification of the committed content is recorded on the Advanced side: focused geometry/reachability **86/86**, detector package typecheck, targeted lint, build and diff-check all passed. A final independent read-only re-review of exact diff `e06ec65f..b18d927b` is complete. Verdict: **No blocking issue remains before cross-stream handoff.** The reviewer explicitly confirmed the transparent-only `occluder.opacity === 0` guard, full descendant scroll-container ancestry, clipped nested-scroller `visibleRect`, sticky-ancestor fail-closed handling, and backward-conservative behavior for older evidence. Advisories were low-severity only: effective ancestor opacity is not modeled (fail-closed false-positive risk) and the version propagation test compares against the implementation constant rather than a literal `1.1.0`.
+
+## Reconciliation inputs
+
+- Overlap manifest: `docs/plans/2026-08-22-zync-invariantum-overlap-manifest.md`.
+- Exact overlap against current Advanced handoff: 15 paths.
+- Advanced preflight contains destination mapping for superseded app-local OS files.
+- Do not merge directly to `main`; create a fresh Phase-H integration worktree from the reviewed Advanced branch and reconcile these semantics manually.
+
+## Readiness
+
+**READY FOR CROSS-STREAM RECONCILIATION.**
+
+Use reviewed Zync source SHA `cc7cd55e1e89b51b0f4e0d253e5351c1b36fa40a` plus separate Invariantum detector SHA `b18d927b`. Recompute overlap once more against the exact Advanced integration base before applying changes, then perform semantic reconciliation in a fresh integration worktree. Do not merge directly to `main`.
diff --git a/docs/plans/2026-08-22-zync-invariantum-overlap-manifest.md b/docs/plans/2026-08-22-zync-invariantum-overlap-manifest.md
new file mode 100644
index 000000000..24502efb1
--- /dev/null
+++ b/docs/plans/2026-08-22-zync-invariantum-overlap-manifest.md
@@ -0,0 +1,140 @@
+# Zync Invariantum recovery — cross-stream overlap manifest
+
+Date: 2026-08-22
+
+Baseline: `5a785a3e7b3d1bb2eb1a3029f9b5b0850ae44a3b`
+Invariantum reviewed source SHA: `cc7cd55e1e89b51b0f4e0d253e5351c1b36fa40a`
+Advanced comparison tip used for this manifest: `888de26014af10af7ebbee49686d08fa3aca359d`
+
+Invariantum changes **87 paths**; Advanced changes **307 paths**; exact intersection: **15 paths**.
+
+## Overlap decisions
+
+| Path | Decision | Invariantum semantic invariant to preserve |
+| --- | --- | --- |
+| `apps/zync-app/package.json` | Semantic merge | Preserve final Invariantum test/dev dependency requirements while retaining Advanced host/package dependencies; regenerate lockfile. |
+| `apps/zync-app/src/os/OsShell.tsx` | Port semantics into Advanced host | Do not restore pre-extraction Desktop/WindowFrame ownership. Preserve Invariantum behavior through current `@zync/os-shell` host/package seams. |
+| `apps/zync-app/src/os/__tests__/desktop-mounted-components.test.tsx` | Adapt/combine tests | Carry long desktop-icon label wrapping regression to the package-owned Desktop/Icon implementation. |
+| `apps/zync-app/src/os/__tests__/os-shell-store.test.ts` | Port test + package-core fix | Preserve existing single-instance deep route when reopen has no explicit location; implement in `packages/os-shell/src/core/os-shell-store.ts`. |
+| `apps/zync-app/src/os/__tests__/window-frame.test.tsx` | Port test + package window fix | Carry 24px physical resize hit targets, keyboard resize and control non-overlap to package `WindowFrame`. |
+| `apps/zync-app/src/os/mobile/MobileAppFrame.tsx` | Do not resurrect | Port sole-main-landmark semantics to `packages/os-shell/src/mobile/MobileAppFrame.tsx`. |
+| `apps/zync-app/src/os/mobile/mobile-surfaces.test.tsx` | Adapt/combine tests | Keep Advanced navigation-intent/shade/Home/Recents tests and add sole-main landmark proof. |
+| `apps/zync-app/src/os/os-shell-store.ts` | Keep Advanced adapter | Never restore app-owned Zustand store; move behavioral delta into package core. |
+| `apps/zync-app/src/os/registry-os.ts` | Semantic merge | Preserve canonical `/inventory` ownership by invoices while retaining Advanced system/module-manager descriptors. |
+| `apps/zync-app/src/os/registry-selectors.ts` | Semantic merge | Preserve fail-closed unique route ownership and stable memoization with Advanced live module filtering. |
+| `apps/zync-app/src/os/wm-geometry.ts` | Keep adapter; port geometry | Move first desktop-icon-column initial-window clearance into package geometry. |
+| `apps/zync-app/src/routes/index.tsx` | Manual route merge | Preserve redirect query strings for true aliases, but keep Advanced `/dashboard` rendering `HomePage` in-place; never restore unconditional `/dashboard -> /`. |
+| `apps/zync-app/tests/e2e/os-shell/09-url-contract.spec.ts` | Adapt/combine tests | Keep Advanced package URL engine and add Invariantum query-retention checks only for genuine redirect aliases. |
+| `apps/zync-app/tests/e2e/os-shell/22-mobile-baselines.mobile.spec.ts` | Adapt/combine tests | Keep Advanced authenticated/mock fixture, `data-app-id`, reset persistence and navigation ordering; carry stable identity/44px/sole-main assertions. |
+| `pnpm-lock.yaml` | Regenerate | Resolve manifests first, then regenerate/frozen-install. Never choose one side wholesale. |
+
+## Non-overlap semantic payload to preserve
+
+- Invariantum scan runner source-hash/topology/reuse integrity and auth adapter.
+- development-only service-worker suppression and correct realtime `ws://`/`wss://` scheme.
+- classic shell RTL grid placement and one-main-landmark runtime/source contracts.
+- locale/catalog corrections for navigation, expenses and invoices.
+- expense filter/bulk-action key correctness.
+- invoice draft and approval-sort catalog behavior.
+- shared Select overflow containment and sheet viewport anchoring.
+- desktop icon long-label wrapping.
+- inventory route ownership and authorization.
+- redirect query retention for marketing/time/profile aliases.
+- mobile launcher identity/overflow and stable independent mobile acceptance oracle.
+- desktop initial-window geometry and physical resize hit-target behavior in LTR/RTL.
+
+## Full Invariantum changed-file inventory
+
+| Status | Path | Area |
+| --- | --- | --- |
+| `M` | `.github/workflows/main.yml` | CI/workflows |
+| `M` | `.github/workflows/pr.yml` | CI/workflows |
+| `M` | `.gitignore` | Repository integration |
+| `A` | `.invariantum/adapters/zync-auth.mjs` | Invariantum runner/adapters |
+| `A` | `.invariantum/runner/package.json` | Invariantum runner/adapters |
+| `A` | `.invariantum/runner/run.mjs` | Invariantum runner/adapters |
+| `A` | `.invariantum/runner/run.test.mjs` | Invariantum runner/adapters |
+| `M` | `apps/zync-api/package.json` | API/storage |
+| `D` | `apps/zync-api/r2-cors/zync-storage.json` | API/storage |
+| `M` | `apps/zync-api/src/lib/portal-file-storage.ts` | API/storage |
+| `M` | `apps/zync-api/test/foundation-monorepo-config.test.ts` | API/storage |
+| `M` | `apps/zync-api/test/portal-file-storage.test.ts` | API/storage |
+| `D` | `apps/zync-api/test/r2-cors-config.test.ts` | API/storage |
+| `M` | `apps/zync-app/package.json` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/components/support/TicketListTable.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/features/expenses/ExpensesPage.tsx` | Expenses/i18n |
+| `A` | `apps/zync-app/src/features/invoices/InvoiceApprovalSortControl.tsx` | Invoices/i18n |
+| `M` | `apps/zync-app/src/lib/realtime/client.ts` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/modules/customers/CustomerListPage.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/modules/customers/tabs/CrossModuleTabs.tsx` | App routing/modules/runtime |
+| `A` | `apps/zync-app/src/modules/marketing.test.ts` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/modules/marketing.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/modules/projects/list/ProjectsTable.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/os/OsShell.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/__tests__/desktop-mounted-components.test.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/__tests__/os-shell-store.test.ts` | OS runtime/acceptance |
+| `A` | `apps/zync-app/src/os/__tests__/registry-selectors.test.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/__tests__/window-frame.test.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/__tests__/wm-geometry.test.ts` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/desktop/Desktop.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/desktop/DesktopIcon.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/mobile/MobileAppFrame.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/mobile/MobileAppIcon.tsx` | OS runtime/acceptance |
+| `A` | `apps/zync-app/src/os/mobile/MobileHome.test.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/mobile/MobileHome.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/mobile/mobile-surfaces.test.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/os-shell-store.ts` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/registry-os.ts` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/registry-selectors.ts` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/window/WindowFrame.tsx` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/os/wm-geometry.ts` | OS runtime/acceptance |
+| `M` | `apps/zync-app/src/pages/invoices/InvoiceDetailPage.tsx` | Invoices/i18n |
+| `M` | `apps/zync-app/src/pages/invoices/InvoiceNewPage.tsx` | Invoices/i18n |
+| `M` | `apps/zync-app/src/pages/invoices/InvoicePage.tsx` | Invoices/i18n |
+| `M` | `apps/zync-app/src/pages/reports/TimeReportPage.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/pages/time/TeamOverviewPage.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/pages/time/TimePage.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/push/usePushOptIn.ts` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/routes/index.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/routes/invoices/approvals.tsx` | Invoices/i18n |
+| `M` | `apps/zync-app/src/routes/reports/ar-aging/AgingTable.tsx` | App routing/modules/runtime |
+| `M` | `apps/zync-app/src/shell/Shell.tsx` | Classic shell/landmarks/RTL |
+| `M` | `apps/zync-app/src/shell/Sidebar.tsx` | Classic shell/landmarks/RTL |
+| `A` | `apps/zync-app/src/shell/nav-model.test.ts` | Classic shell/landmarks/RTL |
+| `M` | `apps/zync-app/src/shell/nav-model.ts` | Classic shell/landmarks/RTL |
+| `A` | `apps/zync-app/src/shell/shell-layout.test.tsx` | Classic shell/landmarks/RTL |
+| `M` | `apps/zync-app/test/app-shell-sidebar.test.tsx` | Classic shell/landmarks/RTL |
+| `A` | `apps/zync-app/test/classic-main-landmark-source.test.ts` | Classic shell/landmarks/RTL |
+| `M` | `apps/zync-app/test/data-table-standardization.test.ts` | App routing/modules/runtime |
+| `A` | `apps/zync-app/test/expense-filter-catalog.test.tsx` | Expenses/i18n |
+| `M` | `apps/zync-app/test/inventory-routing.test.tsx` | App routing/modules/runtime |
+| `A` | `apps/zync-app/test/invoice-approval-sort-control.test.tsx` | Invoices/i18n |
+| `A` | `apps/zync-app/test/invoice-drafts-catalog.test.ts` | Invoices/i18n |
+| `A` | `apps/zync-app/test/push-service-worker-registration.test.tsx` | App routing/modules/runtime |
+| `A` | `apps/zync-app/test/realtime-client.test.ts` | App routing/modules/runtime |
+| `M` | `apps/zync-app/tests/e2e/os-shell/09-url-contract.spec.ts` | OS runtime/acceptance |
+| `M` | `apps/zync-app/tests/e2e/os-shell/22-mobile-baselines.mobile.spec.ts` | OS runtime/acceptance |
+| `A` | `apps/zync-app/tests/e2e/os-shell/23-desktop-geometry.spec.ts` | OS runtime/acceptance |
+| `A` | `apps/zync-app/tests/e2e/os-shell/24-classic-main-landmarks.spec.ts` | OS runtime/acceptance |
+| `A` | `apps/zync-app/tests/invoice-approvals-catalog.spec.mjs` | Invoices/i18n |
+| `M` | `apps/zync-www/scripts/verify-client-config.test.mjs` | Repository integration |
+| `A` | `docs/plans/2026-08-09-zync-invariantum-gpt-workflow.md` | Docs/contracts |
+| `A` | `docs/plans/INDEX.md` | Docs/contracts |
+| `M` | `docs/specs/2026-05-30-time-management.md` | Docs/contracts |
+| `M` | `docs/specs/2026-07-04-inventory-management-design.md` | Docs/contracts |
+| `M` | `docs/specs/2026-07-10-zync-os-desktop-design.md` | Docs/contracts |
+| `M` | `docs/specs/2026-07-11-zync-os-mobile-design.md` | Docs/contracts |
+| `M` | `docs/specs/2026-07-12-zync-os-desktop-qa-recovery-design.md` | Docs/contracts |
+| `M` | `docs/specs/2026-08-07-settings-logo-upload-design.md` | Docs/contracts |
+| `M` | `packages/ui/package.json` | Shared UI/i18n |
+| `M` | `packages/ui/src/i18n/en.json` | Shared UI/i18n |
+| `M` | `packages/ui/src/i18n/he.json` | Shared UI/i18n |
+| `M` | `packages/ui/src/overlays/sheet.tsx` | Shared UI/i18n |
+| `M` | `packages/ui/src/primitives/select.tsx` | Shared UI/i18n |
+| `M` | `packages/ui/test/foundation-design-system.test.tsx` | Shared UI/i18n |
+| `A` | `packages/ui/test/select-value-layout.test.tsx` | Shared UI/i18n |
+| `M` | `pnpm-lock.yaml` | Dependency lock |
+
+## Reconciliation rule
+
+The final Phase-H integration must be semantic. In particular, files that Advanced extracted into `packages/os-shell` must **not** be resurrected from this branch. Port the corresponding Invariantum behavioral fixes/tests into the package-owned destination, regenerate the lockfile after manifest resolution, and rerun both streams’ protecting gates.
diff --git a/docs/plans/INDEX.md b/docs/plans/INDEX.md
new file mode 100644
index 000000000..10210a754
--- /dev/null
+++ b/docs/plans/INDEX.md
@@ -0,0 +1,5 @@
+# Project Plan Index
+
+Audience: AI coding agents first.
+
+- [Zync Invariantum GPT workflow](2026-08-09-zync-invariantum-gpt-workflow.md) — ACTIVE; 14/21 root causes approved, remaining repair/review clustered, broad gate deferred until all focused lanes clean.
diff --git a/docs/specs/2026-07-04-inventory-management-design.md b/docs/specs/2026-07-04-inventory-management-design.md
index 9986dd1ba..f25ec8792 100644
--- a/docs/specs/2026-07-04-inventory-management-design.md
+++ b/docs/specs/2026-07-04-inventory-management-design.md
@@ -89,6 +89,10 @@ Pages (design-token / zc-ui-dev conventions; responsive):
 - `/inventory/locations` — CRUD locations, set default.
 - `/inventory/reports` — valuation report (as-of date), COGS (period), **מפקד מלאי** count report export.
 - Product form (extend spec 85): "Track inventory" toggle → provisions stock item; opening-balance entry.
+- OS shell ownership: `/inventory` and descendants resolve to the existing `invoices` OS application surface. The canonical declaration lives in `registry-os.ts` alongside the unowned-route completeness allowlist; `/inventory` is not allowlisted as unowned and must have exactly one owner. `resolveRouteOwner` consumes that registry declaration rather than maintaining a selector-local copy.
+- The route-level invoices module guard, the `inventory:read` boundary, `inventory:write` and `inventory:manage` capabilities, tenant isolation, locale behavior, and explicit `shell=classic` routing remain unchanged. OS ownership never substitutes for route authorization.
+
+**Rationale (2026-08-12):** Inventory is a host-owned invoices capability rather than a standalone manifest module. Keeping its prefix in the canonical OS registry establishes one auditable owner, while mounted permission matrices prove that users without `inventory:read` cannot expose inventory data in either shell.
 
 ## B.6 RBAC, isolation, migration
 
diff --git a/docs/specs/2026-07-11-zync-os-mobile-design.md b/docs/specs/2026-07-11-zync-os-mobile-design.md
index dba6b529c..6918c3360 100644
--- a/docs/specs/2026-07-11-zync-os-mobile-design.md
+++ b/docs/specs/2026-07-11-zync-os-mobile-design.md
@@ -7,7 +7,7 @@ Slug: `zync-os-mobile` · Date: 2026-07-11 · Audience: AI coding agents (run-pl
 Two deliverables, one plan:
 
 1. **Mobile OS shell** (`MobileShell`): home screen with icon grid + dock + widgets, app drawer, full-screen app frames with OS-style Back/Home/Recents navigation, recent-apps card switcher, notification shade, bottom sheets — per `references/mobile.md` physics. Replaces the landed "mobile forces classic" rule: mobile OS is the DEFAULT on mobile devices, same precedence chain as desktop.
-2. **Module migration sweep**: every remaining module area gains `os` manifest metadata + app-side bindings so it opens windowed on desktop AND full-screen on mobile. After the sweep, the "no-os-metadata → classic deep-link" degradation path has ZERO remaining **manifest-backed in-scope** occupants (`/inventory` and the §2 UNOWNED allowlist intentionally remain on classic fallback; classic shell survives as the legacy fallback only).
+2. **Module migration sweep**: every remaining module area gains `os` manifest metadata + app-side bindings so it opens windowed on desktop AND full-screen on mobile. After the sweep, the "no-os-metadata → classic deep-link" degradation path has ZERO remaining **manifest-backed in-scope** occupants (the §2 UNOWNED allowlist intentionally remains on classic fallback; classic shell survives as the legacy fallback only).
 
 Premium feel is enforced by `.claude/skills/zc-ui-ux-designer` (SKILL.md + `references/mobile.md`; desktop.md for sweep windows) — REQUIRED READING for every UI task; vision judge grades against it. This spec does not restate motion/material/gesture values.
 
@@ -74,7 +74,7 @@ mobile?: {
 | `/projects` | projects |
 | `/calendar` | calendar |
 | `/time-track`, `/time/approvals` | time_management (exact `/time` is a `Navigate` redirect alias → `/time-track`, NOT an owned prefix — but `/time/approvals` is a live non-redirect route and IS owned) |
-| `/invoices`, `/receipts`, `/payments` | invoices |
+| `/invoices`, `/receipts`, `/payments`, `/inventory` | invoices |
 | `/expenses`, `/vendors` | expenses |
 | `/crm` | crm |
 | `/marketing`, `/proposals`, `/contracts` | marketing |
@@ -88,11 +88,13 @@ mobile?: {
 | `/settings/ai`, `/admin/ai` | ai_assistant (landed — longest-prefix beats settings' `/settings` and the `/admin` allowlist entry) |
 | billing module | `routePrefixes: []`, `defaultRoute: '/settings/billing'` — opens as a settings deep link, no prefix ownership |
 
-`UNOWNED_ROUTE_PREFIXES` allowlist (exported const in `registry-os.ts`): auth/onboarding routes, `/portal`, `/contractor-portal`, `/admin` (EXCEPT `/admin/ai` — owned, longest-prefix), `/design-system`, `/search` (OS mode maps the `/search` deep link to opening the command center over home/desktop), `/offline.html`, **`/inventory`** (module has no manifest id yet — pending inventory-management plan; classic fallback until that plan registers it and moves the prefix into the table).
-- **Redirect aliases resolve BEFORE ownership** (canonical-path rule): `/dashboard` → `/` (today) and `/profile/notifications` → `/settings/notifications` (settings) are router `Navigate` redirects — ownership applies to the post-redirect canonical path. The manifest task REMOVES `/profile/notifications` from the landed notifications `routePrefixes` (it is an alias, not a canonical route).
-- **Completeness invariant:** vitest test statically imports the route registry and fails on any top-level prefix that is neither owned by exactly one module nor allowlisted (redirect-only paths exempt). Doubly-owned → fail. Longest-prefix wins at resolution time.
+`UNOWNED_ROUTE_PREFIXES` allowlist (exported const in `registry-os.ts`): auth/onboarding routes, `/portal`, `/contractor-portal`, `/admin` (EXCEPT `/admin/ai` — owned, longest-prefix), `/design-system`, `/search` (OS mode maps the `/search` deep link to opening the command center over home/desktop), `/offline.html`. `/inventory` is explicitly excluded: the same canonical registry exports one host-owned prefix binding from `/inventory` to `invoices`.
+- **Redirect aliases resolve BEFORE ownership** (canonical-path rule): `/dashboard` → `/` (today), `/marketing` → `/marketing/pipeline` (marketing), `/time` → `/time-track` (time management), and `/profile/notifications` → `/settings/notifications` (settings) are router `Navigate` redirects — ownership applies to the post-redirect canonical path. Every alias preserves the complete incoming query string, including an explicit `shell=classic|os` override, duplicate values, and unrelated parameters. The manifest task REMOVES `/profile/notifications` from the landed notifications `routePrefixes` (it is an alias, not a canonical route).
+- **Completeness invariant:** vitest statically imports the route registry and fails on any top-level prefix that is neither owned by exactly one module nor allowlisted (redirect-only paths exempt). `/inventory` must occur exactly once with owner `invoices`; a missing, wrong, or duplicate declaration fails closed. Longest-prefix wins at resolution time.
 - **Single-owner file rule:** ALL manifest `os` metadata for the sweep is written by ONE Wave-1 task (`manifest.ts` is monolithic — parallel cluster tasks MUST NOT touch it). Same task adds `mobile` blocks to already-landed entries and the aggregator imports in `registry-os.ts`; both files then freeze for the plan. Cluster tasks own only their disjoint `bindings/<module>.ts` + module route dirs + specs.
 
+**Rationale (2026-08-12):** Inventory remains guarded by the invoices host module, so the canonical registry assigns `/inventory` to the invoices OS application without introducing a new module id or weakening `inventory:read`. Mounted LTR/RTL coverage exercises both shell overrides for every changed alias and verifies a stable canonical URL with preserved query state.
+
 ### 3. Mobile shell store (`apps/zync-app/src/os/mobile/mobile-shell-store.ts`)
 
 New Zustand store — desktop `os-shell-store` is NOT reused (rect/z/snap vs stack/suspend are disjoint state machines). Registry, bindings, url-sync engine, layout-persistence client, and `shell-layout-schema.ts` ARE reused.
@@ -161,7 +163,7 @@ type OsHistoryState = { zync?:
 - **Cold-entry seeding:** deep-link cold entry `replaceState`s the current entry with the opened app's state; a `/home` entry is NOT synthesized beneath it — browser back at a cold-entry app root exits the site (standard PWA behavior; the nav-bar Home button is the in-app path home). Back-returns-to-home applies only to warm navigation where a real `/home` entry exists.
 - `/home` is the mobile twin of `/desktop`: OS-only; `rewriteClassicDesktopPath` generalizes to both aliases (`/desktop`|`/home` → `/` in classic; cross-rewrite between device classes in OS mode).
 - Deep link cold entry (incl. push tap via `extractNotificationTargetUrl`): resolve owner via `routePrefixes` → open full-screen at URL. Unowned → classic fallback for the session + `deep_link_unowned` counter.
-- Route-addressability acceptance: `/customers/:id`, `/tasks/:id`, `/invoices/:id` cold-open the correct full-screen app.
+- Route-addressability acceptance: `/customers/:id`, `/tasks/:id`, `/invoices/:id`, and `/inventory` cold-open the correct full-screen app.
 
 ### 6. Persistence (mobile slot + schema unification — one Wave-1 task)
 
@@ -220,11 +222,13 @@ Same 8 layers + gate tiering + suite root. Wave-1 scaffold deltas:
 ## Accessibility contract (mobile deltas; landed contract still applies)
 
 - Home/drawer/dock/recents: roving tabindex; full hardware-keyboard operation (Enter opens, arrows move, Del removes-from-home with announcer confirm).
-- `MobileAppFrame` labelled `role="region"`; nav bar `role="toolbar"`; shade/drawer/recents/sheets `role="dialog"` focus-trapped + focus-return; recents cards focusable, Delete = dismiss (guard-aware).
+- `MobileAppFrame` labelled `role="region"` and owns the sole visible `main#main-content` for the mounted application; app route content must not introduce a nested `main`. The nav bar remains `role="toolbar"`; shade/drawer/recents/sheets remain `role="dialog"` focus-trapped + focus-return; recents cards are focusable, Delete = dismiss (guard-aware).
 - All gestures have visible button twins (slopgate-enforced). Touch targets ≥44px (axe + probe).
 - Announcer reused: app open/close/suspend, shade open, dismiss-all, guard-blocked dismissal.
 - axe-core on every mobile surface (no serious/critical), both `dir` values.
 
+**Rationale (2026-08-12):** The mobile home and classic shell already own their main landmarks. An opened mobile application frame owns the sole accessible `main#main-content`; native `<main>` elements and explicit `role="main"` descendants supplied by routed content are neutralized without removing their content, including descendants inserted after navigation settles. Back, Home, and Recents remain outside that landmark in the labelled frame region.
+
 ## Usability & support mitigations
 
 - First-run coach marks: 3 steps (drawer, nav bar, shade) — once, skippable, flag in mobile layout payload.
diff --git a/docs/specs/2026-07-12-zync-os-desktop-qa-recovery-design.md b/docs/specs/2026-07-12-zync-os-desktop-qa-recovery-design.md
index a860fd3f6..230983c92 100644
--- a/docs/specs/2026-07-12-zync-os-desktop-qa-recovery-design.md
+++ b/docs/specs/2026-07-12-zync-os-desktop-qa-recovery-design.md
@@ -8,7 +8,7 @@ The Desktop OS keeps the familiar spatial model of a desktop and taskbar while a
 
 ## Behavioral contract
 
-- Windows snap to either screen edge and resize from all eight edges/corners; app manifests provide responsive minimum and initial sizes.
+- Windows snap to either screen edge and resize from all eight edges/corners; app manifests provide responsive minimum and initial sizes. Initial normal windows clear the first desktop icon column, and every resize handle provides a 24px minimum pointer target.
 - The shell occupies the viewport without page scrolling. Start, Settings, Today, tray controls, notification controls, and lock/sign-out actions are interactive. Locked mode unmounts interactive shell chrome until password reauthentication succeeds.
 - Start, tray, and notification panels use the shell material, z-index, and reduced-motion contracts with stable `data-fx` hooks.
 - Desktop icons use the shared context-menu primitive. Keyboard and double-click activation remain available.
diff --git a/packages/os-shell/src/__tests__/core/os-shell-store.test.ts b/packages/os-shell/src/__tests__/core/os-shell-store.test.ts
index 6704bd298..1fa6b8e71 100644
--- a/packages/os-shell/src/__tests__/core/os-shell-store.test.ts
+++ b/packages/os-shell/src/__tests__/core/os-shell-store.test.ts
@@ -34,7 +34,7 @@ describe('shell store', () => {
     expect(shellWindow).toMatchObject({
       moduleId: 'customers',
       location: { pathname: '/apps/customers', search: '', hash: '' },
-      rect: { x: 96, y: 72, w: 800, h: 560 },
+      rect: { x: 104, y: 72, w: 800, h: 560 },
     })
   })
 
@@ -62,6 +62,20 @@ describe('shell store', () => {
     expect(committedLayout.widgets[0]).not.toHaveProperty('appId')
   })
 
+  it('preserves a single-instance deep route when reopened without an explicit location', () => {
+    store.getState().openWindow('tasks', { pathname: '/apps/tasks/42', search: '?tab=notes', hash: '#item' })
+    const instanceId = store.getState().windows[0]!.instanceId
+
+    store.getState().openWindow('tasks')
+
+    expect(store.getState().windows).toEqual([
+      expect.objectContaining({
+        instanceId,
+        location: { pathname: '/apps/tasks/42', search: '?tab=notes', hash: '#item' },
+      }),
+    ])
+  })
+
   it('keeps a single-instance app at its existing identity and updates its location', () => {
     store.getState().openWindow('tasks')
     const instanceId = store.getState().windows[0]!.instanceId
diff --git a/packages/os-shell/src/__tests__/core/wm-geometry.test.ts b/packages/os-shell/src/__tests__/core/wm-geometry.test.ts
index 51229bb44..1448c966a 100644
--- a/packages/os-shell/src/__tests__/core/wm-geometry.test.ts
+++ b/packages/os-shell/src/__tests__/core/wm-geometry.test.ts
@@ -3,7 +3,7 @@ import { clampWindowRect, responsiveWindowRect, snapZoneForPointer } from '../..
 
 describe('portable window geometry', () => {
   it('derives minimum-size-aware rectangles without host metadata access', () => {
-    expect(responsiveWindowRect({ w: 1200, h: 800 }, { w: 720, h: 480 })).toEqual({ x: 96, y: 72, w: 800, h: 560 })
+    expect(responsiveWindowRect({ w: 1200, h: 800 }, { w: 720, h: 480 })).toEqual({ x: 104, y: 72, w: 800, h: 560 })
     expect(clampWindowRect({ x: 0, y: 0, w: 1, h: 1 }, { w: 1200, h: 800 }, { minSize: { w: 720, h: 480 } })).toEqual({ x: 0, y: 0, w: 720, h: 480 })
   })
 
diff --git a/packages/os-shell/src/core/os-shell-store.ts b/packages/os-shell/src/core/os-shell-store.ts
index 67d400e92..709ae03eb 100644
--- a/packages/os-shell/src/core/os-shell-store.ts
+++ b/packages/os-shell/src/core/os-shell-store.ts
@@ -368,7 +368,9 @@ export function createShellStore<TAppId extends string = string>(options: ShellS
         commit()
       },
       setWindowTitle: (instanceId, title) => updateActive(get().windows.map((window) => window.instanceId === instanceId ? { ...window, title } : window)),
-      openWindow: (appId, location = getDefaultLocation(appId), launchOrigin) => {
+      openWindow: (appId, location, launchOrigin) => {
+        const hasExplicitLocation = location !== undefined
+        const requestedLocation = location ?? getDefaultLocation(appId)
         if (options.isAppAvailable && !options.isAppAvailable(appId)) return
         const state = get()
         const existing = !options.supportsMultipleInstances(appId)
@@ -382,7 +384,7 @@ export function createShellStore<TAppId extends string = string>(options: ShellS
           }
           const nextWindows = focus(
             state.windows.map((window) => window.instanceId === existing.instanceId
-              ? { ...window, location, state: 'normal' as const, mounted: true }
+              ? { ...window, ...(hasExplicitLocation ? { location: requestedLocation } : {}), state: 'normal' as const, mounted: true }
               : window) as NormalizedWindow<TAppId>[],
             existing.instanceId,
             ++usageSequence,
@@ -404,7 +406,7 @@ export function createShellStore<TAppId extends string = string>(options: ShellS
         const window: NormalizedWindow<TAppId> = {
           instanceId: createInstanceId(),
           moduleId: appId,
-          location,
+          location: requestedLocation,
           rect: getInitialWindowRect(appId, options.viewport, getWindowMinSize),
           state: 'normal',
           launchOrigin,
diff --git a/packages/os-shell/src/core/wm-geometry.ts b/packages/os-shell/src/core/wm-geometry.ts
index c4f7575f4..a7c16ce35 100644
--- a/packages/os-shell/src/core/wm-geometry.ts
+++ b/packages/os-shell/src/core/wm-geometry.ts
@@ -28,6 +28,9 @@ export type WindowResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 's
 const SNAP_EDGE_PX = 12
 const TITLEBAR_GRIP_PX = 24
 export const DESKTOP_TASKBAR_INSET_PX = 48
+export const DESKTOP_GRID_INSET_PX = 16
+export const DESKTOP_CELL_WIDTH = 88
+export const DESKTOP_CELL_HEIGHT = 92
 export const DEFAULT_WINDOW_MIN_SIZE: WindowSize = { w: 320, h: 240 }
 
 export function getDesktopViewport(viewport: WindowViewport): WindowViewport {
@@ -47,7 +50,7 @@ export function responsiveWindowRect(viewport: WindowViewport, minSize: Partial<
   const windowHeight = Math.min(height, Math.max(minSize.h ?? DEFAULT_WINDOW_MIN_SIZE.h, 560))
 
   return {
-    x: Math.max(0, Math.min(96, viewport.w - width)),
+    x: Math.max(0, Math.min(DESKTOP_GRID_INSET_PX + DESKTOP_CELL_WIDTH, viewport.w - width)),
     y: Math.max(0, Math.min(72, height - windowHeight)),
     w: width,
     h: windowHeight,
diff --git a/packages/os-shell/src/desktop/Desktop.tsx b/packages/os-shell/src/desktop/Desktop.tsx
index 2bc5c28f9..a1a04a0b5 100644
--- a/packages/os-shell/src/desktop/Desktop.tsx
+++ b/packages/os-shell/src/desktop/Desktop.tsx
@@ -7,9 +7,9 @@ import { Wallpaper } from './Wallpaper'
 import { WidgetFrame } from './WidgetFrame'
 import { FolderSurface, FolderTrigger } from '../features/folders/ui'
 import type { FolderRecord } from '../features/folders/model'
+import { DESKTOP_CELL_HEIGHT, DESKTOP_CELL_WIDTH } from '../core/wm-geometry'
+export { DESKTOP_CELL_HEIGHT, DESKTOP_CELL_WIDTH } from '../core/wm-geometry'
 
-export const DESKTOP_CELL_WIDTH = 88
-export const DESKTOP_CELL_HEIGHT = 92
 const DEFAULT_GRID_ROWS = 10
 
 export interface DesktopApp<TAppId extends string = string> {
@@ -85,7 +85,8 @@ export function Desktop<TAppId extends string = string>({ apps, desktopIcons, wi
       return
     }
     const bounds = surface.getBoundingClientRect()
-    const column = Math.max(0, Math.floor((clientX - bounds.left) / DESKTOP_CELL_WIDTH))
+    const horizontalOffset = dir === 'rtl' ? bounds.right - clientX : clientX - bounds.left
+    const column = Math.max(0, Math.floor(horizontalOffset / DESKTOP_CELL_WIDTH))
     const row = Math.max(0, Math.floor((clientY - bounds.top) / DESKTOP_CELL_HEIGHT))
     const requestedCell = column * DEFAULT_GRID_ROWS + row
     const occupied = new Set(desktopIcons.filter((candidate) => candidate.moduleId !== appId).map((candidate) => candidate.cell))
diff --git a/packages/os-shell/src/desktop/DesktopIcon.tsx b/packages/os-shell/src/desktop/DesktopIcon.tsx
index 326e7f960..089498f9e 100644
--- a/packages/os-shell/src/desktop/DesktopIcon.tsx
+++ b/packages/os-shell/src/desktop/DesktopIcon.tsx
@@ -20,7 +20,7 @@ export function DesktopIcon({ appId, label, icon, selected, dragging = false, ta
     <button
       ref={ref}
       type="button"
-      className="group flex h-full w-full flex-col items-center justify-start gap-2 rounded p-2 text-center text-body-2 text-ink outline-none transition-transform duration-200 ease-out focus-visible:ring-2 focus-visible:ring-accent-border data-[selected=true]:bg-accent-soft data-[dragging=true]:scale-105"
+      className="group flex h-full w-full flex-col items-center justify-start gap-1 rounded p-2 text-center text-body-2 text-ink outline-none transition-transform duration-200 ease-out focus-visible:ring-2 focus-visible:ring-accent-border data-[selected=true]:bg-accent-soft data-[dragging=true]:scale-105"
       aria-pressed={selected}
       aria-label={`Open ${label}`}
       data-desktop-icon={label}
@@ -40,8 +40,8 @@ export function DesktopIcon({ appId, label, icon, selected, dragging = false, ta
       onPointerUp={onPointerUp}
       onPointerCancel={onPointerCancel}
     >
-      <span className="flex size-10 items-center justify-center rounded bg-surface-raised text-ink" aria-hidden="true">{icon ?? label.slice(0, 1)}</span>
-      <span className="max-w-full rounded bg-surface px-1 leading-tight shadow-sm">{label}</span>
+      <span className="flex size-8 items-center justify-center rounded bg-surface-raised text-ink" aria-hidden="true">{icon ?? label.slice(0, 1)}</span>
+      <span className="w-full min-w-0 break-words rounded bg-surface px-1 leading-tight shadow-sm">{label}</span>
     </button>
   )
 }
diff --git a/packages/os-shell/src/desktop/useIconDrag.ts b/packages/os-shell/src/desktop/useIconDrag.ts
index 03845e991..4bc789516 100644
--- a/packages/os-shell/src/desktop/useIconDrag.ts
+++ b/packages/os-shell/src/desktop/useIconDrag.ts
@@ -5,21 +5,36 @@ export interface IconDragState {
   appId: string | null
 }
 
+const DRAG_THRESHOLD_PX = 4
+
 export function useIconDrag(onDrop: (appId: string, clientX: number, clientY: number) => void) {
   const [drag, setDrag] = React.useState<IconDragState>({ active: false, appId: null })
+  const start = React.useRef<{ appId: string; clientX: number; clientY: number } | null>(null)
 
   const onPointerDown = React.useCallback((appId: string, event: React.PointerEvent) => {
     if (event.button !== 0) return
     event.currentTarget.setPointerCapture(event.pointerId)
+    start.current = { appId, clientX: event.clientX, clientY: event.clientY }
     setDrag({ active: true, appId })
   }, [])
 
   const onPointerUp = React.useCallback((event: React.PointerEvent) => {
-    if (drag.appId) onDrop(drag.appId, event.clientX, event.clientY)
+    const origin = start.current
+    if (origin) {
+      const deltaX = event.clientX - origin.clientX
+      const deltaY = event.clientY - origin.clientY
+      if ((deltaX * deltaX) + (deltaY * deltaY) >= DRAG_THRESHOLD_PX * DRAG_THRESHOLD_PX) {
+        onDrop(origin.appId, event.clientX, event.clientY)
+      }
+    }
+    start.current = null
     setDrag({ active: false, appId: null })
-  }, [drag.appId, onDrop])
+  }, [onDrop])
 
-  const onPointerCancel = React.useCallback(() => setDrag({ active: false, appId: null }), [])
+  const onPointerCancel = React.useCallback(() => {
+    start.current = null
+    setDrag({ active: false, appId: null })
+  }, [])
 
   return { drag, onPointerDown, onPointerUp, onPointerCancel }
 }
diff --git a/packages/os-shell/src/mobile/MobileAppFrame.tsx b/packages/os-shell/src/mobile/MobileAppFrame.tsx
index b1843d26b..2db70766a 100644
--- a/packages/os-shell/src/mobile/MobileAppFrame.tsx
+++ b/packages/os-shell/src/mobile/MobileAppFrame.tsx
@@ -3,6 +3,31 @@ import type { SerializedLocation } from '../contracts'
 import { WindowRouter, type WindowRouterWindow } from '../window/WindowRouter'
 import { MobileNavigationBar } from './MobileNavigationBar'
 
+const DESCENDANT_MAIN_SELECTOR = 'main, [role="main"]'
+const DUPLICATE_MAIN_ID_SELECTOR = '#main-content'
+const NEUTRAL_MAIN_ROLE = 'none'
+
+export function neutralizeDescendantMainLandmarks(root: HTMLElement): void {
+  root.querySelectorAll<HTMLElement>(DUPLICATE_MAIN_ID_SELECTOR).forEach((duplicate) => {
+    duplicate.removeAttribute('id')
+  })
+  root.querySelectorAll<HTMLElement>(DESCENDANT_MAIN_SELECTOR).forEach((landmark) => {
+    if (landmark === root) return
+    if (landmark.getAttribute('role') !== NEUTRAL_MAIN_ROLE) landmark.setAttribute('role', NEUTRAL_MAIN_ROLE)
+    if (landmark.getAttribute('data-mobile-main-neutralized') !== 'true') landmark.setAttribute('data-mobile-main-neutralized', 'true')
+  })
+}
+
+export function enforceFrameMainOwnership(root: HTMLElement): () => void {
+  const normalize = () => neutralizeDescendantMainLandmarks(root)
+  normalize()
+  const observer = new MutationObserver((records) => {
+    if (records.some((record) => record.type === 'childList' || record.type === 'attributes')) normalize()
+  })
+  observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['id', 'role'] })
+  return () => observer.disconnect()
+}
+
 export interface MobileAppFrameProps {
   app: WindowRouterWindow & { title: string }
   children: React.ReactNode
@@ -14,6 +39,21 @@ export interface MobileAppFrameProps {
 }
 
 export function MobileAppFrame({ app, children, onNavigate, onBack, onHome, onRecents, onTitleChange }: MobileAppFrameProps): React.JSX.Element {
+  const mainRef = React.useRef<HTMLElement>(null)
   const { title, ...window } = app
-  return <section role="region" aria-label={title || 'Application'} className="flex min-h-dvh flex-col bg-bg text-ink" data-shell data-mobile-frame data-presentation="mobile-frame"><div className="min-h-0 flex-1 overflow-auto" data-module-content><WindowRouter window={window} onNavigate={onNavigate} onTitleChange={onTitleChange}>{children}</WindowRouter></div><MobileNavigationBar onBack={onBack} onHome={onHome} onRecents={onRecents} /></section>
+
+  React.useLayoutEffect(() => {
+    const main = mainRef.current
+    if (!main) return undefined
+    return enforceFrameMainOwnership(main)
+  }, [app.instanceId])
+
+  return (
+    <section role="region" aria-label={title || 'Application'} className="flex min-h-dvh flex-col bg-bg text-ink" data-shell data-mobile-frame data-presentation="mobile-frame">
+      <main ref={mainRef} id="main-content" className="min-h-0 flex-1 overflow-auto" data-module-content>
+        <WindowRouter window={window} onNavigate={onNavigate} onTitleChange={onTitleChange}>{children}</WindowRouter>
+      </main>
+      <MobileNavigationBar onBack={onBack} onHome={onHome} onRecents={onRecents} />
+    </section>
+  )
 }
diff --git a/packages/os-shell/src/mobile/MobileAppIcon.tsx b/packages/os-shell/src/mobile/MobileAppIcon.tsx
index 9104f2576..ad4191a46 100644
--- a/packages/os-shell/src/mobile/MobileAppIcon.tsx
+++ b/packages/os-shell/src/mobile/MobileAppIcon.tsx
@@ -26,7 +26,7 @@ export function MobileAppIcon({ app, badge, tabIndex = 0, buttonRef, onOpen, onR
         else if (buttonRef) buttonRef.current = element
       }}
       type="button"
-      className="relative flex min-h-11 min-w-11 flex-col items-center gap-1 rounded-[var(--radius-shell-frame)] p-2 text-center text-body-2 text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-border"
+      className="relative flex w-full min-h-11 min-w-0 max-w-full flex-col items-center gap-1 rounded-[var(--radius-shell-frame)] p-2 text-center text-body-2 text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-border"
       data-fx="app-open"
       data-app-id={app.appId}
       aria-label={`Open ${app.label}`}
@@ -39,7 +39,7 @@ export function MobileAppIcon({ app, badge, tabIndex = 0, buttonRef, onOpen, onR
       }}
     >
       <span className="flex size-11 items-center justify-center rounded-[var(--radius-shell-frame)] bg-surface text-ink shadow-[var(--shadow-shell)]" aria-hidden="true">{app.icon}</span>
-      <span className="max-w-full truncate">{app.label}</span>
+      <span className="min-w-0 max-w-full truncate">{app.label}</span>
       {badge && badge > 0 ? <span className="absolute end-1 top-1 min-w-5 rounded-full bg-danger px-1 text-caption text-on-accent">{badge > 99 ? '99+' : badge}</span> : null}
     </button>
   )
diff --git a/packages/os-shell/src/mobile/MobileHome.tsx b/packages/os-shell/src/mobile/MobileHome.tsx
index fea616ff7..be81d6d0c 100644
--- a/packages/os-shell/src/mobile/MobileHome.tsx
+++ b/packages/os-shell/src/mobile/MobileHome.tsx
@@ -33,7 +33,7 @@ export function MobileHome({ apps, folders = [], dock, badges, tenantName, notif
   return <main className="flex min-h-dvh flex-col bg-bg text-ink" data-shell data-mobile-surface="home" data-gesture="drawer-open" data-branding-wallpaper={wallpaperUrl ? 'tenant' : 'default'} style={wallpaperUrl ? { backgroundImage: `linear-gradient(var(--z-wallpaper-dim, transparent), var(--z-wallpaper-dim, transparent)), url(${JSON.stringify(wallpaperUrl)})`, backgroundSize: 'cover', backgroundPosition: 'center' } : undefined}>
     <MobileTopStrip tenantName={tenantName} notificationCount={notificationCount} onOpenCommandCenter={onOpenCommandCenter} onOpenShade={onOpenNotifications} />
     <section className="grid flex-1 grid-cols-4 content-start gap-3 px-4 py-6" role="grid" aria-label="Apps">
-      {apps.map((app, index) => <div key={app.appId} role="gridcell"><MobileAppIcon app={app} badge={badges?.[app.appId]} buttonRef={(element) => { refs.current[index] = element }} tabIndex={activeIndex === index ? 0 : -1} onOpen={onOpen} onRove={(direction) => rove(index, direction)} /></div>)}
+      {apps.map((app, index) => <div key={app.appId} role="gridcell" className="min-w-0"><MobileAppIcon app={app} badge={badges?.[app.appId]} buttonRef={(element) => { refs.current[index] = element }} tabIndex={activeIndex === index ? 0 : -1} onOpen={onOpen} onRove={(direction) => rove(index, direction)} /></div>)}
       {folders.map((folder) => <div key={folder.folderId} role="gridcell"><FolderTrigger folder={folder} open={false} onOpen={() => onOpenFolder?.(folder)} /></div>)}
     </section>
     <button ref={drawerButtonRef} type="button" className="mx-auto min-h-11 min-w-11 rounded-[var(--radius-shell-frame)] px-4 text-body-2 focus-visible:ring-2 focus-visible:ring-accent-border" data-gesture-button="drawer-open" onClick={onOpenDrawer}>All apps</button>
diff --git a/packages/os-shell/src/taskbar/Taskbar.tsx b/packages/os-shell/src/taskbar/Taskbar.tsx
index ae2a5dfb3..57f9c8bf8 100644
--- a/packages/os-shell/src/taskbar/Taskbar.tsx
+++ b/packages/os-shell/src/taskbar/Taskbar.tsx
@@ -1,6 +1,5 @@
+import * as React from 'react'
 import { Bell, CircleAlert, Command, LayoutGrid, LoaderCircle, PanelBottom } from 'lucide-react'
-import { useEffect, useMemo, useRef, useState } from 'react'
-import type { ReactNode } from 'react'
 import { resolveTaskbarPlacement, shouldRevealTaskbar, taskbarDataAttributes, type TaskbarPosition, type TextDirection } from '../features/chrome/model'
 
 import { TaskbarApp, type TaskbarAppModel } from './TaskbarApp'
@@ -17,7 +16,7 @@ export interface TaskbarWindow {
 export interface TaskbarAppDefinition {
   appId: string
   label: string
-  icon: ReactNode
+  icon: React.ReactNode
 }
 
 export interface TaskbarModel {
@@ -84,7 +83,7 @@ export interface TaskbarProps {
   position?: TaskbarPosition
   autoHide?: boolean
   direction?: TextDirection
-  timer?: ReactNode
+  timer?: React.ReactNode
   onAppAction: (action: TaskbarAppAction) => void
   onCommand?: () => void
   onTaskView?: () => void
@@ -132,17 +131,17 @@ export function Taskbar({
   showTaskView = true,
   showSystemTray = true,
 }: TaskbarProps) {
-  const model = useMemo(
+  const model = React.useMemo(
     () => buildTaskbarModel({ pinnedAppIds, windows, badges }),
     [badges, pinnedAppIds, windows],
   )
-  const appById = useMemo(() => new Map(apps.map((app) => [app.appId, app])), [apps])
-  const buttonRefs = useRef(new Map<string, HTMLButtonElement>())
-  const [activeIndex, setActiveIndex] = useState(0)
-  const [revealed, setRevealed] = useState(!autoHide)
-  const placement = useMemo(() => resolveTaskbarPlacement(position, direction), [direction, position])
+  const appById = React.useMemo(() => new Map(apps.map((app) => [app.appId, app])), [apps])
+  const buttonRefs = React.useRef(new Map<string, HTMLButtonElement>())
+  const [activeIndex, setActiveIndex] = React.useState(0)
+  const [revealed, setRevealed] = React.useState(!autoHide)
+  const placement = React.useMemo(() => resolveTaskbarPlacement(position, direction), [direction, position])
 
-  useEffect(() => {
+  React.useEffect(() => {
     if (!autoHide) { setRevealed(true); return }
     const onPointerMove = (event: PointerEvent) => setRevealed(shouldRevealTaskbar({
       position, direction, autoHide, pointer: { x: event.clientX, y: event.clientY }, viewport: { width: window.innerWidth, height: window.innerHeight },
@@ -151,7 +150,7 @@ export function Taskbar({
     return () => window.removeEventListener('pointermove', onPointerMove)
   }, [autoHide, direction, position])
 
-  useEffect(() => {
+  React.useEffect(() => {
     for (const [appId, button] of buttonRefs.current) {
       onRegisterButtonRect?.(appId, button.getBoundingClientRect())
     }
diff --git a/packages/os-shell/src/taskbar/TaskbarApp.tsx b/packages/os-shell/src/taskbar/TaskbarApp.tsx
index c29ecdfc9..6c79acf90 100644
--- a/packages/os-shell/src/taskbar/TaskbarApp.tsx
+++ b/packages/os-shell/src/taskbar/TaskbarApp.tsx
@@ -1,4 +1,4 @@
-import type { Ref } from 'react'
+import * as React from 'react'
 
 import type { TaskbarAppDefinition, TaskbarWindow } from './Taskbar'
 
@@ -14,7 +14,7 @@ export interface TaskbarAppModel {
 export interface TaskbarAppProps {
   app: TaskbarAppModel & TaskbarAppDefinition
   tabIndex: number
-  buttonRef: Ref<HTMLButtonElement>
+  buttonRef: React.Ref<HTMLButtonElement>
   onClick: () => void
   onRove: (direction: -1 | 1) => void
 }
diff --git a/packages/os-shell/src/taskbar/TaskbarClock.tsx b/packages/os-shell/src/taskbar/TaskbarClock.tsx
index 346d14ebe..3e5c6c6e8 100644
--- a/packages/os-shell/src/taskbar/TaskbarClock.tsx
+++ b/packages/os-shell/src/taskbar/TaskbarClock.tsx
@@ -1,13 +1,13 @@
-import { useEffect, useState } from 'react'
+import * as React from 'react'
 
 function formatClock(now: Date) {
   return new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(now)
 }
 
 export function TaskbarClock() {
-  const [now, setNow] = useState(() => new Date())
+  const [now, setNow] = React.useState(() => new Date())
 
-  useEffect(() => {
+  React.useEffect(() => {
     const interval = window.setInterval(() => setNow(new Date()), 60_000)
     return () => window.clearInterval(interval)
   }, [])
diff --git a/packages/os-shell/src/window/WindowFrame.tsx b/packages/os-shell/src/window/WindowFrame.tsx
index dad72c608..a21479743 100644
--- a/packages/os-shell/src/window/WindowFrame.tsx
+++ b/packages/os-shell/src/window/WindowFrame.tsx
@@ -33,17 +33,18 @@ export interface WindowFrameProps {
   onCommit(): void
 }
 
-const RESIZE_HANDLES: readonly { handle: WindowResizeHandle; className: string }[] = [
-  { handle: 'n', className: 'inset-x-4 top-0 h-2 cursor-n-resize' },
-  { handle: 's', className: 'inset-x-4 bottom-0 h-2 cursor-s-resize' },
-  { handle: 'e', className: 'inset-y-4 end-0 w-2 cursor-e-resize' },
-  { handle: 'w', className: 'inset-y-4 start-0 w-2 cursor-w-resize' },
-  { handle: 'ne', className: 'end-0 top-0 size-4 cursor-ne-resize' },
-  { handle: 'nw', className: 'start-0 top-0 size-4 cursor-nw-resize' },
-  { handle: 'se', className: 'end-0 bottom-0 size-4 cursor-se-resize' },
-  { handle: 'sw', className: 'start-0 bottom-0 size-4 cursor-sw-resize' },
+const RESIZE_HANDLES: readonly { handle: WindowResizeHandle; className: string; style: React.CSSProperties }[] = [
+  { handle: 'n', className: 'cursor-n-resize', style: { top: 0, left: 24, right: 24, height: 24 } },
+  { handle: 's', className: 'cursor-s-resize', style: { bottom: 0, left: 24, right: 24, height: 24 } },
+  { handle: 'e', className: 'cursor-e-resize', style: { top: 24, bottom: 24, right: 0, width: 24 } },
+  { handle: 'w', className: 'cursor-w-resize', style: { top: 24, bottom: 24, left: 0, width: 24 } },
+  { handle: 'ne', className: 'cursor-ne-resize', style: { top: 0, right: 0, width: 24, height: 24 } },
+  { handle: 'nw', className: 'cursor-nw-resize', style: { top: 0, left: 0, width: 24, height: 24 } },
+  { handle: 'se', className: 'cursor-se-resize', style: { bottom: 0, right: 0, width: 24, height: 24 } },
+  { handle: 'sw', className: 'cursor-sw-resize', style: { bottom: 0, left: 0, width: 24, height: 24 } },
 ]
 
+
 function getViewport(): WindowViewport {
   if (typeof window === 'undefined') return getDesktopViewport({ w: 1280, h: 800 })
   return getDesktopViewport({
@@ -126,14 +127,14 @@ export function WindowFrame({
     drag.current = null
   }
 
-  const beginResize = (event: React.PointerEvent<HTMLButtonElement>, handle: WindowResizeHandle) => {
+  const beginResize = (event: React.PointerEvent<HTMLElement>, handle: WindowResizeHandle) => {
     event.stopPropagation()
     onFocus(frame.instanceId)
     resize.current = { x: event.clientX, y: event.clientY, rect: { ...frame.rect }, handle }
     event.currentTarget.setPointerCapture?.(event.pointerId)
   }
 
-  const moveResize = (event: React.PointerEvent<HTMLButtonElement>) => {
+  const moveResize = (event: React.PointerEvent<HTMLElement>) => {
     if (!resize.current) return
     onResize(frame.instanceId, resizeWindowRect(
       resize.current.rect,
@@ -154,6 +155,23 @@ export function WindowFrame({
     resize.current = null
   }
 
+  const resizeWithKeyboard = (event: React.KeyboardEvent<HTMLButtonElement>, handle: WindowResizeHandle) => {
+    const step = 16
+    const horizontal = handle.includes('e') || handle.includes('w')
+    const vertical = handle.includes('n') || handle.includes('s')
+    const delta = event.key === 'ArrowLeft' ? { x: -step, y: 0 }
+      : event.key === 'ArrowRight' ? { x: step, y: 0 }
+        : event.key === 'ArrowUp' ? { x: 0, y: -step }
+          : event.key === 'ArrowDown' ? { x: 0, y: step }
+            : null
+    if (!delta || (delta.x !== 0 && !horizontal) || (delta.y !== 0 && !vertical)) return
+    event.preventDefault()
+    event.stopPropagation()
+    onFocus(frame.instanceId)
+    onResize(frame.instanceId, resizeWindowRect(frame.rect, handle, delta, getViewport(), minSize))
+    onCommit()
+  }
+
   return (
     <section
       className="absolute isolate overflow-hidden border border-line bg-surface text-ink shadow-window"
@@ -176,7 +194,7 @@ export function WindowFrame({
         onLostPointerCapture={cancelDrag}
       >
         <span>{title}</span>
-        <span className="flex gap-1" onPointerDown={(event) => event.stopPropagation()}>
+        <span className="relative z-30 flex gap-1" onPointerDown={(event) => event.stopPropagation()}>
           {moveTargets.length > 0 && <Button type="button" size="icon" variant="ghost" aria-label="Move window to desktop" aria-haspopup="menu" aria-expanded={moveMenuOpen} onClick={() => setMoveMenuOpen((open) => !open)}>⋯</Button>}
           <Button type="button" size="icon" variant="ghost" aria-label="Minimize window" onClick={() => onMinimize(frame.instanceId)}>−</Button>
           <Button type="button" size="icon" variant="ghost" aria-label={frame.pinned ? 'Unpin window' : 'Keep window on top'} aria-pressed={frame.pinned ?? false} onClick={() => onTogglePinned?.(frame.instanceId)}>⌃</Button>
@@ -210,21 +228,21 @@ export function WindowFrame({
           {host.renderApp(frame.moduleId, frame.instanceId)}
         </ShellWindowHostContext.Provider>
       </div>
-      {RESIZE_HANDLES.map(({ handle, className }) => (
-        <Button
+      {RESIZE_HANDLES.map(({ handle, className, style }) => (
+        <button
           key={handle}
           type="button"
-          variant="ghost"
-          size="icon"
-          aria-label={'Resize window ' + handle}
+          aria-label={`Resize window ${handle}`}
           data-resize-handle={handle}
           data-window-resize-handle={handle}
-          className={'absolute opacity-0 ' + className}
+          className={'absolute z-20 border-0 bg-transparent p-0 opacity-0 pointer-events-auto focus-visible:opacity-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-accent-border ' + className}
+          style={style}
           onPointerDown={(event) => beginResize(event, handle)}
           onPointerMove={moveResize}
           onPointerUp={endResize}
           onPointerCancel={cancelResize}
           onLostPointerCapture={cancelResize}
+          onKeyDown={(event) => resizeWithKeyboard(event, handle)}
         />
       ))}
       <Dialog open={closeConfirmationOpen} onOpenChange={setCloseConfirmationOpen} title="Discard unsaved changes?" description="Your unsaved changes will be lost.">
diff --git a/packages/ui/package.json b/packages/ui/package.json
index a22441435..2c1cb620d 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -62,6 +62,7 @@
     "zod": "^3.24.1"
   },
   "devDependencies": {
+    "@playwright/test": "^1.48.0",
     "@storybook/addon-essentials": "^8.0.0",
     "@storybook/react": "^8.0.0",
     "@storybook/react-vite": "^8.0.0",
diff --git a/packages/ui/src/i18n/en.json b/packages/ui/src/i18n/en.json
index 1556c540e..edd514cb2 100644
--- a/packages/ui/src/i18n/en.json
+++ b/packages/ui/src/i18n/en.json
@@ -12,7 +12,57 @@
   "settings.locale.countryAdminNote": "Changing the country affects invoicing and tax defaults for the entire workspace. Requires admin permission.",
   "settings.hub.configureCategory": "Configure {{category}}",
   "invoices.approvals.title": "Invoice approvals",
+  "invoices.approvals.sort.oldest": "Oldest first",
+  "invoices.approvals.sort.newest": "Newest first",
+  "invoices.approvals.sort.amount": "Amount",
+  "invoices.approvals.sort.customer": "Customer",
+  "invoices.approvals.sortLabel": "Sort approvals",
+  "invoices.approvals.searchPlaceholder": "Search approvals",
+  "invoices.approvals.bulkBarLabel": "Selected invoice approvals",
+  "invoices.approvals.selectedCount": "{{count}} selected",
+  "invoices.approvals.approveSelected": "Approve selected",
+  "invoices.approvals.rejectSelected": "Reject selected",
+  "invoices.approvals.empty": "No invoice approvals pending",
+  "invoices.approvals.emptyDescription": "Invoices requiring approval will appear here.",
+  "invoices.approvals.listLabel": "Invoice approvals",
+  "invoices.approvals.selectAll": "Select all invoices",
+  "invoices.approvals.columns.invoice": "Invoice",
+  "invoices.approvals.columns.customer": "Customer",
+  "invoices.approvals.columns.amount": "Amount",
+  "invoices.approvals.columns.sent": "Sent",
+  "invoices.approvals.selectInvoice": "Select invoice {{number}}",
+  "invoices.approvals.noNumber": "No invoice number",
+  "invoices.approvals.approve": "Approve",
+  "invoices.approvals.reject": "Reject",
+  "invoices.approvals.bulkDialog.title": "Approve selected invoices?",
+  "invoices.approvals.bulkDialog.description": "You are about to approve {{count}} invoices.",
+  "invoices.approvals.bulkDialog.confirm": "Approve invoices",
   "invoices.drafts.title": "Drafts",
+  "invoices.drafts.newInvoice": "New invoice",
+  "invoices.drafts.searchPlaceholder": "Search drafts",
+  "invoices.drafts.section.drafts": "Drafts",
+  "invoices.drafts.section.noDrafts": "No drafts found.",
+  "invoices.drafts.section.templates": "Templates",
+  "invoices.drafts.section.newTemplate": "New template",
+  "invoices.drafts.section.noTemplates": "No templates yet.",
+  "invoices.drafts.useTemplateDialog.title": "Use {{name}} template",
+  "invoices.drafts.useTemplateDialog.description": "Select a customer to create an invoice from this template.",
+  "invoices.drafts.useTemplateDialog.customerLabel": "Customer",
+  "invoices.drafts.useTemplateDialog.confirm": "Create invoice",
+  "invoices.drafts.newTemplateDialog.title": "New template",
+  "invoices.drafts.newTemplateDialog.description": "Create a reusable invoice template.",
+  "invoices.drafts.newTemplateDialog.nameLabel": "Template name",
+  "invoices.drafts.newTemplateDialog.namePlaceholder": "e.g. Monthly retainer",
+  "invoices.drafts.newTemplateDialog.descriptionLabel": "Notes",
+  "invoices.drafts.newTemplateDialog.descriptionPlaceholder": "Optional notes for this template",
+  "invoices.drafts.newTemplateDialog.currencyLabel": "Currency",
+  "invoices.drafts.newTemplateDialog.confirm": "Create template",
+  "invoices.drafts.draftRow.title": "Draft invoice",
+  "invoices.drafts.draftRow.created": "Created {{date}}",
+  "invoices.drafts.draftRow.edited": "Edited {{date}}",
+  "invoices.drafts.draftRow.send": "Send",
+  "invoices.drafts.templateRow.lineCount": "{{count}} line items",
+  "invoices.drafts.templateRow.use": "Use template",
   "common.save": "Save",
   "common.saving": "Saving…",
   "common.cancel": "Cancel",
@@ -120,6 +170,7 @@
   "nav.dashboard": "Dashboard",
   "nav.projects": "Projects",
   "nav.tasks": "Tasks",
+  "nav.myWork": "My Work",
   "nav.timeTrack": "Time Tracking",
   "nav.calendar": "Calendar",
   "nav.customers": "Customers",
@@ -147,6 +198,32 @@
   "nav.withholding": "Withholding Tax",
   "nav.bituachLeumi": "Bituach Leumi",
   "nav.uniformFormat": "Uniform Format",
+  "nav.group.workspace": "Workspace",
+  "nav.group.business": "Business",
+  "nav.group.financials": "Financials",
+  "nav.group.resources": "Resources",
+  "nav.timeTracking": "Time Tracking",
+  "nav.marketing.leads": "Leads",
+  "nav.marketing.proposals": "Proposals",
+  "nav.marketing.campaigns": "Campaigns",
+  "nav.marketing.catalog": "Catalogs",
+  "nav.supportCenter": "Support Center",
+  "nav.invoices.all": "All Invoices",
+  "nav.invoices.approvals": "Approvals",
+  "nav.invoices.reconcile": "Reconcile",
+  "nav.invoices.receipts": "Receipts",
+  "nav.invoices.drafts": "Drafts",
+  "nav.invoices.recurring": "Recurring",
+  "nav.inventory": "Inventory",
+  "nav.knowledgeBase": "Knowledge Base",
+  "nav.reports.analytics": "Analytics",
+  "nav.reports.vat": "VAT Report",
+  "nav.reports.pnl": "Profit & Loss",
+  "nav.reports.cashflow": "Cash Flow",
+  "nav.reports.advanceTax": "Advance Tax",
+  "nav.reports.withholding": "Withholding Tax",
+  "nav.reports.bituachLeumi": "Bituach Leumi",
+  "nav.reports.uniformFormat": "Uniform Format",
   "search.placeholder": "Search…",
   "search.openPalette": "Search (⌘K)",
   "search.group.tasks": "Tasks",
@@ -568,8 +645,14 @@
   "expenses.perDiem.success": "Per-diem expense logged",
   "expenses.perDiem.error": "Failed to log per-diem",
   "expenses.perDiem.dateRequired": "Date is required",
+  "expenses.filter.ariaLabel": "Expense filters",
+  "expenses.filter.all": "All",
+  "expenses.filter.category": "Category",
   "expenses.filter.dateFrom": "From",
   "expenses.filter.dateTo": "To",
+  "expenses.filter.deductionPct": "Deduction %",
+  "expenses.filter.source": "Source",
+  "expenses.filter.status": "Status",
   "expenses.filter.vendor": "Vendor",
   "expenses.mileage.title": "Mileage Logbook",
   "expenses.mileage.total": "{{count}} trips",
diff --git a/packages/ui/src/i18n/he.json b/packages/ui/src/i18n/he.json
index 020107a34..768ab8904 100644
--- a/packages/ui/src/i18n/he.json
+++ b/packages/ui/src/i18n/he.json
@@ -12,7 +12,75 @@
   "settings.locale.countryAdminNote": "שינוי המדינה משפיע על ברירות המחדל של חשבוניות ומיסים לכל הסביבה. דורש הרשאת מנהל.",
   "settings.hub.configureCategory": "הגדרת {{category}}",
   "invoices.approvals.title": "אישורי חשבוניות",
+  "invoices.approvals.sort.oldest": "הישן ביותר תחילה",
+  "invoices.approvals.sort.newest": "החדש ביותר תחילה",
+  "invoices.approvals.sort.amount": "סכום",
+  "invoices.approvals.sort.customer": "לקוח",
+  "invoices.approvals.sortLabel": "מיון אישורים",
+  "invoices.approvals.searchPlaceholder": "חיפוש אישורי חשבוניות",
+  "invoices.approvals.bulkBarLabel": "אישורי חשבוניות שנבחרו",
+  "invoices.approvals.selectedCount": {
+    "zero": "לא נבחרו חשבוניות",
+    "one": "נבחרה חשבונית אחת",
+    "two": "נבחרו שתי חשבוניות",
+    "many": "{{count}} חשבוניות נבחרו",
+    "other": "{{count}} חשבוניות נבחרו"
+  },
+  "invoices.approvals.approveSelected": "אישור הנבחרים",
+  "invoices.approvals.rejectSelected": "דחיית הנבחרים",
+  "invoices.approvals.empty": "אין אישורי חשבוניות ממתינים",
+  "invoices.approvals.emptyDescription": "חשבוניות שממתינות לאישור יופיעו כאן.",
+  "invoices.approvals.listLabel": "אישורי חשבוניות",
+  "invoices.approvals.selectAll": "בחירת כל החשבוניות",
+  "invoices.approvals.columns.invoice": "חשבונית",
+  "invoices.approvals.columns.customer": "לקוח",
+  "invoices.approvals.columns.amount": "סכום",
+  "invoices.approvals.columns.sent": "נשלחה",
+  "invoices.approvals.selectInvoice": "בחירת חשבונית {{number}}",
+  "invoices.approvals.noNumber": "ללא מספר חשבונית",
+  "invoices.approvals.approve": "אישור",
+  "invoices.approvals.reject": "דחייה",
+  "invoices.approvals.bulkDialog.title": "לאשר את החשבוניות שנבחרו?",
+  "invoices.approvals.bulkDialog.description": {
+    "zero": "לא נבחרו חשבוניות לאישור.",
+    "one": "עומדים לאשר חשבונית אחת.",
+    "two": "עומדים לאשר שתי חשבוניות.",
+    "many": "עומדים לאשר {{count}} חשבוניות.",
+    "other": "עומדים לאשר {{count}} חשבוניות."
+  },
+  "invoices.approvals.bulkDialog.confirm": "אישור חשבוניות",
   "invoices.drafts.title": "טיוטות",
+  "invoices.drafts.newInvoice": "חשבונית חדשה",
+  "invoices.drafts.searchPlaceholder": "חיפוש טיוטות",
+  "invoices.drafts.section.drafts": "טיוטות",
+  "invoices.drafts.section.noDrafts": "לא נמצאו טיוטות.",
+  "invoices.drafts.section.templates": "תבניות",
+  "invoices.drafts.section.newTemplate": "תבנית חדשה",
+  "invoices.drafts.section.noTemplates": "אין עדיין תבניות.",
+  "invoices.drafts.useTemplateDialog.title": "שימוש בתבנית {{name}}",
+  "invoices.drafts.useTemplateDialog.description": "בחרו לקוח כדי ליצור חשבונית מתבנית זו.",
+  "invoices.drafts.useTemplateDialog.customerLabel": "לקוח",
+  "invoices.drafts.useTemplateDialog.confirm": "יצירת חשבונית",
+  "invoices.drafts.newTemplateDialog.title": "תבנית חדשה",
+  "invoices.drafts.newTemplateDialog.description": "צרו תבנית חשבונית לשימוש חוזר.",
+  "invoices.drafts.newTemplateDialog.nameLabel": "שם התבנית",
+  "invoices.drafts.newTemplateDialog.namePlaceholder": "לדוגמה: ריטיינר חודשי",
+  "invoices.drafts.newTemplateDialog.descriptionLabel": "הערות",
+  "invoices.drafts.newTemplateDialog.descriptionPlaceholder": "הערות אופציונליות לתבנית זו",
+  "invoices.drafts.newTemplateDialog.currencyLabel": "מטבע",
+  "invoices.drafts.newTemplateDialog.confirm": "יצירת תבנית",
+  "invoices.drafts.draftRow.title": "טיוטת חשבונית",
+  "invoices.drafts.draftRow.created": "נוצרה ב-{{date}}",
+  "invoices.drafts.draftRow.edited": "נערכה ב-{{date}}",
+  "invoices.drafts.draftRow.send": "שליחה",
+  "invoices.drafts.templateRow.lineCount": {
+    "zero": "אין שורות פריט",
+    "one": "שורת פריט אחת",
+    "two": "שתי שורות פריט",
+    "many": "{{count}} שורות פריט",
+    "other": "{{count}} שורות פריט"
+  },
+  "invoices.drafts.templateRow.use": "שימוש בתבנית",
   "common.save": "שמור",
   "common.saving": "שומר…",
   "common.cancel": "ביטול",
@@ -145,6 +213,7 @@
   "nav.dashboard": "לוח בקרה",
   "nav.projects": "פרויקטים",
   "nav.tasks": "משימות",
+  "nav.myWork": "העבודה שלי",
   "nav.timeTrack": "מעקב שעות",
   "nav.calendar": "לוח שנה",
   "nav.customers": "לקוחות",
@@ -172,6 +241,32 @@
   "nav.withholding": "ניכוי מס במקור",
   "nav.bituachLeumi": "ביטוח לאומי",
   "nav.uniformFormat": "פורמט אחיד",
+  "nav.group.workspace": "סביבת עבודה",
+  "nav.group.business": "עסקים",
+  "nav.group.financials": "כספים",
+  "nav.group.resources": "משאבים",
+  "nav.timeTracking": "מעקב שעות",
+  "nav.marketing.leads": "לידים",
+  "nav.marketing.proposals": "הצעות מחיר",
+  "nav.marketing.campaigns": "קמפיינים",
+  "nav.marketing.catalog": "קטלוגים",
+  "nav.supportCenter": "מרכז תמיכה",
+  "nav.invoices.all": "כל החשבוניות",
+  "nav.invoices.approvals": "אישורים",
+  "nav.invoices.reconcile": "פיוס",
+  "nav.invoices.receipts": "קבלות",
+  "nav.invoices.drafts": "טיוטות",
+  "nav.invoices.recurring": "חשבוניות קבועות",
+  "nav.inventory": "מלאי",
+  "nav.knowledgeBase": "בסיס ידע",
+  "nav.reports.analytics": "אנליטיקה",
+  "nav.reports.vat": "דוח מע״מ",
+  "nav.reports.pnl": "רווח והפסד",
+  "nav.reports.cashflow": "תזרים מזומנים",
+  "nav.reports.advanceTax": "מקדמות מס",
+  "nav.reports.withholding": "ניכוי מס במקור",
+  "nav.reports.bituachLeumi": "ביטוח לאומי",
+  "nav.reports.uniformFormat": "פורמט אחיד",
   "search.placeholder": "חיפוש…",
   "search.openPalette": "חיפוש (⌘K)",
   "search.group.tasks": "משימות",
@@ -393,7 +488,13 @@
   "kb.backToRead": "חזרה למאמר",
   "kb.publish": "פרסם",
   "kb.unpublish": "הסר פרסום",
-  "kb.viewCount": "{{count}} צפיות",
+  "kb.viewCount": {
+    "zero": "אין צפיות",
+    "one": "צפייה אחת",
+    "two": "שתי צפיות",
+    "many": "{{count}} צפיות",
+    "other": "{{count}} צפיות"
+  },
   "kb.saveStateSaved": "נשמר",
   "kb.saveStateSaving": "שומר…",
   "kb.saveStateUnsaved": "שינויים לא שמורים",
@@ -508,11 +609,23 @@
   "notifications.trial_expiring.title": "תקופת הניסיון מסתיימת בקרוב",
   "notifications.trial_expiring.body": "תקופת הניסיון שלך מסתיימת בעוד {{days}} ימים. שדרג לשמירת הגישה.",
   "expenses.title": "הוצאות",
-  "expenses.total": "{{count}} הוצאות",
+  "expenses.total": {
+    "zero": "אין הוצאות",
+    "one": "הוצאה אחת",
+    "two": "שתי הוצאות",
+    "many": "{{count}} הוצאות",
+    "other": "{{count}} הוצאות"
+  },
   "expenses.uploadReceipt": "העלה קבלה",
   "expenses.logPerDiem": "רשום יומדמי",
   "expenses.deleteError": "שגיאה במחיקת הוצאה {{id}}",
-  "expenses.deleteSuccess": "נמחקו {{count}} הוצאות",
+  "expenses.deleteSuccess": {
+    "zero": "לא נמחקו הוצאות",
+    "one": "נמחקה הוצאה אחת",
+    "two": "נמחקו שתי הוצאות",
+    "many": "נמחקו {{count}} הוצאות",
+    "other": "נמחקו {{count}} הוצאות"
+  },
   "expenses.tab.all": "הכל",
   "expenses.tab.needsReview": "דורש בדיקה",
   "expenses.tab.recurring": "קבועות",
@@ -520,7 +633,13 @@
   "expenses.empty.title": "אין הוצאות עדיין",
   "expenses.empty.description": "העלה קבלה או רשום יומדמי כדי להתחיל.",
   "expenses.list.loadMore": "טען עוד",
-  "expenses.bulk.selected": "{{count}} נבחרו",
+  "expenses.bulk.selected": {
+    "zero": "לא נבחרו הוצאות",
+    "one": "נבחרה הוצאה אחת",
+    "two": "נבחרו שתי הוצאות",
+    "many": "נבחרו {{count}} הוצאות",
+    "other": "נבחרו {{count}} הוצאות"
+  },
   "expenses.bulk.deleteSelected": "מחק נבחרים",
   "expenses.bulk.evaluateAll": "הערך את כל הממתינות",
   "expenses.bulk.exportSelected": "ייצא נבחרים",
@@ -557,7 +676,13 @@
   "expenses.detail.evaluationError": "ההערכה מחדש נכשלה",
   "expenses.detail.evaluating": "מעריך…",
   "expenses.detail.reEvaluate": "הערך מחדש",
-  "expenses.detail.corrections": "{{count}} תיקון/ים",
+  "expenses.detail.corrections": {
+    "zero": "אין תיקונים",
+    "one": "תיקון אחד",
+    "two": "שני תיקונים",
+    "many": "{{count}} תיקונים",
+    "other": "{{count}} תיקונים"
+  },
   "expenses.detail.receiptPdf": "קבלה PDF: {{name}}",
   "expenses.detail.receiptImg": "תמונת קבלה: {{name}}",
   "expenses.detail.openPdf": "פתח PDF",
@@ -593,11 +718,23 @@
   "expenses.perDiem.success": "הוצאת יומדמי נרשמה",
   "expenses.perDiem.error": "שגיאה ברישום יומדמי",
   "expenses.perDiem.dateRequired": "תאריך נדרש",
+  "expenses.filter.ariaLabel": "מסנני הוצאות",
+  "expenses.filter.all": "הכל",
+  "expenses.filter.category": "קטגוריה",
   "expenses.filter.dateFrom": "מ",
   "expenses.filter.dateTo": "עד",
+  "expenses.filter.deductionPct": "אחוז ניכוי",
+  "expenses.filter.source": "מקור",
+  "expenses.filter.status": "סטטוס",
   "expenses.filter.vendor": "ספק",
   "expenses.mileage.title": "יומן קילומטראז'",
-  "expenses.mileage.total": "{{count}} נסיעות",
+  "expenses.mileage.total": {
+    "zero": "אין נסיעות",
+    "one": "נסיעה אחת",
+    "two": "שתי נסיעות",
+    "many": "{{count}} נסיעות",
+    "other": "{{count}} נסיעות"
+  },
   "expenses.mileage.annualReport": "דוח שנתי (Excel)",
   "expenses.mileage.logTrip": "רשום נסיעה",
   "expenses.mileage.tripLogged": "הנסיעה נרשמה",
@@ -775,7 +912,13 @@
   "common.pause": "השהייה",
   "common.resume": "המשך",
   "recurringTasks.page.title": "משימות חוזרות",
-  "recurringTasks.page.count": "{{count}} משימות חוזרות",
+  "recurringTasks.page.count": {
+    "zero": "אין משימות חוזרות",
+    "one": "משימה חוזרת אחת",
+    "two": "שתי משימות חוזרות",
+    "many": "{{count}} משימות חוזרות",
+    "other": "{{count}} משימות חוזרות"
+  },
   "recurringTasks.page.newRecurring": "+ חוזרת חדשה",
   "recurringTasks.table.title": "כותרת",
   "recurringTasks.table.schedule": "לוח זמנים",
@@ -883,7 +1026,13 @@
   "apiUsage.detail.thisMonth": "החודש",
   "apiUsage.detail.dailyChart": "בקשות יומיות (30 ימים אחרונים)",
   "apiUsage.detail.topEndpoints": "נקודות קצה מובילות החודש",
-  "apiUsage.detail.endpointCount": "{{count}} ({{pct}}%)",
+  "apiUsage.detail.endpointCount": {
+    "zero": "0 ({{pct}}%)",
+    "one": "1 ({{pct}}%)",
+    "two": "2 ({{pct}}%)",
+    "many": "{{count}} ({{pct}}%)",
+    "other": "{{count}} ({{pct}}%)"
+  },
   "apiUsage.detail.editButton": "ערוך מכסה",
   "apiUsage.detail.revokeButton": "בטל",
   "apiUsage.detail.backLink": "כל מפתחות ה-API",
@@ -1153,7 +1302,13 @@
   "modules.disableModal.section2Heading": "המודולים הבאים יאבדו חלק מהפונקציונליות:",
   "modules.disableModal.dataNotice": "כל הנתונים של המודולים המושבתים נשמרים בסביבת העבודה שלך ויהיו זמינים שוב אם המודול יופעל מחדש.",
   "modules.disableModal.confirmSingle": "השבת",
-  "modules.disableModal.confirmMultiple": "השבת {{count}} מודולים",
+  "modules.disableModal.confirmMultiple": {
+    "zero": "השבת 0 מודולים",
+    "one": "השבת מודול אחד",
+    "two": "השבת שני מודולים",
+    "many": "השבת {{count}} מודולים",
+    "other": "השבת {{count}} מודולים"
+  },
   "modules.toast.enabled": "{{name}} הופעל.",
   "modules.toast.disabled": "{{name}} הושבת.",
   "modules.error.generic": "עדכון המודולים נכשל.",
diff --git a/packages/ui/src/primitives/select.tsx b/packages/ui/src/primitives/select.tsx
index e0ae0a2d0..aa2886fae 100644
--- a/packages/ui/src/primitives/select.tsx
+++ b/packages/ui/src/primitives/select.tsx
@@ -51,8 +51,11 @@ export const Select = React.forwardRef<
           className,
         )}
       >
-        <SelectPrimitive.Value placeholder={placeholder} />
-        <SelectPrimitive.Icon>
+        <SelectPrimitive.Value
+          placeholder={placeholder}
+          className="min-w-0 flex-1 truncate whitespace-nowrap text-start"
+        />
+        <SelectPrimitive.Icon className="shrink-0">
           <ChevronDown className="h-4 w-4 text-ink-faint" aria-hidden="true" />
         </SelectPrimitive.Icon>
       </SelectPrimitive.Trigger>
diff --git a/packages/ui/test/select-value-layout.test.tsx b/packages/ui/test/select-value-layout.test.tsx
new file mode 100644
index 000000000..43eae2d5d
--- /dev/null
+++ b/packages/ui/test/select-value-layout.test.tsx
@@ -0,0 +1,85 @@
+import * as React from 'react'
+import { renderToStaticMarkup } from 'react-dom/server'
+import { chromium } from '@playwright/test'
+import { afterAll, describe, expect, it, vi } from 'vitest'
+
+let selectedValue = ''
+
+vi.mock('@radix-ui/react-select', () => ({
+  Root: ({ children, value }: React.PropsWithChildren<{ value?: string }>) => {
+    selectedValue = value ?? ''
+    return <>{children}</>
+  },
+  Trigger: React.forwardRef<HTMLButtonElement, React.ComponentPropsWithoutRef<'button'>>(
+    ({ children, ...props }, ref) => <button ref={ref} {...props}>{children}</button>,
+  ),
+  Value: ({ placeholder: _placeholder, ...props }: React.ComponentPropsWithoutRef<'span'> & { placeholder?: string }) => <span data-select-value="" {...props}>{selectedValue}</span>,
+  Icon: ({ children, ...props }: React.ComponentPropsWithoutRef<'span'>) => <span data-select-icon="" {...props}>{children}</span>,
+  Portal: ({ children }: React.PropsWithChildren) => <>{children}</>,
+  Content: ({ children, ...props }: React.ComponentPropsWithoutRef<'div'>) => <div {...props}>{children}</div>,
+  Viewport: ({ children, ...props }: React.ComponentPropsWithoutRef<'div'>) => <div {...props}>{children}</div>,
+  Item: ({ children, value, disabled, ...props }: React.ComponentPropsWithoutRef<'div'> & { value: string; disabled?: boolean }) => <div data-value={value} aria-disabled={disabled} {...props}>{children}</div>,
+  ItemIndicator: ({ children }: React.PropsWithChildren) => <>{children}</>,
+  ItemText: ({ children }: React.PropsWithChildren) => <>{children}</>,
+}))
+
+import { Select } from '../src/primitives/select'
+
+const values = [
+  'Short',
+  'An exceptionally long selection value that must remain within its trigger',
+  'ערך bilingual selection ארוך במיוחד',
+  'inventory.filters.unresolvedSelection',
+]
+
+const layoutCss = `
+  button { box-sizing: border-box; display: flex; width: 128px; height: 32px; align-items: center; justify-content: space-between; gap: 8px; padding: 0 8px; overflow: hidden; }
+  .min-w-0 { min-width: 0; }
+  .flex-1 { flex: 1 1 0%; }
+  .truncate { overflow: hidden; text-overflow: ellipsis; }
+  .whitespace-nowrap { white-space: nowrap; }
+  .shrink-0 { flex-shrink: 0; }
+  [data-select-icon] { display: inline-flex; width: 16px; height: 16px; }
+`
+
+const browser = await chromium.launch({ headless: true })
+afterAll(async () => browser.close())
+
+describe('Select value layout', () => {
+  it.each(['ltr', 'rtl'] as const)('keeps every value inside a constrained %s trigger', async (dir) => {
+    const markup = values.map((value) => renderToStaticMarkup(
+      <Select aria-label={value} value={value} options={[{ value, label: value }]} />,
+    )).join('')
+    const page = await browser.newPage({ viewport: { width: 320, height: 240 } })
+
+    try {
+      await page.setContent(`<style>${layoutCss}</style><main dir="${dir}">${markup}</main>`)
+      const cases = await page.locator('button').evaluateAll((triggers) => triggers.map((trigger) => {
+        const value = trigger.querySelector('[data-select-value]')!
+        const icon = trigger.querySelector('[data-select-icon]')!
+        const triggerRect = trigger.getBoundingClientRect()
+        const valueRect = value.getBoundingClientRect()
+        const iconRect = icon.getBoundingClientRect()
+        const valueStyle = getComputedStyle(value)
+        return {
+          accessibleValue: trigger.getAttribute('aria-label'),
+          fullValue: value.textContent,
+          iconVisible: iconRect.width > 0 && iconRect.height > 0,
+          oneLine: valueStyle.whiteSpace === 'nowrap' && valueRect.height <= triggerRect.height,
+          valueInsideTrigger: valueRect.left >= triggerRect.left && valueRect.right <= triggerRect.right,
+        }
+      }))
+
+      expect(cases).toHaveLength(values.length)
+      for (const [index, layout] of cases.entries()) {
+        expect(layout.accessibleValue).toBe(values[index])
+        expect(layout.fullValue).toBe(values[index])
+        expect(layout.oneLine).toBe(true)
+        expect(layout.valueInsideTrigger).toBe(true)
+        expect(layout.iconVisible).toBe(true)
+      }
+    } finally {
+      await page.close()
+    }
+  })
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f291feac8..308b26477 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -440,7 +440,7 @@ importers:
         specifier: workspace:*
         version: link:../../packages/modules
       '@zync/os-shell':
-        specifier: file:../../packages/os-shell
+        specifier: workspace:*
         version: link:../../packages/os-shell
       '@zync/payments':
         specifier: workspace:*
@@ -563,6 +563,9 @@ importers:
       autoprefixer:
         specifier: ^10.5.0
         version: 10.5.2(postcss@8.5.23)
+      jsdom:
+        specifier: ^26.1.0
+        version: 26.1.0(canvas@3.2.3)
       otplib:
         specifier: ^13.4.1
         version: 13.4.1
@@ -583,7 +586,7 @@ importers:
         version: 1.3.0(vite@6.4.3(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1)
       vitest:
         specifier: ^3.2.6
-        version: 3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@2.3.0)(canvas@3.2.3))(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)
+        version: 3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(jiti@1.21.7)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)
       wrangler:
         specifier: ^4.0.0
         version: 4.106.0(@cloudflare/workers-types@4.20260630.1)
@@ -1290,6 +1293,9 @@ importers:
         specifier: ^3.24.1
         version: 3.25.76
     devDependencies:
+      '@playwright/test':
+        specifier: ^1.48.0
+        version: 1.61.1
       '@storybook/addon-essentials':
         specifier: ^8.0.0
         version: 8.6.14(@types/react@19.2.17)(storybook@8.6.18(prettier@3.9.4))
@@ -1381,6 +1387,9 @@ packages:
     peerDependencies:
       ajv: '>=8'
 
+  '@asamuzakjp/css-color@3.2.0':
+    resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
+
   '@asamuzakjp/css-color@5.1.11':
     resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==}
     engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -2257,10 +2266,21 @@ packages:
     resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
     engines: {node: '>=12'}
 
+  '@csstools/color-helpers@5.1.0':
+    resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
+    engines: {node: '>=18'}
+
   '@csstools/color-helpers@6.1.0':
     resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==}
     engines: {node: '>=20.19.0'}
 
+  '@csstools/css-calc@2.1.4':
+    resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@csstools/css-parser-algorithms': ^3.0.5
+      '@csstools/css-tokenizer': ^3.0.4
+
   '@csstools/css-calc@3.2.1':
     resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==}
     engines: {node: '>=20.19.0'}
@@ -2268,6 +2288,13 @@ packages:
       '@csstools/css-parser-algorithms': ^4.0.0
       '@csstools/css-tokenizer': ^4.0.0
 
+  '@csstools/css-color-parser@3.1.0':
+    resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@csstools/css-parser-algorithms': ^3.0.5
+      '@csstools/css-tokenizer': ^3.0.4
+
   '@csstools/css-color-parser@4.1.9':
     resolution: {integrity: sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==}
     engines: {node: '>=20.19.0'}
@@ -2275,6 +2302,12 @@ packages:
       '@csstools/css-parser-algorithms': ^4.0.0
       '@csstools/css-tokenizer': ^4.0.0
 
+  '@csstools/css-parser-algorithms@3.0.5':
+    resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      '@csstools/css-tokenizer': ^3.0.4
+
   '@csstools/css-parser-algorithms@4.0.0':
     resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==}
     engines: {node: '>=20.19.0'}
@@ -2289,6 +2322,10 @@ packages:
       css-tree:
         optional: true
 
+  '@csstools/css-tokenizer@3.0.4':
+    resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+    engines: {node: '>=18'}
+
   '@csstools/css-tokenizer@4.0.0':
     resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
     engines: {node: '>=20.19.0'}
@@ -6623,6 +6660,10 @@ packages:
     resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
     engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
 
+  cssstyle@4.6.0:
+    resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
+    engines: {node: '>=18'}
+
   cssstyle@6.2.0:
     resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==}
     engines: {node: '>=20'}
@@ -6678,6 +6719,10 @@ packages:
     resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
     engines: {node: '>= 12'}
 
+  data-urls@5.0.0:
+    resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
+    engines: {node: '>=18'}
+
   data-urls@7.0.0:
     resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==}
     engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -7686,6 +7731,10 @@ packages:
     resolution: {integrity: sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==}
     engines: {node: ^20.17.0 || >=22.9.0}
 
+  html-encoding-sniffer@4.0.0:
+    resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+    engines: {node: '>=18'}
+
   html-encoding-sniffer@6.0.0:
     resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==}
     engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -8177,6 +8226,15 @@ packages:
     resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==}
     engines: {node: '>=12.0.0'}
 
+  jsdom@26.1.0:
+    resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
+    engines: {node: '>=18'}
+    peerDependencies:
+      canvas: ^3.0.0
+    peerDependenciesMeta:
+      canvas:
+        optional: true
+
   jsdom@28.1.0:
     resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==}
     engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -9006,6 +9064,9 @@ packages:
   nth-check@2.1.1:
     resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
 
+  nwsapi@2.2.24:
+    resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==}
+
   oauth-sign@0.9.0:
     resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
 
@@ -9966,6 +10027,9 @@ packages:
   rrule@2.8.1:
     resolution: {integrity: sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==}
 
+  rrweb-cssom@0.8.0:
+    resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
+
   run-applescript@7.1.0:
     resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
     engines: {node: '>=18'}
@@ -10489,9 +10553,16 @@ packages:
   tippy.js@6.3.7:
     resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
 
+  tldts-core@6.1.86:
+    resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
+
   tldts-core@7.4.5:
     resolution: {integrity: sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==}
 
+  tldts@6.1.86:
+    resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
+    hasBin: true
+
   tldts@7.4.5:
     resolution: {integrity: sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==}
     hasBin: true
@@ -10512,6 +10583,10 @@ packages:
     resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
     engines: {node: '>=8.0'}
 
+  tough-cookie@5.1.2:
+    resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
+    engines: {node: '>=16'}
+
   tough-cookie@6.0.1:
     resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==}
     engines: {node: '>=16'}
@@ -10522,6 +10597,10 @@ packages:
   tr46@1.0.1:
     resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
 
+  tr46@5.1.1:
+    resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
+    engines: {node: '>=18'}
+
   tr46@6.0.0:
     resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
     engines: {node: '>=20'}
@@ -11168,6 +11247,10 @@ packages:
   webidl-conversions@4.0.2:
     resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
 
+  webidl-conversions@7.0.0:
+    resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+    engines: {node: '>=12'}
+
   webidl-conversions@8.0.1:
     resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==}
     engines: {node: '>=20'}
@@ -11183,10 +11266,23 @@ packages:
     resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
     engines: {node: '>=0.8.0'}
 
+  whatwg-encoding@3.1.1:
+    resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+    engines: {node: '>=18'}
+    deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
+
+  whatwg-mimetype@4.0.0:
+    resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+    engines: {node: '>=18'}
+
   whatwg-mimetype@5.0.0:
     resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==}
     engines: {node: '>=20'}
 
+  whatwg-url@14.2.0:
+    resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
+    engines: {node: '>=18'}
+
   whatwg-url@16.0.1:
     resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==}
     engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -11546,6 +11642,14 @@ snapshots:
       jsonpointer: 5.0.1
       leven: 3.1.0
 
+  '@asamuzakjp/css-color@3.2.0':
+    dependencies:
+      '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-tokenizer': 3.0.4
+      lru-cache: 10.4.3
+
   '@asamuzakjp/css-color@5.1.11':
     dependencies:
       '@asamuzakjp/generational-cache': 1.0.1
@@ -12612,13 +12716,27 @@ snapshots:
     dependencies:
       '@jridgewell/trace-mapping': 0.3.9
 
+  '@csstools/color-helpers@5.1.0': {}
+
   '@csstools/color-helpers@6.1.0': {}
 
+  '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+    dependencies:
+      '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-tokenizer': 3.0.4
+
   '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
     dependencies:
       '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
       '@csstools/css-tokenizer': 4.0.0
 
+  '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+    dependencies:
+      '@csstools/color-helpers': 5.1.0
+      '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+      '@csstools/css-tokenizer': 3.0.4
+
   '@csstools/css-color-parser@4.1.9(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
     dependencies:
       '@csstools/color-helpers': 6.1.0
@@ -12626,6 +12744,10 @@ snapshots:
       '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
       '@csstools/css-tokenizer': 4.0.0
 
+  '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+    dependencies:
+      '@csstools/css-tokenizer': 3.0.4
+
   '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
     dependencies:
       '@csstools/css-tokenizer': 4.0.0
@@ -12634,6 +12756,8 @@ snapshots:
     optionalDependencies:
       css-tree: 3.2.1
 
+  '@csstools/css-tokenizer@3.0.4': {}
+
   '@csstools/css-tokenizer@4.0.0': {}
 
   '@date-fns/tz@1.5.0': {}
@@ -16995,6 +17119,11 @@ snapshots:
     dependencies:
       css-tree: 2.2.1
 
+  cssstyle@4.6.0:
+    dependencies:
+      '@asamuzakjp/css-color': 3.2.0
+      rrweb-cssom: 0.8.0
+
   cssstyle@6.2.0:
     dependencies:
       '@asamuzakjp/css-color': 5.1.11
@@ -17044,6 +17173,11 @@ snapshots:
 
   data-uri-to-buffer@4.0.1: {}
 
+  data-urls@5.0.0:
+    dependencies:
+      whatwg-mimetype: 4.0.0
+      whatwg-url: 14.2.0
+
   data-urls@7.0.0(@noble/hashes@2.3.0):
     dependencies:
       whatwg-mimetype: 5.0.0
@@ -18228,6 +18362,10 @@ snapshots:
     dependencies:
       lru-cache: 11.5.1
 
+  html-encoding-sniffer@4.0.0:
+    dependencies:
+      whatwg-encoding: 3.1.1
+
   html-encoding-sniffer@6.0.0(@noble/hashes@2.3.0):
     dependencies:
       '@exodus/bytes': 1.15.1(@noble/hashes@2.3.0)
@@ -18308,7 +18446,6 @@ snapshots:
   iconv-lite@0.6.3:
     dependencies:
       safer-buffer: 2.1.2
-    optional: true
 
   iconv-lite@0.7.3:
     dependencies:
@@ -18699,6 +18836,35 @@ snapshots:
 
   jsdoc-type-pratt-parser@4.8.0: {}
 
+  jsdom@26.1.0(canvas@3.2.3):
+    dependencies:
+      cssstyle: 4.6.0
+      data-urls: 5.0.0
+      decimal.js: 10.6.0
+      html-encoding-sniffer: 4.0.0
+      http-proxy-agent: 7.0.2
+      https-proxy-agent: 7.0.6
+      is-potential-custom-element-name: 1.0.1
+      nwsapi: 2.2.24
+      parse5: 7.3.0
+      rrweb-cssom: 0.8.0
+      saxes: 6.0.0
+      symbol-tree: 3.2.4
+      tough-cookie: 5.1.2
+      w3c-xmlserializer: 5.0.0
+      webidl-conversions: 7.0.0
+      whatwg-encoding: 3.1.1
+      whatwg-mimetype: 4.0.0
+      whatwg-url: 14.2.0
+      ws: 8.21.0
+      xml-name-validator: 5.0.0
+    optionalDependencies:
+      canvas: 3.2.3
+    transitivePeerDependencies:
+      - bufferutil
+      - supports-color
+      - utf-8-validate
+
   jsdom@28.1.0(@noble/hashes@2.3.0)(canvas@3.2.3):
     dependencies:
       '@acemir/cssom': 0.9.31
@@ -19762,6 +19928,8 @@ snapshots:
     dependencies:
       boolbase: 1.0.0
 
+  nwsapi@2.2.24: {}
+
   oauth-sign@0.9.0: {}
 
   object-assign@4.1.1: {}
@@ -20854,6 +21022,8 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
+  rrweb-cssom@0.8.0: {}
+
   run-applescript@7.1.0: {}
 
   run-async@2.4.1: {}
@@ -21525,8 +21695,14 @@ snapshots:
     dependencies:
       '@popperjs/core': 2.11.8
 
+  tldts-core@6.1.86: {}
+
   tldts-core@7.4.5: {}
 
+  tldts@6.1.86:
+    dependencies:
+      tldts-core: 6.1.86
+
   tldts@7.4.5:
     dependencies:
       tldts-core: 7.4.5
@@ -21548,6 +21724,10 @@ snapshots:
     dependencies:
       is-number: 7.0.0
 
+  tough-cookie@5.1.2:
+    dependencies:
+      tldts: 6.1.86
+
   tough-cookie@6.0.1:
     dependencies:
       tldts: 7.4.5
@@ -21558,6 +21738,10 @@ snapshots:
     dependencies:
       punycode: 2.3.1
 
+  tr46@5.1.1:
+    dependencies:
+      punycode: 2.3.1
+
   tr46@6.0.0:
     dependencies:
       punycode: 2.3.1
@@ -22081,6 +22265,49 @@ snapshots:
       - tsx
       - yaml
 
+  vitest@3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(jiti@1.21.7)(jsdom@26.1.0(canvas@3.2.3))(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0):
+    dependencies:
+      '@types/chai': 5.2.3
+      '@vitest/expect': 3.2.6
+      '@vitest/mocker': 3.2.6(vite@6.4.3(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0))
+      '@vitest/pretty-format': 3.2.6
+      '@vitest/runner': 3.2.6
+      '@vitest/snapshot': 3.2.6
+      '@vitest/spy': 3.2.6
+      '@vitest/utils': 3.2.6
+      chai: 5.3.3
+      debug: 4.4.3(supports-color@8.1.1)
+      expect-type: 1.4.0
+      magic-string: 0.30.21
+      pathe: 2.0.3
+      picomatch: 4.0.4
+      std-env: 3.10.0
+      tinybench: 2.9.0
+      tinyexec: 0.3.2
+      tinyglobby: 0.2.17
+      tinypool: 1.1.1
+      tinyrainbow: 2.0.0
+      vite: 6.4.3(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)
+      vite-node: 3.2.4(@types/node@22.20.0)(jiti@1.21.7)(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0)
+      why-is-node-running: 2.3.0
+    optionalDependencies:
+      '@types/debug': 4.1.13
+      '@types/node': 22.20.0
+      jsdom: 26.1.0(canvas@3.2.3)
+    transitivePeerDependencies:
+      - jiti
+      - less
+      - lightningcss
+      - msw
+      - sass
+      - sass-embedded
+      - stylus
+      - sugarss
+      - supports-color
+      - terser
+      - tsx
+      - yaml
+
   vitest@3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(jiti@1.21.7)(jsdom@28.1.0(@noble/hashes@2.3.0)(canvas@3.2.3))(lightningcss@1.33.0)(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0):
     dependencies:
       '@types/chai': 5.2.3
@@ -22260,6 +22487,8 @@ snapshots:
 
   webidl-conversions@4.0.2: {}
 
+  webidl-conversions@7.0.0: {}
+
   webidl-conversions@8.0.1: {}
 
   webpack-virtual-modules@0.6.2: {}
@@ -22272,8 +22501,19 @@ snapshots:
 
   websocket-extensions@0.1.4: {}
 
+  whatwg-encoding@3.1.1:
+    dependencies:
+      iconv-lite: 0.6.3
+
+  whatwg-mimetype@4.0.0: {}
+
   whatwg-mimetype@5.0.0: {}
 
+  whatwg-url@14.2.0:
+    dependencies:
+      tr46: 5.1.1
+      webidl-conversions: 7.0.0
+
   whatwg-url@16.0.1(@noble/hashes@2.3.0):
     dependencies:
       '@exodus/bytes': 1.15.1(@noble/hashes@2.3.0)
