afterimage-cli 0.2.2 → 0.2.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.4
4
+
5
+ - **One command to install.** `npx afterimage-cli init` now does the whole setup: init installs afterimage-cli in the project as a dev dependency, at its own version, with the package manager your lockfile names (npm, pnpm, Yarn or Bun). A plain HTML folder no longer needs `npm init -y` first, since init creates `package.json` before the install. A project that already lists afterimage-cli installs nothing, and a failed install leaves the rest of the setup in place and prints the command to finish it. The setup prompt and the skill use the one command too.
6
+
7
+ ## 0.2.3
8
+
9
+ - **Scroll sync covers panels.** In an app that scrolls a panel instead of the page, Scroll synced in side by side, the wipe and holding B only moved the page, which never scrolls, so the two versions drifted apart. Now each panel follows the same panel in the other version, even when the page around it changed between versions.
10
+ - **Switching version crossfades in place.** The frame used to go dark, then blank, then draw the next version, and it jumped back to the top. Now the next version loads out of sight, lines up with where you were scrolled, page and panels, and fades in over the old one.
11
+ - **Wipe and side by side glide into each other.** Each half of the wipe slides into its own column and back, without frames covering each other or going blank on the way. The change check that used to start mid-glide and freeze it now waits until the frames land.
12
+ - **The pill hands over smoothly both ways.** Highlight and Compare now start on screen, after the viewer has taken the page's place, so the outlines fade in and the wipe sweeps in from the edge. Turning them off plays out before the page comes back, on the same route and scroll, panels included. The page's pill and the viewer's blend in place instead of swapping.
13
+ - **The pill changes shape instead of jumping.** Entering or leaving Compare, its segments glide to their new width and their content fades in.
14
+
3
15
  ## 0.2.2
4
16
 
5
17
  - **Apps that scroll a panel instead of the page.** Many app shells keep the page still and scroll the content inside it. There, outlines stayed where they were drawn while the content scrolled away under them, and stepping to a change scrolled nothing. Now outlines follow their elements as the panel scrolls and stop at its edges, and stepping scrolls each panel that holds the change, so it lands in the middle. Outlines also no longer make a page like that scroll on its own.
package/README.md CHANGED
@@ -11,11 +11,10 @@ Afterimage keeps every version of your prototype you save and serves them all fr
11
11
  Afterimage needs Node 22 or later. From your prototype's project root:
12
12
 
13
13
  ```bash
14
- npm install -D afterimage-cli
15
- npx afterimage init
14
+ npx afterimage-cli init
16
15
  ```
17
16
 
18
- A plain HTML folder with no `package.json` needs `npm init -y` first.
17
+ npx asks once to fetch the package. `init` installs it in the project as a dev dependency, with the package manager your lockfile names, and creates `package.json` first for a plain HTML folder that has none.
19
18
 
20
19
  `init` works out what your prototype is: a plain HTML folder, a Vite app, a Next.js static export, an Astro site, or SvelteKit with `@sveltejs/adapter-static`. It puts the Afterimage pill on your prototype and makes `npm run dev` start Afterimage beside your app. It also writes `afterimage.config.json`, lines in `.gitattributes` and `.gitignore`, and host config for Vercel and Netlify, then lists every file it changed. For Next.js, Astro and SvelteKit it prints the one import that adds the pill. When it can't tell which folder holds the prototype, it asks. You can also tell it:
21
20
 
@@ -13,13 +13,11 @@ The user works on the prototype, not in a terminal. Do the steps yourself and re
13
13
 
14
14
  When the user asks to add Afterimage, or to track the prototype's versions, do all of this:
15
15
 
16
- 1. If the project has no `package.json`, run `npm init -y`.
17
- 2. Unless `package.json` already lists it, run `npm install --save-dev afterimage-cli`.
18
- 3. Run `npx afterimage init --agent claude --no-input` if you are Claude Code, or `npx afterimage init --agent cursor --no-input` if you are Cursor. Handle what it prints as "Set up" below says.
19
- 4. Init puts the pill on plain HTML pages and in a Vite app's `index.html` itself. For Next.js, Astro and SvelteKit it prints a note that starts with `Add the Afterimage pill to your app`. Make that change yourself, exactly as the note says.
20
- 5. Unless `afterimage/versions.json` already lists a version, run `npx afterimage save "First version"`.
21
- 6. Start `npm run dev` as a background process. If you started the project's dev server earlier in this session, stop that one first: this is the same server, with Afterimage beside it. Wait for the line that starts with `Afterimage is running at`.
22
- 7. Tell the user where the prototype is running and that the pill at its bottom saves a version and shows them all. The address is the app dev server's own (such as Vite's `Local:` line), or the one after `Your prototype is at` for plain HTML.
16
+ 1. Run `npx --yes afterimage-cli init --agent claude --no-input` if you are Claude Code, or `npx --yes afterimage-cli init --agent cursor --no-input` if you are Cursor. It installs afterimage-cli in the project unless `package.json` already lists it, and creates `package.json` when there is none. Handle what it prints as "Set up" below says.
17
+ 2. Init puts the pill on plain HTML pages and in a Vite app's `index.html` itself. For Next.js, Astro and SvelteKit it prints a note that starts with `Add the Afterimage pill to your app`. Make that change yourself, exactly as the note says.
18
+ 3. Unless `afterimage/versions.json` already lists a version, run `npx afterimage save "First version"`.
19
+ 4. Start `npm run dev` as a background process. If you started the project's dev server earlier in this session, stop that one first: this is the same server, with Afterimage beside it. Wait for the line that starts with `Afterimage is running at`.
20
+ 5. Tell the user where the prototype is running and that the pill at its bottom saves a version and shows them all. The address is the app dev server's own (such as Vite's `Local:` line), or the one after `Your prototype is at` for plain HTML.
23
21
 
24
22
  ## Commands
25
23
 
@@ -13,13 +13,11 @@ The user works on the prototype, not in a terminal. Do the steps yourself and re
13
13
 
14
14
  When the user asks to add Afterimage, or to track the prototype's versions, do all of this:
15
15
 
16
- 1. If the project has no `package.json`, run `npm init -y`.
17
- 2. Unless `package.json` already lists it, run `npm install --save-dev afterimage-cli`.
18
- 3. Run `npx afterimage init --agent claude --no-input` if you are Claude Code, or `npx afterimage init --agent cursor --no-input` if you are Cursor. Handle what it prints as "Set up" below says.
19
- 4. Init puts the pill on plain HTML pages and in a Vite app's `index.html` itself. For Next.js, Astro and SvelteKit it prints a note that starts with `Add the Afterimage pill to your app`. Make that change yourself, exactly as the note says.
20
- 5. Unless `afterimage/versions.json` already lists a version, run `npx afterimage save "First version"`.
21
- 6. Start `npm run dev` as a background process. If you started the project's dev server earlier in this session, stop that one first: this is the same server, with Afterimage beside it. Wait for the line that starts with `Afterimage is running at`.
22
- 7. Tell the user where the prototype is running and that the pill at its bottom saves a version and shows them all. The address is the app dev server's own (such as Vite's `Local:` line), or the one after `Your prototype is at` for plain HTML.
16
+ 1. Run `npx --yes afterimage-cli init --agent claude --no-input` if you are Claude Code, or `npx --yes afterimage-cli init --agent cursor --no-input` if you are Cursor. It installs afterimage-cli in the project unless `package.json` already lists it, and creates `package.json` when there is none. Handle what it prints as "Set up" below says.
17
+ 2. Init puts the pill on plain HTML pages and in a Vite app's `index.html` itself. For Next.js, Astro and SvelteKit it prints a note that starts with `Add the Afterimage pill to your app`. Make that change yourself, exactly as the note says.
18
+ 3. Unless `afterimage/versions.json` already lists a version, run `npx afterimage save "First version"`.
19
+ 4. Start `npm run dev` as a background process. If you started the project's dev server earlier in this session, stop that one first: this is the same server, with Afterimage beside it. Wait for the line that starts with `Afterimage is running at`.
20
+ 5. Tell the user where the prototype is running and that the pill at its bottom saves a version and shows them all. The address is the app dev server's own (such as Vite's `Local:` line), or the one after `Your prototype is at` for plain HTML.
23
21
 
24
22
  ## Commands
25
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "afterimage-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "A version history for prototypes, deployed with the prototype itself.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,3 +1,4 @@
1
+ import { spawn } from 'node:child_process';
1
2
  import fs from 'node:fs/promises';
2
3
  import path from 'node:path';
3
4
  import readline from 'node:readline/promises';
@@ -349,6 +350,52 @@ async function planAgent(cwd, name, { isTTY, ask }) {
349
350
  };
350
351
  }
351
352
 
353
+ // "npx afterimage-cli init" runs from npx's cache, so the project gets the package too, at the version that is
354
+ // running: npm run dev, save and build all run it from node_modules. It goes in with the package manager the project's
355
+ // lockfile names, after every other step, since the install writes package.json too.
356
+ const { version } = JSON.parse(await fs.readFile(new URL('../../../package.json', import.meta.url), 'utf8'));
357
+ const installers = [['pnpm-lock.yaml', 'pnpm add -D'], ['yarn.lock', 'yarn add -D'], ['bun.lock', 'bun add -d'], ['bun.lockb', 'bun add -d']];
358
+
359
+ // The package manager's own output only shows when the install fails, as its last lines.
360
+ function installInTerminal(command, cwd) {
361
+ return new Promise((resolve, reject) => {
362
+ const child = spawn(command, { shell: true, cwd, stdio: ['ignore', 'pipe', 'pipe'] });
363
+ const output = [];
364
+ child.stdout.on('data', chunk => output.push(chunk));
365
+ child.stderr.on('data', chunk => output.push(chunk));
366
+ child.on('error', error => reject(new Error(`The install could not start: ${error.message}.`)));
367
+ child.on('close', code => {
368
+ if (code === 0) return resolve();
369
+ const said = Buffer.concat(output).toString().trim().split(/\r?\n/).slice(-12).join('\n');
370
+ reject(new Error(`${said ? `${said}\n` : ''}The install failed (exit code ${code}).`));
371
+ });
372
+ });
373
+ }
374
+
375
+ async function planInstall(cwd, { manifest }, { stdout, install }) {
376
+ if ('afterimage-cli' in { ...manifest.dependencies, ...manifest.devDependencies }) return { change: null, notes: [], apply: null };
377
+ let installer = 'npm install -D';
378
+ for (const [lockfile, command] of installers) {
379
+ if (await readText(path.join(cwd, lockfile)) !== null) {
380
+ installer = command;
381
+ break;
382
+ }
383
+ }
384
+ const command = `${installer} afterimage-cli@${version}`;
385
+ return {
386
+ change: `Installed afterimage-cli ${version} as a dev dependency`,
387
+ notes: [],
388
+ apply: async () => {
389
+ stdout.write(`Installing afterimage-cli ${version}...\n`);
390
+ try {
391
+ await install(command, cwd);
392
+ } catch (error) {
393
+ throw new Error(`${error.message}\nEverything else is set up. Install Afterimage yourself to finish: ${command}`);
394
+ }
395
+ }
396
+ };
397
+ }
398
+
352
399
  export async function run(args, ctx) {
353
400
  const { cwd, stdout, stderr } = ctx;
354
401
 
@@ -381,7 +428,7 @@ export async function run(args, ctx) {
381
428
 
382
429
  // Plan first, write after. package.json is read before the config step so a bad one stops init before any question,
383
430
  // and detection (which reads package.json) still sees the project as it was.
384
- let plans;
431
+ let plans, agentPlan;
385
432
  try {
386
433
  const packageFile = await readPackage(cwd);
387
434
  const configPlan = await planConfig(cwd, flags, prompt);
@@ -394,7 +441,8 @@ export async function run(args, ctx) {
394
441
  await planLines(cwd, '.gitignore', ['.afterimage/', `${configPlan.config.history}/.afterimage-*`, 'node_modules/'], hasIgnore),
395
442
  await planVercel(cwd, prompt),
396
443
  await planNetlify(cwd),
397
- await planAgent(cwd, agent, prompt)
444
+ agentPlan = await planAgent(cwd, agent, prompt),
445
+ await planInstall(cwd, packageFile, { stdout, install: ctx.install ?? installInTerminal })
398
446
  ];
399
447
  } catch (error) {
400
448
  // Ctrl-C at a prompt. Nothing has been written yet, so stopping here is clean. 130 is the shell's code for SIGINT.
@@ -411,7 +459,7 @@ export async function run(args, ctx) {
411
459
  else stdout.write(notes.length ? 'No files changed. Still left for you:\n' : 'Everything is already set up. Nothing changed.\n');
412
460
  if (notes.length) stdout.write(`${changes.length ? '\n' : ''}${notes.join('\n\n')}\n`);
413
461
  // With --agent the copy is in place, and a fresh one needs loading; without it, say how to add it.
414
- const agentLine = agent ? plans.at(-1).change && agents[agent].loads : skillHint;
462
+ const agentLine = agent ? agentPlan.change && agents[agent].loads : skillHint;
415
463
  const dev = (await readPackage(cwd)).manifest.scripts?.dev;
416
464
  stdout.write(`\n${agentLine ? `${agentLine}\n` : ''}${nextStep(typeof dev === 'string' && dev.trim().startsWith(devWrap))}\n`);
417
465
  return 0;
@@ -39,6 +39,7 @@
39
39
  const inDev = local && (scriptTag ? servedByDev : mode !== 'production');
40
40
  const helperPorts = Array.from({ length: 10 }, (_, i) => 4321 + i);
41
41
  const OPEN_TIMEOUT_MS = 12000;
42
+ const LAYER_FADE_MS = 140; // .layer's fade in; the page's pill stays up under it until it's done
42
43
 
43
44
  const getJson = async (url, timeout = 1500) => {
44
45
  try {
@@ -115,11 +116,12 @@
115
116
  vertical-align: 1px; animation: pulse 1s ease-in-out infinite alternate; }
116
117
  @keyframes pulse { from { opacity: 0.35; } to { opacity: 1; } }
117
118
  /* The viewer waits here, laid out but unseen, until it shows what was asked for. Not display:none, so it loads. */
118
- .layer { position: fixed; inset: 0; z-index: 2147483647; opacity: 0; pointer-events: none; }
119
- .layer[data-shown] { opacity: 1; pointer-events: auto; }
119
+ /* Under the pill the two are the same picture, so the short fade only blends the page's pill into the viewer's. */
120
+ .layer { position: fixed; inset: 0; z-index: 2147483647; opacity: 0; pointer-events: none; transition: opacity 110ms cubic-bezier(0.22, 1, 0.36, 1); }
121
+ .layer[data-shown] { opacity: 1; pointer-events: auto; transition-duration: 140ms; }
120
122
  .layer iframe { display: block; width: 100%; height: 100%; border: 0; background: transparent; }
121
123
  .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
122
- @media (prefers-reduced-motion: reduce) { button { transition: none; } .busy, button[aria-busy='true']::before { animation: none; } }
124
+ @media (prefers-reduced-motion: reduce) { button, .layer { transition: none; } .busy, button[aria-busy='true']::before { animation: none; } }
123
125
  `;
124
126
 
125
127
  const host = document.createElement('afterimage-overlay');
@@ -249,10 +251,21 @@
249
251
  viewer = { el, frame, origin: new URL(state.base).origin, versions: state.versions, listening: false, asked: null, shown: false };
250
252
  }
251
253
 
254
+ // The panels that scroll on this page (an app shell's content), in page order, and where each is. The viewer reads
255
+ // its frame the same way (stage/scrollSync.js), so the two open and close on the same rows.
256
+ const scrollingPanels = () => [...document.querySelectorAll('*')].filter(element => element.scrollHeight > element.clientHeight
257
+ && /^(auto|scroll|overlay)$/.test(getComputedStyle(element).overflowY));
258
+ const panelTops = () => scrollingPanels().map(panel => Math.round(panel.scrollTop));
259
+ function setPanelTops(tops) {
260
+ if (!Array.isArray(tops)) return;
261
+ scrollingPanels().forEach((panel, index) => { if (Number.isFinite(tops[index])) panel.scrollTop = tops[index]; });
262
+ }
263
+ const afterFrames = (count, fn) => requestAnimationFrame(() => (count > 1 ? afterFrames(count - 1, fn) : fn()));
264
+
252
265
  // A click can come before the viewer has started; then it is asked as soon as it says it is listening.
253
266
  function ask() {
254
267
  if (!viewer?.listening || !viewer.asked) return;
255
- viewer.frame.contentWindow.postMessage({ type: 'afterimage:open', view: viewer.asked.view, route: location.hash, scrollY: window.scrollY }, viewer.origin);
268
+ viewer.frame.contentWindow.postMessage({ type: 'afterimage:open', view: viewer.asked.view, route: location.hash, scrollY: window.scrollY, panels: panelTops() }, viewer.origin);
256
269
  }
257
270
 
258
271
  function open(name, event) {
@@ -289,28 +302,44 @@
289
302
  el.setAttribute('aria-modal', 'true');
290
303
  el.setAttribute('aria-label', 'Version history');
291
304
  el.setAttribute('data-shown', '');
292
- dock.hidden = true;
293
- document.documentElement.style.setProperty('overflow', 'hidden');
305
+ const fade = matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : LAYER_FADE_MS;
294
306
  viewer.frame.focus();
307
+ // Once the swap is done, the viewer moves into what was asked for, where it can be seen doing it. One thing at
308
+ // a time: the two pills blend in place first, then the viewer's changes shape.
309
+ const { frame, origin } = viewer;
310
+ afterFrames(1, () => setTimeout(() => {
311
+ if (!viewer?.shown) return;
312
+ dock.hidden = true;
313
+ // Only now, under an opaque viewer: dropping the page's scrollbar moves its layout.
314
+ document.documentElement.style.setProperty('overflow', 'hidden');
315
+ frame.contentWindow?.postMessage({ type: 'afterimage:shown' }, origin);
316
+ }, fade));
295
317
  }
296
318
 
297
- // The viewer stays loaded for next time. The page takes the route and scroll the visitor left the viewer on.
298
- function hide({ route, scrollY } = {}) {
319
+ // The viewer stays loaded for next time. The page takes the route and scroll the visitor left the viewer on, and
320
+ // settles there under the viewer; then the viewer goes. The two look the same by then, so the swap doesn't show.
321
+ function hide({ route, scrollY, panels } = {}) {
299
322
  if (!viewer?.shown) return;
300
323
  viewer.shown = false;
301
324
  const { el } = viewer;
302
- el.removeAttribute('data-shown');
303
- el.removeAttribute('role');
304
- el.removeAttribute('aria-modal');
305
- el.setAttribute('aria-hidden', 'true');
306
325
  el.inert = true;
307
- dock.hidden = false;
308
326
  document.documentElement.style.removeProperty('overflow');
309
327
  if (typeof route === 'string' && route !== location.hash && (route || location.hash)) location.hash = route;
310
- if (typeof scrollY === 'number') requestAnimationFrame(() => requestAnimationFrame(() => window.scrollTo(0, scrollY)));
311
- returnFocus?.focus?.({ preventScroll: true });
312
- // A save the agent made from the terminal meanwhile shows up in the count.
313
- refresh();
328
+ afterFrames(2, () => {
329
+ if (typeof scrollY === 'number') window.scrollTo(0, scrollY);
330
+ setPanelTops(panels);
331
+ afterFrames(1, () => {
332
+ if (viewer?.shown) return; // opened again meanwhile
333
+ el.removeAttribute('data-shown');
334
+ el.removeAttribute('role');
335
+ el.removeAttribute('aria-modal');
336
+ el.setAttribute('aria-hidden', 'true');
337
+ dock.hidden = false;
338
+ returnFocus?.focus?.({ preventScroll: true });
339
+ // A save the agent made from the terminal meanwhile shows up in the count.
340
+ refresh();
341
+ });
342
+ });
314
343
  }
315
344
 
316
345
  window.addEventListener('keydown', event => {
@@ -0,0 +1 @@
1
+ .stage{--label-lift:0px;background:var(--surface);width:100%;height:100%;position:relative;overflow:hidden}.frame-col{pointer-events:none;width:100%;height:100%;position:absolute;top:0;left:0;overflow:hidden}.frame-col[data-role=base]{z-index:1}.frame-box{transform-origin:0 0;pointer-events:none;width:100%;height:100%;position:absolute;top:0;left:0}.stage .frame{pointer-events:auto;position:absolute;inset:0}.stage .frame[data-visible=false]{visibility:hidden}.stage .frame-cover{opacity:0;pointer-events:none}.frame-box[data-gliding]{will-change:transform}.stage[data-dragging]{cursor:ew-resize}.stage[data-dragging] .frame{pointer-events:none}.wipe-divider{z-index:3;cursor:ew-resize;touch-action:none;width:9px;margin-left:-4.5px;position:absolute;top:0;bottom:0}.wipe-line{background:var(--surface);width:1px;position:absolute;top:0;bottom:0;left:4px;box-shadow:0 0 0 .5px #ffffff1f}.wipe-grip{background:var(--surface);width:26px;height:40px;color:var(--muted);box-shadow:0 0 0 1px var(--hairline), 0 2px 8px #0000004d;border-radius:8px;outline:none;place-items:center;transition:color .15s,background-color .15s;display:grid;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.wipe-grip:focus-visible{box-shadow:0 0 0 2px var(--ring), 0 2px 8px #0000004d}.wipe-divider[data-dragging] .wipe-grip{background:var(--raised);color:var(--text)}@media (hover:hover){.wipe-divider:hover .wipe-grip{color:var(--text)}}.compare-label{z-index:2;background:var(--surface);height:28px;color:var(--text);box-shadow:0 0 0 1px var(--hairline), 0 1px 2px #0003, 0 8px 24px #0004;pointer-events:none;white-space:nowrap;border-radius:8px;align-items:center;gap:6px;padding:0 10px;font-size:12px;display:inline-flex;position:absolute}.compare-label .num{font-size:12px}.compare-label-kind{color:var(--muted)}.side-tools{z-index:4;gap:6px;display:flex;position:absolute;top:5px;left:50%;translate:-50%}.zoom-toggle{background:var(--surface);box-shadow:0 0 0 1px var(--hairline), 0 1px 2px #0003, 0 8px 24px #0004;border-radius:999px;padding:2px;display:flex}.zoom-opt{appearance:none;height:24px;color:var(--muted);font:inherit;font-variant-numeric:tabular-nums;white-space:nowrap;cursor:pointer;touch-action:manipulation;background:0 0;border:0;border-radius:999px;align-items:center;margin:0;padding:0 9px;font-size:12px;line-height:1;transition:color .15s;display:inline-flex;position:relative}.zoom-opt[data-on]{color:var(--text);cursor:default}.zoom-opt:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}@media (hover:hover){.zoom-opt:not([data-on]):hover{color:var(--text)}}.zoom-indicator{background:var(--raised);border-radius:999px;position:absolute;inset:0}.zoom-opt-label{position:relative}.sync-chip{appearance:none;background:var(--surface);height:28px;color:var(--muted);font:inherit;white-space:nowrap;cursor:pointer;box-shadow:0 0 0 1px var(--hairline), 0 1px 2px #0003, 0 8px 24px #0004;touch-action:manipulation;border:0;border-radius:999px;align-items:center;gap:6px;margin:0;padding:0 11px 0 10px;font-size:12px;transition:color .15s,background-color .15s;display:inline-flex}.sync-chip[data-on]{color:var(--text)}.sync-chip:focus-visible{outline:2px solid var(--ring);outline-offset:2px}@media (hover:hover){.sync-chip:hover{background:var(--raised)}}.sync-dot{width:6px;height:6px;box-shadow:inset 0 0 0 1.5px var(--muted);border-radius:50%;transition:background-color .15s,box-shadow .15s}.sync-chip[data-on] .sync-dot{background:var(--green);box-shadow:inset 0 0 0 1.5px var(--green)}.pan-bar{z-index:3;touch-action:none;outline:none;height:9px;position:absolute}.pan-track,.pan-thumb{border-radius:999px;height:3px;position:absolute;bottom:2px}.pan-track{background:#ffffff0f;left:0;right:0}.pan-thumb{background:var(--faint);transition:background-color .15s}.pan-bar[data-dragging] .pan-thumb{background:var(--text)}.pan-bar:focus-visible .pan-thumb{background:var(--text);box-shadow:0 0 0 2px var(--ring)}@media (hover:hover){.pan-bar:hover .pan-thumb{background:var(--muted)}}.stage[data-layout=side]{--label-lift:60px}.stage-note{z-index:2;background:var(--surface);height:28px;color:var(--muted);box-shadow:0 0 0 1px var(--hairline), 0 1px 2px #0003, 0 8px 24px #0004;pointer-events:none;white-space:nowrap;border-radius:8px;align-items:center;padding:0 12px;font-size:12px;display:inline-flex;position:absolute;bottom:12px;left:12px}.stage-note .num{color:var(--text)}@media (width<=1100px){.stage-note{bottom:72px}.stage{--label-lift:60px}}.pill-dock{z-index:10;pointer-events:none;justify-content:center;padding:0 16px;display:flex;position:fixed;bottom:16px;left:0;right:0}.pill-dock[data-open]:not([data-phone]){justify-content:flex-end}.pill-dock[data-phone]{bottom:calc(12px + env(safe-area-inset-bottom,0px));padding:0 6px}.dock-body{pointer-events:none;justify-content:center;max-width:100%;display:flex;position:relative}.pill-dock[data-phone] .dock-body{width:100%}.pill{background:var(--surface);max-width:100%;color:var(--text);white-space:nowrap;box-shadow:0 0 0 1px var(--hairline), 0 1px 2px #0000004d, 0 10px 28px #00000040;pointer-events:auto;-webkit-user-select:none;user-select:none;border-radius:999px;align-items:center;gap:2px;padding:5px;font-size:13px;line-height:1;display:flex;position:relative}.pill-seg{border-radius:999px;align-items:center;height:34px;display:flex;position:relative}.pill-sep{background:var(--hairline);flex:none;width:1px;height:18px;margin:0 4px}.pill .muted{color:var(--muted)}.pill-icon{flex:none;display:block}.pill-btn{appearance:none;height:34px;color:inherit;font:inherit;white-space:nowrap;cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;background:0 0;border:0;border-radius:999px;align-items:center;gap:8px;margin:0;padding:0 12px;line-height:1;transition:background-color .15s,color .15s,transform .12s cubic-bezier(.22,1,.36,1);display:inline-flex}.pill-btn:disabled{cursor:default}.pill-btn:active:not(:disabled){transform:scale(.97)}.pill-btn:focus-visible,.pill-step:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}@media (hover:hover){.pill-btn:hover:not(:disabled){background:var(--raised)}}.version-seg{gap:2px}.version-btn{padding:0 8px 0 12px}.pill-dot{width:6px;height:6px;box-shadow:inset 0 0 0 1.5px var(--muted);background:0 0;border-radius:50%;flex:none;transition:background-color .15s,box-shadow .15s}.pill-dot[data-latest]{background:var(--green);box-shadow:inset 0 0 0 1.5px var(--green)}.pill-dock[data-phone] .version-btn{padding:0 12px}.version-field-wrap{display:inline-flex}.version-field{height:24px;color:var(--text);caret-color:var(--text);text-align:center;cursor:text;background:#ffffff0d;border:0;border-radius:6px;outline:none;margin:0;padding:0 6px;font-size:12px;transition:background-color .15s,box-shadow .15s,color .15s}.version-field::selection{background:#ededed40}@media (hover:hover){.version-field:hover{background:var(--raised)}}.version-field:focus{background:#0f0f0f;box-shadow:inset 0 0 0 1px #ffffff40}.version-field[data-invalid]{color:var(--red);background:#0f0f0f;box-shadow:inset 0 0 0 1px #e0705fb3}.version-field[data-invalid]::selection{background:#e0705f40}.version-of{height:34px;color:var(--muted);cursor:text;align-items:center;gap:4px;padding:0 12px 0 6px;display:inline-flex}.version-of .num{font-size:12px}.pill-hint{background:var(--surface);height:28px;color:var(--muted);white-space:nowrap;box-shadow:0 0 0 1px var(--hairline), 0 6px 18px #00000040;pointer-events:none;border-radius:8px;align-items:center;gap:6px;padding:0 10px;font-size:12px;display:flex;position:absolute;bottom:calc(100% + 13px);left:0}.pill-hint[data-error]{color:var(--text)}.pill-hint kbd{background:var(--raised);min-width:18px;height:18px;color:var(--text);font:12px/1 var(--font-mono);border-radius:4px;place-items:center;padding:0 4px;display:inline-grid}.pill-hint kbd+kbd{margin-left:-2px}.tick-strip{cursor:pointer;touch-action:none;border-radius:999px;justify-content:center;align-items:center;min-width:32px;height:34px;padding:0 10px;display:flex;position:relative}.tick-strip[data-dragging]{cursor:grabbing}.tick-strip:has(.tick-input:focus-visible){outline:2px solid var(--ring);outline-offset:-2px}.tick-input{opacity:0;pointer-events:none;width:100%;height:100%;margin:0;position:absolute;inset:0}.tick-track{align-items:flex-end;gap:3px;height:14px;display:flex;position:relative}.tick-track[data-fade=start]{-webkit-mask-image:linear-gradient(90deg,#0000,#000 18px);mask-image:linear-gradient(90deg,#0000,#000 18px)}.tick-track[data-fade=end]{-webkit-mask-image:linear-gradient(270deg,#0000,#000 18px);mask-image:linear-gradient(270deg,#0000,#000 18px)}.tick-track[data-fade=both]{-webkit-mask-image:linear-gradient(90deg,#0000,#000 16px calc(100% - 16px),#0000);mask-image:linear-gradient(90deg,#0000,#000 16px calc(100% - 16px),#0000)}.tick-slot{flex:none;width:2px;height:14px;display:block}.tick{transform-origin:50% 100%;background:#ffffff38;border-radius:1px;width:2px;height:14px;transition:transform .14s cubic-bezier(.22,1,.36,1),background-color .14s;display:block;transform:scaleY(.643)}.tick[data-hover]{background:#ffffff80;transform:scaleY(.857)}.tick[data-role=range]{background:#ffffff80}.tick[data-role=base]{width:4px;box-shadow:inset 0 0 0 1px var(--text);background:0 0;margin-left:-1px;transform:scaleY(1)}.tick-strip[data-range] .tick[data-role=base]{cursor:grab}.tick-strip[data-dragging] .tick[data-role=base]{cursor:grabbing}.tick[data-role=selected]{background:var(--text);transform:scaleY(1)}.tick-tip-anchor{pointer-events:none;position:absolute;bottom:calc(100% + 13px);left:10px}.tick-tip{background:var(--surface);white-space:nowrap;max-width:280px;height:28px;box-shadow:0 0 0 1px var(--hairline), 0 6px 18px #00000040;border-radius:8px;align-items:center;gap:6px;padding:0 10px;font-size:12px;display:flex;transform:translate(-50%)}.tick-tip .num{color:var(--muted)}.tick-tip-sep{color:var(--faint)}.tick-tip-name{text-overflow:ellipsis;min-width:0;color:var(--text);overflow:hidden}.highlight-seg{background-color:#0000;transition:background-color .15s;overflow:hidden}.highlight-seg[data-on]{background-color:var(--raised)}.pill-morph{border-radius:999px;flex:none}.morph-content{align-items:center;display:flex}.auto-width-inner{align-items:center;width:max-content;height:34px;display:flex;position:relative}.seg-content{align-items:center;height:34px;display:flex}.pill-count{min-width:20px;height:20px;color:var(--amber);background:#e5a83b26;border-radius:999px;place-items:center;padding:0 6px;font-size:12px;line-height:1;display:inline-grid}.pill-count[data-zero]{color:var(--muted);background:#ffffff10}.pill-dock[data-phone] .highlight-btn{gap:6px}.highlight-seg[data-on] .pill-btn:hover:not(:disabled){background:#ffffff0a}.stepper-label{padding:0 8px 0 12px}.stepper-label .pill-icon{color:var(--amber)}.stepper-text{align-items:baseline;gap:5px;display:inline-flex}.stepper-n{color:var(--amber);text-align:right;font-size:12px}.stepper-n[data-empty]{color:var(--faint)}.stepper-total{font-size:12px}.stepper-filter-dot{background:var(--amber);border-radius:50%;align-self:center;width:6px;height:6px}.stepper-filter-dot[data-kind=added]{background:var(--green)}.stepper-filter-dot[data-kind=removed]{background:var(--red)}@media (width<=760px){.stepper-text[data-filter] .stepper-prefix{display:none}}.stepper-arrows{padding-right:4px;display:flex}.pill-step{appearance:none;width:26px;height:34px;color:var(--muted);cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;background:0 0;border:0;border-radius:7px;place-items:center;margin:0;padding:0;transition:color .15s;display:grid;position:relative}.pill-step:before{content:"";background:0 0;border-radius:6px;transition:background-color .15s,transform .12s cubic-bezier(.22,1,.36,1);position:absolute;inset:5px 1px}.pill-step svg{position:relative}.pill-step:disabled{color:var(--faint);cursor:default}.pill-step:active:not(:disabled):before{background:#ffffff14;transform:scale(.9)}@media (hover:hover){.pill-step:hover:not(:disabled){color:var(--text)}.pill-step:hover:not(:disabled):before{background:#ffffff14}}.swap-seg{display:grid}.swap-seg>.pill-btn{grid-area:1/1;justify-content:center}.swap-seg>[inert]{pointer-events:none}.compare-btn{color:var(--muted)}@media (hover:hover){.compare-btn:hover:not(:disabled){color:var(--text)}}.latest-btn{background:var(--raised);color:var(--text)}@media (hover:hover){.latest-btn:hover{background:#323232}}.compare-versions{gap:0;padding:0 2px 0 8px}.version-field[data-baseline]:not(:focus):not([data-invalid]){box-shadow:inset 0 0 0 1px #ffffff30}.arrow-btn{color:var(--muted);gap:0;padding:0 6px}@media (hover:hover){.arrow-btn:hover:not(:disabled){color:var(--text)}}.pill-dock[data-phone] .version-btn .muted{color:var(--muted)}.pill-dock[data-phone] .version-btn{gap:5px}.compare-seg{gap:2px}.layout-toggle{background:#0f0f0f;border-radius:999px;padding:2px;display:flex}.layout-opt{appearance:none;height:28px;color:var(--muted);font:inherit;white-space:nowrap;cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;background:0 0;border:0;border-radius:999px;align-items:center;margin:0;padding:0 10px;line-height:1;transition:color .15s;display:inline-flex;position:relative}.layout-opt[data-on]{color:var(--text);cursor:default}.layout-opt:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}@media (hover:hover){.layout-opt:not([data-on]):hover{color:var(--text)}}.layout-indicator{background:var(--raised);border-radius:999px;position:absolute;inset:0}.layout-opt-label{position:relative}.close-btn{width:34px;color:var(--muted);justify-content:center;padding:0}@media (hover:hover){.close-btn:hover{color:var(--text)}}.pill-dock[data-retracted]{justify-content:flex-end}.pill-dock[data-phone][data-retracted] .dock-body{width:auto}.retract-btn{color:var(--muted);padding:0 9px}@media (hover:hover){.retract-btn:hover{color:var(--text)}}.chip{appearance:none;height:36px;font:inherit;cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;border:0;gap:8px;padding:0 14px;font-size:13px;transition:background-color .15s}.chip:focus-visible{outline:2px solid var(--ring);outline-offset:2px}@media (hover:hover){.chip:hover{background:var(--raised)}}.chip .num{font-size:12px}.chip-hint{opacity:0;transition:opacity .15s;left:auto;right:0}@media (hover:hover){.chip:hover .chip-hint{opacity:1;transition-delay:.4s}}.chip:focus-visible .chip-hint{opacity:1}@media (prefers-reduced-motion:reduce){.chip-hint{transition:none}}.panel{background:var(--surface);width:300px;max-height:calc(100dvh - 84px);color:var(--text);text-align:left;white-space:normal;box-shadow:0 0 0 1px var(--hairline), 0 1px 2px #0000004d, 0 16px 40px #00000059;transform-origin:100% 100%;pointer-events:auto;border-radius:14px;flex-direction:column;font-size:13px;line-height:1.4;display:flex;position:absolute;bottom:calc(100% + 8px);right:0;overflow:hidden}.panel-scroll{overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#ffffff26 transparent;flex:auto;min-height:0;overflow-y:auto}.panel .num{font-size:12px}.panel .muted{color:var(--muted)}.panel .amber{color:var(--amber)}:where(.panel) button{appearance:none;color:inherit;font:inherit;cursor:pointer;touch-action:manipulation;-webkit-tap-highlight-color:transparent;background:0 0;border:0;margin:0}:where(.panel) button:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}.sr-only{clip:rect(0 0 0 0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.panel-head{padding:10px 8px 12px 14px}.head-top{align-items:flex-start;gap:8px;display:flex}.panel-name{text-wrap:pretty;overflow-wrap:anywhere;-webkit-line-clamp:2;-webkit-box-orient:vertical;flex:1;min-width:0;margin:0;padding-top:5px;font-size:14px;font-weight:600;line-height:1.3;display:-webkit-box;overflow:hidden}.head-icons{flex:none;gap:2px;display:flex}.icon-btn{width:30px;height:30px;color:var(--muted);border-radius:8px;place-items:center;padding:0;transition:background-color .15s,color .15s,transform .12s cubic-bezier(.22,1,.36,1);display:grid;position:relative}.icon-btn[data-done]{color:var(--green)}.icon-btn:active{transform:scale(.94)}@media (hover:hover){.icon-btn:hover{background:var(--raised);color:var(--text)}.icon-btn[data-done]:hover{color:var(--green)}}.icon-swap{place-items:center;display:grid}.panel-meta{color:var(--muted);margin:3px 6px 0 0;font-size:12px;line-height:20px}.tag{height:20px;color:var(--muted);vertical-align:top;background:#ffffff10;border-radius:999px;align-items:center;margin-left:8px;padding:0 7px;font-size:12px;line-height:1;display:inline-flex}.tag[data-latest]{color:var(--green);background:#6fbf7326}.notes{margin:8px 6px 0 0}.note{color:var(--text);text-wrap:pretty;overflow-wrap:anywhere;margin:0;font-size:13px;line-height:1.45}.note+.note{margin-top:6px}.note[data-clamped]{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.text-btn{height:24px;color:var(--muted);align-items:center;gap:4px;margin-top:2px;padding:0;font-size:12px;transition:color .15s;display:inline-flex}@media (hover:hover){.text-btn:hover{color:var(--text)}}.rows,.rows>*+*{border-top:1px solid var(--hairline)}.row{align-items:center;min-height:44px;display:flex}.row-static{padding:12px 14px}.row-main{text-align:left;flex:1;justify-content:space-between;align-items:center;gap:8px;min-width:0;min-height:44px;padding:0 14px;transition:background-color .15s;display:flex}.row-static-main{cursor:default}@media (hover:hover){button.row-main:hover{background:#ffffff08}}.row-split>.row-main{padding-right:6px}.row-text{flex:1;min-width:0;padding:0 14px}.row-chevron{color:var(--muted);flex:none}.small-btn{background:var(--raised);height:28px;color:var(--text);border-radius:8px;flex:none;margin-right:8px;padding:0 10px;transition:background-color .15s,transform .12s cubic-bezier(.22,1,.36,1)}.small-btn:active{transform:scale(.97)}@media (hover:hover){.small-btn:hover{background:#323232}}.switch{flex:none;place-items:center;width:50px;height:44px;padding:0;display:grid}.switch-track{background:#ffffff26;border-radius:999px;width:30px;height:18px;transition:background-color .16s,opacity .16s,transform .12s cubic-bezier(.22,1,.36,1);display:block;position:relative}.switch-thumb{background:var(--text);border-radius:50%;width:14px;height:14px;transition:transform .2s cubic-bezier(.22,1,.36,1);position:absolute;top:2px;left:2px;box-shadow:0 1px 2px #0000004d}.switch[aria-checked=true] .switch-track{background:var(--amber)}.switch[aria-checked=true] .switch-thumb{transform:translate(12px)}.switch[data-dim] .switch-track{opacity:.45}.switch:active .switch-track{transform:scale(.94)}.switch:focus-visible{outline:none}.switch:focus-visible .switch-track{outline:2px solid var(--ring);outline-offset:2px}.folder{background:#161616;overflow:hidden}.folder-inner{padding:6px}.list-note{color:var(--muted);margin:0;padding:10px 8px;font-size:12px}.filters{gap:2px;margin-bottom:6px;display:flex}.filter{height:28px;color:var(--muted);border-radius:7px;flex:auto;padding:0 8px;font-size:12px;transition:color .15s;position:relative}.filter[aria-pressed=true]{color:var(--text)}@media (hover:hover){.filter:hover{color:var(--text)}}.filter-fill{background:var(--raised);border-radius:7px;position:absolute;inset:0}.filter-label{position:relative}.change-list,.version-list{overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:#ffffff26 transparent;position:relative;overflow-y:auto}.change-list{max-height:260px}.change-list:not([data-end]),.version-list:not([data-end]){-webkit-mask-image:linear-gradient(#000 calc(100% - 32px),#0000);mask-image:linear-gradient(#000 calc(100% - 32px),#0000)}.crow{text-align:left;border-radius:8px;grid-template-columns:20px minmax(0,1fr);align-items:start;gap:10px;width:100%;padding:7px 8px;transition:background-color .15s;display:grid}@media (hover:hover){.crow:hover{background:#ffffff08}}.crow[aria-current]{background:var(--raised)}.crow-badge{width:20px;height:20px;color:var(--amber);background:#e5a83b26;border-radius:6px;place-items:center;margin-top:1px;line-height:1;display:grid}.crow[data-kind=added] .crow-badge{color:var(--green);background:#6fbf7326}.crow[data-kind=removed] .crow-badge{color:var(--red);background:#e0705f26}.crow-text{flex-direction:column;gap:1px;min-width:0;display:flex}.crow-what{color:var(--muted);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}.crow-diff{white-space:nowrap;align-items:baseline;gap:6px;min-width:0;display:flex}.crow-value{text-overflow:ellipsis;flex:0 auto;min-width:2em;overflow:hidden}.crow-value.is-before{color:var(--muted);flex-shrink:3}.crow-arrow{color:var(--faint);flex:none}.crow-swatch{vertical-align:-1px;border-radius:3px;width:10px;height:10px;margin-right:5px;display:inline-block;box-shadow:inset 0 0 0 1px #ffffff2e}.list-more{width:100%;height:32px;color:var(--muted);border-radius:8px;justify-content:center;align-items:center;gap:4px;margin-top:2px;font-size:12px;transition:background-color .15s,color .15s;display:flex}@media (hover:hover){.list-more:hover{color:var(--text);background:#ffffff08}}.search{background:var(--raised);height:32px;color:var(--faint);cursor:text;border-radius:8px;align-items:center;gap:8px;margin-bottom:4px;padding:0 10px;transition:box-shadow .15s,color .15s;display:flex}.search:focus-within{color:var(--muted);box-shadow:inset 0 0 0 1px #ffffff40}.search-input{min-width:0;height:100%;color:var(--text);caret-color:var(--text);font:inherit;background:0 0;border:0;outline:none;flex:1;margin:0;padding:0;font-size:13px}.search-input::placeholder{color:var(--faint)}.search-input::-webkit-search-cancel-button{display:none}.version-list{max-height:286px}.vgroup-label{z-index:1;height:26px;color:var(--faint);background:#161616;margin:0;padding:8px 8px 0;font-size:12px;font-weight:500;line-height:18px;position:sticky;top:0}.vgroup-label.num{font-weight:400}.vrow{text-align:left;border-radius:8px;grid-template-columns:24px minmax(0,1fr) auto 3ch;align-items:center;gap:8px;width:100%;height:32px;padding:0 8px;transition:background-color .15s;display:grid}@media (hover:hover){.vrow:hover{background:#ffffff08}}.vrow[aria-current]{background:var(--raised)}.vrow-n{color:var(--faint)}.vrow[aria-current] .vrow-n{color:var(--text)}.vrow-name{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.vrow-time{color:var(--faint)}.vrow-count{color:var(--amber);text-align:right}.vrow-count[data-zero]{color:var(--faint)}.vrow-count-in{animation:.22s cubic-bezier(.22,1,.36,1) count-in}@keyframes count-in{0%{opacity:0}}.panel[data-phone]{width:auto;max-height:calc(100dvh - 120px - env(safe-area-inset-bottom,0px));transform-origin:50% 100%;border-radius:18px;bottom:calc(100% + 6px);left:0;right:0}.grab{cursor:grab;touch-action:none;flex:none;place-items:center;height:20px;margin-bottom:-6px;display:grid}.grab-bar{background:#fff3;border-radius:2px;width:36px;height:4px}.panel[data-phone] .panel-head{padding-top:6px}.panel[data-phone] .row,.panel[data-phone] .row-main{min-height:48px}.panel[data-phone] .vrow{height:44px}.panel[data-phone] .crow{padding-block:10px}.panel[data-phone] .filter{height:36px}.panel[data-phone] .search{height:40px}.panel[data-phone] .search-input{font-size:16px}.panel[data-phone] .list-more{height:44px}.panel-tip{border-top:1px solid var(--hairline);color:var(--muted);text-wrap:balance;flex:none;margin:0;padding:9px 14px 10px;font-size:12px;line-height:20px}.panel-tip kbd{background:var(--raised);min-width:18px;height:18px;color:var(--text);font:12px/1 var(--font-mono);vertical-align:1px;border-radius:4px;place-items:center;padding:0 4px;display:inline-grid}.panel-tip kbd+kbd{margin-left:2px}.tip-item{white-space:nowrap}.help-scrim{z-index:20;background:#0006;place-items:center;padding:16px;display:grid;position:fixed;inset:0}.help-sheet{background:var(--surface);width:340px;max-width:100%;max-height:100%;color:var(--text);box-shadow:0 0 0 1px var(--hairline), 0 1px 2px #0000004d, 0 16px 40px #00000059;border-radius:14px;flex-direction:column;font-size:13px;line-height:1.4;display:flex;overflow:hidden}.help-head{justify-content:space-between;align-items:center;gap:8px;padding:8px 8px 4px 14px;display:flex}.help-title{margin:0;font-size:14px;font-weight:600;line-height:30px}.help-close{appearance:none;width:30px;height:30px;color:var(--muted);cursor:pointer;touch-action:manipulation;background:0 0;border:0;border-radius:8px;place-items:center;margin:0;padding:0;transition:background-color .15s,color .15s,transform .12s cubic-bezier(.22,1,.36,1);display:grid}.help-close:active{transform:scale(.94)}.help-close:focus-visible{outline:2px solid var(--ring);outline-offset:-2px}@media (hover:hover){.help-close:hover{background:var(--raised);color:var(--text)}}.help-body{overscroll-behavior:contain;padding:4px 14px 14px;overflow-y:auto}.help-group+.help-group{margin-top:12px}.help-group-name{color:var(--faint);margin:0 0 4px;font-size:12px;font-weight:500;line-height:18px}.help-rows{margin:0}.help-row{grid-template-columns:56px 1fr;align-items:baseline;column-gap:8px;padding:4px 0;display:grid}.help-keys{gap:3px;margin:0;display:flex}.help-text{color:var(--text);text-wrap:pretty;margin:0}.help-sheet kbd{background:var(--raised);min-width:20px;height:20px;color:var(--text);font:12px/1 var(--font-mono);border-radius:5px;place-items:center;padding:0 5px;display:inline-grid}:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--surface:#1b1b1b;--raised:#2a2a2a;--hairline:#ffffff14;--text:#ededed;--muted:#8e8e8e;--faint:#5c5c5c;--amber:#e5a83b;--green:#6fbf73;--red:#e0705f;--ring:#ededed8c;--font-ui:system-ui, -apple-system, "Segoe UI", sans-serif;--font-mono:ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace}*{box-sizing:border-box}html,body,#root{height:100%}body{background:var(--surface);color:var(--text);font:14px/1.4 var(--font-ui);-webkit-font-smoothing:antialiased;margin:0}.num{font-family:var(--font-mono);font-variant-numeric:tabular-nums}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:0s!important;animation-duration:0s!important}}.frame{background:var(--surface);border:0;width:100%;height:100%;display:block}.load-error{text-align:center;place-content:center;justify-items:center;gap:8px;height:100%;display:grid}.load-error h1{margin:0;font-size:16px;font-weight:600}.load-error p{color:var(--muted);margin:0}.load-error button{border:1px solid var(--hairline);background:var(--raised);height:32px;color:var(--text);font:inherit;cursor:pointer;border-radius:8px;margin-top:8px;padding:0 14px}.load-error button:hover{background:#323232}.load-error button:focus-visible{outline:2px solid var(--amber);outline-offset:2px}