Files
claude-code-haha/desktop/src/theme/globals.css
T
2026-08-03 02:35:58 +08:00

2779 lines
85 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";
/* ─── Dark variant binding ────────────────────────────────────────
The app has six themes switched via `<html data-theme>`. Following the OS
is resolved in JS (src/theme/systemAppearance.ts) and lands on that same
attribute, so `data-theme` stays the single source of truth here. Without
this binding, Tailwind's default `dark:` compiles to
`prefers-color-scheme: dark` and would fire on the wrong theme — right by
accident for someone following the system, plainly wrong for anyone on a
fixed palette.
Two of the six sit on a dark ground — `dark` and `ink-blue` — so both are
listed. Binding only `dark` is how a `dark:` utility would silently render
its light branch on ink-blue. */
@custom-variant dark (&:where([data-theme="dark"], [data-theme="dark"] *, [data-theme="ink-blue"], [data-theme="ink-blue"] *));
/* ─── Self-hosted fonts (no Google CDN dependency) ────────────── */
/* Inter – variable weight 400-600, latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url('../../public/fonts/inter-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400 600;
font-display: swap;
src: url('../../public/fonts/inter-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Manrope – variable weight 400-800, latin */
@font-face {
font-family: 'Manrope';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url('../../public/fonts/manrope-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Manrope';
font-style: normal;
font-weight: 400 800;
font-display: swap;
src: url('../../public/fonts/manrope-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* JetBrains Mono – weight 400, latin */
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../../public/fonts/jetbrains-mono-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'JetBrains Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('../../public/fonts/jetbrains-mono-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Noto Serif SC – variable weight 600-900, latin only.
The serif headings of 「纸 · 墨 · 印」 are the "墨" half of the identity, but
the CJK cuts of this family run ~6MB per weight. Only the latin subset is
self-hosted (43KB for all three weights); Chinese glyphs fall through to the
platform serif named in `--font-headline`, which is what the handoff
prototype itself falls back to. */
@font-face {
font-family: 'Noto Serif SC';
font-style: normal;
font-weight: 600 900;
font-display: swap;
src: url('../../public/fonts/noto-serif-sc-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: 'Noto Serif SC';
font-style: normal;
font-weight: 600 900;
font-display: swap;
src: url('../../public/fonts/noto-serif-sc-latin-ext.woff2') format('woff2');
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Material Symbols Outlined – variable weight, use block to avoid showing raw text */
@font-face {
font-family: 'Material Symbols Outlined';
font-style: normal;
font-weight: 100 700;
font-display: block;
src: url('../../public/fonts/material-symbols-outlined.woff2') format('woff2');
}
.code-viewer-area .line,
.code-viewer-area .token-line {
display: block;
}
.code-viewer-area .line {
padding: 0 12px;
}
.code-viewer-area .has-line-numbers .line {
padding-left: 12px;
}
.code-viewer-area .line:hover,
.code-viewer-area .token-line:hover {
background: var(--color-surface-hover);
}
.code-viewer-area .has-line-numbers,
.code-viewer-area .rs-has-line-numbers {
counter-reset: line-number calc(var(--line-start, 1) - 1);
--line-numbers-foreground: var(--color-text-tertiary);
--line-numbers-width: 2.5ch;
--line-numbers-padding-left: 0;
--line-numbers-padding-right: 0;
--line-numbers-font-size: 11px;
--rs-line-numbers-foreground: var(--line-numbers-foreground);
--rs-line-numbers-width: var(--line-numbers-width);
--rs-line-numbers-padding-left: var(--line-numbers-padding-left);
--rs-line-numbers-padding-right: var(--line-numbers-padding-right);
--rs-line-numbers-font-size: var(--line-numbers-font-size);
}
.code-viewer-area .line-numbers::before,
.code-viewer-area .rs-line-number::before {
content: counter(line-number);
counter-increment: line-number;
display: inline-flex;
justify-content: flex-end;
box-sizing: content-box;
min-width: var(--rs-line-numbers-width);
padding-left: var(--rs-line-numbers-padding-left);
padding-right: var(--rs-line-numbers-padding-right);
color: var(--rs-line-numbers-foreground);
font-size: var(--rs-line-numbers-font-size);
user-select: none;
pointer-events: none;
margin-right: 1.5ch;
}
.settings-terminal-host .xterm {
height: 100%;
padding: 0;
}
.settings-terminal-host .xterm-viewport {
scrollbar-color: var(--color-outline-a72) transparent;
}
.settings-terminal-host .xterm-screen {
outline: none;
}
.settings-terminal-host .xterm-accessibility:not(.debug),
.settings-terminal-host .xterm-message {
position: absolute;
inset: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.settings-terminal-host .xterm-accessibility-tree {
color: transparent;
pointer-events: none;
}
.settings-terminal-host .xterm-helper-textarea {
position: absolute;
top: 0;
left: -9999em;
z-index: -5;
width: 0;
height: 0;
margin: 0;
border: 0;
padding: 0;
opacity: 0;
overflow: hidden;
resize: none;
white-space: nowrap;
}
.settings-checkbox-input {
position: absolute;
inset: 0;
z-index: 1;
width: 100%;
height: 100%;
margin: 0;
cursor: pointer;
opacity: 0;
}
.settings-checkbox-input:disabled {
cursor: not-allowed;
}
.settings-zoom-kbd {
min-width: 18px;
height: 18px;
padding: 0 5px;
border: 1px solid var(--color-border);
border-radius: 5px;
background: var(--color-surface-container-lowest);
color: var(--color-text-secondary);
font-family: var(--font-label);
font-size: 10px;
font-weight: 600;
line-height: 16px;
text-align: center;
box-shadow: inset 0 -1px 0 rgba(var(--cc-t1-rgb), 0.08);
}
.settings-zoom-control {
--settings-zoom-range-progress: 33.333%;
--settings-zoom-preview-scale: 0.88;
--settings-zoom-preview-opacity: 0;
--settings-zoom-track-empty: var(--color-surface-container-high);
--settings-zoom-thumb-bg: var(--color-surface-container-lowest);
--settings-zoom-thumb-border: var(--color-border);
--settings-zoom-thumb-shadow: 0 2px 8px rgba(var(--cc-t1-rgb), 0.16);
padding-top: 22px;
}
[data-theme="dark"] .settings-zoom-control,
[data-theme="ink-blue"] .settings-zoom-control {
--settings-zoom-track-empty: var(--color-surface-container-highest);
--settings-zoom-thumb-bg: var(--color-surface-bright);
--settings-zoom-thumb-border: var(--color-brand);
--settings-zoom-thumb-shadow: 0 0 0 3px rgba(var(--cc-ac-rgb), 0.2), 0 10px 24px rgba(0, 0, 0, 0.5);
}
.settings-zoom-control.is-dragging {
--settings-zoom-preview-scale: 1;
--settings-zoom-preview-opacity: 1;
}
.settings-zoom-range-wrap {
position: relative;
}
.settings-zoom-preview {
position: absolute;
left: clamp(24px, var(--settings-zoom-range-progress), calc(100% - 24px));
top: -30px;
z-index: 1;
min-width: 52px;
transform: translateX(-50%) scale(var(--settings-zoom-preview-scale));
transform-origin: 50% 100%;
border: 1px solid var(--color-surface-glass-border);
border-radius: 999px;
background: var(--color-surface-glass);
padding: 4px 9px;
color: var(--color-text-primary);
font-size: 12px;
font-weight: 700;
line-height: 16px;
text-align: center;
opacity: var(--settings-zoom-preview-opacity);
box-shadow: var(--shadow-dropdown);
backdrop-filter: blur(14px) saturate(1.15);
-webkit-backdrop-filter: blur(14px) saturate(1.15);
pointer-events: none;
transition:
left 260ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 160ms ease,
transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.settings-zoom-preview::after {
content: "";
position: absolute;
left: 50%;
bottom: -4px;
width: 8px;
height: 8px;
transform: translateX(-50%) rotate(45deg);
border-right: 1px solid var(--color-surface-glass-border);
border-bottom: 1px solid var(--color-surface-glass-border);
background: var(--color-surface-glass);
}
.settings-zoom-range {
appearance: none;
display: block;
height: 20px;
cursor: pointer;
background: transparent;
accent-color: var(--color-brand);
touch-action: none;
}
.settings-zoom-range:focus {
outline: none;
}
.settings-zoom-range:focus-visible {
outline: none;
}
.settings-zoom-range::-webkit-slider-runnable-track {
height: 6px;
border-radius: 999px;
border: 1px solid var(--color-border);
background:
linear-gradient(
90deg,
var(--color-brand) 0%,
var(--color-brand) var(--settings-zoom-range-progress),
var(--settings-zoom-track-empty) var(--settings-zoom-range-progress),
var(--settings-zoom-track-empty) 100%
);
transition: background 220ms ease, border-color 180ms ease;
}
.settings-zoom-range::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 18px;
margin-top: -7px;
border-radius: 999px;
border: 1px solid var(--settings-zoom-thumb-border);
background: var(--settings-zoom-thumb-bg);
box-shadow: var(--settings-zoom-thumb-shadow);
transition: border-color 180ms ease, box-shadow 220ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.settings-zoom-range:hover::-webkit-slider-thumb {
border-color: var(--color-border-focus);
}
.settings-zoom-control.is-dragging .settings-zoom-range::-webkit-slider-thumb {
transform: scale(1.18);
border-color: var(--color-brand);
box-shadow: var(--shadow-dropdown);
}
.settings-zoom-range:focus-visible::-webkit-slider-thumb {
border-color: var(--color-brand);
box-shadow: var(--shadow-focus-ring), 0 2px 8px rgba(var(--cc-t1-rgb), 0.16);
}
.settings-zoom-range::-moz-range-track {
height: 6px;
border-radius: 999px;
border: 1px solid var(--color-border);
background: var(--settings-zoom-track-empty);
transition: background 220ms ease, border-color 180ms ease;
}
.settings-zoom-range::-moz-range-progress {
height: 6px;
border-radius: 999px;
background: var(--color-brand);
}
.settings-zoom-range::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 999px;
border: 1px solid var(--settings-zoom-thumb-border);
background: var(--settings-zoom-thumb-bg);
box-shadow: var(--settings-zoom-thumb-shadow);
transition: border-color 180ms ease, box-shadow 220ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
}
.settings-zoom-control.is-dragging .settings-zoom-range::-moz-range-thumb {
transform: scale(1.18);
border-color: var(--color-brand);
box-shadow: var(--shadow-dropdown);
}
.settings-zoom-range:focus-visible::-moz-range-thumb {
border-color: var(--color-brand);
box-shadow: var(--shadow-focus-ring), 0 2px 8px rgba(var(--cc-t1-rgb), 0.16);
}
@media (prefers-reduced-motion: reduce) {
.settings-zoom-preview,
.settings-zoom-range::-webkit-slider-runnable-track,
.settings-zoom-range::-webkit-slider-thumb,
.settings-zoom-range::-moz-range-track,
.settings-zoom-range::-moz-range-thumb {
transition-duration: 0.01ms !important;
}
}
/* ─── Terminal ANSI palette ───────────────────────────────────────
The embedded terminal stays dark under all three themes (that is what
the per-theme `--color-terminal-*` values encode — light/white/dark
differ only in hue, not in lightness), so the 16 ANSI slots are shared.
Split them into the theme blocks if a theme ever needs its own ramp.
Consumed by `lib/terminalTheme.ts`, which is the only place allowed to
turn these into an xterm ITheme object. */
:root {
--color-terminal-ansi-black: #1F1F1F;
--color-terminal-ansi-red: #FF6D67;
--color-terminal-ansi-green: #7EF18A;
--color-terminal-ansi-yellow: #F8C55F;
--color-terminal-ansi-blue: #77A8FF;
--color-terminal-ansi-magenta: #D699FF;
--color-terminal-ansi-cyan: #61D6D6;
--color-terminal-ansi-white: #D7D2D0;
--color-terminal-ansi-bright-black: #8F8683;
--color-terminal-ansi-bright-red: #FF8A85;
--color-terminal-ansi-bright-green: #9FF7A7;
--color-terminal-ansi-bright-yellow: #FFDD7A;
--color-terminal-ansi-bright-blue: #A6C5FF;
--color-terminal-ansi-bright-magenta: #E3B8FF;
--color-terminal-ansi-bright-cyan: #8CEEEE;
--color-terminal-ansi-bright-white: #FFFFFF;
}
/* ─── Layering scale ──────────────────────────────────────────────
One ordered stack for every overlay. Theme-independent, so it lives
outside the theme blocks.
Two orderings here are deliberate and load-bearing:
1. dropdown/popover sit ABOVE dialog. A modal dialog blocks the page
behind it, so any open dropdown necessarily belongs to the topmost
dialog. Putting dropdowns below dialogs makes DirectoryPicker
unusable inside AgentManager's modal — which is exactly why it used
to hardcode `zIndex: 9999`.
2. toast sits above sheet. Otherwise a confirmation shown while a
mobile bottom sheet is open is invisible to the user.
Tailwind v4 has no z-index theme namespace, so reference these as
`z-[var(--z-dialog)]` (or `zIndex: 'var(--z-dialog)'` inline).
Never introduce a raw z-index outside this scale. */
:root {
--z-base: 0;
--z-raised: 10;
--z-sticky: 20;
--z-nav: 30;
--z-scrim: 40;
--z-drawer: 50;
--z-dialog: 60;
--z-sheet: 65;
--z-dropdown: 70;
--z-popover: 75;
--z-tooltip: 80;
--z-toast: 90;
}
/* ─── Tailwind static defaults ────────────────────────────────────
Tailwind v4 resolves `@theme` at build time, so these have to be literal
values rather than `var()` references. They carry the pure-white palette;
the `:root` semantic layer below re-points every one of them at a
`--cc-*` source variable, which is what actually varies per theme. */
@theme {
--color-primary: #96442B;
--color-primary-container: #7C3620;
--color-primary-fixed: #F6ECE5;
--color-primary-fixed-dim: #E6CDBF;
--color-on-primary: #FBF6EC;
--color-on-primary-container: #FBF6EC;
--color-on-surface: #1F1C17;
--color-on-surface-variant: #5D5850;
--color-surface: #FFFFFF;
--color-surface-bright: #FFFFFF;
--color-surface-dim: #F0EFE9;
--color-surface-container: #FAFAF8;
--color-surface-container-low: #F6F6F3;
--color-surface-container-high: #F0EFE9;
--color-surface-container-highest: #EBE9E2;
--color-surface-container-lowest: #FFFFFF;
--color-surface-variant: #F6F6F3;
--color-background: #FFFFFF;
--color-outline: #D8D5CC;
--color-outline-variant: #EBE9E2;
--color-secondary: #96442B;
--color-secondary-container: #F5ECEA;
--color-tertiary: #3B7068;
--color-tertiary-container: #F1F5F4;
--color-error: #C74436;
--color-on-error: #FBF6EC;
--color-error-container: #FAECE8;
--color-on-error-container: #BD4133;
--color-inverse-surface: #1F1C17;
--color-inverse-on-surface: #FFFFFF;
--color-inverse-primary: #E6CDBF;
--color-success: #3E8153;
--color-success-container: #E8F2E9;
--color-on-success-container: #3A784D;
--color-info: #96442B;
--color-info-container: #F5ECEA;
--color-on-info-container: #96442B;
--color-warning: #A8751F;
--color-warning-container: #F8F0DC;
--color-on-warning-container: #90641B;
/* Type roles. Headings are serif — that is the "墨" half of 「纸 · 墨 · 印」.
Only the latin cut of Noto Serif SC is self-hosted; CJK falls through to
the platform serif, which is what the handoff prototype does too. */
--font-headline: 'Noto Serif SC', 'Songti SC', 'Source Han Serif SC', 'Noto Serif CJK SC', 'SimSun', Georgia, serif;
--font-serif: var(--font-headline);
--font-body: 'Inter', -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
--font-label: var(--font-body);
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
/* Corner scale, from the handoff: 6–7 badges / 9–11 controls /
12–14 inner cards / 16–18 cards / 20 composer / 24 modals / 99 pills. */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 13px;
--radius-xl: 17px;
--radius-2xl: 20px;
--radius-3xl: 24px;
--radius-full: 9999px;
}
/* ─── Theme sources ───────────────────────────────────────────────
Six palettes from the 「纸 · 墨 · 印」 handoff. Each block carries only
raw color values; the semantic layer below turns them into the
`--color-*` tokens the components consume. Generated values
(`*-ink`, the heat ramp, the `*-rgb` triplets) are derived from these
sources — see the header comment on the semantic layer. */
/* 纯白 pure-white — 默认 */
:root,
[data-theme="white"] {
color-scheme: light;
--cc-bg: #FFFFFF;
--cc-s0: #F6F6F3;
--cc-s1: #F0EFE9;
--cc-s2: #FAFAF8;
--cc-bd: #EBE9E2;
--cc-bd2: #D8D5CC;
/* Control boundary — clears WCAG 1.4.11's 3:1 on the card lining. */
--cc-bd-strong: #918E87;
/* Tab strip. Trough stays --cc-s0, so these two carry the shape. */
--cc-tab-edge: #D9D5CC;
--cc-tab-sep: #E3DFD6;
--cc-t1: #1F1C17;
--cc-t2: #5D5850;
--cc-t3: #777269;
--cc-ac: #96442B;
--cc-ac2: #7C3620;
--cc-ac-soft: #F6ECE5;
--cc-ac-soft-hover: #EFDED4;
--cc-ac-border: #E6CDBF;
--cc-ac-ink: #96442B;
--cc-on-ac: #FBF6EC;
--cc-ok: #3E8153;
--cc-ok-soft: #E8F2E9;
--cc-ok-ink: #3A784D;
--cc-wn: #A8751F;
--cc-wn-soft: #F8F0DC;
--cc-wn-ink: #90641B;
--cc-er: #C74436;
--cc-er-soft: #FAECE8;
--cc-er-soft-hover: #F5DBD6;
--cc-er-ink: #BD4133;
--cc-info: #96442B;
--cc-info-soft: #F5ECEA;
--cc-info-ink: #96442B;
--cc-teal: #3B7068;
--cc-teal-soft: #F1F5F4;
--cc-teal-border: #C0D1CF;
--cc-teal-icon: #EBF1F0;
--cc-diff-add: #E8F3E9;
--cc-diff-add-gutter: #D9EBDC;
--cc-diff-add-word: #DEEDE0;
--cc-diff-del: #FAEEED;
--cc-diff-del-gutter: #F6E1DF;
--cc-diff-del-word: #F0CECB;
--cc-diff-mark: #F4EDE2;
--cc-diff-mark-gutter: #ECE1CE;
--cc-code: #F7F7F4;
--cc-heat-0: #F0EFE9;
--cc-heat-1: #E8D6D0;
--cc-heat-2: #D1ADA2;
--cc-heat-3: #B8806F;
--cc-heat-4: #9C4F38;
--cc-shadow-card: 0 1px 3px rgba(30, 28, 22, 0.04), 0 6px 24px rgba(30, 28, 22, 0.06);
--cc-shadow-composer: 0 1px 2px rgba(30, 28, 22, 0.05), 0 14px 44px rgba(30, 28, 22, 0.08);
--cc-shadow-overlay: 0 8px 24px rgba(30, 28, 22, 0.10), 0 24px 64px rgba(30, 28, 22, 0.15);
--cc-bg-rgb: 255, 255, 255;
--cc-s1-rgb: 240, 239, 233;
--cc-t1-rgb: 31, 28, 23;
--cc-t2-rgb: 93, 88, 80;
--cc-bd2-rgb: 216, 213, 204;
--cc-ac-rgb: 150, 68, 43;
--cc-er-rgb: 199, 68, 54;
--cc-scrim-rgb: 19, 17, 14;
}
/* 纸墨 paper */
[data-theme="paper"] {
color-scheme: light;
--cc-bg: #FBF9F4;
--cc-s0: #F3EFE5;
--cc-s1: #ECE6D8;
--cc-s2: #F7F4EC;
--cc-bd: #E7E0CF;
--cc-bd2: #D5C9B0;
/* Control boundary — clears WCAG 1.4.11's 3:1 on the card lining. */
--cc-bd-strong: #938A76;
/* Tab strip. Trough stays --cc-s0, so these two carry the shape. */
--cc-tab-edge: #DBCFB5;
--cc-tab-sep: #E5D9BE;
--cc-t1: #231D12;
--cc-t2: #6E6350;
--cc-t3: #796E5B;
--cc-ac: #96442B;
--cc-ac2: #7C3620;
--cc-ac-soft: #F3E6DA;
--cc-ac-soft-hover: #EBD8C8;
--cc-ac-border: #E2C7B3;
--cc-ac-ink: #96442B;
--cc-on-ac: #FBF6EC;
--cc-ok: #477C52;
--cc-ok-soft: #E8EFE1;
--cc-ok-ink: #43754D;
--cc-wn: #A8751F;
--cc-wn-soft: #F6EED9;
--cc-wn-ink: #8E631A;
--cc-er: #C24532;
--cc-er-soft: #F7E7DF;
--cc-er-soft-hover: #F2D7CE;
--cc-er-ink: #B6412F;
--cc-info: #96442B;
--cc-info-soft: #F1E7E0;
--cc-info-ink: #96442B;
--cc-teal: #3B7068;
--cc-teal-soft: #EEEFEA;
--cc-teal-border: #BECDC7;
--cc-teal-icon: #E8EBE6;
--cc-diff-add: #E9EFDC;
--cc-diff-add-gutter: #DCE7C9;
--cc-diff-add-word: #E0E9CF;
--cc-diff-del: #F6E9E3;
--cc-diff-del-gutter: #F2DCD5;
--cc-diff-del-word: #ECCAC2;
--cc-diff-mark: #F0E8D8;
--cc-diff-mark-gutter: #E9DCC5;
--cc-code: #F5F1E4;
--cc-heat-0: #ECE6D8;
--cc-heat-1: #E5D1C8;
--cc-heat-2: #CFA99C;
--cc-heat-3: #B67E6B;
--cc-heat-4: #9C4F37;
--cc-shadow-card: 0 1px 3px rgba(64, 49, 26, 0.05), 0 6px 24px rgba(64, 49, 26, 0.07);
--cc-shadow-composer: 0 1px 2px rgba(64, 49, 26, 0.06), 0 14px 44px rgba(64, 49, 26, 0.10);
--cc-shadow-overlay: 0 8px 24px rgba(64, 49, 26, 0.12), 0 24px 64px rgba(64, 49, 26, 0.16);
--cc-bg-rgb: 251, 249, 244;
--cc-s1-rgb: 236, 230, 216;
--cc-t1-rgb: 35, 29, 18;
--cc-t2-rgb: 110, 99, 80;
--cc-bd2-rgb: 213, 201, 176;
--cc-ac-rgb: 150, 68, 43;
--cc-er-rgb: 194, 69, 50;
--cc-scrim-rgb: 21, 17, 11;
}
/* 经典暖色 warm-classic */
[data-theme="warm-classic"] {
color-scheme: light;
--cc-bg: #F6F0E1;
--cc-s0: #EDE5D1;
--cc-s1: #E5DBC3;
--cc-s2: #F1EBDA;
--cc-bd: #DED2B8;
--cc-bd2: #C8B897;
/* Control boundary — clears WCAG 1.4.11's 3:1 on the card lining. */
--cc-bd-strong: #908369;
/* Tab strip. Trough stays --cc-s0, so these two carry the shape. */
--cc-tab-edge: #D6C5A2;
--cc-tab-sep: #E2CFAA;
--cc-t1: #251E11;
--cc-t2: #6E6146;
--cc-t3: #746951;
--cc-ac: #96442B;
--cc-ac2: #7C3620;
--cc-ac-soft: #F0E1D0;
--cc-ac-soft-hover: #E8D2BE;
--cc-ac-border: #DFC0A8;
--cc-ac-ink: #96442B;
--cc-on-ac: #FBF6EC;
--cc-ok: #3E8153;
--cc-ok-soft: #E8F2E9;
--cc-ok-ink: #3A784D;
--cc-wn: #A8751F;
--cc-wn-soft: #F8F0DC;
--cc-wn-ink: #90641B;
--cc-er: #C74436;
--cc-er-soft: #FAECE8;
--cc-er-soft-hover: #F5DBD6;
--cc-er-ink: #BD4133;
--cc-info: #96442B;
--cc-info-soft: #ECDFCF;
--cc-info-ink: #96442B;
--cc-teal: #3E6B58;
--cc-teal-soft: #E9E7D7;
--cc-teal-border: #BBC5B5;
--cc-teal-icon: #E4E3D3;
--cc-diff-add: #E6ECD3;
--cc-diff-add-gutter: #D8E2BF;
--cc-diff-add-word: #DCE5C5;
--cc-diff-del: #F2E1D2;
--cc-diff-del-gutter: #EED4C6;
--cc-diff-del-word: #EAC3B5;
--cc-diff-mark: #ECE0C8;
--cc-diff-mark-gutter: #E5D5B6;
--cc-code: #EFE8D3;
--cc-heat-0: #E5DBC3;
--cc-heat-1: #E1CAB9;
--cc-heat-2: #CCA491;
--cc-heat-3: #B57B65;
--cc-heat-4: #9C4E36;
--cc-shadow-card: 0 1px 3px rgba(64, 49, 26, 0.05), 0 6px 24px rgba(64, 49, 26, 0.07);
--cc-shadow-composer: 0 1px 2px rgba(64, 49, 26, 0.06), 0 14px 44px rgba(64, 49, 26, 0.10);
--cc-shadow-overlay: 0 8px 24px rgba(64, 49, 26, 0.12), 0 24px 64px rgba(64, 49, 26, 0.16);
--cc-bg-rgb: 246, 240, 225;
--cc-s1-rgb: 229, 219, 195;
--cc-t1-rgb: 37, 30, 17;
--cc-t2-rgb: 110, 97, 70;
--cc-bd2-rgb: 200, 184, 151;
--cc-ac-rgb: 150, 68, 43;
--cc-er-rgb: 199, 68, 54;
--cc-scrim-rgb: 22, 18, 10;
}
/* 青瓷 celadon */
[data-theme="celadon"] {
color-scheme: light;
--cc-bg: #F5F8F5;
--cc-s0: #EAF0EB;
--cc-s1: #E0E9E2;
--cc-s2: #F0F4F0;
--cc-bd: #DCE5DE;
--cc-bd2: #C6D4C9;
/* Control boundary — clears WCAG 1.4.11's 3:1 on the card lining. */
--cc-bd-strong: #818D84;
/* Tab strip. Trough stays --cc-s0, so these two carry the shape. */
--cc-tab-edge: #C5D3C8;
--cc-tab-sep: #CEDCD1;
--cc-t1: #1C231D;
--cc-t2: #5D6B60;
--cc-t3: #667268;
--cc-ac: #96442B;
--cc-ac2: #7C3620;
--cc-ac-soft: #F0E7DC;
--cc-ac-soft-hover: #E8DACB;
--cc-ac-border: #DFC9B6;
--cc-ac-ink: #96442B;
--cc-on-ac: #FBF6EC;
--cc-ok: #3E8153;
--cc-ok-soft: #E3EFE5;
--cc-ok-ink: #39764C;
--cc-wn: #A8751F;
--cc-wn-soft: #F8F0DC;
--cc-wn-ink: #90641B;
--cc-er: #C74436;
--cc-er-soft: #FAECE8;
--cc-er-soft-hover: #F5DBD6;
--cc-er-ink: #BD4133;
--cc-info: #96442B;
--cc-info-soft: #ECE6E1;
--cc-info-ink: #96442B;
--cc-teal: #2F6E63;
--cc-teal-soft: #E7EEEB;
--cc-teal-border: #B6CCC6;
--cc-teal-icon: #E1EAE6;
--cc-diff-add: #E4F0E5;
--cc-diff-add-gutter: #D4E6D7;
--cc-diff-add-word: #D9E9DB;
--cc-diff-del: #F1E8E4;
--cc-diff-del-gutter: #EEDBD6;
--cc-diff-del-word: #E9C9C3;
--cc-diff-mark: #EBE7D9;
--cc-diff-mark-gutter: #E4DBC6;
--cc-code: #EFF4F0;
--cc-heat-0: #E0E9E2;
--cc-heat-1: #E0D0C9;
--cc-heat-2: #CBA99C;
--cc-heat-3: #B47E6C;
--cc-heat-4: #9C4F37;
--cc-shadow-card: 0 1px 3px rgba(28, 42, 32, 0.05), 0 6px 24px rgba(28, 42, 32, 0.06);
--cc-shadow-composer: 0 1px 2px rgba(28, 42, 32, 0.05), 0 14px 44px rgba(28, 42, 32, 0.08);
--cc-shadow-overlay: 0 8px 24px rgba(28, 42, 32, 0.10), 0 24px 64px rgba(28, 42, 32, 0.15);
--cc-bg-rgb: 245, 248, 245;
--cc-s1-rgb: 224, 233, 226;
--cc-t1-rgb: 28, 35, 29;
--cc-t2-rgb: 93, 107, 96;
--cc-bd2-rgb: 198, 212, 201;
--cc-ac-rgb: 150, 68, 43;
--cc-er-rgb: 199, 68, 54;
--cc-scrim-rgb: 17, 21, 17;
}
/* 墨夜 dark */
[data-theme="dark"] {
color-scheme: dark;
--cc-bg: #201D17;
--cc-s0: #1A1813;
--cc-s1: #2B271F;
--cc-s2: #252119;
--cc-bd: #383226;
--cc-bd2: #4C4433;
/* Control boundary — clears WCAG 1.4.11's 3:1 on the card lining. */
--cc-bd-strong: #756D5C;
/* Tab strip. Trough stays --cc-s0, so these two carry the shape. Both go
*lighter* than the trough here: an ink theme's paper is already so dark
that pure black is only 1.26:1 against it, so there is no room downward. */
--cc-tab-edge: #353024;
--cc-tab-sep: #2D281F;
--cc-t1: #EDE6D6;
--cc-t2: #A79B83;
--cc-t3: #918777;
--cc-ac: #D07B52;
--cc-ac2: #DE8F66;
--cc-ac-soft: #3B2A1E;
--cc-ac-soft-hover: #493424;
--cc-ac-border: #59402C;
--cc-ac-ink: #D2815B;
--cc-on-ac: #201D17;
--cc-ok: #84B383;
--cc-ok-soft: #28301D;
--cc-ok-ink: #84B383;
--cc-wn: #D3A34C;
--cc-wn-soft: #332916;
--cc-wn-ink: #D3A34C;
--cc-er: #E06B52;
--cc-er-soft: #3A241B;
--cc-er-soft-hover: #552F24;
--cc-er-ink: #E17058;
--cc-info: #D07B52;
--cc-info-soft: #3C2C20;
--cc-info-ink: #D3845E;
--cc-teal: #7DD3C7;
--cc-teal-soft: #2B332C;
--cc-teal-border: #3C544C;
--cc-teal-icon: #313E37;
--cc-diff-add: #2B331E;
--cc-diff-add-gutter: #354026;
--cc-diff-add-word: #323C24;
--cc-diff-del: #3B281F;
--cc-diff-del-gutter: #462D23;
--cc-diff-del-word: #563328;
--cc-diff-mark: #39301E;
--cc-diff-mark-gutter: #443822;
--cc-code: #1A1813;
--cc-heat-0: #2B271F;
--cc-heat-1: #473224;
--cc-heat-2: #6D4631;
--cc-heat-3: #985D3F;
--cc-heat-4: #C5754E;
--cc-shadow-card: 0 1px 3px rgba(0, 0, 0, 0.35), 0 6px 24px rgba(0, 0, 0, 0.35);
--cc-shadow-composer: 0 1px 2px rgba(0, 0, 0, 0.45), 0 14px 44px rgba(0, 0, 0, 0.50);
--cc-shadow-overlay: 0 8px 24px rgba(0, 0, 0, 0.50), 0 24px 64px rgba(0, 0, 0, 0.55);
--cc-bg-rgb: 32, 29, 23;
--cc-s1-rgb: 43, 39, 31;
--cc-t1-rgb: 237, 230, 214;
--cc-t2-rgb: 167, 155, 131;
--cc-bd2-rgb: 76, 68, 51;
--cc-ac-rgb: 208, 123, 82;
--cc-er-rgb: 224, 107, 82;
--cc-scrim-rgb: 6, 6, 6;
}
/* 墨夜蓝 ink-blue */
[data-theme="ink-blue"] {
color-scheme: dark;
--cc-bg: #1A1D24;
--cc-s0: #15181E;
--cc-s1: #252A35;
--cc-s2: #20242D;
--cc-bd: #2F3441;
--cc-bd2: #434A5B;
/* Control boundary — clears WCAG 1.4.11's 3:1 on the card lining. */
--cc-bd-strong: #6A707E;
/* Tab strip. Trough stays --cc-s0, so these two carry the shape. Both go
*lighter* than the trough here: an ink theme's paper is already so dark
that pure black is only 1.25:1 against it, so there is no room downward. */
--cc-tab-edge: #2C303C;
--cc-tab-sep: #252833;
--cc-t1: #E8EAF0;
--cc-t2: #9AA1B2;
--cc-t3: #858B97;
--cc-ac: #D07B52;
--cc-ac2: #DE8F66;
--cc-ac-soft: #37281F;
--cc-ac-soft-hover: #453326;
--cc-ac-border: #57402F;
--cc-ac-ink: #D17E56;
--cc-on-ac: #1A1D24;
--cc-ok: #6FB58C;
--cc-ok-soft: #213026;
--cc-ok-ink: #6FB58C;
--cc-wn: #D3A34C;
--cc-wn-soft: #2F2917;
--cc-wn-ink: #D3A34C;
--cc-er: #E0685E;
--cc-er-soft: #37211D;
--cc-er-soft-hover: #522C27;
--cc-er-ink: #E06A60;
--cc-info: #8FA8E8;
--cc-info-soft: #2D3343;
--cc-info-ink: #8FA8E8;
--cc-teal: #7DD3C7;
--cc-teal-soft: #263338;
--cc-teal-border: #385455;
--cc-teal-icon: #2C3E41;
--cc-diff-add: #233028;
--cc-diff-add-gutter: #2C3B31;
--cc-diff-add-word: #29382E;
--cc-diff-del: #36282C;
--cc-diff-del-gutter: #422C30;
--cc-diff-del-word: #513234;
--cc-diff-mark: #34302A;
--cc-diff-mark-gutter: #3F382C;
--cc-code: #15181E;
--cc-heat-0: #252A35;
--cc-heat-1: #42322E;
--cc-heat-2: #6A4638;
--cc-heat-3: #965D43;
--cc-heat-4: #C5754F;
--cc-shadow-card: 0 1px 3px rgba(0, 0, 0, 0.35), 0 6px 24px rgba(0, 0, 0, 0.35);
--cc-shadow-composer: 0 1px 2px rgba(0, 0, 0, 0.45), 0 14px 44px rgba(0, 0, 0, 0.50);
--cc-shadow-overlay: 0 8px 24px rgba(0, 0, 0, 0.50), 0 24px 64px rgba(0, 0, 0, 0.55);
--cc-bg-rgb: 26, 29, 36;
--cc-s1-rgb: 37, 42, 53;
--cc-t1-rgb: 232, 234, 240;
--cc-t2-rgb: 154, 161, 178;
--cc-bd2-rgb: 67, 74, 91;
--cc-ac-rgb: 208, 123, 82;
--cc-er-rgb: 224, 104, 94;
--cc-scrim-rgb: 6, 6, 6;
}
/* ─── Semantic layer ──────────────────────────────────────────────
Every `--color-*` the app consumes is defined exactly once, here, in terms
of the `--cc-*` source variables above. Adding a theme therefore means
adding one source block — never touching this layer.
This replaced three hand-maintained per-theme copies of the same 200 tokens,
where the failure mode was a token defined in two themes and forgotten in
the third: it renders transparent, and only in that one theme. */
:root {
/* Layout dimensions */
--sidebar-width: 280px;
--sidebar-rail-width: 72px;
--titlebar-height: 52px;
--statusbar-height: 36px;
--motion-sidebar-duration: 280ms;
--motion-sidebar-easing: cubic-bezier(0.22, 1, 0.36, 1);
/* Surfaces. The handoff has four grounds: page (--bg), sidebar and
secondary panels (--s0), hover/selected (--s1), card lining (--s2). */
--color-background: var(--cc-bg);
--color-surface: var(--cc-bg);
--color-surface-bright: var(--cc-bg);
--color-surface-container-lowest: var(--cc-bg);
--color-surface-container: var(--cc-s2);
--color-surface-container-low: var(--cc-s0);
--color-surface-container-high: var(--cc-s1);
--color-surface-container-highest: var(--cc-bd);
--color-surface-variant: var(--cc-s0);
--color-surface-dim: var(--cc-s1);
--color-surface-sidebar: var(--cc-s0);
--color-surface-hover: var(--cc-s1);
--color-surface-selected: var(--cc-s1);
--color-surface-info: var(--cc-s0);
--color-surface-user-msg: var(--cc-s1);
/* Borders */
--color-outline: var(--cc-bd2);
--color-outline-variant: var(--cc-bd);
--color-border: var(--cc-bd);
/* For CONTROL boundaries (checkbox/radio outlines): --color-border is a
hairline separator at ~1.2:1 and fails WCAG 1.4.11 the moment it has to
delineate an interactive control. */
--color-border-strong: var(--cc-bd-strong);
--color-border-separator: var(--cc-bd);
--color-border-focus: var(--cc-ac);
/* Tab strip outlines. The strip's trough deliberately stays the sidebar's
own ground, so the selected tab's fill is only 1.05–1.10:1 against it in
all six themes — far too little for a rounded corner to be *visible*, and
a curve nobody can see is not a curve. These two carry the shape instead:
`tab-edge` traces the selected tab at ~1.35:1 against the trough,
`tab-separator` is the hairline between two neighbouring idle tabs at
~1.22:1. They cannot be folded into `--color-border`/`--color-outline`:
those are calibrated against paper, and on the trough they land at 1.12
(invisible) and 1.36–2.01 (a drawn box) respectively, in opposite
directions per theme family. */
--color-tab-edge: var(--cc-tab-edge);
--color-tab-separator: var(--cc-tab-sep);
/* Text */
--color-on-surface: var(--cc-t1);
--color-on-surface-variant: var(--cc-t2);
--color-text-primary: var(--cc-t1);
--color-text-secondary: var(--cc-t2);
--color-text-tertiary: var(--cc-t3);
--color-text-accent: var(--cc-info);
/* Terracotta seal — the brand accent */
--color-primary: var(--cc-ac);
--color-primary-container: var(--cc-ac2);
--color-primary-fixed: var(--cc-ac-soft);
--color-primary-fixed-dim: var(--cc-ac-border);
--color-on-primary: var(--cc-on-ac);
--color-on-primary-container: var(--cc-on-ac);
--color-brand: var(--cc-ac);
--color-brand-hover: var(--cc-ac2);
/* Opaque on purpose: the `/10` alpha modifier compiles to a color function
Safari 15 WebViews cannot parse (guarded in vite-config.test.ts). */
--color-brand-soft: var(--cc-ac-soft);
--color-brand-soft-hover: var(--cc-ac-soft-hover);
/* Text sitting ON `--color-brand-soft`. Not the same as `--color-brand`:
the two ink palettes' terracotta lands at 4.3–4.5:1 on their own soft
fill, so a terracotta chip label has to use this darkened pair instead. */
--color-on-brand-soft: var(--cc-ac-ink);
/* Inner rule engraved on the large seal — the accent's own foreground at
40%, so it stays a highlight rather than a hard white line. */
--color-brand-seal-rule: rgba(251, 246, 236, 0.4);
--color-inverse-surface: var(--cc-t1);
--color-inverse-on-surface: var(--cc-bg);
--color-inverse-primary: var(--cc-ac-border);
--color-secondary: var(--cc-info);
--color-secondary-container: var(--cc-info-soft);
--color-tertiary: var(--cc-teal);
--color-tertiary-container: var(--cc-teal-soft);
/* Status tones. `--color-on-<tone>-container` is the AA-checked foreground
for `--color-<tone>-container`; `--color-<tone>` alone is too light on its
own fill (that is the 2.66:1 warning badge contrast.test.ts guards). */
--color-success: var(--cc-ok);
--color-success-container: var(--cc-ok-soft);
--color-on-success-container: var(--cc-ok-ink);
--color-warning: var(--cc-wn);
--color-warning-container: var(--cc-wn-soft);
--color-on-warning-container: var(--cc-wn-ink);
--color-error: var(--cc-er);
--color-on-error: var(--cc-on-ac);
--color-error-container: var(--cc-er-soft);
--color-on-error-container: var(--cc-er-ink);
--color-error-soft: var(--cc-er-soft);
--color-error-soft-hover: var(--cc-er-soft-hover);
--color-info: var(--cc-info);
--color-info-container: var(--cc-info-soft);
--color-on-info-container: var(--cc-info-ink);
/* Memory / goal surfaces keep a teal identity of their own */
--color-memory-accent: var(--cc-teal);
--color-memory-surface: var(--cc-teal-soft);
--color-memory-border: var(--cc-teal-border);
--color-memory-icon-bg: var(--cc-teal-icon);
--color-goal-accent: var(--cc-teal);
--color-goal-surface: var(--cc-teal-soft);
--color-goal-border: var(--cc-teal-border);
--color-goal-icon-bg: var(--cc-teal-icon);
--color-goal-chip-bg: var(--cc-bg);
--color-goal-chip-border: var(--cc-bd);
/* Controls */
--color-model-option-selected-bg: var(--cc-ac-soft);
--color-model-option-selected-border: var(--cc-ac-border);
--color-switch-checked-bg: var(--color-brand);
--color-switch-thumb: var(--cc-bg);
--color-btn-primary-bg: var(--cc-t1);
--color-btn-primary-fg: var(--cc-bg);
/* Flat ink, not a gradient: the handoff's primary action is a solid ink
block that turns terracotta on hover. Kept as gradient-shaped tokens so
existing call sites keep resolving. */
--gradient-btn-primary: linear-gradient(var(--cc-t1), var(--cc-t1));
--gradient-btn-primary-hover: linear-gradient(var(--cc-ac), var(--cc-ac));
/* Sidebar chrome */
--color-sidebar-filter-bg: var(--cc-bg);
--color-sidebar-filter-border: var(--cc-bd);
--color-sidebar-filter-icon-bg: transparent;
--color-sidebar-search-bg: var(--cc-bg);
--color-sidebar-search-border: var(--cc-bd);
--color-sidebar-item-hover: var(--cc-s1);
--color-sidebar-item-active: var(--cc-bg);
--color-sidebar-item-active-border: var(--cc-bd);
--sidebar-panel-bg-image: none;
/* Activity heatmap */
--color-activity-heat-0: var(--cc-heat-0);
--color-activity-heat-1: var(--cc-heat-1);
--color-activity-heat-2: var(--cc-heat-2);
--color-activity-heat-3: var(--cc-heat-3);
--color-activity-heat-4: var(--cc-heat-4);
--color-activity-cell-border: rgba(var(--cc-bd2-rgb), 0.42);
--color-activity-cell-border-hover: rgba(var(--cc-ac-rgb), 0.58);
--color-activity-cell-border-active: var(--cc-t1);
--shadow-activity-cell-hover: 0 0 0 2px rgba(var(--cc-ac-rgb), 0.14), 0 8px 18px rgba(var(--cc-scrim-rgb), 0.18);
--color-activity-tooltip-surface: var(--cc-t1);
--color-activity-tooltip-border: rgba(var(--cc-bg-rgb), 0.16);
--color-activity-tooltip-text: var(--cc-bg);
--color-activity-tooltip-muted: var(--cc-ac-border);
/* Trace inspector: the handoff renders it with the ordinary panel palette
rather than a private one, so these now alias straight through. */
--color-inspector-surface: var(--cc-bg);
--color-inspector-panel: var(--cc-s0);
--color-inspector-chip: var(--cc-s1);
--color-inspector-border: var(--cc-bd);
--color-inspector-accent: var(--cc-ac);
--color-inspector-accent-hover: var(--cc-ac2);
--color-inspector-accent-secondary: var(--cc-info);
--color-inspector-text: var(--cc-t1);
--color-inspector-heading: var(--cc-t1);
--color-inspector-muted: var(--cc-t3);
--color-inspector-muted-strong: var(--cc-t2);
--color-inspector-success: var(--cc-ok-ink);
--color-inspector-success-bg: var(--cc-ok-soft);
--color-inspector-danger: var(--cc-er-ink);
--color-inspector-danger-bg: var(--cc-er-soft);
--color-inspector-danger-surface: var(--cc-er-soft);
--color-inspector-danger-border: var(--cc-er-soft-hover);
--color-inspector-capacity: var(--cc-t3);
/* Overlays */
/* `--color-overlay-scrim` dims for non-modal layers (the mobile sidebar
drawer); `--color-modal-scrim` is the heavier one true modals sit on. */
--color-overlay-scrim: rgba(var(--cc-scrim-rgb), 0.42);
--color-modal-scrim: rgba(var(--cc-scrim-rgb), 0.5);
/* Dialogs are opaque: see `.dialog-panel`. Light themes put the paper at the
top of the ramp, so the lifted surface is the background value itself. */
--color-surface-dialog: var(--cc-bg);
--color-surface-glass: rgba(var(--cc-bg-rgb), 0.92);
--color-surface-glass-border: rgba(var(--cc-bd2-rgb), 0.4);
/* Shadows: card / composer / floating overlay */
--shadow-card: var(--cc-shadow-card);
--shadow-composer: var(--cc-shadow-composer);
--shadow-overlay: var(--cc-shadow-overlay);
--shadow-dropdown: var(--cc-shadow-overlay);
--shadow-inspector: var(--cc-shadow-overlay);
--shadow-sidebar-filter: none;
--shadow-focus-ring: 0 0 0 1px rgba(var(--cc-ac-rgb), 0.24);
--shadow-error-ring: 0 0 0 1px rgba(var(--cc-er-rgb), 0.26);
--shadow-button-primary: 0 2px 10px rgba(var(--cc-t1-rgb), 0.22);
/* Code + diff */
--color-code-bg: var(--cc-code);
--color-code-fg: var(--cc-t1);
--color-diff-added-bg: var(--cc-diff-add);
--color-diff-added-word: var(--cc-diff-add-word);
--color-diff-added-gutter: var(--cc-diff-add-gutter);
--color-diff-added-text: var(--cc-ok-ink);
--color-diff-removed-bg: var(--cc-diff-del);
--color-diff-removed-word: var(--cc-diff-del-word);
--color-diff-removed-gutter: var(--cc-diff-del-gutter);
--color-diff-removed-text: var(--cc-er-ink);
--color-diff-highlight-bg: var(--cc-diff-mark);
--color-diff-highlight-gutter: var(--cc-diff-mark-gutter);
--color-diff-title-bg: var(--cc-s0);
--color-diff-title-color: var(--cc-t3);
--color-diff-title-border: var(--cc-bd);
/* Syntax highlighting. One ramp for the four paper themes; the two ink
themes override it below. */
--color-code-comment: #8A8478;
--color-code-string: #437220;
--color-code-keyword: #B8533B;
--color-code-function: #1D5A8C;
--color-code-number: #1B7A6A;
--color-code-property: #7A3E20;
--color-code-type: #7E5520;
--color-code-parameter: #5C3D2E;
--color-code-punctuation: #5C504A;
--color-code-inserted: var(--cc-ok-ink);
--color-code-deleted: var(--cc-er-ink);
--color-diff-syntax-foreground: var(--cc-t1);
--color-diff-syntax-comment: #8A8478;
--color-diff-syntax-string: #437220;
--color-diff-syntax-number: #1B7A6A;
--color-diff-syntax-regexp: #1D5A8C;
--color-diff-syntax-keyword: #B8533B;
--color-diff-syntax-variable: #7A3E20;
--color-diff-syntax-parameter: #5C504A;
--color-diff-syntax-property: #7A3E20;
--color-diff-syntax-function: #1D5A8C;
--color-diff-syntax-type: #7E5520;
--color-diff-syntax-punctuation: #5C504A;
/* Terminal. The handoff pins one warm-ink panel across every theme; the
ink-blue theme swaps in its own cool ground below. */
--color-terminal-header: #221F19;
--color-terminal-bg: #191713;
--color-terminal-border: #14120F;
--color-terminal-fg: #E4DCCA;
--color-terminal-cursor: #D07B52;
--color-terminal-selection: #4C4433;
--color-terminal-muted: #8B8069;
--color-terminal-accent: #84B383;
--color-terminal-danger: #E06B52;
--color-terminal-warning: #D3A34C;
/* Find-in-page highlights. These paint over arbitrary page content, not
over a brand-soft panel, so they get their own pair instead of borrowing
the brand chip tokens: an amber wash for matches (browser convention),
full-strength terracotta for the active one. */
--color-search-highlight: var(--cc-diff-mark);
--color-on-search-highlight: var(--cc-t1);
--color-search-highlight-active: var(--cc-ac);
--color-on-search-highlight-active: var(--cc-on-ac);
/* Misc */
--color-window-close-hover: #C74436;
--color-selection-bg: var(--cc-ac-border);
--color-selection-fg: var(--cc-t1);
--color-chat-navigation-target: rgba(var(--cc-ac-rgb), 0.10);
--color-text-secondary-a72: rgba(var(--cc-t2-rgb), 0.72);
--color-text-secondary-a68: rgba(var(--cc-t2-rgb), 0.68);
--color-text-primary-a88: rgba(var(--cc-t1-rgb), 0.88);
--color-text-primary-a82: rgba(var(--cc-t1-rgb), 0.82);
--color-text-primary-a78: rgba(var(--cc-t1-rgb), 0.78);
--color-surface-hover-a34: rgba(var(--cc-s1-rgb), 0.34);
--color-surface-hover-a54: rgba(var(--cc-s1-rgb), 0.54);
--color-outline-a72: rgba(var(--cc-bd2-rgb), 0.72);
--color-outline-a78: rgba(var(--cc-bd2-rgb), 0.78);
--color-outline-a92: rgba(var(--cc-bd2-rgb), 0.92);
/* Radii — mirrored from `@theme` so `var(--radius-*)` resolves at runtime */
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 13px;
--radius-xl: 17px;
--radius-2xl: 20px;
--radius-3xl: 24px;
--radius-full: 9999px;
}
/* Ink themes: lift the syntax ramp off the paper values. */
[data-theme="dark"],
[data-theme="ink-blue"] {
--color-code-comment: #8F8683;
--color-code-string: #A8CE8C;
--color-code-keyword: #E9926B;
--color-code-function: #7FC7E8;
--color-code-number: #86D0C4;
--color-code-property: #E8BBA1;
--color-code-type: #D9BE8A;
--color-code-parameter: var(--cc-t1);
--color-code-punctuation: #9B908C;
--color-diff-syntax-comment: #8F8683;
--color-diff-syntax-string: #A8CE8C;
--color-diff-syntax-number: #86D0C4;
--color-diff-syntax-regexp: #7FC7E8;
--color-diff-syntax-keyword: #E9926B;
--color-diff-syntax-variable: #E8BBA1;
--color-diff-syntax-parameter: #9B908C;
--color-diff-syntax-property: #E8BBA1;
--color-diff-syntax-function: #7FC7E8;
--color-diff-syntax-type: #D9BE8A;
--color-diff-syntax-punctuation: #9B908C;
--color-selection-fg: var(--cc-t1);
/* Ink themes run the background at the bottom of the ramp, so a lifted
surface has to climb it — `--cc-s1`, not the light themes' `--cc-bg`. */
--color-surface-dialog: var(--cc-s1);
--color-surface-glass: rgba(var(--cc-bg-rgb), 0.93);
--color-modal-scrim: rgba(var(--cc-scrim-rgb), 0.66);
--color-overlay-scrim: rgba(var(--cc-scrim-rgb), 0.56);
--shadow-button-primary: 0 2px 10px rgba(0, 0, 0, 0.4);
--color-activity-tooltip-border: rgba(var(--cc-bg-rgb), 0.28);
}
/* Ink-blue runs a cool terminal ground rather than the shared warm ink. */
[data-theme="ink-blue"] {
--color-terminal-header: #1B1F27;
--color-terminal-bg: #14171D;
--color-terminal-border: #101318;
--color-terminal-fg: #DDE1EB;
--color-terminal-selection: #434A5B;
--color-terminal-muted: #79808F;
}
/* ─── Material Symbols ─────────────────────────────────────────────
Inside @layer base ON PURPOSE. Unlayered CSS beats every @layer, and
Tailwind v4 puts its utilities in `@layer utilities` — so an unlayered
`font-size: 24px` here silently overrode every `text-[14px]` on every
hand-written icon span in the app, regardless of source order. In base,
the 24px default still applies when no size utility is present, and the
utilities win when one is. */
@layer base {
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
}
/* ─── Base styles ──────────────────────────────────────────────── */
* {
box-sizing: border-box;
}
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
font-family: var(--font-body);
color: var(--color-on-surface);
background-color: var(--color-background);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::selection {
background: var(--color-selection-bg);
color: var(--color-selection-fg);
}
/* Desktop drag region */
[data-desktop-drag-region] {
app-region: drag;
-webkit-app-region: drag;
}
button, input, textarea, select, a, [role="button"] {
app-region: no-drag;
-webkit-app-region: no-drag;
}
[draggable="true"] {
app-region: no-drag;
-webkit-app-region: no-drag;
}
.tab-bar-interactive,
.tab-bar-interactive * {
app-region: no-drag;
-webkit-app-region: no-drag;
}
/* Chrome's hairline between two neighbouring idle tabs — the thing that keeps
a row of untitled sessions from reading as one continuous smear.
It belongs to the *gap*, not to either tab, so it has to vanish the moment
either side of that gap gets a fill: a line running alongside a rounded
corner reads as a crack in it. Only sibling combinators can express "the
tab after the hovered one", which is why this is here and not in a Tailwind
class list. `::after` on purpose — `::before` is taken by the drag-over
drop indicator. */
.tab-strip-item::after {
content: '';
position: absolute;
left: -1px;
top: 50%;
width: 1px;
height: 16px;
transform: translateY(-50%);
background: var(--color-tab-separator);
pointer-events: none;
}
.tab-strip-item:first-child::after,
.tab-strip-item:hover::after,
.tab-strip-item[data-active='true']::after,
.tab-strip-item[data-dragging='true']::after,
.tab-strip-item:hover + .tab-strip-item::after,
.tab-strip-item[data-active='true'] + .tab-strip-item::after,
.tab-strip-item[data-dragging='true'] + .tab-strip-item::after {
content: none;
}
.glass-panel {
background: var(--color-surface-glass);
border: 1px solid var(--color-surface-glass-border);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
box-shadow: var(--shadow-overlay);
}
/* The fill above is translucent on the assumption that the blur will scramble
whatever shows through. Where `backdrop-filter` is unavailable that
assumption fails silently — no fallback fires, the blur is simply skipped,
and page text stays legible straight through every dropdown and popover.
Going opaque costs the glass look only on engines that never rendered it. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.glass-panel {
background: rgb(var(--cc-bg-rgb));
}
}
.glass-panel:focus-within {
box-shadow: var(--shadow-focus-ring), var(--shadow-dropdown);
}
/* Modals are read-and-fill surfaces, not glances: an 860px provider form that
scrolls needs the page behind it gone, not softened. `.glass-panel` couples
its fill to `backdrop-filter`, and a dialog this large cannot take the bet
that the blur is live — when it is not, the page reads through the form.
No `:focus-within` ring: a dialog holds focus essentially always, so the
glass variant's ring would burn permanently rather than signal anything. */
.dialog-panel {
background: var(--color-surface-dialog);
border: 1px solid var(--color-border);
box-shadow: var(--shadow-overlay);
}
/* The composer sits closer to the page than a floating overlay does, so it
takes the middle step of the shadow scale. Declared as a class rather than a
`shadow-[…]` utility because `.glass-panel` also sets `box-shadow`, and which
of the two wins would come down to stylesheet order. */
.glass-panel--composer,
.glass-panel--composer:focus-within {
box-shadow: var(--shadow-composer);
}
.glass-panel--composer:focus-within {
box-shadow: var(--shadow-focus-ring), var(--shadow-composer);
}
/* Radial wash behind the empty-state seal. */
.brand-seal-glow {
background: radial-gradient(460px 300px at 50% 44%, var(--color-brand-soft) 0%, transparent 72%);
}
.composer-drop-target-active {
border-color: var(--color-border-focus);
box-shadow: var(--shadow-focus-ring), var(--shadow-dropdown);
}
.composer-drop-overlay {
animation: composer-drop-fade 140ms ease-out, composer-drop-pulse 1.1s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.composer-drop-overlay {
animation: none;
}
}
.app-shell-viewport {
min-height: 100vh;
min-height: 100dvh;
height: 100vh;
height: 100dvh;
}
.app-shell {
position: relative;
width: 100%;
overflow-x: hidden;
}
.app-shell-backdrop {
background: var(--color-overlay-scrim);
}
.sidebar-shell {
position: relative;
width: var(--sidebar-width);
flex-shrink: 0;
overflow: hidden;
transition: width var(--motion-sidebar-duration) var(--motion-sidebar-easing);
will-change: width;
}
.sidebar-shell[data-state="closed"] {
width: var(--sidebar-rail-width);
}
/* While the pointer is driving the width, the animation must be off or every
frame chases the previous one and the edge lags behind the cursor. */
.sidebar-shell--resizing,
.sidebar-shell--resizing .sidebar-panel {
transition: none;
}
.sidebar-resize-handle {
position: absolute;
top: 0;
bottom: 0;
right: 0;
width: 8px;
z-index: 10;
cursor: col-resize;
background: transparent;
transition: background-color 140ms ease;
touch-action: none;
}
.sidebar-resize-handle::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
right: 0;
width: 2px;
background: transparent;
transition: background-color 140ms ease;
}
.sidebar-resize-handle:hover::after,
.sidebar-resize-handle:focus-visible::after,
.sidebar-shell--resizing .sidebar-resize-handle::after {
background: var(--color-brand);
}
.sidebar-resize-handle:focus-visible {
outline: none;
}
/* Keep the resize cursor and kill text selection for the whole drag, no matter
what the pointer ends up over once capture takes it outside the handle. */
body.sidebar-resizing {
cursor: col-resize;
user-select: none;
}
body.sidebar-resizing * {
cursor: col-resize !important;
}
.app-shell--mobile .sidebar-shell {
position: fixed;
inset: 0 auto 0 0;
z-index: 50;
width: min(88vw, var(--sidebar-width));
max-width: var(--sidebar-width);
overflow: visible;
transform: translateX(-100%);
transition:
transform var(--motion-sidebar-duration) var(--motion-sidebar-easing),
width var(--motion-sidebar-duration) var(--motion-sidebar-easing);
}
.app-shell--mobile .sidebar-shell[data-state="closed"] {
pointer-events: none;
}
.app-shell--mobile .sidebar-shell[data-state="open"] {
transform: translateX(0);
pointer-events: auto;
}
.sidebar-panel {
width: var(--sidebar-width);
min-width: var(--sidebar-width);
background-image: var(--sidebar-panel-bg-image);
background-repeat: no-repeat;
transition:
background-color var(--motion-sidebar-duration) var(--motion-sidebar-easing),
border-color var(--motion-sidebar-duration) var(--motion-sidebar-easing);
}
.sidebar-panel[data-state="closed"] {
width: var(--sidebar-rail-width);
min-width: var(--sidebar-rail-width);
}
.app-shell--mobile .sidebar-panel,
.app-shell--mobile .sidebar-panel[data-state="closed"] {
width: min(88vw, var(--sidebar-width));
min-width: min(88vw, var(--sidebar-width));
box-shadow: var(--shadow-dropdown);
}
.sidebar-toggle-button {
color: var(--color-text-secondary-a72);
background: transparent;
transition:
color 180ms ease,
background-color 180ms ease,
transform 180ms ease;
border: 0;
/* No `box-shadow: none` here: Tailwind's ring utilities ARE box-shadows,
and this unlayered declaration silently killed every `focus-visible:ring-*`
a caller put on the button — same failure class as the icon font-size. */
}
.sidebar-toggle-button:hover {
color: var(--color-text-primary-a88);
background: var(--color-surface-hover-a34);
}
.sidebar-toggle-button:active {
transform: translateY(0.5px);
background: var(--color-surface-hover-a54);
}
.sidebar-toggle-button--open {
color: var(--color-text-secondary-a68);
}
.sidebar-toggle-button--open:hover {
color: var(--color-text-primary-a82);
}
.sidebar-toggle-button--collapsed {
color: var(--color-text-primary-a78);
}
.sidebar-toggle-button--collapsed:hover {
color: var(--color-text-primary);
}
.sidebar-toggle-button--collapsed:active {
}
.sidebar-toggle-icon {
overflow: visible;
}
.sidebar-toggle-chevron {
stroke: currentColor;
transition:
opacity 180ms ease,
transform 180ms ease;
vector-effect: non-scaling-stroke;
}
.sidebar-toggle-chevron {
opacity: 0.95;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 1.85;
transform-origin: 50% 50%;
}
.sidebar-toggle-icon--open .sidebar-toggle-chevron {
transform: translateX(-0.2px);
}
.sidebar-toggle-button--collapsed .sidebar-toggle-chevron {
stroke-width: 2;
transform: translateX(0.4px);
}
.sidebar-copy {
overflow: hidden;
white-space: nowrap;
transition:
max-width var(--motion-sidebar-duration) var(--motion-sidebar-easing),
opacity calc(var(--motion-sidebar-duration) - 40ms) ease,
transform var(--motion-sidebar-duration) var(--motion-sidebar-easing);
}
.sidebar-copy--visible {
max-width: 240px;
opacity: 1;
transform: translateX(0);
}
.sidebar-copy--hidden {
max-width: 0;
opacity: 0;
transform: translateX(-10px);
}
.sidebar-section {
overflow: hidden;
transition:
max-height var(--motion-sidebar-duration) var(--motion-sidebar-easing),
opacity calc(var(--motion-sidebar-duration) - 40ms) ease,
transform var(--motion-sidebar-duration) var(--motion-sidebar-easing);
}
.sidebar-section--visible {
max-height: 1200px;
opacity: 1;
transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
.sidebar-shell,
.sidebar-panel,
.sidebar-copy,
.sidebar-section {
transition-duration: 0.01ms !important;
}
}
@media (max-width: 767px) {
.app-shell-main--mobile {
width: 100%;
min-width: 0;
overflow-x: hidden;
}
}
.markdown-prose .md-table-wrap table {
border-collapse: separate;
border-spacing: 0;
}
.markdown-prose {
--tw-prose-body: var(--color-text-primary);
--tw-prose-headings: var(--color-text-primary);
--tw-prose-lead: var(--color-text-secondary);
--tw-prose-links: var(--color-text-accent);
--tw-prose-bold: var(--color-text-primary);
--tw-prose-counters: var(--color-text-tertiary);
--tw-prose-bullets: var(--color-text-tertiary);
--tw-prose-hr: var(--color-border);
--tw-prose-quotes: var(--color-text-primary);
--tw-prose-quote-borders: var(--color-outline-variant);
--tw-prose-captions: var(--color-text-secondary);
--tw-prose-kbd: var(--color-text-secondary);
--tw-prose-code: var(--color-code-fg);
--tw-prose-pre-code: var(--color-code-fg);
--tw-prose-pre-bg: var(--color-code-bg);
--tw-prose-th-borders: var(--color-border);
--tw-prose-td-borders: var(--color-border);
}
.markdown-prose .md-table-wrap tbody tr:last-child td {
border-bottom: 0;
}
.markdown-prose a code {
color: inherit;
}
/* Scrollbar */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--color-outline); border-radius: 9999px; opacity: 0.3; }
::-webkit-scrollbar-thumb:hover { opacity: 0.6; }
.sidebar-scroll-area {
padding-bottom: 72px;
scrollbar-width: thin;
scrollbar-color: var(--color-outline-a72) transparent;
}
.sidebar-settings-dock {
z-index: 30;
background-color: var(--color-surface-sidebar);
background-image: var(--sidebar-panel-bg-image);
background-repeat: no-repeat;
background-position: left bottom;
}
.sidebar-scroll-area::-webkit-scrollbar {
width: 4px;
}
.sidebar-scroll-area::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-scroll-area::-webkit-scrollbar-thumb {
background: var(--color-outline-a78);
border-radius: 9999px;
}
.sidebar-scroll-area::-webkit-scrollbar-thumb:hover {
background: var(--color-outline-a92);
}
.activity-summary-panel {
container-type: inline-size;
}
.activity-summary-grid {
grid-template-columns: minmax(0, 1fr);
}
.activity-summary-metric-primary {
grid-column: auto;
}
.activity-summary-value {
overflow-wrap: normal;
word-break: keep-all;
}
@container (min-width: 360px) {
.activity-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@container (min-width: 560px) {
.activity-summary-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.activity-summary-metric-primary {
grid-column: auto;
}
}
.activity-heat-cell {
position: relative;
transform: translateZ(0);
transform-origin: center;
transition:
border-color 150ms ease,
box-shadow 180ms ease,
filter 180ms ease,
transform 180ms cubic-bezier(0.22, 1, 0.36, 1);
}
.activity-heat-cell:hover,
.activity-heat-cell:focus-visible,
.activity-heat-cell.is-active {
z-index: 1;
transform: translateZ(0) scale(1.16);
box-shadow: var(--shadow-activity-cell-hover);
filter: saturate(1.08);
}
@media (prefers-reduced-motion: reduce) {
.activity-heat-cell {
transition-duration: 0.01ms !important;
}
.activity-heat-cell:hover,
.activity-heat-cell:focus-visible,
.activity-heat-cell.is-active {
transform: translateZ(0);
}
}
/* Animations */
@keyframes shimmer {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
@keyframes composer-drop-fade {
from { opacity: 0; transform: scale(0.985); }
to { opacity: 1; transform: scale(1); }
}
@keyframes composer-drop-pulse {
0%, 100% { opacity: 0.96; }
50% { opacity: 1; }
}
/* Screen entrance from the handoff: 8px rise + 98.5% scale, fading in. */
@keyframes screen-pop {
from { opacity: 0; transform: translateY(8px) scale(0.985); }
to { opacity: 1; transform: none; }
}
.animate-shimmer { animation: shimmer 1.5s ease-in-out infinite; }
.animate-spin { animation: spin 0.9s linear infinite; }
.animate-pulse-dot { animation: pulse-dot 1.6s ease-in-out infinite; }
.animate-screen-pop { animation: screen-pop 250ms cubic-bezier(0.2, 0.8, 0.2, 1); }
@media (prefers-reduced-motion: reduce) {
.animate-screen-pop { animation: overlay-fade-in 1ms linear; }
}
@media (prefers-reduced-motion: reduce) {
/* Slow the spinner rather than stopping it. A frozen spinner reads as a
hung UI, which is a worse outcome than the motion it avoids — the
rotation is the whole signal the component carries. */
.animate-spin { animation-duration: 2.4s; }
.animate-shimmer,
.animate-pulse-dot { animation-duration: 3s; }
}
/* Overlay entrances.
These replace the `animate-in slide-in-from-*` classes that came from
`tailwindcss-animate`; that dependency was removed with the shadcn
rollback, so those classes silently did nothing. Keep them here so the
set stays greppable and paired with its keyframes. */
@keyframes overlay-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes overlay-in-from-top {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes overlay-in-from-bottom {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes overlay-in-from-right {
from { opacity: 0; transform: translateX(12px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-overlay-in { animation: overlay-fade-in 150ms ease-out; }
.animate-overlay-in-top { animation: overlay-in-from-top 150ms ease-out; }
.animate-overlay-in-bottom { animation: overlay-in-from-bottom 150ms ease-out; }
.animate-overlay-in-right { animation: overlay-in-from-right 200ms cubic-bezier(0.22, 1, 0.36, 1); }
@media (prefers-reduced-motion: reduce) {
.animate-overlay-in,
.animate-overlay-in-top,
.animate-overlay-in-bottom,
.animate-overlay-in-right {
animation: overlay-fade-in 1ms linear;
}
}
/* Indeterminate (browser-style) loading bar: a segment that slides across the track */
@keyframes progress-indeterminate {
0% { left: -40%; right: 100%; }
60% { left: 100%; right: -40%; }
100% { left: 100%; right: -40%; }
}
.progress-indeterminate-track {
position: relative;
overflow: hidden;
}
.progress-indeterminate-track::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -40%;
right: 100%;
background: var(--color-primary);
animation: progress-indeterminate 1.1s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.progress-indeterminate-track::before {
animation: none;
left: 0;
right: 0;
opacity: 0.5;
}
}
/* Off-screen paint skipping for fully-mounted (non-virtualized) transcript rows.
WebKit (Tauri WKWebView) paints complex message DOM ~2x slower than Blink and
stutters on long worst-case frames; content-visibility lets it skip off-screen
rows WITHOUT unmounting or re-measuring them. Measured (Playwright WebKit, 79-msg
session): worst frame 279ms -> 18ms, jank 2 -> 0. Only non-virtualized rows get
this class — virtualized window items must stay free of content-visibility to
avoid the zero-height measurement regression in the virtualizer. */
.chat-render-item--cv {
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}
@keyframes chat-navigation-target-pulse {
0% { background: transparent; }
22% { background: var(--color-chat-navigation-target); }
100% { background: transparent; }
}
.chat-render-item--navigation-target {
border-radius: var(--radius-lg);
animation: chat-navigation-target-pulse 1.4s ease-out;
}
@media (prefers-reduced-motion: reduce) {
.chat-render-item--navigation-target {
animation: none;
background: var(--color-chat-navigation-target);
}
}
/* Same WebKit paint-skipping trick for trace tree rows (see .chat-render-item--cv
above). Tree rows are fixed-density 34px single-line items, so the intrinsic
size estimate is exact and scrolling never drifts. */
.trace-row-cv {
content-visibility: auto;
contain-intrinsic-size: auto 34px;
}
/* Trace-window variants of the same trick (kept as classes — not inline styles —
so the touch-H5 override below can reach them). */
.trace-message-cv {
content-visibility: auto;
contain-intrinsic-size: auto 120px;
}
.trace-list-row-cv {
content-visibility: auto;
contain-intrinsic-size: auto 56px;
}
/* ============================================================================
Touch-H5 (phone browser) scoped fixes.
`html[data-touch-h5]` is set before first paint by src/lib/touchH5.ts only
when the bundle runs in a browser (no Electron host) on a coarse-pointer
device — WeChat scan-to-open, mobile Safari, etc. Desktop shells and
desktop browsers never match, so nothing here can regress desktop chat.
========================================================================== */
/* iOS WKWebView zooms the whole page (and never zooms back) when a focused
form control renders below 16px. 16px controls remove the trigger at the
root; the JS side additionally caps the viewport scale on iOS. Android has
no focus-zoom but 16px inputs are the native size there too. */
html[data-touch-h5] input,
html[data-touch-h5] textarea,
html[data-touch-h5] select {
font-size: 16px;
}
/* Long-press text selection on iOS WebKit fights content-visibility: auto —
extending a selection into a skipped row forces synchronous layout, which
makes the selection handles jump or drops the selection entirely. The paint
savings were measured for the desktop WKWebView's 860px canvas; a phone
viewport paints far less per row, so correct selection wins here. */
html[data-touch-h5] .chat-render-item--cv,
html[data-touch-h5] .trace-row-cv,
html[data-touch-h5] .trace-message-cv,
html[data-touch-h5] .trace-list-row-cv {
content-visibility: visible;
}
/* Size the shell to the *visual* viewport so the composer rides the soft
keyboard instead of the keyboard covering it (variable published by
touchH5.ts; falls back to 100dvh before the first sync). The safe-area
padding pairs with viewport-fit=cover on iOS: keep the composer above the
home indicator and the header out of the notch, but drop the bottom inset
while the keyboard is up — the keyboard covers that strip anyway. */
html[data-touch-h5] .app-shell-viewport {
min-height: var(--touch-h5-viewport-height, 100dvh);
height: var(--touch-h5-viewport-height, 100dvh);
padding-top: env(safe-area-inset-top, 0px);
padding-bottom: env(safe-area-inset-bottom, 0px);
padding-left: env(safe-area-inset-left, 0px);
padding-right: env(safe-area-inset-right, 0px);
}
html[data-touch-h5][data-touch-h5-keyboard] .app-shell-viewport {
padding-bottom: 0px;
}
/* Message action bars (copy / branch) are hover-revealed on desktop. Keep
them visible whenever the browser is in the narrow H5 shell, even if an
embedded browser reports no coarse pointer, and retain the touch marker as
coverage for wider tablets. */
.app-shell--mobile [data-message-actions],
html[data-touch-h5] [data-message-actions] {
opacity: 1;
pointer-events: auto;
}
.app-shell--mobile [data-message-actions] {
height: 2.75rem;
}
.app-shell--mobile [data-message-actions] > div,
.app-shell--mobile [data-message-actions] button {
min-height: 2.75rem;
}
.app-shell--mobile [data-message-actions] button {
width: 2.75rem;
}
/* Kill the gray flash WKWebView paints over any tapped interactive element;
components carry their own pressed/hover styling. */
html[data-touch-h5],
html[data-touch-h5] * {
-webkit-tap-highlight-color: transparent;
}
/* The app shell is a fixed 100dvh layout with inner scroll areas; the body
itself never scrolls. Stop edge overscroll from rubber-banding the whole
shell (and WeChat from revealing its pull-down domain banner mid-chat). */
html[data-touch-h5] body {
overscroll-behavior-y: none;
}
/* ─── Companion pet window ───────────────────────────────────── */
html[data-window-kind='pet'],
html[data-window-kind='pet'] body,
html[data-window-kind='pet'] #root {
background: transparent !important;
overflow: hidden;
}
.pet-window-root {
position: relative;
box-sizing: border-box;
width: 100%;
height: 100vh;
background: transparent;
pointer-events: none;
user-select: none;
}
.pet-window-stack {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 100%;
height: 100%;
box-sizing: border-box;
padding: 0 12px 12px;
pointer-events: none;
}
/* Dragging clamps against the mascot alone so it can reach every display edge,
which by construction pushes the rest of the window off-screen. At the top
edge that is the menu bar eating the activity panel, so the host asks for the
panel below the mascot instead and the whole stack flips. */
.pet-window-stack[data-panel-placement='below'] {
justify-content: flex-start;
padding: 12px 12px 0;
}
.pet-mascot-button {
position: relative;
z-index: 10;
display: grid;
flex: 0 0 auto;
place-items: center;
margin-bottom: -2px;
border: 0;
padding: 0;
background: transparent;
cursor: grab;
transform: scale(1);
transform-origin: 50% 92%;
transition: transform 160ms ease-out;
-webkit-app-region: no-drag;
pointer-events: auto;
touch-action: none;
will-change: transform;
}
.pet-mascot-wrap {
position: relative;
order: 2;
display: grid;
flex: 0 0 auto;
place-items: center;
pointer-events: none;
}
.pet-window-stack[data-panel-placement='below'] .pet-mascot-wrap {
order: 1;
}
.pet-task-badge {
position: absolute;
z-index: 12;
top: 8px;
right: 2px;
display: grid;
min-width: 24px;
height: 24px;
place-items: center;
border: 1px solid rgba(255, 255, 255, 0.72);
border-radius: 999px;
padding: 0 6px;
background: var(--color-brand);
box-shadow: 0 5px 14px rgba(13, 31, 48, 0.26);
color: white;
cursor: pointer;
font: 650 12px/1 var(--font-body);
pointer-events: auto;
-webkit-app-region: no-drag;
}
.pet-task-badge:hover {
background: var(--color-brand-hover);
transform: translateY(-1px);
}
.pet-task-badge:focus-visible {
outline: 2px solid var(--color-border-focus);
outline-offset: 2px;
}
.pet-mascot-button[data-dragging='true'] {
cursor: grabbing;
transform: scale(0.96);
}
.pet-mascot-button:focus-visible {
outline: 3px solid var(--color-border-focus);
outline-offset: 3px;
border-radius: 999px;
}
.pet-mascot-button::before {
content: '';
position: absolute;
inset: 18% 8% 8%;
border-radius: 50%;
background:
radial-gradient(circle at 35% 45%, rgba(112, 143, 255, 0.22), transparent 58%),
radial-gradient(circle at 68% 62%, rgba(245, 151, 193, 0.16), transparent 62%);
filter: blur(16px);
pointer-events: none;
}
.pet-sprite-stage {
position: relative;
z-index: 1;
isolation: isolate;
overflow: visible;
transform: scale(1.04);
transform-origin: 50% 88%;
}
.pet-sprite-stage--atlas {
transform: scale(1.1);
}
.pet-sprite {
position: relative;
z-index: 1;
transform-origin: 50% 88%;
}
.pet-sprite-stage--atlas .pet-sprite {
filter: drop-shadow(0 8px 12px rgba(35, 27, 54, 0.14));
}
.pet-sprite[data-pet-motion='enabled'] {
will-change: transform;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='idle'] {
animation: pet-motion-idle 3.3s ease-in-out infinite;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='running'] {
animation: pet-motion-work 820ms linear infinite;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='running-right'] {
animation: pet-motion-travel-right 1060ms linear infinite;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='running-left'] {
animation: pet-motion-travel-left 1060ms linear infinite;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='waving'] {
animation: pet-motion-wave 700ms linear infinite;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='jumping'] {
animation: pet-motion-jump 840ms linear infinite;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='failed'] {
animation: pet-motion-fail 1220ms linear infinite;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='waiting'] {
animation: pet-motion-wait 1010ms linear infinite;
}
.pet-sprite-stage--single .pet-sprite[data-pet-motion='enabled'][data-pet-motion-state='review'] {
animation: pet-motion-review 1030ms linear infinite;
}
.pet-activity-card {
position: relative;
z-index: 15;
box-sizing: border-box;
order: 1;
width: calc(100% - 32px);
max-width: 360px;
flex: 0 0 auto;
overflow: visible;
/* Keep the card, collapse control, and visible mascot as one compact stack. */
margin-bottom: 12px;
border: 1px solid rgba(255, 255, 255, 0.76);
border-radius: 24px;
padding: 6px 7px;
background: rgba(255, 255, 255, 0.8);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.68),
0 12px 30px rgba(30, 25, 44, 0.14);
backdrop-filter: blur(20px) saturate(1.18);
-webkit-backdrop-filter: blur(20px) saturate(1.18);
transition: border-radius 180ms ease, padding 180ms ease;
pointer-events: auto;
}
.pet-activity-card[data-expanded='true'] {
border-radius: 22px;
padding-right: 7px;
}
.pet-window-stack[data-panel-placement='below'] .pet-activity-card {
order: 2;
margin-top: 12px;
margin-bottom: 0;
}
html[data-theme='dark'] .pet-activity-card,
html[data-theme='ink-blue'] .pet-activity-card {
border-color: rgba(255, 255, 255, 0.12);
background: rgba(24, 23, 30, 0.78);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 12px 30px rgba(0, 0, 0, 0.24);
}
.pet-session-list {
display: flex;
flex-direction: column;
max-height: 52px;
overflow-x: hidden;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: none;
transition: max-height 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.pet-session-list[data-expanded='true'] {
max-height: 138px;
}
.pet-session-list::-webkit-scrollbar {
display: none;
}
.pet-session-list > [role='listitem'] + [role='listitem'] {
border-top: 1px solid rgba(72, 67, 82, 0.08);
}
.pet-session-list > [role='listitem'] {
transition: opacity 150ms ease, transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.pet-session-list[data-expanded='false'] > [role='listitem']:nth-child(n + 2) {
opacity: 0;
pointer-events: none;
transform: translateY(-4px);
}
html[data-theme='dark'] .pet-session-list > [role='listitem'] + [role='listitem'],
html[data-theme='ink-blue'] .pet-session-list > [role='listitem'] + [role='listitem'] {
border-top-color: rgba(255, 255, 255, 0.07);
}
.pet-session-row {
display: flex;
width: 100%;
min-height: 52px;
align-items: center;
gap: 12px;
border: 0;
border-radius: 17px;
padding: 7px 10px;
background: transparent;
color: #25222b;
cursor: pointer;
text-align: left;
transition: background 140ms ease, transform 140ms ease;
-webkit-app-region: no-drag;
}
.pet-session-row:hover {
background: rgba(255, 255, 255, 0.58);
}
.pet-session-indicator {
width: 9px;
height: 9px;
flex: 0 0 auto;
border-radius: 999px;
background: rgba(117, 113, 126, 0.52);
}
.pet-session-indicator.is-waiting {
background: #f1ad3b;
}
.pet-session-indicator.is-failed {
background: #ee5a73;
}
.pet-session-indicator.is-review {
background: #46bf8c;
}
.pet-session-indicator.is-running {
width: 12px;
height: 12px;
border: 2px solid var(--color-primary-fixed-dim);
border-top-color: var(--color-brand);
background: transparent;
animation: pet-status-spin 1s linear infinite;
}
.pet-session-row:active {
transform: scale(0.985);
}
.pet-session-row:focus-visible,
.pet-panel-toggle:focus-visible {
outline: 2px solid var(--color-border-focus);
outline-offset: -2px;
}
html[data-theme='dark'] .pet-session-row,
html[data-theme='ink-blue'] .pet-session-row {
color: rgba(255, 255, 255, 0.94);
}
html[data-theme='dark'] .pet-session-row:hover,
html[data-theme='ink-blue'] .pet-session-row:hover {
background: rgba(255, 255, 255, 0.08);
}
.pet-session-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
gap: 2px;
}
.pet-session-title,
.pet-session-status {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pet-session-title {
font: 650 13px/1.25 var(--font-body);
}
.pet-session-status {
color: rgba(75, 70, 85, 0.66);
font: 500 11px/1.25 var(--font-body);
}
html[data-theme='dark'] .pet-session-status,
html[data-theme='ink-blue'] .pet-session-status {
color: rgba(236, 232, 243, 0.62);
}
.pet-panel-toggle {
position: absolute;
z-index: 13;
right: 50%;
bottom: -31px;
display: grid;
width: 25px;
height: 25px;
place-items: center;
border-radius: 999px;
border: 1px solid rgba(255, 255, 255, 0.7);
background: rgba(52, 55, 61, 0.72);
color: rgba(255, 255, 255, 0.88);
cursor: pointer;
transform: translateX(50%);
transition: background 140ms ease, color 140ms ease, transform 140ms ease;
-webkit-app-region: no-drag;
}
.pet-activity-card[data-expanded='true'] .pet-panel-toggle {
top: auto;
transform: translateX(50%);
}
/* Flipped, the control stays on the mascot-facing side of the card so the card,
the control, and the mascot remain one compact stack. */
.pet-window-stack[data-panel-placement='below'] .pet-activity-card .pet-panel-toggle {
top: -31px;
bottom: auto;
}
.pet-panel-toggle:hover {
background: rgba(43, 47, 54, 0.88);
color: white;
}
.pet-panel-toggle:active {
transform: translateX(50%) scale(0.94);
}
.pet-activity-card[data-expanded='true'] .pet-panel-toggle:active {
transform: translateX(50%) scale(0.94);
}
html[data-theme='dark'] .pet-panel-toggle,
html[data-theme='ink-blue'] .pet-panel-toggle {
border-color: rgba(255, 255, 255, 0.18);
background: rgba(36, 39, 45, 0.82);
color: rgba(239, 235, 246, 0.62);
}
html[data-theme='dark'] .pet-panel-toggle:hover,
html[data-theme='ink-blue'] .pet-panel-toggle:hover {
background: rgba(255, 255, 255, 0.09);
color: white;
}
.pet-error-card {
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
color: rgba(70, 65, 79, 0.68);
font-size: 12px;
}
.pet-error-card {
position: absolute;
left: 22px;
bottom: 14px;
width: 340px;
border: 1px solid rgba(234, 82, 104, 0.24);
border-radius: 18px;
padding: 18px;
background: rgba(255, 244, 246, 0.9);
color: #a83f54;
pointer-events: auto;
}
@keyframes pet-motion-idle {
0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
18% { transform: translate3d(0, 0.3%, 0) rotate(-0.15deg) scale(1.004, 0.997); }
48% { transform: translate3d(0, -1.1%, 0) rotate(0.2deg) scale(0.997, 1.006); }
72% { transform: translate3d(0, -0.25%, 0) rotate(0deg) scale(1.002, 0.999); }
}
@keyframes pet-motion-work {
0%, 100% {
transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
9% {
transform: translate3d(-0.35%, 0.8%, 0) rotate(-0.5deg) scale(1.01, 0.989);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
29% {
transform: translate3d(0.8%, -1.7%, 0) rotate(0.65deg) scale(0.992, 1.012);
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}
54% {
transform: translate3d(-0.6%, -0.35%, 0) rotate(-0.45deg) scale(1.002, 0.999);
animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}
72% {
transform: translate3d(0.25%, 0.65%, 0) rotate(0.2deg) scale(1.008, 0.992);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
88% { transform: translate3d(0, -0.25%, 0) rotate(-0.1deg) scale(0.998, 1.003); }
}
@keyframes pet-motion-travel-right {
0%, 100% {
transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
8% {
transform: translate3d(-0.7%, 0.75%, 0) rotate(-1deg) scale(1.012, 0.987);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
25% {
transform: translate3d(1.2%, -2.1%, 0) rotate(1.25deg) scale(0.989, 1.016);
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}
50% {
transform: translate3d(0.35%, -0.45%, 0) rotate(0.55deg) scale(1.002, 0.999);
animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}
69% {
transform: translate3d(-0.25%, 0.7%, 0) rotate(-0.35deg) scale(1.01, 0.99);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
86% { transform: translate3d(0.2%, -0.35%, 0) rotate(0.2deg) scale(0.998, 1.003); }
}
@keyframes pet-motion-travel-left {
0%, 100% {
transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
8% {
transform: translate3d(0.7%, 0.75%, 0) rotate(1deg) scale(1.012, 0.987);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
25% {
transform: translate3d(-1.2%, -2.1%, 0) rotate(-1.25deg) scale(0.989, 1.016);
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}
50% {
transform: translate3d(-0.35%, -0.45%, 0) rotate(-0.55deg) scale(1.002, 0.999);
animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}
69% {
transform: translate3d(0.25%, 0.7%, 0) rotate(0.35deg) scale(1.01, 0.99);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
86% { transform: translate3d(-0.2%, -0.35%, 0) rotate(-0.2deg) scale(0.998, 1.003); }
}
@keyframes pet-motion-wave {
0%, 100% {
transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
11% {
transform: translate3d(0.4%, 0.45%, 0) rotate(0.7deg) scale(1.006, 0.994);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
34% {
transform: translate3d(-0.7%, -1.45%, 0) rotate(-1.35deg) scale(0.996, 1.007);
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}
61% {
transform: translate3d(0.55%, -0.6%, 0) rotate(0.95deg) scale(1.001);
animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}
82% { transform: translate3d(-0.15%, 0.2%, 0) rotate(-0.3deg) scale(1.002, 0.998); }
}
@keyframes pet-motion-jump {
0%, 100% {
transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
11% {
transform: translate3d(0, 1.6%, 0) rotate(-0.35deg) scale(1.035, 0.956);
animation-timing-function: cubic-bezier(0.12, 0.8, 0.25, 1);
}
29% {
transform: translate3d(0.45%, -5.8%, 0) rotate(1deg) scale(0.974, 1.035);
animation-timing-function: cubic-bezier(0.4, 0, 0.65, 1);
}
51% {
transform: translate3d(0, -7.5%, 0) rotate(0.3deg) scale(0.988, 1.015);
animation-timing-function: cubic-bezier(0.3, 0, 0.75, 0.55);
}
73% {
transform: translate3d(-0.25%, -2%, 0) rotate(-0.6deg) scale(1.005, 0.995);
animation-timing-function: cubic-bezier(0.1, 0.75, 0.25, 1);
}
82% {
transform: translate3d(0, 1.15%, 0) rotate(0.2deg) scale(1.04, 0.96);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
92% { transform: translate3d(0, -0.45%, 0) rotate(-0.1deg) scale(0.992, 1.008); }
}
@keyframes pet-motion-fail {
0%, 100% {
transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
12% {
transform: translate3d(0.5%, -0.8%, 0) rotate(0.8deg) scale(0.994, 1.007);
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}
33% {
transform: translate3d(-0.7%, 1.1%, 0) rotate(-1.5deg) scale(1.018, 0.978);
animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}
58% { transform: translate3d(-0.3%, 0.75%, 0) rotate(-0.75deg) scale(1.009, 0.991); }
78% { transform: translate3d(0.15%, -0.25%, 0) rotate(0.25deg) scale(0.997, 1.004); }
}
@keyframes pet-motion-wait {
0%, 100% { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); }
21% { transform: translate3d(-0.4%, -0.65%, 0) rotate(-0.35deg) scale(0.998, 1.004); }
49% { transform: translate3d(0.45%, -1.25%, 0) rotate(0.45deg) scale(0.995, 1.007); }
76% { transform: translate3d(-0.1%, 0.2%, 0) rotate(-0.1deg) scale(1.004, 0.997); }
}
@keyframes pet-motion-review {
0%, 100% {
transform: translate3d(0, 0, 0) rotate(0deg) scale(1);
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
}
10% {
transform: translate3d(0, 0.55%, 0) rotate(-0.25deg) scale(1.008, 0.992);
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
31% {
transform: translate3d(-0.45%, -1.35%, 0) rotate(-0.55deg) scale(0.994, 1.009);
animation-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}
57% {
transform: translate3d(0.55%, -0.55%, 0) rotate(0.5deg) scale(1.002, 0.999);
animation-timing-function: cubic-bezier(0.33, 1, 0.68, 1);
}
80% { transform: translate3d(-0.15%, 0.3%, 0) rotate(-0.15deg) scale(1.005, 0.995); }
}
@keyframes pet-status-spin {
to { transform: rotate(360deg); }
}
@media (prefers-reduced-motion: reduce) {
.pet-mascot-button,
.pet-activity-card,
.pet-session-row,
.pet-panel-toggle,
.pet-sprite,
.pet-session-indicator.is-running {
animation: none !important;
transition: none !important;
}
}