Files
openchamber/packages/ui/src/index.css
T
Bohdan Triapitsyn d4a8c4d2e1 feat(terminal): refactor runtime and add mobile workspace (#2280)
Replace the legacy terminal flow with a shared authenticated WebSocket
runtime used across web, desktop, relay, and mobile surfaces.

- introduce the v3 terminal protocol with scoped attachments, snapshots,
  ordered output, bounded replay history, reconnects, and explicit lifecycle
- harden PTY creation, restart, resize, close, force-kill, idle cleanup,
  shell selection, login mode, environment sanitization, and appearance sync
- add runtime-aware terminal APIs with relay authentication and Electron parity
- add a fullscreen mobile terminal workspace with touch scrolling,
  long-press selection, safe-area controls, quick keys, and Ctrl/Alt input
- add terminal selection attachments, preview detection, project actions,
  shell settings, and localized UI
- harden Ghostty rendering, resize recovery, Unicode handling, block
  characters, line height, and stale-row behavior
- remove the obsolete terminal SSE path and update reverse-proxy guidance
- expand terminal runtime, transport, input, selection, and store coverage
- avoid duplicate web builds when preparing mobile assets in root CI builds
2026-07-17 13:17:21 +03:00

1675 lines
43 KiB
CSS

@import "tailwindcss";
@import "tw-animate-css";
@import "./styles/design-system.css";
@import "./styles/typography.css";
@import "./styles/mobile.css";
/* Load KaTeX styles eagerly with the main bundle. Importing them inside the
lazy MarkdownRendererImpl chunk meant a stylesheet was injected late on the
first markdown render of a session; that late injection triggered a style
recalc during which the layered `.chat-message-column` width clamp briefly
lost, flashing message text to full width before it snapped back. */
@import "katex/dist/katex.min.css";
@source "./**/*.{ts,tsx,css}";
/* KaTeX theme overrides for light/dark mode */
.katex {
color: var(--foreground);
}
.katex .mord,
.katex .mopen,
.katex .mclose,
.katex .mbin,
.katex .mrel,
.katex .mpunct,
.katex .mord,
.katex .mop,
.katex .minner {
color: inherit;
}
.katex-error {
color: var(--destructive);
}
@custom-variant dark (&:is(.dark *));
/* Buttons should never look selectable. */
button,
[role="button"] {
user-select: none !important;
-webkit-user-select: none !important;
}
:root.oc-theme-switching *,
:root.oc-theme-switching *::before,
:root.oc-theme-switching *::after {
transition: none !important;
animation: none !important;
}
[data-settings-search-highlight="true"] {
border-radius: 0.25rem;
background: color-mix(in srgb, var(--primary-base) 7%, transparent);
transition: background-color 180ms ease;
}
/* ── macOS vibrancy ───────────────────────────────────────────────────────
The window is created transparent with a native 'sidebar' NSVisualEffectView
behind it. We keep the whole app opaque (bg-background covers the window)
EXCEPT the left sidebar, which is the only translucent surface that lets the
native vibrancy show through. `data-oc-vibrancy` = vibrancy active;
`data-oc-vibrancy-ready` = native layer has settled (set from the main
process after show/restore) — until then the sidebar stays solid to avoid a
flash of raw transparency. */
html[data-oc-vibrancy] body,
html[data-oc-vibrancy] #root {
background: transparent !important;
background-color: transparent !important;
}
html[data-oc-vibrancy] .main-content-safe-area {
background: transparent !important;
background-color: transparent !important;
}
/* Left sidebar: solid until the native layer is ready, then a translucent
overlay so the vibrancy frosts through. */
html[data-oc-vibrancy] .oc-vibrancy-surface {
background: var(--sidebar) !important;
}
html[data-oc-vibrancy][data-oc-vibrancy-ready] .oc-vibrancy-surface {
background: var(--sidebar-vibrancy-overlay) !important;
}
/* Inner bg-sidebar elements inside the vibrant surface must not re-paint an
opaque fill, or they'd cover the frosted effect. */
html[data-oc-vibrancy] .oc-vibrancy-surface .bg-sidebar {
background: transparent !important;
background-color: transparent !important;
}
/* Project-actions pill over the open sidebar: drop its opaque surface so it
frosts to the exact same level as the sidebar (border keeps its outline). */
html[data-oc-vibrancy] .oc-vibrancy-pill {
background: transparent !important;
background-color: transparent !important;
}
/* Accessibility: drop translucency when the OS asks for reduced transparency. */
@media (prefers-reduced-transparency: reduce) {
html[data-oc-vibrancy] body,
html[data-oc-vibrancy] #root,
html[data-oc-vibrancy] .main-content-safe-area {
background: var(--surface-background) !important;
}
html[data-oc-vibrancy] .oc-vibrancy-surface,
html[data-oc-vibrancy][data-oc-vibrancy-ready] .oc-vibrancy-surface {
background: var(--sidebar) !important;
}
}
/* Suppress WebKit-specific hover/focus adornments on the chat textarea */
textarea[data-chat-input="true"] {
-webkit-appearance: none;
background: transparent !important;
border: none;
box-shadow: none;
}
textarea[data-chat-input="true"]:hover,
textarea[data-chat-input="true"]:focus,
textarea[data-chat-input="true"]:focus-visible {
outline: none;
border: none;
box-shadow: none;
}
/* Ensure interactive controls consistently show pointer cursor */
:where(
button,
[role="button"],
a[href]
):not(:disabled) {
cursor: pointer;
}
:where(button, [role="button"]):disabled {
cursor: not-allowed;
}
:root.light .message-content-text::selection,
:root.light .message-content-text ::selection {
background: color-mix(in srgb, var(--interactive-border-focus) 18%, transparent);
color: var(--surface-foreground);
}
.oc-cm-selected-line {
background: color-mix(in srgb, var(--primary-base) 22%, transparent);
}
.pierre-diff-wrapper {
--diffs-bg-selection-override: color-mix(in srgb, var(--primary-base) 22%, transparent);
--diffs-bg-selection-background-override: color-mix(in srgb, var(--primary-base) 14%, transparent);
--diffs-bg-selection-number-override: color-mix(in srgb, var(--primary-base) 28%, transparent);
--diffs-bg-selection-number-background-override: color-mix(in srgb, var(--primary-base) 18%, transparent);
--diffs-selection-color-override: var(--primary-base);
}
/* Codemirror syntax fallback for classHighlighter tokens */
.cm-editor .tok-comment,
.cm-editor .tok-docComment,
.cm-editor .tok-meta,
.cm-editor .tok-documentMeta {
color: var(--syntax-comment);
}
.cm-editor .tok-keyword,
.cm-editor .tok-controlKeyword,
.cm-editor .tok-definitionKeyword,
.cm-editor .tok-modifier,
.cm-editor .tok-moduleKeyword {
color: var(--syntax-keyword);
}
.cm-editor .tok-operator,
.cm-editor .tok-operatorKeyword,
.cm-editor .tok-derefOperator,
.cm-editor .tok-updateOperator,
.cm-editor .tok-definitionOperator,
.cm-editor .tok-typeOperator,
.cm-editor .tok-controlOperator,
.cm-editor .tok-logicOperator,
.cm-editor .tok-bitwiseOperator,
.cm-editor .tok-arithmeticOperator,
.cm-editor .tok-compareOperator {
color: var(--syntax-operator);
}
.cm-editor .tok-string,
.cm-editor .tok-special.tok-string,
.cm-editor .tok-escape,
.cm-editor .tok-regexp,
.cm-editor .tok-attributeValue,
.cm-editor .tok-monospace {
color: var(--syntax-string);
}
.cm-editor .tok-number,
.cm-editor .tok-bool,
.cm-editor .tok-atom,
.cm-editor .tok-null,
.cm-editor .tok-self {
color: var(--syntax-number);
}
.cm-editor .tok-function.tok-variableName,
.cm-editor .tok-function.tok-propertyName,
.cm-editor .tok-standard.tok-variableName,
.cm-editor .tok-special.tok-variableName {
color: var(--syntax-function);
}
.cm-editor .tok-variableName,
.cm-editor .tok-definition.tok-variableName,
.cm-editor .tok-local.tok-variableName,
.cm-editor .tok-constant.tok-variableName,
.cm-editor .tok-propertyName,
.cm-editor .tok-attributeName {
color: var(--syntax-variable);
}
.cm-editor .tok-className,
.cm-editor .tok-typeName,
.cm-editor .tok-namespace {
color: var(--syntax-type);
}
.cm-editor .tok-tagName,
.cm-editor .tok-labelName,
.cm-editor .tok-annotation,
.cm-editor .tok-macroName {
color: var(--syntax-keyword);
}
.cm-editor .tok-link {
color: var(--syntax-function);
text-decoration: underline;
}
.cm-editor .tok-punctuation,
.cm-editor .tok-separator,
.cm-editor .tok-bracket,
.cm-editor .tok-paren,
.cm-editor .tok-brace,
.cm-editor .tok-squareBracket,
.cm-editor .tok-angleBracket {
color: color-mix(in srgb, var(--syntax-comment) 85%, var(--syntax-foreground) 15%);
}
/* Codemirror syntax fallback for class-based HighlightStyle tokens */
.cm-editor .cm-comment,
.cm-editor .cm-meta {
color: var(--syntax-comment);
}
.cm-editor .cm-keyword,
.cm-editor .cm-tag {
color: var(--syntax-keyword);
}
.cm-editor .cm-operator {
color: var(--syntax-operator);
}
.cm-editor .cm-string,
.cm-editor .cm-string-2,
.cm-editor .cm-link {
color: var(--syntax-string);
}
.cm-editor .cm-number,
.cm-editor .cm-atom {
color: var(--syntax-number);
}
.cm-editor .cm-builtin,
.cm-editor .cm-def {
color: var(--syntax-function);
}
.cm-editor .cm-variable,
.cm-editor .cm-variable-2,
.cm-editor .cm-property,
.cm-editor .cm-attribute {
color: var(--syntax-variable);
}
.cm-editor .cm-variable-3 {
color: var(--syntax-type);
}
.cm-editor .cm-punctuation {
color: color-mix(in srgb, var(--syntax-comment) 85%, var(--syntax-foreground) 15%);
}
:root.vscode-runtime textarea[data-chat-input="true"]::placeholder {
color: color-mix(in srgb, var(--vscode-input-placeholderForeground, var(--muted-foreground)) 65%, transparent);
}
/* Match chat footer background to input in WebKit to avoid dual-tone hover/focus */
[data-chat-input="true"] + div[data-chat-input-footer="true"] {
background: transparent !important;
border: none;
box-shadow: none;
-webkit-appearance: none;
}
/* Scroll shadow (HeroUI) fallback styling to ensure visible gradients without the Tailwind plugin */
[data-scroll-shadow="true"] {
--scroll-shadow-size: var(--scroll-shadow-size, 48px);
}
[data-scroll-shadow="true"][data-orientation="vertical"] {
mask-mode: alpha;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
[data-scroll-shadow="true"][data-orientation="vertical"][data-top-bottom-scroll="true"] {
mask-image: linear-gradient(
to bottom,
transparent 0%,
#000 var(--scroll-shadow-size),
#000 calc(100% - var(--scroll-shadow-size)),
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
#000 var(--scroll-shadow-size),
#000 calc(100% - var(--scroll-shadow-size)),
transparent 100%
);
}
[data-scroll-shadow="true"][data-orientation="vertical"][data-top-scroll="true"] {
mask-image: linear-gradient(
to bottom,
transparent 0%,
#000 var(--scroll-shadow-size),
#000 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
#000 var(--scroll-shadow-size),
#000 100%
);
}
[data-scroll-shadow="true"][data-orientation="vertical"][data-bottom-scroll="true"] {
mask-image: linear-gradient(
to bottom,
#000 0%,
#000 calc(100% - var(--scroll-shadow-size)),
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
#000 0%,
#000 calc(100% - var(--scroll-shadow-size)),
transparent 100%
);
}
@keyframes spin-once {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes oc-migrate-up {
0% {
opacity: 1;
transform: translateY(0);
}
100% {
opacity: 0;
transform: translateY(-10px);
}
}
@keyframes gradient-shimmer {
0% {
stop-color: currentColor;
}
50% {
stop-color: color-mix(in srgb, currentColor 60%, white 40%);
}
100% {
stop-color: currentColor;
}
}
@keyframes shimmer-position {
0% {
x: -100%;
}
100% {
x: 100%;
}
}
@keyframes oc-file-mention-marquee {
0% {
transform: translateX(0%);
}
100% {
transform: translateX(calc(-100% + var(--file-mention-marquee-width, 360px)));
}
}
.file-mention-marquee-container {
--file-mention-marquee-width: 360px;
--file-mention-marquee-duration: 4s;
mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%);
-webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent 100%);
}
.file-mention-marquee {
animation: oc-file-mention-marquee var(--file-mention-marquee-duration, 4s) linear 0.2s 1 both;
will-change: transform;
}
/* Safari/WebKit fixes for buttons and SVG icons */
/* Remixicon puts fill="currentColor" on root SVG which WebKit renders as background */
button svg.remixicon,
[role="button"] svg.remixicon,
button svg,
[role="button"] svg {
background: none !important;
background-color: transparent !important;
/* Override inline fill attribute on SVG root */
fill: none !important;
/* Ensure SVG uses CSS rendering, not native */
shape-rendering: geometricPrecision;
}
/* GPU acceleration for non-spinning SVGs */
button svg:not(.animate-spin),
[role="button"] svg:not(.animate-spin) {
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
/* Fix SVG elements inside buttons - remove any rect/circle backgrounds */
button svg rect,
button svg circle,
[role="button"] svg rect,
[role="button"] svg circle {
fill: none !important;
}
/* Ensure SVG paths/shapes inherit color properly */
button svg.remixicon path,
button svg.remixicon circle[fill],
[role="button"] svg.remixicon path,
[role="button"] svg.remixicon circle[fill],
button svg path,
[role="button"] svg path {
fill: currentColor !important;
}
/* Safari/WebKit animation fixes - outside @layer for higher specificity */
.animate-spin {
animation: webkit-spin 1s linear infinite !important;
will-change: transform !important;
background: transparent !important;
background-color: transparent !important;
}
/* Specific SVG spinner fixes for WebKit */
svg.animate-spin {
/* Use CSS box as transform reference for proper centering with scaled SVGs */
transform-box: fill-box !important;
transform-origin: 50% 50% !important;
background: none !important;
background-color: transparent !important;
overflow: visible !important;
/* Ensure SVG content is centered */
display: block !important;
}
@keyframes webkit-spin {
0% {
transform: translateZ(0) rotate(0deg);
}
100% {
transform: translateZ(0) rotate(360deg);
}
}
@keyframes pulse-opacity {
0%,
100% {
opacity: 0.4;
}
50% {
opacity: 1;
}
}
@keyframes pulse-opacity-dim {
0%,
100% {
opacity: 0;
}
50% {
opacity: 0.7;
}
}
.app-region-drag {
-webkit-app-region: drag;
}
.app-region-no-drag {
-webkit-app-region: no-drag;
}
.animate-spin-once {
animation: spin-once 0.5s linear forwards;
}
/* Custom scrollbar styles */
*::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
background: transparent;
-webkit-appearance: none;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background: var(--oc-scrollbar-thumb);
border-radius: 6px;
border: 2px solid transparent;
background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover {
background: var(--oc-scrollbar-thumb-hover);
background-clip: padding-box;
}
.dark {
color-scheme: dark;
--oc-scrollbar-thumb: oklch(0.6 0.02 80 / 0.3);
--oc-scrollbar-thumb-hover: oklch(0.6 0.02 80 / 0.5);
}
[data-scrollbar="chat"]::-webkit-scrollbar-thumb {
border: 2px solid transparent;
background-clip: padding-box;
}
*::-webkit-scrollbar-corner {
background: transparent;
}
[data-scrollbar="chat"].chat-scroll,
.chat-scroll {
scrollbar-gutter: stable both-edges;
}
html:not(.dark) .chat-scroll {
color-scheme: dark;
}
[data-scrollbar="chat"]::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
background: transparent !important;
-webkit-appearance: none;
}
[data-scrollbar="chat"].chat-scroll::-webkit-scrollbar,
.chat-scroll::-webkit-scrollbar {
width: 10px !important;
height: 10px !important;
}
[data-scrollbar="chat"]::-webkit-scrollbar-track {
background: transparent !important;
}
[data-scrollbar="chat"].chat-scroll::-webkit-scrollbar-track,
.chat-scroll::-webkit-scrollbar-track {
background: transparent !important;
}
[data-scrollbar="chat"]::-webkit-scrollbar-thumb {
background: var(--oc-scrollbar-thumb) !important;
border-radius: 6px;
border: 2px solid transparent;
background-clip: padding-box;
}
[data-scrollbar="chat"].chat-scroll::-webkit-scrollbar-thumb,
.chat-scroll::-webkit-scrollbar-thumb {
background: var(--oc-scrollbar-thumb) !important;
border-radius: 6px;
border: 2px solid transparent;
background-clip: padding-box;
}
[data-scrollbar="chat"]::-webkit-scrollbar-thumb:hover {
background: var(--oc-scrollbar-thumb-hover) !important;
background-clip: padding-box;
}
[data-scrollbar="chat"].chat-scroll::-webkit-scrollbar-thumb:hover,
.chat-scroll::-webkit-scrollbar-thumb:hover {
background: var(--oc-scrollbar-thumb-hover) !important;
background-clip: padding-box;
}
[data-scrollbar="chat"]::-webkit-scrollbar-corner {
background: transparent !important;
}
/* Hide native scrollbars for overlay targets (all browsers) */
.overlay-scrollbar-target {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
scrollbar-gutter: auto !important;
}
.overlay-scrollbar-target--no-gutter {
scrollbar-gutter: unset !important;
}
.overlay-scrollbar-target::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
display: none !important;
}
/* Firefox scrollbar support */
* {
scrollbar-width: thin;
scrollbar-color: var(--oc-scrollbar-thumb) transparent;
}
.scrollbar-hidden {
scrollbar-width: none !important;
}
.scrollbar-hidden::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
}
.dark * {
scrollbar-color: oklch(0.6 0.02 80 / 0.3) transparent;
}
/* Overlay scrollbar styling (custom thumb) */
.overlay-scrollbar {
position: absolute;
inset: 0;
pointer-events: none;
transition: opacity 0.2s ease;
z-index: 10;
}
.overlay-scrollbar--dense,
.overlay-scrollbar--bottom-right,
.overlay-scrollbar--zero,
.overlay-scrollbar--flush {
/* no-op: positioning handled inline */
}
.overlay-scrollbar__thumb {
pointer-events: auto;
position: absolute;
width: 6px;
background: var(--oc-scrollbar-thumb);
border-radius: 9999px;
opacity: 0.8;
transition: background-color 0.15s ease, opacity 0.15s ease;
will-change: transform, height;
}
.overlay-scrollbar__thumb:hover,
.overlay-scrollbar__thumb:active {
background: var(--oc-scrollbar-thumb-hover);
opacity: 1;
}
.overlay-scrollbar__thumb--horizontal {
height: 6px;
margin: 0;
}
.overlay-scrollbar__thumb--vertical {
margin: 0;
}
/* Pierre diff viewer styling */
.pierre-diff-wrapper {
--diffs-font-family: var(--font-mono, ui-monospace, monospace);
--diffs-font-size: var(--text-code);
--diffs-line-height: 24px;
--diffs-tab-size: 2;
--diffs-header-font-family: var(--font-sans, system-ui, sans-serif);
--diffs-min-number-column-width: 4ch;
--diffs-gap-inline: 0;
--diffs-gap-block: 0;
/* Prevent browser scroll anchoring jumps during async highlight/virtualization */
overflow-anchor: none;
}
/* Prevent scroll anchoring in stacked diff scroll container */
[data-diff-virtual-root] {
overflow-anchor: none;
}
/* Desktop: slightly smaller diff code text */
.pierre-diff-wrapper diffs-container {
display: block;
}
/* Header tab responsiveness */
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
display: inline;
}
:root:not(.mobile-pointer):not(.vscode-runtime) .header-open-label {
display: inline;
}
@media (max-width: 940px) {
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
display: none;
}
:root:not(.mobile-pointer):not(.vscode-runtime) .header-open-label {
display: none;
}
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-badge {
margin-left: -0.125rem;
}
}
/* ============================================
Streamdown Styles
Minimal overrides - fonts only, using Streamdown defaults
============================================ */
/* VS Code webviews can inject default pre/code/blockquote backgrounds; tool input/output should inherit the card surface. */
.tool-input-surface pre,
.tool-input-surface code,
.tool-input-surface blockquote,
.tool-input-text {
background: transparent !important;
background-color: transparent !important;
}
/* VSCode: tool cards should honor semantic code sizing, even when syntax themes inject sizes. */
:root.vscode-runtime .tool-input-surface,
:root.vscode-runtime .tool-output-surface {
font-size: var(--text-code) !important;
}
:root.vscode-runtime .tool-input-surface code,
:root.vscode-runtime .tool-input-surface pre,
:root.vscode-runtime .tool-output-surface code,
:root.vscode-runtime .tool-output-surface pre {
font-size: inherit !important;
}
/* Model/agent controls: progressively collapse labels to icon-only as the row narrows.
Hide priority (most aggressive first → most resilient last): agent, variant, model. */
/* Desktop (non-VSCode): pick thresholds that fire BEFORE the per-label `max-w-[Xpx]`
constraints would truncate text with an ellipsis. */
@container model-controls (max-width: 22rem) {
:root:not(.vscode-runtime) .model-controls__agent-label {
display: none;
}
}
@container model-controls (max-width: 14rem) {
:root:not(.vscode-runtime) .model-controls__variant-label {
display: none;
}
}
@container model-controls (max-width: 10rem) {
:root:not(.vscode-runtime) .model-controls__model-label {
display: none;
}
}
/* VS Code side panel: tighter thresholds since the panel is narrow and labels are smaller. */
@container model-controls (max-width: 11rem) {
:root.vscode-runtime .model-controls__agent-label {
display: none;
}
}
@container model-controls (max-width: 9rem) {
:root.vscode-runtime .model-controls__variant-label {
display: none;
}
}
@container model-controls (max-width: 10rem) {
:root.vscode-runtime .model-controls__model-label {
display: none;
}
}
/* Commit actions: collapse labels when narrow. */
@container commit-actions (max-width: 21rem) {
.commit-actions__label--long {
display: none;
}
}
@container commit-actions (max-width: 22rem) {
.commit-actions__label--push {
display: none;
}
}
@container commit-actions (max-width: 19rem) {
.commit-actions__label {
display: none;
}
.commit-actions__btn {
padding-inline: 0.5rem;
}
}
/* Utility: hide scrollbars without affecting layout */
.scrollbar-none {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-none::-webkit-scrollbar {
width: 0;
height: 0;
display: none;
}
/* Git right-sidebar header/actions collapse to icon-only when narrow. */
@container git-header (max-width: 26rem) {
.git-header-label {
display: none;
}
}
/* Diff toolbar: drop low-priority labels only when the context panel is genuinely tight. */
@container diff-toolbar (max-width: 28rem) {
.diff-toolbar__scope-count,
.diff-toolbar__review-label,
.diff-toolbar__expand-label {
display: none;
}
.diff-toolbar__review-button,
.diff-toolbar__expand-button {
padding-inline: 0.5rem;
}
}
/* Status row: collapse optional text when narrow to keep both sides in one line. */
@container status-row (max-width: 30rem) {
.status-row__active-todo {
display: none;
}
}
@container status-row (max-width: 24rem) {
.status-row__changed-label {
display: none;
}
}
/* Assistant message footer: collapse text labels only on very narrow layouts. */
@container message-footer (max-width: 16rem) {
.message-footer__label {
display: none;
}
}
/* Animated tabs: collapse labels based on local container width. */
@container animated-tabs (max-width: 23rem) {
.animated-tabs__label {
display: none;
}
.animated-tabs__button {
padding-inline: 0.5rem;
gap: 0;
}
}
.pill-tabs__track {
padding-inline: 0.125rem;
}
@container pill-tabs (max-width: 23rem) {
.pill-tabs__track {
padding-inline: 1px;
}
.pill-tabs__button {
padding-inline: 0.5rem;
}
}
/* Text font */
.markdown-content {
font-family: var(--font-sans);
font-size: var(--text-markdown);
line-height: inherit;
}
.markdown-content p,
.markdown-content li,
.markdown-content blockquote,
.markdown-content td,
.markdown-content th {
line-height: inherit;
}
/* Restore paragraph block spacing inside markdown content. The
--markdown-paragraph-spacing token existed but was never wired, so Tailwind
preflight's zeroed <p> margins left adjacent paragraphs collapsed into one
visual line. */
.markdown-content p {
margin: 0 0 var(--markdown-paragraph-spacing) 0;
}
/* The markdown renderer wraps each block in a display:contents [data-md-block]
element, so the message-level [&_.markdown-content>*:last-child]:mb-0
nullifiers target the wrapper (where margin is ignored), not the paragraph.
Drop the trailing margin on the last paragraph of the last block directly so
messages don't gain extra space at the bottom. */
.markdown-content > [data-md-block]:last-child p:last-child {
margin-bottom: 0;
}
/* Tool cards (code size) and reasoning (meta size, dimmed) are intentionally
compact — keep their paragraphs tight, matching prior behavior. */
.markdown-content.markdown-tool p,
.markdown-content.markdown-reasoning p {
margin-bottom: 0;
}
/* Tool markdown should render at code size to match tool card typography. */
.markdown-content.markdown-tool {
font-size: var(--text-code) !important;
}
/* Reasoning markdown renders at meta size, dimmed. */
.markdown-content.markdown-reasoning {
font-size: var(--text-markdown);
color: var(--surface-muted-foreground);
--markdown-heading1: inherit;
--markdown-heading2: inherit;
--markdown-heading3: inherit;
--markdown-heading4: inherit;
--markdown-link: inherit;
--markdown-link-hover: var(--surface-foreground);
--markdown-blockquote: inherit;
--markdown-bold: inherit;
--markdown-italic: inherit;
--markdown-strikethrough: inherit;
}
.markdown-content.markdown-reasoning p,
.markdown-content.markdown-reasoning li,
.markdown-content.markdown-reasoning td,
.markdown-content.markdown-reasoning th,
.markdown-content.markdown-reasoning h1,
.markdown-content.markdown-reasoning h2,
.markdown-content.markdown-reasoning h3,
.markdown-content.markdown-reasoning h4,
.markdown-content.markdown-reasoning h5,
.markdown-content.markdown-reasoning h6,
.markdown-content.markdown-reasoning strong,
.markdown-content.markdown-reasoning b,
.markdown-content.markdown-reasoning em,
.markdown-content.markdown-reasoning del,
.markdown-content.markdown-reasoning [data-markdown="strong"],
.markdown-content.markdown-reasoning [data-markdown="bold"] {
color: var(--surface-muted-foreground) !important;
}
.markdown-content.markdown-reasoning a,
.markdown-content.markdown-reasoning [data-openchamber-file-link="true"] {
color: var(--surface-muted-foreground) !important;
}
.markdown-content.markdown-reasoning a:hover,
.markdown-content.markdown-reasoning [data-openchamber-file-link="true"]:hover {
color: var(--surface-foreground) !important;
}
.markdown-content.markdown-reasoning strong,
.markdown-content.markdown-reasoning b,
.markdown-content.markdown-reasoning [data-markdown="strong"],
.markdown-content.markdown-reasoning [data-markdown="bold"] {
font-weight: inherit !important;
color: inherit !important;
font-style: inherit !important;
}
.markdown-content.markdown-tool code,
.markdown-content.markdown-tool pre {
font-size: inherit !important;
}
/* Streamdown headers: keep proportional but not oversized */
.markdown-content h1 {
font-size: 1.125em;
font-weight: 600;
margin-top: 1em;
margin-bottom: 0.5em;
}
.markdown-content h2 {
font-size: 1.0625em;
font-weight: 600;
margin-top: 0.875em;
margin-bottom: 0.375em;
}
.markdown-content h3 {
font-size: 1em;
font-weight: 600;
margin-top: 0.75em;
margin-bottom: 0.25em;
}
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
font-size: 1em;
font-weight: 600;
margin-top: 0.625em;
margin-bottom: 0.25em;
}
/* Code font */
.markdown-content code,
.markdown-content pre {
font-family: var(--font-mono);
font-size: var(--text-code);
}
/* Override Streamdown's hardcoded bg-muted for inline code - use theme colors instead */
.markdown-content code[data-markdown="inline-code"] {
background-color: var(--markdown-inline-code-bg, var(--surface-muted)) !important;
color: var(--markdown-inline-code, var(--foreground)) !important;
word-break: break-all;
overflow-wrap: break-word;
}
/* Markdown headings - use theme colors */
.markdown-content h1 {
color: var(--markdown-heading1, var(--primary));
}
.markdown-content h2 {
color: var(--markdown-heading2, var(--primary));
}
.markdown-content h3 {
color: var(--markdown-heading3, var(--primary));
}
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
color: var(--markdown-heading4, var(--foreground));
}
/* Markdown links - use theme colors */
.markdown-content a {
color: var(--markdown-link, var(--primary));
}
.markdown-content a:hover {
color: var(--markdown-link-hover, var(--primary));
}
.markdown-content [data-openchamber-file-link="true"] {
color: var(--markdown-link, var(--primary));
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
user-select: text !important;
-webkit-user-select: text !important;
}
.markdown-content [data-openchamber-file-link="true"]:hover {
color: var(--markdown-link-hover, var(--primary));
}
/* Agent/skill mentions use the primary accent, not the markdown link color */
.markdown-content a[data-openchamber-agent-mention="true"],
.markdown-content a[data-skill-name] {
color: var(--primary);
}
.markdown-content a[data-openchamber-agent-mention="true"]:hover,
.markdown-content a[data-skill-name]:hover {
color: var(--primary);
}
/* Markdown blockquote - use theme colors */
.markdown-content blockquote {
color: var(--markdown-blockquote, var(--muted-foreground));
border-left-color: var(--markdown-blockquote-border, var(--border));
}
/* Markdown horizontal rule - use theme colors */
.markdown-content hr {
border-color: var(--markdown-hr, var(--border));
}
/* Markdown bold/italic/strikethrough - use theme colors */
.markdown-content strong {
color: var(--markdown-bold, var(--foreground));
}
.markdown-content em {
color: var(--markdown-italic, var(--foreground));
}
.markdown-content del {
color: var(--markdown-strikethrough, var(--muted-foreground));
}
/* Streamdown code blocks: ensure light/dark Shiki vars work even when Tailwind doesn't scan Streamdown's internal class names. */
.markdown-content [data-markdown="code-block-body"] {
font-size: var(--text-code);
background-color: transparent;
color: var(--shiki-light, inherit);
}
.markdown-content [data-markdown="code-block-body"] span[style*="--sdm-c"] {
color: var(--sdm-c) !important;
}
.markdown-content [data-markdown="code-block-body"] span[style*="--sdm-tbg"] {
background-color: var(--sdm-tbg) !important;
}
.markdown-content [data-markdown="code-block-body"] span[style*="--shiki-light"] {
color: var(--shiki-light) !important;
}
.markdown-content [data-markdown="code-block-body"] span[style*="--shiki-light-bg"] {
background-color: var(--shiki-light-bg) !important;
}
.dark .markdown-content [data-markdown="code-block-body"] {
background-color: transparent;
color: var(--shiki-dark, var(--shiki-light, inherit));
}
.dark .markdown-content [data-markdown="code-block-body"] span[style*="--shiki-dark"] {
color: var(--shiki-dark) !important;
}
.dark .markdown-content [data-markdown="code-block-body"] span[style*="--shiki-dark-bg"] {
background-color: var(--shiki-dark-bg) !important;
}
/* Reduce code block header height */
[data-markdown="code-block-header"] {
margin: 0;
padding: 0.375rem 0.75rem;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding-right: 5.25rem;
border: 0;
border-bottom: 1px solid var(--interactive-border);
border-radius: 0;
background: var(--surface-muted);
}
/* Disable Streamdown's code block virtualization and keep block chrome fixed */
.markdown-content [data-markdown="code-block"] {
content-visibility: visible !important;
contain-intrinsic-size: none !important;
position: relative;
display: flex;
flex-direction: column;
padding: 0 !important;
box-sizing: border-box;
inline-size: 100%;
max-inline-size: 100%;
min-inline-size: 0;
overflow: hidden;
background: var(--surface-background);
border: 1px solid var(--interactive-border);
border-radius: 0.75rem;
}
.markdown-content [data-markdown="code-block"] > div:not([data-markdown]) {
position: absolute;
top: 0;
right: 0;
margin-top: 0;
height: 2rem;
z-index: 6;
pointer-events: none;
}
/* Streamdown code block body: scroll text content within the card body */
.markdown-content [data-markdown="code-block-body"] {
display: block;
inline-size: 100%;
min-width: 0;
max-width: 100%;
margin: 0;
padding: 0;
overflow-x: auto;
overflow-y: hidden;
box-sizing: border-box;
scrollbar-gutter: stable;
background: transparent;
border: 0;
border-radius: 0;
}
.markdown-content [data-markdown="code-block-body"] pre {
display: block;
width: 100%;
min-width: 100%;
max-width: 100%;
margin: 0 !important;
padding: 0.75rem !important;
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
background: transparent !important;
}
.markdown-content [data-markdown="code-block-body"] > * {
border: 0 !important;
border-radius: 0 !important;
box-shadow: none !important;
background: transparent !important;
}
.markdown-content [data-markdown="code-block-body"] code {
white-space: pre;
overflow-wrap: normal;
word-break: normal;
background: transparent !important;
}
.markdown-content [data-markdown="code-block-actions"] {
pointer-events: auto;
transition: opacity 0.15s ease;
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
gap: 0.125rem;
}
.markdown-content [data-markdown="code-block-actions"] button {
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
padding: 0.25rem !important;
min-width: 1.5rem;
min-height: 1.5rem;
}
.markdown-content [data-markdown="code-block-actions"] button svg {
width: 0.875rem;
height: 0.875rem;
}
@media (hover: hover) and (pointer: fine) {
.markdown-content [data-markdown="code-block"] [data-markdown="code-block-actions"] {
opacity: 0;
pointer-events: none;
}
.markdown-content [data-markdown="code-block"]:hover [data-markdown="code-block-actions"],
.markdown-content [data-markdown="code-block"]:focus-within [data-markdown="code-block-actions"] {
opacity: 1;
pointer-events: auto;
}
}
/* Mermaid block: position context for controls wrapper. */
[data-markdown="mermaid-block"] {
position: relative;
border-radius: 0.75rem;
border: 1px solid var(--interactive-border);
background: var(--surface-elevated);
padding: 0.5rem;
}
[data-markdown="mermaid-block"] [data-markdown="mermaid-scroll"] {
overflow: auto;
min-width: 0;
}
[data-markdown="mermaid-block"][data-mermaid-render="svg"] [data-markdown="mermaid-scroll"] {
overflow: hidden;
}
[data-markdown="mermaid-block"] [data-markdown="mermaid-viewport"] {
width: 100%;
min-width: 0;
height: min(24rem, 70vh);
min-height: 12rem;
overflow: hidden;
touch-action: none;
border-radius: 0.5rem;
background: var(--surface-elevated);
}
[data-markdown="mermaid-block"] [data-markdown="mermaid"] {
width: 100%;
height: 100%;
display: block;
}
[data-markdown="mermaid-block"][data-mermaid-panning="true"] [data-markdown="mermaid-viewport"] {
cursor: grabbing;
}
[data-markdown="mermaid-block"] [data-markdown="mermaid-ascii"] {
margin: 0;
white-space: pre;
width: max-content;
min-width: 100%;
font-family: var(--font-mono);
color: var(--surface-foreground);
background: transparent;
}
[data-markdown="mermaid-block"] [data-markdown="mermaid"] svg {
width: 100% !important;
height: 100% !important;
min-width: 0;
max-width: none !important;
display: block;
background: var(--surface-elevated);
}
/* Fullscreen Mermaid popup: scale diagrams to fill available viewport panel area. */
.markdown-mermaid-fullscreen {
height: 100%;
width: 100%;
}
.markdown-mermaid-fullscreen .markdown-content {
height: 100%;
width: 100%;
}
.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] {
width: 100%;
height: 100%;
min-height: 100%;
padding: 0.75rem;
border: 0;
border-radius: 0;
background: var(--surface-elevated);
}
.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] [data-markdown="mermaid-scroll"] {
width: 100%;
height: 100%;
overflow: auto;
}
.markdown-mermaid-fullscreen [data-markdown="mermaid-block"][data-mermaid-render="svg"] [data-markdown="mermaid-scroll"] {
overflow: hidden;
}
.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] [data-markdown="mermaid-viewport"] {
width: 100%;
height: 100%;
min-width: 100%;
min-height: 100%;
overflow: hidden;
}
.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] [data-markdown="mermaid"] {
width: 100%;
height: 100%;
display: block;
}
.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] [data-markdown="mermaid-ascii"] {
margin: 0;
white-space: pre;
width: max-content;
min-width: 100%;
min-height: 100%;
font-family: var(--font-mono);
color: var(--surface-foreground);
background: transparent;
}
.markdown-mermaid-fullscreen [data-markdown="mermaid-block"] [data-markdown="mermaid"] svg {
width: 100% !important;
height: 100% !important;
max-width: none !important;
max-height: none !important;
min-width: 100%;
display: block;
background: transparent;
}
/* Terminal touch input is portaled to document.body; keep it fully non-painting across engines. */
input[data-terminal-hidden-input="true"],
textarea[data-terminal-hidden-input="true"] {
caret-color: transparent !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
background: transparent !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
text-shadow: none !important;
-webkit-appearance: none;
appearance: none;
}
input[data-terminal-hidden-input="true"]::selection,
textarea[data-terminal-hidden-input="true"]::selection {
background: transparent;
color: transparent;
}
input[data-terminal-hidden-input="true"]::placeholder,
textarea[data-terminal-hidden-input="true"]::placeholder {
color: transparent !important;
}
/* Hide browser/system caret for Ghostty's own input surfaces.
Terminal cursor is rendered by the canvas renderer. */
.terminal-viewport-container[contenteditable="true"][aria-label="Terminal input"],
.terminal-viewport-container [contenteditable="true"][aria-label="Terminal input"] {
caret-color: transparent !important;
outline: none !important;
}
textarea[aria-label="Terminal input"],
input[aria-label="Terminal input"] {
caret-color: transparent !important;
outline: none !important;
}
/* When touch terminal input uses the portaled overlay, keep Ghostty's
internal editable surfaces fully non-painting to avoid duplicate cursors. */
.terminal-viewport-container[data-hidden-input-overlay-active="true"] textarea:not([data-terminal-hidden-input="true"]),
.terminal-viewport-container[data-hidden-input-overlay-active="true"] input:not([data-terminal-hidden-input="true"]),
.terminal-viewport-container[data-hidden-input-overlay-active="true"] [contenteditable="true"] {
caret-color: transparent !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
background: transparent !important;
border: 0 !important;
outline: none !important;
box-shadow: none !important;
text-shadow: none !important;
}
.terminal-viewport-container[data-hidden-input-overlay-active="true"] textarea:not([data-terminal-hidden-input="true"]),
.terminal-viewport-container[data-hidden-input-overlay-active="true"] input:not([data-terminal-hidden-input="true"]),
.terminal-viewport-container[data-hidden-input-overlay-active="true"] [contenteditable="true"] {
opacity: 0 !important;
font-size: 0 !important;
line-height: 0 !important;
pointer-events: none !important;
user-select: none !important;
-webkit-user-select: none !important;
}
/* Mobile Terminal Optimizations */
@media (max-width: 1024px) {
.terminal-viewport-container {
font-variant-ligatures: none;
font-feature-settings: "liga" 0, "calt" 0;
}
}
/* Font loading states */
.fonts-loading .terminal-viewport-container {
font-family: monospace;
}
.fonts-loaded .terminal-viewport-container {
font-family: "JetBrainsMono Nerd Font", "FiraCode Nerd Font", "Fira Code", "JetBrains Mono", "SFMono-Regular", Menlo, Consolas, "Liberation Mono", "Courier New", monospace;
}
@keyframes oc-busy-pulse {
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 1;
}
}
.animate-busy-pulse {
animation: oc-busy-pulse 1.2s ease-in-out infinite;
}
@keyframes navrail-dot-wave {
0%, 100% {
transform: scale(0.72);
opacity: 0.52;
}
50% {
transform: scale(1);
opacity: 1;
}
}
.animate-navrail-dot-wave {
animation: navrail-dot-wave 1.2s ease-in-out infinite;
}
@keyframes border-glow-pulse {
0%, 100% {
border-color: var(--interactive-border);
outline-color: transparent;
}
12.5% {
border-color: color-mix(in srgb, var(--primary) 22%, var(--interactive-border));
outline-color: color-mix(in srgb, var(--primary) 8%, transparent);
}
25% {
border-color: color-mix(in srgb, var(--primary) 40%, var(--interactive-border));
outline-color: color-mix(in srgb, var(--primary) 16%, transparent);
}
37.5% {
border-color: color-mix(in srgb, var(--primary) 62%, var(--interactive-border));
outline-color: color-mix(in srgb, var(--primary) 26%, transparent);
}
50% {
border-color: color-mix(in srgb, var(--primary) 78%, var(--interactive-border));
outline-color: color-mix(in srgb, var(--primary) 34%, transparent);
}
62.5% {
border-color: color-mix(in srgb, var(--primary) 62%, var(--interactive-border));
outline-color: color-mix(in srgb, var(--primary) 26%, transparent);
}
75% {
border-color: color-mix(in srgb, var(--primary) 40%, var(--interactive-border));
outline-color: color-mix(in srgb, var(--primary) 16%, transparent);
}
87.5% {
border-color: color-mix(in srgb, var(--primary) 22%, var(--interactive-border));
outline-color: color-mix(in srgb, var(--primary) 8%, transparent);
}
}
.animate-border-glow-pulse {
outline: 1px solid transparent;
outline-offset: 1px;
will-change: border-color, outline-color;
animation: border-glow-pulse 3.8s linear infinite;
}
@keyframes marquee-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
@keyframes open-file-name-marquee-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
.open-file-name-marquee-track {
display: inline-flex;
min-width: max-content;
animation: open-file-name-marquee-scroll var(--open-file-name-marquee-duration, 14s) linear infinite;
will-change: transform;
}
.open-file-name-marquee-item {
flex-shrink: 0;
padding-right: 2rem;
}
.marquee-text {
display: inline-block;
white-space: nowrap;
}
.marquee-text--auto {
animation: marquee-scroll 6s linear infinite alternate;
will-change: transform;
}
.group:hover .marquee-text--active,
.marquee-text--active:hover {
animation: marquee-scroll 5s linear infinite alternate;
}
@media (prefers-reduced-motion: reduce) {
.marquee-text--auto,
.open-file-name-marquee-track,
.group:hover .marquee-text--active,
.marquee-text--active:hover {
animation: none;
}
.open-file-name-marquee-track {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
will-change: auto;
}
.open-file-name-marquee-track .open-file-name-marquee-item {
display: inline;
padding-right: 0;
}
.open-file-name-marquee-track .open-file-name-marquee-item[aria-hidden="true"] {
display: none;
}
.animate-busy-pulse {
animation: none;
}
}
/* Settings dialog: hide the overlay scrollbar; wheel/keyboard scroll still works. */
[data-settings-view="true"] .overlay-scrollbar {
display: none;
}
/* Desktop app shell (Electron/native window): native apps use the arrow cursor
for clickable controls, not the web's hand/pointer. Neutralize pointer
cursors under the desktop runtime only — web in a browser keeps them.
Resize/grab/text cursors are untouched. */
html.desktop-runtime
:where(button, [role="button"], a[href], summary, label[for]):not(:disabled):not([aria-disabled="true"]) {
cursor: default;
}
html.desktop-runtime [class*="cursor-pointer"] {
cursor: default !important;
}
html.desktop-runtime .markdown-content [data-openchamber-file-link="true"] {
cursor: default;
}