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>
4662 lines
104 KiB
CSS
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);
|
|
}
|
|
}
|