---
/**
* 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>