Files
openchamber/packages/ui/src/index.css
T
Bohdan Triapitsyn 043e71d725 fix(ui): give the scroll shadow size variable a real default
The base rule declared --scroll-shadow-size as var(--scroll-shadow-size,
48px) — a self-referential cycle, which computes to invalid and silently
resolves every mask below to none. The old ScrollShadow component masked
the bug by setting the variable inline; the hook-based path (the chat
scroller) sets only the data attributes, so the chat lost its top and
bottom scroll fades. A concrete 48px default restores them; inline
overrides from the component still win.
2026-08-26 00:09:34 +03:00

1904 lines
50 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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;
}
/* 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;
}
/* Fractional line heights (1.625 × font-size) make streamed content grow by
sub-pixel amounts while scroll positions quantize to the device-pixel grid.
The residue accumulates and bottom-anchored rows (status indicator) creep
and snap by 1px per few lines during streaming. Rounding the line height to
whole pixels keeps content growth on the pixel grid. */
.message-content-text.leading-relaxed,
.message-content-text .leading-relaxed {
line-height: round(1.625em, 1px);
}
:root.light .message-content-text::selection,
:root.light .message-content-text ::selection,
:root.light .oc-file-preview::selection,
:root.light .oc-file-preview ::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);
}
/* Quoted chat fragment kept visibly highlighted while its comment input is
open (the native selection collapses once the input takes focus). The
rectangles come from the selection's own client rects, so shape and shade
both match the native selection that was just showing. */
.oc-chat-comment-rect {
background-color: var(--interactive-selection);
}
:root.light .oc-chat-comment-rect {
background-color: color-mix(in srgb, var(--interactive-border-focus) 18%, 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 [data-chat-input="true"] .cm-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 */
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"] {
/* A concrete default, not var(--scroll-shadow-size, 48px): a custom
property referencing itself is a cycle and computes to invalid, which
silently killed every mask below for consumers that don't set the
variable inline (the hook-based chat scroller). Inline styles from the
ScrollShadow component still override this. */
--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%,
transparent var(--scroll-shadow-top-clear-size, 0px),
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
#000 calc(100% - var(--scroll-shadow-size)),
transparent 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
transparent var(--scroll-shadow-top-clear-size, 0px),
#000 var(--scroll-shadow-top-size, 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%,
transparent var(--scroll-shadow-top-clear-size, 0px),
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
#000 100%
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent 0%,
transparent var(--scroll-shadow-top-clear-size, 0px),
#000 var(--scroll-shadow-top-size, var(--scroll-shadow-size)),
#000 100%
);
}
/* Sticky-fade overlay: the crisp duplicate identity stays
mounted and its visibility is driven synchronously by the scroller's own
top-scroll data attribute — the same signal that engages the mask — so the
overlay appears in the exact frame the mask hides the real header (no blink).
ScrollShadow sets data-top-scroll when nothing is below the viewport and
data-top-bottom-scroll when content is both above and below. */
.oc-sticky-fade-overlay {
opacity: 0;
}
.oc-sticky-fade-root:has(.oc-sticky-fade-scroller[data-top-scroll="true"]) .oc-sticky-fade-overlay,
.oc-sticky-fade-root:has(.oc-sticky-fade-scroller[data-top-bottom-scroll="true"]) .oc-sticky-fade-overlay {
opacity: 1;
}
[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;
}
/* Fully hidden scrollbar for small mobile overlays (session switcher etc.)
where a visible bar reads as chrome noise. Scrolling stays intact. */
.oc-hide-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}
.oc-hide-scrollbar::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
[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;
/* Above sticky zone headers (z-20) — the thumb must never dive under
a stuck sidebar header while scrolling. */
z-index: 30;
}
.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: the walkthrough label goes first. Review is the primary action
in this row, so it keeps its label longest; the walkthrough is reachable from
the rail as well and reads fine as an icon. */
@container diff-toolbar (max-width: 36rem) {
.diff-toolbar__walkthrough-label {
display: none;
}
.diff-toolbar__walkthrough-button {
padding-inline: 0.5rem;
}
}
/* Then the rest, once the 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;
}
}
/* Pull request actions: the walkthrough label goes first, since merge controls
are the reason the row exists. */
@container pr-actions (max-width: 30rem) {
.pr-actions__walkthrough-label {
display: none;
}
.pr-actions__walkthrough-button {
padding-inline: 0.5rem;
}
}
/* Hide the long active todo before it can collide with the changed-files summary. */
@container status-row (max-width: 38rem) {
.status-row__active-todo {
display: none;
}
}
/* Hide the secondary changed-files label on narrow mobile layouts. */
@container status-row (max-width: 30rem) {
.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;
}
}
/*
* Active pill indicator motion. Transitions are opt-in via the --is-animated
* modifier so first measurement and container resizes reposition instantly
* instead of sliding in from the track origin.
*/
.pill-tabs__indicator--is-animated {
/* Decelerating ease with no overshoot: the indicator settles rather than
bouncing back at the end of its travel. */
transition:
transform 280ms cubic-bezier(0.22, 1, 0.36, 1),
width 260ms cubic-bezier(0.22, 1, 0.36, 1),
height 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Non-composited variant (left/top-positioned indicator, see
nonCompositedIndicator in sortable-tabs-strip): same travel curve. */
.pill-tabs__indicator--is-animated-layout {
transition:
left 280ms cubic-bezier(0.22, 1, 0.36, 1),
top 260ms cubic-bezier(0.22, 1, 0.36, 1),
width 260ms cubic-bezier(0.22, 1, 0.36, 1),
height 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Underline indicator shares the pill's travel curve so both tab styles feel
like the same control. */
.underline-tabs__indicator--is-animated {
transition:
transform 280ms cubic-bezier(0.22, 1, 0.36, 1),
width 260ms cubic-bezier(0.22, 1, 0.36, 1);
}
.pill-tabs__button {
transition-property: color, transform;
transition-duration: 150ms;
transition-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
.pill-tabs__button:active {
transform: scale(0.97);
}
@media (prefers-reduced-motion: reduce) {
.pill-tabs__indicator--is-animated,
.pill-tabs__indicator--is-animated-layout,
.underline-tabs__indicator--is-animated {
transition: none;
}
.pill-tabs__button:active {
transform: none;
}
}
/* 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;
}
/* The marked renderer uses its own code-body wrapper instead of Streamdown's. */
.markdown-content [data-md-code-body],
.markdown-content [data-md-code-body] pre,
.markdown-content [data-md-code-body] code,
.markdown-content [data-md-code-body] [data-md-code-lines] {
background: transparent !important;
}
.markdown-content [data-md-code-lines] {
display: block;
min-width: 100%;
}
.markdown-content [data-md-code-line] {
display: grid;
grid-template-columns: 2rem minmax(0, 1fr);
column-gap: 0.75rem;
min-width: 100%;
}
/* First uncached open of a session shows a hydration skeleton; the real
timeline replacing it fades in once instead of popping. Cached session
switches never carry this class and stay instant. */
@keyframes oc-chat-hydration-reveal {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.oc-chat-hydration-reveal {
animation: oc-chat-hydration-reveal 180ms ease-out both;
}
@media (prefers-reduced-motion: reduce) {
.oc-chat-hydration-reveal {
animation: none;
}
}
/* A block committed mid-stream enters with a fade and a gentle rise —
compositor-only properties, deliberately nothing heavier: during a long
stream the follow scroll masks the entrance anyway, so the effect only
really shows on short replies, and those must not pay for a GPU filter.
Blocks committed in the same tick cascade via --oc-md-enter-delay set
inline by the renderer. */
@keyframes oc-md-block-enter {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: none;
}
}
.oc-md-block-enter {
animation: oc-md-block-enter 320ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: var(--oc-md-enter-delay, 0ms);
}
@media (prefers-reduced-motion: reduce) {
.oc-md-block-enter {
animation: none;
}
}
/* While streaming defers the per-line gutter markup, hold its horizontal
footprint (2rem column + 0.75rem gap) so the finished pass only fills in
the numbers instead of shifting every code line. */
.markdown-content pre[data-md-gutter-reserved] > code {
display: block;
padding-left: 2.75rem;
}
.markdown-content [data-md-code-line-number] {
align-self: stretch;
padding-right: 0.75rem;
border-right: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
color: color-mix(in srgb, var(--muted-foreground) 45%, transparent);
font-variant-numeric: tabular-nums;
text-align: right;
user-select: none;
white-space: nowrap;
}
.markdown-content [data-md-code-line-content] {
min-width: 0;
}
.markdown-content [data-md-code-line-break] {
display: none;
}
[data-message-image-export="true"] [data-md-code-body] {
overflow: visible !important;
}
[data-message-image-export="true"] [data-md-code-line-content] {
white-space: pre-wrap !important;
overflow-wrap: anywhere !important;
}
[data-message-image-export="true"] [data-md-code-actions] {
display: none !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;
}
/* Header session tabs: thin separators between inactive neighbours, hidden
around the active or hovered tab (mirrors the titlebar tab language). */
.session-tab-slot {
position: relative;
}
.session-tab-slot:not(:first-child):not([data-active='true'])::before {
content: '';
position: absolute;
top: 8px;
inset-inline-start: -2.75px;
width: 1.5px;
height: 12px;
border-radius: 9999px;
background: var(--border);
}
.session-tab-slot[data-active='true'] + .session-tab-slot::before,
.session-tab-slot:not([data-active='true']):hover::before,
.session-tab-slot:not([data-active='true']):hover + .session-tab-slot::before {
display: none;
}
/* Header session tabs scroller: never show a scrollbar (it shifts the header
content vertically); overflow is communicated by the edge fades. */
.session-tabs-scroll {
scrollbar-width: none;
-ms-overflow-style: none;
}
.session-tabs-scroll::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
/* Session tab titles: fade out instead of "..." — the ellipsis reads as
clutter next to the tab's status dot and hover controls. Short titles
never reach the fade zone (the span spans the tab, not the text). The
fade belongs to the resting state only: while the tab is hovered or its
controls are open, the title clips hard so the smear never sits next to
the menu/close/rename controls. */
.session-tab-title {
-webkit-mask-image: linear-gradient(to right, black calc(100% - 14px), transparent);
mask-image: linear-gradient(to right, black calc(100% - 14px), transparent);
}
.session-tab:hover .session-tab-title,
.session-tab[data-controls-open='true'] .session-tab-title {
-webkit-mask-image: none;
mask-image: none;
}