This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/styles.css
T
Miguel PalhasandClaude Opus 4.6 638155de03 feat(maestro): move activity indicator into scroll area
The "Running 1m ..." status now sits at the tail of the chat
history instead of pinned above the input, matching Claude Code's
placement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-08 13:45:56 +01:00

4662 lines
104 KiB
CSS

/* =========================================================================
maestro web — design system
Register: product (a tool; design serves the task).
Identity: precise, calm, alive. Monospace-as-UI; graphite chrome where the
activity colours (green = alive) are the only hue on screen.
Theme: dark (default) + light, all via OKLCH custom-property tokens.
Palette: `data-palette` on <html> swaps the accent + base tint (see the
"palettes" section at the end of the token area); graphite is the bare
document.
========================================================================= */
:root {
/* ---- type ---- */
--font-mono:
"JetBrains Mono", ui-monospace, "SF Mono", "SFMono-Regular", "Cascadia Code",
"Roboto Mono", Menlo, Consolas, "Liberation Mono", monospace;
--font-sans:
system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
--fs-11: 0.6875rem;
--fs-12: 0.75rem;
--fs-13: 0.8125rem;
--fs-14: 0.875rem;
--fs-15: 0.9375rem;
--fs-17: 1.0625rem;
--fs-20: 1.25rem;
/* ---- spacing ---- */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 20px;
--sp-6: 24px;
--sp-8: 32px;
/* ---- radii ----
Terminal-native: crisp, near-square corners. Round dots (status, brand)
use border-radius:50% directly and are unaffected; these tokens govern
rectangles (buttons, inputs, cards, tags) and stay sharp on purpose. */
--r-1: 2px;
--r-2: 3px;
--r-3: 4px;
--r-pill: 3px;
/* ---- sizing ---- */
--row-h: 46px;
--control-h: 30px;
--tap: 40px;
/* Height of the two top-of-pane bars (sidebar search + attach header) — shared
so they line up across the split. */
--bar-h: 34px;
/* ---- motion ---- */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--dur-fast: 120ms;
--dur: 180ms;
--dur-slow: 240ms;
/* ---- z-index scale ---- */
--z-sticky: 10;
--z-backdrop: 40;
--z-modal: 50;
--z-toast: 60;
/* ---- color: dark (default) ---- */
color-scheme: dark;
--bg: oklch(0.172 0.006 256);
--bg-grid: oklch(0.21 0.007 256);
--surface: oklch(0.208 0.007 256);
--surface-2: oklch(0.246 0.008 256);
--surface-3: oklch(0.286 0.009 256);
--border: oklch(0.312 0.01 256);
--border-strong: oklch(0.395 0.012 256);
--ink: oklch(0.945 0.004 256);
--muted: oklch(0.72 0.008 256);
--faint: oklch(0.58 0.008 256);
/* graphite: the accent is ink itself — primary action, focus, selection.
The activity colours below stay the only hue on screen. */
--accent: oklch(0.92 0.005 256);
--accent-strong: oklch(0.97 0.003 256);
--accent-ink: oklch(0.2 0.012 256);
--accent-tint: oklch(0.92 0.005 256 / 0.1);
--accent-ring: oklch(0.92 0.005 256 / 0.5);
/* preview (docs/PREVIEW.md): a provisional view needs a colour that is not
the accent, because the accent is what says "this is the one you picked".
Defined once, off a single hue variable, so a palette whose accent already
sits on that hue can move it (see periwinkle/orchid below); `light-dark()`
keeps the light pair here instead of in all eighteen theme blocks. */
--preview-h: 305;
--preview: light-dark(
oklch(0.55 0.16 var(--preview-h)),
oklch(0.78 0.14 var(--preview-h))
);
--preview-ink: light-dark(
oklch(0.99 0.005 var(--preview-h)),
oklch(0.19 0.045 var(--preview-h))
);
/* Opaque, not a wash: session rows read as fills, and a fill has to land on
the same colour whatever tier the row was already painted. `--accent-fill`
is the same recipe on the accent, so the two tiers differ by hue alone. */
--preview-tint: color-mix(in oklab, var(--preview) 30%, var(--surface-2));
--accent-fill: color-mix(in oklab, var(--accent) 30%, var(--surface-2));
--preview-ring: light-dark(
oklch(0.55 0.16 var(--preview-h) / 0.5),
oklch(0.78 0.14 var(--preview-h) / 0.6)
);
--live: oklch(0.84 0.17 152);
--live-glow: oklch(0.84 0.17 152 / 0.55);
--danger: oklch(0.72 0.18 25);
--danger-tint: oklch(0.72 0.18 25 / 0.14);
--warn: oklch(0.82 0.14 85);
--shadow-1: 0 1px 2px oklch(0 0 0 / 0.4);
--shadow-2:
0 12px 28px -8px oklch(0 0 0 / 0.6), 0 2px 8px oklch(0 0 0 / 0.4);
--term-bg: oklch(0.155 0.006 256);
}
:root[data-theme="light"],
:root[data-theme="light"] body {
color-scheme: light;
--bg: oklch(0.985 0.002 256);
--bg-grid: oklch(0.93 0.004 256);
--surface: oklch(1 0 0);
--surface-2: oklch(0.965 0.003 256);
--surface-3: oklch(0.93 0.004 256);
--border: oklch(0.892 0.005 256);
--border-strong: oklch(0.82 0.008 256);
--ink: oklch(0.26 0.012 256);
--muted: oklch(0.47 0.012 256);
--faint: oklch(0.62 0.012 256);
--accent: oklch(0.27 0.01 256);
--accent-strong: oklch(0.2 0.01 256);
--accent-ink: oklch(0.985 0.002 256);
--accent-tint: oklch(0.27 0.01 256 / 0.1);
--accent-ring: oklch(0.27 0.01 256 / 0.45);
--live: oklch(0.6 0.16 151);
--live-glow: oklch(0.6 0.16 151 / 0.45);
--danger: oklch(0.55 0.21 27);
--danger-tint: oklch(0.55 0.21 27 / 0.1);
--warn: oklch(0.66 0.15 75);
--shadow-1: 0 1px 2px oklch(0.5 0.02 256 / 0.12);
--shadow-2:
0 16px 36px -12px oklch(0.4 0.03 256 / 0.28),
0 2px 8px oklch(0.5 0.02 256 / 0.1);
--term-bg: oklch(0.165 0.012 256);
}
/* Follow the OS when the user hasn't pinned a theme. */
@media (prefers-color-scheme: light) {
:root:not([data-theme]) {
color-scheme: light;
--bg: oklch(0.985 0.002 256);
--bg-grid: oklch(0.93 0.004 256);
--surface: oklch(1 0 0);
--surface-2: oklch(0.965 0.003 256);
--surface-3: oklch(0.93 0.004 256);
--border: oklch(0.892 0.005 256);
--border-strong: oklch(0.82 0.008 256);
--ink: oklch(0.26 0.012 256);
--muted: oklch(0.47 0.012 256);
--faint: oklch(0.62 0.012 256);
--accent: oklch(0.27 0.01 256);
--accent-strong: oklch(0.2 0.01 256);
--accent-ink: oklch(0.985 0.002 256);
--accent-tint: oklch(0.27 0.01 256 / 0.1);
--accent-ring: oklch(0.27 0.01 256 / 0.45);
--live: oklch(0.6 0.16 151);
--live-glow: oklch(0.6 0.16 151 / 0.45);
--danger: oklch(0.55 0.21 27);
--danger-tint: oklch(0.55 0.21 27 / 0.1);
--warn: oklch(0.66 0.15 75);
--shadow-1: 0 1px 2px oklch(0.5 0.02 256 / 0.12);
--shadow-2:
0 16px 36px -12px oklch(0.4 0.03 256 / 0.28),
0 2px 8px oklch(0.5 0.02 256 / 0.1);
--term-bg: oklch(0.165 0.012 256);
}
}
/* ---- palettes ----
`data-palette` on <html> recolours the accent + base tint on top of the
light/dark machinery above. Graphite (the default) is the bare document.
Each palette ships a dark block, a pinned-light block, and a system-light
block; `--live`/`--danger`/`--warn` stay semantic and are not redefined
(except amber-phosphor, whose accent would collide with the stock warn). */
/* ice-cyan: signal-cyan accent on the slate base. */
:root[data-palette="ice-cyan"] {
--bg: oklch(0.178 0.012 256);
--bg-grid: oklch(0.215 0.014 256);
--surface: oklch(0.214 0.014 256);
--surface-2: oklch(0.252 0.016 256);
--surface-3: oklch(0.292 0.018 256);
--border: oklch(0.318 0.018 256);
--border-strong: oklch(0.4 0.022 256);
--ink: oklch(0.945 0.006 256);
--muted: oklch(0.722 0.014 256);
--faint: oklch(0.58 0.014 256);
--accent: oklch(0.85 0.115 205);
--accent-strong: oklch(0.89 0.12 205);
--accent-ink: oklch(0.24 0.03 230);
--accent-tint: oklch(0.85 0.115 205 / 0.12);
--accent-ring: oklch(0.85 0.115 205 / 0.55);
--term-bg: oklch(0.158 0.012 256);
}
:root[data-palette="ice-cyan"][data-theme="light"],
:root[data-palette="ice-cyan"][data-theme="light"] body {
--bg: oklch(0.985 0.003 256);
--bg-grid: oklch(0.93 0.006 256);
--surface: oklch(1 0 0);
--surface-2: oklch(0.965 0.004 256);
--surface-3: oklch(0.93 0.006 256);
--border: oklch(0.892 0.008 256);
--border-strong: oklch(0.82 0.012 256);
--ink: oklch(0.26 0.014 256);
--muted: oklch(0.47 0.016 256);
--faint: oklch(0.62 0.016 256);
--accent: oklch(0.52 0.1 220);
--accent-strong: oklch(0.47 0.11 220);
--accent-ink: oklch(0.99 0.005 220);
--accent-tint: oklch(0.52 0.1 220 / 0.12);
--accent-ring: oklch(0.52 0.1 220 / 0.5);
--term-bg: oklch(0.165 0.012 256);
}
@media (prefers-color-scheme: light) {
:root[data-palette="ice-cyan"]:not([data-theme]) {
--bg: oklch(0.985 0.003 256);
--bg-grid: oklch(0.93 0.006 256);
--surface: oklch(1 0 0);
--surface-2: oklch(0.965 0.004 256);
--surface-3: oklch(0.93 0.006 256);
--border: oklch(0.892 0.008 256);
--border-strong: oklch(0.82 0.012 256);
--ink: oklch(0.26 0.014 256);
--muted: oklch(0.47 0.016 256);
--faint: oklch(0.62 0.016 256);
--accent: oklch(0.52 0.1 220);
--accent-strong: oklch(0.47 0.11 220);
--accent-ink: oklch(0.99 0.005 220);
--accent-tint: oklch(0.52 0.1 220 / 0.12);
--accent-ring: oklch(0.52 0.1 220 / 0.5);
--term-bg: oklch(0.165 0.012 256);
}
}
/* periwinkle: indigo base, blue-violet accent. */
:root[data-palette="periwinkle"] {
--bg: oklch(0.175 0.018 278);
--bg-grid: oklch(0.212 0.021 278);
--surface: oklch(0.212 0.021 278);
--surface-2: oklch(0.25 0.024 278);
--surface-3: oklch(0.29 0.026 278);
--border: oklch(0.318 0.026 278);
--border-strong: oklch(0.4 0.028 278);
--ink: oklch(0.93 0.012 278);
--muted: oklch(0.71 0.02 278);
--faint: oklch(0.57 0.022 278);
--accent: oklch(0.75 0.125 275);
--accent-strong: oklch(0.79 0.13 275);
--accent-ink: oklch(0.18 0.04 278);
--accent-tint: oklch(0.75 0.125 275 / 0.14);
--accent-ring: oklch(0.75 0.125 275 / 0.55);
/* This accent is already violet, so preview moves to cyan to stay legible
as "not the accent". Set once here: the light blocks below don't touch it. */
--preview-h: 195;
--term-bg: oklch(0.155 0.018 278);
}
:root[data-palette="periwinkle"][data-theme="light"],
:root[data-palette="periwinkle"][data-theme="light"] body {
--bg: oklch(0.982 0.004 278);
--bg-grid: oklch(0.93 0.008 278);
--surface: oklch(1 0 0);
--surface-2: oklch(0.96 0.006 278);
--surface-3: oklch(0.93 0.008 278);
--border: oklch(0.885 0.01 278);
--border-strong: oklch(0.82 0.014 278);
--ink: oklch(0.27 0.02 278);
--muted: oklch(0.47 0.02 278);
--faint: oklch(0.62 0.02 278);
--accent: oklch(0.5 0.17 272);
--accent-strong: oklch(0.45 0.17 272);
--accent-ink: oklch(0.99 0.005 272);
--accent-tint: oklch(0.5 0.17 272 / 0.12);
--accent-ring: oklch(0.5 0.17 272 / 0.5);
--term-bg: oklch(0.165 0.018 278);
}
@media (prefers-color-scheme: light) {
:root[data-palette="periwinkle"]:not([data-theme]) {
--bg: oklch(0.982 0.004 278);
--bg-grid: oklch(0.93 0.008 278);
--surface: oklch(1 0 0);
--surface-2: oklch(0.96 0.006 278);
--surface-3: oklch(0.93 0.008 278);
--border: oklch(0.885 0.01 278);
--border-strong: oklch(0.82 0.014 278);
--ink: oklch(0.27 0.02 278);
--muted: oklch(0.47 0.02 278);
--faint: oklch(0.62 0.02 278);
--accent: oklch(0.5 0.17 272);
--accent-strong: oklch(0.45 0.17 272);
--accent-ink: oklch(0.99 0.005 272);
--accent-tint: oklch(0.5 0.17 272 / 0.12);
--accent-ring: oklch(0.5 0.17 272 / 0.5);
--term-bg: oklch(0.165 0.018 278);
}
}
/* amber-phosphor: CRT amber on warm graphite. `--warn` shifts orange so the
awaiting colour stays distinct from the accent. */
:root[data-palette="amber-phosphor"] {
--bg: oklch(0.178 0.008 70);
--bg-grid: oklch(0.214 0.01 70);
--surface: oklch(0.214 0.01 70);
--surface-2: oklch(0.252 0.012 70);
--surface-3: oklch(0.292 0.013 70);
--border: oklch(0.318 0.014 70);
--border-strong: oklch(0.4 0.016 70);
--ink: oklch(0.94 0.008 78);
--muted: oklch(0.72 0.014 75);
--faint: oklch(0.58 0.014 75);
--accent: oklch(0.83 0.14 78);
--accent-strong: oklch(0.87 0.145 78);
--accent-ink: oklch(0.25 0.05 78);
--accent-tint: oklch(0.83 0.14 78 / 0.13);
--accent-ring: oklch(0.83 0.14 78 / 0.55);
--warn: oklch(0.76 0.15 55);
--term-bg: oklch(0.158 0.008 70);
}
:root[data-palette="amber-phosphor"][data-theme="light"],
:root[data-palette="amber-phosphor"][data-theme="light"] body {
--bg: oklch(0.983 0.004 78);
--bg-grid: oklch(0.93 0.007 78);
--surface: oklch(1 0 0);
--surface-2: oklch(0.962 0.005 78);
--surface-3: oklch(0.93 0.007 78);
--border: oklch(0.888 0.01 78);
--border-strong: oklch(0.82 0.012 78);
--ink: oklch(0.27 0.015 70);
--muted: oklch(0.47 0.016 70);
--faint: oklch(0.62 0.016 70);
--accent: oklch(0.6 0.13 70);
--accent-strong: oklch(0.55 0.13 70);
--accent-ink: oklch(0.99 0.01 78);
--accent-tint: oklch(0.6 0.13 70 / 0.12);
--accent-ring: oklch(0.6 0.13 70 / 0.5);
--warn: oklch(0.58 0.14 50);
--term-bg: oklch(0.165 0.008 70);
}
@media (prefers-color-scheme: light) {
:root[data-palette="amber-phosphor"]:not([data-theme]) {
--bg: oklch(0.983 0.004 78);
--bg-grid: oklch(0.93 0.007 78);
--surface: oklch(1 0 0);
--surface-2: oklch(0.962 0.005 78);
--surface-3: oklch(0.93 0.007 78);
--border: oklch(0.888 0.01 78);
--border-strong: oklch(0.82 0.012 78);
--ink: oklch(0.27 0.015 70);
--muted: oklch(0.47 0.016 70);
--faint: oklch(0.62 0.016 70);
--accent: oklch(0.6 0.13 70);
--accent-strong: oklch(0.55 0.13 70);
--accent-ink: oklch(0.99 0.01 78);
--accent-tint: oklch(0.6 0.13 70 / 0.12);
--accent-ring: oklch(0.6 0.13 70 / 0.5);
--warn: oklch(0.58 0.14 50);
--term-bg: oklch(0.165 0.008 70);
}
}
/* ember: warm orange-red accent. */
:root[data-palette="ember"] {
--bg: oklch(0.18 0.009 50);
--bg-grid: oklch(0.216 0.011 50);
--surface: oklch(0.216 0.011 50);
--surface-2: oklch(0.254 0.013 50);
--surface-3: oklch(0.294 0.014 50);
--border: oklch(0.32 0.015 50);
--border-strong: oklch(0.4 0.017 50);
--ink: oklch(0.94 0.007 50);
--muted: oklch(0.72 0.013 50);
--faint: oklch(0.58 0.013 50);
--accent: oklch(0.74 0.15 45);
--accent-strong: oklch(0.78 0.155 45);
--accent-ink: oklch(0.2 0.04 45);
--accent-tint: oklch(0.74 0.15 45 / 0.14);
--accent-ring: oklch(0.74 0.15 45 / 0.55);
--term-bg: oklch(0.158 0.009 50);
}
:root[data-palette="ember"][data-theme="light"],
:root[data-palette="ember"][data-theme="light"] body {
--bg: oklch(0.983 0.004 50);
--bg-grid: oklch(0.93 0.006 50);
--surface: oklch(1 0 0);
--surface-2: oklch(0.962 0.005 50);
--surface-3: oklch(0.93 0.006 50);
--border: oklch(0.888 0.009 50);
--border-strong: oklch(0.82 0.012 50);
--ink: oklch(0.27 0.014 50);
--muted: oklch(0.47 0.015 50);
--faint: oklch(0.62 0.015 50);
--accent: oklch(0.58 0.17 40);
--accent-strong: oklch(0.53 0.17 40);
--accent-ink: oklch(0.99 0.008 40);
--accent-tint: oklch(0.58 0.17 40 / 0.11);
--accent-ring: oklch(0.58 0.17 40 / 0.5);
--term-bg: oklch(0.165 0.009 50);
}
@media (prefers-color-scheme: light) {
:root[data-palette="ember"]:not([data-theme]) {
--bg: oklch(0.983 0.004 50);
--bg-grid: oklch(0.93 0.006 50);
--surface: oklch(1 0 0);
--surface-2: oklch(0.962 0.005 50);
--surface-3: oklch(0.93 0.006 50);
--border: oklch(0.888 0.009 50);
--border-strong: oklch(0.82 0.012 50);
--ink: oklch(0.27 0.014 50);
--muted: oklch(0.47 0.015 50);
--faint: oklch(0.62 0.015 50);
--accent: oklch(0.58 0.17 40);
--accent-strong: oklch(0.53 0.17 40);
--accent-ink: oklch(0.99 0.008 40);
--accent-tint: oklch(0.58 0.17 40 / 0.11);
--accent-ring: oklch(0.58 0.17 40 / 0.5);
--term-bg: oklch(0.165 0.009 50);
}
}
/* orchid: muted violet accent on a faintly violet base. */
:root[data-palette="orchid"] {
--bg: oklch(0.177 0.014 305);
--bg-grid: oklch(0.213 0.017 305);
--surface: oklch(0.213 0.017 305);
--surface-2: oklch(0.251 0.02 305);
--surface-3: oklch(0.291 0.021 305);
--border: oklch(0.318 0.022 305);
--border-strong: oklch(0.4 0.024 305);
--ink: oklch(0.935 0.01 305);
--muted: oklch(0.715 0.018 305);
--faint: oklch(0.575 0.018 305);
--accent: oklch(0.78 0.13 310);
--accent-strong: oklch(0.82 0.135 310);
--accent-ink: oklch(0.22 0.05 310);
--accent-tint: oklch(0.78 0.13 310 / 0.14);
--accent-ring: oklch(0.78 0.13 310 / 0.55);
/* Same collision as periwinkle: an orchid accent needs a non-purple preview. */
--preview-h: 195;
--term-bg: oklch(0.155 0.014 305);
}
:root[data-palette="orchid"][data-theme="light"],
:root[data-palette="orchid"][data-theme="light"] body {
--bg: oklch(0.983 0.004 305);
--bg-grid: oklch(0.93 0.007 305);
--surface: oklch(1 0 0);
--surface-2: oklch(0.961 0.005 305);
--surface-3: oklch(0.93 0.007 305);
--border: oklch(0.886 0.01 305);
--border-strong: oklch(0.82 0.013 305);
--ink: oklch(0.27 0.018 305);
--muted: oklch(0.47 0.018 305);
--faint: oklch(0.62 0.018 305);
--accent: oklch(0.5 0.17 308);
--accent-strong: oklch(0.45 0.17 308);
--accent-ink: oklch(0.99 0.005 308);
--accent-tint: oklch(0.5 0.17 308 / 0.11);
--accent-ring: oklch(0.5 0.17 308 / 0.5);
--term-bg: oklch(0.165 0.014 305);
}
@media (prefers-color-scheme: light) {
:root[data-palette="orchid"]:not([data-theme]) {
--bg: oklch(0.983 0.004 305);
--bg-grid: oklch(0.93 0.007 305);
--surface: oklch(1 0 0);
--surface-2: oklch(0.961 0.005 305);
--surface-3: oklch(0.93 0.007 305);
--border: oklch(0.886 0.01 305);
--border-strong: oklch(0.82 0.013 305);
--ink: oklch(0.27 0.018 305);
--muted: oklch(0.47 0.018 305);
--faint: oklch(0.62 0.018 305);
--accent: oklch(0.5 0.17 308);
--accent-strong: oklch(0.45 0.17 308);
--accent-ink: oklch(0.99 0.005 308);
--accent-tint: oklch(0.5 0.17 308 / 0.11);
--accent-ring: oklch(0.5 0.17 308 / 0.5);
--term-bg: oklch(0.165 0.014 305);
}
}
/* ---------- reset / base ---------- */
* {
box-sizing: border-box;
}
*:focus {
outline: none;
}
html,
body {
height: 100%;
margin: 0;
/* No pull-to-refresh / scroll chaining — terminal scroll wins on touch. */
overscroll-behavior: none;
}
body {
overflow-x: hidden;
}
body {
background: var(--bg);
color: var(--ink);
font-family: var(--font-mono);
font-size: var(--fs-15);
line-height: 1.45;
font-feature-settings: "ss01", "cv01";
-webkit-text-size-adjust: 100%;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
/* faint structural grid — terminal texture, not a CRT costume */
background-image:
linear-gradient(var(--bg-grid) 1px, transparent 1px),
linear-gradient(90deg, var(--bg-grid) 1px, transparent 1px);
background-size: 100% 64px, 64px 100%;
background-position: center top;
/* dim the grid via opacity on a fixed overlay would cost a node; bake it low instead */
}
#app {
/* `--app-vh` tracks the *visual* viewport (set by installViewportSizing) so
the shell stays above the mobile soft keyboard; falls back to the layout
viewport where `visualViewport` is unavailable. `--app-top` follows the
layout viewport's scroll under the keyboard so the shell stays pinned to
the visible region instead of drifting to the top. */
height: var(--app-vh, 100%);
transform: translateY(var(--app-top, 0px));
display: flex;
flex-direction: column;
overflow: hidden;
}
a {
color: inherit;
text-decoration: none;
}
code {
font-family: var(--font-mono);
font-size: 0.92em;
padding: 1px 5px;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
}
::selection {
background: var(--accent-tint);
color: var(--ink);
}
button,
input,
select,
textarea {
font-family: inherit;
font-size: inherit;
color: inherit;
}
::placeholder {
color: var(--faint);
opacity: 1;
}
:focus-visible {
outline: 2px solid var(--accent-ring);
outline-offset: 2px;
border-radius: var(--r-1);
}
/* ---------- buttons ----------
Terminal-native: the brackets are the chrome. Default + primary wear
[ brackets ]; ghost/icon drop them for quieter nav and single glyphs. */
.btn {
appearance: none;
border: 1px solid transparent;
background: transparent;
color: var(--ink);
padding: 0 var(--sp-1);
height: var(--control-h);
min-height: var(--control-h);
cursor: pointer;
font-family: var(--font-mono);
font-size: var(--fs-13);
font-weight: 500;
letter-spacing: 0.01em;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
white-space: nowrap;
transition:
color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out),
transform var(--dur-fast) var(--ease-out);
}
.btn::before {
content: "[";
color: var(--faint);
}
.btn::after {
content: "]";
color: var(--faint);
}
.btn:hover {
color: var(--accent);
}
.btn:hover::before,
.btn:hover::after {
color: var(--accent);
}
.btn:active {
transform: translateY(1px);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-primary {
color: var(--accent);
font-weight: 600;
}
.btn-primary::before,
.btn-primary::after {
color: var(--accent);
}
.btn-primary:hover {
background: var(--accent-tint);
}
/* Ghost: no brackets — quiet nav (back, disconnect, theme, kit) and icons. */
.btn-ghost {
color: var(--muted);
}
.btn-ghost::before,
.btn-ghost::after {
content: none;
}
.btn-ghost:hover {
color: var(--ink);
background: var(--surface-2);
}
.btn-icon {
width: var(--control-h);
padding: 0;
font-size: var(--fs-15);
}
.btn-icon::before,
.btn-icon::after {
content: none;
}
.btn-icon:hover {
background: var(--surface-2);
}
/* ---------- app shell: sidebar + content ---------- */
.app-shell {
flex: 1 1 auto;
min-height: 0;
}
/* Desktop: persistent two-pane split. Sidebar holds the session list; the
content pane shows the welcome state or the attached terminal. */
@media (min-width: 860px) {
.app-shell {
display: grid;
grid-template-columns:
var(--sidebar-w, clamp(220px, 22vw, 320px)) minmax(0, 1fr);
}
}
.sidebar {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
overflow: hidden;
position: relative;
background: color-mix(in oklab, var(--surface) 26%, var(--bg));
border-right: 1px solid var(--border);
}
/* Drag-to-resize grip on the sidebar's right edge (desktop split only). */
.sidebar-resize {
position: absolute;
top: 0;
right: -3px;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: var(--z-sticky);
touch-action: none;
}
.sidebar-resize:hover,
.sidebar-resize:active {
background: var(--accent-ring);
}
@media (max-width: 859px) {
.sidebar-resize {
display: none;
}
}
/* Sidebar's own slim header: just the session search. */
/* The search input IS the header — it spans edge to edge, flush, with just a
bottom rule for separation. No wrapper padding, no boxed border/radius. */
.sidebar-search {
flex: 0 0 auto;
}
/* App-level actions (help, theme, spawn), pinned to the sidebar's foot. */
.sidebar-footer {
display: flex;
align-items: center;
gap: var(--sp-2);
flex: 0 0 auto;
padding: var(--sp-2) var(--sp-3);
border-top: 1px solid var(--border);
}
.sidebar-footer .spawn-btn {
flex: 1 1 auto;
}
/* Palette picker: a ghost-button-shaped <select>. */
.palette-select {
appearance: none;
height: var(--control-h);
padding: 0 var(--sp-2);
border: 1px solid transparent;
border-radius: var(--r-2);
background: transparent;
color: var(--muted);
font-family: var(--font-mono);
font-size: var(--fs-12);
cursor: pointer;
max-width: 9ch;
text-overflow: ellipsis;
transition:
color var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out);
}
.palette-select:hover {
color: var(--ink);
border-color: var(--border-strong);
}
.palette-select:focus-visible {
border-color: var(--accent);
color: var(--ink);
}
/* ---------- daemon status dot ---------- */
/* Colour is the link state; the tooltip names the release the daemon is
running. The chip is a button so the keyboard reaches the tooltip too. */
.status-chip {
position: relative;
flex: 0 0 auto;
overflow: visible;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
box-shadow: 0 0 0 3px transparent;
transition:
background var(--dur) var(--ease-out),
box-shadow var(--dur) var(--ease-out);
}
.status-chip[data-state="online"] .status-dot {
background: var(--live);
}
.status-chip[data-state="connecting"] .status-dot,
.status-chip[data-state="reconnecting"] .status-dot {
background: var(--warn);
animation: pulse 1.1s var(--ease-out) infinite;
}
.status-chip[data-state="down"] .status-dot,
.status-chip[data-state="updating"] .status-dot {
background: var(--danger);
animation: pulse 1.1s var(--ease-out) infinite;
}
.status-tip {
position: absolute;
bottom: calc(100% + var(--sp-2));
left: 0;
z-index: var(--z-sticky);
display: flex;
flex-direction: column;
gap: 2px;
min-width: 180px;
padding: var(--sp-2);
border: 1px solid var(--border);
border-radius: var(--r-2);
background: var(--surface-2);
box-shadow: var(--shadow-2);
font-size: var(--fs-11);
text-align: left;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition:
opacity var(--dur-fast) var(--ease-out),
visibility var(--dur-fast) var(--ease-out);
}
.status-chip:hover .status-tip,
.status-chip:focus-visible .status-tip {
opacity: 1;
visibility: visible;
}
.status-tip-state {
color: var(--fg);
}
.status-tip-release {
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ---------- restart overlay ---------- */
/* Up while the daemon is unreachable or being replaced. Nothing to dismiss:
it clears itself when the link is back (docs/RESTART.md). */
.restart-modal {
max-width: 380px;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-5) var(--sp-4);
text-align: center;
}
.restart-modal h2 {
font-size: var(--fs-15);
margin: 0;
}
.restart-detail {
margin: 0;
color: var(--muted);
font-size: var(--fs-13);
}
.restart-release {
margin: 0;
color: var(--faint);
font-size: var(--fs-11);
font-variant-numeric: tabular-nums;
}
.restart-spinner {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid var(--border-strong);
border-top-color: var(--accent);
animation: spin 900ms linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.restart-spinner,
.status-dot {
animation: none;
}
}
.sidebar-search .search {
width: 100%;
max-width: none;
height: var(--bar-h);
border: 0;
border-bottom: 1px solid var(--border);
border-radius: 0;
background: transparent;
}
.sidebar-search .search:hover {
background: var(--surface-2);
border-bottom-color: var(--border-strong);
}
.sidebar-search .search:focus {
background: var(--surface-2);
border-bottom-color: var(--accent);
box-shadow: none;
}
.content {
min-width: 0;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
background: var(--bg);
}
.content-welcome {
margin: auto;
max-width: 48ch;
}
/* Off-screen home for parked-but-connected terminals. Kept in the DOM (not
display:none) so the live WS keeps buffering into each terminal; on
re-mount we fit + the scrollback is already there, so switching is instant. */
.conn-cache {
position: fixed;
left: -99999px;
top: 0;
width: 1px;
height: 1px;
overflow: hidden;
}
/* ---------- catalog ---------- */
.catalog {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.catalog-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-2) var(--sp-3);
border-bottom: 1px solid var(--border);
background: color-mix(in oklab, var(--surface) 82%, transparent);
backdrop-filter: blur(8px);
position: sticky;
top: 0;
z-index: var(--z-sticky);
}
.brand {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
margin-right: auto;
}
.brand-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--live);
box-shadow: 0 0 0 4px var(--live-glow);
flex: 0 0 auto;
animation: pulse 2.4s var(--ease-out) infinite;
}
.brand h1 {
margin: 0;
font-size: var(--fs-17);
font-weight: 600;
letter-spacing: -0.01em;
}
.brand .blink {
color: var(--accent);
font-weight: 400;
animation: blink 1.2s steps(1) infinite;
}
.catalog-actions {
display: flex;
flex: 1 1 280px;
gap: var(--sp-2);
align-items: center;
justify-content: flex-end;
}
.search {
flex: 1 1 200px;
max-width: 340px;
height: var(--control-h);
padding: 0 var(--sp-3);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-2);
color: var(--ink);
transition:
border-color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
}
.search:hover {
border-color: var(--border-strong);
}
.search:focus {
border-color: var(--accent);
background: var(--surface);
box-shadow: 0 0 0 3px var(--accent-tint);
}
/* ---------- session tree ---------- */
.tree {
flex: 1 1 auto;
overflow: auto;
padding: var(--sp-1) 0 var(--sp-4);
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.tree-folder {
display: flex;
flex-direction: column;
}
.tree-folder-header {
display: flex;
align-items: center;
gap: var(--sp-2);
background: transparent;
border: 0;
width: 100%;
text-align: left;
height: 38px;
padding-right: var(--sp-4);
color: var(--muted);
cursor: pointer;
font-family: var(--font-mono);
font-size: var(--fs-12);
}
.tree-folder-header:hover {
color: var(--ink);
}
.tree-folder-header .chevron {
width: 16px;
text-align: center;
color: var(--faint);
font-size: var(--fs-11);
/* The rotation is the collapse animation and stays; the hover colour is
immediate, like the row's own hover. */
transition: transform var(--dur) var(--ease-out);
}
.tree-folder-header[aria-expanded="false"] .chevron {
transform: rotate(-90deg);
}
.tree-folder-header:hover .chevron {
color: var(--accent);
}
.folder-name {
flex: 1 1 auto;
min-width: 0;
color: var(--ink);
font-size: var(--fs-14);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Earlier path segments collapse to their initial; the trailing segment (the
one that actually names the dir) stays full. Dim the collapsed prefix. */
.folder-name .folder-stem {
color: var(--faint);
}
.tree-leaf {
display: flex;
align-items: center;
gap: var(--sp-2);
margin: 1px 0;
padding: 4px var(--sp-3);
border-radius: var(--r-1);
color: var(--ink);
position: relative;
/* No transition on `background`: it is what hover, the keyboard cursor and
the preview all paint, and easing it puts a visible delay between the input
landing on a row and the row admitting it. `.is-active`'s ring still eases
— being open in a pane is a state, not a response to the pointer. */
transition: box-shadow var(--dur-fast) var(--ease-out);
}
/* Session-row tiers (docs/SIDEBAR.md). A row is usually several of these at
once, so every qualifier below sits in `:where()`: each rule is worth the
same specificity and the ladder is decided purely by the order they appear
in, latest wins. Add a tier where it belongs in that order, not by piling on
another class to out-weigh the rule above it. Each tier paints a background
and nothing else — the list says all of this with fill, never an edge. */
/* 1. Alive. A faint wash on the row itself — no per-row status icon. */
.tree-leaf:where([data-status="running"]) {
background: color-mix(in oklab, var(--accent) 6%, transparent);
}
/* 2. Under the pointer, or under the keyboard walk. Both are what previews a
row (docs/PREVIEW.md), so both read in the preview hue — never the accent,
which belongs to the row actually open. */
.tree-leaf:where(:hover, .is-highlighted) {
background: color-mix(in oklab, var(--preview) 12%, var(--surface-2));
}
.tree-leaf:where([data-status="running"]):where(:hover, .is-highlighted) {
background: color-mix(in oklab, var(--preview) 18%, var(--surface-2));
}
.tree-leaf:where(:focus-visible) {
outline: none;
background: color-mix(in oklab, var(--accent) 16%, var(--surface-2));
}
/* A folder row previews nothing, so its cursor stays a ring. */
.tree-folder-header.is-highlighted {
box-shadow: inset 0 0 0 2px var(--accent-ring);
}
.tree {
outline: none;
}
/* 3. Open in some pane, but not the focused one (issue #33). */
.tree-leaf:where(.is-open) {
background: color-mix(in oklab, var(--accent) 6%, transparent);
}
/* 4. The session currently mounted in the content pane. */
.tree-leaf:where(.is-active) {
background: var(--accent-fill);
}
.tree-leaf.is-active .leaf-name {
color: var(--ink);
}
/* 5. The row the content pane is previewing right now. Last, so previewing a
session that is already open still reads as provisional — except the session
already mounted in the pane (tier 4). Previewing that one changes nothing on
screen, so its accent stays put instead of blinking out under the pointer. */
.tree-leaf:where(.is-previewing):where(:not(.is-active)) {
background: var(--preview-tint);
}
/* Exited sessions recede: dimmed name + an "exited" badge carry status, so it's
never the running tint's absence alone doing the work. */
.tree-leaf[data-status="dead"] {
color: var(--muted);
}
.tree-leaf[data-status="dead"] .leaf-name {
color: var(--muted);
font-weight: 400;
}
/* #120: every session card has two lines, so its row height is stable. */
.tree-leaf.session-card {
flex-direction: column;
align-items: stretch;
gap: 1px;
/* 24px action row + 16px meta row + 1px gap + 8px vertical padding. */
height: 49px;
}
.card-line1 {
display: flex;
align-items: center;
gap: var(--sp-2);
min-width: 0;
}
.leaf-name {
font-weight: 500;
font-size: var(--fs-13);
/* Grows to push the actions to the right edge but shrinks first, so the name
truncates before it can ever reach them — overlap is a defect (#107). */
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* #131: a name the daemon generated at spawn is provisional — the first prompt
may replace it — so it reads as placeholder text rather than a chosen name.
A session that never prompts simply keeps it, dimmed; that is accurate. */
.leaf-name.is-provisional {
color: var(--muted);
font-weight: 400;
}
/* Inline rename field (#125): sits exactly where the name it replaces did, so
the card does not jump when the edit starts. */
input.leaf-rename {
font: inherit;
font-weight: 500;
font-size: var(--fs-13);
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--accent-ring);
border-radius: var(--r-2);
padding: 0 var(--sp-1);
min-width: 0;
}
input.leaf-rename:focus {
outline: none;
box-shadow: 0 0 0 3px var(--accent-tint);
}
/* Right-click menu on a session card (#125). Positioned from the click point,
so it is fixed to the viewport rather than to the scrolling sidebar. */
.context-menu {
position: fixed;
z-index: var(--z-modal);
display: flex;
flex-direction: column;
min-width: 168px;
padding: var(--sp-1);
background: var(--surface-2);
border: 1px solid var(--border-strong);
border-radius: var(--r-3);
box-shadow: var(--shadow-2);
}
.context-menu-item {
appearance: none;
background: none;
border: 0;
border-radius: var(--r-2);
color: var(--ink);
font: inherit;
font-size: var(--fs-13);
text-align: left;
padding: var(--sp-1) var(--sp-2);
cursor: pointer;
}
.context-menu-item:hover,
.context-menu-item:focus-visible {
background: var(--surface-3);
outline: none;
}
.context-menu-item.is-danger {
color: var(--danger);
}
.context-menu-item.is-danger:hover,
.context-menu-item.is-danger:focus-visible {
background: var(--danger-tint);
}
/* ---------- help overlay (`?`) ---------- */
.help-overlay {
position: fixed;
inset: 0;
z-index: var(--z-modal);
display: grid;
place-items: center;
padding: var(--sp-4);
background: oklch(0 0 0 / 0.5);
backdrop-filter: blur(4px);
}
.help-panel {
width: min(440px, 100%);
max-height: min(80vh, 640px);
overflow-y: auto;
padding: var(--sp-4);
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--r-3);
box-shadow: var(--shadow-2);
}
.help-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-2);
margin-bottom: var(--sp-3);
}
.help-title {
margin: 0;
font-family: var(--font-mono);
font-size: var(--fs-15);
color: var(--ink);
}
.help-close {
appearance: none;
background: none;
border: 0;
border-radius: var(--r-2);
color: var(--muted);
font: inherit;
line-height: 1;
padding: var(--sp-1) var(--sp-2);
cursor: pointer;
}
.help-close:hover,
.help-close:focus-visible {
background: var(--surface-3);
color: var(--ink);
outline: none;
}
.help-group + .help-group {
margin-top: var(--sp-4);
}
.help-group-title {
margin: 0 0 var(--sp-2);
font-size: var(--fs-11);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--faint);
}
.help-list {
margin: 0;
}
/* Keycaps in a fixed column so the descriptions line up down the list. */
.help-row {
display: grid;
grid-template-columns: 14ch 1fr;
align-items: baseline;
gap: var(--sp-3);
padding: var(--sp-1) 0;
}
.help-keys {
display: flex;
flex-wrap: nowrap;
gap: var(--sp-1);
margin: 0;
}
.help-keys kbd {
font-family: var(--font-mono);
font-size: var(--fs-11);
background: var(--surface-2);
border: 1px solid var(--border);
border-bottom-width: 2px;
border-radius: var(--r-1);
padding: 1px 6px;
color: var(--ink);
}
.help-desc {
margin: 0;
font-size: var(--fs-13);
color: var(--muted);
}
/* ---------- command bar (Ctrl+K) — docs/COMMAND-BAR.md ---------- */
/* Same family of surface as the spawn modal and the help overlay: the overlay
tokens below are theirs, not a third set. */
.cmdbar-overlay {
position: fixed;
inset: 0;
z-index: var(--z-modal);
display: grid;
grid-template-rows: 12vh auto 1fr;
justify-items: center;
padding: var(--sp-4);
background: oklch(0 0 0 / 0.5);
backdrop-filter: blur(4px);
}
.cmdbar-panel {
grid-row: 2;
display: flex;
flex-direction: column;
width: min(560px, 100%);
max-height: min(60vh, 520px);
overflow: hidden;
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--r-3);
box-shadow: var(--shadow-2);
}
.cmdbar-input {
appearance: none;
background: none;
border: 0;
border-bottom: 1px solid var(--border);
border-radius: 0;
color: var(--ink);
font-family: var(--font-mono);
font-size: var(--fs-15);
padding: var(--sp-3) var(--sp-4);
}
.cmdbar-input:focus-visible {
outline: none;
}
.cmdbar-results {
overflow-y: auto;
padding: var(--sp-2) 0;
}
/* The action page's header (#151): names the session being acted on, so the
target of kill/rename/spawn-similar is never ambiguous. */
.cmdbar-page-header {
padding: var(--sp-2) var(--sp-4);
border-bottom: 1px solid var(--border);
font-size: var(--fs-12);
color: var(--muted);
}
.cmdbar-group-title {
padding: var(--sp-2) var(--sp-4) var(--sp-1);
font-size: var(--fs-11);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--faint);
}
.cmdbar-row {
display: flex;
align-items: baseline;
gap: var(--sp-3);
padding: var(--sp-1) var(--sp-4);
cursor: pointer;
}
.cmdbar-row.is-active {
background: var(--surface-3);
}
.cmdbar-row-name {
font-size: var(--fs-13);
color: var(--ink);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* A dead session still lists — it just is not where you just were. */
.cmdbar-row.is-dead .cmdbar-row-name {
color: var(--muted);
}
.cmdbar-row-meta {
flex: 1 1 auto;
min-width: 0;
font-size: var(--fs-11);
color: var(--faint);
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* A command row's chord, shown so the bar teaches the keymap. */
.cmdbar-row-keys {
flex: 1 1 auto;
display: flex;
justify-content: flex-end;
gap: var(--sp-1);
min-width: 0;
}
.cmdbar-row-keys kbd {
font-family: var(--font-mono);
font-size: var(--fs-11);
color: var(--muted);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
padding: 0 var(--sp-1);
}
.cmdbar-hit {
background: none;
color: var(--accent);
font-weight: 600;
}
.cmdbar-empty {
padding: var(--sp-3) var(--sp-4);
font-size: var(--fs-13);
color: var(--muted);
}
/* Phone: the bottom-sheet treatment the spawn modal already uses. */
/* Bottom sheet at phone width, the same treatment (and the same 599px break)
the spawn modal uses: the bar is reached by thumb here, through the sidebar's
search field (#157), so it rides the bottom of the screen. */
@media (max-width: 599px) {
.cmdbar-overlay {
grid-template-rows: 1fr auto;
padding: 0;
}
.cmdbar-panel {
grid-row: 2;
width: 100%;
max-height: 70dvh;
border-radius: var(--r-3) var(--r-3) 0 0;
border-bottom: 0;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
/* Thumb-sized rows and a field that clears the on-screen keyboard's
zoom-on-focus threshold (16px). */
.cmdbar-input {
font-size: var(--fs-17);
min-height: 44px;
}
.cmdbar-row {
min-height: 44px;
}
}
/* Line 2 — dot + duration in the state's colour, then muted fragments. The
state word itself is never spelled out here; the colour carries it. */
.card-line2 {
display: flex;
align-items: center;
gap: var(--sp-2);
font-size: var(--fs-11);
min-width: 0;
color: var(--muted);
}
.card-line2[data-activity="active"] {
color: var(--live);
}
.card-line2[data-activity="idle"] {
color: var(--muted);
}
.card-line2[data-activity="awaiting-input"] {
color: var(--warn);
}
.card-line2[data-activity="error"] {
color: var(--danger);
}
/* docs/SIDEBAR.md: the activity rail replaces the old inline dot. It is
pinned to the row's own left edge, full height, square corners — see
catalog.ts. Idle stays solid and still on purpose: motion is reserved for
states that need attention (DESIGN.md's "living signals" rule). */
.activity-rail {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 5px;
background: currentColor;
}
.activity-rail[data-activity="active"] {
color: var(--live);
}
.activity-rail[data-activity="idle"] {
color: var(--border-strong);
}
.activity-rail[data-activity="awaiting-input"] {
color: var(--warn);
animation: blink 1.2s steps(1) infinite;
}
.activity-rail[data-activity="error"] {
color: var(--danger);
animation: rail-flash 1.4s ease-out infinite;
}
.activity-duration {
color: inherit;
flex: 0 0 auto;
}
/* Agent-reported detail (#112): sits after the duration, shrinks with an
ellipsis so it never pushes the trailing fragments off the line. */
.activity-detail {
color: var(--faint);
/* Consume only the space left after the trailing status + command. */
flex: 1 1 0;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Blocked-work / exited / command fragments recede: the ⚠ glyph already
signals severity and "exited" dims with the row. */
.card-flag {
color: var(--faint);
flex: 0 0 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Status fragments stay intact. The unconditional command is the final
truncation target when even the complete trailing set cannot fit. */
.leaf-cmd {
flex: 0 1 auto;
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
}
/* The tool's mark stands in for the program the command line starts with
(docs/SIDEBAR.md). It keeps its own colours where the tool has brand
colours, and inherits the muted fragment colour where it does not. */
.tool-icon {
flex: 0 0 auto;
opacity: 0.9;
}
.leaf-cmd-rest {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Blocked work as diff notation: `+124 -18 ?3 ↑2` (docs/SIDEBAR.md). The
spelled-out breakdown is the tooltip; only the additions and deletions take
colour, so the ⚠-less fragment still reads as a diff and not as an alarm. */
.leaf-blocked {
display: inline-flex;
align-items: center;
gap: 4px;
font-variant-numeric: tabular-nums;
}
.blocked-stat[data-stat="add"] {
color: var(--live);
}
.blocked-stat[data-stat="del"] {
color: var(--danger);
}
.blocked-stat[data-stat="files"],
.blocked-stat[data-stat="untracked"],
.blocked-stat[data-stat="unpushed"] {
color: var(--faint);
}
/* Actions live in line 1's flow at its right edge — never overlaid on the
name (#107). They reserve their width even while hover-faded. */
.leaf-actions {
flex: 0 0 auto;
margin-left: auto;
display: inline-flex;
gap: 2px;
}
.leaf-act {
background: var(--surface-3);
color: var(--faint);
border: 1px solid var(--border);
border-radius: var(--r-1);
width: 24px;
height: 24px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: var(--fs-12);
transition:
color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out);
}
@media (hover: hover) {
.leaf-actions {
opacity: 0;
transition: opacity var(--dur-fast) var(--ease-out);
}
.tree-leaf:hover .leaf-actions,
.tree-leaf:focus-within .leaf-actions {
opacity: 1;
}
}
.leaf-clone:hover {
color: var(--accent);
background: var(--accent-tint);
border-color: color-mix(in oklab, var(--accent) 40%, transparent);
}
.leaf-kill:hover {
color: var(--danger);
background: var(--danger-tint);
border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}
/* ---------- forced-kill prompt (#81) ---------- */
.kill-dialog {
position: fixed;
inset: 0;
z-index: var(--z-modal);
display: grid;
place-items: center;
padding: var(--sp-4);
background: oklch(0 0 0 / 0.5);
backdrop-filter: blur(4px);
}
.kill-dialog-panel {
width: min(400px, 100%);
padding: var(--sp-4);
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--r-3);
box-shadow: var(--shadow-2);
}
.kill-dialog-title {
margin: 0 0 var(--sp-2);
font-family: var(--font-mono);
font-size: var(--fs-15);
color: var(--ink);
}
.kill-dialog-body {
margin: 0;
color: var(--muted);
font-size: var(--fs-13);
line-height: 1.5;
}
.kill-dialog-error {
margin: var(--sp-2) 0 0;
color: var(--danger);
font-size: var(--fs-13);
}
.kill-dialog-actions {
display: flex;
justify-content: flex-end;
gap: var(--sp-2);
margin-top: var(--sp-4);
}
.btn-danger {
color: var(--danger);
border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}
.btn-danger:hover {
background: var(--danger-tint);
border-color: var(--danger);
}
/* ---------- empty / loading ---------- */
.empty {
padding: var(--sp-5) var(--sp-4);
color: var(--muted);
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-3);
font-family: var(--font-sans);
}
.empty-glyph {
font-family: var(--font-mono);
font-size: var(--fs-20);
color: var(--accent);
border: 1px solid var(--border);
border-radius: var(--r-3);
width: 56px;
height: 56px;
display: grid;
place-items: center;
background: var(--surface);
}
.empty-title {
color: var(--ink);
font-size: var(--fs-15);
font-family: var(--font-mono);
}
.empty-hint {
font-size: var(--fs-13);
max-width: 42ch;
}
.empty-hint kbd {
font-family: var(--font-mono);
font-size: 0.9em;
background: var(--surface-2);
border: 1px solid var(--border);
border-bottom-width: 2px;
border-radius: var(--r-1);
padding: 1px 6px;
color: var(--ink);
}
.empty-actions {
display: flex;
gap: var(--sp-2);
margin-top: var(--sp-2);
}
/* ---------- attach view ---------- */
.attach {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg);
}
.attach-header {
display: flex;
align-items: center;
gap: var(--sp-2);
height: var(--bar-h);
flex: 0 0 var(--bar-h);
padding: 0 var(--sp-2);
border-bottom: 1px solid var(--border);
background: var(--surface);
}
/* Compact controls so the bar hugs the title — no tall buttons. */
.attach-header .btn {
height: 26px;
min-height: 26px;
}
.attach-header .btn-icon {
width: 26px;
}
/* Opens the off-canvas session drawer. Desktop keeps the sidebar permanently in
view, so it is mobile-only — same rule as the welcome state's ☰. */
.attach-header .sessions-btn {
font-size: var(--fs-15);
width: 28px;
height: 26px;
}
@media (min-width: 860px) {
.attach-header .sessions-btn,
.welcome-sessions {
display: none;
}
}
.attach-header .disconnect {
color: var(--faint);
}
.attach-header .disconnect:hover {
color: var(--danger);
background: var(--danger-tint);
border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}
.attach-title {
font-family: var(--font-mono);
font-size: var(--fs-13);
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
}
.attach-title .sid {
color: var(--muted);
}
.attach-title .at-name {
color: var(--ink);
font-weight: 500;
}
.attach-title .at-cmd {
color: var(--muted);
display: inline-flex;
align-items: center;
gap: 4px;
}
.attach-title .at-cmd::before {
content: "·";
margin: 0 6px;
color: var(--faint);
}
.attach-title .at-host {
color: var(--faint);
margin-left: 6px;
}
.attach-title .at-host::before {
content: "@";
opacity: 0.6;
}
.attach-state {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--fs-12);
padding: 2px var(--sp-2);
border-radius: var(--r-pill);
background: var(--surface-2);
color: var(--muted);
border: 1px solid var(--border);
white-space: nowrap;
}
.attach-state::before {
content: "";
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
}
.attach-state[data-state="connecting"] {
color: var(--warn);
}
.attach-state[data-state="connecting"]::before {
animation: pulse 1.2s var(--ease-out) infinite;
}
.attach-state[data-state="open"] {
color: var(--accent);
}
.attach-state[data-state="open"]::before {
box-shadow: 0 0 6px var(--live-glow);
}
.attach-state[data-state="closed"],
.attach-state[data-state="error"] {
color: var(--danger);
}
.attach-body {
flex: 1 1 auto;
min-height: 0;
padding: var(--sp-2);
display: flex;
overflow: hidden;
background: var(--term-bg);
}
.attach-body .xterm {
width: 100%;
height: 100%;
}
/* xterm hard-codes overflow-y: scroll on its viewport — an always-on bar that
(a) shows even with no scrollback and (b) makes FitAddon subtract its width,
costing a column. Hide it entirely: scroll still works via wheel/touch/keys,
and fit reclaims the full width. */
/* Higher specificity than xterm's own `.xterm .xterm-viewport` (which loads
after this and would otherwise win the tie) so the always-on scrollbar
reservation actually goes away. */
.attach-body .xterm .xterm-viewport {
overflow-y: auto;
scrollbar-width: none;
}
/* Maestro drives scrollback from touchmove (see attach.ts), so the browser must
not claim the gesture. On `.xterm`, not the viewport: `.xterm-screen` paints
over the viewport and is what a finger lands on. */
.attach-body .xterm {
touch-action: none;
}
.attach-body .xterm .xterm-viewport::-webkit-scrollbar {
width: 0;
height: 0;
}
/* Mobile: full-bleed terminal — the desktop frame padding wastes space and the
sub-cell remainder it left was triggering a stray scrollbar. */
@media (max-width: 859px) {
.attach-body {
padding: 0;
}
}
.attach-status {
margin: var(--sp-4);
padding: var(--sp-2) var(--sp-3);
border-radius: var(--r-2);
background: var(--surface);
border: 1px solid var(--border);
font-size: var(--fs-14);
color: var(--muted);
}
.attach-status.status-error {
border-color: color-mix(in oklab, var(--danger) 50%, var(--border));
color: var(--danger);
}
/* ---------- mobile special-key bar (Termux-style) ---------- */
/* Two compact rows (Termux layout) in an 8-column grid the 16 keys flow into:
Esc/Tab + Ctrl/Alt on the left, cursor keys as a real-keyboard inverted-T in
the middle (Home/End fill the top corners), then PgUp/PgDn and the shell
symbols. Columns never shrink below a thumb's width — on a narrow phone the
bar scrolls sideways instead. Layout is always defined; only *visibility* is
gated: hidden on precise-pointer devices (a physical keyboard already has
these keys), shown on touch where the soft keyboard omits them. The shell
tracks the visual viewport (see `#app`), so it lands above it. */
.attach-keybar {
display: none;
grid-template-columns: repeat(8, minmax(2.5rem, 1fr));
gap: var(--sp-1);
padding: var(--sp-1) var(--sp-2);
padding-bottom: calc(var(--sp-1) + env(safe-area-inset-bottom, 0px));
border-top: 1px solid var(--border);
background: var(--surface);
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
}
.attach-keybar::-webkit-scrollbar {
display: none;
}
/* Shown on touch only while *this* terminal is focused (`keybar-active`, toggled
on textarea focus/blur in attach.ts) AND the soft keyboard is actually up
(`html.kb-open`, detected from the visual-viewport shrink in viewport.ts).
Both are needed: dismissing the keyboard doesn't blur the terminal, so focus
alone would leave the bar lingering. */
@media (pointer: coarse) {
html.kb-open .attach-keybar.keybar-active {
display: grid;
}
}
.attach-keybar .keycap {
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: var(--fs-13);
color: var(--ink);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
cursor: pointer;
transition:
background var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out);
}
.attach-keybar .keycap:active {
background: var(--surface-3);
border-color: var(--border-strong);
}
/* Explicit press feedback: `pointerdown` preventDefault (kept for terminal
focus) suppresses native `:active` on touch, so flash a quick spring. */
@keyframes keycap-press {
0% {
transform: scale(0.92);
background: var(--accent-tint);
border-color: var(--accent);
}
100% {
transform: scale(1);
}
}
.attach-keybar .keycap.is-pressed {
animation: keycap-press 200ms var(--ease-out);
}
/* Armed sticky modifier (Ctrl/Alt): lit in signal-green until it folds into the
next key. */
.attach-keybar .keycap-mod[aria-pressed="true"] {
color: var(--accent-ink);
background: var(--accent);
border-color: var(--accent-strong);
}
/* ---------- structured view (#140) ----------
`.attach.structured` reuses `.attach`'s flex column, `.attach-header` and
`.attach-state` verbatim (attach.ts and structured.ts share paneTitle.ts's
header markup) — only the body differs: a transcript and composer instead
of an xterm surface. Hierarchy is deliberate: the approval is the
only lifted, card-like element on the page; everything else is either a
tinted block (user), plain text (agent), or a thin bordered row (tool). */
.structured-config {
position: relative;
flex: 0 0 40px;
}
.structured-config-trigger {
width: 40px;
height: 40px;
display: inline-flex;
justify-content: center;
align-items: center;
padding: 0;
border: 0;
border-radius: var(--r-2);
background: transparent;
color: var(--muted);
cursor: pointer;
}
.structured-config-trigger svg {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.75;
stroke-linecap: round;
}
.structured-config-trigger:hover,
.structured-config-trigger[aria-expanded="true"] {
color: var(--ink);
background: var(--surface-3);
}
.structured-config-trigger:focus-visible,
.structured-config-select:focus-visible,
.structured-config-toggle:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.structured-config-menu {
position: absolute;
z-index: 20;
bottom: calc(100% + var(--sp-2));
right: 0;
width: min(320px, 86vw);
padding: var(--sp-2);
border: 1px solid var(--border-strong);
border-radius: var(--r-2);
background: var(--surface);
box-shadow: var(--shadow-2);
}
.structured-config-row {
display: grid;
grid-template-columns: minmax(80px, 1fr) minmax(0, 1.6fr);
align-items: center;
gap: var(--sp-3);
min-height: 34px;
padding: var(--sp-1) var(--sp-2);
color: var(--ink);
font-size: var(--fs-12);
}
.structured-config-row + .structured-config-row {
border-top: 1px solid var(--border);
}
.structured-config-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.structured-config-select {
min-width: 0;
height: 28px;
padding: 0 var(--sp-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
background: var(--surface-2);
color: var(--ink);
font: inherit;
}
.structured-config-toggle {
justify-self: end;
width: 17px;
height: 17px;
accent-color: var(--accent);
}
.structured-body {
flex: 1 1 auto;
min-height: 0;
display: flex;
}
.attach.structured,
.attach.structured .attach-title,
.structured-input {
font-family: var(--font-sans);
}
.st-row-agent code,
.st-tool-text,
.st-tool-detail pre,
.st-diff,
.st-approval-path {
font-family: var(--font-mono);
}
.structured-main {
min-width: 0;
min-height: 0;
max-width: 820px;
margin-inline: auto;
width: 100%;
display: flex;
flex-direction: column;
flex: 1 1 auto;
}
.structured-scroll {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
overflow-y: auto;
}
.structured-rows {
display: flex;
flex-direction: column;
min-width: 0;
}
.structured-activity {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: var(--sp-2);
min-height: 30px;
padding: var(--sp-1) var(--sp-3);
color: var(--muted);
font-size: var(--fs-11);
font-variant-numeric: tabular-nums;
}
.structured-activity[hidden] {
display: none;
}
.structured-activity-pulse {
position: relative;
width: 7px;
height: 7px;
flex: 0 0 auto;
border-radius: 50%;
background: var(--live);
box-shadow: 0 0 0 0 var(--live-glow);
animation: structured-thinking 1.6s ease-out infinite;
}
@keyframes structured-thinking {
50% {
box-shadow: 0 0 0 5px transparent;
}
}
/* every transcript row carries its own layout; none of them are cards */
.st-row {
display: flex;
gap: var(--sp-3);
padding: var(--sp-3);
min-width: 0;
}
.st-body {
flex: 1 1 auto;
min-width: 0;
}
/* User turns read as compact chat bubbles while the padded row remains the
full hover/copy target. */
.st-row-user {
justify-content: flex-end;
}
.st-row-user .st-body {
flex: 0 1 auto;
max-width: min(86%, 72ch);
padding: var(--sp-2) var(--sp-3);
border-radius: var(--r-2);
background: var(--surface-3);
}
.st-row-user:hover {
background: var(--surface-2);
}
.st-user-text {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* agent prose: no card, no background — plain text is the point */
.st-row-agent .st-body p {
margin: 0 0 var(--sp-2);
overflow-wrap: anywhere;
}
.st-row-agent .st-body p:last-child {
margin-bottom: 0;
}
.st-row-agent .st-body > :first-child {
margin-top: 0;
}
.st-row-agent .st-body > :last-child {
margin-bottom: 0;
}
.st-row-agent .st-body h1,
.st-row-agent .st-body h2,
.st-row-agent .st-body h3,
.st-row-agent .st-body h4 {
margin: var(--sp-4) 0 var(--sp-2);
color: var(--ink);
font-size: var(--fs-14);
line-height: 1.35;
}
.st-row-agent .st-body ul,
.st-row-agent .st-body ol {
margin: var(--sp-2) 0;
padding-left: var(--sp-5);
}
.st-row-agent .st-body li + li {
margin-top: var(--sp-1);
}
.st-row-agent .st-body pre {
margin: var(--sp-2) 0;
padding: var(--sp-3);
overflow-x: auto;
border: 1px solid var(--border);
border-radius: var(--r-2);
background: var(--surface-2);
}
.st-row-agent .st-body pre code {
padding: 0;
border: 0;
background: transparent;
}
.st-row-agent .st-body a {
color: var(--ink);
text-decoration-color: var(--border-strong);
text-underline-offset: 2px;
}
.st-markdown-table-wrap {
max-width: 100%;
margin: var(--sp-2) 0;
overflow-x: auto;
border: 1px solid var(--border);
border-radius: var(--r-2);
}
.st-row-agent table {
width: 100%;
border-collapse: collapse;
font-size: var(--fs-12);
}
.st-row-agent th,
.st-row-agent td {
padding: var(--sp-2) var(--sp-3);
border-bottom: 1px solid var(--border);
text-align: left;
vertical-align: top;
white-space: nowrap;
}
.st-row-agent th {
color: var(--ink);
background: var(--surface-2);
font-weight: 600;
}
.st-row-agent tr:last-child td {
border-bottom: 0;
}
.st-row-agent code {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
padding: 0 3px;
font-size: 0.92em;
}
.st-row-thought {
color: var(--faint);
font-style: italic;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.st-row-notice {
color: var(--faint);
font-size: var(--fs-12);
}
.st-turn-meta {
align-self: flex-start;
display: flex;
align-items: center;
gap: var(--sp-2);
margin: 0 var(--sp-3) var(--sp-2);
padding: var(--sp-1) var(--sp-2);
border-radius: var(--r-1);
color: var(--faint);
font-size: var(--fs-11);
font-variant-numeric: tabular-nums;
}
.st-turn-meta:hover {
background: var(--surface-2);
color: var(--muted);
}
.st-turn-meta-dot {
width: 5px;
height: 5px;
flex: 0 0 auto;
border-radius: 50%;
background: var(--live);
}
.st-turn-meta-cancelled .st-turn-meta-dot {
background: var(--warn);
}
.st-turn-meta-failed .st-turn-meta-dot {
background: var(--danger);
}
.st-copyable {
position: relative;
border-radius: var(--r-1);
cursor: copy;
transition: background var(--dur-fast) var(--ease-out);
}
.st-copyable::after {
content: none;
position: absolute;
top: var(--sp-1);
right: var(--sp-1);
padding: 1px var(--sp-1);
border-radius: var(--r-1);
background: var(--surface-3);
color: var(--faint);
font-size: var(--fs-11);
font-style: normal;
line-height: 1.4;
opacity: 0;
pointer-events: none;
transition: opacity var(--dur-fast) var(--ease-out);
}
.st-copyable:focus-visible {
background: var(--surface-2);
outline: none;
}
@media (hover: hover) {
.st-copyable:hover {
background: var(--surface-2);
}
.st-copyable:hover::after {
content: "copy";
opacity: 1;
}
.st-copyable[data-copy-state="copied"]:hover::after {
content: "copied";
color: var(--live);
}
.st-copyable[data-copy-state="error"]:hover::after {
content: "copy failed";
color: var(--danger);
}
}
/* tool rows: bordered, compact, deliberately not lifted */
.st-tool {
flex: 1 1 auto;
border: 1px solid var(--border);
border-radius: var(--r-2);
min-width: 0;
}
.st-tool-head {
appearance: none;
width: 100%;
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
min-width: 0;
transition: background var(--dur-fast) var(--ease-out);
}
.st-tool-head:hover {
background: var(--surface-2);
}
.st-row-tool:hover,
.st-row-approval:hover {
background: var(--surface-2);
}
.st-tool-glyph {
width: 16px;
flex: 0 0 auto;
text-align: center;
color: var(--faint);
}
.st-tool-name {
font-weight: 500;
flex: 0 0 auto;
}
.st-tool-arg {
color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
text-align: left;
}
.st-tool-meta {
margin-left: auto;
flex: 0 0 auto;
font-size: var(--fs-11);
color: var(--faint);
font-variant-numeric: tabular-nums;
}
.st-tool-meta-running {
color: var(--live);
}
.st-tool-meta-failed {
color: var(--danger);
}
.st-tool-out {
border-top: 1px solid transparent;
}
.st-disclosure {
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transition:
grid-template-rows var(--dur) var(--ease-out),
opacity var(--dur-fast) var(--ease-out),
border-color var(--dur-fast) var(--ease-out);
}
.st-disclosure.is-expanded {
grid-template-rows: 1fr;
opacity: 1;
}
.st-disclosure-clip {
min-height: 0;
overflow: hidden;
}
.st-tool-out.is-expanded {
border-top-color: var(--border);
}
.st-tool-out .st-disclosure-inner {
display: flex;
flex-direction: column;
gap: var(--sp-2);
padding: var(--sp-2) var(--sp-3);
}
/* `display: flex` above outranks the UA sheet's `[hidden]` (same pitfall as
`.wt-step`/`.raw-wrap`) — restate it explicitly. */
.st-tool-out[hidden] {
display: none;
}
.st-tool-text {
font-size: var(--fs-12);
color: var(--muted);
white-space: pre-wrap;
overflow-wrap: anywhere;
overflow-x: auto;
}
.st-tool-detail {
min-width: 0;
}
.st-tool-detail-label {
margin-bottom: var(--sp-1);
color: var(--faint);
font-size: var(--fs-11);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.st-tool-detail pre {
margin: 0;
padding: var(--sp-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
background: var(--bg);
color: var(--muted);
font: inherit;
font-size: var(--fs-12);
white-space: pre-wrap;
overflow-wrap: anywhere;
overflow-x: auto;
}
.st-tool-fact {
display: flex;
align-items: baseline;
gap: var(--sp-2);
color: var(--faint);
font-size: var(--fs-11);
}
.st-tool-fact code {
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
.st-tool-exit-ok {
color: var(--live);
}
.st-tool-exit-failed {
color: var(--danger);
}
.st-tool-image {
display: block;
width: auto;
max-width: min(100%, 720px);
max-height: 480px;
border: 1px solid var(--border);
border-radius: var(--r-1);
object-fit: contain;
}
.st-tool-audio {
display: block;
width: min(100%, 480px);
min-height: var(--control-h);
}
.st-tool-resource {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
align-items: baseline;
gap: var(--sp-1) var(--sp-2);
min-width: 0;
padding: var(--sp-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
background: var(--bg);
font-size: var(--fs-12);
}
.st-tool-resource-name {
color: var(--ink);
font-weight: 500;
}
.st-tool-resource-uri {
min-width: 0;
overflow-wrap: anywhere;
color: var(--muted);
}
.st-tool-resource-description {
grid-column: 1 / -1;
color: var(--muted);
}
.st-tool-terminal {
color: var(--muted);
font-family: var(--font-mono);
font-size: var(--fs-12);
}
.st-tool-raw {
color: var(--faint);
font-size: var(--fs-11);
}
.st-tool-raw summary {
width: fit-content;
padding-block: var(--sp-1);
cursor: pointer;
user-select: none;
transition: color var(--dur-fast) var(--ease-out);
}
.st-tool-raw summary:hover,
.st-tool-raw summary:focus-visible {
color: var(--ink);
}
.st-tool-raw summary:focus-visible {
border-radius: var(--r-1);
outline: 2px solid var(--accent-ring);
outline-offset: 2px;
}
.st-tool-raw-body {
display: flex;
flex-direction: column;
gap: var(--sp-2);
padding-top: var(--sp-1);
}
.st-tool-group {
min-width: 0;
border: 1px solid var(--border);
border-radius: var(--r-2);
background: color-mix(in oklab, var(--surface-2) 55%, transparent);
padding: var(--sp-1) var(--sp-2);
transition: background var(--dur-fast) var(--ease-out);
}
.st-tool-group:hover {
background: color-mix(in oklab, var(--surface-2) 70%, transparent);
}
.st-tool-group-head {
appearance: none;
width: 100%;
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 0;
min-width: 0;
transition: color var(--dur-fast) var(--ease-out);
}
.st-tool-group-head:hover {
background: var(--surface-2);
}
.st-tool-group-glyph {
color: var(--faint);
transition: transform var(--dur-fast) var(--ease-out);
}
.st-tool-group-head[aria-expanded="true"] .st-tool-group-glyph {
transform: rotate(90deg);
}
.st-tool-group-title {
flex: 0 0 auto;
font-weight: 500;
}
.st-tool-group-body {
padding-top: 0;
}
.st-tool-group-body .st-disclosure-inner {
display: flex;
flex-direction: column;
padding-top: var(--sp-1);
}
.st-tool-group-body .st-disclosure-inner > .st-row-tool {
padding: 0;
}
/* inside a group, tool cards lose their border and background —
the group container already provides the visual boundary. */
.st-tool-group-body .st-tool {
border: 0;
}
.st-tool-group-body .st-tool-head {
padding: var(--sp-1) var(--sp-2);
border-radius: var(--r-1);
}
/* intermediate agent text inside a tool group — subdued so the final
answer outside the group stays the prominent one. */
.st-tool-group-body .st-row-agent {
color: var(--muted);
font-size: var(--fs-12);
padding: var(--sp-1) var(--sp-2);
}
.st-tool-group-body .st-row-agent .st-body > :first-child {
margin-top: 0;
}
.st-tool-group-body .st-row-agent .st-body > :last-child {
margin-bottom: 0;
}
.st-tool-group-body .st-row-thought {
padding: var(--sp-1) var(--sp-2);
}
.st-tool-group-body[hidden] {
display: none;
}
/* Live transcript rows resolve into the stream through a short occlusion,
matching data becoming concrete without replaying on token patches. */
.st-row-enter {
animation: structured-row-enter var(--dur) var(--ease-out) both;
}
@keyframes structured-row-enter {
from {
opacity: 0.2;
transform: translateY(3px);
clip-path: inset(0 0 18% 0);
filter: blur(1px);
}
}
.st-diff-block {
display: flex;
flex-direction: column;
gap: var(--sp-1);
}
.st-diff-path {
font-size: var(--fs-11);
color: var(--muted);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* the diff gets its own horizontal scroller, independent of the page */
.st-diff {
font-size: var(--fs-12);
overflow-x: auto;
border: 1px solid var(--border);
border-radius: var(--r-1);
}
.st-diff pre {
margin: 0;
}
.st-ln {
display: block;
white-space: pre;
padding: 0 var(--sp-3);
}
.st-ln-add {
background: color-mix(in oklab, var(--live) 14%, transparent);
}
.st-ln-del {
background: var(--danger-tint);
}
.st-ln-ctx {
color: var(--muted);
}
.st-ln-hunk {
color: var(--faint);
background: var(--surface-2);
}
.st-ln-eof {
color: var(--faint);
font-style: italic;
}
/* the one lifted, card-like element on the surface: the only thing the
operator owes a decision on */
.st-approval {
border: 1px solid var(--warn);
border-radius: var(--r-2);
background: color-mix(in oklab, var(--warn) 12%, var(--surface));
box-shadow: var(--shadow-1);
padding: var(--sp-3);
display: flex;
flex-direction: column;
gap: var(--sp-2);
min-width: 0;
}
.st-approval-title {
color: var(--warn);
font-size: var(--fs-11);
letter-spacing: 0.1em;
text-transform: uppercase;
display: flex;
align-items: center;
gap: var(--sp-2);
}
.st-approval-what {
color: var(--ink);
}
.st-approval-path {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r-1);
padding: var(--sp-2);
font-size: var(--fs-12);
overflow-x: auto;
white-space: pre;
}
.st-approval-acts {
display: flex;
gap: var(--sp-2);
flex-wrap: wrap;
}
.st-approval-resolved {
font-size: var(--fs-11);
color: var(--faint);
}
/* compact resolved approval inside a tool group — same muted treatment
as intermediate agent text so the audit trail is preserved without
drawing attention to itself. */
.st-approval-compact {
color: var(--muted);
font-size: var(--fs-12);
padding: var(--sp-1) var(--sp-2);
font-style: italic;
}
.act {
appearance: none;
font: inherit;
font-size: var(--fs-12);
height: 26px;
padding: 0 var(--sp-3);
border-radius: var(--r-2);
border: 1px solid var(--border-strong);
background: var(--surface);
color: var(--ink);
cursor: pointer;
transition:
transform var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
}
.act:hover {
background: var(--surface-3);
}
.act:active {
transform: translateY(1px);
}
.act:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.act.primary {
background: var(--accent);
color: var(--accent-ink);
border-color: var(--accent);
font-weight: 500;
}
.act.deny {
color: var(--danger);
border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}
.act.deny:hover {
background: var(--danger-tint);
border-color: var(--danger);
}
/* The composer is a sibling of the scrolling transcript, not part of it: it
stays reachable at the pane edge without changing transcript scroll math.
It is a flush band at the foot of the pane rather than a floating card: its
own `--surface` fill is what separates it from the transcript, over a
hairline that doubles as the focus indicator. Inner metrics line the draft
up with the transcript's own 12px gutter. */
.structured-composer {
position: relative;
flex: 0 0 auto;
padding: var(--sp-1) var(--sp-1) max(var(--sp-1), env(safe-area-inset-bottom));
border-top: 1px solid var(--border);
background: var(--surface);
display: flex;
align-items: flex-end;
gap: var(--sp-1);
transition: border-color var(--dur-fast) var(--ease-out);
}
.structured-queue {
position: absolute;
right: var(--sp-2);
bottom: calc(100% + var(--sp-1));
padding: 3px var(--sp-2);
border: 1px solid var(--border);
border-radius: var(--r-pill);
background: var(--surface);
color: var(--muted);
font-size: var(--fs-11);
box-shadow: var(--shadow-1);
}
.structured-composer:focus-within {
border-top-color: var(--border);
}
.structured-input-wrap {
position: relative;
flex: 1 1 auto;
min-width: 0;
}
.structured-attachments {
display: flex;
flex-wrap: wrap;
gap: var(--sp-1);
padding: var(--sp-1);
}
.structured-attachments[hidden] { display: none; }
.structured-attachment {
display: inline-flex;
align-items: center;
min-width: 0;
max-width: 100%;
padding-left: var(--sp-2);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-1);
font-size: var(--fs-11);
}
.structured-attachment > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.structured-attachment button {
flex-shrink: 0;
min-width: var(--tap);
min-height: var(--tap);
border: 0;
background: transparent;
color: var(--muted);
}
.structured-attachment button:hover { color: var(--ink); }
.structured-attachment button:disabled { opacity: 0.5; }
.structured-input {
display: block;
width: 100%;
min-width: 0;
max-height: 160px;
min-height: 40px;
resize: none;
overflow-y: hidden;
padding: 10px var(--sp-2);
border: 0;
border-radius: var(--r-2);
background: transparent;
color: var(--ink);
font-size: var(--fs-13);
line-height: 20px;
}
.structured-input:focus-visible {
outline: none;
}
.structured-composer .structured-send,
.structured-composer .structured-cancel {
height: 40px;
min-height: 40px;
border-radius: var(--r-2);
font-family: var(--font-sans);
}
.structured-composer .structured-send {
gap: 6px;
padding: 0 var(--sp-3);
border-color: var(--accent);
background: var(--accent);
color: var(--accent-ink);
font-weight: 650;
box-shadow: inset 0 1px 0 color-mix(in oklab, white 24%, transparent);
}
.structured-composer .structured-send::before,
.structured-composer .structured-send::after,
.structured-composer .structured-cancel::before,
.structured-composer .structured-cancel::after {
content: none;
}
.structured-composer .structured-send svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
transition: transform var(--dur-fast) var(--ease-out);
}
.structured-composer .structured-send:hover {
background: var(--accent-strong);
border-color: var(--accent-strong);
color: var(--accent-ink);
}
.structured-composer .structured-send:hover svg {
transform: translateX(2px);
}
.structured-composer .structured-cancel {
position: relative;
width: 40px;
padding: 0;
border-color: color-mix(in oklab, var(--danger) 35%, var(--border));
color: var(--danger);
}
.structured-composer .structured-cancel:hover {
background: var(--danger-tint);
}
.structured-cancel-stop {
width: 16px;
height: 16px;
}
.structured-cancel-spinner {
position: absolute;
inset: 4px;
width: auto;
height: auto;
animation: cancel-spin 0.8s linear infinite;
}
@keyframes cancel-spin {
to { transform: rotate(360deg); }
}
@media (max-width: 520px) {
/* One row cannot hold a draft plus three controls at phone width — the
draft gets a line of its own and the controls drop below it. */
.structured-composer {
flex-wrap: wrap;
justify-content: flex-end;
padding-left: var(--sp-2);
padding-right: var(--sp-2);
}
/* `order` keeps the draft on the first line: without it the attach button,
which precedes it in the DOM, wraps onto a line of its own. */
.structured-input-wrap {
flex: 1 1 100%;
order: -1;
}
.structured-composer .structured-config {
margin-right: auto;
}
.structured-config-menu {
left: 0;
right: auto;
}
}
.structured-command-menu {
position: absolute;
z-index: var(--z-sticky);
right: 0;
bottom: calc(100% + var(--sp-2));
left: 0;
max-height: min(42vh, 320px);
overflow-y: auto;
border: 1px solid var(--border-strong);
border-radius: var(--r-2);
background: var(--surface);
box-shadow: var(--shadow-2);
}
.structured-command-menu[hidden] {
display: none;
}
.structured-command-option {
min-height: var(--tap);
padding: var(--sp-2) var(--sp-3);
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.structured-command-option:last-child {
border-bottom: 0;
}
.structured-command-option:hover,
.structured-command-option[aria-selected="true"] {
background: var(--accent-tint);
}
.structured-command-title {
display: flex;
align-items: baseline;
gap: var(--sp-2);
min-width: 0;
}
.structured-command-name {
flex: 0 0 auto;
color: var(--ink);
font-weight: 600;
}
.structured-command-hint,
.structured-command-description {
overflow: hidden;
color: var(--muted);
font-size: var(--fs-11);
text-overflow: ellipsis;
white-space: nowrap;
}
.structured-command-hint {
min-width: 0;
}
.structured-command-description {
margin-top: var(--sp-1);
}
.structured-input:focus {
outline: none;
box-shadow: none;
}
.structured-input:disabled {
color: var(--faint);
background: transparent;
}
/* `.btn` sets `display: inline-flex`, which outranks the UA sheet's
`[hidden]` — restate it explicitly (same pitfall as `.st-tool-out`). */
.structured-cancel[hidden] {
display: none;
}
.structured-send[hidden] {
display: none;
}
.structured-error {
margin: 0 var(--sp-3);
padding: var(--sp-2) var(--sp-3);
background: var(--danger-tint);
color: var(--danger);
font-size: var(--fs-12);
border-radius: var(--r-2);
}
@media (prefers-reduced-motion: reduce) {
.act,
.st-copyable,
.st-copyable::after,
.st-disclosure,
.st-tool-head,
.st-tool-group-glyph {
transition-duration: 0.01ms;
}
.st-row-enter {
animation: none;
}
.structured-activity-pulse {
animation: none;
}
}
/* ---------- modal ---------- */
.modal-backdrop {
position: fixed;
inset: 0;
background: oklch(0.12 0.01 256 / 0.6);
backdrop-filter: blur(3px);
display: flex;
align-items: flex-end;
justify-content: center;
padding: 0;
z-index: var(--z-modal);
animation: fade var(--dur) var(--ease-out);
}
@media (min-width: 600px) {
.modal-backdrop {
align-items: center;
padding: var(--sp-6);
}
}
.modal {
background: var(--surface);
border-radius: var(--r-3) var(--r-3) 0 0;
border: 1px solid var(--border);
width: 100%;
max-width: 560px;
max-height: 92vh;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-2);
animation: sheet-up var(--dur-slow) var(--ease-out);
}
@media (min-width: 600px) {
.modal {
border-radius: var(--r-3);
animation: pop var(--dur) var(--ease-out);
}
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--sp-3);
border-bottom: 1px solid var(--border);
}
.modal-header h2 {
margin: 0;
font-size: var(--fs-15);
font-weight: 600;
display: inline-flex;
align-items: center;
gap: var(--sp-2);
}
.modal-header h2::before {
content: "+";
color: var(--accent);
font-weight: 700;
}
/* The form is the modal's scrolling body: it must be allowed to shrink below
its content height for `max-height` on the dialog to bite. */
.modal .spawn-form {
min-height: 0;
}
/* Mobile: the spawn form is the tallest dialog we have, so it takes the whole
screen and lays out as one flex column — header, scrolling body, actions
pinned to the bottom edge. Below the fold is where the Submit button used to
hide. */
@media (max-width: 599px) {
.modal.spawn-modal {
height: 100dvh;
max-height: none;
border: 0;
border-radius: 0;
}
.spawn-modal .spawn-form {
flex: 1 1 auto;
padding-bottom: 0;
}
/* One scroll box, not four: the pickers' own caps stack scrollable regions
inside a screen that is already short. Let them grow and let the form
scroll. */
.spawn-modal .picker,
.spawn-modal .picker.projects,
.spawn-modal .picker.wt-choices {
max-height: none;
overflow-y: visible;
}
.spawn-modal .form-actions {
margin-top: auto;
padding-bottom: calc(var(--sp-2) + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -8px 12px -8px oklch(0 0 0 / 0.5);
}
.spawn-modal .form-actions .btn {
flex: 1 1 0;
justify-content: center;
min-height: 44px;
}
}
/* ---------- forms ---------- */
.spawn-form {
width: 100%;
max-width: 60ch;
margin: 0 auto;
padding: var(--sp-3);
overflow: auto;
display: flex;
flex-direction: column;
gap: var(--sp-2);
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.spawn-form label {
display: flex;
flex-direction: column;
gap: 5px;
font-size: var(--fs-12);
color: var(--muted);
letter-spacing: 0.01em;
}
.spawn-form label.row-inline {
flex-direction: row;
align-items: center;
gap: var(--sp-2);
color: var(--ink);
font-size: var(--fs-14);
cursor: pointer;
}
.spawn-form input[type="text"],
.spawn-form input[type="search"],
.spawn-form select {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-2);
padding: 10px var(--sp-3);
color: var(--ink);
font-family: var(--font-mono);
font-size: var(--fs-15);
width: 100%;
transition:
border-color var(--dur-fast) var(--ease-out),
box-shadow var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
}
.spawn-form input[type="text"]:hover,
.spawn-form select:hover {
border-color: var(--border-strong);
}
.spawn-form input[type="text"]:focus,
.spawn-form input[type="search"]:focus,
.spawn-form select:focus {
outline: none;
border-color: var(--accent);
background: var(--surface);
box-shadow: 0 0 0 3px var(--accent-tint);
}
.spawn-form input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--accent);
flex: 0 0 auto;
}
.advanced {
display: flex;
flex-direction: column;
}
.advanced .wt-toggle {
border-top: 1px solid var(--border);
border-radius: 0;
padding-top: var(--sp-2);
padding-bottom: var(--sp-2);
}
.wt-summary {
color: var(--muted);
font-size: var(--fs-12);
}
.wt-collapsible {
display: flex;
flex-direction: column;
gap: var(--sp-3);
padding: var(--sp-2) 0 var(--sp-1);
}
.wt-collapsible[hidden] {
display: none;
}
/* Worktree sub-group: left accent border when expanded, visually nested. */
.adv-group {
display: flex;
flex-direction: column;
gap: var(--sp-3);
padding-left: var(--sp-3);
border-left: 2px solid var(--border);
margin-top: var(--sp-1);
}
.adv-group:has(input[name="worktree_enabled"]:checked) {
border-left-color: var(--accent);
}
.form-error {
color: var(--danger);
margin: 0;
font-size: var(--fs-13);
min-height: 1.2em;
}
.form-error:not(:empty) {
padding: var(--sp-2) var(--sp-3);
background: var(--danger-tint);
border-radius: var(--r-2);
}
/* ---------- spawn form: tiles + recents picker (#27) ---------- */
.spawn-form .field {
display: flex;
flex-direction: column;
gap: var(--sp-2);
}
.field-label {
font-size: var(--fs-13);
color: var(--ink);
}
/* Harness cards: the first decision in the spawn form — which tool runs. */
.harness-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: var(--sp-2);
}
.harness-card {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-3) var(--sp-2);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-2);
color: var(--ink);
font-family: inherit;
cursor: pointer;
text-align: center;
transition:
border-color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
}
.harness-card:hover {
border-color: var(--border-strong);
background: var(--surface-3);
}
.harness-card.is-active {
border-color: var(--accent);
background: var(--accent-tint);
}
.harness-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
color: var(--muted);
}
.harness-card.is-active .harness-icon {
color: var(--ink);
}
.harness-icon .tool-icon {
display: block;
}
.harness-label {
font-size: var(--fs-13);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
/* The "Custom…" card is an escape hatch, not a peer tool — dashed border
and subdued color separate it from the named harnesses. */
.harness-custom {
border-style: dashed;
color: var(--muted);
}
.harness-custom .harness-label {
font-weight: 500;
}
/* Working-directory picker (#95). Two steps: a grid of project boxes, then the
picked project's worktree choices. Stale beyond 7 days dims but stays
selectable. */
.picker {
display: flex;
flex-direction: column;
gap: var(--sp-1);
max-height: 220px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.project-filter {
width: 100%;
}
/* `display` from `.field`/`.raw-wrap` outranks the UA sheet's `[hidden]`, so
step two and the raw-cwd hatch stayed visible while hidden. */
.wt-step[hidden],
.raw-wrap[hidden] {
display: none;
}
.picker.projects {
display: grid;
grid-template-columns: repeat(2, 1fr);
align-content: start;
gap: var(--sp-2);
max-height: 300px;
padding: 1px;
}
.project-box {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
padding: var(--sp-2) var(--sp-3);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-2);
color: var(--ink);
font-family: inherit;
text-align: left;
cursor: pointer;
transition:
border-color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
}
.project-box:hover {
border-color: var(--border-strong);
}
.project-box.is-active {
border-color: var(--accent);
background: var(--accent-tint);
}
.project-box.is-stale {
opacity: 0.55;
}
.project-name {
font-size: var(--fs-13);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The whole path, never abbreviated — it is what tells two projects sharing a
basename apart. Wraps rather than truncating so none of it is hidden. */
.project-path {
font-family: var(--font-mono);
font-size: var(--fs-12);
line-height: 1.35;
color: var(--muted);
overflow-wrap: anywhere;
}
.project-other {
align-self: flex-start;
padding: 4px 0;
min-height: 30px;
background: none;
border: 0;
color: var(--muted);
font-family: inherit;
font-size: var(--fs-13);
font-style: italic;
text-align: left;
cursor: pointer;
}
.project-other:hover {
color: var(--ink);
}
/* Step two: "+ new" (create one) or an existing worktree (run in it). */
.picker.wt-choices {
max-height: 200px;
}
.wt-choice {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--sp-3);
min-height: 32px;
padding: 6px var(--sp-3);
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--r-2);
color: var(--ink);
font-family: inherit;
text-align: left;
cursor: pointer;
transition:
border-color var(--dur-fast) var(--ease-out),
background var(--dur-fast) var(--ease-out);
}
.wt-choice:hover {
border-color: var(--border-strong);
}
.wt-choice.is-active {
border-color: var(--accent);
background: var(--accent-tint);
}
.wt-choice-name {
flex: 0 0 auto;
font-size: var(--fs-13);
font-weight: 600;
white-space: nowrap;
}
.wt-choice-new .wt-choice-name {
font-family: var(--font-mono);
color: var(--accent);
}
.wt-choice-sub {
min-width: 0;
font-family: var(--font-mono);
font-size: var(--fs-12);
line-height: 1.35;
color: var(--muted);
text-align: right;
overflow-wrap: anywhere;
}
.picker-empty {
margin: 0;
padding: 6px var(--sp-3);
color: var(--faint);
font-size: var(--fs-13);
}
.picker-loading {
margin: 0;
padding: 6px var(--sp-3);
color: var(--faint);
font-size: var(--fs-13);
}
.raw-wrap {
display: flex;
}
.raw-input {
width: 100%;
}
/* Worktree-by-branch: inferred repo line + inline hint. */
.wt-hint {
margin: 0;
font-size: var(--fs-12);
color: var(--muted);
}
.wt-body {
display: flex;
flex-direction: column;
gap: var(--sp-3);
}
.wt-repo {
margin: 0;
font-family: var(--font-mono);
font-size: var(--fs-12);
color: var(--muted);
}
/* New-vs-existing branch source (#61). */
.wt-mode {
display: flex;
gap: var(--sp-4);
}
.branch-select {
width: 100%;
}
/* Generated-name field with a reroll button. */
.name-row {
display: flex;
gap: var(--sp-2);
align-items: center;
}
.name-row input {
flex: 1 1 auto;
}
.form-actions {
display: flex;
gap: var(--sp-2);
justify-content: flex-end;
margin-top: var(--sp-2);
position: sticky;
bottom: 0;
padding: var(--sp-2) 0;
background: var(--surface);
z-index: 1;
}
.reroll {
flex: 0 0 auto;
}
/* ---------- directory autocomplete (NAP-329) ---------- */
.dir-ac {
position: fixed;
z-index: var(--z-toast); /* above the modal it completes within */
margin: 0;
padding: var(--sp-1);
list-style: none;
max-height: 280px;
overflow-y: auto;
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--r-2);
box-shadow: var(--shadow-2);
scrollbar-width: thin;
scrollbar-color: var(--border-strong) transparent;
}
.dir-ac-item {
padding: 6px var(--sp-2);
border-radius: var(--r-1);
color: var(--ink);
font-family: var(--font-mono);
font-size: var(--fs-14);
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dir-ac-item:hover {
background: var(--surface-2);
}
.dir-ac-item.is-active {
background: var(--accent-tint);
color: var(--ink);
}
.dir-ac-more {
padding: 6px var(--sp-2);
color: var(--faint);
font-size: var(--fs-12);
font-style: italic;
}
/* ---------- not-found ---------- */
.not-found {
margin: auto;
padding: var(--sp-5) var(--sp-4);
display: flex;
flex-direction: column;
gap: var(--sp-3);
align-items: center;
text-align: center;
}
.not-found h1 {
margin: 0;
font-size: var(--fs-20);
}
.not-found p {
margin: 0;
color: var(--muted);
}
/* ---------- theme toggle ---------- */
.theme-toggle {
position: relative;
overflow: hidden;
}
.theme-toggle .sun,
.theme-toggle .moon {
position: absolute;
inset: 0;
display: grid;
place-items: center;
transition:
transform var(--dur) var(--ease-out),
opacity var(--dur) var(--ease-out);
}
.theme-toggle .moon {
transform: translateY(0);
opacity: 1;
}
.theme-toggle .sun {
transform: translateY(120%);
opacity: 0;
}
:root[data-theme="light"] .theme-toggle .moon {
transform: translateY(-120%);
opacity: 0;
}
:root[data-theme="light"] .theme-toggle .sun {
transform: translateY(0);
opacity: 1;
}
@media (prefers-color-scheme: light) {
:root:not([data-theme]) .theme-toggle .moon {
transform: translateY(-120%);
opacity: 0;
}
:root:not([data-theme]) .theme-toggle .sun {
transform: translateY(0);
opacity: 1;
}
}
/* ---------- keyframes ---------- */
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 0.6;
}
70%,
100% {
transform: scale(2.4);
opacity: 0;
}
}
@keyframes fade {
from {
opacity: 0;
}
}
@keyframes sheet-up {
from {
transform: translateY(16px);
opacity: 0;
}
}
@keyframes pop {
from {
transform: scale(0.98);
opacity: 0;
}
}
@keyframes blink {
0%,
50% {
opacity: 1;
}
50.01%,
100% {
opacity: 0;
}
}
@keyframes rail-flash {
0%,
100% {
opacity: 1;
}
15% {
opacity: 0.25;
}
30% {
opacity: 1;
}
45% {
opacity: 0.25;
}
60% {
opacity: 1;
}
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
.brand h1 {
font-size: var(--fs-15);
}
.catalog-actions {
flex: 1 1 100%;
flex-wrap: wrap;
justify-content: stretch;
}
.search {
flex: 1 1 100%;
max-width: none;
order: -1;
}
.tree-leaf .leaf-actions {
opacity: 1;
}
}
/* Mobile: no room for a split, so show one pane at a time. The catalog route
shows the session list full-width; opening a session swaps to the terminal
(its header's ← returns to the list). The connection still lives in the pool,
so returning to it is instant. */
@media (max-width: 859px) {
.app-shell[data-route="catalog"] > .content {
display: none;
}
.app-shell:not([data-route="catalog"]) > .sidebar {
display: none;
}
.sidebar {
border-right: 0;
}
}
/* ---------- reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
.brand-dot,
[data-status="running"] .leaf-status::before,
.tally-live::before,
.activity-rail {
animation: none;
}
}
/* ---------- debug HUD (#20) ---------- */
.debug-hud {
position: fixed;
right: var(--sp-3);
bottom: var(--sp-3);
z-index: var(--z-toast);
min-width: 180px;
max-width: 280px;
padding: var(--sp-2) var(--sp-3);
background: color-mix(in oklab, var(--surface) 88%, transparent);
border: 1px solid var(--border-strong);
border-radius: var(--r-2);
box-shadow: var(--shadow-2);
font-family: var(--font-mono);
font-size: var(--fs-12);
line-height: 1.5;
/* Diagnostics only — never intercept clicks on the terminal beneath it. */
pointer-events: none;
user-select: none;
}
.hud-head {
color: var(--ink);
font-weight: 600;
margin-bottom: 2px;
border-bottom: 1px solid var(--border);
padding-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hud-row {
display: flex;
justify-content: space-between;
gap: var(--sp-3);
}
.hud-k {
color: var(--muted);
}
.hud-v {
color: var(--ink);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hud-v[data-activity="Active"] {
color: var(--accent);
}
.hud-v[data-activity="AwaitingInput"] {
color: var(--warn);
}
.hud-v[data-activity="Idle"] {
color: var(--muted);
}
/* =========================================================================
multi-view (#30 splits, #31 tabs, #33 highlights)
Tabs of nested split trees; each session leaf hosts a live .attach. The
focused pane reads at full strength, the rest recede — and the sidebar row
for the focused session matches it (loud), open-elsewhere rows stay quiet.
========================================================================= */
.mv {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
/* Mobile: the tab bar is a second navigation surface — collapse it (a floating
☰ inside the pane keeps the sidebar drawer reachable). */
.mv-mobile-solo > .mv-tabs,
.mv-solo-tab > .mv-tabs {
display: none;
}
/* tab bar */
.mv-tabs {
display: flex;
align-items: stretch;
gap: 2px;
height: var(--bar-h);
padding: 0 var(--sp-2);
background: var(--surface);
border-bottom: 1px solid var(--border);
overflow-x: auto;
scrollbar-width: none;
flex: none;
}
.mv-tab {
display: inline-flex;
align-items: center;
gap: var(--sp-2);
padding: 0 var(--sp-3);
align-self: center;
height: calc(var(--bar-h) - 8px);
border-radius: var(--r-1);
font: 500 var(--fs-12) / 1 var(--font-mono);
color: var(--muted);
background: transparent;
border: 1px solid transparent;
cursor: pointer;
white-space: nowrap;
}
.mv-tab:hover {
background: var(--surface-2);
color: var(--ink);
}
.mv-tab.is-active {
background: var(--surface-3);
color: var(--ink);
border-color: var(--border);
}
.mv-tab-close {
opacity: 0.5;
font-size: var(--fs-13);
line-height: 1;
}
.mv-tab-close:hover {
opacity: 1;
color: var(--danger);
}
.mv-tab-add {
align-self: center;
width: 24px;
height: 24px;
border-radius: var(--r-1);
color: var(--muted);
font-size: var(--fs-15);
background: transparent;
border: none;
cursor: pointer;
}
.mv-tab-add:hover {
background: var(--surface-2);
color: var(--ink);
}
/* Preview (docs/PREVIEW.md): the content pane is showing a session the
operator only highlighted, so it reads as provisional — the preview hue on
the pane's titlebar, a dashed tab, a dashed inset ring on the canvas — and
reverts when the highlight moves. The hue is deliberately not the accent:
the accent means "this is what you picked", which a preview is not. */
.mv-tab.is-preview {
border-style: dashed;
border-color: var(--preview-ring);
color: var(--preview);
font-style: italic;
}
.mv.mv-previewing .mv-canvas::before {
content: "preview";
position: absolute;
top: var(--sp-2);
right: var(--sp-3);
z-index: var(--z-sticky);
padding: 1px var(--sp-2);
border-radius: var(--r-1);
font: 500 var(--fs-11) / 1.5 var(--font-mono);
letter-spacing: 0.04em;
color: var(--preview-ink);
background: var(--preview);
opacity: 0.85;
pointer-events: none;
}
.mv.mv-previewing .mv-canvas::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border: 1px dashed var(--preview-ring);
border-radius: var(--r-1);
z-index: var(--z-sticky);
}
/* canvas + welcome */
.mv-canvas {
position: relative;
flex: 1;
min-height: 0;
overflow: hidden;
background: var(--bg);
}
.mv-canvas > .mv-split,
.mv-canvas > .mv-pane {
position: absolute;
inset: 0;
}
.mv-canvas > .content-welcome {
position: absolute;
inset: 0;
margin: auto;
height: max-content;
}
/* splits + dividers */
.mv-split {
display: flex;
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
}
.mv-split.row {
flex-direction: row;
}
.mv-split.col {
flex-direction: column;
}
.mv-split > .mv-slot {
min-width: 0;
min-height: 0;
display: flex;
}
.mv-split > .mv-slot > * {
flex: 1;
min-width: 0;
min-height: 0;
}
.mv-divider {
flex: none;
position: relative;
z-index: 2;
}
.mv-split.row > .mv-divider {
width: 6px;
cursor: col-resize;
}
.mv-split.col > .mv-divider {
height: 6px;
cursor: row-resize;
}
.mv-divider::after {
content: "";
position: absolute;
inset: 0;
margin: auto;
background: var(--border);
}
.mv-split.row > .mv-divider::after {
width: 1px;
height: 100%;
}
.mv-split.col > .mv-divider::after {
height: 1px;
width: 100%;
}
.mv-divider:hover::after,
.mv-divider.is-dragging::after {
background: var(--accent);
}
/* a single pane = wrapper around a live .attach (or an empty placeholder) */
.mv-pane {
position: relative;
display: flex;
min-width: 0;
min-height: 0;
border: 1px solid var(--border);
border-radius: var(--r-2);
overflow: hidden;
background: var(--term-bg);
}
.mv-pane-host {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
}
.mv-pane-host > .attach {
flex: 1;
min-width: 0;
min-height: 0;
}
/* focused pane: titlebar tints instead of a ring, full strength */
.mv-pane.is-focused .attach-header {
background: color-mix(in oklab, var(--accent) 14%, var(--surface));
}
/* unfocused panes recede so the eye finds the live one */
.mv-pane:not(.is-focused) {
border-color: color-mix(in oklab, var(--border) 70%, transparent);
}
.mv-pane:not(.is-focused) .attach-body {
opacity: 0.45;
}
.mv-pane:not(.is-focused) .attach-header {
opacity: 0.7;
}
/* The previewed pane's titlebar carries the preview hue, so the chip is not the
only thing saying the pane is provisional — and so it is never mistaken for
the accent tint of the pane the operator actually focused. It keeps full
strength even though a preview never takes focus. */
.mv-pane.is-preview .attach-header,
.mv-pane.is-preview.is-focused .attach-header {
background: color-mix(in oklab, var(--preview) 34%, var(--surface));
opacity: 1;
}
/* A pane with no session yet: a quiet hint rather than blank terminal black. */
.mv-pane-empty {
flex: 1;
display: grid;
place-items: center;
padding: var(--sp-3);
text-align: center;
color: var(--faint);
font-size: var(--fs-13);
}
/* pane tools cluster (disconnect / split / close), top-right corner. The attach
header's own disconnect button is hidden — ⏻ lives here as the leftmost tool. */
.mv-pane-tools {
position: absolute;
top: 6px;
right: 4px;
z-index: 4;
display: flex;
gap: 2px;
opacity: 0;
pointer-events: none;
transition: opacity var(--dur-fast) var(--ease-out);
}
.mv-pane .attach-header .disconnect {
display: none;
}
.mv-tool-off:hover {
background: var(--danger-tint);
color: var(--danger);
}
.mv-pane:hover .mv-pane-tools {
opacity: 1;
pointer-events: auto;
}
/* The tools share the header's right corner with the connection-state label, so
hide that label while hovering (tools showing) — no overlap. */
.mv-pane:hover .attach-state {
visibility: hidden;
}
/* "connected" is noise — a streaming terminal already shows it's connected. Only
the transient/meaningful states surface (connecting/error); a closed session
auto-closes its pane, so "closed" is rarely seen. */
.attach-state[data-state="open"] {
display: none;
}
.mv-tool {
display: inline-grid;
place-items: center;
width: 22px;
height: 22px;
border-radius: var(--r-1);
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--muted);
font-size: var(--fs-12);
cursor: pointer;
}
.mv-tool:hover {
background: var(--surface-3);
color: var(--ink);
}
/* drag-to-dock (issue #32): a cursor-following ghost + the target pane's drop
zone highlight. */
.mv-drag-ghost {
position: fixed;
z-index: var(--z-toast);
pointer-events: none;
padding: 2px 10px;
border-radius: var(--r-2);
background: var(--surface-3);
border: 1px solid var(--accent);
color: var(--ink);
font: 600 var(--fs-12) / 1.4 var(--font-mono);
box-shadow: var(--shadow-2);
}
.mv-dock-overlay {
position: absolute;
inset: 0;
z-index: 5;
pointer-events: none;
}
.mv-dock-zone {
background: color-mix(in oklab, var(--accent) 30%, transparent);
border: 1px solid var(--accent);
border-radius: var(--r-1);
}
/* =========================================================================
multi-view mobile (#35): below 860px only the focused pane is shown — no tab
bar, no pane switcher; the sidebar drawer (☰) is the navigation.
========================================================================= */
.mv-backdrop {
display: none;
}
.mv-mobile {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
gap: 0;
}
/* One pane fills the screen: no border, radius, or focus ring to box it in. */
.mv-mobile > .mv-pane,
.mv-mobile > .mv-pane.is-focused {
border: none;
border-radius: 0;
box-shadow: none;
}
.mv-mobile > .mv-pane {
flex: 1;
min-height: 0;
}
@media (max-width: 859px) {
/* sidebar → off-canvas drawer (override the route-based show/hide) */
.app-shell > .sidebar {
display: flex !important;
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: min(82vw, 320px);
z-index: var(--z-modal);
transform: translateX(-100%);
transition: transform var(--dur) var(--ease-out);
box-shadow: var(--shadow-2);
border-right: 1px solid var(--border);
}
.app-shell.drawer-open > .sidebar {
transform: none;
}
/* content always visible (the layout/projection); drawer floats over it */
.app-shell[data-route="catalog"] > .content,
.app-shell > .content {
display: flex;
}
.app-shell.drawer-open > .mv-backdrop {
display: block;
position: fixed;
inset: 0;
z-index: var(--z-backdrop);
background: oklch(0 0 0 / 0.5);
}
}