mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 03:35:09 +02:00
ed1e4be2f6
* build: vendor xterm@5 for the Terminal sidebar tab
Adds xterm@5 + xterm-addon-fit as devDependencies and a `vendor:xterm`
build step that copies the assets into `extension/lib/` at build time.
The vendored files are .gitignored so the npm version stays the source
of truth. xterm@5 is eval-free, so no MV3 CSP changes needed.
No runtime callers yet — this just stages the assets.
* feat(server): add pty-session-cookie module for the Terminal tab
Mirrors `sse-session-cookie.ts` exactly. Mints short-lived 30-min HttpOnly
cookies for authenticating the Terminal-tab WebSocket upgrade against
the terminal-agent. Same TTL, same opportunistic-pruning shape, same
"scoped tokens never valid as root" invariant. Two registries instead of
one because the cookie names are different (`gstack_sse` vs `gstack_pty`)
and the token spaces must not overlap.
No callers yet — wired up in the next commit.
* feat(server): add terminal-agent.ts (PTY for the Terminal sidebar tab)
Translates phoenix gbrowser's Go PTY (cmd/gbd/terminal.go) into a Bun
non-compiled process. Lives separately from `sidebar-agent.ts` so a
WS-framing or PTY-cleanup bug can't take down the chat path (codex
outside-voice review caught the coupling risk).
Architecture:
- Bun.serve on 127.0.0.1:0 (never tunneled).
- POST /internal/grant accepts cookie tokens from the parent server over
loopback, authenticated with a per-boot internal token.
- GET /ws upgrades require BOTH (a) Origin: chrome-extension://<id> and
(b) the gstack_pty cookie minted by /pty-session. Either gate alone is
insufficient (CSWSH defense + auth defense).
- Lazy spawn: claude PTY is not started until the WS receives its first
data frame. Idle sidebar opens cost nothing.
- Bun PTY API: `terminal: { rows, cols, data(t, chunk) }` — verified at
impl time on Bun 1.3.10. proc.terminal.write() for input,
proc.terminal.resize() for resize, proc.kill() + 3s SIGKILL fallback
on close.
- process.on('uncaughtException'|'unhandledRejection') handlers so a
framing bug logs but doesn't kill the listener loop.
Test-only `BROWSE_TERMINAL_BINARY` env override lets the integration
tests spawn /bin/bash instead of requiring claude on every CI runner.
Not yet spawned by anything — wired in the next commit.
* feat(server): wire /pty-session route + spawn terminal-agent
Server-side glue connecting the Terminal sidebar tab to the new
terminal-agent process.
server.ts:
- New POST /pty-session route. Validates AUTH_TOKEN, mints a gstack_pty
HttpOnly cookie via pty-session-cookie.ts, posts the cookie value to
the agent's loopback /internal/grant. Returns the terminalPort + Set-Cookie
to the extension.
- /health response gains `terminalPort` (just the port number — never a
shell token). Tokens flow via the cookie path, never /health, because
/health already surfaces AUTH_TOKEN to localhost callers in headed mode
(that's a separate v1.1+ TODO).
- /pty-session and /terminal/* are deliberately NOT added to TUNNEL_PATHS,
so the dual-listener tunnel surface 404s by default-deny.
- Shutdown path now also pkills terminal-agent and unlinks its state files
(terminal-port + terminal-internal-token) so a reconnect doesn't try to
hit a dead port.
cli.ts:
- After spawning sidebar-agent.ts, also spawn terminal-agent.ts. Same
pattern: pkill old instances, Bun.spawn(['bun', 'run', script]) with
BROWSE_STATE_FILE + BROWSE_SERVER_PORT env. Non-fatal if the spawn
fails — chat still works without the terminal agent.
* feat(extension): Terminal as default sidebar tab
Adds a primary tab bar (Terminal | Chat) above the existing tab-content
panes. Terminal is the default-active tab; clicking Chat returns to the
existing claude -p one-shot flow which is preserved verbatim.
manifest.json: adds ws://127.0.0.1:*/ to host_permissions so MV3 doesn't
block the WebSocket upgrade.
sidepanel.html: new primary-tabs nav, new #tab-terminal pane with a
"Press any key to start Claude Code" bootstrap card, claude-not-found
install card, xterm mount point, and "session ended" restart UI. Loads
xterm.js + xterm-addon-fit + sidepanel-terminal.js. tab-chat is no
longer the .active default.
sidepanel.js: new activePrimaryPaneId() helper that reads which primary
tab is selected. Debug-close paths now route back to whichever primary
pane is active (was hardcoded to tab-chat). Primary-tab click handler
toggles .active classes and aria-selected. window.gstackServerPort and
window.gstackAuthToken exposed so sidepanel-terminal.js can build the
/pty-session POST and the WS URL.
sidepanel-terminal.js (new): xterm.js lifecycle. Lazy-spawn — first
keystroke fires POST /pty-session, then opens
ws://127.0.0.1:<terminalPort>/ws. Origin + cookie are set automatically
by the browser. Resize observer sends {type:"resize"} text frames.
ResizeObserver, tab-switch hooks, restart button, install-card retry.
On WS close shows "Session ended, click to restart" — no auto-reconnect
(codex outside-voice flagged that as session-burning).
sidepanel.css: primary-tabs bar + Terminal pane styling (full-height
xterm container, install card, ended state).
* test: terminal-agent + cookie module + sidebar default-tab regression
Three new test files:
terminal-agent.test.ts (16 tests): pty-session-cookie mint/validate/
revoke, Set-Cookie shape (HttpOnly + SameSite=Strict + Path=/, NO Secure
since 127.0.0.1 over HTTP), source-level guards that /pty-session and
/terminal/* are NOT in TUNNEL_PATHS, /health does NOT surface ptyToken
or gstack_pty, terminal-agent binds 127.0.0.1, /ws upgrade enforces
chrome-extension:// Origin AND gstack_pty cookie, lazy-spawn invariant
(spawnClaude is called from message handler, not upgrade), uncaughtException/
unhandledRejection handlers exist, SIGINT-then-SIGKILL cleanup.
terminal-agent-integration.test.ts (7 tests): spawns the agent as a real
subprocess in a tmp state dir. Verifies /internal/grant accepts/rejects
the loopback token, /ws gates (no Origin → 403, bad Origin → 403, no
cookie → 401), real WebSocket round-trip with /bin/bash via the
BROWSE_TERMINAL_BINARY override (write 'echo hello-pty-world\n', read it
back), and resize message acceptance.
sidebar-tabs.test.ts (13 tests): structural regression suite locking the
load-bearing invariants of the default-tab change — Terminal is .active,
Chat is not, xterm assets are loaded, debug-close path no longer hardcodes
tab-chat (uses activePrimaryPaneId), primary-tab click handler exists,
chat surface is not accidentally deleted, terminal JS does NOT auto-
reconnect on close, manifest declares ws:// + http:// localhost host
permissions, no unsafe-eval.
Plan called for Playwright + extension regression; the codebase doesn't
ship Playwright extension launcher infra, so we follow the existing
extension-test pattern (source-level structural assertions). Same
load-bearing intent — locks the invariants before they regress.
* docs: Terminal flow + threat model + v1.1 follow-ups
SIDEBAR_MESSAGE_FLOW.md: new "Terminal flow" section. Documents the WS
upgrade path (/pty-session cookie mint → /ws Origin + cookie gate →
lazy claude spawn), the dual-token model (AUTH_TOKEN for /pty-session,
gstack_pty cookie for /ws, INTERNAL_TOKEN for server↔agent loopback),
and the threat-model boundary — the Terminal tab bypasses the entire
prompt-injection security stack on purpose; user keystrokes are the
trust source. That trust assumption is load-bearing on three transport
guarantees: local-only listener, Origin gate, cookie auth. Drop any
one of those three and the tab becomes unsafe.
CLAUDE.md: extends the "Sidebar architecture" note to include
terminal-agent.ts in the read-this-first list. Adds a "Terminal tab is
its own process" note so a future contributor doesn't bolt PTY logic
onto sidebar-agent.ts.
TODOS.md: three new follow-ups under a new "Sidebar Terminal" section:
- v1.1: PTY session survives sidebar reload (Issue 1C deferred).
- v1.1+: audit /health AUTH_TOKEN distribution (codex finding #2 —
a pre-existing soft leak that cc-pty-import sidesteps but doesn't
fix).
- v1.1+: apply terminal-agent's process.on exception handlers to
sidebar-agent.ts (codex finding #4 — chat path has no fatal
handlers).
* feat(extension): Terminal-only sidebar — auth fix, UX polish, chat rip
The chat queue path is gone. The Chrome side panel is now just an
interactive claude PTY in xterm.js. Activity / Refs / Inspector still
exist behind the `debug` toggle in the footer.
Three threads of change, all from dogfood iteration on top of
cc-pty-import:
1. fix(server): cross-port WS auth via Sec-WebSocket-Protocol
- Browsers can't set Authorization on a WebSocket upgrade. We had
been minting an HttpOnly gstack_pty cookie via /pty-session, but
SameSite=Strict cookies don't survive the cross-port jump from
server.ts:34567 to the agent's random port from a chrome-extension
origin. The WS opened then immediately closed → "Session ended."
- /pty-session now also returns ptySessionToken in the JSON body.
- Extension calls `new WebSocket(url, [`gstack-pty.<token>`])`.
Browser sends Sec-WebSocket-Protocol on the upgrade.
- Agent reads the protocol header, validates against validTokens,
and MUST echo the protocol back (Chromium closes the connection
immediately if a server doesn't pick one of the offered protocols).
- Cookie path is kept as a fallback for non-browser callers (curl,
integration tests).
- New integration test exercises the full protocol-auth round-trip
via raw fetch+Upgrade so a future regression of this exact class
fails in CI.
2. fix(extension): UX polish on the Terminal pane
- Eager auto-connect when the sidebar opens — no "Press any key to
start" friction every reload.
- Always-visible ↻ Restart button in the terminal toolbar (not
gated on the ENDED state) so the user can force a fresh claude
mid-session.
- MutationObserver on #tab-terminal's class attribute drives a
fitAddon.fit() + term.refresh() when the pane becomes visible
again — xterm doesn't auto-redraw after display:none → display:flex.
3. feat(extension): rip the chat tab + sidebar-agent.ts
- Sidebar is Terminal-only. No more Terminal | Chat primary nav.
- sidebar-agent.ts deleted. /sidebar-command, /sidebar-chat,
/sidebar-agent/event, /sidebar-tabs* and friends all deleted.
- The pickSidebarModel router (sonnet vs opus) is gone — the live
PTY uses whatever model the user's `claude` CLI is configured with.
- Quick-actions (🧹 Cleanup / 📸 Screenshot / 🍪 Cookies) survive
in the Terminal toolbar. Cleanup now injects its prompt into the
live PTY via window.gstackInjectToTerminal — no more
/sidebar-command POST. The Inspector "Send to Code" action uses
the same injection path.
- clear-chat button removed from the footer.
- sidepanel.js shed ~900 lines of chat polling, optimistic UI,
stop-agent, etc.
Net diff: -3.4k lines across 16 files. CLAUDE.md, TODOS.md, and
docs/designs/SIDEBAR_MESSAGE_FLOW.md rewritten to match. The sidebar
regression test (browse/test/sidebar-tabs.test.ts) is rewritten as 27
structural assertions locking the new layout — Terminal sole pane,
no chat input, quick-actions in toolbar, eager-connect, MutationObserver
repaint, restart helper.
* feat: live tab awareness for the Terminal pane
claude in the PTY now has continuous tab-aware context. Three pieces:
1. Live state files. background.js listens to chrome.tabs.onActivated /
onCreated / onRemoved / onUpdated (throttled to URL/title/status==
complete so loading spinners don't spam) and pushes a snapshot. The
sidepanel relays it as a custom event; sidepanel-terminal.js sends
{type:"tabState"} text frames over the live PTY WebSocket.
terminal-agent.ts writes:
<stateDir>/tabs.json all open tabs (id, url, title, active,
pinned, audible, windowId)
<stateDir>/active-tab.json current active tab (skips chrome:// and
chrome-extension:// internal pages)
Atomic write via tmp + rename so claude never reads a half-written
document. A fresh snapshot is pushed on WS open so the files exist by
the time claude finishes booting.
2. New $B tab-each <command> [args...] meta-command. Fans out a single
command across every open tab, returns
{command, args, total, results: [{tabId, url, title, status, output}]}.
Skips chrome:// pages; restores the originally active tab in a finally
block (so a mid-batch error doesn't leave the user looking at a
different tab); uses bringToFront: false so the OS window doesn't
jump on every fanout. Scope-checks the inner command BEFORE the loop.
3. --append-system-prompt hint at spawn time. Claude is told about both
the state files and the $B tab-each command up front, so it doesn't
have to discover the surface by trial. Passed via the --append-system-
prompt CLI flag, NOT as a leading PTY write — the hint stays out of
the visible transcript.
Tests:
- browse/test/tab-each.test.ts (new) — registration + source-level
invariants (scope check before loop, finally-restore, bringToFront:false,
chrome:// skip) + behavior tests with a mock BrowserManager that verify
iteration order, JSON shape, error handling, and active-tab restore.
- browse/test/terminal-agent.test.ts — three new assertions for
tabState handler shape, atomic-write pattern, and the
--append-system-prompt wiring at spawn.
Verified live: opened 5 tabs, ran $B tab-each url against the live
server, got per-tab JSON results back, original active tab restored
without OS focus stealing.
* chore: drop sidebar-agent test refs after chat rip
Five test files / describe blocks targeted the deleted chat path:
- browse/test/security-e2e-fullstack.test.ts (full-stack chat-pipeline E2E
with mock claude — whole file gone)
- browse/test/security-review-fullstack.test.ts (review-flow E2E with real
classifier — whole file gone)
- browse/test/security-review-sidepanel-e2e.test.ts (Playwright E2E for
the security event banner that was ripped from sidepanel.html)
- browse/test/security-audit-r2.test.ts (5 describe blocks: agent queue
permissions, isValidQueueEntry stateFile traversal, loadSession session-ID
validation, switchChatTab DocumentFragment, pollChat reentrancy guard,
/sidebar-tabs URL sanitization, sidebar-agent SIGTERM→SIGKILL escalation,
AGENT_SRC top-level read converted to graceful fallback)
- browse/test/security-adversarial-fixes.test.ts (canary stream-chunk split
detection on detectCanaryLeak; one tool-output test on sidebar-agent)
- test/skill-validation.test.ts (sidebar agent #584 describe block)
These all assumed sidebar-agent.ts existed and tested chat-queue plumbing,
chat-tab DOM round-trip, chat-polling reentrancy, or per-message classifier
canary detection. With the live PTY there is no chat queue, no chat tab,
no LLM stream to canary-scan, and no per-message subprocess. The Terminal
pane's invariants are covered by the new browse/test/sidebar-tabs.test.ts
(27 structural assertions), browse/test/terminal-agent.test.ts, and
browse/test/terminal-agent-integration.test.ts.
bun test → exit 0, 0 failures.
* chore: bump version and changelog (v1.14.0.0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(extension): xterm fills the full Terminal panel height
The Terminal pane only rendered into the top portion of the panel — most
of the panel below the prompt was an empty black gap. Three layered
issues, all about xterm.js measuring dimensions during a layout state
that wasn't ready yet:
1. order-of-operations in connect(): ensureXterm() ran BEFORE
setState(LIVE), so term.open() measured els.mount while it was still
display:none. xterm caches a 0-size viewport synchronously inside
open() and never auto-recovers when the container goes visible.
Flipped: setState(LIVE) → ensureXterm.
2. first fit() ran synchronously before the browser had applied the
.active class transition. Wrapped in requestAnimationFrame so layout
has settled before fit() reads clientHeight.
3. CSS flex-overflow trap: .terminal-mount has flex:1 inside the
flex-column #tab-terminal, but .tab-content's `overflow-y: auto` and
the lack of `min-height: 0` on .terminal-mount meant the item
couldn't shrink below content size. flex:1 then refused to expand
into available space and xterm rendered into whatever its initial
2x2 measurement happened to be.
Fixes:
- extension/sidepanel-terminal.js: reorder + RAF fit
- extension/sidepanel.css: .terminal-mount gets `flex: 1 1 0` +
`min-height: 0` + `position: relative`. #tab-terminal overrides
.tab-content's `overflow-y: auto` to `overflow: hidden` (xterm has
its own viewport scroll; the parent shouldn't compete) and explicitly
re-declares `display: flex; flex-direction: column` for #tab-terminal.active.
bun test browse/test/sidebar-tabs.test.ts → 27/27 pass.
Manually verified: side panel opens → Terminal fills full panel height,
xterm scrollback works, debug-tab toggle still repaints correctly.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1750 lines
40 KiB
CSS
1750 lines
40 KiB
CSS
/* gstack browse — Side Panel
|
|
* Design system: DESIGN.md (Industrial/Utilitarian, amber accent, zinc neutrals)
|
|
*/
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
:root {
|
|
/* Brand — amber accent, rare and meaningful */
|
|
--amber-400: #FBBF24;
|
|
--amber-500: #F59E0B;
|
|
--amber-600: #D97706;
|
|
|
|
/* Neutrals — cool zinc */
|
|
--zinc-50: #FAFAFA;
|
|
--zinc-400: #A1A1AA;
|
|
--zinc-600: #52525B;
|
|
--zinc-800: #27272A;
|
|
|
|
/* Surfaces */
|
|
--bg-base: #0C0C0C;
|
|
--bg-surface: #141414;
|
|
--bg-hover: #1a1a1a;
|
|
--border: #262626;
|
|
--border-subtle: #1f1f1f;
|
|
|
|
/* Text hierarchy */
|
|
--text-heading: #FAFAFA;
|
|
--text-body: #e0e0e0;
|
|
--text-label: #A1A1AA;
|
|
--text-meta: #52525B;
|
|
--text-disabled: #3f3f46;
|
|
|
|
/* Semantic */
|
|
--success: #22C55E;
|
|
--warning: #F59E0B;
|
|
--error: #EF4444;
|
|
--info: #3B82F6;
|
|
|
|
/* Typography */
|
|
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
--font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
|
|
|
/* Radius */
|
|
--radius-sm: 4px;
|
|
--radius-md: 8px;
|
|
--radius-lg: 12px;
|
|
--radius-full: 9999px;
|
|
}
|
|
|
|
/* ─── Security Shield ───────────────────────────────────────────── */
|
|
/* 3 states — green=protected, amber=degraded, red=inactive.
|
|
Custom SVG outline + "SEC" label in JetBrains Mono to match the
|
|
industrial/CLI aesthetic (design review Pass 7 decision). */
|
|
|
|
.security-shield {
|
|
position: absolute;
|
|
top: 6px;
|
|
right: 8px;
|
|
z-index: 10;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm, 4px);
|
|
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
letter-spacing: 0.04em;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
transition: color 200ms ease-out, background 200ms ease-out;
|
|
cursor: default;
|
|
}
|
|
.security-shield[data-status="protected"] {
|
|
color: var(--success, #22C55E);
|
|
}
|
|
.security-shield[data-status="degraded"] {
|
|
color: var(--amber-400, #FBBF24);
|
|
}
|
|
.security-shield[data-status="inactive"] {
|
|
color: var(--error, #EF4444);
|
|
}
|
|
|
|
/* ─── Connection Banner ─────────────────────────────────────────── */
|
|
|
|
.conn-banner {
|
|
padding: 6px 10px;
|
|
font-size: 10px;
|
|
font-family: var(--font-mono);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
|
|
.conn-banner.reconnecting {
|
|
background: rgba(245, 158, 11, 0.1);
|
|
border-bottom: 1px solid rgba(245, 158, 11, 0.2);
|
|
color: var(--amber-400);
|
|
}
|
|
|
|
.conn-banner.dead {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
|
|
color: var(--error);
|
|
}
|
|
|
|
.conn-banner.reconnected {
|
|
background: rgba(34, 197, 94, 0.1);
|
|
border-bottom: 1px solid rgba(34, 197, 94, 0.2);
|
|
color: var(--success);
|
|
animation: fadeOut 3s ease forwards;
|
|
animation-delay: 2s;
|
|
}
|
|
|
|
@keyframes fadeOut {
|
|
to { opacity: 0; height: 0; padding: 0; overflow: hidden; }
|
|
}
|
|
|
|
.conn-banner-text {
|
|
flex: 1;
|
|
}
|
|
|
|
/* ─── Security Banner ─────────────────────────────────────────────
|
|
Variant A approved in /plan-design-review 2026-04-19. Centered
|
|
alert-heavy. Fires on security_event — canary leaks + ML BLOCK
|
|
verdicts. Trust UX: layer names + confidence scores in mono so
|
|
the user can see exactly WHY the session was terminated.
|
|
*/
|
|
|
|
.security-banner {
|
|
position: relative;
|
|
/* Sit above the absolutely-positioned security-shield (z-index: 10) so
|
|
the banner's close button and controls receive clicks. Without this
|
|
the shield at top-right overlaps the banner's close X region and
|
|
intercepts pointer events. */
|
|
z-index: 20;
|
|
padding: 20px 16px;
|
|
text-align: center;
|
|
background: rgba(20, 20, 20, 0.98);
|
|
border-bottom: 1px solid rgba(239, 68, 68, 0.3);
|
|
animation: securityBannerEnter 250ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
@keyframes securityBannerEnter {
|
|
from { opacity: 0; transform: translateY(-8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.security-banner-close {
|
|
position: absolute;
|
|
top: 6px;
|
|
right: 6px;
|
|
width: 28px;
|
|
height: 28px;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--zinc-500, #71717A);
|
|
font-size: 20px;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
border-radius: var(--radius-md, 8px);
|
|
padding: 0;
|
|
}
|
|
.security-banner-close:hover {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: var(--zinc-300, #D4D4D8);
|
|
}
|
|
.security-banner-close:focus-visible {
|
|
outline: 2px solid var(--amber-500);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.security-banner-icon {
|
|
color: var(--error);
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.security-banner-title {
|
|
font-family: var(--font-display, 'Satoshi', sans-serif);
|
|
font-weight: 700;
|
|
font-size: 18px;
|
|
color: var(--error);
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.security-banner-subtitle {
|
|
font-family: var(--font-body, 'DM Sans', sans-serif);
|
|
font-size: 13px;
|
|
color: var(--zinc-400, #A1A1AA);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.security-banner-expand {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: transparent;
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: var(--radius-md, 8px);
|
|
padding: 6px 12px;
|
|
color: var(--zinc-300, #D4D4D8);
|
|
font-family: var(--font-body, 'DM Sans', sans-serif);
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
.security-banner-expand:hover {
|
|
background: rgba(255, 255, 255, 0.04);
|
|
}
|
|
.security-banner-expand:focus-visible {
|
|
outline: 2px solid var(--amber-500);
|
|
outline-offset: 2px;
|
|
}
|
|
.security-banner-chevron {
|
|
transition: transform 200ms ease-out;
|
|
}
|
|
|
|
.security-banner-details {
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
text-align: left;
|
|
}
|
|
|
|
.security-banner-section-label {
|
|
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
|
font-size: 10px;
|
|
letter-spacing: 0.08em;
|
|
color: var(--zinc-500, #71717A);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.security-banner-layers {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.security-banner-layer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 4px 8px;
|
|
background: rgba(255, 255, 255, 0.02);
|
|
border-radius: var(--radius-sm, 4px);
|
|
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.security-banner-layer-name {
|
|
color: var(--zinc-300, #D4D4D8);
|
|
}
|
|
|
|
.security-banner-layer-score {
|
|
color: var(--amber-400);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.security-banner-suspect {
|
|
margin: 4px 0 0;
|
|
padding: 8px 10px;
|
|
background: var(--zinc-900, #18181B);
|
|
border: 1px solid var(--zinc-700, #3F3F46);
|
|
border-radius: var(--radius-sm, 4px);
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
color: var(--zinc-300, #D4D4D8);
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
max-height: 160px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.security-banner-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: center;
|
|
margin-top: 14px;
|
|
}
|
|
|
|
.security-banner-btn {
|
|
flex: 1;
|
|
padding: 8px 14px;
|
|
border-radius: var(--radius-md, 6px);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
border: 1px solid transparent;
|
|
transition: background 0.15s, border-color 0.15s;
|
|
}
|
|
|
|
.security-banner-btn-block {
|
|
background: var(--red-600, #DC2626);
|
|
color: white;
|
|
border-color: var(--red-700, #B91C1C);
|
|
}
|
|
|
|
.security-banner-btn-block:hover {
|
|
background: var(--red-700, #B91C1C);
|
|
}
|
|
|
|
.security-banner-btn-allow {
|
|
background: transparent;
|
|
color: var(--zinc-200, #E4E4E7);
|
|
border-color: var(--zinc-600, #52525B);
|
|
}
|
|
|
|
.security-banner-btn-allow:hover {
|
|
background: var(--zinc-800, #27272A);
|
|
border-color: var(--zinc-500, #71717A);
|
|
}
|
|
|
|
.security-banner-btn:focus-visible {
|
|
outline: 2px solid var(--amber-400);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.conn-btn {
|
|
font-size: 9px;
|
|
font-family: var(--font-mono);
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
border: 1px solid var(--border);
|
|
background: var(--bg-surface);
|
|
color: var(--text-label);
|
|
transition: all 150ms;
|
|
}
|
|
|
|
.conn-btn:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-heading);
|
|
}
|
|
|
|
.conn-copy {
|
|
color: var(--text-meta);
|
|
font-style: italic;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg-base);
|
|
color: var(--text-body);
|
|
font-family: var(--font-system);
|
|
font-size: 12px;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Grain texture overlay */
|
|
body::after {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
opacity: 0.03;
|
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
/* ─── Status Dot ──────────────────────────────────────── */
|
|
.dot {
|
|
width: 8px; height: 8px;
|
|
border-radius: var(--radius-full);
|
|
background: var(--text-disabled);
|
|
flex-shrink: 0;
|
|
transition: background 150ms;
|
|
}
|
|
.dot.connected { background: var(--success); }
|
|
.dot.reconnecting {
|
|
background: var(--amber-500);
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.4; }
|
|
50% { opacity: 1; }
|
|
}
|
|
|
|
/* ─── Chat Messages ───────────────────────────────────── */
|
|
.chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.chat-loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
height: 100%;
|
|
text-align: left;
|
|
color: var(--text-meta);
|
|
gap: 12px;
|
|
font-size: 13px;
|
|
padding: 24px;
|
|
}
|
|
.chat-loading-spinner {
|
|
width: 24px;
|
|
height: 24px;
|
|
border: 2px solid var(--border);
|
|
border-top-color: var(--amber-500);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
.chat-welcome {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
height: 100%;
|
|
text-align: left;
|
|
color: var(--text-label);
|
|
gap: 8px;
|
|
padding: 24px;
|
|
}
|
|
.chat-welcome-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
background: var(--amber-500);
|
|
color: #000;
|
|
font-weight: 800;
|
|
font-size: 22px;
|
|
border-radius: var(--radius-md);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 8px;
|
|
}
|
|
.chat-welcome .muted { color: var(--text-meta); font-size: 12px; }
|
|
|
|
.chat-bubble {
|
|
max-width: 90%;
|
|
padding: 6px 10px;
|
|
border-radius: var(--radius-lg);
|
|
font-size: 11px;
|
|
line-height: 1.4;
|
|
word-break: break-word;
|
|
animation: slideIn 150ms ease-out;
|
|
}
|
|
.chat-bubble.user {
|
|
align-self: flex-end;
|
|
background: var(--amber-500);
|
|
color: #000;
|
|
border-bottom-right-radius: var(--radius-sm);
|
|
}
|
|
.chat-notification {
|
|
text-align: left;
|
|
font-size: 11px;
|
|
color: var(--text-meta);
|
|
padding: 4px 12px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
.chat-bubble.assistant {
|
|
align-self: flex-start;
|
|
background: var(--bg-surface);
|
|
color: var(--text-body);
|
|
border: 1px solid var(--border);
|
|
border-bottom-left-radius: var(--radius-sm);
|
|
}
|
|
.chat-bubble.assistant pre {
|
|
background: var(--bg-base);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
padding: 6px 8px;
|
|
margin: 6px 0;
|
|
overflow-x: auto;
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
}
|
|
.chat-bubble .chat-time, .agent-response > .chat-time {
|
|
font-size: 9px;
|
|
opacity: 0.4;
|
|
margin-top: 2px;
|
|
display: block;
|
|
}
|
|
|
|
/* ─── Agent Streaming Response ─────────────────────────── */
|
|
.agent-response {
|
|
align-self: flex-start;
|
|
max-width: 95%;
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
border-bottom-left-radius: var(--radius-sm);
|
|
padding: 6px 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
animation: slideIn 150ms ease-out;
|
|
}
|
|
.agent-tool {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
padding: 4px 8px;
|
|
background: rgba(245, 158, 11, 0.06);
|
|
border-left: 2px solid var(--amber-500);
|
|
border-radius: 0 4px 4px 0;
|
|
font-size: 12px;
|
|
font-family: var(--font-system);
|
|
margin: 2px 0;
|
|
}
|
|
.tool-icon {
|
|
flex-shrink: 0;
|
|
font-size: 11px;
|
|
line-height: 1.5;
|
|
}
|
|
.tool-description {
|
|
color: var(--text-body);
|
|
line-height: 1.5;
|
|
word-break: break-word;
|
|
}
|
|
/* Collapsed reasoning disclosure */
|
|
.agent-reasoning {
|
|
margin: 4px 0;
|
|
}
|
|
.agent-reasoning summary {
|
|
cursor: pointer;
|
|
font-size: 11px;
|
|
font-family: var(--font-mono);
|
|
color: var(--text-meta);
|
|
padding: 3px 0;
|
|
user-select: none;
|
|
list-style: none;
|
|
}
|
|
.agent-reasoning summary::before {
|
|
content: '▶ ';
|
|
font-size: 9px;
|
|
}
|
|
.agent-reasoning[open] summary::before {
|
|
content: '▼ ';
|
|
}
|
|
.agent-reasoning summary:hover {
|
|
color: var(--text-label);
|
|
}
|
|
.agent-reasoning .agent-tool {
|
|
margin-left: 4px;
|
|
}
|
|
/* Legacy classes kept for compat */
|
|
.tool-name {
|
|
color: var(--amber-500);
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
.tool-input {
|
|
color: var(--text-disabled);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.agent-text {
|
|
color: var(--text-body);
|
|
font-size: 12.5px;
|
|
line-height: 1.5;
|
|
word-break: break-word;
|
|
padding: 2px 0;
|
|
}
|
|
.agent-text pre {
|
|
background: var(--bg-base);
|
|
border: 1px solid var(--border-subtle);
|
|
border-radius: 3px;
|
|
padding: 4px 6px;
|
|
margin: 4px 0;
|
|
overflow-x: auto;
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
white-space: pre-wrap;
|
|
}
|
|
.agent-error {
|
|
color: var(--error);
|
|
font-size: 12px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Thinking dots animation */
|
|
.agent-thinking {
|
|
display: flex;
|
|
gap: 4px;
|
|
padding: 4px 0;
|
|
}
|
|
.thinking-dot {
|
|
width: 4px;
|
|
height: 4px;
|
|
background: var(--text-disabled);
|
|
border-radius: 50%;
|
|
animation: thinkingPulse 1.4s ease-in-out infinite;
|
|
}
|
|
.thinking-dot:nth-child(2) { animation-delay: 0.2s; }
|
|
.thinking-dot:nth-child(3) { animation-delay: 0.4s; }
|
|
@keyframes thinkingPulse {
|
|
0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
|
|
40% { opacity: 1; transform: scale(1); }
|
|
}
|
|
|
|
/* ─── Footer Buttons ──────────────────────────────────── */
|
|
.footer-left {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
.footer-btn, .debug-toggle {
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-meta);
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
padding: 2px 6px;
|
|
cursor: pointer;
|
|
transition: all 150ms;
|
|
}
|
|
.footer-btn:hover, .debug-toggle:hover {
|
|
color: var(--text-label);
|
|
border-color: var(--zinc-600);
|
|
}
|
|
.debug-toggle.active {
|
|
color: var(--amber-400);
|
|
border-color: var(--amber-500);
|
|
}
|
|
.debug-tabs {
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
.close-debug {
|
|
width: 36px;
|
|
flex: none !important;
|
|
font-size: 16px;
|
|
color: var(--text-meta) !important;
|
|
}
|
|
.close-debug:hover { color: var(--text-label) !important; }
|
|
|
|
/* ─── Tab Bar ─────────────────────────────────────────── */
|
|
.tabs {
|
|
height: 36px;
|
|
background: var(--bg-surface);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
flex-shrink: 0;
|
|
}
|
|
.tab {
|
|
flex: 1;
|
|
background: none;
|
|
border: none;
|
|
color: var(--text-label);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 150ms;
|
|
}
|
|
.tab:hover:not(.disabled) { color: var(--zinc-50); }
|
|
.tab.active {
|
|
color: var(--text-heading);
|
|
border-bottom-color: var(--amber-500);
|
|
}
|
|
.tab.disabled {
|
|
color: var(--text-disabled);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* ─── Tab Content ─────────────────────────────────────── */
|
|
.tab-content {
|
|
display: none;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
}
|
|
.tab-content.active { display: flex; flex-direction: column; }
|
|
|
|
/* ─── Terminal Tab ────────────────────────────────────────────── */
|
|
/* The Terminal pane manages its own scrolling (xterm has a viewport with
|
|
scrollback). The default .tab-content rules above set overflow-y: auto,
|
|
which collapses min-height for nested flex children — that's why
|
|
.terminal-mount couldn't grow to fill available space. Override here. */
|
|
#tab-terminal {
|
|
background: #0a0a0a;
|
|
padding: 0;
|
|
overflow: hidden;
|
|
min-height: 0;
|
|
}
|
|
#tab-terminal.active {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.terminal-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 6px;
|
|
padding: 4px 8px;
|
|
border-bottom: 1px solid #1a1a1a;
|
|
background: #0a0a0a;
|
|
flex-shrink: 0;
|
|
}
|
|
.terminal-toolbar-actions {
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.terminal-toolbar-btn {
|
|
background: transparent;
|
|
border: 1px solid #27272a;
|
|
color: #a1a1aa;
|
|
padding: 3px 10px;
|
|
font-size: 11px;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
}
|
|
.terminal-toolbar-btn:hover {
|
|
color: #f59e0b;
|
|
border-color: #f59e0b;
|
|
}
|
|
.terminal-bootstrap {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
color: #71717a;
|
|
padding: 24px;
|
|
}
|
|
.terminal-bootstrap-icon {
|
|
font-size: 32px;
|
|
color: #f59e0b;
|
|
margin-bottom: 8px;
|
|
}
|
|
.terminal-bootstrap p { margin: 4px 0; }
|
|
.terminal-install-card {
|
|
margin: 24px;
|
|
padding: 16px;
|
|
border: 1px solid #27272a;
|
|
border-radius: 6px;
|
|
text-align: center;
|
|
}
|
|
.terminal-install-card a { color: #f59e0b; }
|
|
.install-retry-btn {
|
|
margin-top: 12px;
|
|
padding: 6px 14px;
|
|
background: #f59e0b;
|
|
color: #0a0a0a;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-family: inherit;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
.install-retry-btn:hover { opacity: 0.9; }
|
|
.terminal-mount {
|
|
/* min-height: 0 is the standard flex-overflow fix — without it, a flex
|
|
item with overflowing content can't shrink below its content size,
|
|
so flex:1 refuses to expand into available space and xterm renders
|
|
into whatever the content happens to be (i.e. its own initial 2x2
|
|
measurement). With min-height:0 the item respects the flex parent's
|
|
remaining space and xterm grows to fill it. */
|
|
flex: 1 1 0;
|
|
min-height: 0;
|
|
width: 100%;
|
|
background: #0a0a0a;
|
|
padding: 8px;
|
|
box-sizing: border-box;
|
|
/* position: relative so xterm's absolutely-positioned helpers (the
|
|
hidden textarea for input) anchor inside us, not on body. */
|
|
position: relative;
|
|
}
|
|
.terminal-mount .xterm,
|
|
.terminal-mount .xterm .xterm-viewport,
|
|
.terminal-mount .xterm .xterm-screen {
|
|
height: 100% !important;
|
|
}
|
|
.terminal-ended {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #71717a;
|
|
padding: 24px;
|
|
}
|
|
|
|
/* ─── Activity Feed ───────────────────────────────────── */
|
|
#activity-feed { flex: 1; }
|
|
|
|
.activity-entry {
|
|
padding: 8px 12px;
|
|
border-left: 3px solid var(--border);
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
cursor: pointer;
|
|
transition: background 150ms;
|
|
animation: slideIn 150ms ease-out;
|
|
}
|
|
.activity-entry:hover { background: var(--bg-hover); }
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.activity-entry { animation: none; }
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from { transform: translateY(8px); opacity: 0; }
|
|
to { transform: translateY(0); opacity: 1; }
|
|
}
|
|
|
|
/* Left border colors by type */
|
|
.activity-entry.nav { border-left-color: var(--info); }
|
|
.activity-entry.interaction { border-left-color: var(--success); }
|
|
.activity-entry.observe { border-left-color: var(--amber-400); }
|
|
.activity-entry.error { border-left-color: var(--error); }
|
|
.activity-entry.pending {
|
|
border-left-color: var(--amber-500);
|
|
animation: slideIn 150ms ease-out, borderPulse 2s ease-in-out infinite;
|
|
}
|
|
@keyframes borderPulse {
|
|
0%, 100% { border-left-color: rgba(245, 158, 11, 0.3); }
|
|
50% { border-left-color: rgba(245, 158, 11, 1); }
|
|
}
|
|
|
|
.entry-header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
}
|
|
.entry-time {
|
|
color: var(--text-meta);
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
flex-shrink: 0;
|
|
}
|
|
.entry-command {
|
|
color: var(--text-heading);
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
.entry-args {
|
|
color: var(--text-label);
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-top: 2px;
|
|
}
|
|
.entry-status {
|
|
font-size: 11px;
|
|
margin-top: 2px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.entry-status .ok { color: var(--success); }
|
|
.entry-status .err { color: var(--error); }
|
|
.entry-status .duration { color: var(--text-meta); }
|
|
|
|
/* Expanded state */
|
|
.entry-detail {
|
|
display: none;
|
|
margin-top: 8px;
|
|
padding-top: 8px;
|
|
border-top: 1px dashed var(--border);
|
|
}
|
|
.activity-entry.expanded .entry-detail { display: block; }
|
|
.activity-entry.expanded .entry-args { white-space: normal; }
|
|
.entry-result {
|
|
color: var(--zinc-400);
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
|
|
/* ─── Refs Tab ────────────────────────────────────────── */
|
|
.ref-row {
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 0 12px;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
font-size: 12px;
|
|
}
|
|
.ref-id {
|
|
color: var(--amber-400);
|
|
font-family: var(--font-mono);
|
|
font-weight: 600;
|
|
min-width: 32px;
|
|
}
|
|
.ref-role {
|
|
color: var(--text-label);
|
|
min-width: 60px;
|
|
}
|
|
.ref-name {
|
|
color: var(--text-body);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.refs-footer {
|
|
padding: 8px 12px;
|
|
color: var(--text-meta);
|
|
font-size: 11px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
/* ─── Session Placeholder ─────────────────────────────── */
|
|
.session-placeholder {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
height: 100%;
|
|
text-align: left;
|
|
color: var(--text-label);
|
|
padding: 24px;
|
|
gap: 8px;
|
|
}
|
|
.session-placeholder .muted { color: var(--text-meta); font-size: 12px; }
|
|
|
|
/* ─── Empty State ─────────────────────────────────────── */
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding: 40px 24px;
|
|
text-align: left;
|
|
color: var(--text-label);
|
|
gap: 4px;
|
|
}
|
|
.empty-state .muted { color: var(--text-meta); font-size: 12px; }
|
|
.empty-state code {
|
|
background: var(--bg-surface);
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* ─── Gap Banner ──────────────────────────────────────── */
|
|
.gap-banner {
|
|
background: rgba(245, 158, 11, 0.08);
|
|
border-bottom: 1px solid var(--amber-500);
|
|
color: var(--amber-400);
|
|
font-size: 11px;
|
|
padding: 6px 12px;
|
|
animation: bannerSlide 250ms ease-out;
|
|
}
|
|
@keyframes bannerSlide {
|
|
from { transform: translateY(-100%); }
|
|
to { transform: translateY(0); }
|
|
}
|
|
|
|
/* ─── Command Bar ─────────────────────────────────────── */
|
|
/* ─── Quick Actions Toolbar ─────────────────────────────── */
|
|
|
|
.quick-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
padding: 4px 8px;
|
|
background: var(--bg-surface);
|
|
border-top: 1px solid var(--border-subtle);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.quick-action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
height: 26px;
|
|
padding: 0 10px;
|
|
background: none;
|
|
border: 1px solid var(--zinc-600);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-label);
|
|
font-family: var(--font-system);
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
transition: all 150ms;
|
|
}
|
|
|
|
.quick-action-btn:hover {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: var(--text-body);
|
|
border-color: var(--zinc-400);
|
|
}
|
|
|
|
.quick-action-btn:active {
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.quick-action-btn.disabled, .inspector-action-btn.disabled {
|
|
pointer-events: none;
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.quick-action-btn.loading {
|
|
pointer-events: none;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.quick-action-btn.loading::after {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 10px;
|
|
height: 10px;
|
|
border: 2px solid var(--zinc-600);
|
|
border-top-color: var(--amber-400);
|
|
border-radius: 50%;
|
|
animation: spin 0.6s linear infinite;
|
|
}
|
|
|
|
.command-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 8px;
|
|
background: var(--bg-surface);
|
|
border-top: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
.command-prompt {
|
|
color: var(--amber-500);
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
user-select: none;
|
|
}
|
|
.command-input {
|
|
flex: 1;
|
|
background: var(--bg-base);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
padding: 6px 8px;
|
|
color: var(--text-heading);
|
|
font-family: var(--font-system);
|
|
font-size: 11px;
|
|
outline: none;
|
|
transition: border-color 150ms;
|
|
}
|
|
.command-input:focus { border-color: var(--amber-500); }
|
|
.command-input::placeholder { color: var(--text-disabled); font-size: 10px; }
|
|
.command-input.sent {
|
|
border-color: var(--success);
|
|
transition: border-color 150ms;
|
|
}
|
|
.command-input.error {
|
|
border-color: var(--error);
|
|
animation: shake 300ms ease;
|
|
}
|
|
.command-input.error::placeholder {
|
|
color: var(--error);
|
|
opacity: 0.8;
|
|
}
|
|
@keyframes shake {
|
|
0%, 100% { transform: translateX(0); }
|
|
25% { transform: translateX(-4px); }
|
|
75% { transform: translateX(4px); }
|
|
}
|
|
.send-btn {
|
|
width: 26px;
|
|
height: 26px;
|
|
background: var(--amber-500);
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
color: #000;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
transition: all 150ms;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.send-btn:hover { background: var(--amber-400); }
|
|
.send-btn:active { transform: scale(0.93); }
|
|
.send-btn:disabled {
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
}
|
|
.stop-btn {
|
|
width: 26px;
|
|
height: 26px;
|
|
background: var(--error);
|
|
border: none;
|
|
border-radius: var(--radius-sm);
|
|
color: #fff;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
line-height: 26px;
|
|
text-align: center;
|
|
}
|
|
.stop-btn:hover { background: #dc2626; }
|
|
.stop-btn:active { transform: scale(0.93); }
|
|
|
|
/* ─── Footer ──────────────────────────────────────────── */
|
|
footer {
|
|
height: 28px;
|
|
background: var(--bg-surface);
|
|
border-top: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 8px;
|
|
font-size: 10px;
|
|
color: var(--text-meta);
|
|
flex-shrink: 0;
|
|
}
|
|
#footer-url {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: 50%;
|
|
}
|
|
.footer-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.footer-port {
|
|
color: var(--text-meta);
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
transition: color 150ms;
|
|
}
|
|
.footer-port:hover { color: var(--text-label); }
|
|
.port-input {
|
|
width: 56px;
|
|
padding: 2px 6px;
|
|
background: var(--bg-base);
|
|
border: 1px solid var(--zinc-600);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-heading);
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
outline: none;
|
|
transition: border-color 150ms;
|
|
}
|
|
.port-input:focus { border-color: var(--amber-500); }
|
|
|
|
/* ─── Experimental Banner ─────────────────────────────── */
|
|
.experimental-banner {
|
|
background: rgba(59, 130, 246, 0.08);
|
|
border: 1px solid rgba(59, 130, 246, 0.15);
|
|
color: var(--zinc-400);
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
margin: 6px 12px;
|
|
text-align: left;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ─── Browser Tab Bar ─────────────────────────────────── */
|
|
.browser-tabs {
|
|
display: flex;
|
|
gap: 1px;
|
|
padding: 4px 8px;
|
|
background: var(--bg-base);
|
|
border-bottom: 1px solid var(--border);
|
|
overflow-x: auto;
|
|
flex-shrink: 0;
|
|
scrollbar-width: none;
|
|
}
|
|
.browser-tabs::-webkit-scrollbar { display: none; }
|
|
.browser-tab {
|
|
padding: 4px 10px;
|
|
font-size: 11px;
|
|
font-family: var(--font-system);
|
|
color: var(--text-meta);
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
max-width: 140px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
flex-shrink: 0;
|
|
transition: background 100ms, color 100ms;
|
|
}
|
|
.browser-tab:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-label);
|
|
}
|
|
.browser-tab.active {
|
|
background: var(--bg-surface);
|
|
color: var(--text-body);
|
|
border-color: var(--border);
|
|
}
|
|
|
|
/* ─── Inspector Tab ──────────────────────────────────── */
|
|
|
|
.inspector-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 10px;
|
|
background: var(--bg-surface);
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.inspector-pick-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
height: 28px;
|
|
padding: 0 10px;
|
|
background: none;
|
|
border: 1px solid var(--amber-500);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--amber-500);
|
|
font-family: var(--font-system);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition: all 150ms;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.inspector-pick-btn:hover {
|
|
background: rgba(245, 158, 11, 0.1);
|
|
color: var(--amber-400);
|
|
}
|
|
|
|
.inspector-pick-btn.active {
|
|
background: var(--amber-500);
|
|
color: #000;
|
|
}
|
|
|
|
.inspector-pick-icon {
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
}
|
|
|
|
/* ─── Action Buttons (Cleanup, Screenshot) ─────────────────── */
|
|
|
|
.inspector-action-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 28px;
|
|
width: 28px;
|
|
padding: 0;
|
|
background: none;
|
|
border: 1px solid var(--zinc-600);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text-label);
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: all 150ms;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.inspector-action-btn:hover {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
color: var(--text-body);
|
|
border-color: var(--zinc-400);
|
|
}
|
|
|
|
.inspector-action-btn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
.inspector-action-btn.loading {
|
|
pointer-events: none;
|
|
opacity: 0.5;
|
|
position: relative;
|
|
}
|
|
|
|
.inspector-action-btn.loading::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 12px;
|
|
height: 12px;
|
|
border: 2px solid var(--zinc-600);
|
|
border-top-color: var(--amber-400);
|
|
border-radius: 50%;
|
|
animation: spin 0.6s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.inspector-selected {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--text-body);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.inspector-mode-badge {
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
padding: 1px 6px;
|
|
border-radius: var(--radius-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.inspector-mode-badge.basic {
|
|
background: var(--zinc-800);
|
|
color: var(--zinc-400);
|
|
}
|
|
|
|
.inspector-mode-badge.cdp {
|
|
background: rgba(34, 197, 94, 0.15);
|
|
color: var(--success);
|
|
}
|
|
|
|
/* Inspector content area */
|
|
.inspector-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Empty state */
|
|
.inspector-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
justify-content: center;
|
|
padding: 40px 24px;
|
|
text-align: left;
|
|
gap: 6px;
|
|
}
|
|
|
|
.inspector-empty-icon {
|
|
font-size: 24px;
|
|
color: var(--zinc-600);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.inspector-empty p {
|
|
color: var(--zinc-400);
|
|
font-size: 13px;
|
|
margin: 0;
|
|
}
|
|
|
|
.inspector-empty .muted {
|
|
color: var(--zinc-600);
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Loading state */
|
|
.inspector-loading {
|
|
padding: 16px 12px;
|
|
}
|
|
|
|
.inspector-loading-text {
|
|
font-size: 12px;
|
|
color: var(--amber-500);
|
|
margin-bottom: 12px;
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
.inspector-skeleton {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.inspector-skeleton-bar {
|
|
height: 12px;
|
|
background: var(--zinc-800);
|
|
border-radius: var(--radius-sm);
|
|
animation: shimmer 1.5s ease-in-out infinite;
|
|
}
|
|
|
|
.inspector-skeleton-bar:nth-child(1) { width: 80%; }
|
|
.inspector-skeleton-bar:nth-child(2) { width: 60%; }
|
|
.inspector-skeleton-bar:nth-child(3) { width: 70%; }
|
|
|
|
@keyframes shimmer {
|
|
0%, 100% { opacity: 0.3; }
|
|
50% { opacity: 0.7; }
|
|
}
|
|
|
|
/* Error state */
|
|
.inspector-error {
|
|
padding: 16px 12px;
|
|
color: var(--error);
|
|
font-size: 12px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Inspector sections */
|
|
.inspector-section {
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}
|
|
|
|
.inspector-section-header {
|
|
font-family: var(--font-system);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--zinc-400);
|
|
padding: 8px 12px 4px;
|
|
}
|
|
|
|
.inspector-section-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
background: none;
|
|
border: none;
|
|
font-family: var(--font-system);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--zinc-400);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
transition: color 150ms;
|
|
}
|
|
|
|
.inspector-section-toggle:hover {
|
|
color: var(--text-body);
|
|
}
|
|
|
|
.inspector-toggle-arrow {
|
|
font-size: 10px;
|
|
color: var(--zinc-400);
|
|
flex-shrink: 0;
|
|
width: 12px;
|
|
}
|
|
|
|
.inspector-section-body {
|
|
padding: 4px 12px 8px;
|
|
}
|
|
|
|
.inspector-section-body.collapsed {
|
|
display: none;
|
|
}
|
|
|
|
.inspector-rule-count {
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
color: var(--zinc-600);
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.inspector-no-data {
|
|
color: var(--zinc-600);
|
|
font-size: 11px;
|
|
font-style: italic;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
/* ─── Box Model ──────────────────────────────────────── */
|
|
|
|
.inspector-boxmodel {
|
|
padding: 8px 12px 12px;
|
|
}
|
|
|
|
.boxmodel-margin,
|
|
.boxmodel-border,
|
|
.boxmodel-padding,
|
|
.boxmodel-content {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 1px dashed;
|
|
text-align: center;
|
|
}
|
|
|
|
.boxmodel-margin {
|
|
background: rgba(245, 158, 11, 0.08);
|
|
border-color: rgba(245, 158, 11, 0.3);
|
|
padding: 14px 20px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.boxmodel-border {
|
|
background: rgba(161, 161, 170, 0.08);
|
|
border-color: rgba(161, 161, 170, 0.3);
|
|
padding: 14px 20px;
|
|
width: 100%;
|
|
}
|
|
|
|
.boxmodel-padding {
|
|
background: rgba(34, 197, 94, 0.08);
|
|
border-color: rgba(34, 197, 94, 0.3);
|
|
padding: 14px 20px;
|
|
width: 100%;
|
|
}
|
|
|
|
.boxmodel-content {
|
|
background: rgba(59, 130, 246, 0.08);
|
|
border-color: rgba(59, 130, 246, 0.3);
|
|
padding: 8px 12px;
|
|
width: 100%;
|
|
min-height: 28px;
|
|
}
|
|
|
|
.boxmodel-content span {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--text-body);
|
|
}
|
|
|
|
.boxmodel-label {
|
|
position: absolute;
|
|
top: 1px;
|
|
left: 4px;
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
color: var(--zinc-400);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.boxmodel-value {
|
|
position: absolute;
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--text-body);
|
|
}
|
|
|
|
.boxmodel-value.boxmodel-top { top: 1px; left: 50%; transform: translateX(-50%); }
|
|
.boxmodel-value.boxmodel-right { right: 4px; top: 50%; transform: translateY(-50%); }
|
|
.boxmodel-value.boxmodel-bottom { bottom: 1px; left: 50%; transform: translateX(-50%); }
|
|
.boxmodel-value.boxmodel-left { left: 4px; top: 50%; transform: translateY(-50%); }
|
|
|
|
/* ─── Matched Rules ──────────────────────────────────── */
|
|
|
|
.inspector-rule {
|
|
padding: 6px 0;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}
|
|
|
|
.inspector-rule:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.inspector-rule-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.inspector-selector {
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
color: var(--amber-400);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
max-width: 35ch;
|
|
}
|
|
|
|
.inspector-specificity {
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
background: var(--zinc-600);
|
|
color: var(--zinc-400);
|
|
padding: 0 4px;
|
|
border-radius: var(--radius-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.inspector-rule-props {
|
|
padding-left: 12px;
|
|
}
|
|
|
|
.inspector-prop {
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.inspector-prop.overridden {
|
|
text-decoration: line-through;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.inspector-prop-name {
|
|
color: var(--zinc-400);
|
|
}
|
|
|
|
.inspector-prop-value {
|
|
color: var(--text-body);
|
|
}
|
|
|
|
.inspector-important {
|
|
color: var(--error);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.inspector-rule-source {
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--zinc-600);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
/* UA rules */
|
|
.inspector-ua-rules {
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.inspector-ua-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
background: none;
|
|
border: none;
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
color: var(--zinc-600);
|
|
cursor: pointer;
|
|
padding: 4px 0;
|
|
transition: color 150ms;
|
|
}
|
|
|
|
.inspector-ua-toggle:hover {
|
|
color: var(--zinc-400);
|
|
}
|
|
|
|
.inspector-ua-body.collapsed {
|
|
display: none;
|
|
}
|
|
|
|
/* ─── Computed Styles ────────────────────────────────── */
|
|
|
|
.inspector-computed-row {
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
padding: 0 0 0 4px;
|
|
}
|
|
|
|
.inspector-computed-row .inspector-prop-name {
|
|
color: var(--zinc-400);
|
|
}
|
|
|
|
.inspector-computed-row .inspector-prop-value {
|
|
color: var(--text-body);
|
|
}
|
|
|
|
/* ─── Quick Edit ─────────────────────────────────────── */
|
|
|
|
.inspector-quickedit-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.inspector-quickedit-row {
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
line-height: 1.6;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.inspector-quickedit-row .inspector-prop-name {
|
|
color: var(--zinc-400);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.inspector-quickedit-value {
|
|
color: var(--text-body);
|
|
cursor: pointer;
|
|
padding: 1px 4px;
|
|
border-radius: 2px;
|
|
transition: background 150ms;
|
|
min-width: 40px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.inspector-quickedit-value:hover {
|
|
background: var(--bg-hover);
|
|
}
|
|
|
|
.inspector-quickedit-input {
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
background: var(--bg-base);
|
|
border: 1px solid var(--amber-500);
|
|
border-radius: 2px;
|
|
color: var(--text-heading);
|
|
padding: 1px 4px;
|
|
outline: none;
|
|
width: 100%;
|
|
}
|
|
|
|
/* ─── Send to Agent ──────────────────────────────────── */
|
|
|
|
.inspector-send {
|
|
padding: 8px 12px;
|
|
background: var(--bg-surface);
|
|
border-top: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
position: sticky;
|
|
bottom: 0;
|
|
}
|
|
|
|
.inspector-send-btn {
|
|
width: 100%;
|
|
height: 32px;
|
|
background: var(--amber-500);
|
|
border: none;
|
|
border-radius: var(--radius-md);
|
|
color: #000;
|
|
font-family: var(--font-system);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 150ms;
|
|
}
|
|
|
|
.inspector-send-btn:hover {
|
|
background: var(--amber-400);
|
|
}
|
|
|
|
.inspector-send-btn:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
/* ─── Accessibility ───────────────────────────────────── */
|
|
:focus-visible {
|
|
outline: 2px solid var(--amber-500);
|
|
outline-offset: 1px;
|
|
}
|