ManimCat / frontend /src /index.css
Bin29's picture
Sync from main: 531f94c feat(ui): animate return home and connection guide exit
68722e4
Raw History Blame Contribute Delete
29.9 kB
@import url('https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-webfont@1.1.0/style.css');
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 亮色主题变量 */
:root {
--bg-primary-rgb: 245, 245, 245;
--bg-secondary-rgb: 238, 238, 238;
--bg-tertiary-rgb: 224, 224, 224;
/* 舞台 = 工作室自己的场,和页面同色:等待态不摆一块面板,墨直接画在屋里,也就没有"边"可看。
屏只在真的有东西上屏时才出现。暗色主题是例外(见 .dark)——那里需要一块深色的屏。 */
--stage-rgb: 245, 245, 245;
--text-primary-rgb: 33, 33, 33;
--text-secondary-rgb: 117, 117, 117;
--text-tertiary-rgb: 158, 158, 158;
--accent-rgb: 66, 66, 66;
--accent-hover-rgb: 48, 48, 48;
--border-rgb: 0, 0, 0;
/* 动效曲线(HIG 分工):不是一条通吃。
ui 微交互(hover / 变色 / 焦点)——克制,几乎察觉不到
enter 进场浮现(弹层 / 气泡 / 媒体上屏)——减速落定
exit 退场——加速离开,不拖尾巴
move 位移 / 尺寸 / 翻页(侧栏宽度、舞台形状)——长距离柔和收尾
soft 透明度 / 颜色交叉溶解(深底溶解、占位浮现)——慢起手 + 长收尾
spring 回弹强调(猫) */
--ease-ui: cubic-bezier(0.25, 0.1, 0.25, 1);
--ease-enter: cubic-bezier(0.16, 1, 0.3, 1);
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
--ease-move: cubic-bezier(0.22, 1, 0.36, 1);
/* 推卡:两张同向移动、两端都在静止 → 对称 S 曲线(UIKit curveEaseInOut) */
--ease-swap: cubic-bezier(0.42, 0, 0.58, 1);
--ease-soft: cubic-bezier(0.45, 0, 0.25, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* iOS 风格主题变量 */
.warm {
--bg-primary-rgb: 242, 242, 247;
--bg-secondary-rgb: 255, 253, 248;
--bg-tertiary-rgb: 209, 209, 214;
--stage-rgb: 242, 242, 247;
--text-primary-rgb: 0, 0, 0;
--text-secondary-rgb: 60, 60, 67;
--text-tertiary-rgb: 142, 142, 147;
--accent-rgb: 120, 180, 240;
--accent-hover-rgb: 100, 160, 220;
--border-rgb: 198, 198, 200;
}
/* 暗黑主题变量 */
.dark {
--bg-primary-rgb: 18, 18, 18;
--bg-secondary-rgb: 28, 28, 28;
--bg-tertiary-rgb: 38, 38, 38;
--stage-rgb: 16, 20, 21;
--text-primary-rgb: 245, 245, 245;
--text-secondary-rgb: 189, 189, 189;
--text-tertiary-rgb: 117, 117, 117;
--accent-rgb: 138, 138, 138;
--accent-hover-rgb: 158, 158, 158;
--border-rgb: 255, 255, 255;
}
body {
font-family: "LXGW WenKai Screen", "LXGW WenKai TC", "LXGW WenKai Lite", "LXGW WenKai", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@apply bg-bg-primary text-text-primary transition-colors duration-300;
}
/* 自定义下拉菜单样式 */
select option {
padding: 10px 16px;
background-color: #ffffff;
color: #212121;
}
select option:hover,
select option:focus {
background-color: #f5f5f5;
}
select option:checked {
background-color: #424242;
color: #ffffff;
font-weight: 500;
}
/* 极简滚动条 - MD3 风格 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 9999px;
}
.dark ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.2);
}
.dark ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
/* 代码高亮主题适配暗黑模式 */
.dark .react-syntax-highlighter {
background: transparent !important;
}
/* 动画淡入效果 */
@keyframes fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fade-in 0.3s ease-out;
}
@keyframes fade-in-soft {
from { opacity: 0; transform: translateY(12px) scale(0.992); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.animate-fade-in-soft {
animation: fade-in-soft 0.58s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes message-enter {
from { opacity: 0.01; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-message-enter {
animation: message-enter 0.22s cubic-bezier(0.22, 1, 0.36, 1) forwards;
will-change: transform, opacity;
}
@keyframes fade-out-soft {
from { opacity: 1; transform: translateY(0) scale(1); }
to { opacity: 0; transform: translateY(12px) scale(0.992); }
}
.animate-fade-out-soft {
animation: fade-out-soft 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes overlay-wash-in {
from { opacity: 0; backdrop-filter: blur(0px); }
to { opacity: 1; backdrop-filter: blur(6px); }
}
.animate-overlay-wash-in {
animation: overlay-wash-in 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards;
}
@keyframes overlay-wash-out {
from { opacity: 1; backdrop-filter: blur(6px); }
to { opacity: 0; backdrop-filter: blur(0px); }
}
.animate-overlay-wash-out {
animation: overlay-wash-out 0.32s cubic-bezier(0.32, 0, 0.67, 0) forwards;
}
/* Modal 弹体:Apple spring overshoot 曲线,scale 差值放大到 0.96 才能弹出来 */
@keyframes modal-pop-in {
0% { opacity: 0; transform: translateY(8px) scale(0.97); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
.animate-modal-pop-in {
animation: modal-pop-in 0.45s cubic-bezier(0.16, 1, 0.3, 1) forwards;
will-change: transform, opacity;
}
@keyframes modal-pop-out {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(6px) scale(0.97); }
}
.animate-modal-pop-out {
animation: modal-pop-out 0.32s cubic-bezier(0.32, 0, 0.67, 0) forwards;
will-change: transform, opacity;
}
@keyframes paw-stamp {
0%, 100% { transform: translateY(0) rotate(-6deg) scale(0.96); opacity: 0.26; }
35% { transform: translateY(7px) rotate(3deg) scale(1.04); opacity: 0.78; }
60% { transform: translateY(-1px) rotate(-2deg) scale(0.99); opacity: 0.5; }
}
.problem-paw {
animation: paw-stamp 1.2s ease-in-out infinite;
}
@keyframes studio-entrance {
0% { opacity: 0; transform: scale(0.98); filter: blur(12px); }
100% { opacity: 1; transform: scale(1); filter: blur(0px); }
}
@keyframes classic-entrance {
0% { opacity: 0; transform: scale(1.02); filter: blur(8px); }
100% { opacity: 1; transform: scale(1); filter: blur(0px); }
}
.animate-studio-entrance {
animation: studio-entrance 1.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
.animate-studio-exit {
animation: studio-entrance 1.2s cubic-bezier(0.16, 1, 0.3, 1) reverse forwards;
}
/* Workspace 退场:纯 CSS animation(不走 transition,避免入场 animation 移除时跳变) */
@keyframes workspace-exit {
0% { opacity: 1; transform: scale(1); filter: blur(0px); }
100% { opacity: 0; transform: scale(1.02); filter: blur(16px); }
}
.animate-workspace-exit {
animation: workspace-exit 0.6s cubic-bezier(0.42, 0, 0.58, 1) forwards;
will-change: transform, opacity, filter;
}
.animate-classic-entrance {
animation: classic-entrance 1.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
/* ==========================================================
进场:studio 部件错峰组装浮现 + home 淡出退场
退出:studio 右滑出 ‖ home 左滑入(iOS pop,无模糊/无等待)
========================================================== */
@keyframes page-slide-out-right {
0% { transform: translateX(0); }
100% { transform: translateX(100%); }
}
@keyframes page-slide-in-left {
0% { transform: translateX(-30%); }
100% { transform: translateX(0); }
}
/* Studio 部件错峰组装浮现 */
@keyframes assemble-slide-left { 0% { transform: translateX(-100%); } 100% { transform: translateX(0); } }
@keyframes assemble-drop { 0% { opacity: 0; transform: translateY(-6px); } 100% { opacity: 1; transform: translateY(0); } }
@keyframes assemble-fade { 0% { opacity: 0; } 100% { opacity: 1; } }
.page-assemble [data-assemble="sidebar"] { animation: assemble-slide-left 0.48s cubic-bezier(0.22, 1, 0.36, 1) both; }
.page-assemble [data-assemble="header"] { animation: assemble-drop 0.32s cubic-bezier(0.22, 1, 0.36, 1) 0.06s both; }
.page-assemble [data-assemble="strip"] { animation: assemble-fade 0.32s cubic-bezier(0.22, 1, 0.36, 1) 0.12s both; }
/* 舞台晚于其余部件落定,并且只做透明度——一大块深色表面缩放会读成“胀出来”。
浅色机架(header/strip/composer)先就位,屏幕像被点亮一样随后浮现。
透明度用 soft(慢起手),不是 move:这里没有运动去遮挡快起手。 */
.page-assemble [data-assemble="stage"] { animation: assemble-fade 0.58s var(--ease-soft) 0.16s both; }
.page-assemble [data-assemble="composer"] { animation: assemble-fade 0.40s cubic-bezier(0.22, 1, 0.36, 1) 0.08s both; }
/* 表单页(设置 / 提示词)沿用同一份 data-assemble 约定。studio 的部件要“拼”出一台机器:
sidebar 整块从画外推进来。表单页部件少、距离短,位移收到 12px + 淡入,读成“落位”而不是“滑进来”。 */
@keyframes assemble-rise { 0% { opacity: 0; transform: translateY(6px); } 100% { opacity: 1; transform: translateY(0); } }
@keyframes assemble-in-left { 0% { opacity: 0; transform: translateX(-12px); } 100% { opacity: 1; transform: translateX(0); } }
.page-assemble [data-assemble="rail"] { animation: assemble-in-left 0.34s var(--ease-move) 0.04s both; }
.page-assemble [data-assemble="panel"] { animation: assemble-rise 0.38s var(--ease-move) 0.10s both; }
/* home 退场覆盖层:淡出 + 微缩,悄悄退到 studio 组装之下 */
@keyframes home-leave { 0% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(0.985); } }
.page-home-leave { animation: home-leave 0.32s ease both; }
/* 退出滑动(iOS pop):studio 右滑出,home 左滑入 */
.page-slide-out { animation: page-slide-out-right 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; will-change: transform; }
.page-slide-in { animation: page-slide-in-left 0.4s cubic-bezier(0.22, 1, 0.36, 1) both; will-change: transform; }
/* 下拉菜单:从触发按钮下方长出来。位移很小,只用来交代“我从哪来”;缩放原点是顶边中点。 */
@keyframes menu-in { 0% { opacity: 0; transform: translateY(-6px) scale(0.98); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
.menu-in { animation: menu-in 0.18s var(--ease-enter) both; transform-origin: top center; }
/* 分区切换:面板落位。内容换了才播,所以挂在以 section 为 key 的元素上(见 SettingsPage)。 */
@keyframes panel-in { 0% { opacity: 0; transform: translateY(8px); } 100% { opacity: 1; transform: translateY(0); } }
.panel-in { animation: panel-in 0.26s var(--ease-move) both; }
/* 同一层里左右两格互切(CatKey / BYOK、弹窗的选择→表单):新内容从它对应的那一侧进来,
方向跟着手指走,接上药丸的位移。 */
@keyframes panel-in-right { 0% { opacity: 0; transform: translateX(10px); } 100% { opacity: 1; transform: translateX(0); } }
@keyframes panel-in-left { 0% { opacity: 0; transform: translateX(-10px); } 100% { opacity: 1; transform: translateX(0); } }
.panel-in-right { animation: panel-in-right 0.26s var(--ease-move) both; }
.panel-in-left { animation: panel-in-left 0.26s var(--ease-move) both; }
@media (prefers-reduced-motion: reduce) {
.page-slide-in, .page-slide-out, .page-assemble [data-assemble], .page-home-leave,
.menu-in, .panel-in, .panel-in-right, .panel-in-left, [data-assemble] { animation: none !important; }
}
/* ==========================================================
Studio 细节过渡(HIG 曲线分工)
出现/消失是挂载/卸载,没有可过渡的中间态 —— 所以一律走 animation(keyframe),
而不是 transition;能留挂载的位移/尺寸才用 transition。
========================================================== */
@keyframes studio-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes studio-fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes studio-rise-in { from { opacity: 0; transform: translateY(6px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes studio-pop-in { from { opacity: 0; transform: translateY(-4px) scale(0.97); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes studio-pop-out { from { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 0; transform: translateY(-4px) scale(0.98); } }
/* Modal panels: the all-Scenes grid and the delete question. Slower and more decelerating than a
pop-in — a dialog that arrives as fast as a chip reads as a glitch, and the extra 100ms is what
makes the arrival feel placed rather than switched on. The exit keeps the shared exit curve, so
leaving still reads as leaving. */
@keyframes studio-sheet-in { from { opacity: 0; transform: translateY(-8px) scale(0.965); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes studio-sheet-out { from { opacity: 1; transform: translateY(0) scale(1); } to { opacity: 0; transform: translateY(-6px) scale(0.98); } }
@keyframes studio-media-in { from { opacity: 0; transform: scale(0.965); } to { opacity: 1; transform: scale(1); } }
@keyframes studio-slide-in-right { from { opacity: 0; transform: translateX(28px); } to { opacity: 1; transform: translateX(0); } }
@keyframes studio-slide-out-right { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(28px); } }
@keyframes studio-slide-in-left { from { transform: translateX(-100%); } to { transform: translateX(0); } }
@keyframes studio-slide-out-left { from { transform: translateX(0); } to { transform: translateX(-100%); } }
.studio-fade-in { animation: studio-fade-in 0.18s var(--ease-enter) both; }
.studio-fade-out { animation: studio-fade-out 0.15s var(--ease-exit) both; }
.studio-rise-in { animation: studio-rise-in 0.26s var(--ease-enter) both; }
.studio-pop-in { animation: studio-pop-in 0.2s var(--ease-enter) both; }
.studio-pop-out { animation: studio-pop-out 0.15s var(--ease-exit) both; }
.studio-sheet-in { animation: studio-sheet-in 0.3s var(--ease-move) both; }
.studio-sheet-out { animation: studio-sheet-out 0.2s var(--ease-exit) both; }
.studio-media-in { animation: studio-media-in 0.42s var(--ease-enter) both; }
/* 屏幕里的占位字:短而柔的浮现。不能拉长——换 state 时旧字立刻卸载,这段长度就是“黑底无字”的长度。 */
@keyframes studio-placeholder-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.studio-placeholder-in { animation: studio-placeholder-in 0.26s var(--ease-soft) backwards; }
.studio-slide-in-right { animation: studio-slide-in-right 0.26s var(--ease-enter) both; }
.studio-slide-out-right { animation: studio-slide-out-right 0.18s var(--ease-exit) both; }
/* Conversation motion is independent of stage layout: animate only its painted surface. */
@keyframes cinema-history-in {
from { opacity: 0; transform: translateX(100%); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes cinema-history-out {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(100%); }
}
.cinema-history-in { animation: cinema-history-in 0.34s cubic-bezier(0.22, 1, 0.36, 1) both; }
.cinema-history-out { animation: cinema-history-out 0.34s cubic-bezier(0.4, 0, 0.2, 1) both; }
.studio-slide-in-left { animation: studio-slide-in-left 0.3s var(--ease-move) both; }
.studio-slide-out-left { animation: studio-slide-out-left 0.22s var(--ease-exit) both; }
/* 内容换卡:换对话 = 竖轴,换场景 = 横轴。这是“推”:两张卡同向移动、彼此紧贴,
所以中间不会露出页面底色(不是从两边往中间对撞)。离场卡同时缩小 = 卡片飞走。
只走位移和缩放,不走透明度:透明度一低,卡后面的纸色就透出来(就是之前的白闪)。
用 backwards/forwards 而不都是 both:进场卡结束后不留 transform,
否则这个包裹层会变成 `position: fixed` 的包含块,把场景条里的选择器弹层定位弄歪。 */
@keyframes content-rise { from { transform: translateY(100%) scale(0.94); } to { transform: translateY(0) scale(1); } }
@keyframes content-fall { from { transform: translateY(-100%) scale(0.94); } to { transform: translateY(0) scale(1); } }
@keyframes content-from-right { from { transform: translateX(100%) scale(0.94); } to { transform: translateX(0) scale(1); } }
@keyframes content-from-left { from { transform: translateX(-100%) scale(0.94); } to { transform: translateX(0) scale(1); } }
/* 缩放的原点必须落在“接缝边”(两卡相邻的那条边),否则缩放在飞行中点会把接缝撕开
一条纸色带(默认以中心缩放,两张各往回缩 5%+3%)。
接缝边 = 旧卡的“被推进方向的那条边” = 新卡的“进入方向的那条边”:
往上推:旧卡底边 / 新卡顶边;往下推:旧卡顶边 / 新卡底边;往左推:旧卡右边 / 新卡左边。 */
.studio-content-rise { animation: content-rise 0.42s var(--ease-swap) backwards; transform-origin: center top; }
.studio-content-fall { animation: content-fall 0.42s var(--ease-swap) backwards; transform-origin: center bottom; }
.studio-content-from-right { animation: content-from-right 0.42s var(--ease-swap) backwards; transform-origin: left center; }
.studio-content-from-left { animation: content-from-left 0.42s var(--ease-swap) backwards; transform-origin: right center; }
/* 离场卡:往它来的反方向飞走(同时缩小)。用 forwards 保留终点,卡片停在画面外直到卸载。 */
@keyframes content-rise-out { from { transform: translateY(0) scale(1); } to { transform: translateY(-100%) scale(0.9); } }
@keyframes content-fall-out { from { transform: translateY(0) scale(1); } to { transform: translateY(100%) scale(0.9); } }
@keyframes content-to-left { from { transform: translateX(0) scale(1); } to { transform: translateX(-100%) scale(0.9); } }
@keyframes content-to-right { from { transform: translateX(0) scale(1); } to { transform: translateX(100%) scale(0.9); } }
.studio-content-rise-out { animation: content-rise-out 0.42s var(--ease-swap) forwards; transform-origin: center bottom; }
.studio-content-fall-out { animation: content-fall-out 0.42s var(--ease-swap) forwards; transform-origin: center top; }
.studio-content-to-left { animation: content-to-left 0.42s var(--ease-swap) forwards; transform-origin: right center; }
.studio-content-to-right { animation: content-to-right 0.42s var(--ease-swap) forwards; transform-origin: left center; }
/* 猫开口:比通用淡入更慢、带一点由下往上,读起来是“说出来”而不是“跳出来”。 */
@keyframes studio-bubble-in { from { opacity: 0; transform: translateY(8px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } }
.studio-bubble-in { animation: studio-bubble-in 0.34s var(--ease-enter) backwards; }
/* 高度折叠(工具详情 / 提示行):留挂载,0fr → 1fr,位移用 move 曲线。
折叠时高度归零,但父容器的 gap 还会占一格 —— 需要时用 `-mb-<gap>` 在收起时抵消,
所以 margin-bottom 也在过渡里(跟高度同曲线同时长,不会跳)。 */
.studio-collapse {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.24s var(--ease-move), opacity 0.24s var(--ease-move),
margin-bottom 0.24s var(--ease-move);
opacity: 0;
}
.studio-collapse[data-open="true"] {
grid-template-rows: 1fr;
opacity: 1;
}
.studio-collapse > * {
min-height: 0;
overflow: hidden;
}
/* 面板底部的渐隐带:最底那一条固定什么都不放,内容在滚到底之前先淡出。
用 mask 而不是盖一层渐变 —— 不用配色,亮/暖/暗三套主题自动一致,也不会盖住滚动条。
滚动容器要留出同样高度的底部内边距(pb-14 = 56px),否则最后一行会停在渐隐带里。 */
.studio-fade-bottom {
--studio-fade-bottom: 56px;
-webkit-mask-image: linear-gradient(
to bottom,
#000 calc(100% - var(--studio-fade-bottom)),
transparent
);
mask-image: linear-gradient(to bottom, #000 calc(100% - var(--studio-fade-bottom)), transparent);
}
/* 窄屏的会话抽屉:只在 lg 以下滑入(lg 以上侧栏是在流列,宽度过渡已经够了)。 */
@media (max-width: 1023px) {
.studio-drawer-in { animation: studio-slide-in-left 0.3s var(--ease-move) both; }
}
@media (prefers-reduced-motion: reduce) {
.studio-fade-in, .studio-fade-out, .studio-rise-in, .studio-pop-in, .studio-pop-out,
.studio-sheet-in, .studio-sheet-out,
.studio-media-in, .studio-slide-in-right, .studio-slide-out-right, .cinema-history-in, .cinema-history-out,
.studio-slide-in-left, .studio-slide-out-left { animation: none !important; }
.studio-collapse { transition: none !important; }
.studio-drawer-in { animation: none !important; }
.studio-content-rise, .studio-content-fall, .studio-content-from-right, .studio-content-from-left {
animation: none !important;
}
.studio-content-rise-out, .studio-content-fall-out, .studio-content-to-left, .studio-content-to-right {
animation: none !important;
}
.studio-bubble-in { animation: none !important; }
.studio-placeholder-in { animation: none !important; }
/* 分段控件的选中胶囊:减少动态时直接跳位,不平移。 */
.studio-selection-pill { transition: none !important; }
}
/* Studio 终端光标闪烁 */
@keyframes blink-cursor {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Studio 猫爪轻浮动 */
@keyframes studio-paw-float {
0%, 100% { transform: translateY(0) rotate(-3deg); }
50% { transform: translateY(-3px) rotate(3deg); }
}
.studio-paw-float {
animation: studio-paw-float 3s ease-in-out infinite;
display: inline-block;
}
.studio-cursor {
animation: blink-cursor 1s step-end infinite;
color: rgba(var(--text-secondary-rgb), 0.4);
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
}
@keyframes studio-dot-bounce {
0%, 80%, 100% {
transform: translateY(0) scale(0.9);
opacity: 0.28;
}
40% {
transform: translateY(-4px) scale(1);
opacity: 0.88;
}
}
@keyframes studio-type-caret-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.18; }
}
/* Studio 终端基础样式 */
.studio-shell-root {
font-family: "LXGW WenKai Screen", "LXGW WenKai TC", "LXGW WenKai Lite", "LXGW WenKai", sans-serif !important;
}
.studio-shell-root * {
font-family: inherit;
}
.studio-shell-root .font-mono,
.studio-terminal .font-mono,
.studio-cursor {
font-family: "LXGW WenKai Screen", "LXGW WenKai TC", "LXGW WenKai Lite", "LXGW WenKai", sans-serif !important;
}
.studio-terminal {
font-family: inherit;
}
.studio-brand-title {
font-family: "LXGW WenKai Screen", "LXGW WenKai TC", "LXGW WenKai Lite", "LXGW WenKai", sans-serif !important;
letter-spacing: 0.18em;
}
.studio-terminal ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
}
.studio-terminal ::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
}
.studio-thinking-dots {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.studio-thinking-dots span {
width: 0.34rem;
height: 0.34rem;
border-radius: 9999px;
background: rgba(var(--accent-rgb), 0.55);
animation: studio-dot-bounce 1.1s ease-in-out infinite;
}
.studio-thinking-dots span:nth-child(2) {
animation-delay: 0.14s;
}
.studio-thinking-dots span:nth-child(3) {
animation-delay: 0.28s;
}
.studio-type-caret {
margin-left: 0.18rem;
color: rgba(var(--accent-rgb), 0.62);
animation: studio-type-caret-blink 0.9s step-end infinite;
}
.studio-markdown {
word-break: break-word;
}
.studio-markdown > :first-child {
margin-top: 0;
}
.studio-markdown > :last-child {
margin-bottom: 0;
}
.studio-markdown p,
.studio-markdown ul,
.studio-markdown ol,
.studio-markdown blockquote,
.studio-markdown table,
.studio-markdown hr {
margin: 0.9rem 0;
}
.studio-markdown ul,
.studio-markdown ol {
padding-left: 1.4rem;
}
.studio-markdown li + li {
margin-top: 0.3rem;
}
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown p,
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown ul,
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown ol,
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown blockquote,
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown table,
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown hr {
margin: 0.55rem 0;
}
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown blockquote {
border-left: 0;
padding-left: 0;
color: rgba(var(--accent-rgb), 0.6);
}
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown code {
background: transparent !important;
padding: 0;
border-radius: 0;
}
.studio-terminal[data-variant="pure-minimal-bottom"] .react-syntax-highlighter {
background: transparent !important;
padding: 0 !important;
border-radius: 0 !important;
}
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown img,
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown th,
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown td {
border: 0 !important;
border-radius: 0 !important;
background: transparent !important;
}
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown hr {
border-top-color: rgba(var(--accent-rgb), 0.08);
}
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown-inline > p:first-child {
display: inline;
margin: 0;
}
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown-inline > p:first-child + * {
margin-top: 0.55rem;
}
.studio-terminal[data-variant="pure-minimal-bottom"] .studio-markdown-inline > p:only-child {
display: inline;
margin: 0;
}
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.studio-markdown .katex-display {
margin: 1rem 0;
overflow-x: auto;
overflow-y: hidden;
padding: 0.1rem 0;
}
.canvas-transition-grid {
background-image:
linear-gradient(rgba(var(--border-rgb), 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(var(--border-rgb), 0.05) 1px, transparent 1px);
background-size: 64px 64px;
mask-image: radial-gradient(circle at center, black, transparent 80%);
}
.canvas-paper-grid {
background:
repeating-linear-gradient(
to bottom,
transparent 0,
transparent 62px,
rgba(80, 80, 80, 0.12) 62px,
rgba(80, 80, 80, 0.12) 63px,
transparent 63px,
transparent 124px
),
repeating-linear-gradient(
to right,
transparent 0,
transparent 70px,
rgba(100, 100, 100, 0.08) 70px,
rgba(100, 100, 100, 0.08) 71px,
transparent 71px,
transparent 142px
);
}
/* Cat character animations (doc §7.3). Low-frequency, CSS-only; turned off under reduced motion. */
@keyframes cat-blink {
0%, 92%, 100% { transform: scaleY(1); }
96% { transform: scaleY(0.1); }
}
@keyframes cat-breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
@keyframes cat-lean {
0%, 100% { transform: rotate(-1.5deg); }
50% { transform: rotate(1.5deg); }
}
.cinema-cat-character[data-pose="idle"] {
animation: cat-breathe 4s ease-in-out infinite;
}
.cinema-cat-character[data-pose="idle"] [data-part="eye"] {
transform-box: fill-box;
transform-origin: center;
animation: cat-blink 5s ease-in-out infinite;
}
.cinema-cat-character[data-pose="busy"] {
animation: cat-lean 2.4s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.cinema-cat-character,
.cinema-cat-character [data-part="eye"] {
animation: none !important;
}
}
/* Cat jumping into the sidebar header (doc §3.2): the sidebar cat scales+fades in. */
@keyframes cat-enter {
from { opacity: 0; transform: scale(0.5) translateY(8px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
.cinema-cat-enter {
animation: cat-enter 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
@media (prefers-reduced-motion: reduce) {
.cinema-cat-enter {
animation: none;
}
}
/* Conversation rows emerge (doc §7.1): each new message fades in and floats up. */
@keyframes cinema-row-enter {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.cinema-row-enter {
animation: cinema-row-enter 240ms cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* The cat composing: three dots pulse while the assistant has produced no text yet. */
@keyframes cinema-thinking {
0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
40% { opacity: 1; transform: translateY(-2px); }
}
.cinema-thinking-dot {
animation: cinema-thinking 1.2s infinite ease-in-out;
}
.cinema-thinking-dot:nth-child(2) { animation-delay: 0.15s; }
.cinema-thinking-dot:nth-child(3) { animation-delay: 0.3s; }
@media (prefers-reduced-motion: reduce) {
.cinema-row-enter { animation: none; }
.cinema-thinking-dot { animation: none; opacity: 0.6; }
}
/* 删除一行:这个过渡在 Tailwind 类上(grid-rows 1fr → 0fr 收高度、同时淡出),
所以这里只需给 prefers-reduced-motion 一个兜底。顺带把删除确认框用到的
fade/pop 也一起关掉——它们原本就漏在这道闸门外。 */
@media (prefers-reduced-motion: reduce) {
.studio-row-removing,
.studio-fade-in,
.studio-fade-out,
.studio-pop-in,
.studio-pop-out,
.studio-sheet-in,
.studio-sheet-out { animation: none !important; transition: none !important; }
}