/* STATIC design tokens — MUST be present so the boot shell paints correctly
   BEFORE the wasm mounts (a missing :root made the page flash WHITE on load).
   These mirror src/app/style.rs (the canonical values); keep them in sync. */
:root {
  color-scheme: dark;
  --bg: #080808;
  --panel: #0f0f0f;
  --panel-2: #161616;
  --border: #1e1e1e;
  --fg: #c8c8c8;
  --muted: #555;
  --accent: #fff;
  --user: #777;
  --error: #a05050;
  --font-mono: 'IBM Plex Mono', ui-monospace, Menlo, Consolas, monospace;
  --chrome-pad: 16px;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 20px;
  /* Chat vertical rhythm — SSOT (styles.css §3). --turn-gap is the ONE unit of
     space between turns; --line-tight is the line-height WITHIN a paragraph;
     --para-gap is the (larger) space BETWEEN paragraphs in one reply. The square
     header/input/send box is --ctrl-box; the input snaps to multiples of it. */
  --line-tight: 1.45;
  --para-gap: 12px;
  /* The inter-turn gap is the SAME single unit as the header bell·bug·cog gap
     (--chrome-pad) — one rhythm between chat bubbles, between the last message
     and the input, and between the header icons. Don't fork it back to 14px. */
  --turn-gap: var(--chrome-pad);
  --ctrl-box: 38px;
  --z-header: 30;
  --z-modal: 100;
  --z-menu: 120;
  --z-display: 140;
  --z-api-key: 150;
}

/* ===========================================================================
   localharness browser app — component stylesheet (monochrome brutalist).

   DESIGN TOKENS LIVE IN RUST. The `:root { … }` block (palette, --font-mono,
   --chrome-pad, the --space-* scale, the --z-* overlay layers) is the Rust
   source of truth in `src/app/style.rs::root_tokens_css`, injected into
   <head> as <style id="lh-tokens"> at mount, ahead of this file. Edit the
   design system THERE (Rust consts), not by hand here. CSS custom properties
   resolve at use-time, so the var() references below pick up the injected
   block regardless of declaration order.

   ---------------------------------------------------------------------------
   TABLE OF CONTENTS
     1. RESET + GLOBALS .......... box-sizing, body frame, #root, scrollbar,
                                   global no-radius kill switch, boot loader
     2. CHROME .................. sticky header, brand menu, header buttons
     3. CHAT STREAM ............. main layout, transcript, turns, tool-calls,
                                   stage line, inline cards, terminal input,
                                   system/terminal status
     4. OVERLAYS ................ DISPLAY framebuffer, app-fullscreen, admin
                                   modal/tabs, files modal, api-key modal,
                                   shared .modal-close
     5. FORMS + BUTTONS ......... global button archetype + variants, inputs,
                                   labels, key/device rows, pricing card,
                                   tool allowlist
     6. ECONOMY + IDENTITY ...... financial card, verify/tba pills, agents
                                   list, badges, embed card, compose shell,
                                   pairing, share, agent rows/cards, persona
     7. PUBLIC FACE ............. public landing, explore directory
     8. APEX ONBOARDING ......... apex chrome, wallet/seed reveal, step flow,
                                   create-agent form, unclaimed
   Sections are banner-marked inline below; rules are kept in their original
   cascade order (no reordering — that would risk a specificity regression).
   =========================================================================== */

/* === 1. RESET + GLOBALS ================================================== */
* { box-sizing: border-box; }
/* Desktop-app frame: viewport-bound, no page scroll. Border-
   radius is zero everywhere — the user wants the ultra-minimal
   look not the rounded chrome. */
html, body {
  margin: 0; padding: 0;
  background: var(--bg); color: var(--fg);
  /* Single body size — 13px mono. Chrome (rails, panel-titles,
     buttons) uses 11px uppercase via class-specific rules. No
     third or fourth size anywhere. */
  font: 13px/1.5 var(--font-mono);
  /* `100dvh` (dynamic viewport height) accounts for mobile
     browser chrome that resizes the viewport — Safari address bar
     growing/shrinking on scroll, Android navigation gesture
     affordances. Plain 100vh cuts off the bottom on mobile.
     `--lh-vh` is normally unset, so this stays 100dvh; while the
     soft keyboard is open the visualViewport listener (events.rs)
     sets --lh-vh to the VISIBLE height so the app — header included —
     never grows taller than the on-screen area and the sticky header
     can't scroll out of view. */
  height: var(--lh-vh, 100dvh);
  overflow: hidden;
}
#root {
  height: var(--lh-vh, 100dvh);
  display: flex;
  flex-direction: column;
}
/* Keyboard-open compensation (mobile). The class is toggled on <html>
   by the visualViewport listener ONLY while the keyboard occludes the
   viewport; otherwise this whole rule is inert and the plain 100dvh
   layout above applies (desktop never sees it). --lh-vv-top is the
   visualViewport.offsetTop — on iOS the layout viewport scrolls under
   the keyboard, so we translate the app back down into the visible band
   to keep the header pinned to the top of what the user can see. */
html.lh-kb #root {
  transform: translateY(var(--lh-vv-top, 0px));
}
/* Boot placeholder — shown before the wasm bundle mounts and replaces
   #root's contents. Centered (per feedback: the old top-left "loading"
   looked broken on revisit). */
.boot-loading {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: lowercase;
}

/* Global no-radius — wipes all the rounded corners across the
   app. Each component sets its own border-radius:0 explicitly
   too but this is the kill switch. */
button, input, textarea, select, .tag, code, .pricing-card, .financial-card, .header-admin-panel, .terminal-row, .key-row, .seed-words, .button-link, .turn, .tool-call {
  border-radius: 0 !important;
}

/* Custom monochrome scrollbar — thin, no rounding, blends with
   the panel background. Chromium + Firefox both styled. */
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
*::-webkit-scrollbar { width: 8px; height: 8px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--border);
  border: 2px solid var(--bg);
}
*::-webkit-scrollbar-thumb:hover { background: var(--muted); }
*::-webkit-scrollbar-corner { background: transparent; }

/* === 2. CHROME ========================================================== */
/* Sticky header — three zones via CSS grid:
   [brand left][subdomain center][admin right]. Each zone is its
   own column so the center stays centered even when the side
   widths differ. */
.site-header {
  flex-shrink: 0;
  /* Pinned: in the height-locked #root flex column the body never
     scrolls (the transcript scrolls internally), so the header already
     stays put — sticky + z-index makes that resilient on any route
     where an ancestor DOES scroll, so it never slides away on long
     histories. */
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background: var(--bg);
  padding: 0;
  min-height: 40px;
  display: flex;
  align-items: center;
  /* Override the base `header { margin-bottom: 4px }` (an old in-flow
     header rule for unrelated templates). Without this, that 4px leaks
     in as a phantom gap between the sticky header border and the chat
     content below — the "extra px under the header" that no transcript
     rule could reach. */
  margin: 0;
}
/* Chat view only: float the header transparently over the transcript so the
   conversation flows to the very top (no bar, no divider). The header is taken
   OUT of flow (absolute) — the transcript reclaims the full height and scrolls
   UNDER the three icon buttons; `main.layout .transcript`'s padding-top keeps
   the oldest message clear of them at rest. Scoped via :has(+ main.layout) so
   the apex/explore solid header is untouched. The bar is pointer-transparent
   in its dead zones so clicks land on the chat beneath; the brand + admin
   clusters re-enable pointer events for themselves. */
#root { position: relative; }
.site-header:has(+ main.layout) {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  background: transparent;
  pointer-events: none;
}
.site-header:has(+ main.layout) .header-brand,
.site-header:has(+ main.layout) .header-admin { pointer-events: auto; }
.header-inner {
  width: 100%;
  padding: var(--chrome-pad);
  /* iOS status bar / Dynamic Island (PWA standalone + notched Safari):
     pad the header DOWN out of the inset instead of rendering under it.
     env() is 0 on devices without insets; needs viewport-fit=cover. */
  padding-top: calc(var(--chrome-pad) + env(safe-area-inset-top));
  display: flex;
  align-items: center;
  gap: 12px;
}
/* The right cluster is pushed right by `.header-admin { margin-left: auto }`
   (below), NOT by a margin on the brand — that way the turn-status slot can sit
   immediately to the RIGHT of the brand (between it and the auto gap), and a
   header WITHOUT the slot still splits brand-left / admin-right correctly. */
/* Bell + ADMIN live here. Must be a flex row, NOT the default block — as
   block, the inline-flex bell and inline-block admin button aligned by text
   BASELINE (bell sat 3px higher) and had zero gap (touching). Center them on
   one axis with an 8px gap. The dropdown panel is position:fixed, out of flow. */
/* gap == --chrome-pad so the space between the bell and ADMIN equals the
   space from ADMIN to the header's right edge (both 16px) — ADMIN is evenly
   spaced on both sides. */
.header-admin { position: relative; display: inline-flex; align-items: center; gap: var(--chrome-pad); margin-left: auto; }
/* Turn-status slot — the active-phase glyph (brain/waves/wrench) painted by
   chat::stage, sitting immediately right of the brand. Empty at rest:
   `:empty` collapses it to nothing so the button is GONE (not a static
   square) when no turn is in flight. The button itself is fully specified
   (it's NOT a `.header-button`, so it stays pointer-transparent in the
   floating chat header — it's a status, not a control) and pulses while
   shown. 38px box (10px pad + 16px glyph + 1px border) = pixel-match to the
   bell/cog/brand on the same row. */
.turn-status-slot { display: inline-flex; align-items: center; }
.turn-status-slot:empty { display: none; }
.turn-status-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  padding: 10px;
  border: 1px solid var(--border);
  background: var(--bg);
  /* Rests at full --fg (brighter than the idle --muted bell/cog) so the
     "working" state reads as active, then breathes via a COLOR swell — the
     SAME mechanism as the notif bell, so the chip's bg + border stay solid.
     (Animating element opacity faded the whole chip incl. its background —
     "background going transparent with the blinking", feedback #51.) */
  color: var(--fg);
  animation: turn-status-pulse 1.4s ease-in-out infinite;
}
.turn-status-btn svg { display: block; width: 16px; height: 16px; }
/* The working "breathe" — a --fg→white color swell with a soft halo at the
   peak, mirroring notif-bell-pulse. Animating COLOR (not opacity) keeps the
   chip's bg + border solid; the glyph strokes currentColor so only it pulses. */
@keyframes turn-status-pulse {
  0%, 100% { color: var(--fg); filter: drop-shadow(0 0 0 transparent); }
  50%      { color: #fff; filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.55)); }
}

/* Brand dropdown — the top-left "localharness" title is a native
   <details> menu (no JS) exposing Home / Repo / Crate. Marker hidden
   so the summary reads as plain brand text; the panel overlays below
   so it never reflows the header. */
.brand-menu { position: relative; }
/* The brand is the "lh" glyph as a square icon button — same footprint as the
   bell + gear (38px = 10px padding + 16px glyph + 1px border, the box those
   two resolve to). <summary> isn't a <button>, so it doesn't inherit the base
   button border/colour; replicate them here. The lh glyph is wider than tall,
   so the box is fixed-square and the glyph is centred (not stretched). */
.brand-summary {
  cursor: pointer;
  list-style: none;
  user-select: none;
  width: 38px;
  height: 38px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  background: var(--bg);
  border: 1px solid var(--border);
  transition: color 90ms ease, border-color 90ms ease;
}
.brand-summary:hover { color: var(--accent); border-color: var(--fg); }
.brand-summary svg { display: block; height: 15px; width: auto; }
.brand-summary::-webkit-details-marker { display: none; }
.brand-summary::marker { content: ""; }
/* Header icon buttons (bell/feedback/settings) + the play/send control render at
   full white (--accent), BRIGHTER than the --fg body text — the thin line-art
   icons read dimmer than solid text at the same colour, so they need the bump to
   not look muted-grey ("icons too grey, hard to see, more white"). */
.notif-bell-btn,
.feedback-bug-btn,
.admin-button,
.terminal-action-btn { color: var(--accent); }
/* The brand dropdown is now a VERTICAL stack of three SQUARE icon buttons
   (home / github / crab), each one --ctrl-box tall — the SAME footprint as the
   lh summary above and the bell/cog to the right (on-chain #59). The squares
   carry their own borders (like the header buttons), so the menu itself is a
   bare transparent column spaced on the uniform chrome rhythm. */
.brand-menu-items {
  position: absolute;
  top: 100%;
  left: 0;
  /* Drop the menu the uniform chrome gap below the button (same --chrome-pad
     that spaces the header buttons from the chrome edges) — matches the bell
     panel so all header dropdowns share one rhythm. */
  margin-top: var(--chrome-pad);
  z-index: var(--z-menu);
  display: flex;
  flex-direction: column;
  /* Uniform brand spacing between the squares — the same chrome unit that
     separates the header's own icon buttons (.header-admin gap). */
  gap: var(--chrome-pad);
  background: transparent;
  border: 0;
}
/* Each menu entry is a square icon button matching .brand-summary / the header
   chrome buttons: --ctrl-box box, centred glyph, own border, accent line-art. */
.brand-menu-items .brand-icon-btn {
  width: var(--ctrl-box);
  height: var(--ctrl-box);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
  background: var(--bg);
  border: 1px solid var(--border);
  transition: color 90ms ease, border-color 90ms ease;
}
.brand-menu-items .brand-icon-btn:hover { border-color: var(--fg); }
.brand-menu-items .brand-icon-btn svg { display: block; }

/* Header utility buttons — feedback (insect glyph) + admin sit side
   by side at the right of the header. Fixed min-width so the text
   button reads cleanly regardless of label length, and a gap between
   them matching the header's own padding so they don't crowd. */
.header-button {
  min-width: 96px;
}
.header-inner > .header-button + .header-admin, .header-inner > .header-brand + .header-button {
  margin-left: var(--space-2);
}

/* Shared overlay close + admin feedback widget (the overlays that use
   .modal-close are in section 4; the rule lives here so it precedes them in
   the cascade as it always has). */
/* Borderless top-right close — unified across the admin / files / display
   overlays (no bounding box; a plain glyph). */
.modal-close {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  padding: 2px 6px;
}
.modal-close:hover { color: var(--fg); }
/* #64: the textarea no longer carries its OWN border — that nested inside the
   panel's border for a doubled box. It's now a flush inset field (a slightly
   lighter --panel-2 surface) INSIDE the panel's single box; the panel takes the
   focus ring so there's still ONE box, never two. */
.feedback-textarea {
  width: 100%;
  min-height: 120px;
  /* The FIELD box inside the feedback modal — THIS is the element that highlights
     on focus (textarea:focus → accent border), NOT the modal container
     (#64/rawfeedback: "the input field inside the modal should highlight, not the
     whole modal"). outline:none so the border is the only ring (no UA double-ring). */
  background: transparent;
  color: var(--fg);
  border: 1px solid var(--border);
  padding: 10px 12px;
  font: inherit;
  outline: none;
  resize: vertical;
}
/* The modal container border is STATIC — the FIELD inside highlights on focus,
   not the whole modal (#64). (Was .feedback-panel:focus-within → --fg, which lit
   the entire container.) */
/* Inline result message — only takes space when populated, so the bottom
   of the admin feedback section stays flush most of the time. */
.feedback-msg { font-size: 11px; }
.feedback-msg:empty { display: none; }

/* === 3. CHAT STREAM ===================================================== */
main {
  max-width: 1180px;
  margin: 0 auto;
  padding: 32px 24px;
  flex: 1;
  width: 100%;
}
/* Tenant chrome — UNIFIED STREAM: chat IS the app. main is a single
   full-width chat column ([ctx-bar][transcript][terminal-panel]); files
   open as a modal (header [files]), display as a fullscreen overlay.
   No rails, no side columns, no tab bar — same layout on every
   viewport. */
main.layout {
  padding: 0;
  max-width: none;
  flex: 1;                /* claim the rest of the viewport after the header */
  display: flex;
  align-items: stretch;
  gap: 0;
  min-height: 0;          /* let children's overflow:auto kick in  */
  overflow: hidden;       /* prevent the page from growing with chat */
}

/* The chat column — vertical stack: transcript / terminal-panel. The
   transcript scrolls internally; the prompt input stays pinned at the bottom. */
main.layout .col-chat {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* Footer chrome gets the SAME treatment as the header (see
   `.site-header:has(+ main.layout)`): taken OUT of flow and floated
   TRANSPARENTLY over the transcript so the conversation flows to the very
   BOTTOM and scrolls UNDER the input box + send button — no bar, no divider,
   one continuous surface. The containing block is the position:relative #root
   (col-chat / main.layout are static), so main.layout's overflow:hidden never
   clips it — exactly how the absolute header escapes the clip. pointer-events
   are off in the transparent dead zones so clicks land on the chat beneath;
   only the input row + send button (+ the funding CTA when shown) re-enable
   their own, the same way the header re-enables only its brand + admin
   clusters. The input row / send button keep their --bg fill so transcript
   scrolling under them stays legible. */
main.layout .terminal-panel {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  background: transparent;
  pointer-events: none;
}
main.layout .terminal-panel .terminal-row,
main.layout .terminal-panel .terminal-action-btn,
main.layout .terminal-panel .fund-banner { pointer-events: auto; }

/* Transcript fills available vertical space; scrolls internally.
   Content top-aligns; newest is kept in view by scroll_to_bottom()
   on every append (chat.rs). No flex spacer — a `::before { flex: 1 }`
   bottom-pin made SHORT histories sit flush on the input, so focusing
   it (status line + autogrowing textarea) clipped the first messages
   from the top. Mobile already dropped the spacer for the same reason. */
main.layout .transcript {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* NEVER pan sideways (on-chain #81): `overflow-y: auto` alone computes
     overflow-x to auto, so one long hex hash in any child let the whole
     transcript scroll horizontally on a phone. Children wrap (break-word /
     overflow-wrap below); this is the structural guarantee. */
  overflow-x: hidden;
  /* Top + bottom padding clear the two now-FLOATING transparent chrome bars
     (header above, footer below): the first/last message clears the chrome by
     one --chrome-pad and then scrolls UNDER it, so the chat flows edge to edge.
     70px top = header pad (16) + 38px button + 16px uniform-margin gap (the
     first message used to crowd the header at +56). 70px bottom MIRRORS it:
     footer pad (16) + input row (38) + 16px uniform gap — so the last message
     clears the floating input by the SAME single --chrome-pad unit as the first
     clears the header (was 76px → a 22px gap, too large + asymmetric). Plus the
     iOS safe-area insets on each end. Side gutters stay the uniform chrome
     margin. */
  padding: calc(env(safe-area-inset-top) + 70px) var(--chrome-pad) calc(env(safe-area-inset-bottom) + 70px);
  margin-top: 0;
  display: flex;
  flex-direction: column;
  /* The inter-turn rhythm IS this flex `gap` — EXTERNAL space between the turn
     containers, the SSOT. Turns carry no margin/padding of their own (see §3),
     so the only space inside a bubble is the body's square padding; turns float
     apart on this one declared unit (--turn-gap == the uniform --chrome-pad). */
  gap: var(--turn-gap);
  /* Hide the chat scrollbar but KEEP scrolling — the bar over the edge-to-edge
     transcript reads as chrome on the otherwise bare surface. */
  scrollbar-width: none;
}
main.layout .transcript::-webkit-scrollbar { display: none; }
/* Flex `gap` only applies BETWEEN children, so the first turn already sits flush
   under the floating header with no leading gap — no per-turn margin to cancel
   anymore. This zeroing stays as a belt-and-braces guard for any non-`.turn`
   first child (e.g. a leading system-status line). */
main.layout .transcript > :first-child {
  margin-top: 0;
  padding-top: 0;
}
main.layout .transcript > * { flex-shrink: 0; }
/* System status line — lives IN the transcript stream (one replaceable
   line appended by dom::set_status), not in the input chrome. Terse,
   muted, borderless; error variant only changes the colour. */
.system-status {
  font-size: 12px;
  color: var(--muted);
  border: 0;
  overflow-wrap: anywhere; /* tx hashes / addresses wrap, never overflow (#81) */
}
.system-status.err { color: var(--error); }
/* Typed-confirmation prompt: a bordered system panel, deliberately NOT the
   borderless gray of a chat turn so it never reads as user input (feedback). */
.confirm-callout {
  display: inline-block;
  font-size: 12px;
  color: var(--muted);
  border: 1px solid var(--muted);
  padding: 8px 10px;
  margin: 4px 0;
  max-width: 360px;
  overflow-wrap: anywhere; /* long args (addresses/amounts) wrap in-box (#81) */
}
.confirm-callout-head {
  color: var(--muted);
  text-transform: lowercase;
  letter-spacing: 0.08em;
  margin-bottom: 5px;
}
.confirm-callout-body { color: var(--muted); }
.confirm-code {
  color: var(--accent);
  font-weight: 600;
  letter-spacing: 0.18em;
  padding: 0 2px;
}
/* Empty slot divs (stage containers, card slots, cleared regions) must not
   stack 12px gaps into visible whitespace. */
main.layout .transcript > *:empty { display: none; }

/* === 4. OVERLAYS ======================================================== */
/* DISPLAY overlay — the framebuffer "screen" the cartridge loader
   blits into, presented as a fullscreen dismissable overlay (admin-
   modal pattern; ToggleDisplay opens/closes it). Black letterbox
   stage; the canvas is sized 16:9 and scaled with crisp (pixelated)
   edges so individual framebuffer pixels stay sharp instead of being
   smoothed by the browser. Sits above the files modal (--z-modal) and
   below the api-key modal (--z-api-key). */
.display-overlay {
  position: fixed; inset: 0;
  z-index: var(--z-display);
  background: rgba(0, 0, 0, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.display-overlay > .modal-close {
  position: absolute;
  top: 8px;
  right: 12px;
  z-index: 1;
}
.display-overlay .display-wrap { max-width: 1100px; }
/* TERMINAL surface (CLI sandbox, feedback #6) — the text counterpart of the
   display canvas. Reuses the .display-overlay chrome; this is just the scroll
   box + monochrome stream inside it. */
.terminal-surface {
  width: 100%;
  max-width: 900px;
  max-height: 80vh;
  overflow: auto;
  border: 1px solid var(--border);
  background: var(--bg);
  padding: 12px 14px;
  font-size: 13px;
  line-height: 1.5;
}
.terminal-surface .terminal-line { white-space: pre-wrap; word-break: break-word; }
.terminal-surface .term-argv { color: var(--fg); font-weight: 600; }
.terminal-surface .term-muted { color: var(--muted); }
.terminal-surface .term-exit { color: var(--muted); margin-top: 8px; }
.terminal-surface .terminal-label { color: var(--muted); margin-top: 10px; }
.terminal-surface pre.terminal-out,
.terminal-surface pre.terminal-err {
  margin: 6px 0 0;
  white-space: pre-wrap;
  word-break: break-word;
}
.terminal-surface pre.terminal-err { color: var(--error); }
/* Just the canvas stage — no toolbar. */
.display-wrap {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  min-height: 200px;
}
.display-stage {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 0;
  background: #000;
  padding: 8px;
}
.display-canvas {
  display: block;
  /* VARIABLE RESOLUTION: no hard-coded aspect. The canvas keeps the
     intrinsic aspect of its backing store (the cartridge's dims() — the
     worker sets width/height to e.g. 320x240 OR 270x480 portrait), and
     max-width/height scale it DOWN proportionally to fit the stage
     (replaced-element aspect preservation). A hard-coded aspect here squashed
     portrait/square cartridges; the canvas just keeps its own (the 320x240
     default is 4:3). */
  max-width: 100%;
  max-height: 100%;
  background: #000;
  image-rendering: pixelated;
  /* No `object-fit` here: with `max-width/height:100%` the canvas is an
     intrinsically-sized replaced element, so the browser already letterboxes
     it within the centered stage and the element BOX equals the painted
     pixels. `object-fit:contain` only matters when width AND height are both
     pinned (then it letterboxes INSIDE the box), which would desync
     getBoundingClientRect() from the painted content and offset every
     pointer hit (`display::set_pointer` maps client coords off the rect). */
  /* Touch drags draw into the cartridge instead of scrolling/zooming
     the page — required for drawing apps to work on phones. */
  touch-action: none;
}

/* App mode — the subdomain booted straight into its cartridge. The
   canvas fills the viewport; a faint "edit" link bottom-right is the
   owner's escape hatch back to the workshop (?edit=1). */
.app-fullscreen {
  flex: 1;
  position: relative;
  display: flex;
  min-height: 0;
  background: #000;
}
.app-stage {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 0;
  padding: 8px;
}
/* Broadcast composer — a cartridge's broadcast_compose() asked the host
   for a text input (pixels can't summon a keyboard). Floats centered over
   whichever cartridge surface is live (display overlay or fullscreen
   public face), above the display layer. */
.broadcast-composer {
  position: fixed; inset: 0;
  z-index: calc(var(--z-display) + 1);
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.broadcast-composer-panel {
  width: 100%;
  max-width: 420px;
  padding: var(--space-4);
  background: var(--panel);
  border: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}
.broadcast-composer-title {
  color: var(--fg);
  font-weight: 600;
}
.broadcast-composer-panel input {
  width: 100%;
  padding: var(--space-2);
  font-family: var(--font-mono);
  color: var(--fg);
  background: var(--panel-2);
  border: 1px solid var(--border);
}
/* Header notification bell + dropdown panel. Monochrome, matches the
   header-button chrome; the panel hangs below the bell, right-aligned. */
.notif-bell-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.notif-bell-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  /* Icon-only button: opt OUT of the .header-button 96px min-width (that's
     for text buttons). Without this the bell was a 96px-wide empty box with
     a 15px glyph jammed in the left corner. Square it up. */
  min-width: 0;
  padding: 10px;
  /* Solid fill so transcript content scrolling UNDER the floating chat header
     doesn't bleed through the button (on the apex's solid header this is a
     no-op — same colour). */
  background: var(--bg);
}
/* 16px (1:1 with the 0 0 16 16 viewBox — crisp) so the bell's box height
   matches the settings gear on the same row instead of sitting 1.5px
   shorter. */
.notif-bell-btn svg { display: block; width: 16px; height: 16px; }
/* Unread is signalled by a gentle PULSE of the bell itself (below), NOT a
   count pill — a number widened the square icon button. The badge span
   survives only as the unread FLAG: present & not [hidden] ⇒ unread. It
   never renders, so the button stays square. */
.notif-badge { display: none; }
/* Unread → a 1.4s sine swell of the bell glyph that DIPS to --muted and
   peaks at pure white (the icon strokes currentColor), with a soft white
   halo blooming at the peak. The wide --muted→white range is what makes it
   read as a pulse — a --fg→white sweep was both too bright at rest and too
   narrow to perceive (it "just stayed bright"). ease-in-out keeps it a
   smooth swell, not a strobe. Stops the instant the badge gains [hidden]
   (panel opened / cleared); the bell then snaps back to its resting --fg. */
.notif-bell-btn:has(.notif-badge:not([hidden])) {
  animation: notif-bell-pulse 1.4s ease-in-out infinite;
}
@keyframes notif-bell-pulse {
  0%, 100% { color: var(--muted); filter: drop-shadow(0 0 0 transparent); }
  50%      { color: #fff; filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.55)); }
}
.notif-panel {
  position: absolute;
  top: 100%;
  right: 0;
  /* Uniform chrome gap below the bell — matches the brand menu. */
  margin-top: var(--chrome-pad);
  min-width: 240px;
  max-width: 320px;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  z-index: var(--z-header);
}
.notif-panel[hidden] { display: none; }
.notif-panel-empty {
  padding: 12px;
  font-size: 12px;
  color: var(--muted);
}
.notif-item {
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
}
.notif-item:last-child { border-bottom: none; }
.notif-item-title { color: var(--fg); }
/* overflow-wrap: long tokens (URLs/hashes) wrap instead of overflowing the
   narrow mobile panel off-screen (feedback #27). */
.notif-item-body { color: var(--muted); margin-top: 2px; overflow-wrap: anywhere; }
.notif-panel-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 6px 12px;
  border-bottom: 1px solid var(--border);
}
/* Header feedback-bug button + its dropdown (#36) — same square icon-button
   treatment as the bell/cog, with the panel anchored under it like the notif
   panel. */
.feedback-bug-wrap {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.feedback-bug-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Icon-only: opt OUT of the .header-button 96px text min-width and square up. */
  min-width: 0;
  padding: 10px;
  /* Solid fill so transcript scrolling under the floating chat header doesn't
     bleed through (see .notif-bell-btn). */
  background: var(--bg);
}
.feedback-bug-btn svg { display: block; width: 16px; height: 16px; }
.feedback-panel {
  position: absolute;
  top: 100%;
  right: 0;
  /* Uniform chrome gap below the button — matches the bell / brand menu. */
  margin-top: var(--chrome-pad);
  width: 280px;
  max-width: 90vw;
  max-height: 60vh;
  overflow-y: auto;
  background: var(--bg);
  /* The PANEL carries the only frame (#64): one clean box, with the textarea a
     flush --panel-2 inset inside it (its own border removed above) — never a
     box-in-a-box. The :focus-within rule below colors THIS border on focus. */
  border: 1px solid var(--border);
  z-index: var(--z-menu);
  padding: 12px;
}
.feedback-panel[hidden] { display: none; }
/* Feedback widget (#D) — a self-contained popover: NO title, a clean vertical
   column with ONE uniform gap between the textarea, the full-width submit, and
   the receipt slot. The full-width submit kills the old flex-end offset (it sat
   pushed to the right with odd slack); the receipt slot (#feedback-msg) sits on
   the SAME gap below it, no special margin, and collapses when empty so the
   panel doesn't reserve dead space before a submit. */
.feedback-form {
  display: flex;
  flex-direction: column;
  /* ONE uniform unit of space between the textarea, the submit button, and the
     receipt slot (rawfeedback: "uniform space above the submit button"). */
  gap: var(--space-3);
}
.feedback-form .feedback-submit { width: 100%; margin: 0; }
.feedback-panel .feedback-msg { margin: 0; }

.notif-clear-prompt { font-size: 11px; color: var(--muted); margin-right: auto; }
.notif-clear-btn {
  background: none;
  border: none;
  padding: 2px 4px;
  font: inherit;
  font-size: 11px;
  color: var(--muted);
  cursor: pointer;
}
.notif-clear-btn:hover { color: var(--fg); text-decoration: underline; }

.app-edit {
  position: absolute;
  bottom: 8px;
  right: 10px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  text-decoration: none;
  opacity: 0.35;
}
.app-edit:hover { opacity: 1; color: var(--fg); }

/* === 7. PUBLIC FACE ===================================================== */
/* --- Default public face (no cartridge published) --- */
.public-face {
  position: relative;
  /* Fills #root (100dvh flex column) BELOW the shared public-face header —
     mirrors .app-fullscreen. `min-height:100vh` here overflowed by the header's
     height once the header was added (feedback #53); flex:1 + min-height:0 is the
     correct fill. Overflow stays visible (not auto) so justify-center never traps
     tall content above an unscrollable top edge. */
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 28px;
  padding: 48px 20px;
  box-sizing: border-box;
}
.public-hero { text-align: center; }
.public-title {
  margin: 0;
  font-size: 40px;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--accent);
}
.public-tagline {
  margin: 8px 0 0;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--muted);
}
.public-meta {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 220px;
}
.public-meta-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 16px;
}
.public-meta-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
}
.public-meta-value { font-size: 12px; color: var(--fg); text-decoration: none; }
.public-meta-value:hover { color: var(--accent); text-decoration: underline; }
.public-directory { width: 100%; max-width: 360px; }
.public-section-title {
  margin: 0 0 6px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--muted);
  text-align: center;
}
.public-footer {
  position: absolute;
  bottom: 16px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
}
.public-footer a { color: var(--muted); text-decoration: none; }
.public-footer a:hover { color: var(--fg); }

/* Public-face picker (admin → agent tab) */
.public-face-picker { display: flex; justify-content: center; gap: 6px; margin: 6px 0; flex-wrap: wrap; }
.public-face-preview { margin-top: 6px; }
.public-face-preview a {
  font-size: 11px;
  color: var(--muted);
  text-decoration: none;
}
.public-face-preview a:hover { color: var(--fg); }

/* Editor — the editable text surface that opens (inside the files
   modal's #fs-viewer) when a file is clicked in the file list. */
.editor { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.editor-header {
  flex-shrink: 0;
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 16px;
  border-bottom: 1px solid var(--border);
}
.editor-path {
  font-size: 11px; color: var(--muted);
  text-transform: lowercase; word-break: break-all;
}
.editor-actions { display: flex; gap: 12px; }
.editor-textarea {
  /* min-height keeps the editor usable inside the height-bound files
     dialog — without it the flex child collapses to ~2 lines wedged
     under the editor header. */
  flex: 1; min-height: 160px;
  background: var(--bg);
  border: 0; outline: none; resize: none;
  color: var(--fg);
  font: 13px/1.5 var(--font-mono);
  padding: 12px 16px;
}

/* === 8. APEX ONBOARDING (page layouts: apex family + explore) =========== */
/* Uniform top gap under the site-header — matches the base `main`
   rule and `.explore-main` (32px). The old asymmetric 64px top made
   the apex-family pages (apex/claim/landing) sit visibly lower than
   every other page, the "uneven spacing under the header" report. */
/* overflow-y: the body never scrolls (#root is viewport-bound), so a
   short window / soft-keyboard viewport must scroll the apex page
   INTERNALLY or the hero's lower half (the invite input!) gets clipped. */
.apex-main { display: block; padding: 32px 24px; max-width: 1180px; overflow-y: auto; }
.apex-main > .col-chat { max-width: 600px; margin: 0 auto; }
/* Fresh front door (apex(), no wallet): fill the space between header and
   footer and vertically center the single create CTA cluster. Scoped to
   `.apex-front` so the claim/explore pages keep their top-aligned flow. */
.apex-main.apex-front { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.apex-main.apex-front > .col-chat {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* The gap under the site-header is the `main` top padding alone (32px).
   The first content block inside an apex-family page must not stack its
   own top margin on top of that padding, or the gap reads larger than
   on every other page — the "uneven spacing under the header" report.
   `.step`/`.apex-hero` keep their margins for SEPARATION between blocks;
   we only zero the FIRST one. */
.apex-main > .col-chat > :first-child,
.apex-main > :first-child { margin-top: 0; }
/* Soft-keyboard fix (#90): on mobile, focusing the claim/onboarding input
   makes the browser scroll it into the shrunken (above-keyboard) viewport,
   which left the submit button — directly BELOW the input in the .create-form
   column — hidden behind the keyboard, forcing a tap-away to dismiss before
   submit. scroll-margin-bottom reserves room below the input in that scroll
   calculation, so the input parks higher and the button stays on screen.
   ~120px clears the button + its 24px gap on the tightest layout. */
.apex-main .create-input { scroll-margin-bottom: 120px; }
/* Apex footer links (src/landing.rs::apex_links) — ONE centered row;
   explore is the uninvited visitor's escape hatch. wrap keeps both links
   on-screen at 390px. */
.apex-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 12px 32px;
  margin-top: 64px;
}
.apex-links a { color: var(--muted); text-decoration: none; font-size: 12px; }
.apex-links a:hover { color: var(--fg); }
/* Page footer on the fresh front door — pins "for agents →" to the bottom
   (sibling of .apex-main in the #root flex column). The links inside don't
   need their inline 64px top push here. */
.apex-footer {
  flex-shrink: 0;
  display: flex;
  justify-content: center;
  padding: 20px 24px;
}
.apex-footer .apex-links { margin-top: 0; }

/* Public agent directory (?explore=1). `html/body/#root` are height-locked
   (100dvh, overflow:hidden), so the page itself never scrolls — the grid must.
   Make #root a flex column on this route and let .explore-main flex-fill the
   space below the sticky header and scroll internally (was a fixed-height
   overflow that clipped the list). No title header anymore — the grid is the
   page. */
#root:has(> .explore-main) { display: flex; flex-direction: column; }
.explore-main {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  max-width: 1100px;
  margin: 0 auto;
  padding: 24px;
  overflow-y: auto;
  overflow-x: hidden;
}
.explore-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}
.explore-empty { color: var(--muted); font-size: 13px; }
.explore-empty a { color: var(--accent); }
.explore-card {
  display: flex; flex-direction: column; gap: 4px;
  padding: 16px;
  border: 1px solid var(--border);
  background: var(--panel);
  text-decoration: none;
  /* Uniform card height regardless of whether the agent published a persona
     preview: a 3-line card (name + host + preview) is ~103px; persona-less
     cards (2 lines) and the lone last-row card were 77-94px, so grid rows
     rendered ragged (CSS grid stretch only equalizes WITHIN a row, not
     across rows). This floor makes every row the same height. */
  min-height: 103px;
}
.explore-card:hover { border-color: var(--fg); }
/* Single-line clip: long unbroken agent names (e.g. qa-onchain-1780895987-…)
   otherwise overflow/wrap and misalign the card grid. */
.explore-card-name { color: var(--fg); font-size: 15px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.explore-card-host { color: var(--muted); font-size: 11px; font-variant-numeric: tabular-nums; }
/* One-line on-chain-persona preview — same muted, single-line-clipped
   language as .agent-preview (public-landing portfolio cards). A small
   top margin separates it from the host line; no radius/shadow. */
.explore-card-preview {
  margin-top: 4px;
  font-size: 12px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* === 3. CHAT STREAM (cont.): terminal input + status ==================== */
/* Terminal interior — bare prompt, no chrome. Status renders
   empty by default; only fills when there's something to say.
   Vertical geometry MIRRORS the header: one content row wrapped in
   --chrome-pad on every side (the header's .header-inner padding),
   so header and footer read as the same chrome bar. */
.terminal-body {
  padding: var(--chrome-pad);
  /* iOS home indicator (PWA standalone / notched Safari): keep the input
     row above it. env() is 0 everywhere else; needs viewport-fit=cover. */
  padding-bottom: calc(var(--chrome-pad) + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: 4px;
}
/* The funding banner is empty by default — it must contribute ZERO
   height. Without this it stays a flex item and .terminal-body's gap
   inserts 4px ABOVE the input row but not below it (the footer
   asymmetry report). display:none removes it from flex layout
   entirely, gap included. */
.fund-banner:empty { display: none; }
.terminal-status {
  font-size: 11px;
  color: var(--muted);
  min-height: 0;
}
.terminal-status:empty { display: none; }
.terminal-status.err { color: var(--error); }
/* The footer row: [the textarea row][the square action button] (#34). The send
   /stop control is lifted OUT of the input box into its own square bordered
   button to the RIGHT, matching the header bell/cog. */
.terminal-footer {
  display: flex;
  /* --chrome-pad (16px) between the input box and the send button — the SAME
     uniform gap as the header bell·bug·cog cluster (.header-admin), so the
     send button sits off the field by the exact brand chrome rhythm. */
  gap: var(--chrome-pad);
  align-items: flex-end;
}
.terminal-row {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  /* ONE --space-2 (8px) step between the prompt glyph and the textarea, EQUAL to
     the row's own horizontal inset below — so the text sits the same 8px off the
     glyph as the glyph sits off the field edge (the reconciled text margin, #C).
     The send button is no longer inside this row (#34). */
  gap: var(--space-2);
  /* HORIZONTAL inset only (--space-2, matching the glyph→text gap so the inner
     margin is uniform on both sides of the text). The vertical size is the fixed
     min-height below, NOT padding — so the resting row is EXACTLY one --ctrl-box
     (38px) regardless of the textarea font size. (An 8px-padded row grew to 40px
     on the 16px mobile-zoom font, taller than the send button + header chrome it
     must line up with.) The 1-line textarea is vertically centred in the box;
     multi-line content grows the row upward via the textarea autogrow, which
     snaps to --ctrl-box multiples (events::autogrow_textarea). */
  padding: 0 var(--space-2);
  background: var(--bg);
  border: 1px solid var(--border);
  /* EXACTLY one --ctrl-box (the 38px header bell/cog + send button box,
     border-box) so the input field, the send button, and the header buttons are
     pixel-identical. events::autogrow_textarea sets the inline height to the
     next whole --ctrl-box step as content grows (the field that snaps to the
     grid now), CAPPED at 3 boxes — past which the textarea scrolls (below). The
     textarea inside is sized to its CONTENT and stays vertically CENTRED here
     (align-items:center), so a single line's text/cursor sit in the middle of
     the box instead of pinned to the top. */
  min-height: var(--ctrl-box);
  max-height: calc(var(--ctrl-box) * 3);
}
.terminal-row:focus-within { border-color: var(--accent); }
.terminal-prompt {
  color: var(--accent);
  font-weight: 600;
  user-select: none;
  line-height: 1;
}
.terminal-row textarea {
  flex: 1;
  /* A flex child won't shrink below its intrinsic (cols-based) width without
     this — without it the textarea forced .terminal-row past the viewport on
     narrow screens, pushing the new square send button + the header's right
     cluster off-screen (#34 regression, mobile-only). */
  min-width: 0;
  background: transparent;
  border: 0;
  outline: none;
  resize: none;            /* no native grip — height is auto-grown in JS */
  color: var(--fg);
  caret-color: var(--accent);   /* terminal-style accent cursor */
  font: inherit;
  padding: 0;
  min-height: 16px;
  /* Sized to CONTENT by events::autogrow_textarea (inline height = scrollHeight)
     and vertically centred by the row, so one line's text/cursor sit in the
     MIDDLE of the box, not at the top. The parent `.terminal-row` is what snaps
     to the 38px grid + caps at 3 boxes; this textarea max-height matches the cap
     so the content scrolls within it once the row stops growing. A long paste
     can't push the field off-screen. */
  max-height: calc(var(--ctrl-box) * 3);
  overflow-y: auto;
  /* Hide the scrollbar but KEEP scrollability (a long paste still scrolls past
     the 3-box cap) — a visible bar in the bare prompt row reads as chrome. */
  scrollbar-width: none;
  line-height: 1.4;
}
.terminal-row textarea::-webkit-scrollbar { display: none; }
/* The send/stop control as its OWN square bordered icon button to the right of
   the input (#34) — the SAME 38px box, 1px border, --bg fill, centered SVG as
   the header bell/cog (.brand-summary / .admin-button). Sits at the bottom of
   the footer row (align-items:flex-end) so it stays put as the input grows. */
.terminal-action-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  width: 38px;
  height: 38px;
  background: var(--bg);
  flex-shrink: 0;
}
/* 16px icon — the SAME size as the header bell/bug/cog SVGs, so the play/stop
   control reads as one of the header's line-art icon buttons (not a smaller,
   odd glyph). */
.terminal-action-btn svg { display: block; width: 16px; height: 16px; }
.terminal-send {
  padding: 0;
  /* Pure white, matching the (now-white) header icon buttons — the send/play
     glyph is a live control, not muted chrome. */
  color: var(--accent);
  cursor: pointer;
}
.terminal-send:hover { color: var(--accent); border-color: var(--fg); }
/* Stop button (square SVG) occupies the play slot while a turn streams. A clean,
   strong RED — the one intentional colour, for the live destructive control —
   not the muted grey-red --error used for error text (rawfeedback: "just red,
   not grey-red", same spirit as making the icons full-white). */
.terminal-stop { color: #d83a3a; }
.terminal-stop:hover { color: #ff5252; border-color: var(--fg); }

/* === 6. ECONOMY + IDENTITY ============================================== */
/* Financial column visuals */
.financial-card, .financial-empty, .financial-placeholder {
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* Match the 14px bottom margin every .admin-section carries, so the
     section that follows the identity card (AGENT WALLET on the account
     tab) sits on the same rhythm instead of jamming flush against TOOLS. */
  margin-bottom: 14px;
  /* CLS (#64): #financial-slot starts as a one-char placeholder and is
     swapped for the 5-line agent card after async on-chain reads. Reserve
     the card's vertical space up front so the account tab doesn't jump.
     ~100px sits just under a filled card (5 lines + gaps), so no dead
     space when populated. */
  min-height: 100px;
}
.financial-placeholder { color: var(--muted); font-size: 11px; }
/* The empty state is intentionally flat — drop the reservation. */
.financial-empty { min-height: 0; }

.financial-line {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px;
}
.financial-label {
  font-size: 11px; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.06em;
}
.financial-value { font-size: 12px; color: var(--fg); }
.financial-balance { color: var(--fg); font-variant-numeric: tabular-nums; }
.financial-tba {
  font-size: 12px; color: var(--fg); text-decoration: none;
  font-variant-numeric: tabular-nums;
}
.financial-tba:hover { text-decoration: underline; }

header { /* old in-flow header still used in unrelated templates */
  display: flex; align-items: baseline; gap: 12px;
  margin-bottom: 4px;
}
h1 {
  font-size: 16px; font-weight: 600;
  letter-spacing: 0.02em;
  margin: 0;
}
.tag {
  font-size: 11px; color: var(--accent);
  border: 1px solid var(--accent);
  padding: 1px 6px;
}

/* CLS (#64): the verify pill mounts as `verifying…` then swaps in place
   for `✓ owner` / `visitor · owner 0x…` once the on-chain owner resolves.
   The states are different widths, so reserve a baseline width and center
   the label — the header stops reflowing when verification lands. ~92px is
   just under the `verifying…` width, so no dead space in any state. */
.verify-pill {
  display: inline-block;
  min-width: 92px;
  text-align: center;
}
.verify-pill.verify-pending { color: var(--muted);  border-color: var(--border); }
.verify-pill.verify-ok { color: var(--fg); border-color: var(--fg); }
.verify-pill.verify-visitor { color: var(--muted); border-color: var(--muted); }
.verify-pill.verify-unregistered { color: var(--muted);  border-color: var(--border); }
.verify-pill.verify-failed { color: var(--error);  border-color: var(--error); }
.tba-pill { color: var(--fg); border-color: var(--fg); text-decoration: none; font-variant-numeric: tabular-nums; }
.tba-pill:hover { background: rgba(255, 255, 255, 0.05); }

/* Bare agents list — just subdomain names, no border / row chrome.
   Top-aligned on apex (margin handled by .step). */
.agents-list { max-width: 480px; margin: 0 auto; }
.agents-empty { display: none; }
.agents-rows { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
.agent-name {
  color: var(--fg);
  text-decoration: none;
  display: inline-block;
}
.agent-name:hover { color: var(--accent); }
/* MAIN badge — small accent chip next to the user's primary
   subdomain in the agents list. Reads as `name [main]`. */
.main-badge {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 6px;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--bg);
  background: var(--fg);
  vertical-align: middle;
}
/* `alt` chip — the non-MAIN identity marker in the apex agents list.
   Outline-only / muted so MAIN stays the visually dominant one. */
.alt-badge {
  display: inline-block;
  margin-left: 8px;
  padding: 1px 6px;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--border);
  vertical-align: middle;
}

.button-link {
  display: inline-block;
  background: var(--accent);
  color: #0b0d12;
  text-decoration: none;
  padding: 10px 16px;
  border-radius: 6px;
  font-weight: 600;
}
.button-link:hover { opacity: 0.9; }

h1 a { color: inherit; text-decoration: none; }
h1 a:hover { color: var(--accent); }

/* Apex front door (src/landing.rs) — the fresh-visitor hero. ONE centered
   column: wordmark / one value-prop line / the invite input as the
   centerpiece — nothing else competes (the create/import buttons are gone;
   import lives in admin). Hierarchy is SIZE + SPACING only: wordmark huge,
   tagline muted, 56px of air before the form. Preview without a browser
   profile: `cargo test --features browser-app landing_preview` →
   target/landing-preview.html. */
.apex-hero {
  margin: 16vh 0 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
}
/* clamp: ~36px on a 390px phone, 56px on desktop — always one line. */
.apex-wordmark {
  margin: 0;
  font-size: clamp(32px, 9.5vw, 56px);
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
  color: var(--fg);
}
/* 60ch holds the whole sentence on ONE line on desktop; phones wrap
   naturally inside the column padding. */
.apex-tagline {
  margin: 24px 0 0;
  max-width: 60ch;
  font-size: 13px;
  line-height: 1.6;
  color: var(--muted);
}
/* The literal `<name>` placeholder host — same muted tone (monochrome:
   no second color), nowrap so the hostname never breaks mid-domain. */
.apex-tagline-host { white-space: nowrap; }
/* Hero overrides of the generic .create-form (64px top / 24px gap are the
   claim page's numbers): a tight input-over-button stack, full-width
   button so the redeem CTA reads as the page's one action. */
.apex-hero .create-form {
  margin: 56px auto 0;
  gap: 12px;
  width: 100%;
  max-width: 360px;
}
.apex-hero .create-input { padding: 14px 16px; }
/* The redeem CTA is the page's ONE action — INVERTED (filled) so it
   outweighs the outline input above it. Still monochrome: fg-on-bg
   flipped, no new tone. `.failed` falls back to the outline treatment
   in the error tone (parity with the global .create-button.failed). */
.apex-hero .create-button {
  width: 100%;
  padding: 14px 32px;
  background: var(--fg);
  border-color: var(--fg);
  color: var(--bg);
  font-weight: 600;
}
.apex-hero .create-button:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
.apex-hero .create-button.failed {
  background: transparent;
  border-color: var(--error);
  color: var(--error);
}
.apex-hero .step-msg { margin-top: 16px; }

/* Fresh-visitor PRIMARY onboarding CTA (templates::apex_create_identity).
   pip-qa couldn't find a Create button — so this leads the page as the one
   obvious first step. Prominence is monochrome-only: a heavy strong border,
   a large headline, generous spacing, and a FILLED-INVERSE button. No
   colored accent — every tone is --fg / --bg / --muted / --border. */
.apex-onboard {
  /* Shrink-wrap the WIDEST line (the offer) so the stretched input below it
     ends up exactly the offer's width — the WIDE half of the chiasma the user
     wants. `.apex-front` centers the whole cluster in the viewport. */
  width: fit-content;
  max-width: 90vw;
  margin: 0 auto;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.apex-onboard-title {
  margin: 0;
  font-size: clamp(26px, 6vw, 34px);
  font-weight: 600;
  letter-spacing: -0.03em;
  line-height: 1.1;
  color: var(--fg);
}
.apex-onboard-blurb {
  margin: 16px 0 0;
  max-width: 44ch;
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
}
/* Onboarding pitch (landing::onboard_pitch). The label is the NARROW half of
   the chiasma — same 14ch width as the CREATE button below. The offer is the
   WIDE half — its natural width sets the column, which the input then matches. */
.onboard-pitch-label {
  width: 14ch;
  align-self: center;
  margin: 0 0 var(--space-1);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--muted);
}
.onboard-pitch-offer {
  margin: 0 0 var(--chrome-pad);   /* 16px to the input — equals the form's gap */
  /* 16px (== the input's font, incl. the iOS no-zoom floor) so the offer line
     and the 24ch input field render the SAME monospace width on every screen. */
  font-size: 16px;
}
/* The shared claim form (fresh door + authed apex). Uniform control heights ==
   the header bell/cog box (16px glyph + 2×10 pad + 2×1 border = 38px). The
   input fills the WIDE measure; CREATE matches the NARROW label (14ch). The
   input→button gap equals the offer→input gap (the brand 16px). */
/* `.create-form.claim-form` (two classes) outranks the base `.create-form`
   rule that lands later in the file, so the 64px top margin is actually
   dropped — the form is positioned by its container's gap / the pitch margin. */
.create-form.claim-form {
  margin: 0;
  gap: var(--chrome-pad);
  width: auto;
}
.claim-form .create-input {
  height: 38px;
  padding: 0 12px;
  /* 24ch == the offer line's character count, both at 16px monospace → the
     field is exactly as wide as "1 agent + 200 $LH for $2". Fixed (not 100%) so
     the fresh door and the authed apex render an identical-width field. */
  width: 24ch;
  font-size: 16px;
}
.claim-form .create-button {
  height: 38px;
  width: 14ch;
  min-width: 0;
  padding: 0;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* Filled-inverse CTA — now used only by the checkout "pay" button
   (#lh-pay-button); the front-door CREATE moved to the shared .create-button.
   Width is set inline on the pay button (100%), so none here. */
.apex-onboard-cta {
  padding: 14px 32px;
  font-size: 15px;
  font-weight: 600;
  background: var(--fg);
  border: 1px solid var(--fg);
  color: var(--bg);
  cursor: pointer;
  font-family: inherit;
}
.apex-onboard-cta:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
.apex-onboard-cta:disabled { opacity: 0.5; cursor: default; }
/* Quiet secondary door — text-link weight, never competes with the CTA. */
.apex-onboard-secondary { margin: 18px 0 0; }
.apex-onboard-import {
  background: transparent;
  border: 0;
  padding: 4px;
  font-size: 13px;
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
  font-family: inherit;
}
.apex-onboard-import:hover { color: var(--fg); }
/* When the invite hero follows the primary create CTA it's the SECONDARY
   path — tighten its top margin (no second viewport-push) and shrink the
   wordmark so it doesn't rival the create card above it. */
.apex-onboard + .apex-hero { margin-top: 48px; }
.apex-onboard + .apex-hero .apex-wordmark { font-size: clamp(20px, 5vw, 28px); }

/* Signer chrome (signer_no_identity / signer_chrome) still uses the old
   headline scale. */
.apex-headline {
  font-size: 28px; font-weight: 600; letter-spacing: -0.01em;
  margin: 0 0 16px; color: var(--fg);
}
.apex-sub { color: var(--muted); margin: 0 0 32px; max-width: 56ch; line-height: 1.6; }

.apex-fine { color: var(--muted); font-size: 12px; line-height: 1.6; }
.apex-fine a { color: var(--accent); }

/* Apex wallet address rows */
.wallet-address-row {
  display: flex; align-items: center; gap: 12px;
  padding: 12px 14px; background: var(--panel); border: 1px solid var(--border);
  border-radius: 6px; margin: 12px 0 16px;
}
.wallet-label {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted);
}
.wallet-address {
  font-size: 12.5px; color: var(--accent); word-break: break-all; flex: 1;
}
#import-slot:empty { display: none; }

.seed-reveal { padding: 8px 0; }
.seed-words {
  padding: 16px; margin: 8px 0;
  background: var(--bg); border: 1px solid var(--accent); border-radius: 6px;
  font-family: var(--font-mono);
  font-size: 14px; color: var(--accent); line-height: 1.8;
  word-spacing: 0.5em; user-select: all;
}
.link-button {
  background: transparent; color: var(--accent); border: 0; padding: 0;
  font: inherit; cursor: pointer; text-decoration: underline;
}
/* Seed-backup warning (GitHub #33) — monochrome, brutalist: emphasis via a
   left rule + the foreground colour, not a coloured alert box. */
.seed-warn {
  margin: 8px 0; padding: 6px 0 6px 10px;
  border-left: 2px solid var(--fg);
  color: var(--fg); font-size: 12px; line-height: 1.6;
}
.sub { color: var(--muted); margin: 0 0 24px; font-size: 12px; }

.row { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; }
input, textarea {
  background: var(--panel); color: var(--fg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 10px 12px;
  font: inherit;
  outline: none;
}
/* iOS Safari AUTO-ZOOMS the page when a focused input's font-size is under
   16px — on a phone that shifts the layout and can push the primary action
   (the CLAIM button) off-screen (QA-fleet repro). Bump inputs to 16px at
   phone widths ONLY; desktop keeps the 13px brutalist scale untouched. This
   is the FIRST media query in the sheet — it's an input-zoom fix, not a
   layout breakpoint, so it doesn't change the unified one-layout design. */
@media (max-width: 600px) {
  /* !important is REQUIRED here: classed inputs (.create-input { font:
     inherit } et al) beat this element selector on specificity, so the
     16px never applied to them and iOS still auto-zoomed on focus (the
     "inputs zoom in" iPhone report). Zoom prevention must win over every
     per-input font declaration. */
  input, textarea, select { font-size: 16px !important; }
  /* The send button is deliberately NOT grown on phones: it stays the SAME
     38px box as the header bell/cog (.terminal-action-btn) so it matches the
     header buttons and the input field on every viewport. (It used to grow to
     44px for the WCAG touch target, but that made it bigger than the 38px
     header chrome + input box — the mismatch this fixes.) */
}

/* === RENDER MODES ====================================================== */
/* The 9:16 phone frame. MOBILE-FIRST default: a desktop browser is framed by
   default (mod.rs apply_render_modes adds .preview-mobile unless ?preview=desktop
   / a saved "desktop" pref); a real <=600px phone is never framed (native layout)
   and the desktop view opts out. Frames the app as a 390px column and forces the
   <=600px mobile rules above. NO side border rules — the bare column on the page
   background reads as the phone (rawfeedback: the left/right vertical lines were
   "horrendous"). Theme tokens come from `style.rs`, so light/dark composes free. */
html.preview-mobile #root {
  width: 390px;
  max-width: 100%;
  margin: 0 auto;
}
html.preview-mobile input,
html.preview-mobile textarea,
html.preview-mobile select { font-size: 16px !important; }
/* (No send-button growth here either — it stays the 38px header-button box so
   it matches the header chrome + input field. See the @media (max-width:600px)
   note above.) */

.key-row { display: flex; gap: 6px; }
.key-row input { flex: 1; min-width: 0; }
.add-device-form { display: flex; gap: 6px; }
.add-device-form input { flex: 1; min-width: 0; }
.admin-msg-slot:empty { display: none; }
.admin-msg-slot { margin-top: 8px; font-size: 12px; }
.admin-credits-row {
  display: flex; justify-content: space-between; align-items: center;
  gap: 12px;
}

/* --- Composable subdomain: embed mode (rendered inside parent iframe) --- */
/* The card sits on a transparent background so the host can put it
   on whatever surface it wants. No margin on body — the host's
   iframe IS the bounding box. */
.embed-card {
  display: block;
  padding: 16px 18px;
  background: var(--panel);
  border: 1px solid var(--border);
  font-size: 13px;
  color: var(--fg);
  box-sizing: border-box;
}
.embed-card-header {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px; margin-bottom: 10px;
}
.embed-card-name {
  font-size: 15px; font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  letter-spacing: 0.01em;
}
.embed-card-name:hover { text-decoration: underline; }
.embed-card-badge {
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--accent);
  border: 1px solid var(--accent);
  padding: 2px 6px;
}
.embed-card-rows {
  display: flex; flex-direction: column; gap: 4px;
}
.embed-card-row {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: 12px;
}
.embed-card-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em;
  color: var(--muted);
}
.embed-card-value {
  font-size: 12px;
}
.embed-card-muted { color: var(--muted); }

/* --- Compose mode: the host shell --- */
.compose-shell {
  max-width: 1200px;
  margin: 0 auto;
  padding: 32px 24px;
}
.compose-header { margin-bottom: 20px; }
.compose-title {
  font-size: 18px; font-weight: 600; margin: 0; letter-spacing: 0.01em;
}
.compose-sub {
  font-size: 11px; color: var(--muted); margin: 4px 0 0;
  text-transform: uppercase; letter-spacing: 0.07em;
}
.compose-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
  gap: 12px;
}
.compose-cell {
  display: block;
  min-height: 80px;
  border: 1px solid var(--border);
  background: var(--panel);
}
.compose-iframe {
  width: 100%; height: 80px; border: 0; display: block; background: transparent;
}

/* --- Device pairing (admin → linked devices) --- */
.pair-slot { margin-top: 8px; display: flex; justify-content: center; }
.pair-confirm-actions { display: flex; gap: 8px; justify-content: center; margin-top: 10px; }
/* Standalone action buttons in admin sections sit centered (per design
   feedback — horizontal-center buttons like the home-screen button). */
.admin-section > button { display: block; margin: 8px auto 0; }
.admin-credits-row, #model-access-row { justify-content: center; }
.redeem-row { justify-content: center; }
.pair-slot.pair-active {
  display: flex; flex-direction: column; gap: 8px;
  padding: 12px; border: 1px solid var(--border); background: var(--panel);
}
.pair-instructions { color: var(--muted); font-size: 12px; }
.volatile-storage-warn {
  margin: 8px 0; padding: 10px 12px;
  border: 1px solid var(--error); color: var(--error);
  font-size: 12px; line-height: 1.5;
}
.volatile-storage-warn a { color: var(--error); text-decoration: underline; }
.pair-url {
  color: var(--fg); font-size: 12px; word-break: break-all;
  text-decoration: underline;
}
.pair-code-row { display: flex; align-items: baseline; gap: 8px; }
.pair-code-label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
.pair-code {
  font-size: 24px; letter-spacing: 0.25em; color: var(--fg);
  font-weight: 600;
}
.pair-waiting { color: var(--muted); font-size: 11px; }
.pair-qr {
  align-self: flex-start;
  width: 200px; height: 200px;
  padding: 8px; background: #ffffff;
  border: 1px solid var(--border);
}
.pair-qr svg {
  display: block; width: 100%; height: 100%;
  image-rendering: pixelated;
}

/* --- Invite share panel (admin → invite a friend) --- */
.invite-result-card { display: flex; flex-direction: column; gap: 8px; }
/* The QR keeps its WHITE padding/background on purpose: a phone camera
   needs the light quiet zone even when the app theme is dark. Capped at
   280px square; crispEdges so modules never anti-alias when scaled. */
.invite-qr {
  align-self: flex-start;
  width: min(280px, 100%);
  aspect-ratio: 1 / 1;
  box-sizing: border-box;
  padding: 8px; background: #ffffff;
  border: 1px solid var(--border);
}
.invite-qr svg {
  display: block; width: 100%; height: 100%;
  shape-rendering: crispEdges;
}
.invite-result-card .pair-url { overflow-wrap: anywhere; }

/* --- Post-publish share fragment (admin → public face) --- */
.share-block { display: flex; flex-direction: column; gap: 8px; }
.share-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.share-line a { color: var(--fg); }

/* --- Agent row act panel (apex agents list) --- */
.agent-row { display: block; }
/* The whole row is one anchor (templates::agents_list) — give it the
   padding + hover so the entire horizontal strip is the hit target,
   not just the name text. */
.agent-row-line {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 12px;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  border: 1px solid transparent;
}
.agent-row-line:hover { background: rgba(255, 255, 255, 0.05); border-color: var(--border); }
.agent-row-line:hover .agent-name { color: var(--accent); }
.agent-row-spacer { flex: 1; }
/* Public-landing portfolio card — name on top, a one-line persona preview
   below. Monochrome: same border/hover language as .agent-row-line, no
   radius/shadow. The preview is muted + single-line clipped. */
.agent-card {
  display: flex; flex-direction: column; gap: 3px;
  padding: 8px 12px;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  border: 1px solid transparent;
}
.agent-card:hover { background: rgba(255, 255, 255, 0.05); border-color: var(--border); }
.agent-card:hover .agent-name { color: var(--accent); }
.agent-preview {
  font-size: 11px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* === 5. FORMS + BUTTONS ================================================= */
/* --- Studio: custom system prompt --- */
.prompt-form { display: flex; flex-direction: column; gap: 8px; }
.prompt-input {
  width: 100%;
  min-height: 80px;
  font: inherit;
  resize: vertical;
  box-sizing: border-box;
}
.prompt-actions {
  display: flex; justify-content: flex-end; gap: 6px;
}
#keymeta { color: var(--muted); font-weight: 400; text-transform: none; letter-spacing: 0; margin-left: 6px; }
input:focus, textarea:focus { border-color: var(--accent); }
textarea { min-height: 72px; resize: vertical; }
.actions { display: flex; gap: 8px; align-items: center; }
/* SSOT button — every button in the app uses this archetype.
   Square, transparent, mono-uppercase. Hover lights up to --fg.
   Variants below tweak only specific overrides (danger, link),
   never the geometry. Uniform padding (same vertical + horizontal)
   so the button reads as a balanced block rather than a thin
   portrait-aspect rectangle. */
button {
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--border);
  padding: 10px 12px;
  font: inherit;
  font-size: 11px;
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  cursor: pointer;
  /* Short greyscale ease so hover/active reads as deliberate, not abrupt
     (no color — motion only). Matches the 90ms used on cards/rows below. */
  transition: color 90ms ease, border-color 90ms ease, background 90ms ease;
}
button:hover { color: var(--fg); border-color: var(--fg); }
button:disabled { opacity: 0.4; cursor: not-allowed; }

/* Keyboard focus rings. Interactive controls relied on :hover alone, so a
   keyboard/screen-reader user had NO visible focus indicator (only
   .inline-card .ic-row had one). A monochrome 1px --fg outline on
   :focus-visible (keyboard only — never on mouse click) makes focus order
   followable without touching the brutalist look.
   TEXT INPUTS ARE DELIBERATELY EXCLUDED (#64/rawfeedback "highlighted container
   inside a highlighted container"): every input/textarea ALREADY shows focus via
   a border-color change — `input:focus` (below) or the .terminal-row /
   .feedback-panel / .create-input :focus(-within) white/accent border. Adding
   this outline on top drew a SECOND highlighted box INSIDE the already-lit field
   on EVERY input (chat, feedback, apex claim). One active border per field. */
button:focus-visible,
a:focus-visible,
summary:focus-visible,
.explore-card:focus-visible,
.agent-card:focus-visible,
.agent-row-line:focus-visible {
  outline: 1px solid var(--fg);
  outline-offset: 2px;
}
/* Hover/focus motion on the navigable cards + rows (same 90ms language). */
.explore-card, .agent-card, .agent-row-line {
  transition: color 90ms ease, border-color 90ms ease, background 90ms ease;
}
/* Selected chip in a picker row (model selector). The dispatcher sets
   `ghost active` on the persisted choice — without this rule the active
   model is visually indistinguishable from the rest. Inverted, like the
   MAIN badge. */
button.ghost.active { background: var(--fg); color: var(--bg); border-color: var(--fg); }

/* `.ready` — applied by JS when an action is fully valid and the
   user should click. Used by the apex claim flow: the create
   button stays disabled while name length is wrong or the
   on-chain availability check is pending/false, and flips to
   this accent-green state once the name is confirmed available.
   Replaces the previous practice of writing "✓ available" text
   under the input — the button colour IS the feedback. */
button.ready {
  background: var(--accent);
  color: #0b0d12;
  border-color: var(--accent);
}
button.ready:hover {
  color: #0b0d12;
  border-color: var(--fg);
}

/* === 3. CHAT STREAM (cont.): transcript, turns, tool-calls, cards ======= */
.transcript {
  margin-top: 24px;
  display: flex; flex-direction: column; gap: 4px;
}
/* === TURN SPACING — ONE SSOT SYSTEM (ground-up rewrite) ===================
   Every turn body is a SQUARE box: equal padding on all four sides (one var,
   --space-3 ≈ 12px). User and agent are PIXEL-IDENTICAL in spacing; the ONLY
   per-role differences are which edge carries the 2px --fg rule, the bg-shade,
   and the user's muted text colour.

   Inter-block spacing inside a reply is the flex-column `gap` (a DELIBERATE
   space), never a stray margin on a markdown element. `.body > * { margin: 0 }`
   strips the browser-default <p>/<ul>/<h*> margins at the root, so no element
   ever pushes the content down — the box stays square and the content reads as
   vertically centred. No `> * + *` margin chains, no `:first-child` /
   `:last-child` zeroing patches, no `:has()` collapse hacks. */
.turn {
  background: transparent;
  border: 0;
  padding: 0;
  line-height: var(--line-tight);
}
/* Inter-turn space is the TRANSCRIPT's flex `gap` (EXTERNAL — between the turn
   containers), NOT margin/padding on `.turn`. A `.turn + .turn` padding-top lands
   INSIDE the bg'd bubble (a gap-sized empty band atop the agent's content, on top
   of the body's own padding), and pairing it with a margin doubled the gap. So a
   turn carries NO margin/padding of its own; its only internal space is the body's
   square padding, and turns float apart on the transcript gap. (No separator line:
   the bg-shade + alignment + accent rule already distinguish turns — a border on a
   shrink-to-fit user bubble would also be ragged.) */
.turn .role { display: none; }
/* SQUARE body: top = bottom = left = right, ONE value, role-INDEPENDENT. The
   flex column turns inter-block spacing into a single `gap`; resetting every
   child's margin (and nested text-segment children) is what makes that the
   ONLY source of vertical rhythm. */
.turn .body {
  padding: var(--space-3);
  display: flex;
  flex-direction: column;
  gap: var(--para-gap);
  word-break: break-word;
  line-height: var(--line-tight);
}
.turn .body > *,
.turn .body .text-segment,
.turn .body .text-segment > * { margin: 0; }
/* USER: a right-hugging bubble that GROWS FROM THE RIGHT (text-message style) —
   a short message is only as wide as its words; long ones expand leftward to a
   cap, then wrap. The accent rule + shade live on the BUBBLE (the body), so the
   bubble floats right against the transparent row. (Agent replies stay
   full-width, left-bordered blocks.) */
.turn.user .body {
  white-space: pre-wrap;
  color: var(--muted);
  width: fit-content;
  max-width: 80%;
  margin-left: auto;
  background: var(--panel);
  border-right: 2px solid var(--fg);
}
/* AGENT: accent rule on the LEFT edge, the bare-bg shade. The left rule PULSES
   while the turn is in flight (#57). */
.turn.assistant {
  border-left: 2px solid var(--fg);
  background: var(--panel-2);
}
/* Inline `code` in rendered markdown gets a panel bg + 1px border so
   identifiers/paths read as code in a monochrome UI. Block code opts out. */
.turn .body code { background: var(--panel); border: 1px solid var(--border); padding: 1px 4px; }
.turn .body pre code { background: none; border: 0; padding: 0; }
/* A markdown code block (<pre>) WRAPS its long lines (on-chain #81: raw JSON
   / hex overflowing horizontally on a phone read as broken CSS, not intent) —
   the same pre-wrap + break-word every other transcript pre already uses
   (tool-call pre, .ic-body, terminal lines). overflow-x:auto stays as the
   belt-and-braces containment for anything still unbreakable. */
.turn .body pre { max-width: 100%; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
/* `pre-wrap` is ONLY correct while the turn STREAMS — the segment then holds
   raw model text whose newlines must show. The instant streaming ends the
   segment becomes rendered markdown, where pulldown-cmark's trailing "\n" text
   nodes would render as phantom blank lines under pre-wrap. Scope it to
   `.streaming` so the final turn collapses those literal newlines. */
.turn.assistant.streaming .body .text-segment { white-space: pre-wrap; }
/* Empty segments (opened after a tool call but never written to) reserve no
   space — flex `gap` does NOT count a display:none child. */
.turn.assistant .body .text-segment:empty { display: none; }
/* Same trick for the tool-call CARD slot: most tools emit no inline card, so the
   slot div stays empty — but an empty flex item STILL gets a `gap` on each side,
   doubling the space under the folded pill ("too much space under tool cards",
   on-chain #60.1). Drop the empty slot from flow; when a card fills it, it
   becomes non-empty and the single flex `gap` spaces it like any other block. */
.turn .body > .tc-card-slot:empty { display: none; }
/* A PRESENT card already gets the body's flex `gap` above + below (the slot is a
   flex item) — so the inline card itself must not ALSO carry a margin, or the two
   stack and double again. The flex gap is the SSOT. */
.turn .body .tc-card-slot .inline-card { margin: 0; }

/* Inline tool-call blocks inside an assistant turn. */
.tool-call {
  margin: 8px 0;
  border: 1px solid var(--border);
  background: var(--bg);
  font-size: 12.5px;
  overflow: hidden;
}
/* The FOLDED tool card is one of the 38px control boxes — its summary row is
   EXACTLY one --ctrl-box tall (border-box), content vertically centred, the
   same horizontal inset as the input row's glyph→edge step, so it reads as a
   header/input/send button. The body only appears when [open]. */
.tool-call > summary {
  cursor: pointer;
  min-height: var(--ctrl-box);
  padding: 0 var(--space-3);
  color: var(--muted);
  list-style: none;
  display: flex;
  align-items: center;
  gap: 8px;
  user-select: none;
}
.tool-call > summary::-webkit-details-marker { display: none; }
/* Toggle marker (#61): a crisp SVG CHEVRON on the RIGHT (after the name),
   header-icon sized — not the small left triangle that read as the play glyph.
   The name's flex:1 pushes it to the right edge; it rotates 90° down when the
   JSON detail opens (collapsed by default — no `open` attr on the <details>). */
.tool-call > summary .tc-chev {
  display: inline-flex;
  color: var(--muted);
  transition: transform 80ms ease;
}
.tool-call[open] > summary .tc-chev { transform: rotate(90deg); }
.tool-call .tc-name {
  flex: 1;
  text-align: left;
  color: var(--fg);
  font-weight: 600;
}
/* #54 PHASE CUE — right after the user submits, the assistant turn's body is
   empty (one empty text-segment, hidden by the :empty rule). Paint a blinking
   phase word UNDER the user message (= in the assistant body) so the spin-up
   reads as "working", not as a blank gap, until the FIRST real output lands.
   The word is `data-stage` (set by `chat::stage::enter`), so it tracks the
   phase: starting → thinking → streaming → tools, synced with the header glyph.
   It vanishes the instant the body gains a non-empty text segment or a tool-call
   — `:has()` checks for any visible content (the cycling header glyph stays —
   BOTH, not either/or). */
.turn.assistant.streaming
  .body:not(:has(.text-segment:not(:empty))):not(:has(.tool-call))::before {
  content: attr(data-stage);
  display: inline-block;
  font-size: 12px;
  letter-spacing: 0.04em;
  /* Blink via a COLOR swell (--muted→--fg), never opacity — same principle as
     the header glyph / notif bell (opacity faded whole chips, #51). */
  color: var(--muted);
  animation: turn-starting-blink 1.2s ease-in-out infinite;
}
@keyframes turn-starting-blink {
  0%, 100% { color: var(--muted); }
  50%      { color: var(--fg); }
}
/* #57 PULSING ACCENT — the agent turn's LEFT rule (set above) swells in colour
   while the turn is in flight: from the moment the user sends, through the
   cycling header glyph, until the turn finishes (the `streaming` class is
   removed by `mark_turn_done`). COLOR swell only (border-left-color), NOT
   opacity — the SAME technique as turn-status-pulse / notif-bell-pulse so the
   bg + content stay solid (animating opacity faded whole chips, #51). */
.turn.assistant.streaming {
  animation: turn-border-pulse 1.4s ease-in-out infinite;
}
@keyframes turn-border-pulse {
  0%, 100% { border-left-color: var(--muted); }
  50%      { border-left-color: #fff; }
}
/* (The braille trailing `::after` spinner is retired — the #54 starting line,
   the #57 pulsing border, and the cycling header glyph are the working cues.) */
.tool-call .tc-body {
  padding: 8px 10px 10px;
  border-top: 1px solid var(--border);
  display: grid;
  gap: 8px;
}
.tool-call .tc-section-label {
  font-size: 10px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.tool-call pre {
  margin: 0;
  padding: 6px 8px;
  background: var(--panel);
  border: 1px solid var(--border);
  overflow-x: auto;
  font-size: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}
.tool-call .tc-error pre {
  border-color: var(--error);
  color: var(--error);
}

/* Inline result cards under a tool pill (file / directory / display
   outputs) — compact, bordered, height-capped; the transcript shows what
   a tool produced without tab-hopping. Monochrome, no highlighting. */
.inline-card {
  margin: 6px 0 8px;
  border: 1px solid var(--border);
  background: var(--bg);
  font-size: 12px;
  max-height: 320px;
  overflow-y: auto;
}
.inline-card .ic-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 4px 8px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
}
/* Sticky header ONLY inside cards that scroll THEMSELVES (max-height +
   overflow-y above). .embed-app-card sets overflow:visible, so a sticky head
   there latches onto the OUTER chat scrollport and floats over the transcript
   (telemetry #30). */
.inline-card:not(.embed-app-card) .ic-head {
  position: sticky;
  top: 0;
}
.inline-card .ic-title {
  color: var(--fg);
  font-weight: 600;
  overflow-wrap: anywhere;
}
.inline-card .ic-meta { color: var(--muted); font-size: 11px; }
.inline-card .ic-head button {
  margin-left: auto;
  font-size: 11px;
  padding: 1px 8px;
}
.inline-card .ic-body {
  margin: 0;
  padding: 6px 8px;
  background: var(--bg);
  border: 0;
  overflow-x: auto;
  font-size: 12px;
  white-space: pre-wrap;
  word-break: break-word;
}
.inline-card .ic-more {
  padding: 3px 8px;
  color: var(--muted);
  font-size: 11px;
  border-top: 1px solid var(--border);
}
.inline-card .ic-rows { display: grid; }
.inline-card .ic-row {
  padding: 2px 8px;
  cursor: pointer;
}
.inline-card .ic-row:hover, .inline-card .ic-row:focus-visible {
  background: var(--panel);
}
.inline-card .ic-thumb {
  display: block;
  width: 256px;
  max-width: calc(100% - 16px);
  image-rendering: pixelated;
  margin: 8px;
  border: 1px solid var(--border);
}

/* Live inline cartridge embed (`embed_app` tool). The stage spans the card and
   CENTERS the canvas; the canvas keeps the cartridge's OWN aspect ratio (its
   dims() — 512x512 default, or a portrait app) via max-width/max-height ONLY — no
   fixed aspect, no object-fit — so a square/portrait cartridge is no longer
   letterboxed into a 4:3 box (telemetry #30; the old box matched the obsolete
   320x240 default). The canvas element box stays equal to the painted pixels, so
   `display::set_pointer`'s rect-based touch map lands on the right framebuffer
   pixel. v1: one live embed at a time (single worker). */
.embed-app-card { overflow: visible; }
.embed-app-stage {
  width: 100%;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
}
.embed-app-canvas {
  max-width: 100%;
  /* Cap HEIGHT directly (not via the indefinite-height stage, where a % max-height
     resolves to none) so a tall/portrait cartridge stays compact inline; only max
     constraints + no object-fit = the canvas scales DOWN preserving its aspect. */
  max-height: 70vh;
  display: block;
  image-rendering: pixelated;
  touch-action: none;
}

.status { color: var(--muted); font-size: 11px; margin-top: 10px; min-height: 16px; }
.err { color: var(--error); }

footer { color: var(--muted); font-size: 11px; margin-top: 48px; line-height: 1.6; }
footer a { color: var(--accent); text-decoration: none; }
footer a:hover { text-decoration: underline; }

/* === 4. OVERLAYS (cont.): admin modal/tabs, files modal, api-key ======== */
/* Header admin dropdown — sits at top-right, click to expand.
   .header-admin { position: relative } is declared once near the top. */
/* The admin button is now an icon (settings gear), so it opts OUT of the
   .header-button 96px text min-width and squares up like the bell. */
.admin-button {
  min-width: 0;
  padding: 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* Solid fill — see .notif-bell-btn (legible over the floating chat header). */
  background: var(--bg);
}
.admin-button svg { display: block; width: 16px; height: 16px; }
/* The admin (cog) panel is a DROPDOWN (#36), anchored under the cog like the
   notif bell / brand menu — NOT a fullscreen centered modal. `.header-admin`
   is position:relative, so this absolute panel hangs off it; the uniform
   --chrome-pad gap matches the other header dropdowns. */
.header-admin-panel {
  position: absolute;
  top: 100%;
  right: 0;
  margin-top: var(--chrome-pad);
  z-index: var(--z-menu);
}
.admin-dialog {
  width: 360px;
  max-width: 90vw;
  max-height: 80vh;
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--border);
  padding: 20px;
}

/* === UNIFIED HEADER MODALS (notif · feedback · admin) ======================
   ONE treatment for all three header overlays (they were three separate
   ad-hoc dropdowns — different z-layers, anchored under their own top-right
   trigger so the notif panel ran off-screen-left, and able to stack on top of
   each other). Now: CENTERED on the viewport (not offset by the button that
   opened it), one z-layer above the header, clamped so it never overflows.
   Mutual exclusion (only one open at a time) is enforced in events::admin —
   each open path closes the others first. They stay DOM children of their
   header wraps so the existing click-outside check still works; position:fixed
   only relocates them visually. Placed AFTER the per-panel blocks so it wins by
   source order at equal specificity. */
.notif-panel,
.feedback-panel,
.admin-dialog {
  position: fixed;
  top: 50%;
  left: 50%;
  right: auto;
  transform: translate(-50%, -50%);
  margin: 0;
  width: min(360px, calc(100vw - 2 * var(--chrome-pad)));
  max-width: none;
  max-height: calc(100dvh - 6 * var(--chrome-pad));
  overflow-y: auto;
  z-index: var(--z-menu);
}
/* Settings sheet (#36 redesign) — a chromeless disclosure panel that
   generalizes from the notif / feedback dropdowns: NO title bar, NO × close
   (dismiss via outside-click · ESC · a second cog tap). It inherits the base
   `.admin-dialog` box + the UNIFIED HEADER MODALS centered-fixed rule; only the
   padding is tightened. Identity + the $LH balance sit at rest; every other
   concern collapses into a native `<details>` group, so the panel opens small
   instead of the old long scroll. */
.admin-dialog.admin-sheet { padding: 12px 16px; }
/* Native-<details> group: browser-driven disclosure (no Action/handler, no
   Closure); the collapsed body stays in the DOM so prefill ids keep resolving. */
.admin-group { border-top: 1px solid var(--border); }
.admin-group > summary.admin-group-summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 0;
}
.admin-group > summary.admin-group-summary::-webkit-details-marker { display: none; }
/* The +/− twist on the left; a fixed-width cell so the labels line up. */
.admin-group-twist {
  width: 0.85em;
  color: var(--muted);
  text-align: center;
  font-variant-numeric: tabular-nums;
}
.admin-group-twist::after { content: "+"; }
.admin-group[open] > summary .admin-group-twist::after { content: "\2212"; }
.admin-group-label {
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.admin-group-body { padding: 2px 0 10px; }
/* The nested `advanced` sub-group is indented under `agent`. */
.admin-group .admin-group {
  border-top: 0;
  margin-left: 12px;
  padding-left: 10px;
  border-left: 1px solid var(--border);
}
/* The resting $LH line aligns its number like the other identity rows. */
.admin-balance-row code { font-variant-numeric: tabular-nums; }
/* Chat-native admin cards (#36) — the settings sheet's sections rendered
   inline in a transcript turn. The section styles below are global (not scoped
   to .admin-dialog), so they apply as-is; the wrapper is a bordered card. */
.admin-chat-card { border: 1px solid var(--border); padding: 10px 12px; margin: 4px 0; }
.admin-chat-card-title {
  font-size: 10px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 8px;
}
.admin-chat-card-retired {
  border: 1px dashed var(--border);
  color: var(--muted);
  font-size: 12px;
  padding: 6px 10px;
  margin: 4px 0;
}
.admin-section { margin-bottom: 14px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
.admin-section:last-of-type { border-bottom: 0; padding-bottom: 0; margin-bottom: 8px; }
.admin-section-title {
  font-size: 10px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  margin-bottom: 8px;
}
.admin-blurb { margin: 0 0 8px; color: var(--muted); font-size: 12px; line-height: 1.5; }

.admin-footer {
  display: flex; justify-content: space-between; align-items: center;
  gap: 8px; padding-top: 10px; margin-top: 6px;
  border-top: 1px solid var(--border);
}
.admin-version {
  font-size: 10px; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.06em;
}

/* Identity rows — same shape on every admin dropdown:
   [label]  [short-address link]. Same archetype the agent
   tab uses in the financial card; mirroring it inside admin
   so the user sees what identity is active without digging. */
.admin-identity-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  padding: 4px 0;
  font-size: 12px;
}
.admin-identity-label {
  color: var(--muted);
  text-transform: uppercase;
  font-size: 10px;
  letter-spacing: 0.08em;
}
.admin-identity-value {
  color: var(--fg);
  font-family: var(--font-mono);
  font-size: 12px;
  text-decoration: none;
}
a.admin-identity-value:hover { color: var(--accent); }

/* Subsections inside the [security] expanded view — tighter
   than top-level sections so the hierarchy reads as one
   collapsible group instead of three independent rows. */
.admin-subsection {
  margin-bottom: 10px;
  padding-left: 12px;
  border-left: 1px solid var(--border);
}
.admin-subsection-title {
  font-size: 10px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-bottom: 6px;
}
/* Danger variant only changes the colour — geometry stays the
   global button style. */
button.danger { color: var(--error); border-color: var(--error); }
button.danger:hover { color: var(--bg); background: var(--error); }
.reset-confirm { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.reset-confirm-prompt { color: var(--error); font-size: 12px; }
.reset-confirm-actions { display: flex; gap: 6px; }
.opfs-wipe-confirm { display: inline-flex; gap: 4px; }

/* === 8. APEX ONBOARDING (cont.): step flow, seed/create, unclaimed ====== */
/* Step-based apex onboarding */
.step { margin: 48px 0 0; min-height: 200px; }
.step-agents { margin-top: 48px; padding: 0 24px; }
/* Authed apex shares the fresh front door's centered layout (`.apex-front`):
   no top margin (so the cluster centers, not pushed down), a centered column
   with the brand 16px gap between the agents list, the shared claim form, and
   the fund slot. */
.apex-front .step-agents {
  margin-top: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--chrome-pad);
}

/* identity-actions inherits the global button style */
.step-msg { font-size: 12px; min-height: 16px; margin-top: 12px; color: var(--muted); }
.seed-import { display: grid; gap: 8px; margin-top: 8px; }
.seed-import textarea {
  min-height: 64px; background: var(--bg);
  border: 1px solid var(--border); border-radius: 6px;
  padding: 10px 12px; font: inherit; color: var(--fg);
}
.seed-import-actions { display: flex; gap: 8px; }

/* Create-agent form (apex_step_agents) — input + button stacked,
   horizontally centered, equal padding above and below. Wide CTA
   button. */
.create-form {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 24px;          /* identical space above + below the input */
  max-width: 480px;
  margin: 64px auto 0;
}
.create-input {
  width: 100%;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--fg);
  font: inherit;
  padding: 12px 16px;
  outline: none;
  text-align: center;
}
.create-input:focus { border-color: var(--accent); }
.create-button {
  min-width: 200px;
  padding: 12px 32px;
}
.create-button.failed {
  color: var(--error);
  border-color: var(--error);
}

/* Unclaimed (fresh subdomain visited by someone who doesn't own it) */
.step-unclaimed { text-align: center; }
.unclaimed-name {
  font-size: 18px; font-weight: 600; color: var(--accent);
  margin: 0 0 16px; word-break: break-all;
}

/* Pricing card — payment gate config, lives above the OPFS panel */
.pricing-card {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 14px 16px;
  margin-bottom: 16px;
}
.pricing-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 10px;
}
.pricing-title {
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.pricing-value {
  color: var(--fg);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  margin-bottom: 10px;
}
.pricing-edit {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-wrap: wrap;
}
.pricing-edit input {
  flex: 1; min-width: 80px;
  padding: 6px 8px;
  font-size: 12px;
  background: var(--bg);
}
.pricing-unit { color: var(--muted); font-size: 11px; }
.pricing-edit button {
  padding: 6px 12px; font-size: 12px;
}
.pricing-msg { font-size: 11px; margin-top: 6px; min-height: 14px; }

/* Files modal — the OPFS browser as an overlay off the header [files]
   button. Same overlay/dialog machinery as the admin modal; the body is
   a column of breadcrumb / list / editor viewer. */
.files-modal {
  position: fixed; inset: 0;
  z-index: var(--z-modal);
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.files-dialog {
  width: 100%;
  max-width: 640px;
  height: 85vh;
  max-height: 85vh;
  display: flex;
  flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--border);
  padding: 0;
}
.files-head {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 8px 8px 20px;
  border-bottom: 1px solid var(--border);
}
.files-title {
  font-size: 10px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.files-head > .modal-close { border: 0; }
.files-body {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 20px 16px;
}
.files-body .fs-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
/* The editor viewer shares the dialog with the list when a file is
   open; empty (closed) it takes no space at all. */
.fs-viewer:empty { display: none; }
.fs-viewer {
  flex: 2 1 60%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--border);
}

/* File-browser internals — breadcrumb + list (inside the files
   modal's body). */
.fs-breadcrumb {
  font-size: 11px;
  color: var(--muted);
  margin-bottom: 6px;
  word-break: break-all;
}
.fs-breadcrumb a {
  color: var(--accent);
  cursor: pointer;
  text-decoration: none;
}
.fs-breadcrumb a:hover { text-decoration: underline; }
.fs-list {
  flex: 1;
  margin: 0;
  padding: 0;
  list-style: none;
}
.fs-list li {
  padding: 6px 4px;
  border-bottom: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}
.fs-list li.dir { cursor: pointer; }
.fs-list li .name {
  cursor: pointer;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.fs-list li:hover .name, .fs-list li:hover .size { color: var(--accent); }
.file-delete {
  background: transparent;
  color: var(--muted);
  border: 0;
  padding: 0 6px;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  visibility: hidden;
}
.fs-list li:hover .file-delete { visibility: visible; }
.file-delete:hover { color: var(--error); }
.file-edit {
  background: transparent;
  color: var(--muted);
  border: 0;
  padding: 0 6px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  line-height: 1;
  cursor: pointer;
  visibility: hidden;
}
.fs-list li:hover .file-edit { visibility: visible; }
.file-edit:hover { color: var(--fg); }
.fs-list .name { color: var(--fg); }
.fs-list .dir .name::before { content: "▸ "; color: var(--accent); }
.fs-list .size {
  color: var(--muted);
  font-size: 11px;
  flex: 0 0 auto;
  white-space: nowrap;
  text-align: right;
}
.fs-list .empty {
  color: var(--muted);
  font-style: italic;
  padding: 12px 4px;
  cursor: default;
}
.fs-list .empty:hover { background: transparent; }

/* Tool allowlist — compact grid */
.tool-allowlist-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px 12px;
  margin: 8px 0;
}
.tool-checkbox-label {
  font-size: 11px;
  color: var(--fg);
  cursor: pointer;
  padding: 2px 0;
  display: flex;
  align-items: center;
  gap: 4px;
}

/* API key modal — centered overlay */
.api-key-modal {
  position: fixed; inset: 0;
  z-index: var(--z-api-key);
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
}
.api-key-card {
  width: 100%;
  max-width: 420px;
  background: var(--panel);
  border: 1px solid var(--border);
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.api-key-title {
  font-size: 11px;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.api-key-row {
  display: flex;
  gap: 8px;
}
.api-key-row input {
  flex: 1;
  min-width: 0;
}
.api-key-hint { font-size: 11px; }
.api-key-hint a { color: var(--accent); text-decoration: none; }
.api-key-hint a:hover { text-decoration: underline; }

