@suveren/gateway 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/bin/suveren-gateway.js +19 -2
  2. package/dist/control-plane/index.mjs +208 -34
  3. package/dist/ui/assets/index-CkWaRck1.css +1 -0
  4. package/dist/ui/assets/index-DHT9QCrE.js +110 -0
  5. package/dist/ui/favicon.svg +8 -0
  6. package/dist/ui/index.html +6 -2
  7. package/lib/autostart-templates.d.mts +2 -0
  8. package/lib/autostart-templates.mjs +16 -3
  9. package/node_modules/@hap/core/README.md +4 -4
  10. package/node_modules/@hap/core/package.json +1 -1
  11. package/node_modules/@hono/node-server/README.md +47 -1
  12. package/node_modules/@hono/node-server/dist/early-hints.cjs +35 -0
  13. package/node_modules/@hono/node-server/dist/early-hints.d.cts +16 -0
  14. package/node_modules/@hono/node-server/dist/early-hints.d.mts +16 -0
  15. package/node_modules/@hono/node-server/dist/early-hints.mjs +33 -0
  16. package/node_modules/@hono/node-server/dist/index.cjs +4 -1
  17. package/node_modules/@hono/node-server/dist/index.mjs +4 -1
  18. package/node_modules/@hono/node-server/package.json +14 -1
  19. package/node_modules/express-rate-limit/dist/index.cjs +6 -1
  20. package/node_modules/express-rate-limit/dist/index.mjs +6 -1
  21. package/node_modules/express-rate-limit/package.json +6 -6
  22. package/node_modules/hono/dist/cjs/context.js +28 -13
  23. package/node_modules/hono/dist/cjs/hono-base.js +3 -2
  24. package/node_modules/hono/dist/cjs/jsx/base.js +26 -14
  25. package/node_modules/hono/dist/cjs/jsx/hooks/index.js +2 -2
  26. package/node_modules/hono/dist/cjs/middleware/cache/index.js +103 -8
  27. package/node_modules/hono/dist/cjs/middleware/compress/index.js +5 -0
  28. package/node_modules/hono/dist/cjs/middleware/cors/index.js +1 -1
  29. package/node_modules/hono/dist/cjs/middleware/etag/index.js +4 -1
  30. package/node_modules/hono/dist/cjs/middleware/jwk/jwk.js +9 -4
  31. package/node_modules/hono/dist/cjs/middleware/jwt/jwt.js +9 -4
  32. package/node_modules/hono/dist/cjs/middleware/method-not-allowed/index.js +90 -0
  33. package/node_modules/hono/dist/cjs/request.js +6 -8
  34. package/node_modules/hono/dist/cjs/router/reg-exp-router/node.js +55 -56
  35. package/node_modules/hono/dist/cjs/router/reg-exp-router/router.js +64 -85
  36. package/node_modules/hono/dist/cjs/router/reg-exp-router/trie.js +13 -5
  37. package/node_modules/hono/dist/cjs/router/trie-router/node.js +1 -1
  38. package/node_modules/hono/dist/cjs/router.js +1 -1
  39. package/node_modules/hono/dist/cjs/utils/cookie.js +1 -1
  40. package/node_modules/hono/dist/cjs/utils/stream.js +5 -2
  41. package/node_modules/hono/dist/cjs/utils/url.js +7 -7
  42. package/node_modules/hono/dist/context.js +28 -13
  43. package/node_modules/hono/dist/hono-base.js +3 -2
  44. package/node_modules/hono/dist/jsx/base.js +26 -14
  45. package/node_modules/hono/dist/jsx/hooks/index.js +2 -2
  46. package/node_modules/hono/dist/middleware/cache/index.js +103 -8
  47. package/node_modules/hono/dist/middleware/compress/index.js +5 -0
  48. package/node_modules/hono/dist/middleware/cors/index.js +1 -1
  49. package/node_modules/hono/dist/middleware/etag/index.js +4 -1
  50. package/node_modules/hono/dist/middleware/jwk/jwk.js +9 -4
  51. package/node_modules/hono/dist/middleware/jwt/jwt.js +9 -4
  52. package/node_modules/hono/dist/middleware/method-not-allowed/index.js +68 -0
  53. package/node_modules/hono/dist/request.js +7 -9
  54. package/node_modules/hono/dist/router/reg-exp-router/node.js +55 -56
  55. package/node_modules/hono/dist/router/reg-exp-router/router.js +64 -85
  56. package/node_modules/hono/dist/router/reg-exp-router/trie.js +13 -5
  57. package/node_modules/hono/dist/router/trie-router/node.js +1 -1
  58. package/node_modules/hono/dist/router.js +1 -1
  59. package/node_modules/hono/dist/types/client/types.d.ts +1 -1
  60. package/node_modules/hono/dist/types/hono-base.d.ts +4 -3
  61. package/node_modules/hono/dist/types/jsx/base.d.ts +4 -2
  62. package/node_modules/hono/dist/types/jsx/dom/index.d.ts +5 -5
  63. package/node_modules/hono/dist/types/jsx/dom/intrinsic-element/components.d.ts +2 -2
  64. package/node_modules/hono/dist/types/jsx/dom/server.d.ts +5 -5
  65. package/node_modules/hono/dist/types/jsx/hooks/index.d.ts +8 -6
  66. package/node_modules/hono/dist/types/jsx/index.d.ts +5 -5
  67. package/node_modules/hono/dist/types/middleware/cache/index.d.ts +6 -4
  68. package/node_modules/hono/dist/types/middleware/cors/index.d.ts +1 -1
  69. package/node_modules/hono/dist/types/middleware/jsx-renderer/index.d.ts +2 -2
  70. package/node_modules/hono/dist/types/middleware/jwk/jwk.d.ts +2 -0
  71. package/node_modules/hono/dist/types/middleware/jwt/jwt.d.ts +2 -0
  72. package/node_modules/hono/dist/types/middleware/method-not-allowed/index.d.ts +49 -0
  73. package/node_modules/hono/dist/types/router/reg-exp-router/node.d.ts +1 -1
  74. package/node_modules/hono/dist/types/router/reg-exp-router/trie.d.ts +2 -1
  75. package/node_modules/hono/dist/types/router.d.ts +1 -1
  76. package/node_modules/hono/dist/types/utils/headers.d.ts +2 -2
  77. package/node_modules/hono/dist/types/utils/url.d.ts +1 -0
  78. package/node_modules/hono/dist/utils/cookie.js +2 -2
  79. package/node_modules/hono/dist/utils/stream.js +5 -2
  80. package/node_modules/hono/dist/utils/url.js +5 -6
  81. package/node_modules/hono/package.json +9 -1
  82. package/package.json +1 -1
  83. package/dist/ui/assets/index-DIcuh0I4.css +0 -1
  84. package/dist/ui/assets/index-Dl5isaOV.js +0 -105
@@ -13,7 +13,7 @@
13
13
  import { spawn, spawnSync } from 'node:child_process';
14
14
  import { createConnection } from 'node:net';
15
15
  import { existsSync, mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
16
- import { homedir, platform } from 'node:os';
16
+ import { homedir, platform, userInfo } from 'node:os';
17
17
  import { dirname, join, resolve } from 'node:path';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { buildLaunchAgentPlist, buildMacLauncher, buildSystemdUnit, buildWindowsTaskXml } from '../lib/autostart-templates.mjs';
@@ -500,6 +500,22 @@ async function serviceStatusMac() {
500
500
 
501
501
  const WIN_TASK_NAME = 'Suveren';
502
502
 
503
+ /**
504
+ * Who the login task runs as, as `DOMAIN\user` when a domain is known.
505
+ *
506
+ * Without this the task is registered for ANY user's logon, which Windows only
507
+ * lets an administrator do — a standard user just saw "Zugriff verweigert" and
508
+ * had no way to install autostart at all. USERDOMAIN is the machine name on a
509
+ * local account and the AD domain on a joined one; both are correct here.
510
+ * Falling back to the bare username is fine — Task Scheduler resolves it
511
+ * against the local machine.
512
+ */
513
+ function windowsUserId() {
514
+ const name = process.env.USERNAME || userInfo().username;
515
+ const domain = process.env.USERDOMAIN;
516
+ return domain ? `${domain}\\${name}` : name;
517
+ }
518
+
503
519
  async function serviceInstallWindows() {
504
520
  ensureDataDir();
505
521
 
@@ -513,6 +529,7 @@ async function serviceInstallWindows() {
513
529
  serverEntry: SERVER_ENTRY,
514
530
  author: 'Suveren',
515
531
  dataDir: process.env.SUVEREN_DATA_DIR ?? '',
532
+ userId: windowsUserId(),
516
533
  });
517
534
  const xmlPath = join(DATA_DIR, 'suveren-task.xml');
518
535
  writeFileSync(xmlPath, '\ufeff' + xml, { encoding: 'utf16le' });
@@ -766,7 +783,7 @@ Usage:
766
783
  suveren-gateway status Show running state + health
767
784
  suveren-gateway logs [--tail] Print or tail ~/.suveren/gateway.log
768
785
  suveren-gateway service <cmd> Run as a login service that survives reboot
769
- (install | uninstall | status; macOS today)
786
+ (install | uninstall | status)
770
787
  suveren-gateway help Print this help
771
788
 
772
789
  Environment:
@@ -5,9 +5,9 @@ import {
5
5
 
6
6
  // src/index.ts
7
7
  import { randomBytes as randomBytes2 } from "crypto";
8
- import { existsSync as existsSync5, readFileSync as readFileSync4, renameSync } from "fs";
9
- import { dirname as dirname3, join as join4 } from "path";
10
- import { homedir as homedir3 } from "os";
8
+ import { existsSync as existsSync5, readFileSync as readFileSync5, renameSync } from "fs";
9
+ import { dirname as dirname4, join as join5 } from "path";
10
+ import { homedir as homedir4 } from "os";
11
11
  import express from "express";
12
12
  import { createProxyMiddleware } from "http-proxy-middleware";
13
13
 
@@ -1377,6 +1377,38 @@ function requireAuthQueryOrHeader(vault2) {
1377
1377
  };
1378
1378
  }
1379
1379
 
1380
+ // src/middleware/host-guard.ts
1381
+ var ALLOWED_HOSTNAMES = /* @__PURE__ */ new Set([
1382
+ "localhost",
1383
+ "127.0.0.1",
1384
+ "::1",
1385
+ "[::1]",
1386
+ "0.0.0.0"
1387
+ ]);
1388
+ function hostnameOf(hostHeader) {
1389
+ const trimmed = hostHeader.trim().toLowerCase();
1390
+ const bracketed = trimmed.match(/^\[([^\]]+)\](?::\d+)?$/);
1391
+ if (bracketed) return bracketed[1];
1392
+ return trimmed.replace(/:\d+$/, "");
1393
+ }
1394
+ function isAllowedHost(hostHeader) {
1395
+ if (!hostHeader) return false;
1396
+ const name = hostnameOf(hostHeader);
1397
+ if (ALLOWED_HOSTNAMES.has(name)) return true;
1398
+ if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
1399
+ if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
1400
+ if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
1401
+ if (name === "localhost" || name.endsWith(".localhost")) return true;
1402
+ return false;
1403
+ }
1404
+ function requireAllowedHost(req, res, next) {
1405
+ if (!isAllowedHost(req.headers.host)) {
1406
+ res.status(403).json({ error: "Forbidden host" });
1407
+ return;
1408
+ }
1409
+ next();
1410
+ }
1411
+
1380
1412
  // src/lib/read-policy-defaults.ts
1381
1413
  async function backfillReadPolicyDefaults(data) {
1382
1414
  const defaults = /* @__PURE__ */ new Map();
@@ -1508,15 +1540,15 @@ import { promisify } from "util";
1508
1540
  var run = promisify(execFile);
1509
1541
  var SUPPORTED = /* @__PURE__ */ new Set(["darwin", "win32", "linux"]);
1510
1542
  var MODULE_DIR = pathDirname(fileURLToPath(import.meta.url));
1511
- function findCli(dirname4 = MODULE_DIR) {
1543
+ function findCli(dirname5 = MODULE_DIR) {
1512
1544
  const candidates = [
1513
1545
  // Bundled (the shipped layout): dist/control-plane → dist → root → bin
1514
- resolve(dirname4, "..", "..", "bin", "suveren-gateway.js"),
1546
+ resolve(dirname5, "..", "..", "bin", "suveren-gateway.js"),
1515
1547
  // Bundled, were the routes/ directory ever preserved.
1516
- resolve(dirname4, "..", "..", "..", "bin", "suveren-gateway.js"),
1548
+ resolve(dirname5, "..", "..", "..", "bin", "suveren-gateway.js"),
1517
1549
  // Dev, running from source: the repo's own bundle directory.
1518
- resolve(dirname4, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
1519
- resolve(dirname4, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
1550
+ resolve(dirname5, "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js"),
1551
+ resolve(dirname5, "..", "..", "..", "..", "..", "bundle", "bin", "suveren-gateway.js")
1520
1552
  // Deliberately NO process.cwd() fallback: it makes the result depend on
1521
1553
  // where the process happened to be launched from, so the same install
1522
1554
  // resolves differently between a shell and a login service — and it would
@@ -1961,9 +1993,8 @@ function createEventsHandler() {
1961
1993
  res.setHeader("X-Accel-Buffering", "no");
1962
1994
  res.flushHeaders();
1963
1995
  const unsubscribe = eventBus.subscribe((event) => {
1964
- const data = JSON.stringify(event.payload ?? null);
1965
1996
  res.write(`event: ${event.type}
1966
- data: ${data}
1997
+ data: null
1967
1998
 
1968
1999
  `);
1969
2000
  });
@@ -1977,18 +2008,159 @@ data: ${data}
1977
2008
  };
1978
2009
  }
1979
2010
 
2011
+ // src/routes/gateway-settings.ts
2012
+ import { Router as Router10 } from "express";
2013
+
2014
+ // src/lib/gateway-settings.ts
2015
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
2016
+ import { homedir as homedir3 } from "os";
2017
+ import { join as join3, dirname as dirname3 } from "path";
2018
+ var DEFAULT_SETTINGS = {
2019
+ desktopNotifications: true
2020
+ };
2021
+ function settingsPath(dataDir) {
2022
+ const dir = dataDir ?? process.env.SUVEREN_DATA_DIR ?? join3(homedir3(), ".suveren");
2023
+ return join3(dir, "settings.json");
2024
+ }
2025
+ function readSettings(dataDir) {
2026
+ try {
2027
+ const raw = JSON.parse(readFileSync3(settingsPath(dataDir), "utf8"));
2028
+ return {
2029
+ desktopNotifications: typeof raw.desktopNotifications === "boolean" ? raw.desktopNotifications : DEFAULT_SETTINGS.desktopNotifications
2030
+ };
2031
+ } catch {
2032
+ return { ...DEFAULT_SETTINGS };
2033
+ }
2034
+ }
2035
+ function writeSettings(patch, dataDir) {
2036
+ const next = {
2037
+ ...readSettings(dataDir),
2038
+ ...typeof patch.desktopNotifications === "boolean" ? { desktopNotifications: patch.desktopNotifications } : {}
2039
+ };
2040
+ const path = settingsPath(dataDir);
2041
+ mkdirSync3(dirname3(path), { recursive: true });
2042
+ writeFileSync3(path, JSON.stringify(next, null, 2) + "\n", "utf8");
2043
+ return next;
2044
+ }
2045
+
2046
+ // src/routes/gateway-settings.ts
2047
+ function createGatewaySettingsRouter() {
2048
+ const router = Router10();
2049
+ router.get("/", (_req, res) => {
2050
+ res.json(readSettings());
2051
+ });
2052
+ router.put("/", (req, res) => {
2053
+ const body = req.body ?? {};
2054
+ if ("desktopNotifications" in body && typeof body.desktopNotifications !== "boolean") {
2055
+ res.status(400).json({ error: "desktopNotifications must be a boolean" });
2056
+ return;
2057
+ }
2058
+ try {
2059
+ const saved = writeSettings({
2060
+ desktopNotifications: body.desktopNotifications
2061
+ });
2062
+ res.json(saved);
2063
+ } catch (err) {
2064
+ res.status(500).json({ error: `Could not save settings: ${err.message}` });
2065
+ }
2066
+ });
2067
+ return router;
2068
+ }
2069
+
2070
+ // src/lib/notification-dispatcher.ts
2071
+ var TRIGGER_EVENTS = ["proposal-added", "action-approval-needed"];
2072
+ var TITLE = "Suveren";
2073
+ var WAITING_MESSAGE = "Something is waiting for your review.";
2074
+ var FLOOD_THRESHOLD = 10;
2075
+ var floodMessage = (n) => `Unusual volume: ${n} requests in the last minute.`;
2076
+ var DEBOUNCE_MS = 5e3;
2077
+ var COOLDOWN_MS = 6e4;
2078
+ var NotificationDispatcher = class {
2079
+ debounceTimer = null;
2080
+ cooldownTimer = null;
2081
+ pendingWhileCoolingDown = false;
2082
+ /** Events observed since the current window opened — the flood signal only. */
2083
+ observedInWindow = 0;
2084
+ unsubscribe = null;
2085
+ now;
2086
+ setTimeoutFn;
2087
+ notifyFn;
2088
+ settingsFn;
2089
+ url;
2090
+ constructor(opts = {}) {
2091
+ this.now = opts.now ?? Date.now;
2092
+ this.setTimeoutFn = opts.setTimeoutFn ?? setTimeout;
2093
+ this.notifyFn = opts.notifyFn ?? notify;
2094
+ this.settingsFn = opts.settingsFn ?? readSettings;
2095
+ this.url = opts.url;
2096
+ }
2097
+ /** Subscribe to the bus. Returns a stop function. */
2098
+ start() {
2099
+ if (this.unsubscribe) return this.unsubscribe;
2100
+ const off = eventBus.subscribe((event) => {
2101
+ if (TRIGGER_EVENTS.includes(event.type)) this.onTrigger();
2102
+ });
2103
+ this.unsubscribe = () => {
2104
+ off();
2105
+ if (this.debounceTimer) clearTimeout(this.debounceTimer);
2106
+ if (this.cooldownTimer) clearTimeout(this.cooldownTimer);
2107
+ this.debounceTimer = null;
2108
+ this.cooldownTimer = null;
2109
+ this.unsubscribe = null;
2110
+ };
2111
+ return this.unsubscribe;
2112
+ }
2113
+ /** Exposed for tests; the bus subscription calls this. */
2114
+ onTrigger() {
2115
+ this.observedInWindow += 1;
2116
+ if (this.cooldownTimer) {
2117
+ this.pendingWhileCoolingDown = true;
2118
+ return;
2119
+ }
2120
+ if (this.debounceTimer) return;
2121
+ this.debounceTimer = this.setTimeoutFn(() => {
2122
+ this.debounceTimer = null;
2123
+ this.fire();
2124
+ }, DEBOUNCE_MS);
2125
+ }
2126
+ fire() {
2127
+ const count = this.observedInWindow;
2128
+ this.observedInWindow = 0;
2129
+ let enabled = true;
2130
+ try {
2131
+ enabled = this.settingsFn().desktopNotifications;
2132
+ } catch {
2133
+ enabled = true;
2134
+ }
2135
+ if (enabled) {
2136
+ const message = count >= FLOOD_THRESHOLD ? floodMessage(count) : WAITING_MESSAGE;
2137
+ this.notifyFn(TITLE, message, process.platform, this.url);
2138
+ }
2139
+ this.cooldownTimer = this.setTimeoutFn(() => {
2140
+ this.cooldownTimer = null;
2141
+ if (this.pendingWhileCoolingDown) {
2142
+ this.pendingWhileCoolingDown = false;
2143
+ this.fire();
2144
+ }
2145
+ }, COOLDOWN_MS);
2146
+ }
2147
+ };
2148
+ function startNotificationDispatcher(opts = {}) {
2149
+ return new NotificationDispatcher(opts).start();
2150
+ }
2151
+
1980
2152
  // src/lib/denials-reader.ts
1981
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1982
- import { join as join3 } from "path";
2153
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
2154
+ import { join as join4 } from "path";
1983
2155
  function loadDenials(dataDir, decrypt) {
1984
- const encPath = join3(dataDir, "denials.enc.json");
1985
- const plainPath = join3(dataDir, "denials.json");
2156
+ const encPath = join4(dataDir, "denials.enc.json");
2157
+ const plainPath = join4(dataDir, "denials.json");
1986
2158
  if (existsSync4(encPath)) {
1987
- const { blob } = JSON.parse(readFileSync3(encPath, "utf-8"));
2159
+ const { blob } = JSON.parse(readFileSync4(encPath, "utf-8"));
1988
2160
  return JSON.parse(decrypt(blob)).records ?? [];
1989
2161
  }
1990
2162
  if (existsSync4(plainPath)) {
1991
- return JSON.parse(readFileSync3(plainPath, "utf-8")).records ?? [];
2163
+ return JSON.parse(readFileSync4(plainPath, "utf-8")).records ?? [];
1992
2164
  }
1993
2165
  return [];
1994
2166
  }
@@ -2003,8 +2175,8 @@ function selectDenials(records, opts = {}) {
2003
2175
  // src/index.ts
2004
2176
  (function migrateDataDir() {
2005
2177
  if (process.env.SUVEREN_DATA_DIR) return;
2006
- const oldDir = join4(homedir3(), ".hap");
2007
- const newDir = join4(homedir3(), ".suveren");
2178
+ const oldDir = join5(homedir4(), ".hap");
2179
+ const newDir = join5(homedir4(), ".suveren");
2008
2180
  if (!existsSync5(newDir) && existsSync5(oldDir)) {
2009
2181
  try {
2010
2182
  renameSync(oldDir, newDir);
@@ -2018,7 +2190,7 @@ function selectDenials(records, opts = {}) {
2018
2190
  var SP_URL2 = process.env.SUVEREN_AS_URL ?? "https://www.suveren.ai";
2019
2191
  var port = parseInt(process.env.SUVEREN_CP_PORT ?? "3402", 10);
2020
2192
  var HAP_MODE = process.env.HAP_MODE ?? "personal";
2021
- var UI_DIST = process.env.HAP_UI_DIST ?? join4(import.meta.dirname ?? __dirname, "../../ui/dist");
2193
+ var UI_DIST = process.env.HAP_UI_DIST ?? join5(import.meta.dirname ?? __dirname, "../../ui/dist");
2022
2194
  var vault = new Vault();
2023
2195
  var internalSecret2 = process.env.SUVEREN_INTERNAL_SECRET ?? randomBytes2(32).toString("hex");
2024
2196
  setInternalSecret(internalSecret2);
@@ -2247,12 +2419,13 @@ app.get("/auth/oauth/:integrationId/health", authGuard, async (req, res) => {
2247
2419
  res.json({ status: "failed", account, error: err instanceof Error ? err.message : "probe failed" });
2248
2420
  }
2249
2421
  });
2250
- app.get("/events", requireAuthQueryOrHeader(vault), createEventsHandler());
2422
+ app.get("/events", requireAllowedHost, requireAuthQueryOrHeader(vault), createEventsHandler());
2251
2423
  app.use("/vault", jsonParser, authGuard, createVaultRouter(vault));
2252
2424
  app.use("/ai", jsonParser, authGuard, createAIRouter(vault));
2253
2425
  app.use("/ai-prompts", jsonParser, authGuard, createAIPromptsRouter());
2254
2426
  app.use("/mcp", jsonParser, authGuard, createMCPRouter());
2255
2427
  app.use("/autostart", jsonParser, authGuard, createAutostartRouter());
2428
+ app.use("/api/gateway-settings", jsonParser, authGuard, createGatewaySettingsRouter());
2256
2429
  app.use("/api/encrypt-intent", jsonParser, authGuard, createEncryptIntentRouter());
2257
2430
  app.use("/api/decrypt-intent", jsonParser, authGuard, createDecryptIntentRouter(vault));
2258
2431
  app.use("/api/approved-intents", jsonParser, authGuard, createApprovedIntentsRouter(vault));
@@ -2384,7 +2557,7 @@ app.get("/active-authorizations", authGuard, async (_req, res) => {
2384
2557
  });
2385
2558
  app.get("/denials", authGuard, (req, res) => {
2386
2559
  try {
2387
- const dataDir = process.env.SUVEREN_DATA_DIR ?? join4(homedir3(), ".suveren");
2560
+ const dataDir = process.env.SUVEREN_DATA_DIR ?? join5(homedir4(), ".suveren");
2388
2561
  const all = loadDenials(dataDir, (blob) => vault.decrypt(blob));
2389
2562
  const since = Number(req.query.since);
2390
2563
  const limit = Number(req.query.limit);
@@ -2417,16 +2590,16 @@ app.post("/resync-gates", authGuard, async (_req, res) => {
2417
2590
  }
2418
2591
  });
2419
2592
  var AGENT_CONTEXT_MAX_BYTES = 16 * 1024;
2420
- var SUVEREN_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join4(homedir3(), ".suveren");
2593
+ var SUVEREN_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join5(homedir4(), ".suveren");
2421
2594
  app.get("/agent-brief/context", authGuard, async (_req, res) => {
2422
2595
  try {
2423
- const { readFileSync: readFileSync5, existsSync: fileExists } = await import("fs");
2424
- const filePath = join4(SUVEREN_DATA_DIR2, "context.md");
2596
+ const { readFileSync: readFileSync6, existsSync: fileExists } = await import("fs");
2597
+ const filePath = join5(SUVEREN_DATA_DIR2, "context.md");
2425
2598
  if (!fileExists(filePath)) {
2426
2599
  res.json({ content: "" });
2427
2600
  return;
2428
2601
  }
2429
- const content = readFileSync5(filePath, "utf-8");
2602
+ const content = readFileSync6(filePath, "utf-8");
2430
2603
  res.json({ content });
2431
2604
  } catch (err) {
2432
2605
  console.error("[Control Plane] agent-brief/context GET failed:", err);
@@ -2444,11 +2617,11 @@ app.put("/agent-brief/context", jsonParser, authGuard, async (req, res) => {
2444
2617
  res.status(413).json({ error: `Context exceeds ${AGENT_CONTEXT_MAX_BYTES}-byte cap` });
2445
2618
  return;
2446
2619
  }
2447
- const { mkdirSync: mkdirSync3, writeFileSync: writeFileSync3, renameSync: renameSync2 } = await import("fs");
2448
- mkdirSync3(SUVEREN_DATA_DIR2, { recursive: true });
2449
- const filePath = join4(SUVEREN_DATA_DIR2, "context.md");
2620
+ const { mkdirSync: mkdirSync4, writeFileSync: writeFileSync4, renameSync: renameSync2 } = await import("fs");
2621
+ mkdirSync4(SUVEREN_DATA_DIR2, { recursive: true });
2622
+ const filePath = join5(SUVEREN_DATA_DIR2, "context.md");
2450
2623
  const tmpPath = `${filePath}.tmp`;
2451
- writeFileSync3(tmpPath, content, "utf-8");
2624
+ writeFileSync4(tmpPath, content, "utf-8");
2452
2625
  renameSync2(tmpPath, filePath);
2453
2626
  res.json({ ok: true });
2454
2627
  } catch (err) {
@@ -2503,8 +2676,8 @@ function detectInstallMethod() {
2503
2676
  if (dir.includes("/node_modules/@suveren/gateway/")) return "npm";
2504
2677
  let cursor = dir;
2505
2678
  for (let i = 0; i < 8; i++) {
2506
- if (existsSync5(join4(cursor, ".git"))) return "dev";
2507
- const parent = dirname3(cursor);
2679
+ if (existsSync5(join5(cursor, ".git"))) return "dev";
2680
+ const parent = dirname4(cursor);
2508
2681
  if (parent === cursor) break;
2509
2682
  cursor = parent;
2510
2683
  }
@@ -2516,10 +2689,10 @@ function detectRunningVersion() {
2516
2689
  return process.env.HAP_BUILD_SHA ?? "dev";
2517
2690
  }
2518
2691
  const dir = import.meta.dirname ?? __dirname;
2519
- const bundlePkg = join4(dir, "..", "..", "package.json");
2692
+ const bundlePkg = join5(dir, "..", "..", "package.json");
2520
2693
  if (existsSync5(bundlePkg)) {
2521
2694
  try {
2522
- const pkg = JSON.parse(readFileSync4(bundlePkg, "utf8"));
2695
+ const pkg = JSON.parse(readFileSync5(bundlePkg, "utf8"));
2523
2696
  if (pkg.name === "@suveren/gateway" && typeof pkg.version === "string") {
2524
2697
  return pkg.version;
2525
2698
  }
@@ -2557,7 +2730,7 @@ if (existsSync5(UI_DIST)) {
2557
2730
  }));
2558
2731
  app.get("*", (_req, res) => {
2559
2732
  res.setHeader("Cache-Control", "no-store, must-revalidate");
2560
- res.sendFile(join4(UI_DIST, "index.html"));
2733
+ res.sendFile(join5(UI_DIST, "index.html"));
2561
2734
  });
2562
2735
  } else {
2563
2736
  app.get("/", (_req, res) => {
@@ -2573,6 +2746,7 @@ app.listen(port, "0.0.0.0", () => {
2573
2746
  const { title, message, url } = lockedNotification(port);
2574
2747
  notify(title, message, process.platform, url);
2575
2748
  }
2749
+ startNotificationDispatcher({ url: `http://localhost:${port}` });
2576
2750
  console.error(`[Control Plane] SP proxy: ${SP_URL2}`);
2577
2751
  console.error(`[Control Plane] UI dist: ${UI_DIST}`);
2578
2752
  console.error(`[Control Plane] Internal secret: configured`);
@@ -0,0 +1 @@
1
+ @font-face{font-family:Inter;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/Inter-Regular.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/Inter-Medium.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/Inter-SemiBold.woff2) format("woff2")}@font-face{font-family:Inter;font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/Inter-Bold.woff2) format("woff2")}*,*:before,*:after{margin:0;padding:0;box-sizing:border-box}:root{--text-primary: #111111;--text-secondary: #111111;--text-tertiary: #111111;--text-muted: #888888;--bg-main: #fafafa;--bg-elevated: #ffffff;--bg-input: #ffffff;--accent: #888888;--accent-subtle: #f0f0f0;--accent-hover: #555555;--border: #e5e5e5;--edit-border: #c7c7c7;--danger: #dc2626;--danger-subtle: #fef2f2;--success: #16a34a;--success-subtle: #f0fdf4;--warning: #d97706;--btn-primary-bg: #111111;--btn-primary-fg: #ffffff;--btn-primary-bg-hover: #000000;--btn-primary-bg-active: #2a2a2a;--icon-filter: brightness(0) invert(1);--primary-glow: rgba(0, 0, 0, .2);--primary-focus-ring: rgba(0, 0, 0, .15);--bg-field: #111111;--text-field: #ffffff;--border-field: #111111;--placeholder-field: rgba(255, 255, 255, .55);--field-focus-ring: rgba(0, 0, 0, .18);--nav-height: 3.5rem;--sidebar-width: 15rem;--content-width: 75rem;--content-padding: 2rem}:root[data-theme=dark]{--text-primary: #ffffff;--text-secondary: #ffffff;--text-tertiary: #ffffff;--text-muted: #888888;--bg-main: #0a0a0a;--bg-elevated: #1a1a1a;--bg-input: #141414;--accent: #888888;--accent-subtle: #1f1f1f;--accent-hover: #bbbbbb;--border: #2a2a2a;--edit-border: #4a4a4a;--danger-subtle: #2a0a0a;--success-subtle: #0a2a0a;--btn-primary-bg: #ffffff;--btn-primary-fg: #000000;--btn-primary-bg-hover: #e5e5e5;--btn-primary-bg-active: #c4c4c4;--icon-filter: brightness(0);--primary-glow: rgba(255, 255, 255, .25);--primary-focus-ring: rgba(255, 255, 255, .2);--bg-field: #ffffff;--text-field: #111111;--border-field: #ffffff;--placeholder-field: rgba(0, 0, 0, .5);--field-focus-ring: rgba(255, 255, 255, .25)}html{scroll-behavior:smooth}body{font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-feature-settings:"cv11","ss01","ss03";font-size:16px;line-height:1.6;color:var(--text-primary);background:var(--bg-main);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}@media (min-width: 1900px){body{font-size:17.5px}}@media (min-width: 2800px){body{font-size:19px}}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}h1,h2,h3,h4{font-weight:700;letter-spacing:-.02em;line-height:1.2;color:var(--text-primary)}code{background:var(--bg-elevated);border:1px solid var(--border);padding:.15rem .4rem;border-radius:.25rem;font-size:.875em;font-family:SF Mono,Monaco,Cascadia Code,Courier New,monospace}.top-nav{position:fixed;top:0;width:100%;height:var(--nav-height);background:#fafafaeb;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border-bottom:1px solid var(--border);z-index:1000}[data-theme=dark] .top-nav{background:#0a0a0aeb}.top-nav-inner{max-width:100%;margin:0 auto;padding:0 1.5rem;height:100%;display:flex;align-items:center;gap:1rem}.logo-group{display:flex;align-items:center;gap:.5rem;flex-shrink:0}.logo-group svg{flex-shrink:0}.logo{font-weight:700;font-size:.95rem;color:var(--text-primary);text-decoration:none}.logo:hover{text-decoration:none}.version-badge{font-size:.65rem;font-weight:600;padding:.15rem .4rem;background:var(--btn-primary-bg);color:var(--btn-primary-fg);border-radius:.25rem}.gw-version{font-size:.7rem;padding:.15rem .4rem;border-radius:.25rem;color:var(--text-muted);background:var(--accent-subtle);font-variant-numeric:tabular-nums;white-space:nowrap}.gw-version[data-stale=true]{color:var(--warning);background:transparent;border:1px solid var(--warning)}.nav-spacer{flex:1}.nav-actions{display:flex;align-items:center;gap:.75rem}.user-chip{display:flex;align-items:center;gap:.5rem;font-size:.8rem;color:var(--text-secondary)}.user-chip strong{color:var(--text-primary)}.user-chip .dot{width:3px;height:3px;border-radius:50%;background:var(--text-tertiary)}.theme-toggle{width:2rem;height:2rem;background:none;border:1px solid var(--border);border-radius:.375rem;cursor:pointer;color:var(--text-secondary);font-size:1rem;display:flex;align-items:center;justify-content:center}.theme-toggle:hover{color:var(--accent);border-color:var(--accent)}.nav-logout{font-size:.8rem;color:var(--text-tertiary);cursor:pointer;background:none;border:none;font-family:inherit}.nav-logout:hover{color:var(--text-primary)}.domain-switcher{position:relative}.domain-switcher-btn{background:none;border:none;font-family:inherit;font-size:.8rem;color:var(--text-secondary);cursor:pointer;display:flex;align-items:center;gap:.25rem;padding:.125rem .25rem;border-radius:.25rem}.domain-switcher-btn:hover{color:var(--accent)}.domain-switcher-arrow{font-size:.6rem;color:var(--text-tertiary)}.domain-switcher-menu{position:absolute;top:calc(100% + .375rem);right:0;min-width:10rem;background:var(--bg-elevated);border:1px solid var(--border);border-radius:.5rem;box-shadow:0 4px 12px #0000001a;z-index:200;padding:.25rem}.domain-switcher-item{display:block;width:100%;text-align:left;background:none;border:none;font-family:inherit;font-size:.8rem;color:var(--text-secondary);padding:.5rem .75rem;border-radius:.375rem;cursor:pointer;white-space:nowrap}.domain-switcher-item:hover{background:var(--accent-subtle);color:var(--text-primary)}.domain-switcher-item.active{color:var(--accent);font-weight:600}.sidebar{position:fixed;top:var(--nav-height);left:0;width:var(--sidebar-width);height:calc(100vh - var(--nav-height));background:var(--bg-elevated);border-right:1px solid var(--border);padding:1rem 0;z-index:900;display:flex;flex-direction:column}.sidebar-nav{list-style:none;padding:0 .5rem;flex:1}.sidebar-item{display:flex;align-items:center;gap:.625rem;padding:.5rem .75rem;margin-bottom:.125rem;border-radius:.5rem;font-size:.85rem;font-weight:500;color:var(--text-secondary);cursor:pointer;transition:background .15s,color .15s;text-decoration:none}.sidebar-item:hover{background:var(--accent-subtle);color:var(--text-primary);text-decoration:none}.sidebar-item.active{background:var(--accent-subtle);color:var(--accent);font-weight:600}.sidebar-item .icon{width:1.125rem;text-align:center;font-size:.9rem;flex-shrink:0}.sidebar-section-label{font-size:.65rem;font-weight:600;text-transform:uppercase;letter-spacing:.06em;color:var(--text-tertiary);padding:1rem .75rem .375rem}.sidebar-context{padding:.75rem 1rem;border-top:1px solid var(--border);font-size:.75rem;color:var(--text-tertiary)}.sidebar-context .ctx-label{font-weight:600;color:var(--text-secondary);margin-bottom:.125rem}.sidebar-context .ctx-value{color:var(--text-primary);font-weight:500}.main-content{margin-left:var(--sidebar-width);margin-top:var(--nav-height);padding:2rem;min-height:calc(100vh - var(--nav-height))}.main-content.no-sidebar{margin-left:0}.page-inner{max-width:56rem;margin:0 auto}.page-header{margin-bottom:1.5rem}.page-title{font-size:1.5rem;margin-bottom:.25rem}.page-subtitle{font-size:.9rem;color:var(--text-secondary)}.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5rem;padding:.625rem 1.25rem;font-size:.9rem;font-weight:500;font-family:inherit;border-radius:.5rem;border:none;cursor:pointer;transition:all .2s;text-decoration:none;line-height:1.4}.btn:disabled{opacity:.5;cursor:not-allowed}.btn-primary{background:var(--btn-primary-bg);color:var(--btn-primary-fg);border:2px solid var(--btn-primary-bg)}.btn-primary:hover:not(:disabled){background:var(--btn-primary-bg-hover);border-color:var(--btn-primary-bg-hover);color:var(--btn-primary-fg);transform:translateY(-1px);box-shadow:0 4px 12px var(--primary-glow)}.btn-primary:active:not(:disabled){background:var(--btn-primary-bg-active);border-color:var(--btn-primary-bg-active)}.btn-secondary{background:transparent;color:var(--text-primary);border:1px solid var(--border)}.btn-secondary:hover:not(:disabled){border-color:var(--btn-primary-bg);color:var(--text-primary);background:var(--accent-subtle)}.btn-ghost{background:transparent;color:var(--text-secondary);border:none;padding:.5rem .75rem}.btn-ghost:hover:not(:disabled){color:var(--text-primary);background:var(--accent-subtle)}.btn-full{width:100%}.btn-sm{padding:.375rem .75rem;font-size:.8rem}.btn-lg{padding:.875rem 2rem;font-size:1rem;font-weight:600}.form-group{margin-bottom:1.25rem}.form-label{display:block;font-size:.875rem;font-weight:600;color:var(--text-primary);margin-bottom:.375rem}.form-input,.form-select,.form-textarea{width:100%;padding:.625rem .75rem;font-size:.9rem;font-family:inherit;color:var(--text-field);background:var(--bg-field);border:1px solid var(--border-field);border-radius:.5rem;outline:none;transition:box-shadow .2s,border-color .2s}.form-input::placeholder,.form-textarea::placeholder{color:var(--placeholder-field);opacity:1}.form-input:focus,.form-textarea:focus,.form-select:focus{box-shadow:0 0 0 3px var(--field-focus-ring)}.form-input:disabled,.form-textarea:disabled,.form-select:disabled{opacity:.55;cursor:not-allowed}.form-input-invalid,.form-input-invalid:focus{border-color:var(--danger);box-shadow:0 0 0 3px #dc26262e}.form-textarea{resize:vertical;min-height:120px;line-height:1.5}.form-hint{font-size:.8rem;color:var(--text-tertiary);margin-top:.375rem}.form-select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ffffff' d='M6 8L1 3h10z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .75rem center;padding-right:2.25rem}[data-theme=dark] .form-select{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23111111' d='M6 8L1 3h10z'/%3E%3C/svg%3E")}.card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.75rem;padding:1.5rem}.card+.card{margin-top:1rem}.card-highlight{animation:hap-card-highlight 2.4s ease-out 1}@keyframes hap-card-highlight{0%{box-shadow:0 0 0 2px var(--accent),0 0 24px var(--accent-subtle);border-color:var(--accent)}60%{box-shadow:0 0 0 2px var(--accent),0 0 24px var(--accent-subtle);border-color:var(--accent)}to{box-shadow:0 0 0 0 transparent,0 0 0 transparent;border-color:var(--border)}}@media (prefers-reduced-motion: reduce){.card-highlight{animation:none;border-color:var(--accent)}}.card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:1rem}.card-title{font-size:1rem;font-weight:600;margin:0}.auth-card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;transition:opacity .2s,border-color .2s;cursor:pointer}.auth-card:hover{border-color:var(--accent)}.auth-card+.auth-card{margin-top:.75rem}.auth-card.expired{opacity:.5}.auth-card-header{display:flex;align-items:center;gap:.75rem;margin-bottom:.5rem;flex-wrap:wrap}.auth-card-path{font-weight:600;font-size:.95rem}.auth-card-time{font-size:.8rem;color:var(--text-tertiary);margin-left:auto}.success-card{background:var(--accent-subtle);border:1px solid var(--accent);border-radius:.75rem;padding:1.5rem;text-align:center}.success-card-title{font-size:1.1rem;font-weight:700;color:var(--accent);margin-bottom:.5rem}.success-card-hash{font-family:SF Mono,Monaco,monospace;font-size:.8rem;color:var(--text-secondary);word-break:break-all;margin-bottom:.5rem}.status-badge{display:inline-block;padding:.2rem .6rem;border-radius:9999px;font-size:.7rem;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.status-active{background:var(--accent-subtle);color:var(--accent)}.status-pending{background:var(--danger-subtle);color:var(--danger)}.status-expired{background:var(--bg-elevated);color:var(--text-tertiary)}.status-revoked{background:var(--danger-subtle);color:var(--danger)}.int-chip{display:inline-flex;align-items:center;gap:.4rem;padding:.2rem .6rem;border-radius:9999px;font-size:.7rem;font-weight:600;letter-spacing:.03em;text-transform:uppercase}.int-chip .int-dot{width:6px;height:6px;border-radius:50%;flex:none}.int-chip-ok{background:var(--success-subtle);color:var(--success)}.int-chip-ok .int-dot{background:var(--success)}.int-chip-bad{background:var(--danger-subtle);color:var(--danger)}.int-chip-bad .int-dot{background:var(--danger)}.int-chip-idle{background:var(--accent-subtle);color:var(--accent)}.int-chip-idle .int-dot{background:#bbb}.int-details{border-top:1px solid var(--border);padding-top:.7rem;margin-top:.25rem}.int-details>summary{cursor:pointer;font-size:.78rem;color:var(--text-muted);list-style:none;display:flex;align-items:center;gap:.35rem;-webkit-user-select:none;user-select:none}.int-details>summary::-webkit-details-marker{display:none}.int-details>summary:before{content:"▸";font-size:.7rem;transition:transform .15s}.int-details[open]>summary:before{transform:rotate(90deg)}.int-meta{display:grid;grid-template-columns:auto 1fr;gap:.4rem 1rem;font-size:.8rem;margin-top:.7rem}.int-meta .int-k{color:var(--text-muted)}.int-account{font-size:.85rem;display:flex;align-items:center;gap:.5rem}.int-account .int-none{color:var(--text-muted);font-style:italic}.domain-badge{display:inline-block;padding:.2rem .5rem;font-size:.8rem;font-family:SF Mono,Monaco,monospace;background:var(--bg-main);border:1px solid var(--border);border-radius:.25rem;color:var(--text-secondary)}.domain-attested{color:var(--accent);border-color:var(--accent)}.domain-missing{color:var(--text-tertiary)}.profile-badge{display:inline-block;padding:.15rem .5rem;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.03em;background:var(--accent-subtle);color:var(--accent);border-radius:.25rem}.step-indicator{display:flex;align-items:center;justify-content:center;margin-bottom:2rem;gap:0}.step{display:flex;align-items:center;position:relative}.step-circle{width:2rem;height:2rem;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:600;border:2px solid var(--border);color:var(--text-tertiary);background:var(--bg-main);flex-shrink:0;transition:all .2s}.step-circle.completed{background:var(--accent);border-color:var(--accent);color:#fff;cursor:pointer}.step-circle.current{border-color:var(--accent);color:var(--accent);box-shadow:0 0 0 3px var(--primary-focus-ring)}.step-line{width:2rem;height:2px;background:var(--border);flex-shrink:1;min-width:.5rem}.step-line.completed{background:var(--accent)}.step-label{font-size:.65rem;color:var(--text-tertiary);position:absolute;top:2.25rem;white-space:nowrap;text-align:center}.alert{padding:.75rem 1rem;border-radius:.5rem;font-size:.875rem;margin-bottom:1rem;color:#fff}.alert-error{background:var(--danger)}.alert-info{background:var(--accent)}.alert-success{background:#1a7f37}.nav-tabs{display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:1.5rem}.nav-tab{padding:.75rem 1.25rem;font-size:.9rem;font-weight:500;color:var(--text-tertiary);background:none;border:none;border-bottom:2px solid transparent;cursor:pointer;transition:color .2s;margin-bottom:-1px;font-family:inherit}.nav-tab:hover{color:var(--text-primary)}.nav-tab.active{color:var(--accent);border-bottom-color:var(--accent);font-weight:600}.review-grid{display:grid;grid-template-columns:auto 1fr;gap:.5rem 1rem;font-size:.9rem;margin-bottom:1.5rem}.review-grid dt{font-weight:600;color:var(--text-secondary);padding:.25rem 0}.review-grid dd{color:var(--text-primary);padding:.25rem 0}.gate-content-block{background:var(--bg-main);border:1px solid var(--border);border-radius:.5rem;padding:1rem;font-size:.875rem;margin-bottom:1.5rem}.gate-content-item{margin-bottom:.5rem}.gate-content-item:last-child{margin-bottom:0}.gate-content-label{font-weight:600;color:var(--text-secondary);font-size:.75rem;text-transform:uppercase;letter-spacing:.04em;margin-bottom:.125rem}.gate-content-text{color:var(--text-primary);line-height:1.5}.empty-state{text-align:center;padding:3rem 1.5rem;color:var(--text-tertiary)}.empty-state-icon{font-size:2.5rem;margin-bottom:.75rem;opacity:.5}.empty-state-title{font-size:1rem;font-weight:600;color:var(--text-secondary);margin-bottom:.25rem}.empty-state-text{font-size:.875rem;color:var(--text-tertiary);margin-bottom:1rem;max-width:28rem;margin-left:auto;margin-right:auto}.login-split{margin-top:var(--nav-height);display:flex;min-height:calc(100vh - var(--nav-height))}.login-split-left{flex:1;background:var(--bg-main);display:flex;align-items:center;justify-content:center;padding:4rem 3rem}.login-split-right{flex:1;background:var(--bg-elevated);border-left:1px solid var(--border);display:flex;align-items:center;justify-content:center;padding:4rem 3rem}.login-page{display:flex;align-items:center;justify-content:center;min-height:calc(100vh - var(--nav-height));padding:2rem}.login-card{width:100%;max-width:420px}.login-header{text-align:center;margin-bottom:2rem}.login-title{font-size:1.5rem;font-weight:700;margin-bottom:.5rem}.login-subtitle{font-size:.9rem;color:var(--text-secondary);line-height:1.5}.login-divider{display:flex;align-items:center;gap:.75rem;margin:1.5rem 0;font-size:.8rem;color:var(--text-tertiary)}.login-divider:before,.login-divider:after{content:"";flex:1;height:1px;background:var(--border)}.onboarding-card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.75rem;padding:2rem;max-width:32rem;margin:0 auto}.onboarding-step{display:flex;gap:1rem;margin-bottom:1.25rem}.onboarding-step:last-child{margin-bottom:0}.onboarding-num{width:1.75rem;height:1.75rem;border-radius:50%;background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0;margin-top:.1rem}.onboarding-num.done{background:var(--accent)}.onboarding-text h4{font-size:.9rem;margin-bottom:.125rem}.onboarding-text p{font-size:.825rem;color:var(--text-secondary);line-height:1.5}.selection-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:.75rem;margin-bottom:1.5rem}.selection-card{background:var(--bg-elevated);border:2px solid var(--border);border-radius:.75rem;padding:1.25rem;cursor:pointer;transition:all .15s}.selection-card:hover{border-color:var(--accent);transform:translateY(-1px);box-shadow:0 4px 12px #0000000f}.selection-card.selected{border-color:var(--accent);background:var(--accent-subtle)}.selection-card-title{font-size:1rem;font-weight:600;margin-bottom:.25rem}.selection-card-subtitle{font-size:.8rem;color:var(--text-secondary);margin-bottom:.5rem}.selection-card-id{font-size:.7rem;color:var(--text-tertiary);margin-bottom:.375rem;word-break:break-all}.selection-card-meta{display:flex;gap:.375rem;flex-wrap:wrap}.dashboard-section{margin-bottom:2rem}.section-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem}.section-title{font-size:1rem;font-weight:600}.section-count{font-size:.8rem;color:var(--text-tertiary)}.ttl-badge{display:inline-flex;align-items:center;gap:.25rem;font-size:.75rem;font-weight:600;padding:.2rem .5rem;border-radius:.25rem;font-family:SF Mono,Monaco,monospace}.ttl-ok{background:var(--accent-subtle);color:var(--accent)}.ttl-warn{background:var(--danger-subtle);color:var(--danger)}.quick-actions{display:grid;grid-template-columns:repeat(auto-fill,minmax(16rem,1fr));gap:.75rem;margin-bottom:2rem}.quick-action-card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;cursor:pointer;transition:all .15s;display:flex;align-items:flex-start;gap:.875rem;text-decoration:none;color:inherit}.quick-action-card:hover{border-color:var(--accent);transform:translateY(-1px);box-shadow:0 4px 12px #0000000f;text-decoration:none}.quick-action-icon{width:2.5rem;height:2.5rem;border-radius:.5rem;background:var(--accent-subtle);color:var(--accent);display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex-shrink:0}.quick-action-title{font-size:.9rem;font-weight:600;margin-bottom:.125rem}.quick-action-desc{font-size:.8rem;color:var(--text-secondary)}.pr-card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;cursor:pointer;transition:border-color .15s}.pr-card:hover{border-color:var(--accent)}.pr-card+.pr-card{margin-top:.75rem}.pr-card-header{display:flex;align-items:center;gap:.5rem;margin-bottom:.375rem}.pr-card-repo{font-size:.75rem;color:var(--text-tertiary);font-family:SF Mono,Monaco,monospace}.pr-card-title{font-weight:600;font-size:.95rem}.pr-card-footer{display:flex;align-items:center;gap:.75rem;margin-top:.5rem;flex-wrap:wrap}.member-row{display:flex;align-items:center;gap:.75rem;padding:.75rem 0;border-bottom:1px solid var(--border)}.member-row:last-child{border-bottom:none}.member-avatar{width:2rem;height:2rem;border-radius:50%;background:var(--accent-subtle);color:var(--accent);display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;flex-shrink:0}.member-info{flex:1}.member-name{font-size:.9rem;font-weight:600}.member-email{font-size:.75rem;color:var(--text-tertiary)}.member-domains{display:flex;gap:.25rem;flex-wrap:wrap}.char-counter{text-align:right;font-size:.75rem;color:var(--text-tertiary);margin-top:.25rem}.invite-input-row{display:flex;gap:.5rem}.invite-input-row .form-input{flex:1}.service-card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.75rem;padding:1.25rem;display:flex;align-items:center;gap:1rem;transition:border-color .15s}.service-card:hover{border-color:var(--accent)}.service-card+.service-card{margin-top:.75rem}.service-icon{width:2.75rem;height:2.75rem;border-radius:.625rem;display:flex;align-items:center;justify-content:center;font-size:1.25rem;flex-shrink:0}.service-icon-configured{background:var(--success-subtle);color:var(--success)}.service-icon-missing{background:var(--bg-main);color:var(--text-tertiary);border:1px dashed var(--border)}.service-icon-error{background:var(--danger-subtle);color:var(--danger)}.service-info{flex:1}.service-name{font-size:.95rem;font-weight:600;margin-bottom:.125rem}.service-desc{font-size:.8rem;color:var(--text-secondary);margin-bottom:.375rem}.service-status{display:inline-flex;align-items:center;gap:.375rem;font-size:.75rem;font-weight:600}.service-status-dot{width:6px;height:6px;border-radius:50%}.service-status-connected .service-status-dot{background:var(--success)}.service-status-connected{color:var(--success)}.service-status-missing .service-status-dot{background:var(--text-tertiary)}.service-status-missing{color:var(--text-tertiary)}.service-status-error .service-status-dot{background:var(--danger)}.service-status-error{color:var(--danger)}.service-tools{display:flex;gap:.25rem;flex-wrap:wrap;margin-top:.25rem}.service-tool-badge{display:inline-block;padding:.1rem .4rem;font-size:.65rem;font-family:SF Mono,Monaco,monospace;background:var(--bg-main);border:1px solid var(--border);border-radius:.25rem;color:var(--text-tertiary)}.template-card{display:flex;flex-direction:column;align-items:flex-start;text-align:left;padding:.75rem;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-main);cursor:pointer;transition:border-color .15s,background .15s;font-family:inherit;font-size:inherit;color:inherit}.template-card:hover{border-color:var(--btn-primary-bg);background:var(--bg-elevated)}.template-mode{display:inline-block;padding:.1rem .4rem;font-size:.6rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;border-radius:3px}.template-mode-auto{background:#16a34a1f;color:#16a34a}.template-mode-review{background:#d977061f;color:#d97706}.template-risk{font-size:.6rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--text-tertiary)}.template-tag{display:inline-block;padding:.1rem .35rem;font-size:.6rem;background:var(--bg-elevated);border:1px solid var(--border);border-radius:3px;color:var(--text-secondary)}@media (max-width: 560px){.template-card{grid-column:span 1!important}}.modal-backdrop{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;z-index:1500;display:flex;align-items:center;justify-content:center}.modal{background:var(--bg-elevated);border-radius:.75rem;width:100%;max-width:32rem;max-height:calc(100vh - 4rem);overflow-y:auto;box-shadow:0 16px 48px #00000026}.modal-header{padding:1.5rem 1.5rem 0;display:flex;justify-content:space-between;align-items:flex-start}.modal-title{font-size:1.125rem;font-weight:700}.modal-close{width:2rem;height:2rem;background:none;border:none;cursor:pointer;color:var(--text-tertiary);font-size:1.25rem;display:flex;align-items:center;justify-content:center;border-radius:.375rem}.modal-close:hover{background:var(--bg-main);color:var(--text-primary)}.modal-body{padding:1.25rem 1.5rem}.modal-footer{padding:0 1.5rem 1.5rem;display:flex;gap:.5rem;justify-content:flex-end}.status-banner{padding:.625rem 1rem;font-size:.8rem;display:flex;align-items:center;gap:.5rem;border-radius:.5rem;margin-bottom:1rem}.status-banner-warn{background:var(--accent-subtle);color:var(--accent-hover);border:1px solid var(--accent)}.status-banner-error{background:var(--danger-subtle);color:var(--danger);border:1px solid var(--danger)}.status-banner-success{background:var(--success-subtle);color:var(--success);border:1px solid var(--success)}.status-banner-icon{font-size:1rem;flex-shrink:0}.status-banner-text{flex:1}.status-banner a{font-weight:600}.vault-progress{display:flex;flex-direction:column;align-items:center;gap:.75rem;padding:1.5rem 0}.vault-spinner{width:2rem;height:2rem;border:3px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.vault-progress-text{font-size:.875rem;color:var(--text-secondary)}.service-health-row{display:flex;align-items:center;gap:.75rem;padding:.625rem 0;border-bottom:1px solid var(--border);font-size:.85rem}.service-health-row:last-child{border-bottom:none}.service-health-indicator{width:8px;height:8px;border-radius:50%;flex-shrink:0}.service-health-indicator.connected{background:var(--success)}.service-health-indicator.missing{background:var(--text-tertiary)}.service-health-indicator.error{background:var(--danger)}.service-health-name{font-weight:600;flex:1}.service-health-detail{font-size:.75rem;color:var(--text-tertiary)}.cred-field{position:relative}.cred-field .form-input{padding-right:3rem}.cred-toggle{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:none;border:none;font-size:.75rem;color:var(--text-tertiary);cursor:pointer;padding:.25rem}.cred-toggle:hover{color:var(--accent)}.context-strip{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.5rem;padding:.625rem 1rem;margin-bottom:1.5rem;display:flex;align-items:center;gap:.75rem;font-size:.8rem}.context-strip-label{font-weight:600;color:var(--text-secondary)}.timeline{position:relative;padding-left:2rem}.timeline:before{content:"";position:absolute;left:.5rem;top:0;bottom:0;width:2px;background:var(--border)}.timeline-event{position:relative;margin-bottom:1.5rem}.timeline-event:before{content:"";position:absolute;left:-1.625rem;top:.5rem;width:10px;height:10px;border-radius:50%;background:var(--accent);border:2px solid var(--bg-main)}.timeline-event.expired:before{background:var(--text-tertiary)}.error-message{padding:.75rem 1rem;background:#dc262614;border:1px solid var(--danger);border-radius:.5rem;color:var(--danger);font-size:.875rem;margin-bottom:1rem}.mobile-menu-btn{display:none;width:2.25rem;height:2.25rem;background:none;border:1px solid var(--border);border-radius:.375rem;cursor:pointer;color:var(--text-secondary);font-size:1.1rem;align-items:center;justify-content:center;font-family:inherit}.mobile-menu-btn:hover{color:var(--accent);border-color:var(--accent)}.mobile-menu-backdrop{display:none;position:fixed;top:0;right:0;bottom:0;left:0;background:#0006;z-index:2000}.mobile-menu-panel{display:none;position:fixed;top:0;right:0;bottom:0;width:16rem;background:var(--bg-elevated);border-left:1px solid var(--border);z-index:2001;flex-direction:column;overflow-y:auto}.mobile-menu-header{padding:1.25rem 1rem;border-bottom:1px solid var(--border);display:flex;flex-direction:column;gap:.25rem}.mobile-menu-nav{flex:1;padding:.5rem;display:flex;flex-direction:column}.mobile-menu-item{display:flex;align-items:center;gap:.625rem;padding:.5rem .75rem;margin-bottom:.125rem;border-radius:.375rem;font-size:.85rem;color:var(--text-secondary);text-decoration:none;transition:background .15s,color .15s}.mobile-menu-item:hover{background:var(--accent-subtle);color:var(--text-primary)}.mobile-menu-item.active{background:var(--accent-subtle);color:var(--accent);font-weight:600}.mobile-menu-item .icon{width:1.125rem;text-align:center;font-size:.9rem;flex-shrink:0}.mobile-menu-footer{padding:.75rem 1rem;border-top:1px solid var(--border);display:flex;flex-direction:column;gap:.25rem}.mobile-menu-action{display:flex;align-items:center;gap:.625rem;padding:.5rem .75rem;border-radius:.375rem;font-size:.85rem;color:var(--text-secondary);background:none;border:none;cursor:pointer;font-family:inherit;text-align:left}.mobile-menu-action:hover{background:var(--accent-subtle);color:var(--text-primary)}.mobile-menu-action .icon{width:1.125rem;text-align:center;font-size:.9rem;flex-shrink:0}@media (max-width: 768px){.sidebar{display:none}.main-content{margin-left:0}.quick-actions,.selection-grid{grid-template-columns:1fr}.nav-actions-desktop,.step-label-text{display:none}.mobile-menu-btn{display:flex}.mobile-menu-backdrop{display:block}.mobile-menu-panel{display:flex}.login-split{flex-direction:column;min-height:auto}.login-split-left{padding:3rem 1.5rem;align-items:flex-start}.login-split-right{max-width:none;border-left:none;border-top:1px solid var(--border);padding:2.5rem 1.5rem}}.update-banner{position:fixed;top:var(--nav-height);left:0;right:0;z-index:900;background:#c0392b;color:#fff;border-top:1px solid rgba(0,0,0,.2);border-bottom:1px solid rgba(0,0,0,.2);padding:.6rem 1.25rem;display:flex;flex-direction:column;gap:.6rem}body.has-update-banner .sidebar{top:calc(var(--nav-height) + var(--update-banner-h, 0px));height:calc(100vh - var(--nav-height) - var(--update-banner-h, 0px))}body.has-update-banner .main-content{margin-top:calc(var(--nav-height) + var(--update-banner-h, 0px))}.update-banner-row{display:flex;align-items:center;justify-content:space-between;gap:1rem;min-width:0}.update-banner-cmd-row{flex-wrap:wrap}.update-banner-text{font-size:.9rem;font-weight:600;color:#fff}.update-banner-cmd{display:block;flex:1;min-width:0;font-size:.78rem;padding:.5rem .75rem;word-break:break-all;white-space:pre-wrap;background:#0003;border:1px solid rgba(255,255,255,.65);border-radius:.375rem;font-family:SF Mono,Fira Code,Fira Mono,Menlo,Consolas,monospace;color:#fff;-webkit-user-select:all;user-select:all}.update-banner-hint{margin:0;font-size:.8rem;color:#ffffffe6}.update-banner .btn-secondary{color:#fff;border-color:#ffffffa6;background:transparent}.update-banner .btn-secondary:hover:not(:disabled){background:#ffffff26;color:#fff;border-color:#fff}.update-banner-x{background:transparent;border:none;color:#ffffffd9;cursor:pointer;font-size:1.15rem;line-height:1;padding:0 .25rem}.update-banner-x:hover{color:#fff}.update-banner.is-live{background:var(--success)}body.is-updating .top-nav,body.is-updating .sidebar,body.is-updating .main-content,body.is-updating .update-banner{filter:blur(5px);pointer-events:none;-webkit-user-select:none;user-select:none}.update-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:2000;display:flex;align-items:center;justify-content:center;background:#11111159;padding:1rem}.update-overlay-card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:1rem;padding:2rem 2.25rem;max-width:460px;width:100%;text-align:center;box-shadow:0 20px 60px #00000040}.update-overlay-card h2{margin:0 0 .6rem;font-size:1.15rem}.update-overlay-card p{margin:0 0 .5rem;font-size:.88rem;color:var(--text-muted);line-height:1.55}.update-spinner{width:2.25rem;height:2.25rem;border:3px solid var(--border);border-top-color:var(--text-primary);border-radius:50%;margin:0 auto 1.1rem;animation:update-spin 1s linear infinite}@keyframes update-spin{to{transform:rotate(360deg)}}.update-cmd-row{display:flex;gap:.5rem;align-items:flex-start;margin:.9rem 0;text-align:left}.update-cmd{flex:1;min-width:0;font-size:.78rem;padding:.6rem .75rem;background:#111;color:#fff;border-radius:.45rem;font-family:SF Mono,Fira Code,Fira Mono,Menlo,Consolas,monospace;word-break:break-all;white-space:pre-wrap}.update-status{font-size:.8rem;color:var(--text-muted);margin:.4rem 0 1.1rem;display:flex;align-items:center;justify-content:center;gap:.4rem}.update-pulse{width:.55rem;height:.55rem;border-radius:50%;background:var(--success);animation:update-pulse 1.2s ease infinite}@keyframes update-pulse{0%,to{opacity:.3}50%{opacity:1}}.update-overlay-actions{display:flex;gap:.6rem;justify-content:center}.search-filter-bar{display:flex;gap:.5rem;margin-bottom:.75rem}.search-input-wrap{flex:1;position:relative}.search-icon{position:absolute;left:.75rem;top:50%;transform:translateY(-50%);color:var(--text-tertiary);font-size:1rem;pointer-events:none}.search-input{padding-left:2.25rem!important;padding-right:2rem!important}.search-clear{position:absolute;right:.5rem;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text-tertiary);font-size:1.1rem;cursor:pointer;padding:.25rem;line-height:1}.search-clear:hover{color:var(--text-primary)}.filter-panel{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.5rem;padding:1rem;margin-bottom:.75rem;display:flex;flex-direction:column;gap:.75rem}.filter-section{display:flex;align-items:baseline;gap:.75rem}.filter-label{font-size:.75rem;font-weight:600;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;min-width:5rem;flex-shrink:0}.filter-chips{display:flex;flex-wrap:wrap;gap:.375rem}.filter-chip{font-family:inherit;font-size:.8rem;padding:.3rem .625rem;border-radius:1rem;border:1px solid var(--border);background:var(--bg-main);color:var(--text-secondary);cursor:pointer;transition:all .15s}.filter-chip:hover{border-color:var(--accent);color:var(--text-primary)}.filter-chip.selected{background:var(--accent-subtle);border-color:var(--accent);color:var(--accent);font-weight:600}.active-filters{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;margin-bottom:.75rem}.active-chip{display:inline-flex;align-items:center;gap:.25rem;font-size:.75rem;padding:.2rem .5rem;border-radius:1rem;background:var(--accent-subtle);color:var(--accent);cursor:pointer;transition:background .15s}.active-chip:hover{background:var(--accent);color:var(--bg-elevated)}.chip-remove{font-size:.85rem;line-height:1}.clear-filters{font-family:inherit;font-size:.75rem;color:var(--text-tertiary);background:none;border:none;cursor:pointer;padding:.2rem 0}.clear-filters:hover{color:var(--text-primary)}.receipt-action{display:inline-block;font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:1rem;background:var(--bg-main);border:1px solid var(--border);color:var(--text-secondary);font-family:SF Mono,Monaco,monospace}.bounds-section,.context-section{background:var(--bg-elevated);border:2px solid var(--edit-border);border-radius:.75rem;padding:1.25rem;margin-bottom:1.25rem;transition:border-color .15s}.bounds-section:hover,.bounds-section:focus-within,.context-section:hover,.context-section:focus-within{border-color:var(--accent-hover)}.bounds-section-header{display:flex;align-items:flex-start;gap:.625rem;margin-bottom:1rem}.bounds-section-icon{font-size:1.1rem;flex-shrink:0;margin-top:.1rem}.bounds-section-title{font-size:.9rem;font-weight:700;color:var(--text-primary)}.bounds-section-subtitle{font-size:.75rem;color:var(--text-tertiary)}.bounds-fields-grid{display:flex;flex-direction:column;gap:.75rem}.bounds-field-row{display:flex;flex-direction:row;align-items:center;justify-content:space-between;gap:1rem;width:100%}.bounds-field-label{flex:1;min-width:0}.bounds-field-label .form-label{display:inline;margin-bottom:0}.bounds-field-input{flex-shrink:0;display:flex;flex-direction:column;align-items:flex-end}.bounds-field-input .bounded-input{align-items:flex-end}.bounds-field-input .bounded-input-presets{justify-content:flex-end}.bounds-field-label-inline{display:flex;align-items:center;gap:.375rem;margin-bottom:.375rem}.bounds-field-label-inline .form-label{margin-bottom:0}.field-info-toggle{display:inline-flex;align-items:center;justify-content:center;width:1.125rem;height:1.125rem;border-radius:50%;border:1px solid var(--border);background:var(--bg-main);color:var(--text-tertiary);font-size:.65rem;font-weight:700;cursor:pointer;flex-shrink:0;transition:color .15s,border-color .15s;font-family:inherit;line-height:1;padding:0}.field-info-toggle:hover{color:var(--accent);border-color:var(--accent)}.field-description{margin-top:.25rem;margin-bottom:.25rem}@media (max-width: 768px){.bounds-field-row{flex-direction:column;gap:.375rem}}.bounded-input{display:flex;flex-direction:column;align-items:flex-start;gap:.375rem}.bounded-input-field{display:inline-flex;align-items:stretch;border:1px solid var(--border-field);border-radius:.5rem;overflow:hidden;background:var(--bg-field);color:var(--text-field)}.stepper-btn{display:flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;background:transparent;border:none;cursor:pointer;font-size:1.1rem;font-weight:600;color:var(--text-field);font-family:inherit;flex-shrink:0;transition:background .15s}.stepper-btn:hover:not(:disabled){background:var(--placeholder-field)}.stepper-btn:disabled{opacity:.35;cursor:not-allowed}.stepper-input{width:4.5rem;text-align:center;border:none;border-left:1px solid var(--placeholder-field);border-right:1px solid var(--placeholder-field);padding:.5rem .25rem;font-size:.9rem;font-family:inherit;color:var(--text-field);background:var(--bg-field);outline:none;-moz-appearance:textfield}.stepper-input::-webkit-outer-spin-button,.stepper-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}.stepper-input:focus{box-shadow:inset 0 0 0 2px var(--field-focus-ring)}.bounded-input-unit{display:inline-flex;align-items:center;padding:0 .75rem;font-size:.8rem;color:var(--text-field);opacity:.7;white-space:nowrap;-webkit-user-select:none;user-select:none;background:transparent}.bounded-input-presets{display:flex;flex-wrap:wrap;gap:.25rem}.preset-chip{font-family:inherit;font-size:.75rem;padding:.2rem .625rem;border:1px solid var(--border);border-radius:999px;background:transparent;color:var(--text-secondary);cursor:pointer;transition:background .15s,color .15s,border-color .15s}.preset-chip:hover{background:var(--accent-subtle);color:var(--accent);border-color:var(--accent)}.preset-chip-selected{background:var(--accent);color:var(--bg-main);border-color:var(--accent)}.bounded-input-echo{font-size:.75rem;color:var(--text-secondary);font-style:italic}.field-label-with-tip{display:inline-flex;align-items:center;gap:.375rem;position:relative}.field-tip-trigger{display:inline-flex;align-items:center;justify-content:center;width:1.125rem;height:1.125rem;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);font-size:.7rem;font-style:italic;font-family:serif;font-weight:600;line-height:1;cursor:help;padding:0}.field-tip-trigger:hover,.field-tip-trigger[aria-expanded=true]{border-color:var(--accent);color:var(--accent)}.field-tip-bubble{position:absolute;top:calc(100% + .375rem);left:0;z-index:10;width:max-content;max-width:22rem;padding:.5rem .75rem;background:var(--bg-elevated);border:1px solid var(--border);border-radius:.375rem;font-size:.8rem;color:var(--text-primary);line-height:1.45;box-shadow:0 4px 12px #00000040;pointer-events:none}.tag-input{display:flex;flex-wrap:wrap;align-items:center;gap:.375rem;padding:.375rem .5rem;background:var(--bg-field);border:1px solid var(--border-field);border-radius:.5rem;cursor:text;min-height:2.5rem;transition:box-shadow .2s,border-color .2s;color:var(--text-field)}.tag-input:focus-within{box-shadow:0 0 0 3px var(--field-focus-ring)}.tag-input-invalid{border-color:var(--danger)}.tag-input-invalid:focus-within{box-shadow:0 0 0 3px #dc26262e}.tag-pill{display:inline-flex;align-items:center;gap:.25rem;font-size:.8rem;padding:.2rem .5rem;border-radius:1rem;background:var(--placeholder-field);color:var(--text-field);font-weight:500;white-space:nowrap}.tag-remove{display:inline-flex;align-items:center;justify-content:center;background:none;border:none;font-size:.9rem;line-height:1;color:var(--text-field);cursor:pointer;padding:0;margin-left:.125rem;opacity:.7;transition:opacity .15s}.tag-remove:hover{opacity:1}.tag-input-field{flex:1;min-width:8rem;border:none;outline:none;background:transparent;font-size:.875rem;font-family:inherit;color:var(--text-field);padding:.2rem 0}.tag-input-field::placeholder{color:var(--placeholder-field);opacity:1}.checkbox-group{display:flex;flex-wrap:wrap;gap:.5rem}.checkbox-pill{display:inline-flex;align-items:center;gap:.35rem;font-size:.85rem;padding:.35rem .75rem;border-radius:1rem;border:1px solid var(--border);background:var(--bg-secondary);color:var(--text-secondary);cursor:pointer;transition:all .15s;-webkit-user-select:none;user-select:none}.checkbox-pill input[type=checkbox]{display:none}.checkbox-pill:hover{border-color:var(--accent);color:var(--text-primary)}.checkbox-pill-selected{background:var(--btn-primary-bg);border-color:var(--btn-primary-bg);color:var(--btn-primary-fg);font-weight:500}.checkbox-pill-selected:hover{background:var(--btn-primary-bg-hover);border-color:var(--btn-primary-bg-hover);color:var(--btn-primary-fg)}@keyframes hap-skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.skeleton{display:block;position:relative;border-radius:.375rem;background-color:var(--bg-main);background-image:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.06) 20%,rgba(255,255,255,.16) 50%,rgba(255,255,255,.06) 80%,transparent 100%);background-size:200% 100%;background-repeat:no-repeat;animation:hap-skeleton-shimmer 1.6s ease-in-out infinite;will-change:background-position}[data-theme=light] .skeleton,:root:not([data-theme=dark]) .skeleton{background-image:linear-gradient(90deg,transparent 0%,rgba(0,0,0,.03) 20%,rgba(0,0,0,.08) 50%,rgba(0,0,0,.03) 80%,transparent 100%)}.skeleton-title{height:1.5rem;width:3rem;margin:0 auto}.skeleton-line{height:.75rem;width:100%}.skeleton-line-sm{height:.65rem;width:60%}.skeleton-card{background:var(--bg-elevated);border:1px solid var(--border);border-radius:.5rem;padding:.75rem 1rem;box-shadow:0 1px 2px #0000000a;display:flex;align-items:center;gap:.75rem}.skeleton-dot{width:.5rem;height:.5rem;border-radius:50%;flex-shrink:0}@media (prefers-reduced-motion: reduce){.skeleton{animation:none;background-image:none}}.intent-layout{display:grid;grid-template-columns:minmax(320px,1fr) minmax(420px,1.4fr);gap:1.25rem;align-items:start}.intent-pane.chat{display:flex;flex-direction:column;gap:.75rem;position:sticky;top:1rem;align-self:start;max-height:calc(100vh - 4rem)}.intent-pane.document{display:flex;flex-direction:column;gap:.875rem}.intent-pane .card-title{font-size:1.25rem;margin:0 0 1rem}.intent-textarea{width:100%;background:var(--bg-field);border:1px solid var(--border-field);border-radius:.5rem;padding:.625rem .75rem;color:var(--text-field);font-size:.9rem;line-height:1.5;resize:vertical;min-height:22rem;outline:none;font-family:inherit;transition:box-shadow .15s}.intent-textarea:focus{box-shadow:0 0 0 3px var(--field-focus-ring)}.intent-textarea::placeholder{color:var(--placeholder-field);opacity:1}.intent-footer{display:flex;align-items:center;gap:.625rem;margin-top:.5rem}.intent-continue-btn{flex:1}.intent-chat{display:flex;flex-direction:column;gap:.75rem;flex:1;min-height:0}.intent-chat .chat-header{font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--text-muted)}.intent-chat .chat-messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;padding:.25rem 0;min-height:14rem;max-height:32rem}.intent-chat .chat-msg{display:flex;max-width:100%}.intent-chat .chat-msg.assistant{justify-content:flex-start}.intent-chat .chat-msg.assistant .chat-msg-body{max-width:100%;color:var(--text-primary);font-size:.9rem;line-height:1.65;white-space:pre-wrap;word-break:break-word}.intent-chat .chat-msg.user{justify-content:flex-end}.intent-chat .chat-msg.user .chat-msg-body{max-width:85%;background:var(--bg-main);border:1px solid var(--border);border-radius:1.125rem;padding:.55rem .875rem;color:var(--text-primary);font-size:.9rem;line-height:1.55;white-space:pre-wrap;word-break:break-word}.intent-chat .chat-msg-prose{white-space:pre-wrap;word-break:break-word}.intent-chat .chat-msg-thinking{color:var(--text-muted);font-style:italic}.intent-chat .chat-fenced-block{display:block;background:var(--bg-elevated);border:1px solid var(--border);border-radius:.5rem;padding:.625rem .75rem;margin:.5rem 0;font-family:SF Mono,Monaco,Cascadia Code,monospace;font-size:.8rem;white-space:pre-wrap;color:var(--text-primary);line-height:1.55}.intent-chat .chat-apply-btn{margin-top:.5rem}.intent-chat .chat-composer{display:flex;flex-direction:column;gap:.375rem}.intent-chat .chat-composer-pill{display:flex;align-items:center;gap:.5rem;background:var(--bg-field);border:1px solid var(--border-field);border-radius:1.5rem;padding:.25rem .25rem .25rem 1rem;transition:box-shadow .15s;color:var(--text-field)}.intent-chat .chat-composer-pill:focus-within{box-shadow:0 0 0 3px var(--field-focus-ring)}.intent-chat .chat-input{flex:1;resize:none;background:transparent;border:none;padding:.5rem 0;color:var(--text-field);font-size:.9rem;line-height:1.4;outline:none;font-family:inherit;max-height:8rem;align-self:stretch}.intent-chat .chat-input::placeholder{color:var(--placeholder-field);opacity:1}.intent-chat .chat-send-btn{flex-shrink:0;width:2rem;height:2rem;border-radius:999px;background:var(--text-field);color:var(--bg-field);border:none;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:1rem;font-weight:700;line-height:1;align-self:center;transition:opacity .15s,transform .1s}.intent-chat .chat-send-btn:hover:not(:disabled){opacity:.85}.intent-chat .chat-send-btn:active:not(:disabled){transform:scale(.95)}.intent-chat .chat-send-btn:disabled{opacity:.35;cursor:not-allowed}.intent-chat .chat-composer-hint{font-size:.72rem;color:var(--text-muted);padding-left:.5rem}.hint-box{display:flex;gap:.625rem;padding:.75rem .875rem;background:var(--bg-main);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:.5rem;font-size:.85rem;color:var(--text-primary);line-height:1.55;margin-top:.25rem}.hint-icon{display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:1.125rem;height:1.125rem;border-radius:999px;border:1px solid var(--accent);color:var(--accent);font-size:.7rem;font-style:italic;font-family:serif;font-weight:700;line-height:1;margin-top:.125rem}.hint-body{flex:1;min-width:0}.hint-head{font-weight:600;margin-bottom:.25rem}@media (max-width: 1024px){.intent-layout{grid-template-columns:1fr}.intent-pane.chat{position:static;max-height:none}}@media (max-width: 768px){.intent-layout>.intent-pane.chat{display:none}.floating-help{display:inline-flex}}.floating-help{display:none;align-items:center;gap:.5rem;position:fixed;bottom:1rem;right:1rem;z-index:5;background:var(--btn-primary-bg);color:var(--btn-primary-fg);border:2px solid var(--btn-primary-bg);border-radius:999px;padding:.625rem 1.125rem;font-weight:700;font-size:.85rem;cursor:pointer;font-family:inherit;box-shadow:0 6px 18px var(--primary-glow)}.floating-help:hover{background:var(--btn-primary-bg-hover);border-color:var(--btn-primary-bg-hover)}.bottom-sheet{position:fixed;left:0;right:0;bottom:0;height:88vh;background:var(--bg-elevated);border-top:1px solid var(--border);border-radius:.75rem .75rem 0 0;z-index:11;padding:1rem 1rem 1.25rem;overflow-y:auto;display:flex;flex-direction:column;gap:.75rem;transform:translateY(100%);transition:transform .25s ease-out;box-shadow:0 -8px 32px #0009;outline:none}.bottom-sheet.is-open{transform:translateY(0)}.bottom-sheet-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#0000008c;z-index:10;opacity:0;pointer-events:none;transition:opacity .2s}.bottom-sheet-backdrop.is-open{opacity:1;pointer-events:auto}.bottom-sheet-handle{width:2.5rem;height:.25rem;background:var(--text-muted);border-radius:999px;margin:0 auto .5rem;flex-shrink:0}.bottom-sheet-close{position:absolute;top:.5rem;right:.875rem;background:transparent;border:none;color:var(--text-primary);cursor:pointer;font-size:1.5rem;line-height:1;font-family:inherit}@media (prefers-reduced-motion: reduce){.bottom-sheet,.bottom-sheet-backdrop{transition:none}}.field-editable{background:var(--bg-elevated);border:2px solid var(--edit-border);border-radius:.5rem;padding:.875rem 1rem;margin-bottom:1rem;transition:border-color .15s}.field-editable:hover,.field-editable:focus-within{border-color:var(--accent-hover)}.field-label{font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-tertiary);margin-bottom:.5rem;display:flex;align-items:center;gap:.35rem}.field-label .pen{opacity:.7;font-size:.75rem}.info-block{background:var(--bg-main);border-left:3px solid var(--border);border-radius:0 .375rem .375rem 0;padding:.75rem 1rem;margin-bottom:.75rem;font-size:.85rem;color:var(--text-secondary)}.info-head{display:flex;justify-content:space-between;align-items:baseline;margin-bottom:.4rem}.info-title{font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-tertiary)}.info-edit{font-size:.72rem;color:var(--text-tertiary);text-decoration:underline;cursor:pointer;background:none;border:none;font-family:inherit;padding:0}.info-edit:hover{color:var(--accent-hover)}.info-kv{display:grid;grid-template-columns:max-content 1fr;gap:.15rem 1rem;margin:0}.info-kv dt{color:var(--text-tertiary)}.info-kv dd{margin:0;color:var(--text-secondary);word-break:break-word}.info-section-label{margin:1.25rem 0 .5rem;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.05em;color:var(--text-tertiary)}.intent-pane.chat.tool{background:var(--bg-main);border:1px dashed var(--border)}.recent-blocks .card-header{margin-bottom:.35rem}.recent-blocks .rb-sub{color:var(--text-muted);font-size:.8rem;margin:0 0 1.1rem;line-height:1.45;max-width:36ch}.recent-blocks .rb-count{flex:none;min-width:1.6rem;height:1.6rem;padding:0 .45rem;display:inline-flex;align-items:center;justify-content:center;font-size:.8rem;font-weight:600;font-variant-numeric:tabular-nums;background:color-mix(in srgb,var(--warning) 14%,transparent);color:var(--warning);border:1px solid color-mix(in srgb,var(--warning) 35%,transparent);border-radius:999px}.recent-blocks .rb-rows{display:flex;flex-direction:column}.recent-blocks .rb-row{display:grid;grid-template-columns:auto 1fr auto;gap:.75rem .85rem;align-items:start;padding:.85rem 0;border-top:1px solid var(--border)}.recent-blocks .rb-row:first-child{border-top:none}.recent-blocks .rb-dot{width:.5rem;height:.5rem;border-radius:999px;margin-top:.35rem;background:var(--warning)}.recent-blocks .rb-dot.act{background:var(--danger)}.recent-blocks .rb-dot.info{background:var(--text-muted)}.recent-blocks .rb-body{min-width:0}.recent-blocks .rb-line{font-size:.86rem;line-height:1.4;color:var(--text-primary)}.recent-blocks .rb-line b{font-weight:600}.recent-blocks .rb-meta{display:flex;align-items:center;gap:.5rem;margin-top:.3rem;flex-wrap:wrap}.recent-blocks .rb-chip{font-size:.64rem;letter-spacing:.06em;text-transform:uppercase;font-weight:600;padding:.12rem .44rem;border-radius:.35rem;background:var(--accent-subtle);color:var(--text-muted)}.recent-blocks .rb-chip.warn{background:color-mix(in srgb,var(--warning) 14%,transparent);color:var(--warning)}.recent-blocks .rb-chip.act{background:var(--danger-subtle);color:var(--danger)}.recent-blocks .rb-via{font-size:.72rem;color:var(--text-muted)}.recent-blocks .rb-when{font-size:.74rem;color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap;text-align:right}.recent-blocks .rb-fix{display:inline-block;margin-top:.4rem;font-size:.76rem;color:var(--text-primary);text-underline-offset:2px}.recent-blocks .rb-foot{margin-top:1rem;padding-top:.9rem;border-top:1px solid var(--border)}.recent-blocks .rb-viewall{font:inherit;font-size:.8rem;color:var(--text-primary);background:none;border:none;padding:0;cursor:pointer;text-decoration:underline;text-underline-offset:3px}.recent-blocks .rb-empty{text-align:center;padding:1.25rem 1rem .25rem}.recent-blocks .rb-mark{width:2.25rem;height:2.25rem;border-radius:999px;background:var(--success-subtle);color:var(--success);display:inline-flex;align-items:center;justify-content:center;margin-bottom:.75rem}.recent-blocks .rb-empty h3{font-size:.9rem;margin:0 0 .3rem}.recent-blocks .rb-empty p{font-size:.8rem;color:var(--text-muted);margin:0 auto;max-width:30ch;line-height:1.45}.receipt-head{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.receipt-what{font-size:.95rem;font-weight:600;letter-spacing:-.01em;color:var(--text-primary)}.receipt-tag{font-size:.68rem;padding:.12rem .4rem;border-radius:4px;border:1px solid var(--border);color:var(--text-muted);white-space:nowrap}.receipt-tag-bound{color:var(--success);border-color:var(--success)}.receipt-scope{font-size:.82rem;color:var(--text-muted);margin-top:.2rem}.receipt-foot{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-top:.55rem;padding-top:.55rem;border-top:1px solid var(--border)}.receipt-link{font-size:.78rem;color:var(--text-secondary);text-decoration:none;border-bottom:1px solid var(--border)}.receipt-link:hover{color:var(--accent-hover);border-color:var(--accent-hover)}.receipt-id{font-family:SF Mono,Monaco,monospace;font-size:.7rem;color:var(--text-muted)}.receipt-more{margin-top:.5rem}.receipt-more summary{cursor:pointer;font-size:.75rem;color:var(--text-muted);list-style:none}.receipt-more summary::-webkit-details-marker{display:none}.receipt-more summary:before{content:"▸"}.receipt-more[open] summary:before{content:"▾"}.receipt-kv{font-size:.72rem;color:var(--text-muted);margin-top:.4rem;line-height:1.7;font-family:SF Mono,Monaco,monospace;word-break:break-all}