# Agent Screen

> An agent's live screen with an expandable viewer and recording.

- **Slug:** `agent-screen`
- **Kind:** component · **Category:** Agent
- **Source:** ported from [Beautiful UI](https://github.com/slev12397/beautiful-ui/blob/main/components/primitives/AgentScreen.tsx) (MIT)
- **Docs page:** https://viewership.ai/ui/components/agent-screen
- **All files in one bundle:** https://viewership.ai/ui/components/agent-screen.bundle.txt
- **Structured JSON:** https://viewership.ai/ui/components/agent-screen.json

## Install

1. Import the foundation tokens once (download: https://viewership.ai/ui/raw/foundation.css.txt):

```astro
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
---
```

2. Copy this file into `src/components/viewership/agent-screen/`:

   - `AgentScreen.astro` (raw: https://viewership.ai/ui/raw/agent-screen/AgentScreen.astro.txt)

3. Copy its registry dependencies, each into its own sibling folder under `src/components/viewership/`:

   - `button`: `Button.astro` (docs: https://viewership.ai/ui/components/button.md)

npm packages: none.

## Usage

```astro
---
import AgentScreen from "../components/viewership/agent-screen/AgentScreen.astro";
---

<AgentScreen />
```

## Example

The gallery demo, verbatim. Use the import path from Usage instead of `./`.

```astro
---
import AgentScreen from "./AgentScreen.astro";

interface Props {
  variant?: string;
}
const { variant = "Working" } = Astro.props;
---

<div class="stage">
  <AgentScreen agentName="Reporting agent" state={variant === "Loading" ? "connecting" : "live"} />
</div>

<style>
  .stage {
    display: flex;
    width: 100%;
    justify-content: center;
  }
</style>
```

## Variants

Demo variants: `Working`, `Loading`.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `agentName` | `string` | `"Agent"` | Caption and viewer title. |
| `state` | `"live" \| "connecting"` | `"live"` | connecting disables opening. |
| `src` | `string` |  | Image or video URL. Falls back to a sample desktop. |
| `<slot />` | `HTML` |  | Custom screen markup. Size in em or cqw. |
| `labels` | `Partial<AgentScreenLabels>` |  | Text overrides. |
| `vs-agent-screen-open / -close` | `CustomEvent` |  | Fires when the viewer opens or collapses. |
| `vs-agent-screen-record` | `CustomEvent<{ recording: true }>` |  | Fires when recording starts. |
| `vs-agent-screen-end` | `CustomEvent<{ seconds: number }>` |  | Fires when recording ends. |

## Source

### `src/components/viewership/agent-screen/AgentScreen.astro`

```astro
---
/**
 * AgentScreen: a card showing an agent's screen, with a viewer and recording.
 *
 * Ported from Beautiful UI `components/primitives/AgentScreen.tsx`
 * (MIT, © 2026 Shane Levine).
 *
 * Hover or focus shows "Open", which expands a native <dialog> viewer.
 * Recording keeps running when the viewer closes. Custom markup goes in the
 * default slot (size in em or container units), or pass `src`.
 *
 * Events (bubbling): vs-agent-screen-open, -close, -record, -end { seconds }
 *
 * @example
 * <AgentScreen agentName="Reporting agent" />
 */
import type { HTMLAttributes } from "astro/types";
import Button from "../button/Button.astro";

export interface AgentScreenLabels {
  /** "{name}" is replaced with agentName. */
  screen: string;
  open: string;
  collapse: string;
  teach: string;
  end: string;
  connecting: string;
  recording: string;
  recordingStarted: string;
  /** "{time}" is replaced. */
  recordingEnded: string;
}

type Props = HTMLAttributes<"div"> & {
  agentName?: string;
  state?: "live" | "connecting";
  /** Image or video URL for a real stream (replaces the faux desktop). */
  src?: string;
  labels?: Partial<AgentScreenLabels>;
};

const { agentName = "Agent", state = "live", src, labels, class: className, ...rest } = Astro.props;

const l: AgentScreenLabels = {
  screen: "{name}'s screen",
  open: "Open",
  collapse: "Collapse",
  teach: "Teach a task",
  end: "End",
  connecting: "Connecting to agent's screen",
  recording: "Recording",
  recordingStarted: "Recording started",
  recordingEnded: "Recording ended at {time}",
  ...labels,
};

const uid = `vs-agent-screen-${Math.random().toString(36).slice(2, 8)}`;
const live = state === "live";
const screenName = l.screen.replace("{name}", agentName);
const isVideo = src ? /\.(mp4|webm|mov|m4v)(\?|$)/i.test(src) : false;
const R = 12; // (26 - 2) / 2
const C = 2 * Math.PI * R;
const cursorPath =
  "M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z";
const lots = [
  { name: "Spring Trail Search", origin: "Google Ads", price: "$7.80", target: true },
  { name: "Brand Defense", origin: "Google Ads", price: "$6.95" },
  { name: "Camp Stove Retarget", origin: "Meta Ads", price: "$5.40" },
  { name: "Gear Guide Video", origin: "YouTube", price: "$9.10" },
];
const mail = { from: "Dana Whitfield", subject: "Weekly report for Northwind Outdoor", time: "9:38" };
const dock = ["accent", "green", "orange", "red", "ink-2", "accent", "green", "orange"];
---

<div
  class:list={["vs-agent-screen", className]}
  data-state={state}
  data-labels={JSON.stringify({ recordingStarted: l.recordingStarted, recordingEnded: l.recordingEnded })}
  {...rest}
>
  <div class="frame" data-frame>
    <div class="slot" data-slot>
      <div class="screen" data-screen>
        {src ? (
          isVideo ? (
            <video class="media" src={src} autoplay muted loop playsinline></video>
          ) : (
            <img class="media" src={src} alt="" />
          )
        ) : (
          <slot>
            <div class="desk" aria-hidden="true">
              <div class="menubar">
                <span class="mb-strong">Browser</span><span>File</span><span>Edit</span><span>View</span>
                <span class="mb-right">Mon Oct 12 · 9:41</span>
              </div>

              <div class="win win--mail">
                <div class="win-bar"><span class="lights"><i></i><i></i><i></i></span></div>
                <div class="mail">
                  <div class="mail-head">
                    <span class="mail-avatar">D</span>
                    <span class="mail-meta">
                      <span class="mail-from">{mail.from}<span class="mail-time">{mail.time}</span></span>
                      <span class="mail-to">To: team@northwind.test</span>
                    </span>
                  </div>
                  <span class="mail-title">{mail.subject}</span>
                  <span class="line" style="width: 92%"></span>
                  <span class="line" style="width: 80%"></span>
                  <span class="line" style="width: 86%"></span>
                  <span class="line" style="width: 44%"></span>
                </div>
              </div>

              <div class="win win--browser">
                <div class="win-bar">
                  <span class="lights"><i></i><i></i><i></i></span>
                  <span class="tab"><span class="tab-icon"></span>Ads report</span>
                </div>
                <div class="url">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"></path></svg>
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"></path></svg>
                  <span class="url-field">reports.northwind.test/ads</span>
                </div>
                <div class="page">
                  <div class="page-head">
                    <span class="logo">N</span>
                    <span class="page-title">Campaign spend</span>
                    <span class="page-btn">Queue · 2</span>
                  </div>
                  <div class="search"><span class="search-field">campaign, channel…</span><span class="search-go">Search</span></div>
                  {lots.map((lot) => (
                    <div class:list={["lot", lot.target && "is-target"]}>
                      <span class="lot-avatar"></span>
                      <span class="lot-text">
                        <span class="lot-name">{lot.name}</span>
                        <span class="lot-origin">{lot.origin} · last 7 days</span>
                      </span>
                      <span class="lot-price">{lot.price}</span>
                      <span class="lot-cta">Review</span>
                    </div>
                  ))}
                </div>
              </div>

              <div class="dock">
                {dock.map((tone) => <span class="dock-icon" style={`background: var(--vs-${tone})`}></span>)}
              </div>
            </div>
          </slot>
        )}
      </div>
    </div>

    {live ? (
      <>
        <svg class="cursor cursor--rest" width="30" height="30" viewBox="0 0 24 24" aria-hidden="true"><path d={cursorPath}></path></svg>
        <div class="reveal">
          <span class="reveal-btn">
            <Button variant="accent" size="sm" aria-haspopup="dialog" aria-controls={`${uid}-viewer`} aria-label={`${l.open} ${screenName}`} data-open>
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                <polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" y1="3" x2="14" y2="10"></line><line x1="3" y1="21" x2="10" y2="14"></line>
              </svg>
              {l.open}
            </Button>
          </span>
        </div>
        <span class="rec-badge" hidden data-rec-badge>
          <span class="rec-dot" aria-hidden="true"></span>
          <span class="vh">{l.recording}</span>
          <span data-time>00:00</span>
        </span>
      </>
    ) : (
      <div class="loading" role="status">
        <svg class="spinner" width="26" height="26" aria-hidden="true">
          <circle cx="13" cy="13" r={R} fill="none" stroke="rgba(255,255,255,0.18)" stroke-width="2"></circle>
          <circle cx="13" cy="13" r={R} fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-dasharray={`${C * 0.28} ${C * 0.72}`}></circle>
        </svg>
        <span class="loading-text">{l.connecting}</span>
      </div>
    )}
  </div>

  <div class="caption">{screenName}</div>

  {live && (
    <dialog class="viewer" id={`${uid}-viewer`} aria-labelledby={`${uid}-name`} data-dialog>
      <div class="panel">
        <div class="titlebar">
          <div class="tb-left">
            <span class="tb-name" id={`${uid}-name`}>{agentName}</span>
            <span class="rec-pill" hidden data-rec-pill>
              <span class="rec-dot" aria-hidden="true"></span>
              <span class="vh">{l.recording}</span>
              <span data-time>00:00</span>
            </span>
          </div>
          <div class="controls">
            <Button variant="secondary" size="sm" class="teach" data-teach>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                <circle cx="12" cy="12" r="9"></circle><circle cx="12" cy="12" r="3.5" fill="currentColor" stroke="none"></circle>
              </svg>
              {l.teach}
            </Button>
            <button type="button" class="end" hidden data-end>
              <span class="end-square" aria-hidden="true"></span>
              {l.end}
            </button>
            <button type="button" class="icon-btn" aria-label={l.collapse} data-collapse>
              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                <polyline points="4 14 10 14 10 20"></polyline><polyline points="20 10 14 10 14 4"></polyline><line x1="14" y1="10" x2="21" y2="3"></line><line x1="3" y1="21" x2="10" y2="14"></line>
              </svg>
            </button>
          </div>
        </div>
        <div class="view" data-view>
          <svg class="cursor cursor--follow" width="30" height="30" viewBox="0 0 24 24" aria-hidden="true" data-follow><path d={cursorPath}></path></svg>
        </div>
      </div>
    </dialog>
  )}
  <span class="vh" aria-live="polite" data-live></span>
</div>

<script>
  const fmt = (total: number) =>
    `${String(Math.floor(total / 60)).padStart(2, "0")}:${String(total % 60).padStart(2, "0")}`;

  function init(root: HTMLElement) {
    if (root.dataset.state !== "live") return;
    const frame = root.querySelector<HTMLElement>("[data-frame]")!;
    const slot = root.querySelector<HTMLElement>("[data-slot]")!;
    const screen = root.querySelector<HTMLElement>("[data-screen]")!;
    const openBtn = root.querySelector<HTMLButtonElement>("[data-open]")!;
    const dialog = root.querySelector<HTMLDialogElement>("[data-dialog]")!;
    const view = root.querySelector<HTMLElement>("[data-view]")!;
    const follow = root.querySelector<SVGElement>("[data-follow]")!;
    const teach = root.querySelector<HTMLButtonElement>("[data-teach]")!;
    const end = root.querySelector<HTMLButtonElement>("[data-end]")!;
    const collapse = root.querySelector<HTMLButtonElement>("[data-collapse]")!;
    const badge = root.querySelector<HTMLElement>("[data-rec-badge]")!;
    const pill = root.querySelector<HTMLElement>("[data-rec-pill]")!;
    const times = [...root.querySelectorAll<HTMLElement>("[data-time]")];
    const live = root.querySelector<HTMLElement>("[data-live]")!;
    const labels = JSON.parse(root.dataset.labels || "{}") as { recordingStarted: string; recordingEnded: string };

    let secs = 0;
    let tick: number | undefined;

    const open = () => {
      if (dialog.open) return;
      view.prepend(screen); // one screen node: a stream keeps playing
      dialog.showModal();
      document.documentElement.style.overflow = "hidden";
      root.dispatchEvent(new CustomEvent("vs-agent-screen-open", { bubbles: true }));
    };

    dialog.addEventListener("close", () => {
      slot.append(screen);
      follow.style.opacity = "0";
      document.documentElement.style.overflow = "";
      openBtn.focus({ preventScroll: true });
      root.dispatchEvent(new CustomEvent("vs-agent-screen-close", { bubbles: true }));
    });
    // A click on the dialog box itself (outside the panel) is a backdrop click.
    dialog.addEventListener("click", (event) => {
      if (event.target === dialog) dialog.close();
    });
    collapse.addEventListener("click", () => dialog.close());

    frame.addEventListener("click", open);
    openBtn.addEventListener("click", (event) => {
      event.stopPropagation();
      open();
    });

    const paintTime = () => times.forEach((t) => (t.textContent = fmt(secs)));
    const setRecording = (on: boolean) => {
      window.clearInterval(tick);
      teach.hidden = on;
      end.hidden = !on;
      badge.hidden = !on;
      pill.hidden = !on;
      root.toggleAttribute("data-recording", on);
      if (on) {
        secs = 0;
        paintTime();
        tick = window.setInterval(() => {
          secs += 1;
          paintTime();
        }, 1000);
      }
    };

    teach.addEventListener("click", () => {
      setRecording(true);
      end.focus();
      live.textContent = labels.recordingStarted;
      root.dispatchEvent(new CustomEvent("vs-agent-screen-record", { bubbles: true, detail: { recording: true } }));
    });
    end.addEventListener("click", () => {
      const seconds = secs;
      setRecording(false);
      teach.focus();
      live.textContent = labels.recordingEnded.replace("{time}", fmt(seconds));
      root.dispatchEvent(new CustomEvent("vs-agent-screen-end", { bubbles: true, detail: { seconds } }));
    });

    // The viewer hides the system pointer and draws the agent-style one instead.
    view.addEventListener("mousemove", (event) => {
      const r = view.getBoundingClientRect();
      follow.style.transform = `translate(${event.clientX - r.left}px, ${event.clientY - r.top}px)`;
      follow.style.opacity = "1";
    });
    view.addEventListener("mouseleave", () => (follow.style.opacity = "0"));

    document.addEventListener(
      "astro:before-swap",
      () => {
        window.clearInterval(tick);
        if (dialog.open) dialog.close();
      },
      { once: true },
    );
  }

  const boot = () =>
    document.querySelectorAll<HTMLElement>(".vs-agent-screen:not([data-vs-ready])").forEach((root) => {
      root.dataset.vsReady = "";
      init(root);
    });
  boot();
  document.addEventListener("astro:page-load", boot);
</script>

<style>
  .vs-agent-screen {
    width: 100%;
    max-width: 340px;
  }
  .vh {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }

  /* ── resting card ── */
  .frame {
    position: relative;
    aspect-ratio: 2964 / 1856;
    overflow: hidden;
    border-radius: var(--vs-radius-window);
    background: var(--vs-inset);
    box-shadow: var(--vs-shadow-card);
    animation: vs-fade-up 380ms var(--vs-ease-out) both;
    transition: box-shadow 150ms ease-out;
  }
  [data-state="live"] .frame {
    cursor: pointer;
  }
  [data-state="live"] .frame:hover {
    box-shadow: var(--vs-shadow-raised);
  }
  .slot,
  .screen {
    position: absolute;
    inset: 0;
  }
  .screen {
    overflow: hidden;
    background: var(--vs-inset);
    container-type: inline-size;
  }
  .media {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .cursor {
    position: absolute;
    pointer-events: none;
    fill: #111318;
    stroke: #fff;
    stroke-width: 1.4;
    stroke-linejoin: round;
    filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, 0.35));
  }
  .cursor--rest {
    left: 42%;
    top: 53%;
  }
  .cursor--follow {
    top: 0;
    left: 0;
    z-index: 10;
    opacity: 0;
  }

  .reveal {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(17, 19, 24, 0);
    transition: background-color 150ms ease-out;
  }
  .reveal-btn {
    opacity: 0;
    transform: translateY(4px);
    transition:
      opacity 150ms ease-out,
      transform 150ms ease-out;
  }
  .frame:hover .reveal,
  .frame:focus-within .reveal {
    background: rgba(17, 19, 24, 0.18);
  }
  .frame:hover .reveal-btn,
  .frame:focus-within .reveal-btn {
    opacity: 1;
    transform: translateY(0);
  }
  @media (hover: none) {
    /* No hover on touch: keep the way in visible. */
    .reveal-btn {
      opacity: 1;
      transform: none;
    }
  }

  .rec-badge,
  .rec-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border-radius: var(--vs-radius-pill);
    color: var(--vs-red);
    font-size: 11.5px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
  }
  .rec-badge {
    position: absolute;
    top: 8px;
    left: 8px;
    padding: 2px 8px 2px 6px;
    background: var(--vs-surface);
    box-shadow: var(--vs-shadow-btn);
    animation: vs-pop-in 200ms var(--vs-ease-out) both;
  }
  .rec-pill {
    flex-shrink: 0;
    padding: 2px 8px 2px 6px;
    background: var(--vs-red-tint);
  }
  .rec-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--vs-red);
    animation: vs-pulse 1.1s ease-in-out infinite;
  }

  .loading {
    position: absolute;
    inset: 0;
    background: #000;
  }
  .spinner {
    position: absolute;
    top: calc(50% - 13px);
    left: calc(50% - 13px);
    display: block;
    animation: vs-spin 1.1s linear infinite;
  }
  .loading-text {
    position: absolute;
    top: calc(50% + 28px);
    right: 0;
    left: 0;
    color: rgba(255, 255, 255, 0.7);
    font-size: 12.5px;
    font-weight: 500;
    text-align: center;
  }

  .caption {
    margin-top: 10px;
    padding: 0 2px;
    overflow: hidden;
    color: var(--vs-ink);
    font-size: 13px;
    font-weight: 500;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ── viewer ── */
  .viewer {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    box-sizing: border-box;
    margin: 0;
    padding: 16px;
    border: 0;
    background: transparent;
    overflow: hidden;
  }
  .viewer[open] {
    display: flex;
    align-items: center;
    justify-content: center;
  }
  @media (min-width: 640px) {
    .viewer {
      padding: 24px;
    }
  }
  .viewer::backdrop {
    background: rgba(0, 0, 0, 0.6);
    animation: vs-fade-in 180ms ease-out both;
  }
  :global(:root[data-theme="dark"]) .viewer::backdrop {
    background: rgba(0, 0, 0, 0.75);
  }
  @media (prefers-color-scheme: dark) {
    :global(:root:not([data-theme="light"])) .viewer::backdrop {
      background: rgba(0, 0, 0, 0.75);
    }
  }
  .panel {
    position: relative;
    display: flex;
    max-height: 100%;
    flex-direction: column;
    overflow: hidden;
    padding: 0 8px 8px;
    border-radius: 16px;
    background: var(--vs-surface);
    box-shadow: var(--vs-shadow-overlay);
    animation: vs-pop-in 240ms var(--vs-ease-out) both;
  }
  .titlebar {
    display: flex;
    height: 44px;
    flex-shrink: 0;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 0 6px;
  }
  .tb-left {
    display: flex;
    min-width: 0;
    align-items: center;
    gap: 8px;
  }
  .tb-name {
    overflow: hidden;
    color: var(--vs-ink);
    font-size: 13px;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .controls {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    gap: 6px;
  }
  .controls :global(.teach) {
    gap: 4px;
    padding-left: 6px;
  }
  .end {
    display: inline-flex;
    height: 27px;
    align-items: center;
    gap: 6px;
    padding: 0 12px 0 10px;
    border: none;
    border-radius: var(--vs-radius-pill);
    background: var(--vs-red);
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14);
    color: var(--vs-surface);
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    line-height: 1;
    cursor: pointer;
    transition:
      transform 150ms ease-out,
      filter 150ms ease-out;
  }
  .end:hover {
    filter: brightness(0.95);
  }
  .end:active {
    transform: scale(0.96);
  }
  .end-square {
    width: 10px;
    height: 10px;
    border-radius: 2px;
    background: white;
  }
  .icon-btn {
    display: inline-flex;
    width: 28px;
    height: 28px;
    align-items: center;
    justify-content: center;
    padding: 0;
    border: none;
    border-radius: var(--vs-radius-control);
    background: transparent;
    color: var(--vs-ink-3);
    cursor: pointer;
    transition:
      background-color 100ms ease-out,
      color 100ms ease-out;
  }
  .icon-btn:hover {
    background: var(--vs-hover);
    color: var(--vs-ink);
  }
  :is(.end, .icon-btn):focus-visible {
    outline: none;
    box-shadow: var(--vs-focus-ring);
  }
  .view {
    position: relative;
    width: min(960px, 90vw, calc((100vh - 150px) * 2964 / 1856));
    min-height: 0;
    aspect-ratio: 2964 / 1856;
    overflow: hidden;
    border-radius: 8px;
    background: var(--vs-inset);
    cursor: none;
  }

  /* ── faux desktop: 1em = 1/34 of the screen width, so it scales with any frame ── */
  .desk {
    position: absolute;
    inset: 0;
    overflow: hidden;
    background: linear-gradient(
      140deg,
      color-mix(in oklch, var(--vs-accent) 62%, var(--vs-page)) 0%,
      color-mix(in oklch, var(--vs-orange) 55%, var(--vs-page)) 62%,
      color-mix(in oklch, var(--vs-red) 50%, var(--vs-page)) 100%
    );
    color: var(--vs-ink);
    font-size: calc(100cqw / 34);
    line-height: 1.25;
    user-select: none;
  }
  .menubar {
    position: absolute;
    inset: 0 0 auto;
    display: flex;
    height: 1.3em;
    align-items: center;
    gap: 0.9em;
    padding: 0 0.9em;
    background: color-mix(in oklch, var(--vs-surface) 45%, transparent);
    font-size: 0.6em;
  }
  .mb-strong {
    font-weight: 600;
  }
  .mb-right {
    margin-left: auto;
  }

  .win {
    position: absolute;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    border-radius: 0.7em;
    background: var(--vs-surface);
    box-shadow:
      0 0 0 0.05em oklch(0 0 0 / 0.12),
      0 0.6em 1.6em oklch(0 0 0 / 0.22);
  }
  /* the browser overlaps the mail window's left edge by a hair, like stacked windows */
  .win--mail {
    top: 10%;
    left: 53%;
    width: 42%;
    height: 50%;
  }
  .win--browser {
    top: 15%;
    left: 4%;
    width: 50%;
    height: 70%;
  }
  .win-bar {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    gap: 0.5em;
    height: 1.7em;
    padding: 0 0.7em;
    border-bottom: 0.05em solid var(--vs-line);
    background: var(--vs-inset);
  }
  .lights {
    display: flex;
    gap: 0.3em;
  }
  .lights i {
    width: 0.55em;
    height: 0.55em;
    border-radius: 50%;
    background: var(--vs-red);
  }
  .lights i:nth-child(2) {
    background: var(--vs-orange);
  }
  .lights i:nth-child(3) {
    background: var(--vs-green);
  }
  .tab {
    display: flex;
    align-items: center;
    gap: 0.35em;
    align-self: flex-end;
    height: 1.3em;
    padding: 0 0.6em;
    border-radius: 0.4em 0.4em 0 0;
    background: var(--vs-surface);
    font-size: 0.55em;
  }
  .tab-icon {
    width: 0.8em;
    height: 0.8em;
    border-radius: 0.2em;
    background: var(--vs-accent);
  }
  .url {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    gap: 0.5em;
    padding: 0.35em 0.7em;
    border-bottom: 0.05em solid var(--vs-line);
    color: var(--vs-ink-3);
  }
  .url svg {
    width: 0.9em;
    height: 0.9em;
  }
  .url-field {
    flex: 1;
    padding: 0.2em 0.8em;
    border-radius: 99em;
    background: var(--vs-field);
    font-family: var(--vs-font-sans);
    font-variant-numeric: tabular-nums;
    font-size: 0.5em;
  }
  .page {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 0.55em;
    padding: 0.8em 0.9em;
    overflow: hidden;
  }
  .page-head {
    display: flex;
    align-items: center;
    gap: 0.5em;
  }
  .logo {
    display: grid;
    width: 1.2em;
    height: 1.2em;
    place-items: center;
    border-radius: 0.3em;
    background: var(--vs-accent-tint);
    color: var(--vs-accent-ink);
    font-size: 0.7em;
    font-weight: 700;
  }
  .page-title {
    font-size: 0.7em;
    font-weight: 600;
  }
  .page-btn {
    margin-left: auto;
    padding: 0.25em 0.8em;
    border-radius: 99em;
    background: var(--vs-inset);
    box-shadow: var(--vs-shadow-btn);
    font-size: 0.45em;
  }
  .search {
    display: flex;
    gap: 0.4em;
    padding: 0.35em;
    border-radius: 0.6em;
    background: var(--vs-inset);
  }
  .search-field {
    flex: 1;
    padding: 0.2em 0.7em;
    border-radius: 99em;
    background: var(--vs-surface);
    box-shadow: var(--vs-shadow-hairline);
    color: var(--vs-ink-3);
    font-size: 0.45em;
  }
  .search-go {
    padding: 0.2em 0.8em;
    border-radius: 99em;
    background: var(--vs-accent);
    color: var(--vs-on-accent);
    font-size: 0.45em;
  }
  .lot {
    display: flex;
    align-items: center;
    gap: 0.6em;
    padding: 0.25em 0.35em;
    border-radius: 0.45em;
  }
  .lot.is-target {
    background: var(--vs-accent-tint);
    box-shadow: 0 0 0 0.08em var(--vs-accent);
  }
  .lot-avatar {
    width: 1.5em;
    height: 1.5em;
    flex-shrink: 0;
    border-radius: 50%;
    background: color-mix(in oklch, var(--vs-orange) 35%, var(--vs-surface));
  }
  .lot-text {
    display: flex;
    min-width: 0;
    flex: 1;
    flex-direction: column;
  }
  .lot-name {
    font-size: 0.55em;
    font-weight: 600;
  }
  .lot-origin {
    color: var(--vs-ink-3);
    font-size: 0.45em;
  }
  .lot-price {
    color: var(--vs-ink-2);
    font-size: 0.5em;
    font-variant-numeric: tabular-nums;
  }
  .lot-cta {
    padding: 0.2em 0.7em;
    border-radius: 99em;
    background: var(--vs-inset);
    box-shadow: var(--vs-shadow-hairline);
    font-size: 0.45em;
  }
  .is-target .lot-cta {
    background: var(--vs-accent);
    box-shadow: none;
    color: var(--vs-on-accent);
  }

  .mail {
    display: flex;
    flex: 1;
    min-height: 0;
    flex-direction: column;
    gap: 0.45em;
    padding: 0.8em 0.9em;
  }
  .mail-head {
    display: flex;
    align-items: center;
    gap: 0.5em;
    padding-bottom: 0.5em;
    border-bottom: 0.05em solid var(--vs-line);
  }
  .mail-avatar {
    display: grid;
    width: 1.6em;
    height: 1.6em;
    flex-shrink: 0;
    place-items: center;
    border-radius: 50%;
    background: var(--vs-green);
    color: var(--vs-surface);
    font-size: 0.6em;
    font-weight: 600;
  }
  .mail-meta {
    display: flex;
    min-width: 0;
    flex: 1;
    flex-direction: column;
  }
  .mail-from {
    display: flex;
    justify-content: space-between;
    font-size: 0.55em;
    font-weight: 600;
  }
  .mail-time {
    color: var(--vs-ink-3);
    font-weight: 400;
  }
  .mail-to {
    color: var(--vs-ink-3);
    font-size: 0.45em;
  }
  .mail-title {
    margin: 0.2em 0;
    font-size: 0.65em;
    font-weight: 600;
  }
  .line {
    height: 0.3em;
    border-radius: 99em;
    background: var(--vs-line-strong);
  }

  .dock {
    position: absolute;
    bottom: 2.5%;
    left: 50%;
    display: flex;
    gap: 0.45em;
    padding: 0.4em 0.5em;
    border-radius: 0.8em;
    background: color-mix(in oklch, var(--vs-surface) 45%, transparent);
    box-shadow: 0 0 0 0.05em color-mix(in oklch, var(--vs-surface) 60%, transparent);
    transform: translateX(-50%);
  }
  .dock-icon {
    width: 1.5em;
    height: 1.5em;
    border-radius: 0.4em;
  }

  @media (prefers-reduced-motion: reduce) {
    .frame,
    .panel,
    .rec-badge,
    .viewer::backdrop,
    .rec-dot,
    .spinner {
      animation: none;
    }
    .reveal,
    .reveal-btn {
      transition: none;
    }
  }
</style>
```
