/* OpenResearch design system, mirrored from openresearch.sh www/src/styles.css.
Light theme by default, dark via prefers-color-scheme. Quiet idiom:
hairline borders, soft radii, flat surfaces that tint on hover. */
:root {
--base: #ffffff;
--canvas: #faf8f4;
--panel: #f3f0ea;
--surface: #faf7f2;
--surface-bright: #fdfbfb;
--highlight: #fdf3f1;
--text: #1d1b1a;
--subtext: #737373;
--muted: #a1a1a1;
--primary: #9a2036;
--primary-subtle: #f7e9ec;
--secondary: #4a90e2;
--error: #e2a100;
--border: #d4d4d4;
--border-variant: #e5e5e5;
--accent-orange: #da642c;
--accent-red: #d94654;
--accent-teal: #209a84;
--accent-blue: #3a8dff;
--accent-pink: #ff2b90;
--accent-amber: #da9100;
--accent-green: #5eb64c;
--accent-purple: #9c5cff;
--accent-green-subtle: #e7f4e5;
--accent-amber-subtle: #fff3e1;
--accent-teal-subtle: #e1f3f0;
--accent-red-subtle: #fbe9ea;
--accent-blue-subtle: #e5f0ff;
--accent-purple-subtle: #f1e8ff;
--dots-muted: #e3ded5;
--dots-strong: #bdb6a8;
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, sans-serif;
/* Type scale. Sizes are px steps; anything smaller than --fs-2xs is banned. */
--fs-2xs: 10px;
--fs-xs: 11px;
--fs-sm: 12px;
--fs-md: 13px;
--fs-base: 14px;
--fs-lg: 15px;
--fs-xl: 16px;
--fs-2xl: 18px;
--fs-3xl: 22px;
--fs-4xl: 28px;
--fs-5xl: 34px;
/* Weight scale. Display weight is for hero headings only; the brand
wordmark keeps a literal 800. */
--fw-regular: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-display: 700;
/* Radius scale. True circles stay literal 50%. */
--radius-xs: 4px;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 10px;
--radius-xl: 12px;
--radius-full: 999px;
/* xterm surface — deliberately dark in both themes; keep in sync with the
terminal theme in LogTerminal.tsx. */
--term-bg: #1a1a1a;
/* Readable column cap shared by the chat header/thread/composer (which must
stay in lockstep or the chat column misaligns) and the settings/doc views. */
--readable-col: 760px;
/* Legacy aliases (pre-restyle class/inline usage). New code uses the tokens above. */
--bg: var(--base);
--bg-canvas: var(--canvas);
--surface-2: var(--panel);
--border-strong: var(--border);
--accent: var(--primary);
--teal: var(--accent-teal);
--green: var(--accent-green);
--red: var(--accent-red);
--amber: var(--accent-amber);
--purple: var(--accent-purple);
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root {
--base: #0e0c0c;
--canvas: #141110;
--panel: #221f1e;
--surface: #1d1b1a;
--surface-bright: #130f0f;
--highlight: #393433;
--text: #e6e1e0;
--subtext: #a68e8b;
--muted: #737373;
--primary: #ffb3ad;
--primary-subtle: #33191b;
--secondary: #3e7fcf;
--error: #ffb951;
--border: #525252;
--border-variant: #404040;
--accent-amber: #e67e22;
--accent-green-subtle: #1c2b18;
--accent-amber-subtle: #33260f;
--accent-teal-subtle: #12332d;
--accent-red-subtle: #331418;
--accent-blue-subtle: #10233a;
--accent-purple-subtle: #251933;
--dots-muted: #2a2523;
--dots-strong: #555555;
color-scheme: dark;
}
}
* {
box-sizing: border-box;
}
html,
body,
#root {
height: 100%;
margin: 0;
}
body {
background: var(--base);
color: var(--text);
font-family: var(--sans);
font-size: var(--fs-base);
line-height: 1.45;
overflow: hidden;
}
::selection {
background: var(--highlight);
}
button {
font: inherit;
color: inherit;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
input,
textarea,
select {
font: inherit;
color: var(--text);
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 6px 10px;
outline: none;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--text);
}
input::placeholder,
textarea::placeholder {
color: var(--muted);
opacity: 1; /* Firefox dims placeholders on its own */
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: var(--radius-sm);
border: 2px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-track {
background: transparent;
}
@keyframes pulse {
50% {
opacity: 0.35;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* ---- app shell ---- */
.app {
display: flex;
flex-direction: column;
height: 100%;
}
/* Horizontal padding gives the chat column a constant gutter to the screen
edge; it stays even when a side panel collapses (a flex gap would not).
The panels' inner margins add the matching gutter when a panel is present. */
.app-body {
display: flex;
flex: 1;
min-height: 0;
padding: 0 14px;
}
/* ---- projects home ---- */
.home {
flex: 1;
min-height: 0;
overflow-y: auto;
scrollbar-gutter: stable both-edges;
background: var(--canvas);
}
.home-inner {
max-width: 620px;
margin: 0 auto;
padding: 48px 24px 64px;
}
.wordmark {
display: inline-flex;
align-items: center;
gap: 0.4em;
color: var(--text);
}
.wordmark svg {
width: 1em;
height: 1em;
flex-shrink: 0;
}
.home-brand {
/* Matches the openresearch.sh landing wordmark (font-bold = 700). */
font-weight: var(--fw-display);
font-size: var(--fs-4xl);
letter-spacing: -0.02em;
margin-bottom: 36px;
}
.home-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
}
.home-head h2 {
margin: 0;
font-size: var(--fs-2xl);
letter-spacing: -0.01em;
}
.home-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.project-card {
position: relative;
display: flex;
flex-direction: column;
gap: 4px;
text-align: left;
padding: 14px 16px;
background: var(--base);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
cursor: pointer;
transition:
border-color 120ms ease,
box-shadow 120ms ease;
}
.project-card:hover {
border-color: var(--text);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
}
.project-delete {
position: absolute;
top: 10px;
right: 10px;
display: none;
padding: 5px;
border-radius: var(--radius-sm);
color: var(--muted);
line-height: 0;
}
.project-card:hover .project-delete {
display: block;
}
.project-delete:hover {
color: var(--danger, #d33);
background: var(--overlay, rgba(0, 0, 0, 0.05));
}
.project-card .name {
font-weight: var(--fw-semibold);
font-size: var(--fs-base);
}
.project-card .repo {
color: var(--text);
}
.project-card .paper {
font-size: var(--fs-xs);
color: var(--muted);
}
.project-card .time {
font-size: var(--fs-xs);
color: var(--muted);
}
/* ---- rail header (brand + project switcher atop the agents rail) ---- */
.rail-brand {
display: flex;
align-items: center;
gap: 4px;
height: 48px;
/* 12px left text inset + ~14px icon right edge — keep in step with
.rail-nav below. */
padding: 0 8px 0 12px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
/* Back arrow + current project name; truncates, arrow stays visible. */
.rail-brand .brand {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
font-weight: var(--fw-semibold);
font-size: var(--fs-md);
color: var(--text);
padding: 4px 6px 4px 0;
border-radius: var(--radius-sm);
}
.rail-brand .brand:hover {
color: var(--subtext);
}
.rail-brand .brand svg {
flex-shrink: 0;
}
.rail-brand .brand .brand-project {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.icon-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
color: var(--subtext);
}
.icon-btn:hover {
color: var(--text);
background: var(--surface);
}
/* CSS-only tooltip for icon-only controls: `data-tip` holds the label.
Inverted bubble (text-on-base swap) so it reads in both themes. Default
below the control; `.tip-up` flips above (for footer rows). */
[data-tip] {
position: relative;
}
[data-tip]::after {
content: attr(data-tip);
position: absolute;
top: calc(100% + 6px);
right: -4px;
background: var(--text);
color: var(--base);
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
line-height: 1;
padding: 5px 8px;
border-radius: var(--radius-sm);
white-space: nowrap;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.1s ease 0.3s, visibility 0s linear 0.4s;
z-index: 60;
}
[data-tip]:hover::after {
opacity: 1;
visibility: visible;
}
.tip-up[data-tip]::after {
top: auto;
bottom: calc(100% + 6px);
}
/* ---- buttons (quiet: 1px border, rounded, flat) ---- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 14px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--base);
color: var(--text);
white-space: nowrap;
transition:
background 120ms ease,
border-color 120ms ease,
color 120ms ease;
}
.btn:hover:not(:disabled) {
background: var(--surface);
}
.btn:active:not(:disabled) {
background: var(--highlight);
}
.btn:disabled {
opacity: 0.45;
cursor: default;
}
.btn.primary {
background: var(--primary);
border-color: var(--primary);
color: var(--base);
}
.btn.primary:hover:not(:disabled) {
background: color-mix(in oklab, var(--primary) 88%, var(--text));
border-color: color-mix(in oklab, var(--primary) 88%, var(--text));
}
.btn.primary:active:not(:disabled) {
background: color-mix(in oklab, var(--primary) 80%, var(--text));
border-color: color-mix(in oklab, var(--primary) 80%, var(--text));
}
.btn.danger {
color: var(--accent-red);
}
.btn.danger:hover:not(:disabled) {
background: color-mix(in oklab, var(--accent-red) 8%, transparent);
}
.btn.danger:active:not(:disabled) {
background: color-mix(in oklab, var(--accent-red) 14%, transparent);
}
.btn.ghost {
border-color: transparent;
color: var(--text);
}
.btn.ghost:hover:not(:disabled) {
color: var(--text);
background: var(--surface);
}
.btn.sm {
padding: 3px 9px;
font-size: var(--fs-xs);
border-radius: var(--radius-sm);
}
/* ---- status badge (colored dot + quiet label; live statuses pulse) ---- */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
color: var(--text);
white-space: nowrap;
}
.status-badge .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
}
.status-badge.live .dot {
animation: pulse 1.2s ease-in-out infinite;
}
/* The status color lives on the dot; the label stays neutral. */
.st-done .dot {
color: var(--accent-green);
}
.st-failed .dot {
color: var(--accent-red);
}
.st-running .dot {
color: var(--accent-teal);
}
.st-starting .dot {
color: var(--accent-amber);
}
.st-cancelled .dot {
color: var(--accent-orange);
}
.st-editing .dot {
color: var(--accent-purple);
}
.st-idle .dot {
color: var(--muted);
}
/* ---- panes ---- */
/* Shared chrome for the floating side panels (rail + right pane): border,
rounded corners, soft two-layer shadow. Background and margins stay
per-panel. */
.floating-panel {
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow:
0 6px 24px color-mix(in oklab, var(--text) 5%, transparent),
0 1px 4px color-mix(in oklab, var(--text) 4%, transparent);
overflow: hidden;
}
/* Floating agents rail: fixed-width, collapsible (not resizable). */
.session-rail {
width: 232px;
flex-shrink: 0;
display: flex;
flex-direction: column;
/* right margin = the gutter to the chat column; screen-edge gutter comes
from .app-body's left padding. */
margin: 10px 14px 10px 0;
background: var(--base);
min-height: 0;
}
/* Rail nav: new session + settings sections, above the Recents list. */
.rail-nav {
display: flex;
flex-direction: column;
gap: 2px;
padding: 8px;
flex-shrink: 0;
}
.rail-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
font-size: var(--fs-md);
color: var(--text);
border-radius: var(--radius-md);
text-align: left;
}
.rail-nav-item:hover {
background: var(--surface);
}
.rail-nav-item.active {
background: var(--panel);
font-weight: var(--fw-semibold);
}
.rail-section-label {
padding: 10px 10px 6px;
font-size: var(--fs-sm);
font-weight: var(--fw-medium);
color: var(--subtext);
}
/* "Recents" label + the session filter button on one line. */
.rail-section-head {
display: flex;
align-items: center;
justify-content: space-between;
}
.rail-filter {
position: relative;
display: inline-flex;
margin-right: 2px;
}
/* A non-default filter keeps the button lit via the shared .icon-btn.active. */
.rail-filter-btn {
width: 24px;
height: 24px;
border-radius: var(--radius-sm);
}
.rail-empty {
padding: 6px 10px;
font-size: var(--fs-sm);
color: var(--muted);
}
.session-rail .rail-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 4px 8px;
}
/* The pane fills the leftover space so the thread's scrollbar reaches its
right edge; the readable-col cap lives on the header, the composer, and
.chat-thread-inner (inside the scroller), which center themselves. */
.chat-pane {
flex: 1;
/* min-width:0 (not 360) so a wide panel can never force the flex row past
the viewport; the panel's own clamp keeps the usable minimum. */
min-width: 0;
display: flex;
flex-direction: column;
background: var(--base);
min-height: 0;
}
/* Floating right panel: a closable, resizable card. `.max` lifts it out of
the flex row to cover (nearly) the whole window, still floating. */
.right-pane {
position: relative;
flex-shrink: 0;
min-width: 0;
display: flex;
flex-direction: column;
/* left margin = the gutter to the chat column; screen-edge gutter comes
from .app-body's right padding. */
margin: 10px 0 10px 14px;
background: var(--canvas);
}
.right-pane.max {
position: fixed;
inset: 10px;
margin: 0;
z-index: 60;
box-shadow: 0 12px 40px color-mix(in oklab, var(--text) 22%, transparent);
}
/* Invisible grab strip on the panel's left edge. Sits above the detail
overlays (.drawer/.term-view are z-index 20, inset 0) so resizing works in
every view, not just the tree. */
.panel-resizer {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6px;
cursor: col-resize;
z-index: 30;
}
.panel-resizer:hover,
.panel-resizer:active {
background: color-mix(in oklab, var(--text) 12%, transparent);
}
/* Expand/close cluster at the right end of the panel's tab bar. */
.panel-controls {
display: inline-flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
/* Panel tab bar: pinned Log/Artifacts + closable experiment/file tabs. */
.tabs {
display: flex;
align-items: center;
gap: 4px;
padding: 0 10px;
height: 40px;
border-bottom: 1px solid var(--border);
background: var(--base);
flex-shrink: 0;
}
/* Scrollable strip of tabs. */
.tab-strip {
display: flex;
align-items: center;
gap: 4px;
flex: 1;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
}
.tab-strip::-webkit-scrollbar {
display: none;
}
/* Dynamic (closable) tabs: truncated label, inline close "x". */
.tab.closable {
max-width: 176px;
padding-right: 4px;
}
.tab .tab-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tab .tab-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: var(--radius-xs);
color: var(--muted);
flex-shrink: 0;
}
.tab .tab-close:hover {
background: color-mix(in oklab, var(--text) 15%, transparent);
color: var(--text);
}
/* One tab style for the whole strip — pinned tabs simply have no close "x". */
.tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
border-radius: var(--radius-sm);
white-space: nowrap;
flex-shrink: 0;
}
.tab.active {
background: var(--surface);
}
.tab-body {
flex: 1;
min-height: 0;
position: relative;
display: flex;
flex-direction: column;
}
/* Toolbar row above a tab's content (e.g. the tree/table switch). */
.pane-toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 10px 12px 0;
flex-shrink: 0;
}
.pane-content {
flex: 1;
min-height: 0;
position: relative;
}
/* Right-pane plan tab: a plain, padded document surface — the plan is a
markdown file, not a card, so no tinted background. */
.plan-tab-content {
overflow-y: auto;
background: var(--base);
padding: 18px 24px;
}
.plan-tab-content .md {
max-width: var(--readable-col);
}
/* Segmented control: a clearly-darker track (the theme's creams all blend
into each other) with a raised white pill on the active segment. */
.seg {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 3px;
border-radius: var(--radius-md);
background: color-mix(in oklab, var(--text) 10%, transparent);
}
.seg button {
padding: 3px 12px;
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
border-radius: var(--radius-sm);
}
.seg button:hover {
color: var(--text);
}
.seg button.active {
background: var(--base);
box-shadow: 0 1px 3px color-mix(in oklab, var(--text) 25%, transparent);
}
/* ---- chat ---- */
/* Borderless header that fades into the chat: the thread scrolls under a
gradient hanging below the bar instead of hitting a hard rule. */
.chat-header {
display: flex;
align-items: center;
gap: 8px;
padding: 0 16px;
background: var(--base);
flex-shrink: 0;
height: 48px;
position: relative;
z-index: 4;
width: 100%;
max-width: var(--readable-col);
margin: 0 auto;
}
/* Rail hidden: the header spans the full pane (Claude-desktop style) so the
reopen toggle lands in the window's top-left corner with the title beside
it; the tighter padding puts the icon on the screen-edge gutter. */
.chat-header.rail-hidden {
max-width: none;
padding: 0 2px;
}
/* Breathing room between the corner toggle and the title. */
.chat-header.rail-hidden > .icon-btn:first-child {
margin-right: 12px;
}
.chat-header::after {
content: "";
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 24px;
background: linear-gradient(to bottom, var(--base), transparent);
pointer-events: none;
}
.chat-header .title {
font-size: var(--fs-md);
font-weight: var(--fw-semibold);
color: var(--text);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-row {
position: relative;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
text-align: left;
padding: 7px 10px;
border-radius: var(--radius-md);
font-size: var(--fs-md);
color: var(--text);
cursor: pointer;
user-select: none;
}
.session-row:hover {
background: var(--surface);
}
.session-row.active {
background: var(--surface);
font-weight: var(--fw-medium);
}
/* Reserved status column so titles align whether or not a busy dot shows. */
.session-row .session-dot {
width: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.session-row .session-title {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.session-row .session-time {
font-size: var(--fs-2xs);
color: var(--muted);
flex-shrink: 0;
}
/* Hover (or an open menu) swaps the timestamp for the three-dot menu button. */
.session-row .session-menu-btn {
display: none;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
margin: -2px 0;
border-radius: var(--radius-sm);
color: var(--muted);
flex-shrink: 0;
}
.session-row .session-menu-btn:hover {
color: var(--text);
background: var(--panel);
}
/* :focus-within (not :focus-visible): it holds while the kebab itself is
focused — a row-only match would display:none the button mid-Tab and dump
focus to
, making the menu keyboard-unreachable. */
.session-row:hover .session-menu-btn,
.session-row:focus-within .session-menu-btn,
.session-row.menu-open .session-menu-btn {
display: inline-flex;
}
.session-row:hover .session-time,
.session-row:focus-within .session-time,
.session-row.menu-open .session-time {
display: none;
}
.session-row .busy-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--primary);
animation: pulse 1.2s infinite;
flex-shrink: 0;
}
/* Turn held on an unanswered card: ready for the user — steady, no pulse. */
.session-row .busy-dot.waiting {
animation: none;
}
/* Inline rename: the title span is replaced by an input sharing its box so the
text doesn't jump. Take the row's full width and hide the kebab/time. */
.session-row .session-title-input {
flex: 1;
min-width: 0;
padding: 1px 5px;
margin: -2px 0;
font: inherit;
color: var(--text);
background: var(--base);
border: 1px solid var(--primary);
border-radius: var(--radius-sm);
outline: none;
}
.session-row.editing {
background: var(--surface);
cursor: default;
}
.session-row.editing .session-menu-btn,
.session-row.editing .session-time {
display: none;
}
.chat-thread {
flex: 1;
min-height: 0;
overflow-y: auto;
/* stable: no shift when the bar appears; both-edges: keeps the column centered */
scrollbar-gutter: stable both-edges;
}
.chat-thread-inner {
max-width: var(--readable-col);
margin: 0 auto;
/* Bottom padding clears the composer's 24px fade so the last message stays
fully legible when scrolled to the very bottom. */
padding: 16px 16px 32px;
display: flex;
flex-direction: column;
gap: 16px;
}
.chat-empty {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--text);
padding: 20px;
text-align: center;
}
.chat-empty h2 {
margin: 0;
font-size: var(--fs-5xl);
font-weight: var(--fw-display);
letter-spacing: -0.02em;
color: var(--text);
}
.chat-empty .chat-empty-hint {
font-size: var(--fs-sm);
color: var(--muted);
max-width: 340px;
text-align: center;
}
.chat-empty p {
margin: 0;
font-size: var(--fs-md);
max-width: 320px;
}
/* one-click /reproduce-paper prefill for paper-born projects */
.chat-empty .chat-suggest {
font-family: var(--mono);
font-size: var(--fs-sm);
color: var(--text);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-full);
padding: 6px 14px;
cursor: pointer;
transition: border-color 120ms ease;
}
.chat-empty .chat-suggest:hover {
border-color: var(--text);
}
/* Shown while an existing session's transcript is fetched, so switching
sessions never flashes the empty state before its messages seed in. */
.chat-loading {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
color: var(--subtext);
font-size: var(--fs-xl);
padding: 20px;
}
/* Larger, more visible wheel than the inline .spinner used in the thread. */
.chat-loading .spinner {
width: 22px;
height: 22px;
border-width: 3px;
}
/* user message: a right-aligned rounded bubble, no border, no label */
.msg-user {
align-self: flex-end;
max-width: 88%;
background: var(--surface);
border-radius: 16px; /* chat bubble — deliberate outsized radius */
padding: 10px 15px;
font-size: var(--fs-md);
white-space: pre-wrap;
word-break: break-word;
}
.msg-images {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 8px;
}
.msg-images img {
max-width: 220px;
max-height: 160px;
border: 1px solid var(--border-variant);
border-radius: var(--radius-xs);
display: block;
}
.msg-assistant {
font-size: var(--fs-base);
line-height: 1.62;
color: var(--text);
min-width: 0;
}
.reasoning {
color: var(--muted);
font-size: var(--fs-sm);
margin: 2px 0;
}
.reasoning summary {
cursor: pointer;
list-style: none;
user-select: none;
font-weight: var(--fw-semibold);
}
.reasoning[open] {
white-space: pre-wrap;
}
/* tool calls: bordered surface card, one row per call */
/* ---- tool calls: compact, coalesced, gray (Claude-desktop style) ---- */
.tool-group {
margin: 2px 0;
}
.tool-group-summary {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 3px 2px;
cursor: pointer;
color: var(--muted);
font-size: var(--fs-md);
text-align: left;
border-radius: var(--radius-sm);
}
.tool-group-summary:hover {
color: var(--subtext);
background: var(--surface);
}
.tool-group.has-error .tool-group-summary {
color: var(--accent-red);
}
.tool-chevron {
flex-shrink: 0;
color: var(--muted);
transition: transform 0.12s ease;
}
.tool-chevron.open {
transform: rotate(90deg);
}
.tool-group-rows {
display: flex;
flex-direction: column;
gap: 1px;
margin: 2px 0 4px 7px;
padding-left: 10px;
border-left: 1px solid var(--border-variant);
}
.tool-row {
display: flex;
flex-direction: column;
}
.tool-row summary {
display: flex;
align-items: center;
gap: 8px;
padding: 3px 4px;
cursor: pointer;
list-style: none;
user-select: none;
min-width: 0;
border-radius: var(--radius-sm);
}
.tool-row summary:hover {
background: var(--surface);
}
.tool-row summary::-webkit-details-marker {
display: none;
}
.tool-line {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var(--fs-md);
color: var(--subtext);
}
.tool-open {
flex-shrink: 0;
font-size: var(--fs-xs);
color: var(--primary);
}
.tool-open:hover {
text-decoration: underline;
}
.tool-status {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
background: var(--muted);
}
.tool-status.running {
background: var(--accent-amber);
animation: pulse 1.2s ease-in-out infinite;
}
.tool-status.error {
background: var(--accent-red);
}
.tool-detail {
margin: 2px 0 4px 14px;
}
.tool-cmd-full {
padding: 6px 10px;
font-family: var(--mono);
font-size: var(--fs-xs);
color: var(--text);
background: var(--surface);
border-radius: var(--radius-sm);
white-space: pre-wrap;
word-break: break-word;
}
.tool-output {
margin-top: 3px;
padding: 6px 10px;
font-family: var(--mono);
font-size: var(--fs-xs);
color: var(--subtext);
white-space: pre-wrap;
word-break: break-word;
max-height: 260px;
overflow-y: auto;
background: var(--base);
border: 1px solid var(--border-variant);
border-radius: var(--radius-sm);
}
.working {
display: flex;
align-items: center;
gap: 8px;
color: var(--subtext);
font-size: var(--fs-md);
padding: 2px 0 8px;
}
/* Turn held open on an unanswered card: waiting on the user, not the model. */
.working.awaiting {
font-style: italic;
}
.spinner {
width: 13px;
height: 13px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
/* composer: rounded box and send button. Borderless like the header —
the thread fades into the box through a gradient hanging above it. */
.composer {
/* No top padding: the fade hanging above leads straight into the box. */
padding: 0 12px 12px;
flex-shrink: 0;
position: relative;
z-index: 4;
background: var(--base);
width: 100%;
max-width: var(--readable-col);
margin: 0 auto;
}
.composer::before {
content: "";
position: absolute;
bottom: 100%;
left: 0;
right: 0;
height: 24px;
background: linear-gradient(to top, var(--base), transparent);
pointer-events: none;
}
.composer-box {
position: relative; /* anchors the slash-skill menu */
display: flex;
flex-direction: column;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--base);
}
.composer textarea {
border: none;
background: none;
resize: none;
padding: 10px 12px 4px;
font-size: var(--fs-md);
min-height: 42px;
max-height: 180px;
}
/* pasted images waiting to be sent */
.composer-attachments {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 8px 12px 0;
}
.attachment-thumb {
position: relative;
}
.attachment-thumb img {
width: 52px;
height: 52px;
object-fit: cover;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
display: block;
}
.attachment-thumb button {
position: absolute;
top: -5px;
right: -5px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
border: 1px solid var(--border);
border-radius: 50%;
background: var(--surface);
color: var(--text);
cursor: pointer;
}
.attachment-thumb button:hover {
background: var(--text);
color: var(--base);
}
.composer-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
padding: 6px 8px 8px;
}
.send-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-md);
background: var(--primary);
color: var(--base);
transition:
background 100ms ease,
opacity 100ms ease;
}
.send-btn:hover:not(:disabled) {
background: color-mix(in oklab, var(--primary) 88%, var(--text));
}
.send-btn:disabled {
opacity: 0.4;
cursor: default;
}
.send-btn.stop {
background: var(--surface);
color: var(--text);
}
.send-btn.stop:hover:not(:disabled) {
background: color-mix(in oklab, var(--surface) 88%, var(--text));
}
/* ---- markdown ---- */
.md {
min-width: 0;
word-break: break-word;
color: var(--text);
line-height: 1.62;
}
.md > *:first-child {
margin-top: 0;
}
.md > *:last-child {
margin-bottom: 0;
}
.md p {
margin: 10px 0;
}
.md strong {
color: var(--text);
font-weight: var(--fw-semibold);
}
.md pre {
background: var(--surface);
border: 1px solid color-mix(in oklab, var(--border) 50%, transparent);
border-radius: var(--radius-md);
padding: 8px 12px;
overflow-x: auto;
font-size: var(--fs-sm);
color: var(--text);
}
.md code {
font-family: var(--mono);
font-size: 0.9em;
font-weight: var(--fw-medium);
color: var(--primary);
background: var(--panel);
border: 1px solid var(--border-variant);
border-radius: var(--radius-xs);
padding: 1px 5px;
}
/* KaTeX math — tame the library's 1.21em default so formulas sit at body
size, and let wide display equations scroll instead of overflowing. */
.md .katex {
font-size: 1.05em;
}
.md .katex-display {
margin: 12px 0;
overflow-x: auto;
overflow-y: hidden;
padding: 2px 0;
}
/* File mention chip — reads as clickable inline code (thin border, panel
fill, mono), matching openresearch.sh's FileMention. */
.md .file-chip {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
margin: 0 1px;
padding: 0 6px;
vertical-align: baseline;
font-family: var(--mono);
font-size: 0.9em;
font-weight: var(--fw-medium);
color: var(--text);
background: var(--panel);
border: 1px solid var(--border-variant);
border-radius: var(--radius-xs);
cursor: pointer;
}
.md .file-chip:hover:not(:disabled) {
background: var(--surface);
color: var(--primary);
}
.md .file-chip svg {
flex: none;
opacity: 0.6;
}
.md .file-chip-label {
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.md pre code {
background: none;
border: none;
color: inherit;
padding: 0;
font-weight: var(--fw-regular);
}
.md-code {
position: relative;
margin: 10px 0;
}
.md-code pre {
margin: 0;
}
.md-code-copy {
position: absolute;
top: 6px;
right: 6px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
color: var(--muted);
background: var(--base);
border: 1px solid var(--border-variant);
border-radius: var(--radius-sm);
opacity: 0;
transition: opacity 0.12s ease, color 0.12s ease;
}
.md-code:hover .md-code-copy {
opacity: 1;
}
.md-code-copy:hover {
color: var(--text);
border-color: var(--muted);
}
.md h1,
.md h2,
.md h3,
.md h4 {
color: var(--text);
font-size: 1.05em;
font-weight: var(--fw-semibold);
margin: 12px 0 6px;
}
.md ul,
.md ol {
margin: 6px 0;
padding-left: 22px;
}
.md li::marker {
color: var(--primary);
}
.md a {
color: var(--primary);
}
/* Same wide-table treatment as .report-md table: cells keep whole-word
min-content widths (see .md td below), so an over-wide table scrolls in
place instead of clipping at the pane edge. */
.md table {
border-collapse: collapse;
display: block;
width: max-content;
max-width: 100%;
font-size: var(--fs-md);
margin: 10px 0;
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow-x: auto;
}
.md th,
.md td {
border-bottom: 1px solid var(--border-variant);
padding: 8px 14px;
text-align: left;
color: var(--text);
/* .md's word-break: break-word shrinks cell min-content width to one
character, letting auto table layout split short words mid-word;
overflow-wrap keeps full-word min-content and only breaks overflow. */
word-break: normal;
overflow-wrap: break-word;
}
.md tr:last-child td {
border-bottom: none;
}
.md thead th {
background: var(--surface);
font-weight: var(--fw-medium);
color: var(--text);
border-bottom: 1px solid var(--border);
}
.md tbody tr:hover td {
background: var(--surface-bright);
}
.md blockquote {
margin: 6px 0;
padding: 2px 0 2px 10px;
border-left: 3px solid var(--border);
color: var(--subtext);
}
/* ---- tree nodes (graph cards on the dotted canvas) ---- */
/* Cards must stay `selectable` (ReactFlow sets pointer-events:none on nodes
* that are neither draggable nor selectable, which would kill the action
* buttons) — but selection itself means nothing here, so suppress its
* pointer cursor. Specificity beats RF's stylesheet. Focus/selected visuals
* need no override: nodesFocusable={false} removes the tab stop, RF's own
* CSS zeroes node focus outlines, and its .selected box-shadow only targets
* built-in node types, not custom ones. */
.react-flow__node.react-flow__node-exp.selectable {
cursor: default;
}
.exp-node {
/* Keep in sync with NODE_W in TreeView.tsx (layout math uses it). */
width: 264px;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--base);
padding: 10px 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
font-size: var(--fs-md);
transition: box-shadow 120ms ease;
}
.exp-node:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.exp-node.live {
border-color: var(--accent-teal);
box-shadow: 0 2px 12px rgba(32, 154, 132, 0.2);
}
.exp-node .node-eyebrow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
font-size: var(--fs-2xs);
font-weight: var(--fw-medium);
color: var(--muted);
}
.exp-node .node-head {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.exp-node .node-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.exp-node .node-slug {
font-family: var(--mono);
font-size: var(--fs-sm);
font-weight: var(--fw-semibold);
color: var(--text);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.exp-node .node-title {
margin-top: 4px;
color: var(--text);
font-size: var(--fs-sm);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.exp-node .node-meta {
margin-top: 8px;
display: flex;
align-items: center;
gap: 8px;
font-size: var(--fs-2xs);
color: var(--muted);
}
/* Direct view shortcuts (Changes / Logs) at the card's foot. */
.exp-node .node-actions {
margin-top: 8px;
padding-top: 6px;
border-top: 1px solid var(--border-variant);
display: flex;
align-items: center;
gap: 3px;
}
.exp-node .node-action {
display: inline-flex;
align-items: center;
gap: 5px;
/* Tight: Changes + Logs + Code + the GitHub icon share one fixed-width row. */
padding: 3px 6px;
font-size: var(--fs-xs);
font-weight: var(--fw-medium);
color: var(--text);
border-radius: var(--radius-sm);
/* The GitHub shortcut is an among sibling