hermes-agent/apps/desktop/src/styles.css
brooklyn! f9d7bca252
fix(desktop): overlay scrollbars on conversation code blocks (#73670)
The app-wide `.scrollbar-dt *` webkit rules force classic always-on
gutters on every descendant scroller. Opt code-card scroll surfaces out
via `.scrollbar-overlay` so Electron keeps platform overlay bars (fade
in on scroll, no reserved track).
2026-07-28 22:13:54 +00:00

2025 lines
71 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';
@plugin '@tailwindcss/typography';
@import 'tw-shimmer';
@import 'katex/dist/katex.min.css';
@import '@vscode/codicons/dist/codicon.css';
@custom-variant dark (&:is(.dark *));
/* Blanket reduced-motion override: kill ALL CSS animations and transitions
when the user (or the E2E test harness via prefers-reduced-motion) requests
it. Per-component @media rules below handle specific cases; this catches
everything else so overlays and loading bars resolve instantly instead of
being caught mid-fade by a screenshot. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Sidebar sections: tall viewports give each its own scroller; compact ones
(this variant) flatten everything into one shared scroll. See ChatSidebar. */
@custom-variant compact (@media (max-height: 768px));
@font-face {
font-family: 'Collapse';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('../../../node_modules/@nous-research/ui/dist/fonts/Collapse-Bold.woff2') format('woff2');
}
/* JetBrains Mono — bundled terminal font (Apache-2.0) so bold/italic share the
regular face's metrics instead of squeezing against a system fallback. */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('./fonts/JetBrainsMono-Regular.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('./fonts/JetBrainsMono-Bold.woff2') format('woff2');
}
@font-face {
font-family: 'JetBrains Mono';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url('./fonts/JetBrainsMono-Italic.woff2') format('woff2');
}
@theme inline {
--color-background: var(--dt-background);
--color-foreground: var(--dt-foreground);
--color-card: var(--dt-card);
--color-card-foreground: var(--dt-card-foreground);
--color-muted: var(--dt-muted);
--color-muted-foreground: var(--dt-muted-foreground);
--color-popover: var(--dt-popover);
--color-popover-foreground: var(--dt-popover-foreground);
--color-primary: var(--dt-primary);
--color-primary-foreground: var(--dt-primary-foreground);
--color-secondary: var(--dt-secondary);
--color-secondary-foreground: var(--dt-secondary-foreground);
--color-accent: var(--dt-accent);
--color-accent-foreground: var(--dt-accent-foreground);
--color-border: var(--dt-border);
--color-input: var(--dt-input);
--color-ring: var(--dt-ring);
--color-destructive: var(--dt-destructive);
--color-destructive-foreground: var(--dt-destructive-foreground);
--color-midground: var(--dt-midground);
--color-midground-foreground: var(--dt-midground-foreground);
--font-sans: var(--dt-font-sans);
--font-mono: var(--dt-font-mono);
--spacing-mul: var(--dt-spacing-mul, 1);
--radius-xs: calc(var(--radius-scalar) * 0.125rem);
--radius-sm: calc(var(--radius-scalar) * 0.5rem);
--radius-md: calc(var(--radius-scalar) * 0.625rem);
--radius-lg: calc(var(--radius-scalar) * 0.75rem);
--radius-xl: calc(var(--radius-scalar) * 1rem);
--radius-2xl: calc(var(--radius-scalar) * 1.5rem);
--radius-3xl: calc(var(--radius-scalar) * 2rem);
--radius-4xl: calc(var(--radius-scalar) * 2.5rem);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--shadow-xs: 0 0.0625rem 0.125rem color-mix(in srgb, #000 5%, transparent);
--shadow-sm:
0 0 0 0.0625rem color-mix(in srgb, var(--dt-foreground) 6%, transparent),
0 0.125rem 0.5rem color-mix(in srgb, #000 4%, transparent);
--shadow-md:
0 0 0 0.0625rem color-mix(in srgb, var(--dt-foreground) 8%, transparent),
0 0.25rem 1rem color-mix(in srgb, #000 8%, transparent),
0 1rem 2rem -1.5rem color-mix(in srgb, #000 18%, transparent);
/* Soft floating shadow for borderless modals/overlays. Single top light
source: every layer is centered (x=0) and cast downward, with negative
spread that grows with the blur so each layer is pulled horizontally inward
— the shadow pools below the panel instead of bleeding out every side.
Layered (contact → ambient) for a smooth, natural falloff. */
--shadow-nous:
0 0.125rem 0.25rem -0.125rem color-mix(in srgb, #000 7%, transparent),
0 0.5rem 0.75rem -0.375rem color-mix(in srgb, #000 6%, transparent),
0 1.25rem 1.75rem -0.875rem color-mix(in srgb, #000 6%, transparent),
0 2.25rem 3rem -1.75rem color-mix(in srgb, #000 0%, transparent);
/* Hairline border paired with --shadow-nous on borderless overlays.
currentColor resolves per-element, so it adapts to text color/theme. */
--stroke-nous: color-mix(in srgb, currentColor 3%, transparent);
--shadow-lg:
inset 0 0.0625rem 0 color-mix(in srgb, #fff 28%, transparent),
0 0 0 0.0625rem color-mix(in srgb, var(--dt-foreground) 8%, transparent),
0 0.75rem 2rem color-mix(in srgb, #000 12%, transparent);
--shadow-composer: 0 0.0625rem 0.125rem color-mix(in srgb, #000 5%, transparent);
}
@layer base {
:root {
color-scheme: light;
--theme-foreground: #17171a;
--theme-primary: #0053fd;
--theme-secondary: color-mix(in srgb, #0053fd 7%, #ffffff);
--theme-accent-soft: color-mix(in srgb, #0053fd 10%, #ffffff);
--theme-midground: #0053fd;
--theme-warm: #cf806d;
--theme-background-seed: #f8faff;
--theme-sidebar-seed: #f3f7ff;
--theme-card-seed: #ffffff;
--theme-elevated-seed: #ffffff;
--theme-bubble-seed: color-mix(in srgb, #0053fd 6%, #ffffff);
--theme-neutral-chrome: #f3f3f3;
--theme-neutral-sidebar: #f3f3f3;
--theme-neutral-card: #fcfcfc;
--theme-mix-chrome: 92%;
--theme-mix-sidebar: 100%;
--theme-mix-card: 22%;
--theme-mix-elevated: 28%;
--theme-mix-bubble: 0%;
--theme-fill-primary-accent-mix: 16%;
--theme-fill-secondary-accent-mix: 11%;
--theme-fill-tertiary-accent-mix: 8%;
--theme-fill-quaternary-accent-mix: 5%;
--theme-fill-quinary-accent-mix: 3%;
--theme-stroke-primary-accent-mix: 24%;
--theme-stroke-secondary-accent-mix: 16%;
--theme-stroke-tertiary-accent-mix: 10%;
--theme-stroke-quaternary-accent-mix: 6%;
--theme-row-hover-accent-mix: 4%;
--theme-row-active-accent-mix: 8%;
--theme-control-hover-accent-mix: 6%;
--theme-control-active-accent-mix: 8%;
--ui-base: var(--theme-foreground);
--ui-accent: var(--theme-midground);
--ui-accent-secondary: var(--theme-primary);
--ui-warm: var(--theme-warm);
--ui-red: #cf2d56;
--ui-orange: #db704b;
--ui-yellow: #c08532;
--ui-green: #1f8a65;
--ui-cyan: #4c7f8c;
--ui-blue: #0053fd;
--ui-purple: #9e94d5;
--context-usage-system: color-mix(in srgb, var(--ui-base) 55%, transparent);
--context-usage-tools: var(--ui-purple);
--context-usage-rules: var(--ui-green);
--context-usage-skills: var(--ui-yellow);
--context-usage-mcp: color-mix(in srgb, var(--ui-red) 72%, var(--ui-purple));
--context-usage-subagents: color-mix(in srgb, var(--ui-blue) 70%, var(--ui-cyan));
--context-usage-memory: color-mix(in srgb, var(--ui-orange) 80%, var(--ui-yellow));
--context-usage-conversation: var(--ui-cyan);
/* Diff add/remove, derived from the semantic palette so every diff surface
(tool cards, preview, review pane) tracks the theme's green/red. Only the
foregrounds need a dark override — they mix toward the page instead of
away from it. */
--ui-diff-add-border: var(--ui-green);
--ui-diff-add-background: color-mix(in srgb, var(--ui-green) 12%, transparent);
--ui-diff-add-foreground: color-mix(in srgb, var(--ui-green) 70%, #000);
--ui-diff-remove-border: var(--ui-red);
--ui-diff-remove-background: color-mix(in srgb, var(--ui-red) 12%, transparent);
--ui-diff-remove-foreground: color-mix(in srgb, var(--ui-red) 70%, #000);
/* Overlay ladder. DESIGN.md: app-wide surfaces must not compete through
ad-hoc z-index literals — pick the rung that describes the surface.
Values are deliberately sparse so a one-off can slot between two rungs
without a renumber. Local stacking inside a component (a sticky header
over its own scroll area) stays on plain `z-10`/`z-20`; these rungs are
only for surfaces that float over the app. */
--z-modal-backdrop: 120;
--z-modal: 130;
/* A select/dropdown/popover opened from inside a modal, portaled to body. */
--z-modal-popover: 140;
/* Must clear any open modal: toasts, tooltips, command-surface backdrops. */
--z-over-modal: 200;
--z-over-modal-content: 210;
--z-switcher-backdrop: 219;
--z-switcher: 220;
/* Boot and blocking states, in the order they can stack. */
--z-connecting: 1200;
--z-onboarding: 1300;
--z-onboarding-popover: 1310;
--z-setup: 1400;
--z-crash: 1500;
--ui-bg-chrome: color-mix(
in srgb,
var(--theme-background-seed) var(--theme-mix-chrome),
var(--theme-neutral-chrome)
);
--ui-bg-sidebar: color-mix(
in srgb,
var(--theme-sidebar-seed) var(--theme-mix-sidebar),
var(--theme-neutral-sidebar)
);
--ui-bg-editor: color-mix(in srgb, var(--theme-card-seed) var(--theme-mix-card), var(--theme-neutral-card));
--ui-bg-elevated: color-mix(
in srgb,
var(--theme-elevated-seed) var(--theme-mix-elevated),
var(--theme-neutral-card)
);
--ui-bg-card: color-mix(in srgb, var(--ui-accent) 4%, color-mix(in srgb, var(--ui-base) 4%, transparent));
--ui-bg-input: #fcfcfc;
--ui-bg-primary: color-mix(
in srgb,
var(--ui-accent) var(--theme-fill-primary-accent-mix),
color-mix(in srgb, var(--ui-base) 10%, transparent)
);
--ui-bg-secondary: color-mix(
in srgb,
var(--ui-accent) var(--theme-fill-secondary-accent-mix),
color-mix(in srgb, var(--ui-base) 7%, transparent)
);
--ui-bg-tertiary: color-mix(
in srgb,
var(--ui-accent) var(--theme-fill-tertiary-accent-mix),
color-mix(in srgb, var(--ui-base) 5%, transparent)
);
--ui-bg-quaternary: color-mix(
in srgb,
var(--ui-accent) var(--theme-fill-quaternary-accent-mix),
color-mix(in srgb, var(--ui-base) 4%, transparent)
);
--ui-bg-quinary: color-mix(
in srgb,
var(--ui-accent) var(--theme-fill-quinary-accent-mix),
color-mix(in srgb, var(--ui-base) 3%, transparent)
);
--ui-row-hover-background: color-mix(
in srgb,
var(--ui-accent) var(--theme-row-hover-accent-mix),
color-mix(in srgb, var(--ui-base) 3%, transparent)
);
--ui-row-active-background: color-mix(
in srgb,
var(--ui-accent) var(--theme-row-active-accent-mix),
color-mix(in srgb, var(--ui-base) 5%, transparent)
);
--ui-control-hover-background: color-mix(
in srgb,
var(--ui-accent) var(--theme-control-hover-accent-mix),
color-mix(in srgb, var(--ui-base) 4%, transparent)
);
--ui-control-active-background: color-mix(
in srgb,
var(--ui-accent) var(--theme-control-active-accent-mix),
color-mix(in srgb, var(--ui-base) 5%, transparent)
);
--ui-text-primary: color-mix(in srgb, var(--ui-base) 94%, transparent);
--ui-text-secondary: color-mix(in srgb, var(--ui-base) 74%, transparent);
--ui-text-tertiary: color-mix(in srgb, var(--ui-base) 54%, transparent);
--ui-text-quaternary: color-mix(in srgb, var(--ui-base) 36%, transparent);
/* Transcript scaffolding — thinking headers, settled tool runs, the live
activity ticker. One colour for all of them (see `ScaffoldRow`), pitched
between the secondary and tertiary greys those lines used to pick
individually, and dimmed once more by the fade rule below so the prose
reading column stays primary. */
--conversation-scaffold-text: color-mix(in srgb, var(--ui-base) 64%, transparent);
--conversation-scaffold-meta: color-mix(in srgb, var(--ui-base) 44%, transparent);
--ui-stroke-primary: color-mix(
in srgb,
var(--ui-accent) var(--theme-stroke-primary-accent-mix),
color-mix(in srgb, var(--ui-base) 10%, transparent)
);
--ui-stroke-secondary: color-mix(
in srgb,
var(--ui-accent) var(--theme-stroke-secondary-accent-mix),
color-mix(in srgb, var(--ui-base) 7%, transparent)
);
--ui-stroke-tertiary: color-mix(
in srgb,
var(--ui-accent) var(--theme-stroke-tertiary-accent-mix),
color-mix(in srgb, var(--ui-base) 5%, transparent)
);
--ui-stroke-quaternary: color-mix(
in srgb,
var(--ui-accent) var(--theme-stroke-quaternary-accent-mix),
color-mix(in srgb, var(--ui-base) 3%, transparent)
);
--ui-sash-hover-border: color-mix(in srgb, var(--ui-accent) 18%, var(--ui-stroke-tertiary));
--ui-sash-hover-background: color-mix(in srgb, var(--ui-accent) 6%, transparent);
--ui-surface-background: var(--ui-bg-editor);
--ui-sidebar-surface-background: var(--ui-bg-sidebar);
--ui-chat-surface-background: var(--ui-bg-chrome);
--ui-editor-surface-background: var(--ui-bg-chrome);
--ui-chat-bubble-background: color-mix(
in srgb,
var(--theme-bubble-seed) var(--theme-mix-bubble),
var(--theme-neutral-card)
);
--ui-chat-bubble-opaque-background: var(--ui-bg-editor);
--ui-inline-code-background: color-mix(in srgb, #141414 5%, transparent);
--ui-inline-code-foreground: color-mix(in srgb, #141414 88%, transparent);
--ui-selection-background: color-mix(in srgb, #ffd24a 55%, transparent);
--dt-background: var(--ui-bg-chrome);
--dt-foreground: var(--ui-text-primary);
--dt-card: var(--ui-bg-editor);
--dt-card-foreground: var(--ui-text-primary);
--dt-muted: var(--ui-bg-tertiary);
--dt-muted-foreground: var(--ui-text-tertiary);
--dt-popover: color-mix(in srgb, var(--ui-bg-elevated) 96%, transparent);
--dt-popover-foreground: var(--ui-text-primary);
--dt-primary: var(--theme-primary);
--dt-primary-foreground: #fcfcfc;
--dt-secondary: var(--theme-secondary);
--dt-secondary-foreground: var(--ui-text-secondary);
--dt-accent: var(--theme-accent-soft);
--dt-accent-foreground: var(--ui-text-primary);
--dt-border: var(--ui-stroke-secondary);
--dt-input: var(--ui-stroke-primary);
/* THE single knob for input-field borders: the resting alpha (% of the ring
color). Hover doubles it; focus/open go full. 0% = invisible at rest. */
--dt-input-border: 7%;
/* Knob for input-field background fill: alpha (% of --dt-card) across all
states. 100% = fully opaque, lower = translucent over the blurred chrome. */
--dt-input-bg: 0%;
/* Classic recessed "inset" — a crisp 1px inner shadow at the TOP edge.
:root.dark bumps the alpha since a dark card swallows shadow. Removed on
focus (the focus border carries the state). */
--dt-input-inset: inset 0 1px 1px color-mix(in srgb, #000 10%, transparent);
/* Hovered inactive tab: deepen it AWAY from the lighter active surface, so
hover can never be mistaken for selected. Dark mode needs more — black
over a dark gutter shows less. */
--ui-tab-hover-darken: 2.5%;
--dt-ring: var(--ui-stroke-primary);
--dt-midground: var(--theme-midground);
--dt-composer-ring: var(--ui-base);
--dt-destructive: #cf2d56;
--dt-destructive-foreground: #ffffff;
--dt-sidebar-bg: var(--ui-bg-sidebar);
--dt-sidebar-border: var(--ui-stroke-secondary);
--dt-user-bubble: var(--ui-chat-bubble-background);
--dt-user-bubble-border: var(--ui-stroke-tertiary);
--dt-font-sans:
'Segoe WPC', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;
/* Key caps always use the native UI face — never theme typography overrides. */
--dt-font-kbd: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
/* JetBrains Mono first — the face we bundle (@font-face above) and the
terminal's primary — so code/diff match the terminal on every platform
instead of drifting to a system Cascadia Code where it's installed. */
--dt-font-mono:
'JetBrains Mono', 'Cascadia Code', 'SF Mono', ui-monospace, Menlo, Consolas, monospace, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;
--dt-base-size: 1rem;
--dt-line-height: 1.5;
--dt-letter-spacing: 0;
--dt-spacing-mul: 1;
--radius: 0.75rem;
/* Effective value everywhere: the chat backdrop used to force 0.2 onto the
root element at runtime (an ungated leva slider default), so this 0.6
only ever applied to the windows that don't mount the backdrop. Pinned
to the value the app actually renders at. */
--radius-scalar: 0.2;
/* Space under last message vs overlay composer — driven by the measured composer height (see composer/index.tsx)
plus the out-of-flow status stack's measured height (see status-stack/index.tsx) when one is showing.
These are the defaults; each chat surface re-declares the calc against its own measurements (see below). */
--status-stack-measured-height: 0px;
--thread-last-message-clearance: calc(var(--composer-measured-height) + var(--status-stack-measured-height) + 2rem);
--composer-shell-pad-block-end: 0.625rem;
--message-text-indent: 0.75rem;
--conversation-text-font-size: 0.8125rem;
--conversation-tool-font-size: 0.6875rem;
--conversation-caption-font-size: 0.75rem;
--conversation-line-height: 1.125rem;
--conversation-caption-line-height: 1rem;
--conversation-turn-gap: 0.375rem;
/* Gap between top-level turn blocks (prose ↔ tools ↔ thinking) — enough air
that scaffolding reads as separate from the reply, not crammed into it. */
--turn-block-gap: 0.75rem;
/* Between adjacent scaffolding, though: a back-to-back run of thinking
headers and tool rows is one sequence, not blocks to hold apart. Derived
so retuning the block gap carries it along. */
--scaffold-block-gap: calc(var(--turn-block-gap) / 3);
/* Tight gap between tool rows inside a single action group, so a back-to-back
run still reads as one cohesive sequence. */
--tool-row-gap: 0.375rem;
/* Resting opacity of a disclosure caret — invisible until hover, except
where a surface opts into a faint hint of the affordance. */
--disclosure-caret-rest: 0;
/* Paragraph spacing — vertical gap between prose paragraphs, both inside a
markdown block and between consecutive prose parts. Single knob; tweak
freely. */
--paragraph-gap: 0.7rem;
--sticky-human-top: 0.23rem;
--file-tree-row-height: 1.375rem;
--composer-width: 48.75rem;
--composer-control-size: 1.5rem;
--composer-control-primary-size: 1.625rem;
--composer-control-gap: 0.25rem;
--composer-row-gap: 0.25rem;
--composer-ring-strength: 1;
--composer-surface-pad-x: 0.5rem;
--composer-surface-pad-y: 0.3125rem;
--composer-input-min-height: 1.625rem;
--composer-input-max-height: 9.375rem;
--composer-input-inline-min-width: 8rem;
--composer-fallback-height: 2.75rem;
--composer-measured-height: calc(0.5rem + var(--composer-shell-pad-block-end) + var(--composer-fallback-height));
--composer-surface-measured-height: var(--composer-fallback-height);
--thread-viewport-height: max(
0rem,
calc(100% - var(--composer-measured-height) + var(--composer-surface-measured-height))
);
--vsq: min(0.5vh, 0.5vw);
--image-preview-max-width: 34rem;
--image-preview-height: clamp(16.25rem, calc(var(--vsq) * 100), 26.25rem);
--sidebar-width: 14.8125rem;
--chat-min-width: 28rem;
--titlebar-control-size: 1.25rem;
--titlebar-control-height: 1.375rem;
--sidebar-content-inline-padding: 1rem;
--sidebar: var(--dt-sidebar-bg);
--sidebar-foreground: var(--dt-foreground);
--sidebar-primary: var(--dt-primary);
--sidebar-primary-foreground: var(--dt-primary-foreground);
--sidebar-accent: var(--ui-control-active-background);
--sidebar-accent-foreground: var(--dt-accent-foreground);
--sidebar-border: var(--dt-sidebar-border);
--sidebar-ring: var(--dt-ring);
--sidebar-edge-border: color-mix(in srgb, var(--ui-base) 7.5%, transparent);
--chrome-action-hover: var(--ui-control-hover-background);
--midground: var(--dt-midground);
--background: var(--dt-background);
--foreground: var(--dt-foreground);
--warm-glow: color-mix(in srgb, var(--ui-warm) 32%, color-mix(in srgb, var(--ui-accent) 6%, transparent));
/* `--noise-opacity-mul` is set per-mode by `applyTheme()`. */
--noise-opacity-mul: 1;
--backdrop-invert-mul: 1;
}
:root.dark {
/* Per-mode mix knobs — overridden inline by `applyTheme()` per skin. */
--theme-mix-chrome: 74%;
--theme-mix-card: 38%;
--theme-mix-elevated: 46%;
--theme-mix-bubble: 46%;
--theme-neutral-chrome: #0d0d0e;
--theme-neutral-sidebar: #0a0a0b;
--theme-neutral-card: #161618;
/* Dark-only accent palette overrides. */
--ui-red: #e75e78;
--ui-green: #55a583;
--ui-cyan: #6f9ba6;
--ui-diff-add-foreground: color-mix(in srgb, var(--ui-green) 62%, #fff);
--ui-diff-remove-foreground: color-mix(in srgb, var(--ui-red) 62%, #fff);
--sidebar-edge-border: color-mix(in srgb, var(--ui-base) 12%, transparent);
--composer-ring-strength: 1.3;
--backdrop-invert-mul: 0;
/* Dark mode: a dark card needs a stronger black inset to show the recess. */
--dt-input-inset: inset 0 1px 1px color-mix(in srgb, #000 38%, transparent);
--ui-tab-hover-darken: 6%;
/* Dark needs a lighter resting border than light mode. */
--dt-input-border: 4%;
--ui-inline-code-background: color-mix(in srgb, #ffffff 7%, transparent);
--ui-inline-code-foreground: color-mix(in srgb, #ffffff 88%, transparent);
--ui-selection-background: color-mix(in srgb, #ffd24a 38%, transparent);
}
* {
box-sizing: border-box;
border-color: var(--dt-border);
}
html,
body,
#root {
height: 100%;
/* App shell, not a document: the window itself never scrolls on either axis
(panes own their own scroll). Belt to the auto-scroll axis-lock in the
sidebar reorder DnD — nothing can drag the whole shell sideways. */
overflow: hidden;
}
html {
font-size: var(--dt-base-size, 0.875rem);
}
body {
margin: 0;
background: var(--ui-chat-surface-background);
color: var(--dt-foreground);
font-family: var(--dt-font-sans);
font-size: 0.8125rem;
line-height: var(--dt-line-height, 1.55);
letter-spacing: var(--dt-letter-spacing, 0);
-webkit-user-select: none;
user-select: none;
-webkit-font-smoothing: antialiased;
}
button,
textarea {
font: inherit;
}
:where(
a,
.underline,
[class~='hover:underline'],
[class~='focus:underline'],
[class~='focus-visible:underline'],
[class~='group-hover:underline'],
[class~='peer-hover:underline']
) {
text-decoration-color: color-mix(in srgb, currentColor 20%, transparent);
text-underline-offset: 0.25rem;
}
*::selection {
background: var(--ui-selection-background);
color: inherit;
}
}
.dither {
background: repeating-conic-gradient(currentColor 0% 25%, transparent 0% 50%) 0 0 / 0.125rem 0.125rem;
}
/* Inline content links: color at rest, a tinted chip on hover, never an
underline. Tint is currentColor-relative (the old `decoration-current/20`
idiom), so text and fill share a hue on every theme from one `color`. */
.link-chip {
/* The one knob: resting fill. 0% = color-only until hovered. */
--link-chip-tint: 0%;
/* Prose links inherit the surrounding text's weight. `@tailwindcss/typography`
sets `prose a { font-weight: 500 }`, which outranks a utility class on the
anchor — so the override belongs here, on the shared chip. */
font-weight: inherit;
/* `ch`/`em` so the chip tracks the text at any size. Block padding is
asymmetric because an inline box's content area is ascent+descent — equal
padding paints the glyphs high. Keep it small: `padding-block` doesn't grow
the line box, so an over-padded chip creeps into the line above. */
padding: 0.05ch 0.5ch 0.2ch;
border-radius: 0.25rem;
color: var(--dt-primary);
background: color-mix(in srgb, currentColor var(--link-chip-tint), transparent);
/* Explicit: the base layer underlines every `a` (see the :where(a, …) reset). */
text-decoration: none;
/* A wrapped link gets a chip per line fragment, not one ragged box. */
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
transition: background-color 0.12s ease;
}
.link-chip:hover {
--link-chip-tint: 15%;
}
/* Hover-reveal suppression — the shared, declarative escape hatch.
A collapsed pane slides in when the pointer dwells on its thin edge trigger.
Controls that sit over that edge gutter would drag the panel in by accident.
Mark any such region and, while it's hovered, the matching edge trigger(s) go
pointer-transparent. Auto-resets on mouse-out, no JS.
- data-suppress-pane-reveal → kills BOTH edges (use for small regions
like the thread timeline).
- data-suppress-pane-reveal-side → kills only the hovered region's OWN side
(side read from its [data-pane-side]
pane ancestor), so the opposite sidebar
stays summonable while you work a pane. */
[data-pane-shell]:has([data-suppress-pane-reveal]:hover) [data-pane-reveal-trigger] {
pointer-events: none;
}
[data-pane-shell]:has([data-pane-side='left'] [data-suppress-pane-reveal-side]:hover)
[data-pane-side='left']
[data-pane-reveal-trigger],
[data-pane-shell]:has([data-pane-side='right'] [data-suppress-pane-reveal-side]:hover)
[data-pane-side='right']
[data-pane-reveal-trigger] {
pointer-events: none;
}
/* A pane's self-naming label ("REVIEW" atop the review pane) is redundant
when its zone header is showing a tab with the same name — hide it there.
Headerless zones (and the pane used outside the tree) keep the label. */
[data-tree-group][data-zone-header] [data-pane-self-label] {
display: none;
}
:root:not([style*='--theme-asset-bg:']) .theme-default-filler {
display: block;
}
:root[style*='--theme-asset-bg:'] .theme-default-filler {
display: none;
}
/* Primitive-level pointer cursor for every interactive control (buttons,
selects, menu items, switches, tabs, summaries). Keeps individual
components from having to hardcode `cursor-pointer`; explicit cursor
utilities (cursor-grab, cursor-default, disabled:cursor-*) still win since
they live in the utilities layer. */
@layer base {
button:not(:disabled):not([aria-disabled='true']),
summary,
[role='button']:not([aria-disabled='true']),
[role='menuitem']:not([aria-disabled='true']),
[role='menuitemradio']:not([aria-disabled='true']),
[role='menuitemcheckbox']:not([aria-disabled='true']),
[role='option']:not([aria-disabled='true']),
[role='switch']:not([aria-disabled='true']),
[role='tab']:not([aria-disabled='true']) {
cursor: pointer;
}
}
@layer utilities {
[class*='rounded-full'],
[class*=':rounded-full'] {
border-radius: calc(var(--radius-scalar) * 9999rem);
}
}
/* Interactive list-row hover — the sessions-list interaction, shared by every
row list (sessions, capabilities, messaging, file trees, timeline): the
highlight lands instantly on hover-in and fades out over 100ms on leave. */
@utility row-hover {
cursor: pointer;
transition:
color 100ms ease-out,
background-color 100ms ease-out;
&:hover {
background-color: var(--ui-row-hover-background);
transition: none;
}
}
@keyframes arc-border {
0% {
background-position: 15% 15%;
}
100% {
background-position: 75% 75%;
}
}
.arc-border {
--arc-c0: color-mix(in srgb, var(--dt-foreground) 0%, transparent);
--arc-c1: var(--dt-midground);
--arc-c2: var(--dt-background);
--arc-angle: 160deg;
--arc-width: 0.078125rem;
/* How far the ring stands off its host, per side. */
--arc-standoff: 0.125rem;
/* An outset ring is only concentric when its radius grows by the standoff
(r = r_host + gap); `border-radius: inherit` leaves the corner pinched.
Defaults to the shared `rounded-md`; a host on another radius sets it. */
--arc-radius: var(--radius-md);
--arc-duration: 2.23s;
pointer-events: none;
position: absolute;
overflow: hidden;
border-radius: calc(var(--arc-radius) + var(--arc-standoff));
inset: calc(var(--arc-standoff) * -1);
padding: var(--arc-width);
mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
:root.dark .arc-border {
--arc-c1: var(--dt-foreground);
}
/* Quest-style "needs you" pulse for a clarify-blocked session's dot —
a soft amber glow that breathes so the row draws the eye without a toast. */
@keyframes quest-glow {
0%,
100% {
transform: scale(1);
box-shadow:
0 0 0.1875rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 70%, transparent),
0 0 0.5rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 45%, transparent);
}
50% {
transform: scale(1.18);
box-shadow:
0 0 0.3125rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 90%, transparent),
0 0 0.875rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 65%, transparent);
}
}
.quest-glow {
animation: quest-glow 1.8s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.quest-glow {
animation: none;
box-shadow:
0 0 0.25rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 80%, transparent),
0 0 0.625rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 55%, transparent);
}
}
/* Command-palette deep-link: briefly flash the targeted settings row. */
@keyframes setting-field-flash {
0% {
background-color: color-mix(in srgb, var(--dt-primary, #f59e0b) 22%, transparent);
}
100% {
background-color: transparent;
}
}
.setting-field-highlight {
animation: setting-field-flash 1.6s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.setting-field-highlight {
animation: none;
}
}
.arc-border::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(
var(--arc-angle),
transparent 0%,
var(--arc-c0) 15%,
var(--arc-c1) 20%,
var(--arc-c2) 25%,
transparent 35%,
transparent 40%,
var(--arc-c0) 55%,
var(--arc-c1) 60%,
var(--arc-c2) 65%,
transparent 75%,
transparent 80%,
var(--arc-c0) 95%,
var(--arc-c1) 100%
);
background-size: 300% 300%;
animation: arc-border var(--arc-duration) linear infinite;
}
/* Flip the arc's travel direction (e.g. the Nous Portal hero row). */
.arc-border.arc-reverse::before {
animation-direction: reverse;
}
/* Nous Portal hero: slower, blue → orange arc. */
.arc-border.arc-nous,
:root.dark .arc-border.arc-nous {
--arc-c1: #4f8cff;
--arc-c2: #ff8c42;
--arc-duration: 3.27s;
}
/* Sidebar session row — a thin, clipped, single-line host, so it differs twice
from the default ring:
1. Flush, not outset. The sidebar scroller is `overflow-x-hidden` and rows sit
`gap-px` apart, so an outset ring loses its left/right runs to the clip.
2. A visible tail. `--arc-c2` defaults to `--dt-background` — invisible
against the sidebar. With c0 a transparent sentinel and c2 invisible, only
the bright c1 stop survives each pass, and the gradient has three of them:
three moving dots instead of an arc. Tying c2 to c1 gives each spike a
falloff. (`arc-nous` already overrides c2, so the hero is unaffected.) */
.arc-border.arc-row,
:root.dark .arc-border.arc-row {
--arc-standoff: 0rem;
--arc-c2: color-mix(in srgb, var(--arc-c1) 45%, transparent);
}
@media (prefers-reduced-motion: reduce) {
.arc-border::before {
animation: none;
}
}
/* No focus rings, anywhere. Kills the native outline plus Tailwind's
`focus-visible:ring-*` (a box-shadow driven by --tw-ring-*). Unlayered so it
beats the utilities layer without !important on the outline. The composer /
.desktop-input-chrome focus glow is untouched — those set `box-shadow`
directly rather than through the ring vars. */
*:focus,
*:focus-visible {
outline: none;
}
*:focus-visible {
--tw-ring-shadow: 0 0 #0000 !important;
--tw-ring-offset-shadow: 0 0 #0000 !important;
}
button {
-webkit-app-region: no-drag;
}
/* Button variant styling lives entirely in the cva in components/ui/button.tsx
(the single source of truth). Don't re-add [data-slot='button'] rules here —
attribute selectors out-specify the Tailwind utilities and silently override
the variants. */
[data-slot='dropdown-menu-content'],
[data-slot='select-content'],
[data-slot='dialog-content'],
[data-slot='thread-timeline-popover'] {
border-color: var(--ui-stroke-secondary);
background: color-mix(in srgb, var(--ui-bg-elevated) 96%, transparent);
box-shadow: var(--shadow-md);
backdrop-filter: blur(0.75rem) saturate(1.08);
-webkit-backdrop-filter: blur(0.75rem) saturate(1.08);
}
[data-slot='dropdown-menu-item']:focus,
[data-slot='dropdown-menu-checkbox-item']:focus,
[data-slot='dropdown-menu-radio-item']:focus {
background: var(--ui-bg-tertiary);
color: var(--ui-text-primary);
}
input,
textarea,
[contenteditable]:not([contenteditable='false']),
[data-slot='aui_user-message-root'],
[data-slot='aui_assistant-message-content'],
[data-slot='aui_system-message-root'],
[data-selectable-text='true'],
[data-selectable-text='true'] * {
-webkit-user-select: text;
user-select: text;
}
button,
[role='button'] {
-webkit-user-select: none;
user-select: none;
}
img,
picture,
video,
canvas,
svg {
-webkit-user-select: none;
user-select: none;
}
img,
video,
canvas {
-webkit-user-drag: none;
}
/* Tabs render 2-wide on every HTML code surface — the source preview, inline
diffs, markdown code blocks, tool output — so indentation matches the editor
and the terminal instead of the browser default (8). */
pre,
code {
tab-size: 2;
-moz-tab-size: 2;
}
/* Arc-style multicolor action surface (static, not animated). Reusable on any
Button via className. Unlayered so it beats Tailwind's bg-*/
text-* variant utilities. */ .btn-arc {
background-image: linear-gradient(110deg, #5b6cff 0%, #8b5cf6 28%, #d946ef 58%, #fb7185 82%, #fb923c 100%);
color: #fff;
border-color: transparent;
}
.btn-arc:hover:not(:disabled) {
filter: brightness(1.08) saturate(1.06);
}
/* Shared input chrome — mirrors composer hover/focus FX. Unlayered to beat Tailwind utilities. */
/* Border strength is driven by the single --dt-input-border alpha knob (× the
theme's tuned ring color); hover doubles it and focus triples it. Only the
border-color animates — animating the translucent background over the window's
backdrop-blur flickers (badly on textareas), so the bg fill snaps instead.
(:focus is declared after :hover so a focused+hovered field shows focus.) */
.desktop-input-chrome {
background: color-mix(in srgb, var(--dt-card) var(--dt-input-bg), transparent);
border-color: color-mix(in srgb, var(--dt-composer-ring) var(--dt-input-border), transparent);
box-shadow: var(--dt-input-inset);
transition: border-color 200ms ease-out;
}
.desktop-input-chrome:hover {
border-color: color-mix(in srgb, var(--dt-composer-ring) calc(var(--dt-input-border) * 2), transparent);
}
/* `[data-state='open']` keeps the trigger looking focused while its dropdown is
open — Radix moves focus into the list, so the trigger itself loses :focus. */
.desktop-input-chrome:focus,
.desktop-input-chrome:focus-within,
.desktop-input-chrome[data-state='open'] {
border-color: var(--dt-composer-ring);
box-shadow: none;
outline: none;
}
.desktop-input-chrome[aria-invalid='true'] {
border-color: var(--dt-destructive);
}
@layer components {
/* Chromium 121+ (Electron) prefers standard scrollbar-* over
::-webkit-scrollbar and ignores the latter when both are set — platform
thin on mac still looks chunky. Use webkit for real thin thumbs; gate the
standard props so only non-webkit engines (Firefox) take that path.
https://developer.chrome.com/docs/css-ui/scrollbar-styling
https://syntackle.com/blog/changes-to-scrollbar-styling-in-chrome-121/
Any author rule on ::-webkit-scrollbar switches that scroller from
platform overlay (fade in on scroll, zero gutter) to classic always-on
gutters. `.scrollbar-overlay` opts a surface out of the themed rules so
Electron keeps native overlay — used by conversation code blocks where a
permanent 4px track eats the card and always draws a bar. */
.scrollbar-dt::-webkit-scrollbar,
.scrollbar-dt *:not(.scrollbar-overlay)::-webkit-scrollbar {
width: 0.25rem;
height: 0.25rem;
}
.scrollbar-dt::-webkit-scrollbar-track,
.scrollbar-dt::-webkit-scrollbar-corner,
.scrollbar-dt *:not(.scrollbar-overlay)::-webkit-scrollbar-track,
.scrollbar-dt *:not(.scrollbar-overlay)::-webkit-scrollbar-corner {
background: transparent;
}
.scrollbar-dt::-webkit-scrollbar-thumb,
.scrollbar-dt *:not(.scrollbar-overlay)::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--dt-midground) 18%, transparent);
border-radius: 9999rem;
background-clip: padding-box;
}
.scrollbar-dt::-webkit-scrollbar-thumb:hover,
.scrollbar-dt *:not(.scrollbar-overlay)::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--dt-midground) 40%, transparent);
background-clip: padding-box;
}
.scrollbar-dt::-webkit-scrollbar-button,
.scrollbar-dt *:not(.scrollbar-overlay)::-webkit-scrollbar-button {
display: none;
}
/* Variant for portaled overlays (Radix DropdownMenu, Popover, etc.) that
render under document.body, outside the `.scrollbar-dt` scope on
#root. Same visual treatment, applied directly to the overlay
container so its (and only its) internal scrollbar is themed. */
.dt-portal-scrollbar::-webkit-scrollbar {
width: 0.25rem;
height: 0.25rem;
}
.dt-portal-scrollbar::-webkit-scrollbar-track,
.dt-portal-scrollbar::-webkit-scrollbar-corner {
background: transparent;
}
.dt-portal-scrollbar::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--dt-midground) 28%, transparent);
border-radius: 9999rem;
background-clip: padding-box;
}
.dt-portal-scrollbar::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--dt-midground) 50%, transparent);
background-clip: padding-box;
}
.dt-portal-scrollbar::-webkit-scrollbar-button {
display: none;
}
@supports not selector(::-webkit-scrollbar) {
.scrollbar-dt,
.scrollbar-dt *:not(.scrollbar-overlay) {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--dt-midground) 18%, transparent) transparent;
}
.dt-portal-scrollbar {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--dt-midground) 28%, transparent) transparent;
}
/* Firefox has no real overlay mode; thin + transparent track is the closest
match to the Chromium overlay opt-out above. */
.scrollbar-overlay {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--dt-midground) 28%, transparent) transparent;
}
}
}
/* Bottom clearance lives on [data-slot='aui_composer-clearance'] —
virtualized items unmount, so :nth-last-child can't fire reliably. */
[data-slot='aui_assistant-message-content'] {
padding-left: var(--message-text-indent);
font-size: var(--conversation-text-font-size);
line-height: 1.5;
}
[data-slot='aui_assistant-message-root'] {
width: 100%;
}
[data-slot='aui_assistant-message-content'] .aui-md,
[data-slot='aui_assistant-message-content'] .aui-md :where(p, li, blockquote, table, pre) {
font-size: inherit;
}
/* Tailwind Typography sets `.prose :where(p) { margin: 1.25em }` (~16px). That
selector ties our `my-*` utility on specificity and wins on source order, so
paragraph spacing must be reclaimed here at higher specificity. One tight
top-margin (bottom zeroed to avoid doubling), first child reset to flush. */
[data-slot='aui_assistant-message-content'] .aui-md :where(p) {
margin-block: var(--paragraph-gap) 0;
}
/* Headings are section breaks, so they own a larger top gap than prose — a
leading `# title` (common in re-entered/delegated turns) no longer butts
against the block above. Top-owned + bottom snug; the first-child reset
below still flushes a leading heading. */
[data-slot='aui_assistant-message-content'] .aui-md :where(h1, h2, h3, h4) {
margin-block: 1rem 0.25rem;
}
/* First rendered element of a prose block is flush — the block-level gap above
(tool / paragraph) already provides the separation. Reach one level deep too:
Streamdown wraps blocks in a `div.space-y-*`, so the real first line is the
first child's first child. */
[data-slot='aui_assistant-message-content'] .aui-md > :first-child,
[data-slot='aui_assistant-message-content'] .aui-md > :first-child > :first-child {
margin-top: 0;
}
/* Prose, tools, todos, and thinking all share one left edge (the message
content's --message-text-indent). No extra prose indent — a single gutter
reads cleaner than a ragged tool-vs-reply column. */
/* RTL/bidi chat text (#44150): each block resolves its own base direction from
its first strong char (UAX#9 plaintext). text-align:start makes that resolved
direction drive alignment too — load-bearing, since the user bubble pins
text-left. direction is never set, so chrome/layout/list-indent stay LTR (the
issue asks not to flip the whole UI). Covers assistant prose, user lines, and
both composers (main + edit share composer-rich-input). */
[data-slot='aui_assistant-message-content'] .aui-md :where(p, h1, h2, h3, h4, h5, h6, li, blockquote),
[data-slot='aui_user-inline-text'],
[data-slot='composer-rich-input'] {
unicode-bidi: plaintext;
text-align: start;
}
/* Inline code/KaTeX don't vote on direction and keep their own order: isolate
makes bidi treat each as one neutral, so a block that *starts* with `./run.sh`
then Arabic still resolves RTL, and the command's neutrals (dots/slashes)
aren't reordered by the surrounding RTL run. */
[data-slot='aui_assistant-message-content'] .aui-md :where(:not(pre) > code),
[data-slot='aui_user-inline-code'],
[data-slot='aui_assistant-message-content'] .aui-md .katex {
direction: ltr;
unicode-bidi: isolate;
}
/* Fenced code stays LTR even inside an RTL list item/blockquote — never mirrors. */
[data-slot='aui_assistant-message-content'] .aui-md [data-slot='code-card'],
[data-slot='aui_user-fence'] {
direction: ltr;
text-align: left;
}
[data-slot='aui_user-message-root'] {
top: var(--sticky-human-top);
}
[data-slot='aui_user-message-root'],
[data-slot='aui_edit-composer-root'] {
--human-msg-line-height: 1.3;
font-size: var(--conversation-text-font-size);
}
[data-slot='aui_user-inline-text'] {
line-height: var(--human-msg-line-height);
}
[data-slot='aui_edit-composer-root'] [data-slot='composer-rich-input'] {
line-height: var(--human-msg-line-height);
}
/* Sticky human bubbles clamp to ~2 lines with a soft bottom fade so a long
prompt doesn't dominate the viewport. The clamp lifts only in the edit
composer; expanding on read-only :focus-within ran on mousedown (before the
swap) and fought stick-to-bottom when parked at the bottom. */
.sticky-human-clamp {
cursor: pointer;
max-height: calc(4 * var(--human-msg-line-height) * var(--conversation-text-font-size) + 0.15rem);
overflow: hidden;
}
.sticky-human-clamp[data-clamped='true'] {
-webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent);
mask-image: linear-gradient(to bottom, #000 55%, transparent);
}
/* Stick-to-bottom owns scrollTop while following. Once escaped, native anchoring
is safe and keeps sticky human edits from shoving the viewport; data-editing
enables that path before React swaps in the inline editor. */
[data-slot='aui_thread-viewport'] {
overflow-anchor: none;
}
[data-slot='aui_thread-viewport'][data-following='false'],
[data-slot='aui_thread-viewport'][data-editing='true'] {
overflow-anchor: auto;
}
[data-slot='aui_thread-content'] {
max-width: var(--composer-width);
padding-inline: 1.5rem;
}
[data-slot='aui_intro'] {
align-items: center;
justify-content: center;
padding-bottom: var(--composer-measured-height);
text-align: center;
}
[data-slot='aui_intro'] > div {
max-width: min(var(--composer-width), 82vw);
}
[data-slot='aui_intro'] p:last-child {
max-width: 34rem;
margin-inline: auto;
color: var(--ui-text-tertiary);
font-size: 0.875rem;
line-height: 1.45;
}
.fit-text {
--fit-captured-length: initial;
--fit-support-sentinel: var(--fit-captured-length, 9999px);
display: flex;
container-type: inline-size;
}
.fit-text > [aria-hidden] {
visibility: hidden;
}
.fit-text > :not([aria-hidden]) {
flex-grow: 1;
container-type: inline-size;
--fit-captured-length: 100cqi;
--fit-available-space: var(--fit-captured-length);
}
.fit-text > :not([aria-hidden]) > * {
--fit-support-sentinel: inherit;
--fit-captured-length: 100cqi;
--fit-ratio: tan(atan2(var(--fit-available-space), var(--fit-available-space) - var(--fit-captured-length)));
display: block;
inline-size: var(--fit-available-space);
font-size: clamp(
var(--fit-min, 1em),
1em * var(--fit-ratio),
var(--fit-max, infinity * 1px) - var(--fit-support-sentinel)
);
}
@container (inline-size > 0) {
.fit-text > :not([aria-hidden]) > * {
white-space: nowrap;
}
}
@property --fit-captured-length {
syntax: '<length>';
initial-value: 0px;
inherits: true;
}
[data-slot='composer-root'] {
/* +10px width compensates the 5px side padding so the visible surface keeps
its exact width/position — the inline padding is just transparent grab space
for the peel-out drag, matching the floating composer's 5px platform. */
width: calc(min(var(--composer-width), calc(100% - 2rem)) + 10px);
padding-inline: 5px;
padding-bottom: var(--composer-shell-pad-block-end);
}
/* Popped-out (floating) composer: compact width + an even 5px transparent grab
platform. The higher-specificity selector resets the base rule's padding-bottom
so the inset is equal on all four sides (not 5px sides / shell-pad bottom). */
[data-slot='composer-root'][data-popped-out] {
width: var(--composer-popout-width, 24rem);
max-width: calc(100vw - 1.5rem);
padding: 5px;
}
/* Dock glow intensity scale — dimmer in light mode (the primary glow reads
much stronger over a light backdrop), full strength in dark mode. */
:root {
--dock-glow-scale: 0.55;
}
.dark {
--dock-glow-scale: 1;
}
/* Drag-region hatch — a diagonal ///// pattern (Photoshop-style) that fades into
the transparent grab margin on hover (and stays while dragging) to signal the
composer is draggable. Inherits the root radius so it clips to the corners. */
[data-slot='composer-drag-region'] {
/* Hatch frame radius (tuned by hand). */
border-radius: 0.4rem;
opacity: 0;
transition: opacity 150ms ease;
background-image: repeating-linear-gradient(
-45deg,
color-mix(in srgb, var(--ui-text-tertiary) 38%, transparent) 0,
color-mix(in srgb, var(--ui-text-tertiary) 38%, transparent) 1px,
transparent 1px,
transparent 3.5px
);
}
[data-slot='composer-drag-region']:hover,
[data-slot='composer-drag-region'][data-dragging] {
opacity: 0.33;
}
[data-slot='composer-root'] > .pointer-events-none {
background: linear-gradient(
to bottom,
transparent,
color-mix(in srgb, var(--ui-chat-surface-background) 88%, transparent)
) !important;
}
[data-slot='composer-surface'] {
border-color: var(--ui-stroke-secondary) !important;
}
[data-slot='composer-fade'] {
min-height: 2.375rem;
}
[data-slot='composer-rich-input'] {
color: var(--ui-text-primary);
font-size: 0.8125rem;
}
[data-slot='composer-rich-input']:empty::before {
color: var(--ui-text-tertiary) !important;
}
/* ── Composer fill — ONE var painted by the surface AND anything docked to it
(slash·@ popover, `?` help). State ladder sets the var; consumers just paint
`background: var(--composer-fill)`, so every state matches by construction.
The :has() rule is last on purpose: while a completion drawer is open it
beats focus/scroll and forces an OPAQUE fill (both mix endpoints solid) —
translucent glass can never match across the two layers because they sample
different backdrops. */
:root {
/* Fallback for drawers outside the main composer (e.g. edit-message). */
--composer-fill: color-mix(in srgb, var(--dt-card) 90%, var(--dt-background));
}
[data-slot='composer-root'] {
--composer-fill: color-mix(in srgb, var(--dt-card) 72%, transparent);
}
[data-slot='composer-root'][data-thread-scrolled-up] {
--composer-fill: color-mix(in srgb, var(--dt-card) 48%, transparent);
}
/* Tool/thinking blocks now live at message-text alignment (no leading
chevron column to escape into), so their headers and bodies share a
common left edge with the model's text. */
[data-slot='aui_assistant-message-content'] > [data-slot='tool-block'],
[data-slot='aui_assistant-message-content'] > [data-slot='aui_thinking-disclosure'] {
width: 100%;
max-width: 100%;
}
/* A delegation is a list of short rows — goal, model, timer — not prose, so it
reads better narrow than stretched across the full reading column. Scoped
here rather than as a utility on the element: the rule above sets width on
every tool block and would win over it. */
[data-slot='aui_assistant-message-content'] [data-slot='tool-block'][data-delegate-card] {
max-width: 75%;
}
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block'] code {
max-width: none;
font-family: inherit;
font-size: inherit;
padding: 0;
border-radius: 0;
background: transparent;
color: inherit;
overflow-x: visible;
overflow-wrap: inherit;
vertical-align: baseline;
word-break: inherit;
white-space: inherit;
}
/* Streamdown's adapter wraps code fences in a `data-streamdown="code-block"`
container with its own card chrome. We render our own <CodeCard>, so this
strips the upstream chrome down to a layout-only passthrough. */
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block'] {
contain: none;
overflow: visible;
margin-block: var(--paragraph-gap) 0 !important;
padding: 0 !important;
gap: 0 !important;
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
color: inherit;
}
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block']:has(.aui-prose-fence) {
margin-block: 0 !important;
}
[data-slot='aui_assistant-message-content'] .aui-md [data-slot='code-card'] {
/* Streamdown nests blocks, so the container's child-combinator rhythm can't
reach the card. Carry the paragraph gap on the card itself (top-owned);
collapses cleanly with the wrapper's margin when one is present, and the
first-child reset still flushes a leading code block. */
margin-block: var(--paragraph-gap) 0;
position: relative;
transition:
box-shadow 180ms ease-out,
background-color 180ms ease-out;
}
[data-slot='aui_assistant-message-content'] .aui-md [data-slot='code-card'][data-streaming='true'] {
animation:
code-card-stream-enter 180ms cubic-bezier(0.16, 1, 0.3, 1) both,
code-card-stream-glow 1.8s ease-in-out 180ms infinite alternate;
box-shadow:
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 10%, transparent),
0 0.625rem 1.75rem color-mix(in srgb, var(--dt-ring) 8%, transparent);
}
[data-slot='aui_assistant-message-content']
.aui-md
[data-slot='code-card'][data-streaming='true']
[data-slot='code-card-body'] {
-webkit-mask-image: linear-gradient(to bottom, black 0%, black calc(100% - 1.5rem), rgb(0 0 0 / 64%) 100%);
mask-image: linear-gradient(to bottom, black 0%, black calc(100% - 1.5rem), rgb(0 0 0 / 64%) 100%);
}
[data-slot='aui_assistant-message-content'] .aui-md :not(pre) > code {
background: var(--ui-inline-code-background);
color: var(--ui-inline-code-foreground);
}
[data-slot='aui_assistant-message-content'] .aui-md :where(.aui-shiki, .aui-shiki > pre) {
margin: 0 !important;
}
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table {
border-spacing: 0;
}
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table > table,
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table thead,
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table tbody,
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table tr,
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table th,
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table td {
margin: 0 !important;
margin-block-start: 0 !important;
margin-block-end: 0 !important;
}
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table thead {
border-bottom-color: var(--dt-border) !important;
}
/* Tool / thinking blocks are scaffolding around the model's reply, so we
keep them transparent and fade them slightly. The reading column (prose)
stays at full strength; scaffolding lifts back to full opacity on
hover/focus so it stays legible when the user actually wants to read it. */
[data-slot='tool-block'],
[data-slot='aui_thinking-disclosure'] {
background: transparent !important;
}
/* Fade scaffolding so the prose reading column stays primary. Each surface
opts in with `data-conversation-scaffold` — thinking header, tool row, run
summary, live status line — instead of being named by its own selector here.
Spelling them out individually is how the status line came to sit a shade
brighter than the rows either side of it.
Marked per surface and never on a container: opacity opens a stacking
context, so nothing inside a faded parent can be more opaque than it, and
one row (an open diff) could not stay lit while its siblings dimmed. Hence
the tool group wrapper carries no mark and each row inside it carries its
own — no two marked elements nest, so the fade never compounds. */
[data-slot='aui_assistant-message-content'] [data-conversation-scaffold] {
opacity: 0.67;
transition: opacity 120ms ease-out;
}
/* Lift on hover or *keyboard* focus only. `:focus-within` also matches the
focus a mouse click leaves on the disclosure toggle, which kept a row lit
after you clicked to collapse it; `:has(:focus-visible)` excludes that. */
[data-slot='aui_assistant-message-content'] [data-conversation-scaffold]:is(:hover, :has(:focus-visible)) {
opacity: 1;
}
/* Shiki surfaces in the inline file diff + source preview: strip the theme's
own background/margins, and lay each `.line` on its own grid row so the
inter-line `\n` text nodes can't double-space full-width rows. Empty lines
carry a non-breaking space so blank rows keep their height. */
[data-slot='file-diff-panel'] .shiki,
[data-slot='file-diff-panel'] .shiki code,
.preview-source-code .shiki,
.preview-source-code .shiki code {
margin: 0;
background: transparent !important;
}
[data-slot='file-diff-panel'] .shiki code,
.preview-source-code .shiki code {
display: grid;
}
[data-slot='file-diff-panel'] .shiki code .line:empty::before,
.preview-source-code .shiki code .line:empty::before {
content: '\00a0';
}
/* Inline diff rows keep their utility-class padding; just floor the height. */
[data-slot='file-diff-panel'] .shiki code .line {
min-height: 1.25rem;
white-space: pre;
}
/* Source rows are a fixed editor-height box so source⇄diff toggling never
shifts and the gutter stays aligned. */
.preview-source-code .shiki code {
min-width: max-content;
}
.preview-source-code .shiki code .line {
display: block;
height: 1.25rem;
padding: 0 0.625rem;
line-height: 1.25rem;
white-space: pre;
}
/* The github-dark token palette reads candy-bright at our small code size.
`github-dark-dimmed` only dims the *background* (which we strip), so soften
the token *foregrounds* directly — a small saturation + brightness pullback,
hues preserved — for both code blocks and inline diffs. Dark mode only. */
.dark .shiki {
filter: saturate(0.82) brightness(0.92);
}
/* File edits (write_file / edit_file / patch) are the deliverable, not
scaffolding — the diff is what the user reviews, like a PR. An *expanded*
edit stays at full strength; collapsed it fades like any other row. The
`data-file-edit` marker sits on the same row element and is only present
while the row is open. */
[data-slot='aui_assistant-message-content'] [data-slot='tool-block'][data-tool-row][data-file-edit] {
opacity: 1;
}
/* Conversation block rhythm. assistant-ui renders each range as a direct child
of the message content with no per-part wrapper, so adjacency rules cover
every pairing — first block needs no reset, nested tool rows are untouched.
One gap between top-level blocks, whatever they are. Spelling out which
*pairs* qualified meant the live status line (not tool, thinking or prose)
fell through every branch and carried its own half-size margin instead. */
[data-slot='aui_assistant-message-content']
> :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall'], .aui-md)
+ :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall'], .aui-md) {
margin-top: var(--turn-block-gap);
}
/* Except prose ↔ prose: one reading column that happens to arrive as several
parts, so it keeps the in-block paragraph rhythm. Same specificity as the
rule above and deliberately after it. */
[data-slot='aui_assistant-message-content'] > .aui-md + .aui-md {
margin-top: var(--paragraph-gap);
}
/* And except scaffolding ↔ scaffolding: the full gap is the space between the
reply and the work around it, so a back-to-back run of thinking headers,
tool rows and status lines spent it between every line and read as stacked
cards. Same list as the rule above minus prose — `data-conversation-scaffold`
is the wrong hook here: it's absent on a multi-call tool GROUP, which is most
of a real run, and present on rows nested inside one. */
[data-slot='aui_assistant-message-content']
> :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall'])
+ :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall']) {
margin-top: var(--scaffold-block-gap);
}
/* Except a file edit, which is the deliverable rather than another line in the
run — a diff you read like a PR, so it keeps the full block gap on both
sides while the scaffolding around it stays tight. `data-file-edit` is only
on the row while it's open, so a collapsed edit rejoins the run. Margins on
the block itself, not adjacency pairs: it needs the clearance whatever sits
next to it, and adjacent margins collapse rather than stacking. */
[data-slot='aui_assistant-message-content'] > [data-tool-row][data-file-edit] {
margin-block: var(--turn-block-gap);
}
/* Thinking headers carry a faint resting caret. A run of them is the one place
the affordance isn't otherwise discoverable — every other disclosure sits in
a row you're already reaching for. The scaffold fade multiplies this, so the
painted value lands well under the number here. */
[data-slot='aui_thinking-disclosure'] {
--disclosure-caret-rest: 0.4;
}
/* A streaming turn is sealed into several assistant bubbles as it goes
(`message.interim`) and rehydrates into fewer, so the same two blocks are
sometimes siblings inside one bubble and sometimes split across two. The
flex gap between bubbles is the turn gap — half the block gap — which made
the rhythm visibly tighten and then relax as a turn settled. Top it up so
the column ticks at one interval either way. */
[data-slot='aui_turn-pair'] > [data-slot='aui_assistant-message-root'] + [data-slot='aui_assistant-message-root'] {
margin-top: calc(var(--turn-block-gap) - var(--conversation-turn-gap));
}
/* Unless the blocks either side of that seal are both scaffolding, in which
case the scaffold gap is the target and the flex gap already overshoots it.
Topping up to the full block gap here is what made a live run relax between
rows and then tighten once it rehydrated into one bubble. A bubble carrying
an open diff is excluded — it keeps the full gap, same as in-flow. */
[data-slot='aui_turn-pair']
> [data-slot='aui_assistant-message-root']:has(
> [data-slot='aui_assistant-message-content']
> :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall']):last-child
):not(:has([data-file-edit]))
+ [data-slot='aui_assistant-message-root']:has(
> [data-slot='aui_assistant-message-content']
> :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], [data-slot='aui_stream-stall']):first-child
):not(:has([data-file-edit])) {
margin-top: calc(var(--scaffold-block-gap) - var(--conversation-turn-gap));
}
/* Message action bars — flat icon hits with default dim; only the hovered/focused control is full-strength. */
[data-slot='aui_msg-actions'] button {
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
padding: 0;
gap: 0;
height: auto;
width: auto;
min-height: 0;
min-width: 0;
flex-shrink: 0;
cursor: pointer;
color: var(--color-muted-foreground);
opacity: 0.5;
}
[data-slot='aui_msg-actions'] button:disabled {
cursor: default;
}
[data-slot='aui_msg-actions'] button:hover {
background: transparent;
color: var(--color-foreground);
opacity: 1;
}
[data-slot='aui_msg-actions'] button:active {
background: transparent;
}
[data-slot='aui_msg-actions'] button:focus-visible {
opacity: 1;
}
[data-slot='aui_msg-actions'] button svg {
width: 0.875rem;
height: 0.875rem;
}
/* Re-declare the clearance calc on every chat surface. `:root` computes it
once against the ROOT measurements, so scoping only the inputs would leave
every thread reading the same substituted value. Redeclaring here makes each
surface resolve the calc against its own composer + status-stack heights,
falling back to the root defaults until this surface publishes its first
measurement. See surface-vars.ts. */
[data-chat-surface] {
--thread-last-message-clearance: calc(var(--composer-measured-height) + var(--status-stack-measured-height) + 2rem);
}
/* A live tool run, shown as one line. `ToolRunTicker` stacks the run's rows
into a reel and offsets it by the newest row's index, so each new action
slides the one before it up and out of a single-line window — the run reads
as one line ticking over in place rather than a list growing down the page.
Rows are clipped to a uniform line box so the offset stays exact whatever a
row contains. */
.tool-ticker {
height: var(--conversation-line-height);
overflow: hidden;
}
.tool-ticker__reel {
transform: translateY(calc(var(--tool-ticker-index, 0) * var(--conversation-line-height) * -1));
transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
.tool-ticker__row {
display: flex;
height: var(--conversation-line-height);
align-items: center;
overflow: hidden;
}
@media (prefers-reduced-motion: reduce) {
.tool-ticker__reel {
transition: none;
}
}
@keyframes code-card-stream-enter {
from {
opacity: 0.74;
transform: translateY(0.375rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Floating "jump to bottom" control (see scroll-to-bottom-button.tsx).
Directional scale: it contracts toward 1 as it arrives (from 1.1) and keeps
contracting to 0.9 as it leaves — always shrinking in the direction of
travel, so the motion reads as a soft settle / recede rather than a pop. The
X half-offset stays baked into every transform so `left-1/2` centering holds
through the animation. */
.thread-jump-button {
opacity: 0;
transform: translateX(-50%) translateY(0.3rem) scale(0.9);
}
.thread-jump-button[data-state='in'] {
animation: thread-jump-in 200ms cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
.thread-jump-button[data-state='out'] {
animation: thread-jump-out 180ms ease-in forwards;
}
@keyframes thread-jump-in {
from {
opacity: 0;
transform: translateX(-50%) translateY(0.3rem) scale(1.1);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
}
@keyframes thread-jump-out {
from {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
to {
opacity: 0;
transform: translateX(-50%) translateY(0.3rem) scale(0.9);
}
}
@media (prefers-reduced-motion: reduce) {
.thread-jump-button[data-state='in'],
.thread-jump-button[data-state='out'] {
animation: none;
transition: opacity 120ms linear;
}
.thread-jump-button[data-state='in'] {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
}
@keyframes code-card-stream-glow {
from {
box-shadow:
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 6%, transparent),
0 0.5rem 1.5rem color-mix(in srgb, var(--dt-ring) 5%, transparent);
}
to {
box-shadow:
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 12%, transparent),
0 0.75rem 2rem color-mix(in srgb, var(--dt-ring) 10%, transparent);
}
}
@media (prefers-reduced-motion: reduce) {
[data-slot='aui_assistant-message-content'] .aui-md [data-slot='code-card'][data-streaming='true'] {
animation: none;
}
}
/* -------------------------------------------------------------------------- */
/* Pet egg hatch (Cmd-K → Pets → Generate) */
/* The incubation wobble + reveal flash/pop give the draft→pet step a */
/* Pokémon-style "egg is hatching" beat instead of a bare spinner. */
/* -------------------------------------------------------------------------- */
.pet-egg {
position: relative;
width: 5.5rem;
height: 7rem;
border-radius: 50% 50% 50% 50% / 62% 62% 38% 38%;
background: radial-gradient(
120% 90% at 32% 26%,
color-mix(in srgb, var(--ui-accent) 14%, #fff) 0%,
#f4ecd8 46%,
#e4d3ad 100%
);
box-shadow:
inset -0.45rem -0.6rem 1.1rem color-mix(in srgb, #000 16%, transparent),
inset 0.35rem 0.4rem 0.7rem color-mix(in srgb, #fff 70%, transparent),
0 0.4rem 0.9rem color-mix(in srgb, #000 22%, transparent);
transform-origin: 50% 88%;
animation: pet-egg-wobble 2.4s ease-in-out infinite;
}
/* Compact egg (empty-state hero). Children are %-based so they track the size;
only the rem box-shadow needs scaling down to stay crisp. */
.pet-egg--sm {
width: 3.25rem;
height: 4.1rem;
box-shadow:
inset -0.28rem -0.38rem 0.7rem color-mix(in srgb, #000 16%, transparent),
inset 0.22rem 0.26rem 0.45rem color-mix(in srgb, #fff 70%, transparent),
0 0.25rem 0.55rem color-mix(in srgb, #000 22%, transparent);
}
.pet-egg__shine {
position: absolute;
top: 14%;
left: 22%;
width: 28%;
height: 22%;
border-radius: 50%;
background: color-mix(in srgb, #fff 85%, transparent);
filter: blur(2px);
opacity: 0.85;
}
.pet-egg__spot {
position: absolute;
border-radius: 50%;
background: color-mix(in srgb, var(--ui-accent) 70%, #b89b63);
opacity: 0.55;
}
.pet-egg__glow {
position: absolute;
inset: -35%;
border-radius: 50%;
background: radial-gradient(circle, color-mix(in srgb, var(--ui-accent) 55%, transparent) 0%, transparent 62%);
animation: pet-egg-glow 2.4s ease-in-out infinite;
pointer-events: none;
}
.pet-egg-shadow {
width: 4.5rem;
height: 0.8rem;
border-radius: 50%;
/* Lighter on light backgrounds (~20% less ink); dark mode keeps it grounded. */
background: radial-gradient(
circle,
color-mix(in srgb, #000 var(--pet-egg-shadow-ink, 26%), transparent) 0%,
transparent 72%
);
animation: pet-egg-shadow 2.4s ease-in-out infinite;
}
.dark .pet-egg-shadow {
--pet-egg-shadow-ink: 32%;
}
/* Contact shadow sized for the compact incubator egg (roughly its footprint). */
.pet-egg-shadow--sm {
width: 3rem;
height: 0.6rem;
}
/* Contact shadow under the revealed pet — mirrors the floating mascot's in-app
shadow: an ellipse at the feet, ~55% of the sprite width, sitting behind it. */
.pet-contact-shadow {
position: absolute;
bottom: -0.15rem;
left: 50%;
width: 55%;
aspect-ratio: 100 / 28;
transform: translateX(-50%);
background: radial-gradient(ellipse at center, color-mix(in srgb, #000 42%, transparent) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
}
/* Hatch wiggle for the pixel egg (rocks around its base). */
.pet-wobble {
transform-origin: 50% 85%;
animation: pet-egg-wobble 2.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.pet-wobble {
animation: none;
}
}
@keyframes pet-egg-wobble {
0%,
62%,
100% {
transform: rotate(0deg);
}
8% {
transform: rotate(-7deg);
}
16% {
transform: rotate(6deg);
}
24% {
transform: rotate(-5deg);
}
32% {
transform: rotate(4deg);
}
40% {
transform: rotate(0deg);
}
/* the "almost out" burst */
70% {
transform: rotate(-12deg);
}
76% {
transform: rotate(12deg);
}
82% {
transform: rotate(-9deg);
}
88% {
transform: rotate(7deg);
}
94% {
transform: rotate(-3deg);
}
}
@keyframes pet-egg-glow {
0%,
100% {
opacity: 0.35;
transform: scale(0.92);
}
70% {
opacity: 0.4;
}
84% {
opacity: 0.85;
transform: scale(1.08);
}
}
@keyframes pet-egg-shadow {
0%,
62%,
100% {
transform: scaleX(1);
opacity: 0.6;
}
76% {
transform: scaleX(0.8);
opacity: 0.45;
}
}
.pet-reveal {
animation: pet-reveal-pop 620ms cubic-bezier(0.22, 1.4, 0.4, 1) both;
}
@keyframes pet-reveal-pop {
0% {
opacity: 0;
transform: scale(0.35) translateY(0.4rem);
}
60% {
opacity: 1;
transform: scale(1.12) translateY(0);
}
100% {
transform: scale(1) translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
.pet-egg,
.pet-egg__glow,
.pet-egg-shadow,
.pet-reveal {
animation: none;
}
.pet-reveal {
opacity: 1;
transform: none;
}
}
/* Pet generation progress bar — determinate (hatch rows: done/total) or */
/* indeterminate (drafts, which return together so a % would just snap). */
/* Sliding indeterminate block for the `accent` Progress variant (pet hatch
flow). Color/positioning come from the primitive; this owns the animation. */
.progress-slide {
width: 40%;
animation: progress-slide 1.15s ease-in-out infinite;
}
@keyframes progress-slide {
0% {
left: -42%;
}
100% {
left: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
.progress-slide {
animation: none;
left: 0;
width: 100%;
opacity: 0.4;
}
}