friend-assistant 0.1.4 → 0.1.6

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.
package/README.md CHANGED
@@ -127,10 +127,11 @@ Connectors (Gmail, Chrome control, any MCP service) are managed inside the app,
127
127
 
128
128
  | Key | What it is |
129
129
  |---|---|
130
- | `provider` | Which model answers: `claude` (API key), `claude-code` (your subscription) or `ollama` (this PC). |
130
+ | `provider` | Which model answers: `claude` (API key), `claude-code` (your subscription), `ollama` (this PC) or `openai` (any OpenAI-compatible server, such as OmniRoute). |
131
131
  | `apiKey`, `model` | The Anthropic API key and the Claude model used with it. |
132
132
  | `claudeCode.model`, `claudeCode.command`, `claudeCode.fallback` | The subscription route: model (`sonnet`, `haiku`, `opus`), the command that starts Claude Code, and `ollama` or `none` for what to use when Claude is unavailable. |
133
133
  | `ollama.url`, `ollama.model`, `ollama.vision`, `ollama.contextTokens` | The local model: server address, model name, whether it can see pictures, and how much it remembers at once. |
134
+ | `openai.url`, `openai.model`, `openai.apiKey`, `openai.vision` | An OpenAI-compatible server: its address (ending in `/v1`), the model name it knows, its API key (or empty), and whether the model can see pictures. For OmniRoute (https://github.com/diegosouzapw/OmniRoute) run `npm install -g omniroute` and `omniroute`, then `friend config set provider openai` (the defaults are OmniRoute's: `http://localhost:20128/v1` and model `auto`). Also works with LM Studio, LiteLLM, vLLM and OpenRouter. |
134
135
  | `voice.enabled`, `voice.sttModel`, `voice.ttsVoice`, `voice.language`, `voice.speakTyped`, `voice.accept`, `voice.challenge` | Speech on or off, the listening model, the speaking voice, a forced language, speaking typed replies, and the two thresholds for recognising your voice. |
135
136
  | `bind`, `port` | Where Friend listens (easier: `friend lan`). |
136
137
  | `https` | `true` serves a secure `https://` address with a certificate Friend makes for itself, so a phone may use the microphone (easier: `friend lan https on`). |
package/dist/BUILD.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "friend-assistant",
3
- "version": "0.1.4",
4
- "builtAt": "2026-10-09T05:21:33.354Z"
3
+ "version": "0.1.6",
4
+ "builtAt": "2026-10-09T11:59:13.273Z"
5
5
  }
package/dist/friend.mjs CHANGED
@@ -3263,6 +3263,7 @@ import { join as join12 } from "node:path";
3263
3263
  import { chmodSync as chmodSync3, existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "node:fs";
3264
3264
  import { homedir as homedir2 } from "node:os";
3265
3265
  import { join as join8 } from "node:path";
3266
+ var DEFAULT_OPENAI = { url: "http://localhost:20128/v1", model: "auto", apiKey: null, vision: false };
3266
3267
  var DEFAULT_CLAUDE_CODE = { model: "sonnet", command: "claude", fallback: "ollama" };
3267
3268
  function sanitizeClaudeCode(raw) {
3268
3269
  const r = raw && typeof raw === "object" ? raw : {};
@@ -3351,6 +3352,7 @@ var defaults = (dataDir) => ({
3351
3352
  apiKey: null,
3352
3353
  provider: "claude",
3353
3354
  ollama: { ...DEFAULT_OLLAMA },
3355
+ openai: { ...DEFAULT_OPENAI },
3354
3356
  claudeCode: { ...DEFAULT_CLAUDE_CODE },
3355
3357
  voice: { ...DEFAULT_VOICE },
3356
3358
  view: "companion",
@@ -3386,6 +3388,7 @@ function loadConfig(dataDir = defaultDataDir()) {
3386
3388
  https: stored.https === true,
3387
3389
  destructivePatterns,
3388
3390
  ollama: { ...DEFAULT_OLLAMA, ...stored.ollama },
3391
+ openai: { ...DEFAULT_OPENAI, ...stored.openai },
3389
3392
  claudeCode: sanitizeClaudeCode(stored.claudeCode),
3390
3393
  voice: sanitizeVoice({ ...DEFAULT_VOICE, ...stored.voice }),
3391
3394
  view: stored.view === "minimal" || stored.view === "workspace" ? stored.view : "companion",
@@ -4991,7 +4994,7 @@ import { z as z11 } from "zod";
4991
4994
  var word = /^[\w.:/\\-]+$/;
4992
4995
  var modelName = z11.string().trim().min(1).max(100).regex(/^[A-Za-z0-9][\w.:/-]*$/, "A model name has no spaces");
4993
4996
  var SettingsPatch = z11.object({
4994
- provider: z11.enum(["claude", "claude-code", "ollama"]),
4997
+ provider: z11.enum(["claude", "claude-code", "ollama", "openai"]),
4995
4998
  model: modelName,
4996
4999
  /** Empty means "leave the saved key alone". */
4997
5000
  apiKey: z11.string().trim().max(400),
@@ -5013,6 +5016,13 @@ var SettingsPatch = z11.object({
5013
5016
  vision: z11.boolean(),
5014
5017
  contextTokens: z11.number().int().min(1024).max(131072)
5015
5018
  }).partial().strict(),
5019
+ openai: z11.object({
5020
+ url: z11.string().trim().max(300),
5021
+ model: modelName,
5022
+ /** Empty means "leave the saved key alone". */
5023
+ apiKey: z11.string().trim().max(400),
5024
+ vision: z11.boolean()
5025
+ }).partial().strict(),
5016
5026
  claudeCode: z11.object({
5017
5027
  model: modelName,
5018
5028
  command: z11.string().trim().min(1).max(260).regex(word, "The command is one word or path without spaces"),
@@ -5036,6 +5046,7 @@ function settingsView(c) {
5036
5046
  view: c.view,
5037
5047
  look: { ...c.look },
5038
5048
  ollama: { url: c.ollama.url, model: c.ollama.model, vision: !!c.ollama.vision, contextTokens: c.ollama.contextTokens },
5049
+ openai: { url: c.openai.url, model: c.openai.model, vision: !!c.openai.vision, apiKeySet: !!c.openai.apiKey },
5039
5050
  claudeCode: { ...c.claudeCode },
5040
5051
  voice: {
5041
5052
  enabled: c.voice.enabled,
@@ -5053,10 +5064,11 @@ var SettingsError = class extends Error {
5053
5064
  function refuseForAgent(raw) {
5054
5065
  const r = raw && typeof raw === "object" ? raw : {};
5055
5066
  const cc = r.claudeCode && typeof r.claudeCode === "object" ? r.claudeCode : {};
5056
- if ("apiKey" in r) throw new SettingsError("The API key can only be changed by the owner on the settings screen.");
5067
+ const oa = r.openai && typeof r.openai === "object" ? r.openai : {};
5068
+ if ("apiKey" in r || "apiKey" in oa) throw new SettingsError("The API key can only be changed by the owner on the settings screen.");
5057
5069
  if ("command" in cc) throw new SettingsError("The Claude Code command can only be changed by the owner on the settings screen.");
5058
5070
  }
5059
- var PROVIDER_KEYS = /* @__PURE__ */ new Set(["provider", "model", "apiKey", "ollama", "claudeCode"]);
5071
+ var PROVIDER_KEYS = /* @__PURE__ */ new Set(["provider", "model", "apiKey", "ollama", "openai", "claudeCode"]);
5060
5072
  var RESTART_VOICE = ["enabled", "sttModel"];
5061
5073
  function applySettings(config, raw) {
5062
5074
  const parsed = SettingsPatch.safeParse(raw);
@@ -5076,6 +5088,17 @@ function applySettings(config, raw) {
5076
5088
  if (u.protocol !== "http:" && u.protocol !== "https:") throw new SettingsError("ollama.url: The URL must start with http:// or https://");
5077
5089
  p.ollama.url = u.origin;
5078
5090
  }
5091
+ const openaiUrl = p.openai?.url;
5092
+ if (openaiUrl !== void 0) {
5093
+ let u;
5094
+ try {
5095
+ u = new URL(openaiUrl);
5096
+ } catch {
5097
+ throw new SettingsError("openai.url: Expected a URL such as http://localhost:20128/v1");
5098
+ }
5099
+ if (u.protocol !== "http:" && u.protocol !== "https:") throw new SettingsError("openai.url: The URL must start with http:// or https://");
5100
+ p.openai.url = (u.origin + u.pathname).replace(/\/+$/, "");
5101
+ }
5079
5102
  const next = {
5080
5103
  provider: p.provider ?? config.provider,
5081
5104
  apiKey: p.apiKey ? p.apiKey : config.apiKey,
@@ -5098,6 +5121,11 @@ function applySettings(config, raw) {
5098
5121
  if (p.model) config.model = p.model;
5099
5122
  if (p.apiKey) config.apiKey = p.apiKey;
5100
5123
  if (p.ollama) Object.assign(config.ollama, p.ollama);
5124
+ if (p.openai) {
5125
+ const { apiKey, ...rest } = p.openai;
5126
+ Object.assign(config.openai, rest);
5127
+ if (apiKey) config.openai.apiKey = apiKey;
5128
+ }
5101
5129
  if (p.claudeCode) Object.assign(config.claudeCode, p.claudeCode);
5102
5130
  if (p.voice) {
5103
5131
  const safe = next.voice;
@@ -5282,6 +5310,173 @@ var OllamaProvider = class {
5282
5310
  }
5283
5311
  };
5284
5312
 
5313
+ // ../server/src/provider/openai.ts
5314
+ import { Agent as Agent3, fetch as undiciFetch2 } from "undici";
5315
+ var encodeName3 = (n) => n.replaceAll(".", "__");
5316
+ var decodeName3 = (n) => n.replaceAll("__", ".");
5317
+ function toOpenAiMessages(system, messages, vision) {
5318
+ const out = [{ role: "system", content: system }];
5319
+ for (const m of messages) {
5320
+ if (m.role === "user") {
5321
+ out.push({ role: "user", content: m.content.map((b) => b.type === "text" ? b.text : "").join("\n") });
5322
+ } else if (m.role === "assistant") {
5323
+ const text2 = m.content.map((b) => b.type === "text" ? b.text : "").join("");
5324
+ const calls = m.content.filter((b) => b.type === "tool_use");
5325
+ out.push({
5326
+ role: "assistant",
5327
+ content: text2 || null,
5328
+ ...calls.length ? { tool_calls: calls.map((c) => ({ id: c.id, type: "function", function: { name: encodeName3(c.name), arguments: JSON.stringify(c.input ?? {}) } })) } : {}
5329
+ });
5330
+ } else {
5331
+ for (const b of m.content) if (b.type === "tool_result") out.push({ role: "tool", tool_call_id: b.toolUseId, content: b.content });
5332
+ const pictures = vision ? m.content.flatMap((b) => b.type === "image" ? [`data:${b.mime};base64,${b.data}`] : []) : [];
5333
+ if (pictures.length) out.push({ role: "user", content: [{ type: "text", text: "Here is the picture from the camera." }, ...pictures.map((url) => ({ type: "image_url", image_url: { url } }))] });
5334
+ }
5335
+ }
5336
+ return out;
5337
+ }
5338
+ function parseArgs2(raw) {
5339
+ if (!raw.trim()) return {};
5340
+ try {
5341
+ return JSON.parse(raw);
5342
+ } catch {
5343
+ return {};
5344
+ }
5345
+ }
5346
+ var OpenAiProvider = class {
5347
+ constructor(opts) {
5348
+ this.opts = opts;
5349
+ const limit = opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : 0;
5350
+ this.agent = new Agent3({ headersTimeout: limit, bodyTimeout: limit });
5351
+ }
5352
+ opts;
5353
+ id = "openai";
5354
+ agent;
5355
+ get vision() {
5356
+ return this.opts.vision === true;
5357
+ }
5358
+ async *stream(req) {
5359
+ const base = this.opts.url.replace(/\/+$/, "");
5360
+ const body = {
5361
+ model: this.opts.model,
5362
+ stream: true,
5363
+ stream_options: { include_usage: true },
5364
+ messages: [
5365
+ ...toOpenAiMessages(req.system, req.messages, this.vision),
5366
+ // last of all, so every message before it stays identical between requests and a gateway can reuse its cache of them
5367
+ ...req.context ? [{ role: "user", content: req.context }] : []
5368
+ ],
5369
+ ...req.tools.length ? { tools: req.tools.map((t) => ({ type: "function", function: { name: encodeName3(t.name), description: t.description, parameters: t.inputSchema } })) } : {}
5370
+ };
5371
+ let res;
5372
+ try {
5373
+ res = await undiciFetch2(`${base}/chat/completions`, {
5374
+ method: "POST",
5375
+ headers: { "content-type": "application/json", ...this.opts.apiKey ? { authorization: `Bearer ${this.opts.apiKey}` } : {} },
5376
+ body: JSON.stringify(body),
5377
+ signal: req.signal,
5378
+ dispatcher: this.agent
5379
+ });
5380
+ } catch (e) {
5381
+ if (req.signal.aborted) return;
5382
+ const code = e.cause?.code ?? "";
5383
+ if (code === "UND_ERR_HEADERS_TIMEOUT" || code === "UND_ERR_BODY_TIMEOUT") {
5384
+ yield { type: "error", userFacing: true, message: "The model took too long to answer. Try again, or choose a faster model." };
5385
+ return;
5386
+ }
5387
+ yield { type: "error", userFacing: true, message: `I can't reach the model server at ${base}. Is it running? Check the address in Settings.` };
5388
+ return;
5389
+ }
5390
+ if (!res.ok) {
5391
+ let detail = "";
5392
+ try {
5393
+ const j = await res.json();
5394
+ detail = typeof j.error === "string" ? j.error : j.error?.message ?? "";
5395
+ } catch {
5396
+ }
5397
+ if (res.status === 401 || res.status === 403) yield { type: "error", userFacing: true, message: "The model server refused the API key. Check it in Settings." };
5398
+ else if (res.status === 404) yield { type: "error", userFacing: true, message: `The model server has no "${this.opts.model}", or the address is wrong (it should end in /v1). ${detail}`.trim() };
5399
+ else if (res.status === 429) yield { type: "error", userFacing: true, message: "The model server says too many requests or the limit is used up. Try again in a moment." };
5400
+ else yield { type: "error", message: `Model server error (${res.status})${detail ? `: ${detail}` : ""}` };
5401
+ return;
5402
+ }
5403
+ const reader = res.body.getReader();
5404
+ const decoder = new TextDecoder();
5405
+ let buffer = "";
5406
+ let finished = false;
5407
+ const calls = /* @__PURE__ */ new Map();
5408
+ let usage = null;
5409
+ const flush = function* () {
5410
+ for (const [, c] of [...calls.entries()].sort((a, b) => a[0] - b[0])) {
5411
+ if (c.name) yield { type: "tool_call", id: c.id || `openai-${Date.now().toString(36)}-${c.name}`, name: decodeName3(c.name), args: parseArgs2(c.args) };
5412
+ }
5413
+ calls.clear();
5414
+ };
5415
+ const handle = function* (line) {
5416
+ const text2 = line.trim();
5417
+ if (!text2.startsWith("data:")) return;
5418
+ const data = text2.slice(5).trim();
5419
+ if (data === "[DONE]") {
5420
+ yield* flush();
5421
+ if (usage) yield { type: "usage", ...usage };
5422
+ yield { type: "done" };
5423
+ finished = true;
5424
+ return;
5425
+ }
5426
+ let obj;
5427
+ try {
5428
+ obj = JSON.parse(data);
5429
+ } catch {
5430
+ return;
5431
+ }
5432
+ if (obj.error) {
5433
+ yield { type: "error", message: `Model server error: ${typeof obj.error === "string" ? obj.error : obj.error.message ?? "unknown"}` };
5434
+ finished = true;
5435
+ return;
5436
+ }
5437
+ if (obj.usage) usage = { input: obj.usage.prompt_tokens ?? 0, output: obj.usage.completion_tokens ?? 0 };
5438
+ for (const choice of obj.choices ?? []) {
5439
+ const delta = choice.delta ?? {};
5440
+ if (typeof delta.content === "string" && delta.content) yield { type: "text_delta", text: delta.content };
5441
+ for (const tc of delta.tool_calls ?? []) {
5442
+ const i = typeof tc.index === "number" ? tc.index : 0;
5443
+ const cur = calls.get(i) ?? { id: "", name: "", args: "" };
5444
+ if (tc.id) cur.id = tc.id;
5445
+ if (tc.function?.name) cur.name += tc.function.name;
5446
+ if (typeof tc.function?.arguments === "string") cur.args += tc.function.arguments;
5447
+ calls.set(i, cur);
5448
+ }
5449
+ }
5450
+ };
5451
+ try {
5452
+ while (!finished) {
5453
+ const { value, done } = await reader.read();
5454
+ if (done) break;
5455
+ buffer += decoder.decode(value, { stream: true });
5456
+ let nl;
5457
+ while (!finished && (nl = buffer.indexOf("\n")) >= 0) {
5458
+ const line = buffer.slice(0, nl);
5459
+ buffer = buffer.slice(nl + 1);
5460
+ yield* handle(line);
5461
+ }
5462
+ }
5463
+ if (!finished && buffer.trim()) yield* handle(buffer);
5464
+ if (!finished) {
5465
+ yield* flush();
5466
+ const u = usage;
5467
+ if (u) yield { type: "usage", input: u.input, output: u.output };
5468
+ yield { type: "done" };
5469
+ }
5470
+ } catch (e) {
5471
+ if (req.signal.aborted) return;
5472
+ yield { type: "error", message: `Lost the connection to the model server: ${e.message}` };
5473
+ } finally {
5474
+ reader.cancel().catch(() => {
5475
+ });
5476
+ }
5477
+ }
5478
+ };
5479
+
5285
5480
  // ../server/src/tls.ts
5286
5481
  import { chmodSync as chmodSync5, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "node:fs";
5287
5482
  import { networkInterfaces } from "node:os";
@@ -5826,6 +6021,7 @@ async function createApp(opts) {
5826
6021
  return config.claudeCode.fallback === "ollama" ? new FallbackProvider(cc, new OllamaProvider({ ...config.ollama }), (reason) => tellFallback(reason)) : cc;
5827
6022
  }
5828
6023
  if (config.provider === "ollama") return new OllamaProvider({ ...config.ollama });
6024
+ if (config.provider === "openai") return new OpenAiProvider({ ...config.openai });
5829
6025
  return config.apiKey ? new ClaudeProvider({ apiKey: config.apiKey, model: config.model }) : new MissingKeyProvider();
5830
6026
  };
5831
6027
  const switchable = opts.provider ? null : new SwitchableProvider(makeProvider());
@@ -6529,7 +6725,7 @@ var PARSERS = {
6529
6725
  return n;
6530
6726
  },
6531
6727
  provider: (v) => {
6532
- if (v !== "claude" && v !== "ollama" && v !== "claude-code") throw new Error("Provider must be claude, ollama or claude-code");
6728
+ if (v !== "claude" && v !== "ollama" && v !== "claude-code" && v !== "openai") throw new Error("Provider must be claude, ollama, claude-code or openai");
6533
6729
  return v;
6534
6730
  },
6535
6731
  roots: list,
@@ -6558,6 +6754,24 @@ var GROUPS = {
6558
6754
  return v;
6559
6755
  }
6560
6756
  },
6757
+ openai: {
6758
+ url: (v) => {
6759
+ let u;
6760
+ try {
6761
+ u = new URL(v.trim());
6762
+ } catch {
6763
+ throw new Error("Expected a URL that ends in /v1, such as http://localhost:20128/v1");
6764
+ }
6765
+ if (u.protocol !== "http:" && u.protocol !== "https:") throw new Error("The URL must start with http:// or https://");
6766
+ return (u.origin + u.pathname).replace(/\/+$/, "");
6767
+ },
6768
+ model: (v) => {
6769
+ if (!/^[A-Za-z0-9][\w.:/-]*$/.test(v.trim())) throw new Error("Expected a model name without spaces, such as auto");
6770
+ return v.trim();
6771
+ },
6772
+ apiKey: (v) => v.trim() === "" ? null : v.trim(),
6773
+ vision: bool
6774
+ },
6561
6775
  ollama: {
6562
6776
  url: httpUrl,
6563
6777
  model: nonEmpty,
@@ -6572,7 +6786,7 @@ var GROUPS = {
6572
6786
  var KEY_HELP = {
6573
6787
  apiKey: 'Anthropic API key, used when the model is "claude"',
6574
6788
  model: "Claude model name, used with the API key",
6575
- provider: "which model answers: claude (API key), claude-code (your Claude subscription) or ollama (on this PC)",
6789
+ provider: "which model answers: claude (API key), claude-code (your Claude subscription), ollama (on this PC) or openai (any OpenAI-compatible server, such as OmniRoute)",
6576
6790
  bind: "address Friend listens on: 127.0.0.1 is this PC only, 0.0.0.0 is your whole network (easier: friend lan on)",
6577
6791
  port: "the web port (easier: friend lan port N)",
6578
6792
  https: "true serves a secure https:// address (Friend makes its own certificate), so a phone may use the microphone (easier: friend lan https on)",
@@ -6592,6 +6806,10 @@ var KEY_HELP = {
6592
6806
  "ollama.model": "the local model, for example qwen2.5:7b",
6593
6807
  "ollama.vision": "true or false: the local model can look at pictures",
6594
6808
  "ollama.contextTokens": "how much the local model remembers at once (1024 to 131072)",
6809
+ "openai.url": "address of an OpenAI-compatible server, ending in /v1; OmniRoute is http://localhost:20128/v1",
6810
+ "openai.model": "the model name that server knows; OmniRoute answers to auto",
6811
+ "openai.apiKey": "the server's API key, or empty when it asks for none (stored in your private config file)",
6812
+ "openai.vision": "true or false: that model can look at pictures",
6595
6813
  "claudeCode.model": "model for your Claude subscription: sonnet, haiku, opus or a full name",
6596
6814
  "claudeCode.command": "the command that starts Claude Code, if it is not on the PATH",
6597
6815
  "claudeCode.fallback": "ollama or none: what to use when Claude is unavailable"
@@ -8348,7 +8566,7 @@ async function model(c) {
8348
8566
  io2.out("No model is ready yet. Finish one of: `friend claude setup --use`, `friend ollama setup --use`, or `friend config set apiKey <key>`.");
8349
8567
  return "later";
8350
8568
  }
8351
- io2.out(`Friend will think with: ${cfg.provider === "claude-code" ? `Claude (${cfg.claudeCode.model}) through your subscription${cfg.claudeCode.fallback === "ollama" ? ", with the local model as a safety net" : ""}` : cfg.provider === "claude" ? "Claude with your API key" : `${cfg.ollama.model} on this PC`}.`);
8569
+ io2.out(`Friend will think with: ${cfg.provider === "claude-code" ? `Claude (${cfg.claudeCode.model}) through your subscription${cfg.claudeCode.fallback === "ollama" ? ", with the local model as a safety net" : ""}` : cfg.provider === "claude" ? "Claude with your API key" : cfg.provider === "openai" ? `${cfg.openai.model} through ${cfg.openai.url}` : `${cfg.ollama.model} on this PC`}.`);
8352
8570
  return "done";
8353
8571
  }
8354
8572
  async function voice(c) {
@@ -8463,7 +8681,7 @@ function summary(cfg, c) {
8463
8681
  }
8464
8682
  return [
8465
8683
  ` Assistant: ${owner}`,
8466
- ` Thinks with: ${cfg.provider === "claude-code" ? `Claude (${cfg.claudeCode.model}), your subscription` : cfg.provider === "claude" ? cfg.apiKey ? "Claude, API key" : "Claude, API key missing" : `${cfg.ollama.model} on this PC`}`,
8684
+ ` Thinks with: ${cfg.provider === "claude-code" ? `Claude (${cfg.claudeCode.model}), your subscription` : cfg.provider === "claude" ? cfg.apiKey ? "Claude, API key" : "Claude, API key missing" : cfg.provider === "openai" ? `${cfg.openai.model} through ${cfg.openai.url}` : `${cfg.ollama.model} on this PC`}`,
8467
8685
  ` Voice: ${has(c, "voice") ? voiceLabel(cfg.voice.ttsVoice) : "off"}`,
8468
8686
  ` Workspace: ${cfg.roots[0] ?? "none (every file action asks)"}`,
8469
8687
  ` Address: ${u.local}${u.network.length ? ` and ${u.network.join(", ")}` : ""}`,
@@ -8916,7 +9134,7 @@ async function run(argv, io2) {
8916
9134
  case "version":
8917
9135
  case "--version":
8918
9136
  case "-v":
8919
- io2.out(true ? "0.1.4" : "development");
9137
+ io2.out(true ? "0.1.6" : "development");
8920
9138
  return 0;
8921
9139
  case "lan":
8922
9140
  return await lanCommand(rest, io2);
@@ -1 +1 @@
1
- :root{--accent:#3fd8ff;--warn:#ffb454;--bad:#ff6b7a;--ok:#5ef0b0;--ink:#d4ecf7;--muted:#7fa3b6;--glass:#06141c9e;--edge:color-mix(in srgb, var(--accent) 48%, transparent);--ui:"Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;--mono:"Cascadia Mono", "SF Mono", Consolas, ui-monospace, monospace;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{color:var(--ink);font:16px/1.5 var(--ui);-webkit-tap-highlight-color:transparent;background:#000;overflow:hidden}button,input{font:inherit;color:inherit}button{cursor:pointer}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.stage{touch-action:manipulation;background:#000;position:fixed;inset:0;overflow:hidden}.ambient{pointer-events:none;background:radial-gradient(ellipse at 50% 55%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 62%), linear-gradient(#3fd8ff0b 1px, transparent 1px) 0 0 / 64px 64px, linear-gradient(90deg, #3fd8ff0b 1px, transparent 1px) 0 0 / 64px 64px;position:absolute;inset:0;-webkit-mask-image:radial-gradient(#000 30%,#0000 78%);mask-image:radial-gradient(#000 30%,#0000 78%)}.presence{z-index:1;transition:inset .7s cubic-bezier(.2,.7,.2,1),width .7s,height .7s,opacity .5s;position:absolute}.presence-full{inset:0}.presence-corner{left:max(10px, env(safe-area-inset-left));bottom:max(52px, calc(env(safe-area-inset-bottom) + 42px));width:clamp(96px,16vw,190px);height:clamp(96px,16vw,190px)}.objects{pointer-events:none;position:absolute;inset:0}.objects-stack{pointer-events:auto;padding:max(56px, env(safe-area-inset-top)) 14px 140px;flex-direction:column;gap:12px;display:flex;overflow-y:auto}.auto-grid{pointer-events:none;padding:max(24px, env(safe-area-inset-top)) 24px 24px clamp(120px, 20vw, 230px);grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));align-content:safe center;align-items:start;gap:12px;display:grid;position:absolute;inset:0;overflow-y:auto}.auto-grid>.obj{pointer-events:auto}.objects-stack>.auto-grid{flex-direction:column;padding:0;display:flex;position:static;overflow:visible}.obj.leaving{pointer-events:none;animation:.35s forwards leave}@keyframes leave{to{opacity:0;transform:scale(.96)translateY(-6px)}}@media (prefers-reduced-motion:reduce){.obj.leaving{animation:none}}.glass{background:var(--glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--edge);box-shadow:0 0 22px color-mix(in srgb, var(--accent) 16%, transparent), inset 0 0 28px #3fd8ff0d;clip-path:polygon(0 0,calc(100% - 14px) 0,100% 14px,100% 100%,14px 100%,0 calc(100% - 14px))}.obj{pointer-events:auto;min-width:0;padding:16px 18px;animation:.45s both appear;overflow:auto}.obj.alert{border-color:var(--warn)}@keyframes appear{0%{opacity:0;transform:translateY(8px)scale(.985)}}.obj-close{width:44px;height:44px;color:var(--muted);background:0 0;border:0;font-size:1.5rem;line-height:1;position:absolute;top:2px;right:6px}.obj-close:hover{color:var(--accent)}.obj-title{letter-spacing:.16em;text-transform:uppercase;color:var(--accent);margin:0 36px 8px 0;font-size:.78rem;font-weight:600}.obj-md>:first-child{margin-top:0}.obj-md>:last-child{margin-bottom:0}.obj-md a{color:var(--accent)}.obj-md code{font-family:var(--mono);background:#3fd8ff1a;padding:0 4px;font-size:.9em}.obj-stat{gap:2px;display:grid}.obj-label{letter-spacing:.16em;text-transform:uppercase;color:var(--muted);font-size:.78rem}.obj-value{font-variant-numeric:tabular-nums;font-size:clamp(2rem,6vw,3.6rem);font-weight:300;line-height:1.05}.obj-value small{color:var(--muted);margin-left:4px;font-size:.4em}.obj-delta{color:var(--ok);font-size:.9rem}.obj-list{gap:4px;margin:0;padding-left:1.1rem;display:grid}.obj-list .done{color:var(--muted);text-decoration:line-through}.obj-table-wrap{overflow-x:auto}.obj-table{border-collapse:collapse;width:100%;font-size:.92rem}.obj-table th{text-align:left;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);font-size:.72rem;font-weight:600}.obj-table th,.obj-table td{font-variant-numeric:tabular-nums;border-bottom:1px solid #3fd8ff2e;padding:6px 10px}.obj-code{font:.86rem/1.5 var(--mono);margin:0;overflow:auto}.obj-empty{color:var(--muted);margin:0}.obj-chart{width:100%;height:auto;display:block}.obj-chart .grid{stroke:#3fd8ff24;stroke-width:1px}.obj-chart .tick{fill:var(--muted);font:10px var(--mono);font-variant-numeric:tabular-nums}.legend{color:var(--muted);flex-wrap:wrap;gap:6px 14px;margin:6px 0 0;padding:0;font-size:.8rem;list-style:none;display:flex}.legend i{width:10px;height:10px;margin-right:6px;display:inline-block}.obj-gauge{justify-items:center;display:grid}.obj-gauge svg{width:min(100%,260px);height:auto}.obj-gauge .track{stroke:#3fd8ff26}.obj-gauge .value{stroke:var(--accent);filter:drop-shadow(0 0 6px var(--accent))}.obj-gauge.gauge-warn .value{stroke:var(--warn);filter:drop-shadow(0 0 6px var(--warn))}.obj-gauge.gauge-crit .value{stroke:var(--bad);filter:drop-shadow(0 0 6px var(--bad))}.gauge-read{align-items:baseline;gap:4px;margin-top:-62px;display:flex}.gauge-value{font-variant-numeric:tabular-nums;font-size:clamp(1.8rem,5vw,2.8rem);font-weight:300}.gauge-unit{color:var(--muted)}.obj-gauge .obj-label{margin-top:18px}.obj-timeline{border-left:1px solid var(--edge);gap:14px;margin:8px 0 0;padding:0 0 0 20px;list-style:none;display:grid}.obj-timeline li{gap:2px;display:grid;position:relative}.obj-timeline li:before{content:"";background:var(--accent);width:9px;height:9px;box-shadow:0 0 8px var(--accent);border-radius:50%;position:absolute;top:.5em;left:-25px}.obj-timeline time{font:.75rem/1.3 var(--mono);color:var(--accent)}.obj-timeline span{color:var(--muted);font-size:.9rem}.obj-graph{width:100%;height:auto;max-height:70vh;margin:0 auto;display:block}.obj[style*=" height:"] .obj-graph{max-height:calc(100% - 32px)}.obj-graph .edge{stroke:color-mix(in srgb, var(--accent) 55%, transparent);stroke-width:1.4px}.obj-graph .arrow{fill:var(--accent)}.obj-graph .node{fill:#3fd8ff26;stroke:var(--accent);stroke-width:1.5px}.obj-graph .node-label{fill:var(--ink);font:10px var(--ui)}.obj-graph .edge-label{fill:var(--muted);font:9px var(--ui)}.captions{z-index:20;left:50%;bottom:max(7%, env(safe-area-inset-bottom));text-align:center;pointer-events:none;text-shadow:0 0 18px #000,0 0 4px #000;text-wrap:balance;width:min(780px,100% - 32px);margin:0;font-size:clamp(1.1rem,2.6vw,1.6rem);font-weight:300;position:absolute;transform:translate(-50%)}.captions.done{animation:1.4s 8s forwards fadeOut}@keyframes fadeOut{to{opacity:0}}.command-bar{z-index:30;left:50%;bottom:max(14px, env(safe-area-inset-bottom));background:var(--glass);border:1px solid var(--edge);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);gap:8px;width:min(640px,100% - 24px);padding:8px;display:flex;position:absolute;transform:translate(-50%)}.command-bar input{background:0 0;border:0;outline:none;flex:1;min-height:44px;padding:0 10px;font-size:1.05rem}.command-bar button{border:1px solid var(--edge);background:0 0;min-width:44px;min-height:44px}.conn-badge{top:max(10px, env(safe-area-inset-top));font:500 .78rem var(--mono);letter-spacing:.08em;color:var(--warn);border:1px solid var(--warn);background:#0009;padding:6px 12px;position:absolute;right:12px}.mic-indicator{left:50%;top:max(12px, env(safe-area-inset-top));font:500 .82rem var(--mono);letter-spacing:.1em;text-transform:uppercase;color:#ffd9a0;border:1px solid var(--warn);background:#000000c7;align-items:center;gap:10px;padding:8px 16px;display:flex;position:absolute;transform:translate(-50%)}.mic-indicator i{background:var(--bad);width:10px;height:10px;box-shadow:0 0 10px var(--bad);border-radius:50%;animation:1s ease-in-out infinite mic-pulse}.mic-transcribing i{background:var(--warn);box-shadow:0 0 10px var(--warn)}@keyframes mic-pulse{50%{opacity:.35}}@media (prefers-reduced-motion:reduce){.mic-indicator i{animation:none}}.toasts{top:max(48px, env(safe-area-inset-top));gap:6px;width:min(420px,100% - 24px);display:grid;position:absolute;left:50%;transform:translate(-50%)}.toast{border:1px solid var(--edge);background:#000000d1;padding:10px 14px;font-size:.92rem}.toast-warn{border-color:var(--warn);color:#ffd9a0}.toast-error{border-color:var(--bad);color:#ffc2c9}.approval{border-color:var(--warn);background:#020c12eb;width:min(460px,100% - 24px);max-height:calc(100% - 24px);padding:20px;position:absolute;top:50%;left:50%;overflow:auto;transform:translate(-50%,-50%);box-shadow:0 0 0 100vmax #0000008c,0 0 30px #ffb45440}.plugins{width:min(560px,100% - 24px)}.plugins-heading{letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:14px 0 6px;font-size:.78rem}.plugin-list{gap:8px;margin:0;padding:0;list-style:none;display:grid}.plugin-list li{background:#ffffff0a;justify-content:space-between;align-items:center;gap:10px;padding:8px 10px;display:flex}.plugin-info{min-width:0}.plugin-info .auth-note{font-size:.82rem}.plugin-actions{flex-shrink:0;gap:6px;display:flex}.plugin-actions button{border:1px solid var(--edge);background:0 0;padding:0 12px}.obj-sandbox{flex-direction:column;gap:6px;width:100%;min-width:min(560px,80vw);height:min(420px,60vh);display:flex}.obj-sandbox iframe{border:1px solid var(--edge);background:#050b10;flex:1;width:100%;min-height:200px}.camera-indicator{border-color:var(--bad);color:#ffc2c9;letter-spacing:.12em;text-transform:uppercase;background:#020c12e6;align-items:center;gap:10px;padding:6px 12px 6px 6px;font-size:.8rem;display:flex;position:absolute;bottom:12px;right:12px}.camera-indicator video{object-fit:cover;background:#000;width:72px;height:54px}.camera-indicator span:before{content:"";background:var(--bad);border-radius:50%;width:8px;height:8px;margin-right:8px;display:inline-block}.challenge,.enroll{border-color:var(--accent);box-shadow:0 0 0 100vmax #0000008c,0 0 30px #3fd8ff40}.challenge .approval-title,.enroll .approval-title{color:var(--accent)}.approval-head{align-items:center;gap:10px;display:flex}.approval-title{letter-spacing:.18em;text-transform:uppercase;color:var(--warn);font-size:.8rem}.tier{font:600 .74rem var(--mono);border:1px solid;padding:2px 8px}.tier-T2{color:var(--warn)}.tier-T3{color:var(--bad)}.approval-summary{font:.95rem/1.5 var(--mono);word-break:break-all;background:#ffffff0a;max-height:40vh;margin:14px 0;padding:10px 12px;overflow:auto}.approval-actions{flex-wrap:wrap;gap:8px;margin-top:14px;display:flex}.approval-actions button,.owner-menu button,.auth button{border:1px solid var(--edge);letter-spacing:.04em;background:0 0;padding:0 16px}button.primary{background:color-mix(in srgb, var(--accent) 22%, transparent);border-color:var(--accent)}button.danger{border-color:var(--bad);color:#ffc2c9}button:disabled{opacity:.4;cursor:not-allowed}.pinpad{justify-items:center;gap:8px;display:grid}.pin-dots{letter-spacing:.4em;min-height:1.6em;color:var(--accent);font-size:1.6rem}.pin-keys{grid-template-columns:repeat(3,64px);gap:8px;display:grid}.pin-keys button{border:1px solid var(--edge);background:#3fd8ff0d;font-size:1.2rem}.owner-menu{top:max(60px, env(safe-area-inset-top));background:#020c12f0;gap:8px;width:220px;padding:12px;display:grid;left:12px}.boot{background:#000;height:100%}.auth{gap:8px;width:min(380px,100% - 28px);padding:24px;display:grid;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}.auth h1{letter-spacing:.3em;text-transform:uppercase;color:var(--accent);margin:0 0 6px;font-size:1.1rem;font-weight:300}.auth label{letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-top:6px;font-size:.78rem}.auth input{border:1px solid var(--edge);background:#ffffff0a;padding:0 12px}.auth button{margin-top:12px}.auth-note{color:var(--muted);margin:0;font-size:.9rem}.auth-error{color:#ffc2c9;margin:4px 0 0;font-size:.9rem}@media (width<=599px){.presence-full{inset:0}.captions{bottom:max(11%, env(safe-area-inset-bottom))}}@media (prefers-reduced-motion:reduce){.obj,.presence{transition:none;animation:none}}.status-badge{left:12px;bottom:max(12px, env(safe-area-inset-bottom));font:500 .78rem var(--mono);letter-spacing:.08em;color:var(--ink);border:1px solid var(--edge);background:#0000008c;align-items:center;gap:9px;max-width:min(60vw,360px);padding:6px 12px;display:flex;position:absolute}.status-badge span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.status-badge i{background:var(--muted);border-radius:50%;flex:none;width:9px;height:9px}.status-idle i{background:var(--ok);box-shadow:0 0 8px var(--ok)}.status-working i,.status-speaking i{background:var(--accent);box-shadow:0 0 10px var(--accent);animation:1s ease-in-out infinite mic-pulse}.status-listening i{background:var(--bad);box-shadow:0 0 10px var(--bad);animation:1s ease-in-out infinite mic-pulse}.status-waiting{border-color:var(--warn);color:#ffd9a0}.status-waiting i{background:var(--warn);box-shadow:0 0 10px var(--warn);animation:1s ease-in-out infinite mic-pulse}.status-offline{border-color:var(--warn);color:var(--warn)}.status-offline i{background:var(--warn)}@media (prefers-reduced-motion:reduce){.status-badge i{animation:none!important}}.clock{right:12px;bottom:max(12px, env(safe-area-inset-bottom));text-align:right;color:var(--muted);line-height:1.15;font-family:var(--mono);pointer-events:none;position:absolute}.clock b{color:var(--ink);letter-spacing:.06em;font-size:1.15rem;font-weight:500;display:block}.clock span{letter-spacing:.1em;text-transform:uppercase;font-size:.7rem}.settings{flex-direction:column;width:min(520px,100% - 24px);max-height:calc(100% - 24px);display:flex}.settings-body{gap:6px;min-height:0;padding-right:4px;display:grid;overflow-y:auto}.settings-field{gap:3px;display:grid}.settings-field>span{letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-size:.74rem}.settings-field input:not([type=range]):not([type=checkbox]),.settings-field select{border:1px solid var(--edge);color:var(--ink);min-height:40px;font:inherit;background:#ffffff0a;padding:0 10px}.settings-field select option{background:#06141c}.settings-check{align-items:center;gap:10px;min-height:40px;display:flex}.slash-list{border:1px solid var(--edge);background:#020c12f5;max-height:40vh;margin:0;padding:4px;list-style:none;position:absolute;bottom:calc(100% + 6px);left:0;right:0;overflow-y:auto}.slash-list li{cursor:pointer;align-items:baseline;gap:10px;padding:7px 10px;display:flex}.slash-list li.on,.slash-list li:hover{background:color-mix(in srgb, var(--accent) 18%, transparent)}.slash-list b{font:600 .9rem var(--mono);color:var(--accent)}.slash-list i{font:.78rem var(--mono);color:var(--warn);font-style:normal}.slash-list span{color:var(--muted);font-size:.82rem}.movable{position:absolute}.movable[data-panel=status],.movable[data-panel=clock]{cursor:grab;touch-action:none;pointer-events:auto}.movable:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.panel{resize:both;flex-direction:column;width:clamp(260px,28vw,420px);min-width:220px;min-height:140px;max-height:80vh;padding:0;display:flex;overflow:hidden}.panel-chat{height:62vh}.panel-tasks{width:clamp(240px,26vw,360px);height:40vh}.panel-head{cursor:grab;touch-action:none;border-bottom:1px solid var(--edge);-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:10px;padding:8px 12px;display:flex}.panel-head .obj-close{position:static}.chat-lines{flex:1;align-content:start;gap:8px;min-height:0;padding:10px 12px;display:grid;overflow-y:auto}.chat-line{white-space:pre-wrap;overflow-wrap:anywhere;max-width:92%;margin:0;padding:7px 10px;font-size:.9rem}.chat-user{background:color-mix(in srgb, var(--accent) 18%, transparent);border:1px solid var(--edge);justify-self:end}.chat-assistant{background:#ffffff0d;justify-self:start}.chat-form{border-top:1px solid var(--edge);gap:6px;padding:8px;display:flex}.chat-form input{border:1px solid var(--edge);min-width:0;color:var(--ink);min-height:40px;font:inherit;background:#ffffff0a;flex:1;padding:0 10px}.chat-form button{border:1px solid var(--edge);color:var(--ink);background:0 0;min-height:40px;padding:0 14px}.task-body{flex:1;min-height:0;padding:10px 12px;overflow-y:auto}.task-list{gap:8px;margin:0;padding:0;list-style:none;display:grid}.task-list li{border-left:3px solid var(--accent);background:#ffffff0a;gap:2px;padding:8px 10px;display:grid}.task-list li.overdue{border-left-color:var(--bad)}.task-title{overflow-wrap:anywhere;font-size:.95rem}.task-due{font:.75rem var(--mono);color:var(--muted)}.task-list li.overdue .task-due{color:var(--bad)}@media (width<=600px){.panel{resize:none;width:calc(100% - 24px);left:12px!important}.panel-chat{height:52vh;bottom:56px;top:auto!important}.panel-tasks{height:30vh;top:56px!important}}.help{flex-direction:column;width:min(560px,100% - 24px);max-height:calc(100% - 24px);display:flex}.help-list{gap:4px;min-height:0;margin:8px 0;display:grid;overflow-y:auto}.help-row{border-bottom:1px solid #ffffff0f;grid-template-columns:minmax(130px,38%) 1fr;gap:12px;padding:6px 4px;display:grid}.help-row dt{font:600 .9rem var(--mono);color:var(--accent);overflow-wrap:anywhere}.help-row dt i{color:var(--warn);font-style:normal;font-weight:400}.help-row dd{color:var(--ink);margin:0;font-size:.9rem}@media (width<=600px){.help-row{grid-template-columns:1fr;gap:2px}}.connectors{width:min(600px,100% - 24px);max-height:calc(100% - 24px);overflow-y:auto}.connector-form{gap:8px;display:grid}.connector-form textarea,.settings-field textarea{border:1px solid var(--edge);color:var(--ink);font:.85rem var(--mono);resize:vertical;background:#ffffff0a;padding:8px 10px}.connector-steps{color:var(--muted);gap:4px;margin:0;padding-left:20px;font-size:.88rem;display:grid}.connector-warning{border-left:3px solid var(--warn);color:#ffd9a0;padding-left:10px}.conn-status{font:.74rem var(--mono)}.conn-ready{color:var(--ok)}.conn-error{color:var(--bad)}.conn-connecting{color:var(--warn)}.conn-off{color:var(--muted)}.settings-wide{width:min(980px,100% - 24px);height:min(720px,100% - 24px)}.settings-layout{flex:1;grid-template-columns:190px 1fr;gap:16px;min-height:0;margin-top:8px;display:grid}.settings-tabs{align-content:start;gap:6px;display:grid}.settings-tab{text-align:left;color:var(--ink);background:0 0;border:1px solid #0000;gap:2px;padding:10px 12px;display:grid}.settings-tab small{color:var(--muted);font-size:.74rem}.settings-tab:hover{background:#ffffff0d}.settings-tab.on{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 16%, transparent)}.settings-wide .settings-body{align-content:start;padding-right:8px}@media (width<=700px){.settings-wide{height:calc(100% - 24px)}.settings-layout{grid-template-columns:1fr;gap:8px}.settings-tabs{grid-auto-columns:1fr;grid-auto-flow:column;overflow-x:auto}.settings-tab small{display:none}}.look-tab{grid-template-columns:minmax(220px,300px) 1fr;align-items:start;gap:18px;display:grid}.look-side{gap:10px;display:grid;position:sticky;top:0}.look-controls{gap:6px;min-width:0;display:grid}@media (width<=860px){.look-tab{grid-template-columns:1fr}.look-side{position:static}}.look-preview{border:1px solid var(--edge);background:#000;height:230px;position:relative;overflow:hidden}.look-preview-glow{background:radial-gradient(ellipse at 50% 55%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 65%);position:absolute;inset:0}.look-preview-stage{position:absolute;inset:0}.look-preview-caption{text-align:center;text-shadow:0 0 10px #000;margin:0;position:absolute;bottom:8px;left:8px;right:8px}.look-preview .presence-full{inset:0}.theme-grid{grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:8px;display:grid}.theme-card{border:1px solid var(--edge);color:var(--ink);text-align:left;background:#ffffff08;justify-items:start;gap:1px;padding:10px;display:grid}.theme-card i{background:var(--swatch);width:22px;height:22px;box-shadow:0 0 12px var(--swatch);border-radius:50%;margin-bottom:4px}.theme-card small{color:var(--muted);font-size:.72rem}.theme-card.on,.style-choice.on{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 18%, transparent)}.style-row{grid-template-columns:repeat(3,1fr);gap:8px;display:grid}.style-choice{border:1px solid var(--edge);color:var(--ink);text-align:left;background:#ffffff08;gap:2px;padding:10px;display:grid}.style-choice small{color:var(--muted);font-size:.72rem}.swatch-row{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.swatch{border:2px solid #0000;border-radius:50%;width:32px;height:32px;min-height:32px;padding:0}.swatch.on{box-shadow:0 0 0 2px var(--accent);border-color:#fff}.swatch-custom{color:var(--muted);align-items:center;gap:6px;font-size:.8rem;display:flex}.swatch-custom input[type=color]{border:1px solid var(--edge);background:0 0;width:40px;height:32px;padding:0}.captions-small{font-size:clamp(.9rem,2vw,1.15rem)}.captions-medium{font-size:clamp(1.1rem,2.6vw,1.6rem)}.captions-large{font-size:clamp(1.4rem,3.6vw,2.3rem)}.voice-hear{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.connector-command{border:1px solid var(--edge);font:.85rem var(--mono);-webkit-user-select:all;user-select:all;background:#ffffff0d;margin:0;padding:8px 10px;overflow-x:auto}html,body{overscroll-behavior:none}@media (pointer:coarse){.stage{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}}input,textarea,select,.connector-command{-webkit-user-select:text;user-select:text}.owner-menu{max-height:calc(100dvh - 80px);overflow-y:auto}.touch-controls{z-index:25;left:0;right:0;bottom:max(14px, env(safe-area-inset-bottom));pointer-events:none;justify-content:center;align-items:flex-end;gap:26px;display:flex;position:absolute}.touch-controls>*{pointer-events:auto}.touch-talk{justify-items:center;gap:6px;display:grid}.touch-hint{max-width:70vw;font:500 .74rem var(--mono);letter-spacing:.06em;color:var(--muted);text-align:center;text-shadow:0 0 6px #000,0 0 2px #000;padding:2px 10px}.touch-mic,.touch-side{border:1px solid var(--edge);color:var(--ink);background:var(--glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);touch-action:none;-webkit-user-select:none;user-select:none;border-radius:50%;place-items:center;padding:0;display:grid}.touch-mic{border-color:var(--accent);width:76px;height:76px;box-shadow:0 0 22px color-mix(in srgb, var(--accent) 30%, transparent)}.touch-mic.on{background:color-mix(in srgb, var(--bad) 40%, transparent);border-color:var(--bad);box-shadow:0 0 28px var(--bad);animation:1s ease-in-out infinite mic-pulse}.touch-side{touch-action:manipulation;width:52px;height:52px;margin-bottom:12px}@media (prefers-reduced-motion:reduce){.touch-mic.on{animation:none}}@media (pointer:coarse){.captions{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 150px)}.status-badge,.clock{bottom:max(8px, env(safe-area-inset-bottom))}.presence-corner{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 70px)}}@media (width<=600px) and (pointer:coarse){.panel-chat{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 150px);height:min(48vh,100dvh - 260px)}.objects-stack{padding-bottom:190px}}
1
+ :root{--accent:#3fd8ff;--warn:#ffb454;--bad:#ff6b7a;--ok:#5ef0b0;--ink:#d4ecf7;--muted:#7fa3b6;--glass:#06141c9e;--edge:color-mix(in srgb, var(--accent) 48%, transparent);--ui:"Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;--mono:"Cascadia Mono", "SF Mono", Consolas, ui-monospace, monospace;--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}*{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{color:var(--ink);font:16px/1.5 var(--ui);-webkit-tap-highlight-color:transparent;background:#000;overflow:hidden}button,input{font:inherit;color:inherit}button{cursor:pointer}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.stage{touch-action:manipulation;background:#000;position:fixed;inset:0;overflow:hidden}.ambient{pointer-events:none;background:radial-gradient(ellipse at 50% 55%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 62%), linear-gradient(#3fd8ff0b 1px, transparent 1px) 0 0 / 64px 64px, linear-gradient(90deg, #3fd8ff0b 1px, transparent 1px) 0 0 / 64px 64px;position:absolute;inset:0;-webkit-mask-image:radial-gradient(#000 30%,#0000 78%);mask-image:radial-gradient(#000 30%,#0000 78%)}.presence{z-index:1;transition:inset .7s cubic-bezier(.2,.7,.2,1),width .7s,height .7s,opacity .5s;position:absolute}.presence-full{inset:0}.presence-corner{left:max(10px, env(safe-area-inset-left));bottom:max(52px, calc(env(safe-area-inset-bottom) + 42px));width:clamp(96px,16vw,190px);height:clamp(96px,16vw,190px)}.objects{pointer-events:none;position:absolute;inset:0}.objects-stack{pointer-events:auto;padding:max(56px, env(safe-area-inset-top)) 14px 140px;flex-direction:column;gap:12px;display:flex;overflow-y:auto}.auto-grid{pointer-events:none;padding:max(24px, env(safe-area-inset-top)) 24px 24px clamp(120px, 20vw, 230px);grid-template-columns:repeat(auto-fit,minmax(min(280px,100%),1fr));align-content:safe center;align-items:start;gap:12px;display:grid;position:absolute;inset:0;overflow-y:auto}.auto-grid>.obj{pointer-events:auto}.objects-stack>.auto-grid{flex-direction:column;padding:0;display:flex;position:static;overflow:visible}.obj.leaving{pointer-events:none;animation:.35s forwards leave}@keyframes leave{to{opacity:0;transform:scale(.96)translateY(-6px)}}@media (prefers-reduced-motion:reduce){.obj.leaving{animation:none}}.glass{background:var(--glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border:1px solid var(--edge);box-shadow:0 0 22px color-mix(in srgb, var(--accent) 16%, transparent), inset 0 0 28px #3fd8ff0d;clip-path:polygon(0 0,calc(100% - 14px) 0,100% 14px,100% 100%,14px 100%,0 calc(100% - 14px))}.obj{pointer-events:auto;min-width:0;padding:16px 18px;animation:.45s both appear;overflow:auto}.obj.alert{border-color:var(--warn)}@keyframes appear{0%{opacity:0;transform:translateY(8px)scale(.985)}}.obj-close{width:44px;height:44px;color:var(--muted);background:0 0;border:0;font-size:1.5rem;line-height:1;position:absolute;top:2px;right:6px}.obj-close:hover{color:var(--accent)}.obj-title{letter-spacing:.16em;text-transform:uppercase;color:var(--accent);margin:0 36px 8px 0;font-size:.78rem;font-weight:600}.obj-md>:first-child{margin-top:0}.obj-md>:last-child{margin-bottom:0}.obj-md a{color:var(--accent)}.obj-md code{font-family:var(--mono);background:#3fd8ff1a;padding:0 4px;font-size:.9em}.obj-stat{gap:2px;display:grid}.obj-label{letter-spacing:.16em;text-transform:uppercase;color:var(--muted);font-size:.78rem}.obj-value{font-variant-numeric:tabular-nums;font-size:clamp(2rem,6vw,3.6rem);font-weight:300;line-height:1.05}.obj-value small{color:var(--muted);margin-left:4px;font-size:.4em}.obj-delta{color:var(--ok);font-size:.9rem}.obj-list{gap:4px;margin:0;padding-left:1.1rem;display:grid}.obj-list .done{color:var(--muted);text-decoration:line-through}.obj-table-wrap{overflow-x:auto}.obj-table{border-collapse:collapse;width:100%;font-size:.92rem}.obj-table th{text-align:left;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);font-size:.72rem;font-weight:600}.obj-table th,.obj-table td{font-variant-numeric:tabular-nums;border-bottom:1px solid #3fd8ff2e;padding:6px 10px}.obj-code{font:.86rem/1.5 var(--mono);margin:0;overflow:auto}.obj-empty{color:var(--muted);margin:0}.obj-chart{width:100%;height:auto;display:block}.obj-chart .grid{stroke:#3fd8ff24;stroke-width:1px}.obj-chart .tick{fill:var(--muted);font:10px var(--mono);font-variant-numeric:tabular-nums}.legend{color:var(--muted);flex-wrap:wrap;gap:6px 14px;margin:6px 0 0;padding:0;font-size:.8rem;list-style:none;display:flex}.legend i{width:10px;height:10px;margin-right:6px;display:inline-block}.obj-gauge{justify-items:center;display:grid}.obj-gauge svg{width:min(100%,260px);height:auto}.obj-gauge .track{stroke:#3fd8ff26}.obj-gauge .value{stroke:var(--accent);filter:drop-shadow(0 0 6px var(--accent))}.obj-gauge.gauge-warn .value{stroke:var(--warn);filter:drop-shadow(0 0 6px var(--warn))}.obj-gauge.gauge-crit .value{stroke:var(--bad);filter:drop-shadow(0 0 6px var(--bad))}.gauge-read{align-items:baseline;gap:4px;margin-top:-62px;display:flex}.gauge-value{font-variant-numeric:tabular-nums;font-size:clamp(1.8rem,5vw,2.8rem);font-weight:300}.gauge-unit{color:var(--muted)}.obj-gauge .obj-label{margin-top:18px}.obj-timeline{border-left:1px solid var(--edge);gap:14px;margin:8px 0 0;padding:0 0 0 20px;list-style:none;display:grid}.obj-timeline li{gap:2px;display:grid;position:relative}.obj-timeline li:before{content:"";background:var(--accent);width:9px;height:9px;box-shadow:0 0 8px var(--accent);border-radius:50%;position:absolute;top:.5em;left:-25px}.obj-timeline time{font:.75rem/1.3 var(--mono);color:var(--accent)}.obj-timeline span{color:var(--muted);font-size:.9rem}.obj-graph{width:100%;height:auto;max-height:70vh;margin:0 auto;display:block}.obj[style*=" height:"] .obj-graph{max-height:calc(100% - 32px)}.obj-graph .edge{stroke:color-mix(in srgb, var(--accent) 55%, transparent);stroke-width:1.4px}.obj-graph .arrow{fill:var(--accent)}.obj-graph .node{fill:#3fd8ff26;stroke:var(--accent);stroke-width:1.5px}.obj-graph .node-label{fill:var(--ink);font:10px var(--ui)}.obj-graph .edge-label{fill:var(--muted);font:9px var(--ui)}.captions{z-index:20;left:50%;bottom:max(7%, env(safe-area-inset-bottom));text-align:center;pointer-events:none;text-shadow:0 0 18px #000,0 0 4px #000;text-wrap:balance;width:min(780px,100% - 32px);margin:0;font-size:clamp(1.1rem,2.6vw,1.6rem);font-weight:300;position:absolute;transform:translate(-50%)}.captions.done{animation:1.4s 8s forwards fadeOut}@keyframes fadeOut{to{opacity:0}}.command-bar{z-index:30;left:50%;bottom:max(14px, env(safe-area-inset-bottom));background:var(--glass);border:1px solid var(--edge);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);gap:8px;width:min(640px,100% - 24px);padding:8px;display:flex;position:absolute;transform:translate(-50%)}.command-bar input{background:0 0;border:0;outline:none;flex:1;min-height:44px;padding:0 10px;font-size:1.05rem}.command-bar button{border:1px solid var(--edge);background:0 0;min-width:44px;min-height:44px}.conn-badge{top:max(10px, env(safe-area-inset-top));font:500 .78rem var(--mono);letter-spacing:.08em;color:var(--warn);border:1px solid var(--warn);background:#0009;padding:6px 12px;position:absolute;right:12px}.mic-indicator{left:50%;top:max(12px, env(safe-area-inset-top));font:500 .82rem var(--mono);letter-spacing:.1em;text-transform:uppercase;color:#ffd9a0;border:1px solid var(--warn);background:#000000c7;align-items:center;gap:10px;padding:8px 16px;display:flex;position:absolute;transform:translate(-50%)}.mic-indicator i{background:var(--bad);width:10px;height:10px;box-shadow:0 0 10px var(--bad);border-radius:50%;animation:1s ease-in-out infinite mic-pulse}.mic-transcribing i{background:var(--warn);box-shadow:0 0 10px var(--warn)}@keyframes mic-pulse{50%{opacity:.35}}@media (prefers-reduced-motion:reduce){.mic-indicator i{animation:none}}.toasts{top:max(48px, env(safe-area-inset-top));gap:6px;width:min(420px,100% - 24px);display:grid;position:absolute;left:50%;transform:translate(-50%)}.toast{border:1px solid var(--edge);background:#000000d1;padding:10px 14px;font-size:.92rem}.toast-warn{border-color:var(--warn);color:#ffd9a0}.toast-error{border-color:var(--bad);color:#ffc2c9}.approval{border-color:var(--warn);background:#020c12eb;width:min(460px,100% - 24px);max-height:calc(100% - 24px);padding:20px;position:absolute;top:50%;left:50%;overflow:auto;transform:translate(-50%,-50%);box-shadow:0 0 0 100vmax #0000008c,0 0 30px #ffb45440}.plugins{width:min(560px,100% - 24px)}.plugins-heading{letter-spacing:.14em;text-transform:uppercase;color:var(--muted);margin:14px 0 6px;font-size:.78rem}.plugin-list{gap:8px;margin:0;padding:0;list-style:none;display:grid}.plugin-list li{background:#ffffff0a;justify-content:space-between;align-items:center;gap:10px;padding:8px 10px;display:flex}.plugin-info{min-width:0}.plugin-info .auth-note{font-size:.82rem}.plugin-actions{flex-shrink:0;gap:6px;display:flex}.plugin-actions button{border:1px solid var(--edge);background:0 0;padding:0 12px}.obj-sandbox{flex-direction:column;gap:6px;width:100%;min-width:min(560px,80vw);height:min(420px,60vh);display:flex}.obj-sandbox iframe{border:1px solid var(--edge);background:#050b10;flex:1;width:100%;min-height:200px}.camera-indicator{border-color:var(--bad);color:#ffc2c9;letter-spacing:.12em;text-transform:uppercase;background:#020c12e6;align-items:center;gap:10px;padding:6px 12px 6px 6px;font-size:.8rem;display:flex;position:absolute;bottom:12px;right:12px}.camera-indicator video{object-fit:cover;background:#000;width:72px;height:54px}.camera-indicator span:before{content:"";background:var(--bad);border-radius:50%;width:8px;height:8px;margin-right:8px;display:inline-block}.challenge,.enroll{border-color:var(--accent);box-shadow:0 0 0 100vmax #0000008c,0 0 30px #3fd8ff40}.challenge .approval-title,.enroll .approval-title{color:var(--accent)}.approval-head{align-items:center;gap:10px;display:flex}.approval-title{letter-spacing:.18em;text-transform:uppercase;color:var(--warn);font-size:.8rem}.tier{font:600 .74rem var(--mono);border:1px solid;padding:2px 8px}.tier-T2{color:var(--warn)}.tier-T3{color:var(--bad)}.approval-summary{font:.95rem/1.5 var(--mono);word-break:break-all;background:#ffffff0a;max-height:40vh;margin:14px 0;padding:10px 12px;overflow:auto}.approval-actions{flex-wrap:wrap;gap:8px;margin-top:14px;display:flex}.approval-actions button,.owner-menu button,.auth button{border:1px solid var(--edge);letter-spacing:.04em;background:0 0;padding:0 16px}button.primary{background:color-mix(in srgb, var(--accent) 22%, transparent);border-color:var(--accent)}button.danger{border-color:var(--bad);color:#ffc2c9}button:disabled{opacity:.4;cursor:not-allowed}.pinpad{justify-items:center;gap:8px;display:grid}.pin-dots{letter-spacing:.4em;min-height:1.6em;color:var(--accent);font-size:1.6rem}.pin-keys{grid-template-columns:repeat(3,64px);gap:8px;display:grid}.pin-keys button{border:1px solid var(--edge);background:#3fd8ff0d;font-size:1.2rem}.owner-menu{top:max(60px, env(safe-area-inset-top));background:#020c12f0;gap:8px;width:220px;padding:12px;display:grid;left:12px}.boot{background:#000;height:100%}.auth{gap:8px;width:min(380px,100% - 28px);padding:24px;display:grid;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%)}.auth h1{letter-spacing:.3em;text-transform:uppercase;color:var(--accent);margin:0 0 6px;font-size:1.1rem;font-weight:300}.auth label{letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-top:6px;font-size:.78rem}.auth input{border:1px solid var(--edge);background:#ffffff0a;padding:0 12px}.auth button{margin-top:12px}.auth-note{color:var(--muted);margin:0;font-size:.9rem}.auth-error{color:#ffc2c9;margin:4px 0 0;font-size:.9rem}@media (width<=599px){.presence-full{inset:0}.captions{bottom:max(11%, env(safe-area-inset-bottom))}}@media (prefers-reduced-motion:reduce){.obj,.presence{transition:none;animation:none}}.status-badge{left:12px;bottom:max(12px, env(safe-area-inset-bottom));font:500 .78rem var(--mono);letter-spacing:.08em;color:var(--ink);border:1px solid var(--edge);background:#0000008c;align-items:center;gap:9px;max-width:min(60vw,360px);padding:6px 12px;display:flex;position:absolute}.status-badge span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.status-badge i{background:var(--muted);border-radius:50%;flex:none;width:9px;height:9px}.status-idle i{background:var(--ok);box-shadow:0 0 8px var(--ok)}.status-working i,.status-speaking i{background:var(--accent);box-shadow:0 0 10px var(--accent);animation:1s ease-in-out infinite mic-pulse}.status-listening i{background:var(--bad);box-shadow:0 0 10px var(--bad);animation:1s ease-in-out infinite mic-pulse}.status-waiting{border-color:var(--warn);color:#ffd9a0}.status-waiting i{background:var(--warn);box-shadow:0 0 10px var(--warn);animation:1s ease-in-out infinite mic-pulse}.status-offline{border-color:var(--warn);color:var(--warn)}.status-offline i{background:var(--warn)}@media (prefers-reduced-motion:reduce){.status-badge i{animation:none!important}}.clock{right:12px;bottom:max(12px, env(safe-area-inset-bottom));text-align:right;color:var(--muted);line-height:1.15;font-family:var(--mono);pointer-events:none;position:absolute}.clock b{color:var(--ink);letter-spacing:.06em;font-size:1.15rem;font-weight:500;display:block}.clock span{letter-spacing:.1em;text-transform:uppercase;font-size:.7rem}.settings{flex-direction:column;width:min(520px,100% - 24px);max-height:calc(100% - 24px);display:flex}.settings-body{gap:6px;min-height:0;padding-right:4px;display:grid;overflow-y:auto}.settings-field{gap:3px;display:grid}.settings-field>span{letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-size:.74rem}.settings-field input:not([type=range]):not([type=checkbox]),.settings-field select{border:1px solid var(--edge);color:var(--ink);min-height:40px;font:inherit;background:#ffffff0a;padding:0 10px}.settings-field select option{background:#06141c}.settings-check{align-items:center;gap:10px;min-height:40px;display:flex}.slash-list{border:1px solid var(--edge);background:#020c12f5;max-height:40vh;margin:0;padding:4px;list-style:none;position:absolute;bottom:calc(100% + 6px);left:0;right:0;overflow-y:auto}.slash-list li{cursor:pointer;align-items:baseline;gap:10px;padding:7px 10px;display:flex}.slash-list li.on,.slash-list li:hover{background:color-mix(in srgb, var(--accent) 18%, transparent)}.slash-list b{font:600 .9rem var(--mono);color:var(--accent)}.slash-list i{font:.78rem var(--mono);color:var(--warn);font-style:normal}.slash-list span{color:var(--muted);font-size:.82rem}.movable{position:absolute}.movable[data-panel=status],.movable[data-panel=clock]{cursor:grab;touch-action:none;pointer-events:auto}.movable:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.panel{resize:both;flex-direction:column;width:clamp(260px,28vw,420px);min-width:220px;min-height:140px;max-height:80vh;padding:0;display:flex;overflow:hidden}.panel-chat{height:62vh}.panel-tasks{width:clamp(240px,26vw,360px);height:40vh}.panel-head{cursor:grab;touch-action:none;border-bottom:1px solid var(--edge);-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:10px;padding:8px 12px;display:flex}.panel-head .obj-close{position:static}.chat-lines{flex:1;align-content:start;gap:8px;min-height:0;padding:10px 12px;display:grid;overflow-y:auto}.chat-line{white-space:pre-wrap;overflow-wrap:anywhere;max-width:92%;margin:0;padding:7px 10px;font-size:.9rem}.chat-user{background:color-mix(in srgb, var(--accent) 18%, transparent);border:1px solid var(--edge);justify-self:end}.chat-assistant{background:#ffffff0d;justify-self:start}.chat-form{border-top:1px solid var(--edge);gap:6px;padding:8px;display:flex}.chat-form input{border:1px solid var(--edge);min-width:0;color:var(--ink);min-height:40px;font:inherit;background:#ffffff0a;flex:1;padding:0 10px}.chat-form button{border:1px solid var(--edge);color:var(--ink);background:0 0;min-height:40px;padding:0 14px}.task-body{flex:1;min-height:0;padding:10px 12px;overflow-y:auto}.task-list{gap:8px;margin:0;padding:0;list-style:none;display:grid}.task-list li{border-left:3px solid var(--accent);background:#ffffff0a;gap:2px;padding:8px 10px;display:grid}.task-list li.overdue{border-left-color:var(--bad)}.task-title{overflow-wrap:anywhere;font-size:.95rem}.task-due{font:.75rem var(--mono);color:var(--muted)}.task-list li.overdue .task-due{color:var(--bad)}@media (width<=600px){.panel{resize:none;width:calc(100% - 24px);left:12px!important}.panel-chat{height:52vh;bottom:56px;top:auto!important}.panel-tasks{height:30vh;top:56px!important}}.help{flex-direction:column;width:min(560px,100% - 24px);max-height:calc(100% - 24px);display:flex}.help-list{gap:4px;min-height:0;margin:8px 0;display:grid;overflow-y:auto}.help-row{border-bottom:1px solid #ffffff0f;grid-template-columns:minmax(130px,38%) 1fr;gap:12px;padding:6px 4px;display:grid}.help-row dt{font:600 .9rem var(--mono);color:var(--accent);overflow-wrap:anywhere}.help-row dt i{color:var(--warn);font-style:normal;font-weight:400}.help-row dd{color:var(--ink);margin:0;font-size:.9rem}@media (width<=600px){.help-row{grid-template-columns:1fr;gap:2px}}.connectors{width:min(600px,100% - 24px);max-height:calc(100% - 24px);overflow-y:auto}.connector-form{gap:8px;display:grid}.connector-form textarea,.settings-field textarea{border:1px solid var(--edge);color:var(--ink);font:.85rem var(--mono);resize:vertical;background:#ffffff0a;padding:8px 10px}.connector-steps{color:var(--muted);gap:4px;margin:0;padding-left:20px;font-size:.88rem;display:grid}.connector-warning{border-left:3px solid var(--warn);color:#ffd9a0;padding-left:10px}.conn-status{font:.74rem var(--mono)}.conn-ready{color:var(--ok)}.conn-error{color:var(--bad)}.conn-connecting{color:var(--warn)}.conn-off{color:var(--muted)}.settings-wide{width:min(980px,100% - 24px);height:min(720px,100% - 24px)}.settings-layout{flex:1;grid-template-columns:190px 1fr;gap:16px;min-height:0;margin-top:8px;display:grid}.settings-tabs{align-content:start;gap:6px;display:grid}.settings-tab{text-align:left;color:var(--ink);background:0 0;border:1px solid #0000;gap:2px;padding:10px 12px;display:grid}.settings-tab small{color:var(--muted);font-size:.74rem}.settings-tab:hover{background:#ffffff0d}.settings-tab.on{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 16%, transparent)}.settings-wide .settings-body{align-content:start;padding-right:8px}@media (width<=700px){.settings-wide{height:calc(100% - 24px)}.settings-layout{grid-template-columns:1fr;gap:8px}.settings-tabs{grid-auto-columns:1fr;grid-auto-flow:column;overflow-x:auto}.settings-tab small{display:none}}.look-tab{grid-template-columns:minmax(220px,300px) 1fr;align-items:start;gap:18px;display:grid}.look-side{gap:10px;display:grid;position:sticky;top:0}.look-controls{gap:6px;min-width:0;display:grid}@media (width<=860px){.look-tab{grid-template-columns:1fr}.look-side{position:static}}.look-preview{border:1px solid var(--edge);background:#000;height:230px;position:relative;overflow:hidden}.look-preview-glow{background:radial-gradient(ellipse at 50% 55%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 65%);position:absolute;inset:0}.look-preview-stage{position:absolute;inset:0}.look-preview-caption{text-align:center;text-shadow:0 0 10px #000;margin:0;position:absolute;bottom:8px;left:8px;right:8px}.look-preview .presence-full{inset:0}.theme-grid{grid-template-columns:repeat(auto-fill,minmax(118px,1fr));gap:8px;display:grid}.theme-card{border:1px solid var(--edge);color:var(--ink);text-align:left;background:#ffffff08;justify-items:start;gap:1px;padding:10px;display:grid}.theme-card i{background:var(--swatch);width:22px;height:22px;box-shadow:0 0 12px var(--swatch);border-radius:50%;margin-bottom:4px}.theme-card small{color:var(--muted);font-size:.72rem}.theme-card.on,.style-choice.on{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 18%, transparent)}.style-row{grid-template-columns:repeat(3,1fr);gap:8px;display:grid}.style-choice{border:1px solid var(--edge);color:var(--ink);text-align:left;background:#ffffff08;gap:2px;padding:10px;display:grid}.style-choice small{color:var(--muted);font-size:.72rem}.swatch-row{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.swatch{border:2px solid #0000;border-radius:50%;width:32px;height:32px;min-height:32px;padding:0}.swatch.on{box-shadow:0 0 0 2px var(--accent);border-color:#fff}.swatch-custom{color:var(--muted);align-items:center;gap:6px;font-size:.8rem;display:flex}.swatch-custom input[type=color]{border:1px solid var(--edge);background:0 0;width:40px;height:32px;padding:0}.captions-small{font-size:clamp(.9rem,2vw,1.15rem)}.captions-medium{font-size:clamp(1.1rem,2.6vw,1.6rem)}.captions-large{font-size:clamp(1.4rem,3.6vw,2.3rem)}.voice-hear{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.connector-command{border:1px solid var(--edge);font:.85rem var(--mono);-webkit-user-select:all;user-select:all;background:#ffffff0d;margin:0;padding:8px 10px;overflow-x:auto}html,body{overscroll-behavior:none}@media (pointer:coarse){.stage{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none}}input,textarea,select,.connector-command{-webkit-user-select:text;user-select:text}.owner-menu{max-height:calc(100dvh - 80px);overflow-y:auto}.touch-controls{z-index:25;left:0;right:0;bottom:max(14px, env(safe-area-inset-bottom));pointer-events:none;justify-content:center;align-items:flex-end;gap:26px;display:flex;position:absolute}.touch-controls>*{pointer-events:auto}.touch-talk{justify-items:center;gap:6px;display:grid}.touch-hint{max-width:70vw;font:500 .74rem var(--mono);letter-spacing:.06em;color:var(--muted);text-align:center;text-shadow:0 0 6px #000,0 0 2px #000;padding:2px 10px}.touch-mic,.touch-side{border:1px solid var(--edge);color:var(--ink);background:var(--glass);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);touch-action:none;-webkit-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;border-radius:50%;place-items:center;padding:0;display:grid}.touch-mic{border-color:var(--accent);width:76px;height:76px;box-shadow:0 0 22px color-mix(in srgb, var(--accent) 30%, transparent)}.touch-mic.on{background:color-mix(in srgb, var(--bad) 40%, transparent);border-color:var(--bad);box-shadow:0 0 28px var(--bad);animation:1s ease-in-out infinite mic-pulse}.touch-side{touch-action:manipulation;width:52px;height:52px;margin-bottom:12px}@media (prefers-reduced-motion:reduce){.touch-mic.on{animation:none}}@media (pointer:coarse){.captions{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 150px)}.status-badge,.clock{bottom:max(8px, env(safe-area-inset-bottom))}.presence-corner{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 70px)}}@media (width<=600px) and (pointer:coarse){.panel-chat{bottom:calc(max(14px, env(safe-area-inset-bottom)) + 150px);height:min(48vh,100dvh - 260px)}.objects-stack{padding-bottom:190px}}.elsewhere{z-index:3000;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#000000c7;place-items:center;padding:16px;display:grid;position:fixed;inset:0}.elsewhere-card{border:1px solid var(--edge);background:var(--glass);max-width:380px;color:var(--ink);text-align:center;border-radius:16px;padding:24px}.elsewhere-card h2{margin:0 0 10px;font-size:1.15rem}.elsewhere-card p{color:var(--muted);margin:0 0 18px;line-height:1.45}.elsewhere-btn{border:1px solid var(--accent);background:color-mix(in srgb, var(--accent) 25%, transparent);min-height:48px;color:var(--ink);font:600 1rem var(--mono);cursor:pointer;border-radius:24px;padding:0 28px}