agent-office-studio 0.5.0 → 0.6.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.
@@ -0,0 +1,475 @@
1
+ // Visits: a guest office's own agents working in one of our projects. The
2
+ // agent runs on the guest's machine and the guest's Claude account; only its
3
+ // file tools reach us, as `visit.*` requests over the guest link. We answer
4
+ // them here, against the limits on the link (Guests checks the policy first).
5
+ //
6
+ // An agent never sees the project folder itself. Each visit gets a scratch
7
+ // copy under data/visits/<id>: the working tree as it stood (tracked and
8
+ // untracked files, not ignored ones, so .env and node_modules stay home),
9
+ // checked out from a throwaway index. Every path is held inside that copy,
10
+ // symlinks included, and .git and key files are refused. What the agent
11
+ // changes stays in the copy until the boss applies it from the job card.
12
+ //
13
+ // Each visit shows in our job list as a job of its own (`visit-<hex>`), with a
14
+ // log of every file the agent touched.
15
+ // See docs/linked-offices.md, "Their agents in your projects".
16
+
17
+ import crypto from 'node:crypto';
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { execFile, spawn } from 'node:child_process';
22
+ import { snapshotTree, summarize, fileDiff } from './changes.js';
23
+
24
+ export const ACCESS = ['off', 'read', 'edit'];
25
+ const MAX_OPEN = 3; // working visits per guest link
26
+ const MAX_READ = 24 * 1024; // what one answer carries, under the relay's 64 KB a frame
27
+ const MAX_WRITE = 40 * 1024; // what one write may carry, likewise
28
+ const MAX_FILE = 2 * 1024 * 1024; // the most a file may grow to
29
+ const MAX_WRITTEN = 50 * 1024 * 1024; // the most one visit may write in all
30
+ const MAX_LIST = 400;
31
+ const MAX_LOG = 300;
32
+ const SEARCH_MS = 10_000;
33
+ const IDLE_MS = 60 * 60 * 1000; // a visit nobody has used for this long is closed
34
+ const VISIT_ID = /^visit-[0-9a-f]{8}$/;
35
+ // Never read or written, wherever they sit in the copy.
36
+ const SECRET = [/^\.env(\..*)?$/i, /\.(pem|key|p12|pfx|keystore|jks)$/i, /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i, /^\.(npmrc|netrc|pypirc)$/i];
37
+ const fail = (code, message) => Object.assign(new Error(message), { code });
38
+
39
+ const git = (cwd, args, env = {}, input) => new Promise((resolve, reject) => {
40
+ // As in changes.js: nothing the project's config names should run.
41
+ const child = execFile('git', ['-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', ...args], { cwd, env: { ...process.env, ...env }, maxBuffer: 64 * 1024 * 1024, timeout: 120_000 }, (err, out, errOut) => {
42
+ if (err) reject(Object.assign(err, { stderr: String(errOut || '') })); else resolve(out);
43
+ });
44
+ if (input !== undefined) child.stdin.end(input);
45
+ });
46
+
47
+ const tmpIndex = () => path.join(os.tmpdir(), `office-visit-${process.pid}-${crypto.randomUUID().slice(0, 8)}`);
48
+
49
+ // What the boss's page shows of a visit: job-shaped, so it sits in the job
50
+ // list. The capability (sid), the copy's path and the base stay here.
51
+ export function visitJob(v, tail = 40) {
52
+ const { sid: _s, dir: _d, base: _b, theirJob: _t, written: _w, usedAt: _u, ...rest } = v;
53
+ return { ...rest, log: v.log.slice(-tail) };
54
+ }
55
+
56
+ export class Visits {
57
+ // office: { repoPath(name), send(msg), post(text, extra), feed(linkId, msg) }
58
+ constructor(dataDir, { office }) {
59
+ this.office = office;
60
+ this.root = path.join(dataDir, 'visits');
61
+ this.file = path.join(dataDir, 'visits.json');
62
+ this.list = [];
63
+ try { this.list = JSON.parse(fs.readFileSync(this.file, 'utf8')).visits || []; } catch {}
64
+ this.sweeper = setInterval(() => this.sweep(), 5 * 60 * 1000);
65
+ this.sweeper.unref?.();
66
+ }
67
+
68
+ save() {
69
+ try {
70
+ fs.mkdirSync(path.dirname(this.file), { recursive: true });
71
+ fs.writeFileSync(this.file + '.tmp', JSON.stringify({ visits: this.list }), { mode: 0o600 });
72
+ fs.renameSync(this.file + '.tmp', this.file);
73
+ } catch (err) { console.error('[visits] could not save', err.message); }
74
+ }
75
+
76
+ push(v) {
77
+ this.office.send({ t: 'job', job: visitJob(v, 0) });
78
+ this.save();
79
+ }
80
+
81
+ log(v, summary, kind = 'tool', extra = {}) {
82
+ const entry = { kind, summary, ts: Date.now(), ...extra };
83
+ v.log.push(entry);
84
+ if (v.log.length > MAX_LOG) v.log.splice(0, v.log.length - MAX_LOG);
85
+ this.office.send({ t: 'log', jobId: v.id, entry });
86
+ }
87
+
88
+ jobs() { return this.list.map((v) => visitJob(v)); }
89
+ get(id) { return VISIT_ID.test(String(id)) ? this.list.find((v) => v.id === id) || null : null; }
90
+
91
+ // ---------- requests from the guest's office ----------
92
+
93
+ // A guest's agent starting (or, on a follow-up, going back to) work in one
94
+ // of our projects. The same job of theirs gets the same copy until the
95
+ // boss applies or throws it away, so a reply carries on where it stopped.
96
+ async open(link, policy, args) {
97
+ const repo = String(args.repo ?? '');
98
+ if (!policy.repos.includes(repo)) throw fail(403, 'That project isn’t shared with you.');
99
+ if (!ACCESS.includes(policy.theirAgents) || policy.theirAgents === 'off') throw fail(403, 'This office doesn’t let your agents work in its projects.');
100
+ const theirJob = String(args.job ?? '').slice(0, 64);
101
+ const agent = { name: String(args.agent?.name ?? '').slice(0, 40) || 'Their agent', role: String(args.agent?.role ?? '').slice(0, 60) };
102
+ const title = String(args.title ?? '').slice(0, 200) || 'Work in progress';
103
+ const access = policy.theirAgents;
104
+ let v = this.list.find((x) => x.guest.link === link.id && x.theirJob === theirJob && x.repo === repo && !x.applied && !x.discarded);
105
+ if (v && v.status === 'working') return { sid: v.sid, access: v.access };
106
+ if (this.list.filter((x) => x.guest.link === link.id && x.status === 'working').length >= MAX_OPEN) {
107
+ throw fail(429, `You can have ${MAX_OPEN} agents working here at once. Wait for one to finish.`);
108
+ }
109
+ if (v && fs.existsSync(v.dir)) {
110
+ Object.assign(v, { status: 'working', access, sid: crypto.randomBytes(16).toString('hex'), endedAt: null, error: null, usedAt: Date.now(), title });
111
+ this.log(v, `${agent.name} came back to it.`, 'system');
112
+ this.push(v);
113
+ return { sid: v.sid, access };
114
+ }
115
+ const src = this.office.repoPath(repo);
116
+ if (!src) throw fail(404, 'That project folder is gone.');
117
+ const base = await snapshotTree(src);
118
+ if (!base) throw fail(409, `${repo} isn’t a git repository, so it can’t be shared with visiting agents.`);
119
+ const id = `visit-${crypto.randomBytes(4).toString('hex')}`;
120
+ const dir = path.join(this.root, id);
121
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
122
+ const idx = tmpIndex();
123
+ try {
124
+ await git(src, ['read-tree', base], { GIT_INDEX_FILE: idx });
125
+ await git(src, [`--work-tree=${dir}`, 'checkout-index', '-a', '-f'], { GIT_INDEX_FILE: idx });
126
+ } catch (err) {
127
+ fs.rmSync(dir, { recursive: true, force: true });
128
+ throw fail(500, `Could not copy ${repo}: ${String(err.stderr || err.message).slice(0, 200)}`);
129
+ } finally {
130
+ fs.rmSync(idx, { force: true });
131
+ }
132
+ const now = Date.now();
133
+ v = {
134
+ id, repo, title, mode: access === 'edit' ? 'edit' : 'plan', access, status: 'working',
135
+ createdAt: now, queuedAt: now, startedAt: now, endedAt: null, agentId: null,
136
+ visitor: { ...agent, handle: link.peer }, from: `@${link.peer}`, guest: { link: link.id },
137
+ turns: 0, tools: 0, error: null, changes: null, applied: null, discarded: null, log: [],
138
+ sid: crypto.randomBytes(16).toString('hex'), dir, base, theirJob, written: 0, usedAt: now,
139
+ };
140
+ this.list.push(v);
141
+ this.log(v, `${agent.name} from @${link.peer} started on a copy of ${repo} (${access === 'edit' ? 'can edit the copy' : 'read only'}): ${title}`, 'system');
142
+ this.push(v);
143
+ this.office.post(`**Visitor** · ${agent.name} from @${link.peer} is working in a copy of ${repo}, ${access === 'edit' ? 'and can edit it. Nothing reaches the real folder until you apply it.' : 'read only.'} “${title}”`, { jobId: id });
144
+ return { sid: v.sid, access };
145
+ }
146
+
147
+ // The visit a request names, if it's this guest's and still open.
148
+ session(link, sid) {
149
+ const v = this.list.find((x) => x.guest.link === link.id && x.sid && x.sid === String(sid ?? ''));
150
+ if (!v) throw fail(404, 'That work session has ended. Start the job again.');
151
+ if (v.status !== 'working') throw fail(410, v.status === 'stopped' ? 'The project’s owner stopped this. Stop work and say so.' : 'That work session has ended.');
152
+ v.usedAt = Date.now();
153
+ return v;
154
+ }
155
+
156
+ async fs(link, args, policy) {
157
+ const v = this.session(link, args.sid);
158
+ if (policy && !policy.repos.includes(v.repo)) {
159
+ await this.end(v, 'stopped', '', 'Stopped: that project isn’t shared with them any more.');
160
+ throw fail(403, 'That project isn’t shared with you any more.');
161
+ }
162
+ if (policy?.theirAgents === 'read' && v.access === 'edit') {
163
+ v.access = 'read';
164
+ this.log(v, 'The owner made this read only.', 'system');
165
+ }
166
+ const op = String(args.op ?? '');
167
+ v.tools += 1;
168
+ try {
169
+ const text = await this.run(v, op, args);
170
+ return { text };
171
+ } catch (err) {
172
+ this.log(v, `${op} refused: ${err.message}`, 'error');
173
+ throw err.code ? err : fail(400, err.message);
174
+ } finally {
175
+ this.save();
176
+ }
177
+ }
178
+
179
+ async run(v, op, args) {
180
+ const rel = typeof args.path === 'string' ? args.path : '';
181
+ switch (op) {
182
+ case 'list': {
183
+ const { abs, shown } = this.resolve(v, rel || '.');
184
+ const depth = Math.min(Math.max(Number.isInteger(args.depth) ? args.depth : 1, 1), 4);
185
+ const out = [];
186
+ this.walk(v, abs, depth, out);
187
+ this.log(v, `Listed ${shown}`);
188
+ if (!out.length) return '(empty)';
189
+ return out.slice(0, MAX_LIST).join('\n') + (out.length > MAX_LIST ? `\n… ${out.length - MAX_LIST} more. List a subfolder.` : '');
190
+ }
191
+ case 'read': {
192
+ const { abs, shown } = this.resolve(v, rel);
193
+ const st = this.stat(abs, shown);
194
+ if (!st.isFile()) throw fail(400, `${shown} is not a file.`);
195
+ if (st.size > MAX_FILE) throw fail(413, `${shown} is ${Math.round(st.size / 1024)} KB, too big to read here.`);
196
+ const buf = fs.readFileSync(abs);
197
+ if (buf.subarray(0, 8000).includes(0)) return `${shown} is a binary file (${st.size} bytes).`;
198
+ const lines = buf.toString('utf8').split('\n');
199
+ const from = Math.max(Number.isInteger(args.offset) ? args.offset : 1, 1);
200
+ const want = Math.min(Math.max(Number.isInteger(args.limit) ? args.limit : 2000, 1), 2000);
201
+ let out = '', i = from - 1;
202
+ for (; i < lines.length && i < from - 1 + want; i++) {
203
+ const row = `${String(i + 1).padStart(6)}\t${lines[i].length > 2000 ? `${lines[i].slice(0, 2000)}…` : lines[i]}\n`;
204
+ if (out.length + row.length > MAX_READ) break;
205
+ out += row;
206
+ }
207
+ this.log(v, `Read ${shown}`);
208
+ if (i < lines.length) out += `… ${lines.length - i} more lines. Read on with offset ${i + 1}.`;
209
+ return out || `(${shown} is empty${from > 1 ? ' past that line' : ''})`;
210
+ }
211
+ case 'search': {
212
+ const pattern = String(args.pattern ?? '');
213
+ if (!pattern || pattern.length > 500) throw fail(400, 'Give a pattern to search for.');
214
+ const { abs, shown } = this.resolve(v, rel || '.');
215
+ const glob = typeof args.glob === 'string' && args.glob && !args.glob.includes('\0') ? ['--include', args.glob.slice(0, 200)] : [];
216
+ const out = await this.grep(v, ['-rnIE', '--exclude-dir=.git', ...glob, '-e', pattern, '--', path.relative(v.dir, abs) || '.']);
217
+ this.log(v, `Searched ${shown} for “${pattern.slice(0, 80)}”`);
218
+ return out || 'No matches.';
219
+ }
220
+ case 'find': {
221
+ const pattern = String(args.pattern ?? '');
222
+ if (!pattern || pattern.length > 200) throw fail(400, 'Give a file name pattern, like **/*.test.js.');
223
+ const re = globRe(pattern);
224
+ const out = [];
225
+ this.walk(v, v.dir, 12, out, true);
226
+ const hits = out.filter((p) => re.test(p));
227
+ this.log(v, `Looked for ${pattern}`);
228
+ if (!hits.length) return 'No files match.';
229
+ return hits.slice(0, MAX_LIST).join('\n') + (hits.length > MAX_LIST ? `\n… ${hits.length - MAX_LIST} more.` : '');
230
+ }
231
+ case 'write': case 'edit': {
232
+ if (v.access !== 'edit') throw fail(403, 'This project is shared read only. You can’t change files in it.');
233
+ const { abs, shown } = this.resolve(v, rel, true);
234
+ let before = null;
235
+ try { const st = fs.lstatSync(abs); if (!st.isFile()) throw fail(400, `${shown} is not a file.`); before = fs.readFileSync(abs, 'utf8'); } catch (err) { if (err.code !== 'ENOENT') throw err; }
236
+ let after;
237
+ if (op === 'write') {
238
+ const content = String(args.content ?? '');
239
+ if (Buffer.byteLength(content) > MAX_WRITE) throw fail(413, `Write at most ${MAX_WRITE / 1024} KB at a time. Write the first part, then add the rest with append.`);
240
+ after = args.append === true ? (before ?? '') + content : content;
241
+ } else {
242
+ if (before === null) throw fail(404, `${shown} doesn’t exist. Create it with files_write.`);
243
+ const old = String(args.old ?? ''), now = String(args.new ?? '');
244
+ if (!old) throw fail(400, 'Say which text to replace.');
245
+ if (Buffer.byteLength(now) > MAX_WRITE) throw fail(413, `Replace at most ${MAX_WRITE / 1024} KB at a time.`);
246
+ const n = before.split(old).length - 1;
247
+ if (!n) throw fail(400, `That text isn’t in ${shown}. Read the file again and copy it exactly.`);
248
+ if (n > 1 && args.all !== true) throw fail(400, `That text is in ${shown} ${n} times. Add more around it so it’s unique, or set all.`);
249
+ after = args.all === true ? before.split(old).join(now) : before.replace(old, () => now);
250
+ }
251
+ const size = Buffer.byteLength(after);
252
+ if (size > MAX_FILE) throw fail(413, `${shown} would be over ${MAX_FILE / 1024 / 1024} MB.`);
253
+ if (v.written + size > MAX_WRITTEN) throw fail(413, 'This visit has written as much as it may.');
254
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
255
+ fs.writeFileSync(abs, after);
256
+ v.written += size;
257
+ this.log(v, `${before === null ? 'Created' : 'Changed'} ${shown}`);
258
+ return `${before === null ? 'Created' : 'Updated'} ${shown}.`;
259
+ }
260
+ default:
261
+ throw fail(400, 'Unknown file operation.');
262
+ }
263
+ }
264
+
265
+ // A path the agent named, as an absolute path inside the copy, or a refusal.
266
+ // `forWrite` lets the last parts not exist yet.
267
+ resolve(v, rel, forWrite = false) {
268
+ if (typeof rel !== 'string' || rel.includes('\0') || rel.length > 1000) throw fail(400, 'That isn’t a path.');
269
+ const clean = rel.replace(/\\/g, '/').replace(/^\/+/, '');
270
+ const abs = path.resolve(v.dir, clean || '.');
271
+ const inside = (p) => p === v.dir || p.startsWith(v.dir + path.sep);
272
+ if (!inside(abs)) throw fail(403, 'Paths stay inside the project.');
273
+ const shown = path.relative(v.dir, abs) || '.';
274
+ for (const part of shown.split(path.sep)) {
275
+ if (part.toLowerCase() === '.git') throw fail(403, 'The .git folder is off limits.');
276
+ if (SECRET.some((re) => re.test(part))) throw fail(403, `${shown} looks like a secret, so it isn’t shared.`);
277
+ }
278
+ // Symlinks: what the path really points at must be inside the copy too.
279
+ const realRoot = fs.realpathSync(v.dir);
280
+ let probe = abs;
281
+ while (!fs.existsSync(probe)) {
282
+ if (!forWrite) throw fail(404, `${shown} doesn’t exist.`);
283
+ probe = path.dirname(probe);
284
+ }
285
+ const real = fs.realpathSync(probe);
286
+ if (real !== realRoot && !real.startsWith(realRoot + path.sep)) throw fail(403, 'That path leads outside the project.');
287
+ if (forWrite && probe === abs && fs.lstatSync(abs).isSymbolicLink()) throw fail(403, 'Links can’t be written through.');
288
+ return { abs, shown };
289
+ }
290
+
291
+ stat(abs, shown) {
292
+ try { return fs.statSync(abs); } catch { throw fail(404, `${shown} doesn’t exist.`); }
293
+ }
294
+
295
+ // Paths under `abs`, relative to the copy, folders with a trailing slash.
296
+ // Links are listed but not followed; .git and secrets are left out.
297
+ walk(v, abs, depth, out, filesOnly = false) {
298
+ if (out.length > 20_000) return;
299
+ let entries;
300
+ try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch { return; }
301
+ entries.sort((a, b) => a.name.localeCompare(b.name));
302
+ for (const e of entries) {
303
+ if (e.name.toLowerCase() === '.git' || SECRET.some((re) => re.test(e.name))) continue;
304
+ const rel = path.relative(v.dir, path.join(abs, e.name)).split(path.sep).join('/');
305
+ if (e.isDirectory()) {
306
+ if (!filesOnly) out.push(`${rel}/`);
307
+ if (depth > 1) this.walk(v, path.join(abs, e.name), depth - 1, out, filesOnly);
308
+ } else out.push(rel);
309
+ }
310
+ }
311
+
312
+ // grep in the copy, with a deadline and a cap on what comes back. grep's
313
+ // matcher doesn't backtrack, so a pattern can't hang it the way it could a
314
+ // JavaScript RegExp.
315
+ grep(v, args) {
316
+ return new Promise((resolve, reject) => {
317
+ const child = spawn('grep', args, { cwd: v.dir, stdio: ['ignore', 'pipe', 'pipe'] });
318
+ let out = '', cut = false, errOut = '';
319
+ const timer = setTimeout(() => { cut = true; child.kill('SIGKILL'); }, SEARCH_MS);
320
+ child.stdout.on('data', (d) => {
321
+ if (out.length >= MAX_READ) return;
322
+ out += d;
323
+ if (out.length >= MAX_READ) { cut = true; child.kill('SIGKILL'); }
324
+ });
325
+ child.stderr.on('data', (d) => { errOut += d; });
326
+ child.on('error', (err) => { clearTimeout(timer); reject(err); });
327
+ child.on('close', (code) => {
328
+ clearTimeout(timer);
329
+ if (code === 2 && !out && !cut) return reject(fail(400, `Search failed: ${errOut.trim().slice(0, 200) || 'bad pattern'}`));
330
+ // Matches in secret-looking files don't go out either.
331
+ const rows = out.slice(0, MAX_READ).split('\n').filter((r) => r && !r.split(':')[0].split('/').some((p) => SECRET.some((re) => re.test(p))));
332
+ resolve(rows.join('\n') + (cut ? '\n… more matches cut. Narrow the search.' : ''));
333
+ });
334
+ });
335
+ }
336
+
337
+ // The guest's job ended (or the agent finished a turn and the job closed).
338
+ async close(link, args) {
339
+ const v = this.session(link, args.sid);
340
+ await this.end(v, ['done', 'failed', 'stopped'].includes(args.status) ? args.status : 'done', typeof args.summary === 'string' ? args.summary.slice(0, 2000) : '');
341
+ return { ok: true };
342
+ }
343
+
344
+ async end(v, status, summary = '', error = null) {
345
+ if (v.status !== 'working') return;
346
+ Object.assign(v, { status, endedAt: Date.now(), error });
347
+ if (summary) this.log(v, summary, 'text');
348
+ await this.record(v);
349
+ const n = v.changes?.count || 0;
350
+ const what = v.access !== 'edit' ? '' : n ? ` ${n} file${n === 1 ? '' : 's'} changed in the copy. Review them on the job and apply or discard.` : ' Nothing changed.';
351
+ this.log(v, `${{ done: 'Finished', failed: 'Hit a wall', stopped: 'Stopped' }[status] || 'Ended'}.${what}`, 'result', { ok: status === 'done' });
352
+ this.push(v);
353
+ if (status !== 'stopped' || error) {
354
+ this.office.post(`**Visitor** · ${v.visitor.name} from @${v.visitor.handle} ${status === 'done' ? 'finished' : status === 'failed' ? 'hit a wall on' : 'stopped'} “${v.title}” in ${v.repo}.${what}`, { jobId: v.id });
355
+ }
356
+ }
357
+
358
+ async record(v) {
359
+ if (!fs.existsSync(v.dir)) return;
360
+ const src = this.office.repoPath(v.repo);
361
+ if (!src) return;
362
+ const end = await this.snapshot(v, src).catch(() => null);
363
+ if (end) v.changes = await summarize(src, v.base, end);
364
+ }
365
+
366
+ // The copy as a tree in the project's object store, from a throwaway index
367
+ // seeded with the base so it's quick.
368
+ async snapshot(v, src) {
369
+ const idx = tmpIndex();
370
+ try {
371
+ await git(src, ['read-tree', v.base], { GIT_INDEX_FILE: idx });
372
+ await git(src, [`--work-tree=${v.dir}`, 'add', '-A'], { GIT_INDEX_FILE: idx });
373
+ return (await git(src, ['write-tree'], { GIT_INDEX_FILE: idx })).trim();
374
+ } finally {
375
+ fs.rmSync(idx, { force: true });
376
+ }
377
+ }
378
+
379
+ // ---------- the boss ----------
380
+
381
+ stop(v, why = 'Stopped by you.') {
382
+ if (v.status !== 'working') return;
383
+ this.office.feed(v.guest.link, { t: 'visit', job: v.theirJob, ended: 'stopped' });
384
+ return this.end(v, 'stopped', '', why);
385
+ }
386
+
387
+ async diff(v, file) {
388
+ const src = this.office.repoPath(v.repo);
389
+ if (!src) throw fail(404, 'That project folder is gone.');
390
+ return fileDiff(src, v.changes, file);
391
+ }
392
+
393
+ // The copy's changes onto the real folder. git apply takes all of it or
394
+ // none, so a project that has moved on underneath gets a clear refusal.
395
+ async apply(v) {
396
+ if (v.status === 'working') throw fail(409, 'Wait for them to finish, or stop them first.');
397
+ if (v.applied || v.discarded) throw fail(409, 'Already dealt with.');
398
+ if (!v.changes?.count) throw fail(400, 'Nothing changed.');
399
+ const src = this.office.repoPath(v.repo);
400
+ if (!src) throw fail(404, 'That project folder is gone.');
401
+ const patch = await git(src, ['diff', '--binary', '--no-renames', '--no-color', '--no-ext-diff', '--no-textconv', v.changes.base, v.changes.end]);
402
+ try {
403
+ await git(src, ['apply', '--binary', '--whitespace=nowarn', '-'], {}, patch);
404
+ } catch (err) {
405
+ throw fail(409, `Those changes don’t apply cleanly: ${String(err.stderr || err.message).split('\n')[0].slice(0, 200)}. The project has moved on since; nothing was changed.`);
406
+ }
407
+ v.applied = Date.now();
408
+ this.log(v, `Applied to ${v.repo}.`, 'system');
409
+ this.drop(v);
410
+ this.push(v);
411
+ return { ok: true };
412
+ }
413
+
414
+ discard(v) {
415
+ if (v.status === 'working') throw fail(409, 'Stop them first.');
416
+ if (v.applied || v.discarded) throw fail(409, 'Already dealt with.');
417
+ v.discarded = Date.now();
418
+ this.log(v, 'Thrown away.', 'system');
419
+ this.drop(v);
420
+ this.push(v);
421
+ return { ok: true };
422
+ }
423
+
424
+ // Clear finished: visits that are dealt with go. One with changes still
425
+ // waiting for review stays, so a click can't throw work away unseen.
426
+ clear() {
427
+ const done = (v) => v.status !== 'working' && (v.applied || v.discarded || !v.changes?.count || v.access !== 'edit');
428
+ for (const v of this.list) if (done(v)) this.drop(v);
429
+ this.list = this.list.filter((v) => !done(v));
430
+ this.save();
431
+ }
432
+
433
+ drop(v) {
434
+ fs.rmSync(v.dir, { recursive: true, force: true });
435
+ }
436
+
437
+ // ---------- housekeeping ----------
438
+
439
+ // Links that went: their visits stop at once.
440
+ linksChanged(live) {
441
+ for (const v of this.list) if (v.status === 'working' && !live.has(v.guest.link)) this.end(v, 'stopped', '', 'Stopped: the guest was unlinked.').catch(() => {});
442
+ }
443
+
444
+ // The boss changed a guest's limits: visits they no longer allow stop now,
445
+ // not at the guest's next call.
446
+ policyChanged(linkId, policy) {
447
+ for (const v of this.list) {
448
+ if (v.status !== 'working' || v.guest.link !== linkId) continue;
449
+ if (!['read', 'edit'].includes(policy?.theirAgents)) this.stop(v, 'Stopped: you turned their agents off.')?.catch(() => {});
450
+ else if (!policy.repos.includes(v.repo)) this.stop(v, 'Stopped: that project isn’t shared with them any more.')?.catch(() => {});
451
+ }
452
+ }
453
+
454
+ // A visit nobody has touched in a while is closed, so a guest office that
455
+ // went away mid-job doesn't leave one open for ever.
456
+ sweep() {
457
+ const now = Date.now();
458
+ for (const v of this.list) if (v.status === 'working' && now - (v.usedAt || v.startedAt) > IDLE_MS) this.end(v, 'stopped', '', 'Closed: nothing happened in it for an hour.').catch(() => {});
459
+ }
460
+ }
461
+
462
+ // A glob (`*`, `**`, `?`) as a RegExp over slash paths. Built by hand from
463
+ // escaped literals, so it can't backtrack badly.
464
+ export function globRe(glob) {
465
+ let re = '';
466
+ for (let i = 0; i < glob.length; i++) {
467
+ const c = glob[i];
468
+ if (c === '*' && glob[i + 1] === '*') { re += glob[i + 2] === '/' ? '(?:[^/]*/)*' : '.*'; i += glob[i + 2] === '/' ? 2 : 1; }
469
+ else if (c === '*') re += '[^/]*';
470
+ else if (c === '?') re += '[^/]';
471
+ else re += c.replace(/[.+^${}()|[\]\\]/g, '\\$&');
472
+ }
473
+ // A bare name matches at any depth, as people expect from `*.js`.
474
+ return new RegExp(glob.includes('/') ? `^${re}$` : `(?:^|/)${re}$`);
475
+ }
@@ -1 +0,0 @@
1
- @font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-cyrillic-ext-wght-normal-B2xhLi22.woff2) format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-cyrillic-wght-normal-CMZtQduZ.woff2) format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-greek-ext-wght-normal-CGAr0uHJ.woff2) format("woff2-variations");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-greek-wght-normal-CaVNZxsx.woff2) format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2) format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-latin-ext-wght-normal-CFHvXkgd.woff2) format("woff2-variations");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/assets/inter-latin-wght-normal-C2S99t-D.woff2) format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(data:font/woff2;base64,d09GMgABAAAAAASIABAAAAAACQQAAAQuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbHhwoBmA/U1RBVF4AdBEICoRkhAALIAABNgIkAzoEIAWFAAeBFAwHG2oHKJ6DceNW4lJEFHn9tEOE37LNsvkjHr7f739rn3u+OSrSxLQytE91Dw2PYtNVq3Qaw/vJ7fUBMdOj/u2ImB98cT5WUx9F13ZKt06mU7tG1sAYcM26yCRX4f/0904bmwq8hwkUUMTRX61pa9C3xlpAGYw1vOu8C2SBZZFlld7DAW82RJWmwYMI1AJCKBNCIASqdZGFmtPSNQRhMpR0EKjWTwm6z6sJ+4jqhxjovTinVdRpZaQjaQzAYjI8NgAKDJCokgunCn9oUQE8VZd/F//+v6P4F1U9gLr58yNI/dJX9BAshEoJbTDogz7ocKoUsPvES8UK/aQIUQgpGgECBVLkOEV6iASTYgNAPKdNXayhDPL7IJuAZnEYSZ0eOLc9i5Rv5/+lEi3sW/kfnCf/+fTe0sxS7CKLx5erRJkKOixEqQaoqrvx5HN4iz4VhX0gS6DjFIWUzn/fIXJRVFY1NFRVNVeFt+SYmrK6vCU6eJsnKnJj5HTESMTQZOTI3Y/tzr3rUcOsiRy/ciP50s3Mycgxmhz9GMXhjzVH30ut796NHP0UUa31/Zoa6vCH6iPvdIM0IRANaT60FablmZlZd+UKNFy9NrvnFGxb5NC2CRxR0/rIyF/WNn+35sv9r+tHVhkseuWGKkCPPh96+GXt8Lc/Sh24ujfjf+tNw1lZWreiPuHb/PSpyjdv2rf/++ZE5TSKjFdZxYsBvr1sEHl5STuHZqYR7jGYGvGBBdjcaB5bODY1GAxMjsd0wDMIzXAXODRd74DxNMtO4YuYHclgLQRzp9KlUmxptCwv9bYgvWGD2xBW7r9413fdJu+mxzvzAUYeeONQvZP4kgfqZle4jhH/MWh+UbxgtJdrLdXLymNL53PRU0D3Q6FcXTfGqGcul6vrqhgLGAVSv+IVIfZOHpu+mlLqSlnj903j8mazXDsba/wbxhS/4Oubh1c5/uXDgt4j5KtnGEy5BIJHo0ur+jD+r2qCkuP1aRVM8EpUoRYSJyqNRA38uwDvHBNo0AejO2Z+ZYLLal1QiFyFh1EDQOgSCSCpjwwKU8yDbIjroEwne0G5Qca49cYp4AlqtQNZfWTQoFb1tRpFYAmRD+HXTkV4uQhG2bg4OKmyoypAsUGtWgQXyFxYrkpODeUxvGqIZA61erXo0sFG1UBGuPgpuon8RNUG2DhovAjZCBuZwtXrU3mQGki9+pm0eVZAXoWxLWTQoYHBPrJR4WunNxIFbCHfkTyPK+sqxjxDmGArZvf79JGJ3GwEqjwc7a7NIrl/7bJ7Nzu4+n1Ow6shEPnUrtVo7cnd5Wi5qCUbBIQbNKCP95FMCSZylEV5VCiiMqqiOmrKZ/I/C0gI1fyuulVcM2E1r4MhtcV/fbCD+HwEvVzH1dGNAzFE0FTbKzyQz3gootrNbN2a4PuG0j0JOgE=) format("woff2"),url(data:font/woff;base64,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) format("woff");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2) format("woff2"),url(/assets/jetbrains-mono-cyrillic-400-normal-CIEtRijs.woff) format("woff");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2) format("woff2"),url(/assets/jetbrains-mono-greek-400-normal-CNojz0t3.woff) format("woff");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(data:font/woff2;base64,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) format("woff2"),url(/assets/jetbrains-mono-vietnamese-400-normal-BeU3T8wf.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2) format("woff2"),url(/assets/jetbrains-mono-latin-ext-400-normal-DHMOLxAi.woff) format("woff");unicode-range:U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-display:swap;font-weight:400;src:url(/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2) format("woff2"),url(/assets/jetbrains-mono-latin-400-normal-B11XCQ5g.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{--ink: #1d1b18;--ink-2: #5d574e;--ink-3: #958d81;--paper: #fbf9f5;--paper-2: #f4f0e9;--line: rgba(29, 27, 24, .09);--good: #2f7d4f;--bad: #b4412f;--work: #2f6fae;--wait: #b9861f;--accent: var(--work);--surface: #fff;--glass: color-mix(in srgb, var(--paper) 88%, transparent);--pane: color-mix(in srgb, var(--paper) 58%, transparent);--pane-edge: color-mix(in srgb, #fff 55%, var(--line));--t1: rgba(29, 27, 24, .05);--t2: rgba(29, 27, 24, .07);--t3: rgba(29, 27, 24, .1);--good-bg: #dcefe2;--good-fg: #25653f;--bad-bg: #f7e0db;--bad-fg: #9a3424;--work-bg: #dde9f6;--work-fg: #24598f;--wait-bg: #fbf0d4;--wait-fg: #8a6312;--view-1: #f6f2ea;--view-2: #ece5da;--view-3: #ddd4c6;--sans: "Inter Variable", Inter, system-ui, sans-serif;--mono: "JetBrains Mono", ui-monospace, monospace;--ease: cubic-bezier(.2, .8, .2, 1);color-scheme:light}html{--h: 80}html[data-theme=ocean]{--h: 235}html[data-theme=forest]{--h: 150}html[data-theme=sunset]{--h: 35}html[data-theme=lavender]{--h: 300}html:is([data-theme=ocean],[data-theme=forest],[data-theme=sunset],[data-theme=lavender]){--ink: oklch(23% .03 var(--h));--ink-2: oklch(44% .03 var(--h));--ink-3: oklch(62% .025 var(--h));--paper: oklch(98.4% .008 var(--h));--paper-2: oklch(95.6% .016 var(--h));--accent: oklch(52% .16 var(--h));--line: color-mix(in srgb, var(--ink) 10%, transparent);--t1: color-mix(in srgb, var(--ink) 5%, transparent);--t2: color-mix(in srgb, var(--ink) 7%, transparent);--t3: color-mix(in srgb, var(--ink) 10%, transparent);--view-1: oklch(96.5% .015 var(--h));--view-2: oklch(92% .02 var(--h));--view-3: oklch(86% .025 var(--h))}html[data-mode=dark]{--ink: oklch(94% .012 var(--h));--ink-2: oklch(78% .016 var(--h));--ink-3: oklch(62% .016 var(--h));--paper: oklch(20% .012 var(--h));--paper-2: oklch(25% .014 var(--h));--surface: oklch(24% .014 var(--h));--pane: color-mix(in srgb, var(--paper) 52%, transparent);--pane-edge: color-mix(in srgb, #fff 14%, transparent);--line: color-mix(in srgb, var(--ink) 12%, transparent);--t1: color-mix(in srgb, var(--ink) 6%, transparent);--t2: color-mix(in srgb, var(--ink) 9%, transparent);--t3: color-mix(in srgb, var(--ink) 13%, transparent);--good: #5fbf85;--bad: #e8806c;--work: #5b9ad8;--wait: #d9a43f;--good-bg: color-mix(in srgb, var(--good) 18%, var(--paper));--good-fg: var(--good);--bad-bg: color-mix(in srgb, var(--bad) 18%, var(--paper));--bad-fg: var(--bad);--work-bg: color-mix(in srgb, var(--work) 18%, var(--paper));--work-fg: #8dbbea;--wait-bg: color-mix(in srgb, var(--wait) 18%, var(--paper));--wait-fg: var(--wait);--view-1: oklch(27% .014 var(--h));--view-2: oklch(22% .013 var(--h));--view-3: oklch(17% .012 var(--h));color-scheme:dark}html[data-mode=dark]:is([data-theme=ocean],[data-theme=forest],[data-theme=sunset],[data-theme=lavender]){--accent: oklch(74% .13 var(--h))}*{box-sizing:border-box}[hidden]{display:none!important}html,body{margin:0;height:100%;overflow:hidden;background:var(--paper)}body{font-family:var(--sans);color:var(--ink);font-size:14px;line-height:1.45;-webkit-font-smoothing:antialiased;font-feature-settings:"cv11","ss01"}button,input,textarea,select{font:inherit;color:inherit}.app{display:grid;grid-template-columns:var(--side-w, 480px) minmax(0,1fr) auto;height:100vh;height:100dvh;background:radial-gradient(40% 36% at 8% 14%,color-mix(in srgb,var(--accent) 22%,transparent) 0%,transparent 100%),radial-gradient(38% 34% at 94% 86%,color-mix(in srgb,var(--accent) 16%,transparent) 0%,transparent 100%),radial-gradient(120% 90% at 50% 38%,var(--view-1) 0%,var(--view-2) 58%,var(--view-3) 100%)}.view{position:relative;overflow:hidden;min-width:0;container-type:inline-size}#stage{position:absolute;top:0;right:0;bottom:0;left:0}#stage canvas{display:block;width:100%;height:100%;touch-action:none}#labels{position:absolute;top:0;right:0;bottom:0;left:0;pointer-events:none;overflow:hidden}.side-grip{position:absolute;z-index:30;top:0;bottom:0;right:-4px;width:9px;cursor:col-resize;touch-action:none}.side-grip:after{content:"";position:absolute;top:0;bottom:0;left:4px;width:2px;background:var(--accent);opacity:0;transition:opacity .15s}.side-grip:hover:after,.side-grip:focus-visible:after,.app.resizing .side-grip:after{opacity:.6}.side-grip:focus-visible{outline:none}.app.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.app.resizing #stage{pointer-events:none}.side{position:relative;zoom:var(--side-zoom, 1);display:flex;flex-direction:column;min-height:0;background:var(--pane);border-right:1px solid var(--pane-edge);-webkit-backdrop-filter:blur(24px) saturate(1.5);backdrop-filter:blur(24px) saturate(1.5)}.brand{padding:20px 20px 14px;position:relative}.title-end{display:flex;align-items:center;gap:6px}.theme-btn{all:unset;cursor:pointer;width:26px;height:26px;display:grid;place-items:center;border-radius:8px;color:var(--ink-3);transition:background .15s,color .15s}.theme-btn:hover,.theme-btn[aria-expanded=true]{background:var(--t2);color:var(--ink)}.theme-btn:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.theme-btn svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.theme-pop{position:absolute;z-index:40;top:52px;right:14px;width:252px;padding:10px 12px 12px;background:var(--paper);border:1px solid var(--line);border-radius:14px;box-shadow:0 10px 30px #00000029}.theme-pop[hidden]{display:none}.theme-k{margin:4px 2px 6px;font-size:11px;font-weight:620;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-3)}.theme-k:not(:first-child){margin-top:12px}.theme-swatches{display:grid;grid-template-columns:repeat(5,1fr);gap:4px}.theme-swatches button{all:unset;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px 0 5px;border-radius:10px;font-size:10.5px;font-weight:580;color:var(--ink-2)}.theme-swatches button:hover{background:var(--t1)}.theme-swatches button:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.theme-swatches button[aria-checked=true]{background:var(--t2);color:var(--ink)}.theme-swatches i{width:24px;height:24px;border-radius:50%;box-shadow:inset 0 0 0 1px #0000001f;background:linear-gradient(135deg,oklch(94% .03 var(--sh)) 50%,oklch(55% .15 var(--sh)) 50%)}.theme-swatches [data-t=paper]{--sh: 80}.theme-swatches [data-t=ocean]{--sh: 235}.theme-swatches [data-t=forest]{--sh: 150}.theme-swatches [data-t=sunset]{--sh: 35}.theme-swatches [data-t=lavender]{--sh: 300}.theme-modes{display:grid;grid-template-columns:repeat(3,1fr);gap:2px;padding:2px;border-radius:10px;background:var(--t1)}.theme-modes button{all:unset;cursor:pointer;text-align:center;padding:5px 0;border-radius:8px;font-size:12.5px;font-weight:580;color:var(--ink-2)}.theme-modes button:hover{color:var(--ink)}.theme-modes button:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.theme-modes button[aria-checked=true]{background:var(--paper);color:var(--ink);box-shadow:0 1px 2px #0000001f}.acct-pop{width:280px}.acct-modes{grid-template-columns:1fr 1fr}.acct-note{margin:10px 2px 0;font-size:12px;line-height:1.45;color:var(--ink-2)}.acct-err{color:var(--bad)}.acct-fine{font-size:11px;color:var(--ink-3)}.acct-form{display:flex;gap:6px;margin-top:10px}.acct-form input{flex:1;min-width:0;padding:6px 9px;border-radius:8px;border:1px solid var(--line);background:var(--paper-2);color:var(--ink);font:inherit;font-size:12.5px}.acct-form input:focus-visible{outline:2px solid var(--accent);outline-offset:-1px}.acct-forget{all:unset;cursor:pointer;margin:8px 2px 0;font-size:12px;color:var(--ink-3);text-decoration:underline}.acct-forget:hover{color:var(--ink)}.lb-pop{width:300px}.lb-me{margin:6px 2px 0;font-size:12.5px;line-height:1.4;color:var(--ink-2)}.lb-me strong{color:var(--ink);font-weight:650}.lb-tabs{margin-top:10px}.lb-period{grid-template-columns:1fr 1fr}.lb-metric{margin-top:6px}.lb-board{margin-top:8px}.lb-list{list-style:none;margin:0;padding:0;max-height:232px;overflow-y:auto}.lb-row{display:grid;grid-template-columns:24px 1fr auto;gap:6px;align-items:baseline;padding:5px 6px;border-radius:8px;font-size:12.5px;color:var(--ink-2)}.lb-row:nth-child(-n+3) .lb-rank{color:var(--accent);font-weight:700}.lb-rank{color:var(--ink-3);font-variant-numeric:tabular-nums}.lb-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink)}.lb-val{font-variant-numeric:tabular-nums;font-weight:600;color:var(--ink)}.lb-you{background:var(--t2)}.lb-share{display:flex;align-items:center;gap:8px;margin:10px 2px 0;font-size:12.5px;color:var(--ink);cursor:pointer}.lb-share input,.link-repos input,.link-warn input{accent-color:var(--accent);margin:0}.link-pop{width:320px;max-height:calc(100dvh - 80px);overflow-y:auto;overscroll-behavior:contain}.link-actions{display:grid;gap:8px;margin-top:10px}.link-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.link-actions .link-row .ghost{flex:1;background:var(--t1)}.link-actions .link-row .ghost:hover{background:var(--t2)}.link-redeem{margin-top:0}.link-redeem input{font-family:var(--mono);letter-spacing:.04em}.link-redeem input::placeholder{font-family:inherit;letter-spacing:0}.link-code,.link-guest{margin-top:12px;padding:8px 10px 10px;border-radius:12px;background:var(--t1)}.link-code .theme-k,.link-guest .theme-k{margin-top:2px}.link-code-row{display:flex;align-items:center;gap:8px}.link-code-v{flex:1;font-family:var(--mono);font-size:22px;font-weight:600;letter-spacing:.08em;color:var(--ink);font-variant-numeric:tabular-nums;-webkit-user-select:all;user-select:all}.link-repos{border:0;margin:10px 0 0;padding:0;max-height:168px;overflow-y:auto}.link-repos legend,.link-k{padding:0;margin:10px 2px 6px;font-size:12px;font-weight:600;color:var(--ink-2)}.link-repos legend{margin-top:0}.link-repos label{display:flex;align-items:center;gap:8px;padding:4px;border-radius:6px;font-size:12.5px;color:var(--ink);cursor:pointer}.link-repos label:hover{background:var(--t2)}.link-repos span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.link-cap button:disabled{opacity:.45;cursor:default}.link-warn{margin-top:8px;padding:8px 10px;border-radius:10px;font-size:12px;line-height:1.45;background:color-mix(in srgb,var(--bad) 10%,transparent);border:1px solid color-mix(in srgb,var(--bad) 30%,transparent)}.link-warn p{margin:0 0 6px;color:var(--ink)}.link-warn label{display:flex;align-items:center;gap:8px;font-weight:600;color:var(--bad);cursor:pointer}.link-tokens{display:flex;align-items:center;gap:8px}.link-tokens span:first-child{flex:1}.link-tokens input{width:108px;padding:5px 8px;border-radius:8px;border:1px solid var(--line);background:var(--paper-2);color:var(--ink);font:inherit;font-size:12.5px;font-variant-numeric:tabular-nums}.link-tokens input:focus-visible{outline:2px solid var(--accent);outline-offset:-1px}.link-guest>.link-row{margin-top:12px}.link-list{max-height:260px}.link-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 6px;border-radius:8px;font-size:12.5px}.link-item+.link-item{border-top:1px solid var(--line)}.link-who{display:grid;min-width:0}.link-who strong{color:var(--ink);font-weight:650;overflow:hidden;text-overflow:ellipsis}.link-who span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.link-item .acct-forget{margin:0}.visit-bar{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:0 14px 8px;padding:7px 10px;border-radius:10px;font-size:12.5px;color:var(--ink);background:color-mix(in srgb,var(--accent) 16%,transparent);border:1px solid color-mix(in srgb,var(--accent) 35%,transparent)}.visit-bar a{color:var(--ink-2);white-space:nowrap}.visit-bar{flex-wrap:wrap}.visit-bar.visit-off{background:color-mix(in srgb,var(--bad) 10%,transparent);border-color:color-mix(in srgb,var(--bad) 30%,transparent)}.app.visit-ended #detail .d-foot{display:none}.job .m .from{font-weight:600;color:var(--accent)}.visit-note{flex-basis:100%;margin:0;font-size:12px;color:var(--bad)}.app.visiting :is(#attach,#mic,#channels,#detail .reply .attach),.app.visiting :is(#lbBtn,#linkBtn,#acctBtn,#lbPop,#linkPop,#acctPop,#clear,#standup,#away),.app.visiting :is(.tabs [data-tab=team],.mnav [data-m=team],#teamPane){display:none!important}.brand .title{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}.title-start{display:flex;align-items:center;gap:10px;min-width:0}.brand h1{margin:0;font-size:20px;font-weight:720;letter-spacing:-.03em;white-space:nowrap;background:linear-gradient(120deg,var(--ink) 40%,color-mix(in srgb,var(--accent) 75%,var(--ink)) 100%);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}.lv svg,.meter .bar{display:block;overflow:visible;transform:rotate(-90deg)}.trk,.arc{fill:none;stroke-width:3}.trk{stroke:var(--t3)}.arc{stroke:var(--ring, var(--bar, var(--accent)));stroke-linecap:round;stroke-dasharray:100;stroke-dashoffset:calc(100 - max(var(--p, 0),.01));filter:drop-shadow(0 0 2px color-mix(in srgb,var(--ring, var(--bar, var(--accent))) 45%,transparent));transition:stroke-dashoffset .9s var(--ease),stroke .3s}.lv{--ring: var(--accent);position:relative;flex:none;width:34px;height:34px;display:grid;place-items:center;cursor:default}.lv svg{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%}.lv .trk,.lv .arc{stroke-width:2.6}.lv b{position:relative;display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:linear-gradient(145deg,color-mix(in srgb,var(--accent) 88%,#fff),color-mix(in srgb,var(--accent) 80%,#000));box-shadow:inset 0 1px #ffffff4d,0 2px 6px -2px color-mix(in srgb,var(--accent) 70%,transparent);color:#fff;font-size:11.5px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-.02em}.level,.gauges[hidden]{display:none}.brand h1{cursor:pointer}.fold{all:unset;cursor:pointer;flex:none;display:grid;place-items:center;width:24px;height:24px;border-radius:8px;color:var(--ink-3);transition:color .15s,background .15s}.fold:hover{color:var(--ink);background:var(--t2)}.fold:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.fold svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:transform .2s var(--ease)}.fold[aria-expanded=true] svg{transform:rotate(180deg)}@media (prefers-reduced-motion: reduce){.fold svg{transition:none}}.gauges{display:grid;grid-template-columns:repeat(auto-fit,minmax(0,1fr));padding:10px 0 2px;border-top:1px solid var(--line)}.usage,#load{display:contents}.usage[hidden],#load[hidden],.meter[hidden]{display:none}.meter{--bar: var(--work);display:grid;grid-template-columns:auto minmax(0,1fr);grid-template-areas:"ring v" "ring k";column-gap:8px;align-items:center;padding:0 10px;min-width:0;cursor:default;position:relative}#useSession{padding-left:0}.meter:not(#useSession):before{content:"";position:absolute;left:0;top:3px;bottom:3px;width:1px;background:var(--line)}.meter .bar{grid-area:ring;width:30px;height:30px}.meter .ico{fill:none;stroke:var(--ink-3);stroke-width:1.3;stroke-linecap:round;stroke-linejoin:round;transform:rotate(90deg);transform-origin:18px 18px}.meter.hot .ico{stroke:var(--bad)}.meter .k{grid-area:k;font-size:9.5px;font-weight:650;color:var(--ink-3);letter-spacing:.07em;text-transform:uppercase;line-height:1.2}.meter .v{grid-area:v;font-size:13.5px;font-weight:700;color:var(--ink);letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1.2;white-space:nowrap}.meter .r{display:none}.meter.warm{--bar: var(--wait)}.meter.hot{--bar: var(--bad)}.meter.hot .v{color:var(--bad)}@media (prefers-reduced-motion: reduce){.arc{transition:none}}.u-err{grid-column:1 / -1;margin:0;font-size:12px;color:var(--ink-3)}.tabs{display:flex;gap:2px;padding:0 12px 8px;border-bottom:1px solid var(--line)}.tabs button{all:unset;cursor:pointer;padding:6px 10px;border-radius:8px;font-weight:600;font-size:13px;color:var(--ink-3);display:inline-flex;gap:6px;align-items:center;transition:color .15s,background .15s}.tabs button:hover{color:var(--ink)}.tabs button.on{color:var(--ink);background:var(--t2)}.tabs #jobCount:not(:empty){font-size:11px;background:var(--accent);color:var(--paper);border-radius:99px;padding:0 6px;line-height:17px}.pane{flex:1;min-height:0;display:flex;flex-direction:column}#jobsPane{padding:8px 12px 12px}.team{flex:1;overflow-y:auto;padding:8px 12px 12px;display:flex;flex-direction:column;gap:2px}.member{border-radius:10px;transition:background .15s}.member:hover,.member.open{background:var(--t1)}.member.later{opacity:.6}.member .head{all:unset;cursor:pointer;display:grid;grid-template-columns:10px 1fr;gap:1px 10px;width:100%;box-sizing:border-box;padding:9px 10px 4px}.member:not(.open) .head:only-child{padding-bottom:9px}.member .head:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;border-radius:10px}.member .head i{grid-row:1 / 3;margin-top:6px;width:8px;height:8px;border-radius:50%}.member .n{font-weight:600}.member .n em{font-style:normal;font-weight:500;font-size:11.5px;color:var(--ink-3);margin-left:4px}.member .r{font-size:12px;color:var(--ink-2)}.member .p{margin:0;padding:2px 10px 9px 30px;font-size:12.5px;color:var(--ink-3);line-height:1.45;text-wrap:pretty;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.member form{display:flex;flex-direction:column;gap:10px;padding:6px 10px 10px 30px}.member label{display:flex;flex-direction:column;gap:4px;font-size:11.5px;font-weight:600;color:var(--ink-3)}.member input,.member textarea{font-size:13.5px;font-weight:400;color:var(--ink);line-height:1.45;resize:vertical;border:1px solid var(--line);border-radius:8px;padding:7px 9px;background:var(--surface);outline:none}.member input:focus,.member textarea:focus{border-color:var(--ink-3)}.member .hint-t{margin:0;font-size:12px;color:var(--ink-3);text-wrap:pretty}.member .row{display:flex;justify-content:flex-end;gap:6px}.viewbar{align-self:flex-end;display:flex;gap:2px;margin:8px 12px 0;padding:2px;border-radius:9px;background:var(--t1)}.viewbar button{all:unset;cursor:pointer;display:grid;place-items:center;width:28px;height:24px;border-radius:7px;color:var(--ink-3);transition:background .15s,color .15s}.viewbar button:hover{color:var(--ink)}.viewbar button[aria-pressed=true]{color:var(--ink);background:var(--surface);box-shadow:0 1px 2px #00000014}.viewbar button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.viewbar svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}#teamPane,#projectsPane{display:flex;flex-direction:column}#teamPane[hidden],#projectsPane[hidden]{display:none}.pico{--s: 1em;display:inline-grid;place-items:center;flex:none;width:var(--s);height:var(--s);border-radius:28%;font-weight:700;line-height:1;background:hsl(var(--h) 50% 55% / .2);color:hsl(var(--h) 45% 34%);position:relative;overflow:hidden}.pico img{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;object-fit:contain;border-radius:inherit;background:var(--surface)}.pico:not(.has) img{opacity:0}.pico.has{color:transparent;background:none}.tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:6px}.tile{all:unset;cursor:pointer;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:2px;padding:10px 6px 8px;border-radius:12px;text-align:center;min-width:0;transition:background .15s,transform .15s var(--ease)}.tile:hover,.tile.open{background:var(--t1)}.tile:active{transform:scale(.97)}.tile:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.tile.later{opacity:.6}.tile .art{position:relative;margin-bottom:4px}.tile .pico{--s: 52px;font-size:22px}.tile .face{display:block;width:52px;height:52px;border-radius:50%}.tile .dot{position:absolute;right:-2px;bottom:-2px;width:11px;height:11px;border-radius:50%;background:var(--line);box-shadow:0 0 0 2px var(--paper)}.tile .n{max-width:100%;font-weight:600;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tile .c{max-width:100%;font-size:11.5px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tiles+.project,.tiles+.member{margin-top:8px}.project .note{margin:0;padding:12px 16px 2px;font-size:12.5px;font-weight:500;color:var(--ink-2);text-wrap:pretty}.team.icons .member.open form{padding-left:10px}@media (prefers-reduced-motion: reduce){.tile{transition:none}.tile:active{transform:none}}.msg .who .ptag{display:inline-flex;margin-left:2px}.msg .who .ptag .pico{--s: 15px;font-size:9px}.projects{flex:1;overflow-y:auto;padding:8px 12px 12px;display:flex;flex-direction:column;gap:2px}.project{border-radius:10px;transition:background .15s}.project:hover,.project.open{background:var(--t1)}.project .head{all:unset;cursor:pointer;display:grid;grid-template-columns:10px 1fr auto;gap:1px 10px;width:100%;box-sizing:border-box;padding:9px 10px}.project .head:focus-visible{outline:2px solid var(--accent);outline-offset:-2px;border-radius:10px}.project .dot{grid-row:1 / 3;margin-top:6px;width:8px;height:8px;border-radius:50%;background:var(--line)}.project .n{font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project .m{grid-column:2 / 4;font-size:12px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project .c{font-size:11.5px;color:var(--ink-3);align-self:center}.project .sub{display:flex;flex-direction:column;gap:2px;padding:6px 6px 8px}.project .sub .job{padding:8px 10px;align-items:start}.project .sub .job .dot{margin-top:5px}.project .sub .job .t{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;line-height:1.35;text-wrap:pretty;overflow-wrap:anywhere}.project .sub .job .m{margin-top:2px}.project .sub .job:hover{background:var(--t2)}.project .sub .none{margin:2px 8px 6px;font-size:12px;color:var(--ink-3)}.project .sub .ghost{align-self:stretch;display:flex;align-items:center;justify-content:center;gap:6px;font-size:12.5px;margin:6px 4px 0;padding:8px;border:1px dashed var(--line);border-radius:10px}.project .sub .ghost:hover{border-style:solid}.jobs{flex:1;overflow-y:auto;display:flex;flex-direction:column;gap:2px}.jgroup{display:flex;align-items:center;gap:6px;margin:14px 10px 4px;font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3)}.jgroup:first-child{margin-top:4px}.jgroup span{font-variant-numeric:tabular-nums;font-weight:500;letter-spacing:0;opacity:.8}.jobs>.job{grid-template-columns:10px 1fr auto;padding:10px 12px}.jobs>.job .t{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;font-weight:540;line-height:1.35;text-wrap:pretty;overflow-wrap:anywhere}.jobs>.job .dot{margin-top:6px}.jobs>.job .w{align-self:start;margin-top:2px;font-size:11.5px;color:var(--ink-3);font-variant-numeric:tabular-nums}.jobs>.job .m{grid-column:2 / 4;display:flex;align-items:center;margin-top:3px;white-space:pre}.jobs>.job .m .pico{--s: 14px;font-size:8px;margin-right:6px}.jobs>.job .m .r{overflow:hidden;text-overflow:ellipsis}.job .m .s.working{color:var(--work-fg);font-weight:560}.job .m .s.queued{color:var(--wait-fg);font-weight:560}.job .m .s.failed{color:var(--bad);font-weight:560}.job{all:unset;cursor:pointer;display:grid;grid-template-columns:10px 1fr;gap:1px 10px;padding:9px 10px;border-radius:10px;transition:background .15s}.job:hover{background:var(--t1)}.job.open{background:var(--t2)}.job .dot{grid-row:1 / 3;margin-top:6px;width:8px;height:8px;border-radius:50%}.job .t{font-weight:560;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.job .m{font-size:12px;color:var(--ink-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dot.queued{background:var(--wait)}.dot.working{background:var(--work);box-shadow:0 0 0 3px #2f6fae2e;animation:ring 1.6s infinite}.dot.done{background:var(--good)}.dot.failed{background:var(--bad)}.dot.stopped{background:var(--ink-3)}@keyframes ring{50%{box-shadow:0 0 0 6px #2f6fae00}}@media (prefers-reduced-motion: reduce){.dot.working{animation:none}}.empty{margin:12px 4px;color:var(--ink-3);font-size:13px;text-wrap:pretty}#clear{font-size:12.5px;padding:7px;margin-top:6px}.channels{flex:none;display:flex;gap:4px;align-items:center;overflow-x:auto;scrollbar-width:none;padding:8px 12px 6px}.channels::-webkit-scrollbar{display:none}.channel{all:unset;cursor:pointer;flex:none;display:inline-flex;align-items:center;gap:2px;padding:4px 9px;border-radius:99px;font-size:12.5px;font-weight:580;color:var(--ink-3);transition:color .15s,background .15s}.channel:hover{color:var(--ink);background:var(--t1)}.channel.on{color:var(--paper);background:var(--ink)}.channel .hash{opacity:.55}.channel .badge{margin-left:4px;background:var(--accent);color:var(--paper);border-radius:99px;font-size:10.5px;font-weight:700;line-height:15px;padding:0 5px}.channel.add,.channel.close{color:var(--ink-3);font-size:14px;line-height:1;padding:4px 8px}.channel:focus-visible{outline:2px solid var(--accent);outline-offset:1px}.new-channel{flex:none;display:inline-flex;align-items:center;gap:2px;padding:3px 8px;border:1px solid var(--line);border-radius:99px;background:var(--surface)}.new-channel .hash{color:var(--ink-3);font-size:12.5px}.new-channel input{all:unset;width:96px;font-size:12.5px;font-weight:580}.msgs{flex:1;overflow-y:auto;padding:10px 20px 8px;display:flex;flex-direction:column;gap:4px;-webkit-mask-image:linear-gradient(transparent,#000 14px);mask-image:linear-gradient(transparent,#000 14px)}.msg{position:relative;margin:0 -10px;padding:7px 10px;border-radius:12px;transition:background .15s}.msg .who{display:flex;align-items:center;gap:6px;font-size:12.5px;margin-bottom:3px}.msg .av{display:inline-flex;flex:none}.msg .av .face{display:block;width:20px;height:20px;border-radius:50%}.msg .who b{font-weight:640}.msg .who .arrow{color:var(--ink-3)}.msg .who time{color:var(--ink-3);font-size:11.5px;font-variant-numeric:tabular-nums;margin-left:2px}.msg .body{padding-left:26px;overflow-wrap:anywhere}.msg .body p{margin:0 0 6px}.msg .body>:last-child{margin-bottom:0}.msg.from-you .body p{white-space:pre-wrap}.msg.from-chief .body{color:var(--ink-2)}.msg.failed .body{color:var(--bad)}.msg.finished .body{border-left:3px solid var(--line);padding-left:10px;margin-left:4px;color:var(--ink)}.msg.flag .body{border-left:3px solid var(--bad);padding-left:10px;margin-left:4px;color:var(--ink)}.msg.dm .body,.msg.handoff .body,.msg.standup.from-agent .body,.msg.chill .body{border-left:2px solid var(--line);padding-left:10px;margin-left:4px;color:var(--ink-2)}.msg.chill .body{font-style:italic}.msg .kind{font-size:11px;font-weight:620;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-3);margin:0 0 3px 26px}.msg.clamp:not(.full) .body{max-height:190px;overflow:hidden;-webkit-mask-image:linear-gradient(#000 70%,transparent);mask-image:linear-gradient(#000 70%,transparent)}.msg .acts{display:flex;gap:12px;align-items:center;padding-left:26px;margin-top:6px;flex-wrap:wrap}.msg .reacts{display:flex;flex-wrap:wrap;gap:4px;align-items:center;padding-left:26px;margin-top:5px}.msg .reacts:not(.some){position:absolute;top:5px;right:8px;margin:0}.msg .reacts button{all:unset;cursor:pointer;display:inline-flex;align-items:center;gap:4px;font-size:13px;line-height:1}.msg .reacts .pill{padding:3px 7px;border-radius:999px;border:1px solid var(--line);background:var(--paper)}.msg .reacts .pill span{font-size:11.5px;font-weight:620;color:var(--ink-2);font-variant-numeric:tabular-nums}.msg .reacts .pill.mine{border-color:var(--work);background:color-mix(in srgb,var(--work) 10%,transparent)}.msg .reacts .pill:hover{border-color:var(--ink-3)}.msg .reacts .add{padding:3px 7px;border-radius:999px;font-size:12px;color:var(--ink-3);opacity:0;transition:opacity .12s}.msg:hover .reacts .add,.msg .reacts .add:focus-visible{opacity:1}.msg .reacts .add:hover{color:var(--ink);background:var(--line)}.emoji-picker{position:fixed;z-index:50;width:296px;max-height:320px;overflow-y:auto;padding:6px 8px 8px;border-radius:12px;border:1px solid var(--line);background:var(--paper);box-shadow:0 10px 30px #00000029}.emoji-picker[hidden]{display:none}.emoji-picker p{margin:6px 2px 2px;font-size:11px;font-weight:620;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-3)}.emoji-picker .grid{display:grid;grid-template-columns:repeat(8,1fr)}.emoji-picker button{all:unset;cursor:pointer;display:grid;place-items:center;height:32px;border-radius:8px;font-size:19px}.emoji-picker button:hover,.emoji-picker button:focus-visible{background:var(--paper-2)}.msg .acts .icon{all:unset;cursor:pointer;display:inline-flex;padding:3px;margin:-3px;border-radius:6px;color:var(--ink-3)}.msg .acts .icon:hover,.msg .acts .icon:focus-visible{color:var(--ink);background:var(--t2)}.msg .acts .icon svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.msg .acts .icon.quiet{opacity:0;transition:opacity .12s}.msg:hover .acts .icon.quiet,.msg .acts .icon.quiet:focus-visible{opacity:1}@media (hover: hover){.msg:hover{background:var(--t1)}.msg .acts:not(:has(>:not(.quiet))){position:absolute;top:9px;right:46px;margin:0;padding:0}}.msg .thread-sum{all:unset;cursor:pointer;display:flex;align-items:center;gap:6px;margin:6px 0 0 8px;padding:4px 8px;border-radius:8px;font-size:12.5px;max-width:calc(100% - 24px);box-sizing:border-box}.msg .thread-sum:hover,.msg .thread-sum:focus-visible{background:var(--t1)}.msg .thread-sum .faces{display:flex;flex:none}.msg .thread-sum .faces .av{margin-right:-5px;border-radius:50%;box-shadow:0 0 0 1.5px var(--paper)}.msg .thread-sum .faces .face{width:18px;height:18px}.msg .thread-sum b{flex:none;font-weight:620;color:var(--accent);margin-left:2px}.msg .thread-sum .last{color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.thread-head{flex:none;display:flex;align-items:center;gap:12px;padding:12px 20px 10px;border-bottom:1px solid var(--line);font-size:13px}.thread-count{margin:0;font-size:11.5px;color:var(--ink-3);display:flex;align-items:center;gap:8px}.thread-count:after{content:"";flex:1;height:1px;background:var(--line)}.msg .blocked{margin:6px 0 0 26px;font-size:12.5px;color:var(--bad)}.link{all:unset;cursor:pointer;font-size:12.5px;font-weight:600;color:var(--accent)}.link:hover{text-decoration:underline;text-underline-offset:2px}.msg .acts .link{font-weight:560}.tag{font-weight:600;color:var(--accent)}.typing{flex:none;padding:0 20px 8px 34px;font-size:12px;color:var(--ink-3);display:flex;align-items:center;gap:3px}.typing span{width:4px;height:4px;border-radius:50%;background:var(--ink-3);animation:dot 1.1s infinite}.typing span:nth-child(2){animation-delay:.15s}.typing span:nth-child(3){animation-delay:.3s;margin-right:5px}@keyframes dot{30%{transform:translateY(-3px);opacity:1}0%,60%,to{opacity:.4}}.composer{flex:none;position:relative;margin:0 12px 12px;border:1px solid var(--line);border-radius:14px;background:var(--surface);padding:10px 10px 8px 12px;box-shadow:0 1px 2px #00000008;transition:border-color .15s,box-shadow .15s}.composer:focus-within{border-color:var(--ink-3);box-shadow:0 0 0 4px var(--t1)}.composer textarea{all:unset;display:block;width:100%;font-size:14px;line-height:1.45;min-height:20px;white-space:pre-wrap;overflow-y:auto}.composer textarea::placeholder{color:var(--ink-3)}.composer .controls{display:flex;align-items:center;gap:3px;margin-top:8px}.composer .controls>button{flex:none}.composer .controls select{border:0;background:var(--t1);font-size:12px;padding:4px 18px 4px 6px;border-radius:7px;color:var(--ink-2);flex:0 1 auto;min-width:72px;text-overflow:ellipsis;outline:none;-moz-appearance:none;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6' fill='none' stroke='%23958d81' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 6px center;background-size:9px 6px}.composer .controls select:hover{background-color:var(--t2)}.composer .controls #model{flex:0 1 160px}.composer .controls button[type=submit]{margin-left:auto}.ac{position:absolute;left:0;right:0;bottom:calc(100% + 6px);background:var(--surface);border:1px solid var(--line);border-radius:12px;box-shadow:0 10px 30px #281e0f24;padding:4px;max-height:280px;overflow-y:auto;z-index:5}.ac .item{display:flex;align-items:center;gap:8px;padding:7px 9px;border-radius:8px;cursor:pointer;font-size:13.5px}.ac .item.on{background:var(--t2)}.ac .item i{width:8px;height:8px;border-radius:50%;flex:none}.ac .item i.hash{width:auto;height:auto;font-style:normal;color:var(--ink-3);font-weight:600}.ac .item span{font-weight:560}.ac .item em{margin-left:auto;font-style:normal;font-size:12px;color:var(--ink-3)}.primary,.ghost{border:0;cursor:pointer;border-radius:10px;padding:10px 14px;font-weight:600;display:inline-flex;align-items:center;justify-content:center;gap:8px;transition:transform .12s,background .15s,opacity .15s}.primary{background:var(--ink);color:var(--paper)}.primary:hover{background:color-mix(in srgb,var(--ink) 85%,var(--paper))}.primary:active,.ghost:active{transform:scale(.97)}.ghost{background:transparent;color:var(--ink-2)}.ghost:hover{background:var(--t2);color:var(--ink)}.ghost.danger{color:var(--bad)}.small{padding:7px 12px;font-size:13px}select{border:1px solid var(--line);border-radius:8px;padding:6px 8px;background:var(--surface)}.hud{position:absolute;top:14px;right:14px;display:flex;gap:8px;pointer-events:none}.chip{pointer-events:auto;display:flex;align-items:center;height:38px;padding:0 14px;gap:16px;background:var(--glass);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid var(--line);border-radius:12px;font-size:13px;color:var(--ink-2);box-shadow:0 1px 2px #0000000a,0 6px 18px #3c2d190f}#stats span{white-space:nowrap}#stats b{color:var(--ink);font-weight:650;font-variant-numeric:tabular-nums;margin-right:3px}.chip.tools{padding:0 4px;gap:2px}.chip.tools button{all:unset;cursor:pointer;width:30px;height:30px;display:grid;place-items:center;border-radius:8px;color:var(--ink-2);transition:background .15s,color .15s,transform .12s}.chip.tools button:hover{background:var(--t2);color:var(--ink)}.chip.tools button:active{transform:scale(.92)}.chip.tools button:disabled{opacity:.4;pointer-events:none}.chip.tools svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.chip .sep{width:1px;height:18px;background:var(--line);margin:0 3px}.hint{position:absolute;bottom:14px;left:50%;transform:translate(-50%);margin:0;font-size:12px;color:var(--ink-3);white-space:nowrap;pointer-events:none;transition:opacity .6s}.hint.gone{opacity:0}.detail{width:440px;display:flex;flex-direction:column;min-height:0;padding-top:18px;background:var(--pane);border-left:1px solid var(--pane-edge);-webkit-backdrop-filter:blur(24px) saturate(1.5);backdrop-filter:blur(24px) saturate(1.5);animation:open .3s var(--ease)}@keyframes open{0%{opacity:0;transform:translate(12px)}}.d-head{display:flex;justify-content:space-between;align-items:center;padding:0 20px}.x{all:unset;cursor:pointer;font-size:22px;line-height:1;color:var(--ink-3);padding:2px 6px;border-radius:6px}.x:hover{color:var(--ink);background:var(--t2)}.pill{font-size:11.5px;font-weight:620;letter-spacing:.02em;text-transform:uppercase;padding:3px 8px;border-radius:6px}.pill.queued{background:var(--wait-bg);color:var(--wait-fg)}.pill.working{background:var(--work-bg);color:var(--work-fg)}.pill.done{background:var(--good-bg);color:var(--good-fg)}.pill.failed{background:var(--bad-bg);color:var(--bad-fg)}.pill.stopped{background:var(--t2);color:var(--ink-2)}.detail h2{margin:12px 20px;font-size:18px;font-weight:650;letter-spacing:-.015em;line-height:1.3;text-wrap:balance}.meta{margin:0 20px 14px;display:grid;grid-template-columns:repeat(3,1fr);gap:10px 12px}.meta dt{font-size:11px;color:var(--ink-3);margin-bottom:1px}.meta dd{margin:0;font-size:13px;font-weight:550;font-variant-numeric:tabular-nums;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.log{flex:1;overflow-y:auto;padding:14px 20px 16px;border-top:1px solid var(--line)}.e{margin:0 0 6px}.e p{margin:0;white-space:pre-wrap;overflow-wrap:anywhere}.e .who{display:block;font-size:11px;font-weight:620;letter-spacing:.04em;text-transform:uppercase;color:var(--ink-3);margin-bottom:3px}.e.user{margin:4px 0 14px;padding:10px 12px;background:var(--t1);border-radius:10px}.e.text{margin:10px 0 12px}.e.sys{font-size:12px;color:var(--ink-3)}.e.tool{font-family:var(--mono);font-size:12px;color:var(--ink-2);display:flex;align-items:center;gap:8px}.e.err{font-family:var(--mono);font-size:12px;color:var(--bad);white-space:pre-wrap;overflow-wrap:anywhere}.e.result{margin:14px 0 6px;padding:12px;border-radius:10px;background:var(--good-bg)}.e.result.bad{background:var(--bad-bg)}.st{flex:none;width:7px;height:7px;border-radius:2px;background:var(--ink-3)}.st.library{background:#3d7ea6}.st.servers{background:#2b2f38}.st.window{background:#4f8fc0;border-radius:50%}.st.whiteboard{background:#e07a5f}.st.desk{background:#c39a6b}code{font-family:var(--mono);font-size:.88em;background:var(--t2);padding:1px 4px;border-radius:4px}.d-foot{border-top:1px solid var(--line);padding:12px 20px 16px;display:flex;flex-direction:column;gap:10px}.notice{font-size:13px;color:var(--ink-2);display:flex;flex-direction:column;align-items:flex-start;gap:8px}.reply textarea{width:100%;resize:none;border:1px solid var(--line);border-radius:10px;padding:9px 11px;background:var(--surface);outline:none}.reply textarea:focus{border-color:var(--ink-3)}.reply .row{display:flex;justify-content:space-between;gap:8px;margin-top:8px}.md>:first-child{margin-top:0}.md>:last-child{margin-bottom:0}.md p{margin:0 0 8px;white-space:normal}.md h1,.md h2,.md h3,.md h4{font-size:14px;font-weight:650;margin:12px 0 6px;letter-spacing:-.01em}.md ul,.md ol{margin:0 0 8px;padding-left:20px}.md li{margin:2px 0}.md a{color:var(--accent)}.md pre{font-family:var(--mono);font-size:12px;background:var(--t1);padding:10px 12px;border-radius:8px;overflow-x:auto;margin:0 0 8px}.md pre code{background:none;padding:0}.md table{border-collapse:collapse;width:100%;font-size:12.5px;margin:4px 0 10px;display:block;overflow-x:auto}.md th,.md td{text-align:left;padding:5px 10px 5px 0;border-bottom:1px solid var(--line);vertical-align:top}.md th{font-weight:620;color:var(--ink-2)}.md td{font-variant-numeric:tabular-nums}.md blockquote{margin:0 0 8px;padding-left:10px;border-left:2px solid var(--line);color:var(--ink-2)}.agent-label{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;gap:5px;will-change:transform}.agent-label .name{display:flex;align-items:center;gap:5px;font-size:11.5px;font-weight:620;background:var(--glass);padding:2px 8px 2px 6px;border-radius:99px;color:var(--ink);box-shadow:0 1px 3px #0000001a}.agent-label .name i{width:7px;height:7px;border-radius:50%}.agent-label.selected .name{background:var(--ink);color:var(--paper)}.agent-label.far .bubble,.agent-label.crowded .bubble{display:none!important}.agent-label.buried{visibility:hidden}.bubble{max-width:170px;font-size:12px;font-weight:500;line-height:1.3;padding:5px 9px;border-radius:11px;background:var(--surface);color:var(--ink);box-shadow:0 2px 10px #281e0f24;text-align:center;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;animation:bub .2s ease-out}@keyframes bub{0%{opacity:0;transform:translateY(4px) scale(.96)}}.bubble.thinking{color:var(--ink-3);font-style:italic}.bubble.idle{color:var(--ink-2)}.bubble.warn{background:var(--bad-bg);color:var(--bad-fg)}.bubble.good{background:var(--good-bg);color:var(--good-fg)}.bubble.talk{background:var(--work-bg);color:var(--work-fg)}.xpfloat{position:absolute;top:-10px;font-size:14px;font-weight:720;color:var(--good-fg);animation:float 1.8s ease-out forwards;text-shadow:0 1px 0 var(--paper)}@keyframes float{0%{opacity:0;transform:translateY(8px)}15%{opacity:1}to{opacity:0;transform:translateY(-36px)}}.toast{position:absolute;left:50%;bottom:44px;transform:translate(-50%,12px);opacity:0;background:var(--ink);color:var(--paper);padding:10px 16px;border-radius:12px;font-weight:500;transition:opacity .2s,transform .25s var(--ease);pointer-events:none;max-width:calc(100% - 32px)}.toast.show{opacity:1;transform:translate(-50%)}.toast.good{background:#22603b;color:#fff}.toast.bad{background:#8f3121;color:#fff}.toast.act{pointer-events:auto}.toast button{all:unset;cursor:pointer;margin-left:10px;font-weight:650;text-decoration:underline;text-underline-offset:3px}.chip.away{gap:4px;padding:0 6px 0 12px}.chip.away span{margin-right:4px;color:var(--ink-3);white-space:nowrap}.chip.away button{all:unset;cursor:pointer;display:flex;align-items:center;gap:6px;height:28px;padding:0 9px;border-radius:8px;color:var(--ink);font-weight:550;transition:background .15s}.chip.away button:hover{background:var(--t2)}.chip.away i{width:8px;height:8px;border-radius:50%}.banner{position:absolute;left:50%;top:32%;transform:translate(-50%,-50%);text-align:center;pointer-events:none;opacity:0;display:flex;flex-direction:column;gap:4px}.banner b{font-size:56px;font-weight:760;letter-spacing:-.04em;line-height:1;text-shadow:0 2px 30px var(--paper)}.banner span{font-size:16px;color:var(--ink-2);font-weight:520}.banner.show{animation:banner 3.4s var(--ease) forwards}@keyframes banner{0%{opacity:0;transform:translate(-50%,-40%) scale(.94)}12%,78%{opacity:1;transform:translate(-50%,-50%) scale(1)}to{opacity:0;transform:translate(-50%,-56%)}}@media (max-width: 1180px){.app{grid-template-columns:min(var(--side-w, 320px),320px) minmax(0,1fr) auto}.detail{width:380px}}@container (max-width: 820px){#stats span:nth-child(n+3){display:none}.hint{display:none}}@container (max-width: 520px){#stats{display:none}}.mnav{display:none}@media (max-width: 860px){.app{grid-template-columns:minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto}.view,.side{grid-area:1 / 1;min-width:0}.side-grip{display:none}.app[data-m=office] .side,.app[data-m=side] .view{visibility:hidden;pointer-events:none}.side{border-right:0;padding-top:env(safe-area-inset-top)}.brand{display:block;padding:12px 16px 8px}.brand .title{margin-bottom:6px}.brand h1{font-size:18px}.meter{padding:6px 10px;row-gap:4px}.tabs{display:none}.msgs{padding:12px 14px 8px}.channels{padding:8px 10px 0}.channel{padding:7px 12px;font-size:13.5px}.composer{margin:0 10px 10px}.composer textarea,.new-channel input,input,select,textarea{font-size:16px}.composer .controls{gap:6px}.composer .controls>button{min-width:38px;min-height:38px;justify-content:center;align-items:center}.composer .controls select{font-size:13px;min-height:36px;padding-right:22px}.composer .controls button[type=submit]{min-height:38px;padding:0 18px}#jobsPane,.team,.projects{padding-bottom:12px}.hud{top:calc(10px + env(safe-area-inset-top));left:10px;right:10px;flex-direction:column;align-items:flex-end}.chip.tools button{width:36px;height:36px}.chip.away{max-width:100%;height:auto;min-height:38px;flex-wrap:wrap;padding:4px 6px 4px 12px}.hint,#stats{display:none}.toast{max-width:calc(100% - 24px);bottom:18px}.banner b{font-size:40px}.detail{position:fixed;top:0;right:0;bottom:0;left:0;width:auto;z-index:30;padding-top:calc(14px + env(safe-area-inset-top));padding-bottom:env(safe-area-inset-bottom);border-left:0}.meta{margin:0 16px 14px}.detail h2{margin:12px 16px}.d-head{padding:0 16px}.x{font-size:28px;padding:4px 12px}.mnav{display:grid;grid-template-columns:repeat(5,1fr);grid-row:2;z-index:25;background:color-mix(in srgb,var(--paper) 96%,transparent);border-top:1px solid var(--line);padding-bottom:env(safe-area-inset-bottom)}.mnav button{all:unset;cursor:pointer;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;min-height:54px;font-size:11px;font-weight:600;color:var(--ink-3);-webkit-tap-highlight-color:transparent;transition:color .15s}.mnav button.on{color:var(--ink)}.mnav button:active svg{transform:scale(.88)}.mnav button:focus-visible{outline:2px solid var(--accent);outline-offset:-4px;border-radius:10px}.mnav svg{width:22px;height:22px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;transition:transform .12s}.mnav b:not(:empty){position:absolute;top:5px;left:calc(50% + 6px);font-size:10.5px;background:var(--accent);color:var(--paper);border-radius:99px;padding:0 5px;line-height:15px;font-weight:700}.mnav .dot{position:absolute;top:8px;left:calc(50% + 8px);width:8px;height:8px;border-radius:50%;background:var(--accent)}}@media (hover: none){.chip.tools button:hover,.channel:hover{background:none}}body{overscroll-behavior:none;-webkit-tap-highlight-color:transparent}@media (prefers-reduced-motion: reduce){*,*:before,*:after{animation-duration:.01ms!important;transition-duration:.01ms!important}}.attach{border:0;background:none;cursor:pointer;padding:4px;border-radius:7px;color:var(--ink-3);display:inline-flex}.attach:hover{background:var(--t1);color:var(--ink-2)}.attach svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.composer.dropping,.reply.dropping{border-color:var(--work);box-shadow:0 0 0 4px var(--t2)}.reply.dropping textarea{border-color:var(--work)}.tray{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}.tray .file,.files .file{display:inline-flex;align-items:center;gap:6px;max-width:200px;padding:3px 4px 3px 3px;border-radius:8px;background:var(--t1);font-size:12px;color:var(--ink-2);text-decoration:none}.tray .file.loading{opacity:.55}.tray .file img{width:28px;height:28px;object-fit:cover;border-radius:6px;flex:none}.tray .file b,.files .file b{font-size:9px;font-weight:700;letter-spacing:.04em;color:var(--ink-3);background:var(--surface);border-radius:5px;padding:5px 4px;flex:none}.tray .file span,.files .file span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tray .file button{border:0;background:none;cursor:pointer;color:var(--ink-3);font-size:15px;line-height:1;padding:0 3px}.tray .file button:hover{color:var(--ink)}.msg .files{display:flex;flex-wrap:wrap;gap:6px;padding-left:14px;margin-top:4px}.msg .files .thumb img{display:block;max-width:160px;max-height:120px;border-radius:8px;border:1px solid var(--line)}.reply .row .attach{margin-left:auto}.mic{border:0;background:none;cursor:pointer;padding:4px;border-radius:7px;color:var(--ink-3);display:inline-flex}.mic:hover{background:var(--t1);color:var(--ink-2)}.mic:disabled{opacity:.45;pointer-events:none}.mic svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.mic.on{color:#fff;background:var(--bad)}.mic.on:hover{background:var(--bad);color:#fff}.rec{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:12.5px;color:var(--ink-2)}.rec i{width:8px;height:8px;border-radius:50%;background:var(--bad);flex:none;animation:pulse 1.1s ease-in-out infinite}.rec span{font-variant-numeric:tabular-nums}.rec .link{margin-left:auto;color:var(--ink-3)}.rec .link+.link{margin-left:0}.rec.working i{background:var(--work)}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.8)}}@media (prefers-reduced-motion: reduce){.rec i{animation:none}}audio.voice{width:100%;max-width:260px;height:32px}.msg .files audio.voice{margin-top:2px}.tray .file b{white-space:nowrap}.chip.tools button.on{color:var(--accent);background:color-mix(in srgb,var(--accent) 14%,transparent)}.changes{margin:0 20px 14px;border:1px solid var(--line);border-radius:10px;background:var(--paper);overflow:hidden}.changes.none{padding:9px 12px;font-size:12.5px;color:var(--ink-3)}.c-head{all:unset;box-sizing:border-box;width:100%;display:flex;align-items:center;gap:10px;padding:9px 12px;cursor:pointer;font-size:13px}.c-head:hover{background:color-mix(in srgb,var(--ink) 4%,transparent)}.c-head:focus-visible,.c-file:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.c-title{font-weight:620}.c-sum{flex:1;color:var(--ink-2);font-variant-numeric:tabular-nums}.c-caret{color:var(--ink-3)}.changes .add{color:var(--good-fg);font-weight:600}.changes .del{color:var(--bad-fg);font-weight:600}.c-files{list-style:none;margin:0;padding:0;border-top:1px solid var(--line);max-height:240px;overflow-y:auto}.c-files li{border-bottom:1px solid var(--line)}.c-files li:last-child{border-bottom:0}.c-files .more{padding:7px 12px;font-size:12px;color:var(--ink-3)}.c-file{all:unset;box-sizing:border-box;width:100%;display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;font-size:12.5px}.c-file:hover{background:color-mix(in srgb,var(--ink) 4%,transparent)}.c-file .st{font:700 10.5px/1 ui-monospace,monospace;width:16px;text-align:center;padding:3px 0;border-radius:4px;background:var(--t2);color:var(--ink-2)}.c-file .st.added{background:var(--good-bg);color:var(--good-fg)}.c-file .st.deleted{background:var(--bad-bg);color:var(--bad-fg)}.c-file .p{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left;font-family:ui-monospace,monospace}.c-file .n{color:var(--ink-3);font-variant-numeric:tabular-nums}.c-diff{margin:0;padding:8px 12px;max-height:320px;overflow:auto;font:11.5px/1.5 ui-monospace,monospace;background:color-mix(in srgb,var(--ink) 3%,transparent);white-space:pre}.c-diff .add{color:var(--good-fg);background:var(--good-bg);display:inline-block;min-width:100%;font-weight:400}.c-diff .del{color:var(--bad-fg);background:var(--bad-bg);display:inline-block;min-width:100%;font-weight:400}.c-diff .hunk{color:var(--ink-3)}.name-ask{border:1px solid var(--line);border-radius:16px;padding:22px;max-width:380px;width:calc(100vw - 48px);background:var(--paper);color:inherit;box-shadow:0 24px 60px #00000040}.name-ask::backdrop{background:#00000059}.name-ask h2{margin:0 0 8px;font-size:18px}.name-ask .acct-forget{display:block;margin:12px 0 0}.settings{width:min(640px,calc(100vw - 24px));max-height:min(720px,calc(100dvh - 24px));padding:0;margin:auto;display:none;flex-direction:column;overflow:hidden;color:var(--ink);background:var(--paper);border:1px solid var(--line);border-radius:16px;box-shadow:0 24px 60px #00000047}.settings[open]{display:flex}.settings::backdrop{background:#00000052;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}.set-head{display:flex;align-items:center;justify-content:space-between;padding:16px 18px 12px 22px;border-bottom:1px solid var(--line)}.set-head h2{margin:0;font-size:17px;font-weight:650;letter-spacing:-.01em}.set-x{width:30px;height:30px}.set-body{flex:1;min-height:0;overflow-y:auto;padding:4px 22px 8px;overscroll-behavior:contain}.set-group{padding:8px 0 4px}.set-group+.set-group{border-top:1px solid var(--line)}.set-group .theme-k{margin:10px 0 4px}.set-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(150px,220px);gap:8px 18px;align-items:center;padding:10px 0;position:relative}.set-row.set-changed:before{content:"";position:absolute;left:-12px;top:12px;bottom:12px;width:2px;border-radius:2px;background:var(--accent)}.set-label{font-size:13.5px;font-weight:580;color:var(--ink)}.set-fine{margin:2px 0 0}.set-reset{display:inline;margin:0;font-size:inherit}.set-input{width:100%;box-sizing:border-box;padding:7px 10px;border-radius:9px;border:1px solid var(--line);background:var(--paper-2);color:var(--ink);font:inherit;font-size:13px;font-variant-numeric:tabular-nums}.set-input:focus-visible{outline:2px solid var(--accent);outline-offset:-1px}.set-choice{width:100%;box-sizing:border-box}.set-choice button{text-transform:capitalize}.set-switch{all:unset;cursor:pointer;justify-self:end;position:relative;width:38px;height:22px;border-radius:11px;background:var(--t3);transition:background .18s}.set-switch:after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 1px 3px #0000004d;transition:transform .18s cubic-bezier(.2,.8,.2,1)}.set-switch[aria-checked=true]{background:var(--accent)}.set-switch[aria-checked=true]:after{transform:translate(16px)}.set-switch:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.set-foot{display:flex;align-items:center;gap:12px;padding:12px 22px 14px;border-top:1px solid var(--line);background:var(--paper-2)}.set-msg{flex:1;font-size:12px;color:var(--ink-3)}.set-foot .acct-forget{margin:0}@media (max-width: 520px){.set-row{grid-template-columns:1fr}.set-switch{justify-self:start}}@media (prefers-reduced-motion: reduce){.set-switch,.set-switch:after{transition:none}}.agent-label.visitor .name{box-shadow:0 0 0 1.5px var(--tint),0 1px 3px #0000001a}.agent-label.visitor .name .handle{color:var(--ink-2);font-weight:520;white-space:pre}.door-sign{position:absolute;left:0;top:0;will-change:transform;display:flex;flex-direction:column;gap:2px;align-items:center;font-size:11.5px;font-weight:620;color:var(--ink);background:var(--glass);padding:4px 10px;border-radius:8px;box-shadow:0 1px 3px #0000001f}.visitor-card{position:absolute;left:0;top:0;z-index:20000;width:max-content;max-width:260px;display:flex;flex-direction:column;gap:3px;font-size:12px;line-height:1.35;color:var(--ink);background:var(--surface);padding:8px 11px;border-radius:10px;border-left:3px solid var(--tint);box-shadow:0 4px 16px #281e0f29}.visitor-card .vc-who{font-weight:650}.visitor-card .vc-role,.visitor-card .vc-state{color:var(--ink-2)}.visitor-card .vc-job{font-weight:520}.visitor-card .vc-state{font-size:11px}