From bd13cffe1467bd6b14d7160ed0993f1161e3306d Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Thu, 4 Jun 2026 21:52:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BA=AE=E8=89=B2/?= =?UTF-8?q?=E6=9A=97=E8=89=B2=E4=B8=BB=E9=A2=98=E5=88=87=E6=8D=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 index.css 中新增 html.light 亮色主题 CSS 变量和过渡动画 - 在 App.tsx Header 添加 Sun/Moon 主题切换按钮 - 主题偏好通过 localStorage 持久化,默认暗色主题 - 将所有组件中的硬编码 Tailwind 颜色值转换为 CSS 变量引用 - 状态色(emerald/amber/red/violet)保持不变,两种主题均适用 --- web/src/App.tsx | 116 ++++++---- web/src/components/Chat/MessageBubble.tsx | 108 +++++----- web/src/components/Chat/MessageInput.tsx | 38 ++-- web/src/components/Chat/MessageList.tsx | 38 ++-- web/src/components/Panel/ToolPanel.tsx | 34 +-- .../components/Sidebar/SchedulerJobList.tsx | 34 +-- web/src/components/Sidebar/SessionInfo.tsx | 20 +- web/src/components/Sidebar/TopicList.tsx | 36 ++-- web/src/index.css | 201 +++++++++++++----- 9 files changed, 373 insertions(+), 252 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index be2c9c2..124dd52 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ -import { useCallback, useEffect, useMemo, useRef } from 'react' -import { Zap, Cpu, MessageSquare, ArrowLeft, Bot, Clock } from 'lucide-react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Zap, Cpu, MessageSquare, ArrowLeft, Bot, Clock, Sun, Moon } from 'lucide-react' import { ChatContainer } from './components/Chat/ChatContainer' import { TopicList } from './components/Sidebar/TopicList' import { SessionInfo } from './components/Sidebar/SessionInfo' @@ -59,6 +59,32 @@ function App() { onMessage: handleServerMessage, }) + // ---- 主题状态 ---- + + const [theme, setTheme] = useState<'dark' | 'light'>(() => { + const saved = localStorage.getItem('picobot-theme') + return saved === 'light' ? 'light' : 'dark' + }) + + useEffect(() => { + const root = document.documentElement + if (theme === 'light') { + root.classList.add('light') + } else { + root.classList.remove('light') + } + localStorage.setItem('picobot-theme', theme) + + // 切换时启用平滑过渡 + root.classList.add('theme-transitioning') + const timer = setTimeout(() => { + root.classList.remove('theme-transitioning') + }, 350) + return () => clearTimeout(timer) + }, [theme]) + + // ---- WebSocket 初始化 ---- + // 连接建立后自动加载 Session useEffect(() => { if (isConnected && status === 'connected') { @@ -261,23 +287,33 @@ function App() { const toolMessages = messages return ( -
+
{/* Header */} -
+
- +

- Pico - Bot + Pico + Bot

-
+
+ {/* 主题切换按钮 */} +
+
-
+
- + AI Ready
{session && ( @@ -294,21 +330,21 @@ function App() { {/* Main Content */}
{/* Left Sidebar */} -
+
-
+
{/* Tab 栏 */} -
+
{/* Center - Chat */} -
+
{/* Scheduler job view back bar */} {schedulerView && ( -
+
-
-
+
+
- 定时任务: - {schedulerView.description} + 定时任务: + {schedulerView.description}
-
+
- 通道: - {schedulerView.channel} + 通道: + {schedulerView.channel}
)} {/* Sub-agent back bar */} {subAgentView && ( -
+
-
-
+
+
- 子智能体: - {subAgentView.description} + 子智能体: + {subAgentView.description}
-
+
- 类型: - {subAgentView.subagentType || '...'} + 类型: + {subAgentView.subagentType || '...'}
-
+
- 状态: + 状态: {subAgentView.status === 'completed' ? '已完成' : subAgentView.status === 'failed' ? '失败' : @@ -431,7 +467,7 @@ function App() {
{/* Right Sidebar - Tool Panel */} -
+
diff --git a/web/src/components/Chat/MessageBubble.tsx b/web/src/components/Chat/MessageBubble.tsx index 080dbe1..a620488 100644 --- a/web/src/components/Chat/MessageBubble.tsx +++ b/web/src/components/Chat/MessageBubble.tsx @@ -125,20 +125,20 @@ function AttachmentCard({ attachment }: { attachment: Attachment }) { return (
- + {getAttachmentIcon(attachment.media_type)} - + {fileName} - {attachment.media_type} + {attachment.media_type} {canDownload && ( - + )}
) @@ -162,13 +162,13 @@ function CopyButton({ text, className = '' }: { text: string; className?: string return ( ) @@ -281,7 +281,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
- {message.toolName || 'Tool'} + {message.toolName || 'Tool'} {isTaskTool && ( 子智能体·{subagentType} @@ -292,11 +292,11 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr 子智能体 )} - {formatTime(message.timestamp)} + {formatTime(message.timestamp)}
setToolExpanded(!toolExpanded)} - className={`cursor-pointer rounded-xl border bg-[#1a1a25]/60 w-full transition-all duration-500 hover:bg-[#1a1a25]/80 group ${ + className={`cursor-pointer rounded-xl border bg-[var(--bg-tertiary)]/60 w-full transition-all duration-500 hover:bg-[var(--bg-tertiary)]/80 group ${ taskResult ? taskStatusConfig[taskResult.status].borderColor : statusConfig.fullBorder }`} > @@ -305,7 +305,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr - + {isTaskTool ? (taskDescription || '子智能体任务') : (message.toolName || 'Tool')} {status === 'result' && message.durationMs != null && ( - + ⏱️ {formatDuration(message.durationMs)} )} {hasResult && } {toolExpanded ? ( - + ) : ( - + )}
@@ -336,7 +336,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr {!toolExpanded && ( <> {hasArgs && argsPreview && !taskResult && ( -
)} {taskResult && taskResult.summary && ( -
+
{taskResult.summary}
)} @@ -358,7 +358,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr e.stopPropagation() onNavigateToSubAgent?.(taskResult.task_id, taskDescription || '子智能体任务') }} - className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1" + className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1" > 查看完整会话 @@ -372,7 +372,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr e.stopPropagation() onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务') }} - className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1" + className="text-xs text-[var(--accent-cyan)] hover:text-[var(--accent-cyan)]/80 hover:underline transition-colors flex items-center gap-1" > 查看实时进度 @@ -380,7 +380,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
)} {isTaskTool && !taskResult && !message.subagentTaskId && ( -
+
子智能体正在执行...
)} @@ -389,13 +389,13 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr {/* Expanded */} {toolExpanded && ( -
+
{taskResult ? ( <> {taskPrompt && (
-
任务指令
-
+                        
任务指令
+
                           {taskPrompt}
                         
@@ -406,19 +406,19 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr taskResult.status === 'failed' ? 'bg-red-500/10 border border-red-500/30' : 'bg-amber-500/10 border border-amber-500/30' }`}> -
摘要
-
{taskResult.summary}
+
摘要
+
{taskResult.summary}
)}
-
输出
-
+
输出
+
{taskResult.output}
-
+
task_id: {taskResult.task_id}
- + {isUser ? 'You' : isTool ? message.toolName || 'Tool' : 'Assistant'} - {formatTime(message.timestamp)} + {formatTime(message.timestamp)}
{children} @@ -554,7 +554,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr ) } return ( -
+                      
                         
                           {children}
                         
@@ -563,13 +563,13 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
                   },
                   // 标题样式
                   h1: ({ children }) => (
-                    

{children}

+

{children}

), h2: ({ children }) => ( -

{children}

+

{children}

), h3: ({ children }) => ( -

{children}

+

{children}

), // 段落 p: ({ children }) =>

{children}

, @@ -582,7 +582,7 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr href={href} target="_blank" rel="noopener noreferrer" - className="text-[#00f0ff] hover:underline" + className="text-[var(--accent-cyan)] hover:underline" > {children} @@ -591,24 +591,24 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr table: ({ children }) => ( {children}
), - thead: ({ children }) => {children}, + thead: ({ children }) => {children}, th: ({ children }) => ( - {children} + {children} ), td: ({ children }) => ( - {children} + {children} ), // 引用块 blockquote: ({ children }) => ( -
+
{children}
), // 分隔线 - hr: () =>
, + hr: () =>
, // 加粗和斜体 - strong: ({ children }) => {children}, - em: ({ children }) => {children}, + strong: ({ children }) => {children}, + em: ({ children }) => {children}, }} > {message.content} diff --git a/web/src/components/Chat/MessageInput.tsx b/web/src/components/Chat/MessageInput.tsx index 36aa92c..ce8506f 100644 --- a/web/src/components/Chat/MessageInput.tsx +++ b/web/src/components/Chat/MessageInput.tsx @@ -251,22 +251,22 @@ export function MessageInput({ // 只读模式:显示提示占位符 if (isReadOnly) { return ( -
+
-
+
-
+
只读模式
-

+

{channelName ? ( <>{channelName} 通道仅支持查看历史消息 ) : ( '当前通道仅支持查看历史消息' )}

-

+

请切换至 WebSocket 通道进行输入

@@ -277,7 +277,7 @@ export function MessageInput({ } return ( -
+
{/* 错误提示 */} {error && ( @@ -292,7 +292,7 @@ export function MessageInput({ {attachments.map((att, index) => (
{att.preview ? ( ) : ( -
+
{getAttachmentIcon(att.attachment.media_type)}
)} - + {att.attachment.file_name} @@ -321,7 +321,7 @@ export function MessageInput({ {/* 输入区域 */}
{/* 拖拽提示 */} {isDragging && ( -
-
+
+
拖放文件到这里
@@ -340,7 +340,7 @@ export function MessageInput({ @@ -363,9 +363,9 @@ export function MessageInput({ placeholder={placeholder} disabled={disabled} rows={1} - className="w-full resize-none rounded-xl border border-white/10 bg-[#1a1a25] px-4 py-3 pr-12 text-sm text-white placeholder:text-zinc-500 focus:border-[#00f0ff]/50 focus:outline-none focus:ring-1 focus:ring-[#00f0ff]/20 disabled:opacity-50 transition-all self-center scrollbar-hide" + className="w-full resize-none rounded-xl border border-[var(--border-color)] bg-[var(--bg-tertiary)] px-4 py-3 pr-12 text-sm text-[var(--text-primary)] placeholder:text-[var(--text-muted)] focus:border-[var(--accent-cyan)]/50 focus:outline-none focus:ring-1 focus:ring-[var(--focus-ring)] disabled:opacity-50 transition-all self-center scrollbar-hide" /> - +
{/* 发送/停止按钮 */} @@ -381,7 +381,7 @@ export function MessageInput({
{/* 提示 */} -
+
按 Enter 发送,Shift+Enter 换行 · 支持拖拽/粘贴文件 · 最大 50MB
) -} \ No newline at end of file +} diff --git a/web/src/components/Chat/MessageList.tsx b/web/src/components/Chat/MessageList.tsx index 2538b99..3f5ddb4 100644 --- a/web/src/components/Chat/MessageList.tsx +++ b/web/src/components/Chat/MessageList.tsx @@ -131,14 +131,14 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps return (
-
- +
+
-

开始新的对话

-

在下方输入消息开始与 AI 助手聊天

-
- /new 创建话题 - /list 查看列表 +

开始新的对话

+

在下方输入消息开始与 AI 助手聊天

+
+ /new 创建话题 + /list 查看列表
@@ -168,11 +168,11 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps onClick={scrollToTop} className="pointer-events-auto group relative flex items-center justify-center w-9 h-9 rounded-full - bg-[#1a1a25]/80 backdrop-blur-md - border border-white/[0.06] - text-zinc-500 hover:text-[#00f0ff] - hover:border-[#00f0ff]/30 - hover:shadow-[0_0_20px_rgba(0,240,255,0.12)] + bg-[var(--bg-tertiary)]/80 backdrop-blur-md + border border-[var(--border-color)] + text-[var(--text-muted)] hover:text-[var(--accent-cyan)] + hover:border-[var(--accent-cyan)]/30 + hover:shadow-[0_0_20px_var(--shadow-glow-sm)] transition-all duration-300 ease-out animate-fade-in" aria-label="回到顶部" @@ -187,11 +187,11 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps onClick={() => scrollToBottom('smooth')} className="pointer-events-auto group relative flex items-center justify-center w-9 h-9 rounded-full - bg-[#1a1a25]/80 backdrop-blur-md - border border-white/[0.06] - text-zinc-500 hover:text-[#00f0ff] - hover:border-[#00f0ff]/30 - hover:shadow-[0_0_20px_rgba(0,240,255,0.12)] + bg-[var(--bg-tertiary)]/80 backdrop-blur-md + border border-[var(--border-color)] + text-[var(--text-muted)] hover:text-[var(--accent-cyan)] + hover:border-[var(--accent-cyan)]/30 + hover:shadow-[0_0_20px_var(--shadow-glow-sm)] transition-all duration-300 ease-out animate-fade-in" aria-label="回到底部" @@ -201,8 +201,8 @@ export function MessageList({ messages, onNavigateToSubAgent }: MessageListProps {/* 未读消息指示点 */} {hasNewMessage && ( - - + + )} diff --git a/web/src/components/Panel/ToolPanel.tsx b/web/src/components/Panel/ToolPanel.tsx index 38a7559..347bddc 100644 --- a/web/src/components/Panel/ToolPanel.tsx +++ b/web/src/components/Panel/ToolPanel.tsx @@ -128,11 +128,11 @@ export function ToolPanel({ messages }: ToolPanelProps) { return (
-
- +
+ 工具调用
-
+
暂无工具调用 @@ -145,10 +145,10 @@ export function ToolPanel({ messages }: ToolPanelProps) { return (
-
- +
+ 工具调用 - + {toolCalls.length}
@@ -167,31 +167,31 @@ export function ToolPanel({ messages }: ToolPanelProps) { return (
@@ -200,7 +200,7 @@ export function ToolPanel({ messages }: ToolPanelProps) { {hasResult && (
toggleExpand(tool.toolCallId)} @@ -212,7 +212,7 @@ export function ToolPanel({ messages }: ToolPanelProps) { )}
{!isExpanded && hasMore && ( -
+
点击展开全部 ({displayContent.split('\n').length} 行)
)} @@ -221,9 +221,9 @@ export function ToolPanel({ messages }: ToolPanelProps) { {/* 展开区域:参数 */} {isExpanded && tool.arguments ? ( -
-
参数:
-
+                  
+
参数:
+
                       {JSON.stringify(tool.arguments, null, 2)}
                     
diff --git a/web/src/components/Sidebar/SchedulerJobList.tsx b/web/src/components/Sidebar/SchedulerJobList.tsx index 9c80e1f..225c4ea 100644 --- a/web/src/components/Sidebar/SchedulerJobList.tsx +++ b/web/src/components/Sidebar/SchedulerJobList.tsx @@ -68,7 +68,7 @@ function lastStatusIcon(lastStatus: string | undefined) { case 'error': return default: - return + return } } @@ -76,17 +76,17 @@ export function SchedulerJobList({ jobs, onRefresh, onViewJob, sessionId }: Sche return (
{/* Header */} -
-

- +
+

+ 定时任务 {jobs.length > 0 && ( - ({jobs.length}) + ({jobs.length}) )}

diff --git a/web/src/index.css b/web/src/index.css index 8b1e012..b6bd62b 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,6 +1,8 @@ @import "tailwindcss"; -/* PicoBot Theme - Tech Dark */ +/* ============================================ + PicoBot Theme — Dark (default) + ============================================ */ :root { /* Core colors */ --bg-primary: #0a0a0f; @@ -24,9 +26,104 @@ /* Border */ --border-color: rgba(255, 255, 255, 0.08); --border-accent: rgba(0, 240, 255, 0.3); + + /* Overlay — for hover / subtle surface effects */ + --overlay-hover: rgba(255, 255, 255, 0.05); + --overlay-subtle: rgba(255, 255, 255, 0.10); + --overlay-medium: rgba(255, 255, 255, 0.20); + --overlay-dim: rgba(0, 0, 0, 0.20); + --overlay-dim-strong: rgba(0, 0, 0, 0.30); + --overlay-dim-heavy: rgba(0, 0, 0, 0.40); + --overlay-code: rgba(0, 0, 0, 0.30); + + /* Shadows */ + --shadow-glow-sm: rgba(0, 240, 255, 0.12); + --shadow-glow: rgba(0, 240, 255, 0.20); + --shadow-glow-strong: rgba(0, 240, 255, 0.30); + --shadow-glow-soft: rgba(0, 240, 255, 0.50); + + /* Focus ring */ + --focus-ring: rgba(0, 240, 255, 0.20); + + /* Selection / code highlight */ + --selection-bg: rgba(0, 240, 255, 0.30); + + /* Divider */ + --divider-color: rgba(255, 255, 255, 0.20); + + color-scheme: dark; } -/* Custom scrollbar */ +/* ============================================ + PicoBot Theme — Light + ============================================ */ +html.light { + /* Backgrounds */ + --bg-primary: #f5f5f7; + --bg-secondary: #ffffff; + --bg-tertiary: #ebecf0; + --bg-hover: #dfe0e5; + + /* Accents — slightly darker for readability on white */ + --accent-cyan: #008899; + --accent-blue: #2563eb; + --accent-purple: #7c3aed; + --accent-green: #059669; + --accent-amber: #d97706; + + /* Text */ + --text-primary: #1a1a2e; + --text-secondary: #52525b; + --text-muted: #a1a1aa; + --text-accent: var(--accent-cyan); + + /* Borders */ + --border-color: rgba(0, 0, 0, 0.08); + --border-accent: rgba(0, 136, 153, 0.30); + + /* Overlays */ + --overlay-hover: rgba(0, 0, 0, 0.04); + --overlay-subtle: rgba(0, 0, 0, 0.06); + --overlay-medium: rgba(0, 0, 0, 0.10); + --overlay-dim: rgba(0, 0, 0, 0.04); + --overlay-dim-strong: rgba(0, 0, 0, 0.06); + --overlay-dim-heavy: rgba(0, 0, 0, 0.08); + --overlay-code: rgba(0, 0, 0, 0.06); + + /* Shadows — softer */ + --shadow-glow-sm: rgba(0, 136, 153, 0.10); + --shadow-glow: rgba(0, 136, 153, 0.15); + --shadow-glow-strong: rgba(0, 136, 153, 0.22); + --shadow-glow-soft: rgba(0, 136, 153, 0.35); + + /* Focus ring */ + --focus-ring: rgba(0, 136, 153, 0.20); + + /* Selection / code highlight */ + --selection-bg: rgba(0, 136, 153, 0.20); + + /* Divider */ + --divider-color: rgba(0, 0, 0, 0.10); + + color-scheme: light; +} + +/* ============================================ + Smooth theme transition + ============================================ */ +html.theme-transitioning *, +html.theme-transitioning *::before, +html.theme-transitioning *::after { + transition: background-color 0.3s ease, + color 0.3s ease, + border-color 0.3s ease, + box-shadow 0.3s ease, + text-shadow 0.3s ease !important; +} + +/* ============================================ + Custom scrollbar + ============================================ */ ::-webkit-scrollbar { width: 6px; height: 6px; @@ -55,7 +152,9 @@ display: none; } -/* Base styles */ +/* ============================================ + Base styles + ============================================ */ * { box-sizing: border-box; } @@ -71,20 +170,26 @@ body { line-height: 1.6; } -/* Custom selection */ +/* ============================================ + Custom selection + ============================================ */ ::selection { - background: rgba(0, 240, 255, 0.3); + background: var(--selection-bg); color: var(--text-primary); } -/* Glowing text effect */ +/* ============================================ + Glowing text effect + ============================================ */ .glow-text { - text-shadow: 0 0 10px rgba(0, 240, 255, 0.5), - 0 0 20px rgba(0, 240, 255, 0.3), - 0 0 30px rgba(0, 240, 255, 0.1); + text-shadow: 0 0 10px var(--shadow-glow-soft), + 0 0 20px var(--shadow-glow-strong), + 0 0 30px var(--shadow-glow-sm); } -/* Gradient border */ +/* ============================================ + Gradient border + ============================================ */ .gradient-border { position: relative; } @@ -106,7 +211,9 @@ body { mask-composite: exclude; } -/* Animations */ +/* ============================================ + Animations + ============================================ */ @keyframes pulse-glow { 0%, 100% { box-shadow: 0 0 5px var(--accent-cyan), @@ -132,63 +239,35 @@ body { } @keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; } + to { opacity: 1; } } @keyframes typing-dot { - 0%, 60%, 100% { - transform: translateY(0); - } - 30% { - transform: translateY(-4px); - } + 0%, 60%, 100% { transform: translateY(0); } + 30% { transform: translateY(-4px); } } @keyframes scale-in { - 0% { - opacity: 0; - transform: scale(0.5); - } - 50% { - transform: scale(1.1); - } - 100% { - opacity: 1; - transform: scale(1); - } + 0% { opacity: 0; transform: scale(0.5); } + 50% { transform: scale(1.1); } + 100% { opacity: 1; transform: scale(1); } } -.animate-slide-in { - animation: slide-in 0.3s ease-out; -} - -.animate-fade-in { - animation: fade-in 0.2s ease-out; -} - -.animate-scale-in { - animation: scale-in 0.3s ease-out; -} +.animate-slide-in { animation: slide-in 0.3s ease-out; } +.animate-fade-in { animation: fade-in 0.2s ease-out; } +.animate-scale-in { animation: scale-in 0.3s ease-out; } .typing-indicator span { animation: typing-dot 1.4s infinite; display: inline-block; } +.typing-indicator span:nth-child(2) { animation-delay: 0.2s; } +.typing-indicator span:nth-child(3) { animation-delay: 0.4s; } -.typing-indicator span:nth-child(2) { - animation-delay: 0.2s; -} - -.typing-indicator span:nth-child(3) { - animation-delay: 0.4s; -} - -/* Code block styling */ +/* ============================================ + Code block styling + ============================================ */ pre { background: var(--bg-tertiary); border: 1px solid var(--border-color); @@ -202,15 +281,19 @@ code { font-size: 0.9em; } -/* Focus styles */ +/* ============================================ + Focus styles + ============================================ */ input:focus, textarea:focus { outline: none; border-color: var(--accent-cyan); - box-shadow: 0 0 0 2px rgba(0, 240, 255, 0.2); + box-shadow: 0 0 0 2px var(--focus-ring); } -/* Button hover effects */ +/* ============================================ + Button hover effects + ============================================ */ button { transition: all 0.2s ease; } @@ -223,7 +306,9 @@ button:active:not(:disabled) { transform: translateY(0); } -/* Link styles */ +/* ============================================ + Link styles + ============================================ */ a { color: var(--accent-cyan); text-decoration: none;