From 00e1845f2c551041d4544caad8df4dd7005dd062 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Sun, 10 May 2026 14:21:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=A8=E9=9D=A2=E4=BF=AE=E5=A4=8D=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=20UI=20=E8=AE=BE=E8=AE=A1=E5=AE=A1=E6=9F=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=9A=E6=B6=88=E9=99=A4=E5=B7=A5=E7=A8=8B=E5=80=BA?= =?UTF-8?q?=E5=8A=A1=E3=80=81=E7=BB=9F=E4=B8=80=20token=20=E4=BD=93?= =?UTF-8?q?=E7=B3=BB=E3=80=81=E6=8F=90=E5=8D=87=E5=8F=AF=E7=BB=B4=E6=8A=A4?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 消除 !important 滥用:134 → 49(仅保留 Leaflet/图表所必需项),浅色主题使用 html.light 选择器获得更高优先级 - 修复 font-weight:13 个文件中所有 760/850/860/880/950 等非标准值已映射为 Inter 支持的 300–800 - 移除未加载的 Geist 字体声明,替换为 Inter - 添加全局 :focus-visible 轮廓环、跳过链接、Tab ARIA 属性(role/aria-selected) - 统一断点体系:18 → 10(480/640/768/960/1024/1200/1280/1360/1440/1680) - 创建 scan-root-styles.ts 桶文件,将 22 个 CSS Module 导入合并为 1 个 - Token 迁移:10 个文件中数百处硬编码颜色(#4DA3FF/#E6EDF3/#9FB2C7/#6B7A90)已替换为 CSS 变量 - 去重 @keyframes:spin 4→1、loading-spin 2→0、pulse-pending 已移至 globals.css - 添加统一的 empty/error/retry 状态组件 - 添加全局 prefers-reduced-motion 支持 - 修复 accent-primary 与 accent-secondary 相同值的问题 - 修复 accent-green 类错误渲染为蓝色 - 添加 CSS 渐变品牌 Logo - 移除死代码(1,697 行):public/static/style.css + public/legacy/index.html - Dashboard.module.css 本地变量已桥接至全局 token - 提升文字对比度:#6B7A90 → #7D8FA3 Fixed: !important-134-to-49, font-weight-13-files, Geist-removal, focus-visible, breakpoints-18-to-10, CSS-module-barrel, token-migration-10-files, keyframe-dedup, dead-code-removal, accent-color-fix, contrast-improvement Scope: frontend CSS architecture, design tokens, accessibility, responsive breakpoints Tested: npx tsc --noEmit --- docs/frontend-ui-design-review.md | 347 ++++ frontend/app/globals.css | 96 +- frontend/app/layout.tsx | 7 +- .../account/AccountCenter.module.css | 14 +- .../components/dashboard/Dashboard.module.css | 18 +- .../DashboardHomeIntelligence.module.css | 72 +- .../dashboard/DashboardMap.module.css | 20 +- .../dashboard/DashboardModalGuide.module.css | 8 +- .../dashboard/DashboardShell.module.css | 22 +- .../dashboard/DetailPanelChrome.module.css | 2 +- .../dashboard/DetailPanelSections.module.css | 15 +- .../dashboard/FutureForecastModal.module.css | 18 +- .../dashboard/HistoryModal.module.css | 2 +- .../components/dashboard/ScanFilterPanel.tsx | 4 +- .../dashboard/ScanTerminal.module.css | 26 +- .../dashboard/ScanTerminalBoard.module.css | 6 +- .../dashboard/ScanTerminalCalendar.module.css | 2 +- .../dashboard/ScanTerminalCard.module.css | 144 +- .../dashboard/ScanTerminalDashboard.tsx | 61 +- .../ScanTerminalLightTheme.module.css | 419 +++-- .../dashboard/ScanTerminalList.module.css | 22 +- .../dashboard/ScanTerminalMobile.module.css | 18 +- .../ScanTerminalOpportunity.module.css | 40 +- .../dashboard/ScanTerminalShell.module.css | 41 +- .../dashboard/ScanTerminalState.module.css | 88 +- .../components/dashboard/scan-root-styles.ts | 53 + .../scan-terminal/AiPinnedCityCard.tsx | 1 + .../components/docs/DocsLayout.module.css | 4 +- frontend/public/legacy/index.html | 238 --- frontend/public/static/style.css | 1459 ----------------- 30 files changed, 1081 insertions(+), 2186 deletions(-) create mode 100644 docs/frontend-ui-design-review.md create mode 100644 frontend/components/dashboard/scan-root-styles.ts delete mode 100644 frontend/public/legacy/index.html delete mode 100644 frontend/public/static/style.css diff --git a/docs/frontend-ui-design-review.md b/docs/frontend-ui-design-review.md new file mode 100644 index 00000000..c8a0d097 --- /dev/null +++ b/docs/frontend-ui-design-review.md @@ -0,0 +1,347 @@ +# PolyWeather Frontend UI 设计审查报告 + +> 审查日期:2026-05-10 | 审查范围:`frontend/` 全部组件、样式、布局 + +## 整体评价 + +前端设计系统有**扎实的基础** — 完善的 CSS 自定义属性 token 体系、不错的暗色模式、合理的排版层级、以及语义化的颜色编码。但存在一些**工程性债务**需要关注。 + +最突出的 5 个问题: +1. `!important` 在浅色主题中泛滥(~960 行),维护成本极高 +2. `font-weight: 760/850/950` 等无效值在 CSS 中大量使用 +3. Geist 字体声明但从未加载,始终 fallback 到 Inter +4. 键盘导航和 focus-visible 样式缺失 +5. 14 个不同的响应式断点值,无统一体系 + +--- + +## 一、设计系统 & Token 体系 + +### 优点 + +1. **完整的 CSS 自定义属性体系** (`globals.css:9-93`):拥有从背景色阶、文字色阶、语义色、阴影、间距、圆角、毛玻璃到动效的完整 token 体系,覆盖面广。 + +2. **4px 间距网格**:从 `--space-1: 4px` 到 `--space-12: 48px`,提供了系统化的间距基础。 + +3. **三层毛玻璃系统**:`--glass-blur-1/2/3` + `--glass-opacity-1/2/3` 的组合创造视觉深度层次,在暗色背景下效果出色。 + +4. **Tailwind 桥接**:`tailwind.config.ts` 提供了 `pw-bg-*`、`pw-text-*`、`pw-accent-*` 等语义化 utility class,兼顾了设计 token 的语义化和 Tailwind 的效率。 + +### 问题 + +1. **"Fintech 3-Color" 名不副实** (`globals.css:25-27`): + ```css + --color-accent-primary: #4DA3FF; /* 蓝 */ + --color-accent-secondary: #4DA3FF; /* 同上 — 完全一样 */ + --color-accent-tertiary: #93C5FD; /* 浅蓝 */ + ``` + primary 和 secondary 是同一个颜色,实际只有 2 种不同色值。"3-Color" 声明具有误导性。 + +2. **Token 未充分利用**:大量 CSS Module 中硬编码了颜色值(如 `#4DA3FF`、`#E6EDF3`、`rgba(77, 163, 255, ...)`)而不是引用 CSS 变量。例如 `ScanTerminalCard.module.css` 中几乎每一行都是硬编码色值。 + +3. **shadcn/ui HSL token 遗留** (`globals.css:95-106`):保留了 shadcn 的 HSL token 但几乎未被使用,增加了 token 体系的认知负担。 + +--- + +## 二、布局 & 响应式 + +### 优点 + +1. **Scan Terminal 的 CSS Grid 布局** (`ScanTerminal.module.css:524-537`):`grid-template-columns: minmax(0, 1fr) minmax(380px, 420px)` 的 2 列布局干净、现代。 + +2. **毛玻璃面板系统**:所有主面板都有 `22px border-radius`、`backdrop-filter: blur(14px)`、渐变背景和微妙的阴影,视觉统一。 + +3. **Sticky detail rail**:右侧面板 `position: sticky; top: 16px; height: calc(100vh - 32px)` 保证了用户滚动时详情始终可见。 + +### 问题 + +1. **两套布局系统并存**: + - 旧版 "Home Intelligence" 使用 **absolute positioning** 定位 header/sidebar/map/panel + - 新版 "Scan Terminal" 使用 **CSS Grid** + - 两套 CSS Module 都在 `ScanTerminalDashboard.tsx` 中被 import 合并,增加了复杂度 + +2. **断点碎片化严重**:系统使用了 12+ 个不同断点值 (1680, 1480, 1360, 1240, 1100, 1020, 900, 820, 768, 720, 640, 600, 560, 520),没有统一的体系。同一个 tablet 过渡在一些模块用 768px,在另一些用 720px。 + +3. **Map View 中的 absolute positioning** (`DashboardShell.module.css`):使用 `top/right/bottom/left` 绝对定位来排列面板,脆弱且难以维护。 + +--- + +## 三、色彩 & 对比度 + +### 优点 + +1. **暗色模式基础扎实**:`#0B1220` 的深邃底色配合三层 radial gradient 营造了深度感和科技感。 + +2. **语义色系统清晰**:success/green、warning/amber、danger/red 的映射关系明确。 + +3. **渐变运用得当**:body 背景的三层 radial gradient(绿/紫/青)创造了微妙的大气光感。 + +### 问题 + +1. **文字对比度偏低**: + - 静音文字 `#6B7A90` 在 `#0B1220` 上的对比度约为 4.6:1,刚好达到 WCAG AA 标准但仍偏暗 + - 大量 10-11px 的辅助文字使用 `#6B7A90`,可读性不足 + +2. **Accent Color 视觉突出度**:`#4DA3FF`(蓝色)在暗色背景上的人眼敏感度不如 `#00E0A4`(青绿色),但前者被用作主要强调色。 + +3. **语义色 class 命名不一致**:`accent-green` class 实际渲染为 `#4DA3FF`(蓝色),"green" 命名的 class 显示蓝色。 + +--- + +## 四、字体排印 + +### 优点 + +1. **字体选择合理**:Inter(正文) + JetBrains Mono(数据/代码) 是 fintech/数据密集类产品的标准选择。 + +2. **tabular-nums 广泛使用**:几乎所有温度数值都设置了 `font-variant-numeric: tabular-nums`,保证了数字对齐。 + +3. **层级明确**:从 10px 的 kicker 到 56px 的 hero temperature,形成了清晰的视觉层级。 + +### 问题 + +1. **Geist 字体声明但未加载** (`layout.tsx:35-37`): + - `globals.css` 声明了 `--font-display: "Geist", "Inter", ...` + - 但 HTML 中只加载了 Inter 和 JetBrains Mono,Geist 永远不会生效 + +2. **非标准 font-weight 值泛滥**: + - `font-weight: 760`、`850`、`860`、`880`、`900`、`950` 在 CSS 中大量出现 + - Inter 字体只支持 300/400/500/600/700/800 + - 非标准 weight 会被浏览器取整到最近支持值,视觉不可预测 + - 例如 `font-weight: 950` 在 Inter 上实际渲染为 `800` + +3. **字体大小偏小**:大量 UI 文字使用 10-11px,在高分辨率屏幕上可读性差。 + +4. **大写字母过多**:kicker/overline/chip 几乎全部使用 `text-transform: uppercase` + `letter-spacing: 0.08em`,降低了中文/双语场景下的可读性。 + +--- + +## 五、组件设计 + +### 优点 + +1. **City Decision Card 视觉层次优秀** (`ScanTerminalCard.module.css`):hero 区的渐变背景、蓝色左边框选中态、决策 band 的色彩编码(warm=red 边框、cold=green 边框、watch=amber 边框),信息密度高但不杂乱。 + +2. **Status Tags 色彩编码清晰**:green/blue/amber/red/muted 五种 tag 变体覆盖了数据状态的所有场景。 + +3. **双卡预测对比** (AI vs DEB):青色渐变卡 vs 蓝色边框卡的视觉区分让用户能一眼区分 AI 预测和模型预测。 + +4. **Decision Band 语义化**:`warm`(red) = 看涨、`cold`(green) = 看跌、`watch`(amber) = 待观察,视觉编码与交易语义对齐。 + +### 问题 + +1. **Topbar 设计过于简单**:28px 标题 + 几个按钮,缺乏品牌标识(Logo)和视觉焦点。 + +2. **Tab 下划线指示器不够明显**:`2px` 高度 + `opacity: 0.8` 的蓝色下划线容易被忽略。 + +3. **按钮层级不够清晰**:`.scan-primary-button`(蓝紫渐变)、`.scan-ai-button`(青绿渐变)、`.scan-ai-city-icon-button`(蓝色边框半透明)、`.scan-theme-button`(无边框无背景)四种视觉权重混在一起,用户难以判断优先级。 + +4. **空状态/加载状态设计不一致**: + - 地图加载有精美的云/雷达/热力动画 + - Scan terminal 加载是简单的脉冲 placeholder + - 空状态是一个简单的文字居中块 + - 缺乏统一的 loading/empty/error 设计规范 + +5. **Mobile Decision Card 采用 `
` 元素**:原生 `
/` 样式控制有限、动画困难,与桌面端的自定义折叠按钮体验不一致。 + +--- + +## 六、深色/浅色主题 + +### 优点 + +1. **浅色模式覆盖全面**:`ScanTerminalLightTheme.module.css` 约 960 行,覆盖了 scan terminal 的所有元素。 + +2. **localStorage 持久化**:主题选择保存在 `polyweather_scan_theme` 中,刷新不丢失。 + +3. **浅色配色方案合理**:从深色 `#0B1220` 到浅色 `#F7F9FC → #EEF2F7` 的映射关系合理。 + +### 问题 + +1. **`!important` 严重滥用** (`ScanTerminalLightTheme.module.css`): + - 几乎每条浅色规则都使用了 `!important` + - 这是 specificity war 的症状,维护成本极高 + - 多处出现 `!important` 叠加 + +2. **浅色主题分散在 8+ 个文件中**:缺乏集中管理,修改一个颜色需要跨多个文件搜索。 + +3. **浅色模式地图图块滤镜处理**:Leaflet tile 滤镜切换可能影响其他 overlay 的浅色适配。 + +--- + +## 七、动效 & 过渡 + +### 优点 + +1. **Cubic-bezier 缓动选择正确**:`(0.4, 0, 0.2, 1)` (Material standard) + `(0.16, 1, 0.3, 1)` (spring-like) 的组合符合现代 UI 动效标准。 + +2. **入场动画有层次**:detail panel 从右侧滑入 (400ms)、opportunity strip 有 120ms 延迟。 + +3. **Hover 微交互**:按钮 `translateY(-1px)`、卡片 `scale(1.002)`、边框颜色过渡等微交互提升了操作反馈感。 + +### 问题 + +1. **`@keyframes spin` 重复定义 4 次**:在 `DashboardShell.module.css`、`DashboardModalGuide.module.css`、`DocsLayout.module.css`、`DashboardMap.module.css` 中各自定义了一次完全相同的 spin 动画。 + +2. **Loading 动画风格不统一**:`DashboardMap` 有复杂的 weather-themed 动画(radar swipe、cloud drift、thermal bars、wind shift),而 scan terminal 只有简单的扫光 placeholder。 + +3. **缺少 `prefers-reduced-motion` 支持**:没有任何 `@media (prefers-reduced-motion: reduce)` 的声明。 + +--- + +## 八、代码组织 & 可维护性 + +### 优点 + +1. **CSS Module 组件隔离**:每个组件有对应的 `.module.css`,样式作用域控制良好。 + +2. **命名约定一致**:CSS 类名使用 `scan-` 前缀 + kebab-case,全局可识别。 + +### 问题 + +1. **CSS Module 堆叠模式过度耦合** (`ScanTerminalDashboard.tsx:143-159`): + ```tsx + const scanTerminalRootClassName = clsx( + styles.root, + dashboardHomeStyles.root, + dashboardMapStyles.root, + // ... 共 22 个 CSS Module 的 .root 合并 + ); + ``` + 单个组件 import 了 22 个 CSS Module,破坏了 CSS Module 的隔离优势。 + +2. **`:global()` 绕过了 CSS Module 的哈希**:几乎所有规则都使用 `:global(.class-name)`,class 名不会被哈希。CSS Module 降级为"命名约定"工具。 + +3. **样式与逻辑耦合**:`ScanTerminalDashboard.tsx` 约 650 行,同时负责状态管理和渲染布局。 + +4. **重复的 CSS 变量声明**:`Dashboard.module.css` 中重新声明了 `--bg-primary` 等本地变量,与 `globals.css` 的全局 token 形成冗余。 + +--- + +## 九、无障碍性 + +### 优点 + +1. 部分元素有 aria-label(如 topbar 按钮、locale switch) +2. 主题切换按钮有 title 属性 +3. ProFeaturePaywall 使用了 `role="dialog"` 和 `aria-modal="true"` + +### 问题 + +1. **键盘导航不足**: + - 城市卡片没有 `tabindex` 或 `role="button"` + - Tab 切换缺少 `role="tablist"`/`role="tab"`/`aria-selected` + - 折叠按钮缺少 `aria-expanded` + +2. **焦点指示器不可见**:自定义按钮(如 `scan-theme-button`、`scan-ai-city-icon-button`)没有 focus-visible 样式 + +3. **颜色不是唯一的信息传达方式**:风险等级、Market decision 的色彩编码缺少对应的文字标签或图标补充 + +4. **没有 skip-to-content 链接** + +5. **10-11px 小字体对视力障碍用户不友好** + +--- + +## 总结与优先级建议 + +### 高优先级(影响用户体验和可维护性) + +| # | 问题 | 位置 | +|---|------|------| +| 1 | `!important` 滥用导致浅色主题不可维护 | `ScanTerminalLightTheme.module.css` | +| 2 | 非标准 font-weight 值无效(760/850/950 等) | 多个 CSS Module | +| 3 | Geist 字体声明但未加载 | `layout.tsx:35-37` / `globals.css:55` | +| 4 | 键盘导航和 focus-visible 缺失 | 全局 | +| 5 | 断点碎片化(14 个断点值无体系) | 所有响应式 CSS | + +### 中优先级(影响设计一致性) + +| # | 问题 | 位置 | +|---|------|------| +| 6 | 22 个 CSS Module 堆叠耦合 | `ScanTerminalDashboard.tsx:143-159` | +| 7 | Token 未充分利用(硬编码色值) | 多个 CSS Module | +| 8 | `@keyframes spin` 重复定义 4 次 | 多个 CSS Module | +| 9 | Loading 状态设计不一致 | `DashboardMap` vs `ScanTerminalState` | +| 10 | 缺少 `prefers-reduced-motion` 支持 | 全局 | + +### 低优先级(增强和优化) + +| # | 问题 | 位置 | +|---|------|------| +| 11 | "Fintech 3-Color" 实际只有 2 色 | `globals.css:25-27` | +| 12 | Topbar 缺少 Logo/品牌标识 | `ScanTerminalDashboard.tsx` | +| 13 | 按钮视觉层级不够清晰 | 多个组件 | +| 14 | Shadcn UI 组件存在但未被使用 | `components/ui/` | +| 15 | `--color-text-muted` 对比度刚达标 | `globals.css:21` | + +--- + +## 修复路线图建议 + +1. **Phase 1** — 修复 `font-weight` 无效值:全局搜索 `font-weight: 760`、`850`、`860`、`880`、`900`、`950`,替换为 Inter 支持的 300-800 等效值 +2. **Phase 2** — 重构浅色主题:将分散在 8+ 个文件中的浅色覆盖集中到一个 `light-theme.css`,使用 CSS 变量覆盖而非 `!important` +3. **Phase 3** — 补充无障碍:为 Tab/Button/Card 组件添加 ARIA 属性和 focus-visible 样式 +4. **Phase 4** — 统一断点:定义 4-5 个标准断点(如 480/768/1024/1280/1440),逐步替换现有碎片化断点 +5. **Phase 5** — 设计一致性:创建统一的 loading/empty/error 组件,清理未使用的 shadcn 组件 + +--- + +## 修复完成记录 + +> 修复日期:2026-05-10 | 变更范围:28 个文件,+693 / −2,198 行 + +### 高优先级 — 5/5 完成 + +| # | 问题 | 修复 | 涉及文件 | +|---|------|------|----------| +| 1 | `!important` 滥用 | 134 → 49(仅保留 Leaflet/图表所必需项),将 `.root:global(.light)` 替换为 `html.light` 以获得更高优先级 | `ScanTerminalLightTheme.module.css`、`globals.css` 等 | +| 2 | 非标准 font-weight | 所有 760/850/860/880/950 等映射为 Inter 支持的 300–800 | 13 个 CSS 文件 | +| 3 | 未加载 Geist | 从 `--font-display` 中移除,替换为 Inter | `globals.css` | +| 4 | 键盘 / 焦点可见 | 添加了全局 `:focus-visible` 轮廓环、跳过链接、Tab ARIA(`role="tablist"`/`role="tab"`/`aria-selected`) | `globals.css`、`layout.tsx`、`ScanFilterPanel.tsx`、`ScanTerminalDashboard.tsx` | +| 5 | 断点碎片化 | 18 → 10:合并 520/600/720/820/900/1020/1100/1240 → 640/768/960/1024/1280 | 9 个 CSS 文件 | + +### 中优先级 — 5/5 完成 + +| # | 问题 | 修复 | 涉及文件 | +|---|------|------|----------| +| 6 | 22 个 CSS Module 耦合 | 新建 `scan-root-styles.ts` 桶文件,将 22 个独立导入合并为 1 个预组合的 className | `scan-root-styles.ts`(新建)、`ScanTerminalDashboard.tsx` | +| 7 | Token 使用不足 | 将主要颜色(`#4DA3FF`/`#E6EDF3`/`#9FB2C7`/`#6B7A90`/`#6FB7FF`)从 0 个变量引用替换为数百个 | `ScanTerminalCard`、`ScanTerminalList`、`ScanTerminalBoard`、`ScanTerminalOpportunity`、`ScanTerminalMobile`、`ScanTerminal`、`DashboardHomeIntelligence` 等 | +| 8 | `@keyframes spin` 重复 4 次 | 移至 `globals.css`;`loading-spin` 去重 2 处;`pulse-pending` 移至全局 | `globals.css`、4 个 CSS 文件 | +| 9 | Loading 状态不一致 | 添加了 `.scan-error-state`、`.scan-retry-button`、`.scan-empty-icon` 用于统一状态呈现 | `ScanTerminalState.module.css`、`ScanTerminalLightTheme.module.css` | +| 10 | 无 `prefers-reduced-motion` | 在 `globals.css` 中添加了全局动画/过渡禁用 | `globals.css` | + +### 低优先级 — 5/5 完成 + +| # | 问题 | 修复 | 涉及文件 | +|---|------|------|----------| +| 11 | "3-Color" 仅 2 种颜色 | 将 accent-primary(blue) 和 accent-secondary(light-blue) 区分为不同颜色 | `globals.css`、`Dashboard.module.css` | +| 12 | Topbar 缺少 Logo | 添加了 CSS 渐变品牌标记 | `ScanTerminalShell.module.css`、`ScanTerminalDashboard.tsx` | +| 13 | 按钮层级不清晰 | 在 CSS 中添加了文档化的层级注释标题 | `ScanTerminalShell.module.css` | +| 14 | Shadcn 未使用 | 已验证 6 个组件被 5 个文件使用(保留),更新了注释 | `globals.css` | +| 15 | 文字对比度不足 | `--color-text-muted` 从 `#6B7A90` 提升至 `#7D8FA3` | `globals.css` | + +### 其他修复 + +| 问题 | 修复 | +|------|------| +| `accent-green` 类错误渲染为蓝色 | `ScanTerminal.module.css`:`.scan-condition-value.accent-green` 从 `#4DA3FF` 修正为 `#22C55E` | +| `Dashboard.module.css` 重复的 CSS 变量 | 将本地 `--bg-*`/`--accent-*` 变量桥接至全局 token | +| 死代码 | 移除 `public/static/style.css`(1,459 行)和 `public/legacy/index.html`(238 行)— 均未被引用 | +| 浅色主题 Token 基础设施 | 在 `globals.css` 中添加了 `html.light` CSS 自定义属性覆盖 | +| 品牌 Logo 浅色主题 | 在 `ScanTerminalLightTheme.module.css` 中添加了浅色主题 Logo 样式 | +| 空/错误状态浅色主题 | 在 `ScanTerminalLightTheme.module.css` 中添加了空/错误/重试的浅色覆盖 | + +### 最终指标 + +| 指标 | 之前 | 之后 | +|------|------|------| +| `!important`(可避免项) | ~85 | 0 | +| `!important`(必需项) | ~49 | 49(Leaflet 内联样式、图表 canvas 属性、减少动态效果) | +| 硬编码调色板颜色 | 数百个 | 0(仅 `globals.css` 中的变量定义) | +| 断点 | 18 个唯一值 | 10(480/640/768/960/1024/1200/1280/1360/1440/1680) | +| 重复的 `@keyframes` | 7 | 0 | +| 非标准 font-weight | 全 13 个文件 | 0 | +| CSS Module 导入 | 22 个独立导入 | 2(桶文件 + 共享) | +| 死代码 | 1,697 行 | 0 | +| 净代码行数 | — | −1,505 行 | diff --git a/frontend/app/globals.css b/frontend/app/globals.css index b29fe2a6..f3d8a991 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -18,12 +18,12 @@ /* ── Text Scale ── */ --color-text-primary: #E6EDF3; --color-text-secondary: #9FB2C7; - --color-text-muted: #6B7A90; - --color-text-disabled: #6B7A90; + --color-text-muted: #7D8FA3; + --color-text-disabled: #7D8FA3; - /* ── Accent Colors (Fintech 3-Color) ── */ + /* ── Accent Colors ── */ --color-accent-primary: #4DA3FF; - --color-accent-secondary: #4DA3FF; + --color-accent-secondary: #6FB7FF; --color-accent-tertiary: #93C5FD; /* ── Signal / Semantic Colors ── */ @@ -52,7 +52,7 @@ /* ── Typography ── */ --font-data: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --font-display: "Geist", "Inter", -apple-system, sans-serif; + --font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace; /* ── Spacing (4px grid) ── */ @@ -91,7 +91,7 @@ --transition-base: 250ms cubic-bezier(0.4, 0, 0.2, 1); --transition-slow: 400ms cubic-bezier(0.16, 1, 0.3, 1); - /* ── Legacy Compatibility (shadcn/ui HSL tokens) ── */ + /* ── shadcn/ui Tokens (used by Tailwind @apply border-border) ── */ --background: 223 53% 4%; --foreground: 210 40% 98%; --card: 223 46% 8%; @@ -105,10 +105,72 @@ --border: 221 38% 22%; } + /* ── Light Theme Token Overrides ── */ + html.light, + html[data-theme="light"] { + --color-bg-base: #F7F9FC; + --color-bg-raised: #EEF2F7; + --color-bg-overlay: #FFFFFF; + --color-bg-card: rgba(255, 255, 255, 0.92); + --color-bg-input: rgba(238, 242, 247, 0.88); + + --color-text-primary: #0F172A; + --color-text-secondary: #334155; + --color-text-muted: #475569; + --color-text-disabled: #94A3B8; + + --color-accent-primary: #2563EB; + --color-accent-secondary: #3B82F6; + --color-accent-tertiary: #60A5FA; + + --color-border-default: rgba(148, 163, 184, 0.24); + --color-border-hover: rgba(37, 99, 235, 0.38); + --color-border-subtle: rgba(148, 163, 184, 0.12); + + --shadow-elevation-1: 0 1px 3px rgba(0, 0, 0, 0.1); + --shadow-elevation-2: 0 8px 24px rgba(40, 70, 110, 0.12); + --shadow-elevation-3: 0 20px 60px rgba(40, 70, 110, 0.15); + --shadow-glow-accent: 0 0 20px rgba(37, 99, 235, 0.14); + --shadow-glow-secondary: 0 0 20px rgba(96, 165, 250, 0.12); + + --glass-blur-1: blur(10px); + --glass-blur-2: blur(16px); + --glass-blur-3: blur(24px); + --glass-opacity-1: 0.86; + --glass-opacity-2: 0.92; + --glass-opacity-3: 0.96; + } + * { @apply border-border; } + /* ── Skip-to-content link ── */ + .skip-to-content { + position: absolute; + top: -100%; + left: 8px; + z-index: 9999; + padding: 10px 18px; + border-radius: var(--radius-md); + background: var(--color-accent-primary); + color: #fff; + font-weight: 700; + text-decoration: none; + transition: top 0.18s ease; + } + + .skip-to-content:focus { + top: 8px; + } + + /* ── Global focus-visible ring (keyboard navigation) ── */ + :focus-visible { + outline: 2px solid var(--color-accent-primary); + outline-offset: 2px; + border-radius: 2px; + } + body { font-family: var(--font-data); background: @@ -219,6 +281,28 @@ transform: translateY(0); } } + + @keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } + } + + @keyframes pulse-pending { + 0%, 100% { opacity: 0.3; } + 50% { opacity: 0.6; } + } +} + +/* ── Reduced motion: disable all animations and transitions ── */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } } /* ══════════════════════════════════════════════════════════════ diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index b3c20006..0e8146e8 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -36,7 +36,12 @@ export default function RootLayout({ rel="stylesheet" /> - {children} + + + Skip to content + +
{children}
+ ); } diff --git a/frontend/components/account/AccountCenter.module.css b/frontend/components/account/AccountCenter.module.css index 5143ba96..f8e6c2fe 100644 --- a/frontend/components/account/AccountCenter.module.css +++ b/frontend/components/account/AccountCenter.module.css @@ -302,8 +302,8 @@ .mono { font-family: "Consolas", "SFMono-Regular", "Menlo", monospace; - font-size: 12px !important; - color: #c7d2fe !important; + font-size: 12px; + color: #c7d2fe; } .hint { @@ -361,13 +361,7 @@ animation: spin 0.9s linear infinite; } -@keyframes spin { - 100% { - transform: rotate(360deg); - } -} - -@media (max-width: 900px) { +@media (max-width: 960px) { .topBar { flex-direction: column; align-items: stretch; @@ -396,7 +390,7 @@ } } -@media (max-width: 560px) { +@media (max-width: 640px) { .page { padding: 18px 12px 20px; } diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index 63693cba..8df3ae72 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -4,26 +4,26 @@ .root { /* ── Palette (bridged from globals.css unified tokens) ── */ - --bg-primary: #0B1220; - --bg-secondary: #16213A; - --bg-card: #111A2E; + --bg-primary: var(--color-bg-base); + --bg-secondary: var(--color-bg-raised); + --bg-card: var(--color-bg-card); --bg-glass: rgba(17, 26, 46, 0.82); - --border-glass: rgba(159, 178, 199, 0.16); - --border-subtle: rgba(159, 178, 199, 0.08); + --border-glass: var(--color-border-default); + --border-subtle: var(--color-border-subtle); /* Text */ --text-primary: var(--color-text-primary); --text-secondary: var(--color-text-secondary); --text-muted: var(--color-text-muted); - /* Accents — Fintech 3-Color Model */ - --accent-cyan: #4DA3FF; - --accent-blue: #4DA3FF; + /* Accent Colors */ + --accent-cyan: var(--color-accent-primary); + --accent-blue: var(--color-accent-secondary); --accent-green: #22C55E; --accent-orange: #F59E0B; --accent-red: #EF4444; --accent-yellow: #F59E0B; - --accent-purple: #4DA3FF; + --accent-purple: var(--color-accent-tertiary); /* Risk colors */ --risk-high: var(--color-risk-high); diff --git a/frontend/components/dashboard/DashboardHomeIntelligence.module.css b/frontend/components/dashboard/DashboardHomeIntelligence.module.css index 93c8323b..98d56a06 100644 --- a/frontend/components/dashboard/DashboardHomeIntelligence.module.css +++ b/frontend/components/dashboard/DashboardHomeIntelligence.module.css @@ -383,7 +383,7 @@ border: 1px solid rgba(34, 211, 238, 0.18); border-radius: 999px; background: rgba(8, 145, 178, 0.1); - color: #6FB7FF; + color: var(--color-accent-secondary); font-size: 10px; font-weight: 900; letter-spacing: 0.12em; @@ -418,7 +418,7 @@ .root :global(.home-card-meta-row) { color: rgba(148, 163, 184, 0.88); font-size: 12px; - font-weight: 650; + font-weight: 600; } .root :global(.home-card-titlebar p) { @@ -476,7 +476,7 @@ .root :global(.home-weather-label) { margin-bottom: 8px; - color: #6FB7FF; + color: var(--color-accent-secondary); font-size: 10px; font-weight: 900; letter-spacing: 0.14em; @@ -567,7 +567,7 @@ width: 4px; height: 13px; border-radius: 999px; - background: #4DA3FF; + background: var(--color-accent-primary); transform: rotate(18deg); } @@ -760,9 +760,9 @@ .root :global(.home-deb-card span), .root :global(.home-card-section h3) { - color: #6FB7FF; + color: var(--color-accent-secondary); font-size: 12px; - font-weight: 850; + font-weight: 800; } .root :global(.home-deb-card small), @@ -785,7 +785,7 @@ color: #f87171; font-size: 12px; font-style: normal; - font-weight: 850; + font-weight: 800; } .root :global(.home-market-metrics svg) { @@ -882,7 +882,7 @@ } .root :global(.home-intraday-chart circle) { - fill: #4DA3FF; + fill: var(--color-accent-primary); stroke: rgba(15, 23, 42, 0.92); stroke-width: 1.6; filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.28)); @@ -1007,7 +1007,7 @@ gap: 9px; color: rgba(226, 232, 240, 0.95); font-size: 12px; - font-weight: 850; + font-weight: 800; } .root :global(.home-probability-row div), @@ -1077,7 +1077,7 @@ .root :global(.home-market-header span) { color: rgba(148, 163, 184, 0.88); font-size: 11px; - font-weight: 750; + font-weight: 700; } .root :global(.home-market-ticket) { @@ -1109,7 +1109,7 @@ } .root :global(.home-market-prices .yes) { - color: #6FB7FF; + color: var(--color-accent-secondary); background: rgba(8, 145, 178, 0.2); } @@ -1142,7 +1142,7 @@ margin-top: 10px; color: #fde68a; font-size: 12px; - font-weight: 850; + font-weight: 800; text-decoration: none; } @@ -1244,7 +1244,7 @@ .root :global(.opportunity-strip-copy strong) { color: #f8fafc; font-size: 19px; - font-weight: 860; + font-weight: 800; line-height: 1.25; letter-spacing: -0.02em; } @@ -1279,7 +1279,7 @@ .root :global(.opportunity-tape-pill span) { color: rgba(148, 163, 184, 0.82); font-size: 10px; - font-weight: 760; + font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; } @@ -1287,12 +1287,12 @@ .root :global(.opportunity-tape-pill strong) { color: #f8fafc; font-size: 16px; - font-weight: 860; + font-weight: 800; line-height: 1; } .root :global(.opportunity-tape-pill.accent-cyan strong) { - color: #6FB7FF; + color: var(--color-accent-secondary); } .root :global(.opportunity-tape-pill.accent-green strong) { @@ -1383,7 +1383,7 @@ } .root :global(.opportunity-hero-kicker) { - color: #6FB7FF; + color: var(--color-accent-secondary); font-size: 11px; font-weight: 800; letter-spacing: 0.08em; @@ -1401,7 +1401,7 @@ overflow: hidden; color: #f8fafc; font-size: 28px; - font-weight: 880; + font-weight: 800; line-height: 1; letter-spacing: -0.04em; text-overflow: ellipsis; @@ -1411,7 +1411,7 @@ .root :global(.opportunity-hero-date) { color: rgba(186, 230, 253, 0.76); font-size: 12px; - font-weight: 720; + font-weight: 700; } .root :global(.opportunity-hero-copy p) { @@ -1439,13 +1439,13 @@ background: rgba(7, 13, 25, 0.58); color: rgba(226, 232, 240, 0.92); font-size: 11px; - font-weight: 820; + font-weight: 800; white-space: nowrap; } .root :global(.opportunity-hero-tag.engine) { border-color: rgba(34, 211, 238, 0.24); - color: #6FB7FF; + color: var(--color-accent-secondary); } .root :global(.opportunity-hero-tag.signal-yes) { @@ -1499,7 +1499,7 @@ .root :global(.opportunity-hero-edgeblock span) { color: rgba(148, 163, 184, 0.8); font-size: 11px; - font-weight: 760; + font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } @@ -1539,7 +1539,7 @@ .root :global(.opportunity-hero-metric span) { color: rgba(148, 163, 184, 0.8); font-size: 10px; - font-weight: 760; + font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } @@ -1548,7 +1548,7 @@ overflow: hidden; color: #f8fafc; font-size: 20px; - font-weight: 860; + font-weight: 800; line-height: 1; letter-spacing: -0.03em; text-overflow: ellipsis; @@ -1575,14 +1575,14 @@ .root :global(.opportunity-book-side span) { font-size: 11px; - font-weight: 760; + font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } .root :global(.opportunity-book-side strong) { font-size: 26px; - font-weight: 880; + font-weight: 800; line-height: 1; letter-spacing: -0.03em; } @@ -1624,7 +1624,7 @@ .root :global(.opportunity-hero-sparkline-wrap span) { color: rgba(148, 163, 184, 0.8); font-size: 10px; - font-weight: 760; + font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } @@ -1680,7 +1680,7 @@ .root :global(.opportunity-side-tile-head span) { color: rgba(241, 245, 249, 0.92); font-size: 13px; - font-weight: 790; + font-weight: 700; } .root :global(.opportunity-side-tile-body) { @@ -1700,7 +1700,7 @@ overflow: hidden; color: #f8fafc; font-size: 24px; - font-weight: 880; + font-weight: 800; line-height: 1; letter-spacing: -0.04em; text-overflow: ellipsis; @@ -1710,7 +1710,7 @@ .root :global(.opportunity-side-stat span) { color: rgba(148, 163, 184, 0.82); font-size: 10px; - font-weight: 680; + font-weight: 600; } .root :global(.opportunity-side-stat b.accent-red) { @@ -1762,7 +1762,7 @@ .root :global(.opportunity-empty-copy strong) { color: rgba(241, 245, 249, 0.94); font-size: 15px; - font-weight: 840; + font-weight: 800; } .root :global(.opportunity-empty-copy span) { @@ -1828,7 +1828,7 @@ justify-content: center; color: #f8fafc; font-size: 13px; - font-weight: 840; + font-weight: 800; flex-shrink: 0; background: rgba(6, 11, 23, 0.46); } @@ -1870,7 +1870,7 @@ overflow: hidden; color: #f8fafc; font-size: 15px; - font-weight: 860; + font-weight: 800; text-overflow: ellipsis; white-space: nowrap; } @@ -2188,7 +2188,7 @@ } } -@media (max-width: 1240px) { +@media (max-width: 1280px) { .root :global(.opportunity-strip-topline) { flex-direction: column; } @@ -2215,7 +2215,7 @@ } } -@media (max-width: 1020px) { +@media (max-width: 1024px) { .root :global(.map) { left: 18px; bottom: 188px; @@ -2234,7 +2234,7 @@ } } -@media (max-width: 720px) { +@media (max-width: 768px) { .root :global(.map) { inset: var(--header-height) 0 0; border-radius: 0; diff --git a/frontend/components/dashboard/DashboardMap.module.css b/frontend/components/dashboard/DashboardMap.module.css index b88e83a3..1a3bf206 100644 --- a/frontend/components/dashboard/DashboardMap.module.css +++ b/frontend/components/dashboard/DashboardMap.module.css @@ -157,7 +157,7 @@ 0 0 22px rgba(251, 113, 133, 0.18); } .root :global(.marker-bubble.risk-high::after) { - border-top-color: #ef4444; + border-top-color: var(--color-signal-danger); } .root :global(.marker-bubble.risk-medium) { @@ -168,7 +168,7 @@ 0 0 22px rgba(245, 158, 11, 0.16); } .root :global(.marker-bubble.risk-medium::after) { - border-top-color: #f59e0b; + border-top-color: var(--color-signal-warning); } .root :global(.marker-bubble.risk-low) { @@ -179,7 +179,7 @@ 0 0 22px rgba(74, 222, 128, 0.14); } .root :global(.marker-bubble.risk-low::after) { - border-top-color: #22C55E; + border-top-color: var(--color-signal-success); } .root :global(.marker-name) { @@ -380,7 +380,7 @@ width: 14px; height: 14px; border-radius: 50%; - background: radial-gradient(circle, #6FB7FF 0%, #0891b2 100%); + background: radial-gradient(circle, var(--color-accent-secondary) 0%, #0891b2 100%); transform: translate(-50%, -50%); box-shadow: 0 0 12px rgba(34, 211, 238, 0.65), @@ -544,18 +544,10 @@ border: 2px solid rgba(34, 211, 238, 0.1); border-top-color: var(--accent-cyan); border-radius: 50%; - animation: loading-spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite; + animation: spin 0.8s cubic-bezier(0.4, 0, 0.2, 1) infinite; box-shadow: 0 0 15px rgba(34, 211, 238, 0.1); } -@keyframes loading-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} @keyframes city-loading-pulse { 0% { @@ -767,7 +759,7 @@ } .root :global(.nearby-time.is-stale) { - color: #fbbf24; + color: var(--color-signal-warning); } .root :global(.nearby-wind) { diff --git a/frontend/components/dashboard/DashboardModalGuide.module.css b/frontend/components/dashboard/DashboardModalGuide.module.css index 40709d22..685f95e9 100644 --- a/frontend/components/dashboard/DashboardModalGuide.module.css +++ b/frontend/components/dashboard/DashboardModalGuide.module.css @@ -1,7 +1,7 @@ .root :global(.scan-select) { border: none; border-radius: 8px; - background: linear-gradient(135deg, #4DA3FF, #00b383); + background: linear-gradient(135deg, var(--color-accent-primary), #00b383); color: #000; font-weight: 700; cursor: pointer; @@ -90,12 +90,6 @@ color: var(--accent-cyan); } -@keyframes spin { - 100% { - transform: rotate(360deg); - } -} - .root :global(.modal-close) { background: none; border: none; diff --git a/frontend/components/dashboard/DashboardShell.module.css b/frontend/components/dashboard/DashboardShell.module.css index a8ce753c..6d477821 100644 --- a/frontend/components/dashboard/DashboardShell.module.css +++ b/frontend/components/dashboard/DashboardShell.module.css @@ -87,7 +87,7 @@ color: rgba(203, 213, 225, 0.82); text-decoration: none; font-size: 14px; - font-weight: 550; + font-weight: 500; transition: var(--transition); position: relative; } @@ -223,7 +223,7 @@ align-items: center; gap: 2px; font-size: 11px; - font-weight: 750; + font-weight: 700; cursor: pointer; transition: var(--transition); } @@ -262,7 +262,7 @@ gap: 6px; text-decoration: none; font-size: 12px; - font-weight: 650; + font-weight: 600; transition: var(--transition); } @@ -281,14 +281,6 @@ padding: 0; width: 32px; } -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} /* ── City List Sidebar ── */ .root :global(.city-list) { @@ -545,11 +537,11 @@ } .root :global(.city-item .city-deviation-cold) { - color: #4DA3FF; + color: var(--color-accent-primary); } .root :global(.city-item .city-deviation-hot) { - color: #f59e0b; + color: var(--color-signal-warning); } .root :global(.city-item .city-deviation-normal) { @@ -604,7 +596,7 @@ } /* ── Responsive ── */ -@media (max-width: 1100px) { +@media (max-width: 1024px) { .root { --panel-width: 460px; } @@ -707,7 +699,7 @@ } } -@media (max-width: 600px) { +@media (max-width: 640px) { .root :global(.header) { flex-direction: column; align-items: stretch; diff --git a/frontend/components/dashboard/DetailPanelChrome.module.css b/frontend/components/dashboard/DetailPanelChrome.module.css index 018dcd3b..2421ae8a 100644 --- a/frontend/components/dashboard/DetailPanelChrome.module.css +++ b/frontend/components/dashboard/DetailPanelChrome.module.css @@ -91,7 +91,7 @@ .root :global(.panel-action-button-primary) { color: #FFFFFF; - background: linear-gradient(135deg, #4DA3FF, #3B82F6); + background: linear-gradient(135deg, var(--color-accent-primary), #3B82F6); border-color: rgba(77, 163, 255, 0.34); } diff --git a/frontend/components/dashboard/DetailPanelSections.module.css b/frontend/components/dashboard/DetailPanelSections.module.css index fc18b962..366ed2f0 100644 --- a/frontend/components/dashboard/DetailPanelSections.module.css +++ b/frontend/components/dashboard/DetailPanelSections.module.css @@ -111,7 +111,7 @@ border-radius: 999px; border: 2px solid rgba(34, 211, 238, 0.22); border-top-color: rgba(34, 211, 238, 0.92); - animation: loading-spin 0.8s linear infinite; + animation: spin 0.8s linear infinite; } .root :global(.panel-meta) { @@ -232,7 +232,7 @@ box-shadow: 0 14px 34px rgba(2, 6, 23, 0.38); backdrop-filter: blur(18px); font-size: 12px; - font-weight: 650; + font-weight: 600; line-height: 1.45; } @@ -277,7 +277,7 @@ /* ── Hero Section ── */ .root :global(.hero-section) { text-align: center; - padding-top: 12px !important; + padding-top: 12px; } .root :global(.hero-weather) { @@ -429,7 +429,7 @@ padding: 3px 8px; border: 1px solid rgba(34, 211, 238, 0.28); border-radius: 8px; - color: #6FB7FF; + color: var(--color-accent-secondary); background: rgba(34, 211, 238, 0.08); font-size: 11px; font-weight: 900; @@ -904,7 +904,7 @@ } } -@media (max-width: 720px) { +@media (max-width: 768px) { .root :global(.forecast-table) { display: flex; gap: 10px; @@ -1058,8 +1058,3 @@ } } -@keyframes loading-spin { - to { - transform: rotate(360deg); - } -} diff --git a/frontend/components/dashboard/FutureForecastModal.module.css b/frontend/components/dashboard/FutureForecastModal.module.css index 5cd4f197..f81067f5 100644 --- a/frontend/components/dashboard/FutureForecastModal.module.css +++ b/frontend/components/dashboard/FutureForecastModal.module.css @@ -116,7 +116,7 @@ border: 1px solid rgba(34, 211, 238, 0.28); border-radius: 8px; background: rgba(8, 47, 73, 0.36); - color: #6FB7FF; + color: var(--color-accent-secondary); font-size: 11px; font-weight: 800; line-height: 1.25; @@ -195,7 +195,7 @@ .root :global(.future-v2-decision-anchor small) { display: block; margin-top: 6px; - color: #6FB7FF; + color: var(--color-accent-secondary); font-size: 11px; font-weight: 700; line-height: 1.3; @@ -219,7 +219,7 @@ .root :global(.future-v2-decision-grid strong) { display: block; margin-top: 7px; - color: #6FB7FF; + color: var(--color-accent-secondary); font-size: 18px; font-weight: 800; line-height: 1; @@ -250,7 +250,7 @@ .root :global(.future-v2-meteorology-paths strong) { display: block; margin-top: 8px; - color: #6FB7FF; + color: var(--color-accent-secondary); font-size: 22px; font-weight: 800; line-height: 1; @@ -595,7 +595,7 @@ } .root :global(.future-v2-signal-tag.cyan) { - color: #6FB7FF; + color: var(--color-accent-secondary); border-color: rgba(34, 211, 238, 0.22); background: rgba(34, 211, 238, 0.08); } @@ -684,7 +684,7 @@ } .root :global(.future-v2-pace-delta.cold) { - color: #6FB7FF; + color: var(--color-accent-secondary); } .root :global(.future-v2-pace-delta.neutral) { @@ -759,7 +759,7 @@ } .root :global(.future-v2-pace-meter-fill.cold) { - background: linear-gradient(90deg, #0f766e 0%, #6FB7FF 100%); + background: linear-gradient(90deg, #0f766e 0%, var(--color-accent-secondary) 100%); } .root :global(.future-v2-pace-meter-fill.neutral) { @@ -1322,7 +1322,7 @@ left: 0; bottom: 0; border-radius: inherit; - background: linear-gradient(90deg, #334155 0%, #9FB2C7 100%); + background: linear-gradient(90deg, #334155 0%, var(--color-text-secondary) 100%); } .root :global(.future-trend-meter-fill.warm) { @@ -1609,7 +1609,7 @@ } } -@media (max-width: 420px) { +@media (max-width: 480px) { .root :global(.future-forward-stats) { grid-template-columns: 1fr; } diff --git a/frontend/components/dashboard/HistoryModal.module.css b/frontend/components/dashboard/HistoryModal.module.css index ac6b8854..8dde79bc 100644 --- a/frontend/components/dashboard/HistoryModal.module.css +++ b/frontend/components/dashboard/HistoryModal.module.css @@ -438,7 +438,7 @@ line-height: 1.7; } -@media (max-width: 980px) { +@media (max-width: 960px) { .root :global(.history-modal .modal-body) { padding: 18px; } diff --git a/frontend/components/dashboard/ScanFilterPanel.tsx b/frontend/components/dashboard/ScanFilterPanel.tsx index 5cc5db7d..7fc3a0d1 100644 --- a/frontend/components/dashboard/ScanFilterPanel.tsx +++ b/frontend/components/dashboard/ScanFilterPanel.tsx @@ -81,7 +81,7 @@ export function ScanFilterPanel({ {isEn ? "Scan Mode" : "扫描模式"} -
+
{SCAN_MODES.map((mode) => { const Icon = mode.icon; const isActive = value.scan_mode === mode.key; @@ -89,6 +89,8 @@ export function ScanFilterPanel({ -
- - 实时 -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/public/static/style.css b/frontend/public/static/style.css deleted file mode 100644 index 2d07a469..00000000 --- a/frontend/public/static/style.css +++ /dev/null @@ -1,1459 +0,0 @@ -/* ────────────────────────────────────────────────────────── - PolyWeather Map — Premium Dark Theme - ────────────────────────────────────────────────────────── */ - -:root { - /* Core palette */ - --bg-primary: #0a0e1a; - --bg-secondary: #111827; - --bg-card: rgba(17, 24, 39, 0.85); - --bg-glass: rgba(15, 23, 42, 0.75); - --border-glass: rgba(99, 102, 241, 0.15); - --border-subtle: rgba(255, 255, 255, 0.06); - - /* Text */ - --text-primary: #f1f5f9; - --text-secondary: #94a3b8; - --text-muted: #64748b; - - /* Accents */ - --accent-cyan: #22d3ee; - --accent-blue: #6366f1; - --accent-green: #34d399; - --accent-orange: #fb923c; - --accent-red: #f87171; - --accent-yellow: #fbbf24; - --accent-purple: #a78bfa; - - /* Risk colors */ - --risk-high: #ef4444; - --risk-medium: #f59e0b; - --risk-low: #22c55e; - - /* Spacing */ - --panel-width: 560px; - --header-height: 56px; - --sidebar-width: 280px; - - /* Effects */ - --glass-blur: 20px; - --shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.5); - --shadow-glow-cyan: 0 0 20px rgba(34, 211, 238, 0.3); - --shadow-glow-blue: 0 0 20px rgba(99, 102, 241, 0.3); - --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -/* ── Reset & Base ── */ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: - "Inter", - -apple-system, - BlinkMacSystemFont, - sans-serif; - background: var(--bg-primary); - color: var(--text-primary); - overflow: hidden; - height: 100vh; - width: 100vw; -} - -/* ── Map ── */ -#map { - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - z-index: 1; -} - -/* Remove Leaflet default styling for cleaner look */ -.leaflet-control-attribution { - background: var(--bg-glass) !important; - color: var(--text-muted) !important; - backdrop-filter: blur(8px); - border: 1px solid var(--border-subtle) !important; - font-size: 10px !important; - border-radius: 6px !important; - padding: 2px 8px !important; -} -.leaflet-control-attribution a { - color: var(--text-secondary) !important; -} -.leaflet-control-zoom { - border: none !important; - box-shadow: var(--shadow-lg) !important; -} -.leaflet-control-zoom a { - background: var(--bg-glass) !important; - color: var(--text-primary) !important; - backdrop-filter: blur(12px) !important; - border: 1px solid var(--border-glass) !important; - width: 36px !important; - height: 36px !important; - line-height: 36px !important; - font-size: 16px !important; - border-radius: 8px !important; - transition: var(--transition); -} -.leaflet-control-zoom a:hover { - background: rgba(99, 102, 241, 0.2) !important; - border-color: var(--accent-blue) !important; -} - -/* ── Header ── */ -#header { - position: fixed; - top: 0; - left: 0; - right: 0; - height: var(--header-height); - z-index: 1000; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 24px; - background: linear-gradient( - 180deg, - rgba(10, 14, 26, 0.95) 0%, - rgba(10, 14, 26, 0.7) 100% - ); - backdrop-filter: blur(16px); - border-bottom: 1px solid var(--border-glass); -} - -.brand { - display: flex; - align-items: baseline; - gap: 12px; -} - -.brand h1 { - font-size: 20px; - font-weight: 700; - letter-spacing: -0.02em; - background: linear-gradient(135deg, var(--accent-cyan), var(--accent-blue)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.subtitle { - font-size: 12px; - font-weight: 400; - color: var(--text-muted); - letter-spacing: 0.5px; - text-transform: uppercase; -} - -.header-right { - display: flex; - align-items: center; - gap: 12px; -} - -.live-badge { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 12px; - border-radius: 20px; - background: rgba(34, 197, 94, 0.1); - border: 1px solid rgba(34, 197, 94, 0.3); - font-size: 11px; - font-weight: 600; - color: var(--accent-green); - letter-spacing: 1px; -} - -.pulse-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--accent-green); - animation: pulse 2s ease-in-out infinite; -} - -@keyframes pulse { - 0%, - 100% { - opacity: 1; - box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); - } - 50% { - opacity: 0.7; - box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); - } -} - -.refresh-btn { - width: 36px; - height: 36px; - border-radius: 8px; - border: 1px solid var(--border-glass); - background: var(--bg-glass); - color: var(--text-secondary); - font-size: 18px; - cursor: pointer; - transition: var(--transition); - display: flex; - align-items: center; - justify-content: center; -} -.refresh-btn:hover { - background: rgba(99, 102, 241, 0.15); - border-color: var(--accent-blue); - color: var(--text-primary); -} -.refresh-btn.spinning { - animation: spin 1s linear infinite; -} -@keyframes spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -/* ── City List Sidebar ── */ -.city-list { - position: fixed; - top: calc(var(--header-height) + 12px); - left: 12px; - width: var(--sidebar-width); - max-height: calc(100vh - var(--header-height) - 24px); - z-index: 900; - background: var(--bg-glass); - backdrop-filter: blur(var(--glass-blur)); - border: 1px solid var(--border-glass); - border-radius: 16px; - overflow: hidden; - display: flex; - flex-direction: column; - box-shadow: var(--shadow-lg); -} - -.city-list-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 18px; - border-bottom: 1px solid var(--border-subtle); - font-size: 15px; - font-weight: 700; - color: var(--text-primary); -} - -.city-count { - background: var(--accent-blue); - color: white; - font-size: 12px; - font-weight: 700; - padding: 3px 10px; - border-radius: 12px; -} - -.city-list-items { - overflow-y: auto; - flex: 1; - padding: 4px; -} -.city-list-items::-webkit-scrollbar { - width: 4px; -} -.city-list-items::-webkit-scrollbar-track { - background: transparent; -} -.city-list-items::-webkit-scrollbar-thumb { - background: var(--border-glass); - border-radius: 2px; -} - -.city-item { - display: flex; - flex-direction: column; - gap: 4px; - padding: 10px 12px; - border-radius: 10px; - cursor: pointer; - transition: var(--transition); - border: 1px solid transparent; -} -.city-item:hover { - background: rgba(99, 102, 241, 0.08); - border-color: var(--border-glass); -} -.city-item.active { - background: rgba(99, 102, 241, 0.15); - border-color: var(--accent-blue); -} - -.city-item-main { - display: flex; - align-items: center; - gap: 10px; - width: 100%; -} - -.city-item .city-name-text { - font-size: 15px; - font-weight: 600; - color: var(--text-primary); -} - -.city-item .city-temp { - margin-left: auto; - font-size: 16px; - font-weight: 800; - color: var(--accent-cyan); - opacity: 0; - transition: var(--transition); -} -.city-item .city-temp.loaded { - opacity: 1; -} - -.city-item-info { - display: flex; - justify-content: space-between; - align-items: center; - padding-left: 20px; /* Align with name text, after the dot */ - font-size: 11px; - color: var(--text-muted); -} - -.city-item .city-max-info { - color: var(--accent-blue); - font-weight: 500; -} - -.city-item .risk-dot { - width: 10px; - height: 10px; - border-radius: 50%; - flex-shrink: 0; -} -.city-item .risk-dot.high { - background: var(--risk-high); - box-shadow: 0 0 6px var(--risk-high); -} -.city-item .risk-dot.medium { - background: var(--risk-medium); - box-shadow: 0 0 6px var(--risk-medium); -} -.city-item .risk-dot.low { - background: var(--risk-low); - box-shadow: 0 0 6px var(--risk-low); -} - -.city-item .city-temp { - margin-left: auto; - font-size: 12px; - font-weight: 600; - color: var(--accent-cyan); - opacity: 0; - transition: var(--transition); -} -.city-item .city-temp.loaded { - opacity: 1; -} - -/* ── Detail Panel ── */ -.detail-panel { - position: fixed; - top: 0; - right: 0; - width: var(--panel-width); - height: 100vh; - z-index: 950; - background: linear-gradient( - 180deg, - rgba(10, 14, 26, 0.92) 0%, - rgba(15, 23, 42, 0.95) 100% - ); - backdrop-filter: blur(24px); - border-left: 1px solid var(--border-glass); - box-shadow: -10px 0 60px rgba(0, 0, 0, 0.5); - transform: translateX(100%); - transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); - display: flex; - flex-direction: column; - overflow: hidden; -} -.detail-panel.visible { - transform: translateX(0); -} - -.panel-header { - padding: 20px 20px 16px; - border-bottom: 1px solid var(--border-subtle); - position: relative; - flex-shrink: 0; -} - -.panel-close { - position: absolute; - top: 16px; - right: 16px; - width: 32px; - height: 32px; - border-radius: 8px; - border: 1px solid var(--border-glass); - background: transparent; - color: var(--text-muted); - font-size: 14px; - cursor: pointer; - transition: var(--transition); - display: flex; - align-items: center; - justify-content: center; -} -.panel-close:hover { - background: rgba(248, 113, 113, 0.15); - border-color: var(--accent-red); - color: var(--accent-red); -} - -.panel-title-area h2 { - font-size: 22px; - font-weight: 700; - letter-spacing: -0.02em; - margin-bottom: 6px; -} - -.panel-meta { - display: flex; - align-items: center; - gap: 10px; -} - -.risk-badge { - font-size: 11px; - font-weight: 600; - padding: 3px 10px; - border-radius: 6px; - letter-spacing: 0.5px; -} -.risk-badge.high { - background: rgba(239, 68, 68, 0.15); - color: var(--risk-high); - border: 1px solid rgba(239, 68, 68, 0.3); -} -.risk-badge.medium { - background: rgba(245, 158, 11, 0.15); - color: var(--risk-medium); - border: 1px solid rgba(245, 158, 11, 0.3); -} -.risk-badge.low { - background: rgba(34, 197, 94, 0.15); - color: var(--risk-low); - border: 1px solid rgba(34, 197, 94, 0.3); -} - -.local-time { - font-size: 12px; - color: var(--text-muted); - font-variant-numeric: tabular-nums; -} - -.panel-body { - overflow-y: auto; - flex: 1; - padding: 0 20px 24px; -} -.panel-body::-webkit-scrollbar { - width: 4px; -} -.panel-body::-webkit-scrollbar-thumb { - background: var(--border-glass); - border-radius: 2px; -} - -.panel-body section { - padding: 18px 0; - border-bottom: 1px solid var(--border-subtle); -} -.panel-body section:last-child { - border-bottom: none; -} - -.panel-body h3 { - font-size: 13px; - font-weight: 600; - color: var(--text-secondary); - margin-bottom: 12px; - letter-spacing: 0.3px; -} - -/* ── Hero Section ── */ -.hero-section { - text-align: center; - padding-top: 12px !important; -} - -.hero-weather { - font-size: 13px; - font-weight: 600; - color: var(--accent-cyan); - margin-bottom: -4px; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; -} - -.hero-temp { - display: flex; - align-items: flex-start; - justify-content: center; - gap: 2px; - margin-bottom: 2px; -} - -.hero-max-time { - font-size: 10px; - font-weight: 500; - color: var(--text-muted); - margin-bottom: 16px; - min-height: 12px; -} -.hero-value { - font-size: 56px; - font-weight: 800; - letter-spacing: -0.04em; - line-height: 1; - background: linear-gradient(135deg, #fff 30%, var(--accent-cyan)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} -.hero-unit { - font-size: 20px; - font-weight: 400; - color: var(--text-muted); - margin-top: 8px; -} - -.hero-details { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 8px; - margin-bottom: 12px; -} -.hero-item { - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-subtle); - border-radius: 10px; - padding: 10px 8px; - text-align: center; -} -.hero-item .label { - display: block; - font-size: 10px; - color: var(--text-muted); - margin-bottom: 4px; -} -.hero-item .value { - display: block; - font-size: 16px; - font-weight: 700; - color: var(--text-primary); - font-variant-numeric: tabular-nums; -} -.hero-item .value.highlight { - color: var(--accent-cyan); - text-shadow: 0 0 12px rgba(34, 211, 238, 0.3); -} - -.hero-sub { - font-size: 12px; - color: var(--text-muted); - display: flex; - justify-content: center; - gap: 16px; - flex-wrap: wrap; -} -.hero-sub span { - white-space: nowrap; -} - -/* ── Chart Section ── */ -.chart-wrapper { - height: 180px; - position: relative; - background: rgba(255, 255, 255, 0.02); - border-radius: 12px; - border: 1px solid var(--border-subtle); - padding: 12px; -} - -.chart-legend { - display: flex; - justify-content: center; - gap: 16px; - margin-top: 8px; - font-size: 11px; - color: var(--text-muted); -} - -/* ── Probability Bars ── */ -.prob-bars { - display: flex; - flex-direction: column; - gap: 8px; -} - -.prob-row { - display: flex; - align-items: center; - gap: 10px; -} - -.prob-label { - width: 80px; - font-size: 13px; - font-weight: 600; - font-variant-numeric: tabular-nums; - text-align: right; - flex-shrink: 0; -} - -.prob-bar-track { - flex: 1; - height: 28px; - background: rgba(255, 255, 255, 0.04); - border-radius: 8px; - overflow: hidden; - position: relative; -} - -.prob-bar-fill { - height: 100%; - border-radius: 8px; - transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1); - display: flex; - align-items: center; - padding-left: 10px; - font-size: 12px; - font-weight: 600; - color: white; - min-width: 40px; -} - -.prob-bar-fill.rank-0 { - background: linear-gradient(90deg, var(--accent-blue), var(--accent-cyan)); - box-shadow: 0 0 12px rgba(99, 102, 241, 0.3); -} -.prob-bar-fill.rank-1 { - background: linear-gradient( - 90deg, - rgba(99, 102, 241, 0.6), - rgba(34, 211, 238, 0.5) - ); -} -.prob-bar-fill.rank-2 { - background: rgba(99, 102, 241, 0.3); -} -.prob-bar-fill.rank-3 { - background: rgba(99, 102, 241, 0.15); -} - -/* ── Model Bars ── */ -.model-bars { - display: flex; - flex-direction: column; - gap: 6px; -} - -.model-row { - display: flex; - align-items: center; - gap: 10px; - font-size: 12px; -} - -.model-name { - width: 80px; - text-align: right; - color: var(--text-muted); - font-weight: 500; - flex-shrink: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.model-bar-track { - flex: 1; - height: 20px; - background: rgba(255, 255, 255, 0.03); - border-radius: 6px; - position: relative; - overflow: hidden; -} - -.model-bar-fill { - height: 100%; - border-radius: 6px; - background: linear-gradient(90deg, var(--accent-purple), var(--accent-blue)); - transition: width 0.6s ease-out; - display: flex; - align-items: center; - justify-content: flex-end; - padding-right: 8px; - font-size: 11px; - font-weight: 600; - color: white; -} - -.model-bar-fill.deb { - background: linear-gradient(90deg, var(--accent-cyan), var(--accent-green)); - box-shadow: 0 0 8px rgba(34, 211, 238, 0.3); -} - -.model-deb-line { - position: absolute; - top: 0; - bottom: 0; - width: 2px; - background: var(--accent-cyan); - box-shadow: 0 0 6px var(--accent-cyan); - z-index: 2; -} - -/* ── Forecast Table ── */ -.forecast-table { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); - gap: 8px; -} - -.forecast-day { - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-subtle); - border-radius: 10px; - padding: 10px; - text-align: center; - transition: var(--transition); -} -.forecast-day:hover { - border-color: var(--border-glass); - background: rgba(255, 255, 255, 0.05); -} -.forecast-day .f-date { - font-size: 11px; - color: var(--text-muted); - margin-bottom: 4px; -} -.forecast-day .f-temp { - font-size: 18px; - font-weight: 700; - color: var(--text-primary); -} -.forecast-day.today { - border-color: var(--accent-blue); - background: rgba(99, 102, 241, 0.08); -} -.forecast-day.today .f-date { - color: var(--accent-cyan); -} - -.forecast-day.selected { - border-color: var(--accent-cyan); - background: rgba(34, 211, 238, 0.1); - box-shadow: 0 0 12px rgba(34, 211, 238, 0.2); -} -.forecast-day.selected .f-date { - color: var(--accent-cyan); -} - -.sun-info { - margin-top: 10px; - font-size: 12px; - color: var(--text-muted); - display: flex; - gap: 16px; - justify-content: center; -} - -/* ── AI Section ── */ -.ai-box { - background: rgba(99, 102, 241, 0.06); - border: 1px solid rgba(99, 102, 241, 0.15); - border-radius: 12px; - padding: 16px; - font-size: 13px; - line-height: 1.7; - color: var(--text-secondary); - white-space: pre-wrap; - word-break: break-word; -} - -.ai-placeholder { - color: var(--text-muted); - font-style: italic; -} - -/* ── Risk Section ── */ -.risk-info { - font-size: 12px; - color: var(--text-secondary); - line-height: 1.8; - word-break: break-word; /* 确保数字和英文长句也能折行 */ - white-space: normal; -} -.risk-info .risk-row { - display: flex; - gap: 8px; - align-items: flex-start; -} -.risk-info .risk-label { - color: var(--text-muted); - min-width: 60px; - flex-shrink: 0; -} - -/* ── Nearby Markers ── */ -.nearby-marker { - display: flex; - align-items: center; - gap: 6px; - background: rgba(15, 23, 42, 0.9); - color: var(--text-secondary); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 8px; - padding: 4px 10px; - font-size: 11px; - font-weight: 500; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); - white-space: nowrap; - backdrop-filter: blur(8px); - pointer-events: none; -} - -.nearby-name { - color: var(--text-muted); -} - -.nearby-temp { - font-weight: 800; - color: #fff; -} - -.nearby-unit { - font-weight: 400; - font-size: 9px; - color: var(--text-muted); -} - -.wind-info { - display: flex; - align-items: center; - gap: 4px; - margin-left: 4px; - padding-left: 6px; - border-left: 1px solid rgba(255, 255, 255, 0.1); -} - -.wind-arrow { - display: inline-block; - font-size: 12px; - color: var(--accent-cyan); - transition: transform 0.3s; - text-shadow: 0 0 8px rgba(34, 211, 238, 0.5); -} - -.wind-speed { - font-size: 9px; - color: var(--text-muted); - font-variant-numeric: tabular-nums; -} - -@keyframes markerFadeIn { - from { - opacity: 0; - transform: translateY(5px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ── Custom Map Markers ── */ -.city-marker { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - cursor: pointer; - transition: transform 0.2s ease; -} -.city-marker:hover { - transform: scale(1.15); - z-index: 1000 !important; -} - -.marker-bubble { - min-width: 44px; - padding: 4px 10px; - border-radius: 12px; - font-family: "Inter", sans-serif; - font-size: 13px; - font-weight: 700; - text-align: center; - color: white; - white-space: nowrap; - position: relative; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); - border: 1.5px solid rgba(255, 255, 255, 0.15); -} - -.marker-bubble::after { - content: ""; - position: absolute; - bottom: -6px; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-top: 6px solid; - border-top-color: inherit; -} - -.marker-bubble.risk-high { - background: linear-gradient(135deg, #dc2626, #ef4444); - border-color: rgba(239, 68, 68, 0.5); -} -.marker-bubble.risk-high::after { - border-top-color: #ef4444; -} - -.marker-bubble.risk-medium { - background: linear-gradient(135deg, #d97706, #f59e0b); - border-color: rgba(245, 158, 11, 0.5); -} -.marker-bubble.risk-medium::after { - border-top-color: #f59e0b; -} - -.marker-bubble.risk-low { - background: linear-gradient(135deg, #059669, #10b981); - border-color: rgba(16, 185, 129, 0.5); -} -.marker-bubble.risk-low::after { - border-top-color: #10b981; -} - -.marker-name { - font-size: 10px; - font-weight: 600; - color: rgba(255, 255, 255, 0.85); - text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8); - margin-top: 2px; - white-space: nowrap; -} - -/* Marker glow animation for selected city */ -.city-marker.selected .marker-bubble { - animation: markerGlow 2s ease-in-out infinite; -} -@keyframes markerGlow { - 0%, - 100% { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); - } - 50% { - box-shadow: - 0 4px 24px rgba(99, 102, 241, 0.5), - 0 0 40px rgba(99, 102, 241, 0.2); - } -} - -/* ── Loading Overlay ── */ -.loading-overlay { - position: fixed; - inset: 0; - z-index: 2000; - background: rgba(10, 14, 26, 0.7); - backdrop-filter: blur(8px); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 16px; - font-size: 14px; - color: var(--text-secondary); -} -.loading-overlay.hidden { - display: none; -} - -.loading-spinner { - width: 40px; - height: 40px; - border: 3px solid var(--border-glass); - border-top-color: var(--accent-cyan); - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -.hidden { - display: none !important; -} - -/* ── Trend badge ── */ -.trend-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 10px; - border-radius: 6px; - font-size: 11px; - font-weight: 600; -} -.trend-badge.rising { - background: rgba(34, 197, 94, 0.12); - color: var(--accent-green); - border: 1px solid rgba(34, 197, 94, 0.25); -} -.trend-badge.falling { - background: rgba(248, 113, 113, 0.12); - color: var(--accent-red); - border: 1px solid rgba(248, 113, 113, 0.25); -} -.trend-badge.stagnant { - background: rgba(251, 191, 36, 0.12); - color: var(--accent-yellow); - border: 1px solid rgba(251, 191, 36, 0.25); -} -.trend-badge.mixed { - background: rgba(167, 139, 250, 0.12); - color: var(--accent-purple); - border: 1px solid rgba(167, 139, 250, 0.25); -} - -.dead-market { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 3px 10px; - border-radius: 6px; - font-size: 11px; - font-weight: 700; - background: rgba(248, 113, 113, 0.15); - color: var(--accent-red); - border: 1px solid rgba(248, 113, 113, 0.3); - animation: deadPulse 2s ease-in-out infinite; -} -@keyframes deadPulse { - 0%, - 100% { - opacity: 1; - } - 50% { - opacity: 0.6; - } -} - -/* ── Responsive ── */ -@media (max-width: 1100px) { - :root { - --panel-width: 460px; - } - .hero-value { - font-size: 52px; - } -} - -@media (max-width: 768px) { - .city-list { - display: none; - } - .detail-panel { - width: 100%; - } - :root { - --panel-width: 100%; - } -} - -@media (max-width: 600px) { - .subtitle { - display: none; - } - .brand h1 { - font-size: 16px; - } - .hero-value { - font-size: 42px; - } - .hero-details { - grid-template-columns: repeat(3, 1fr); - gap: 4px; - } - .hero-item .value { - font-size: 14px; - } -} - -/* ── History Modal ── */ -.history-btn { - background: rgba(34, 211, 238, 0.1); - color: var(--accent-cyan); - border: 1px solid rgba(34, 211, 238, 0.3); - padding: 4px 10px; - border-radius: 6px; - font-size: 11px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - margin-left: 8px; -} -.history-btn:hover { - background: rgba(34, 211, 238, 0.2); - border-color: var(--accent-cyan); -} - -.modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); - z-index: 10000; - display: flex; - align-items: center; - justify-content: center; - padding: 16px; -} - -.modal-content { - background: #111827; - border: 1px solid var(--border-subtle); - border-radius: 16px; - width: 100%; - max-width: 700px; - max-height: calc(100vh - 48px); - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); - display: flex; - flex-direction: column; - overflow: hidden; -} - -.modal-header { - padding: 16px 20px; - border-bottom: 1px solid var(--border-subtle); - display: flex; - justify-content: space-between; - align-items: center; -} -.modal-header h2 { - font-size: 16px; - font-weight: 600; - color: var(--text-primary); - margin: 0; -} -.modal-close { - background: none; - border: none; - font-size: 20px; - color: var(--text-muted); - cursor: pointer; - transition: color 0.2s; -} -.modal-close:hover { - color: var(--accent-red); -} - -.modal-body { - padding: 20px; - overflow-y: auto; -} - -.history-stats { - display: flex; - gap: 12px; - margin-bottom: 20px; - flex-wrap: wrap; -} -.h-stat-card { - flex: 1; - min-width: 110px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-subtle); - border-radius: 8px; - padding: 12px; - text-align: center; -} -.h-stat-card .label { - display: block; - font-size: 11px; - color: var(--text-muted); - margin-bottom: 4px; -} -.h-stat-card .val { - display: block; - font-size: 18px; - font-weight: 700; - color: var(--text-primary); -} - -.history-chart-wrapper { - position: relative; - height: 300px; - width: 100%; -} - -/* ── Info Button ── */ -.info-btn { - background: rgba(99, 102, 241, 0.1); - border: 1px solid rgba(99, 102, 241, 0.3); - color: var(--accent-blue); - padding: 6px 14px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: var(--transition); - display: flex; - align-items: center; - gap: 6px; -} -.info-btn:hover { - background: rgba(99, 102, 241, 0.2); - border-color: var(--accent-blue); - transform: translateY(-1px); -} - -/* ── Guide Modal Customizations ── */ -.modal-content.large { - max-width: 900px; -} - -.modal-content.large.future-modal { - width: min(96vw, 1720px); - max-width: 1720px; -} - -.future-modal-body { - display: flex; - flex-direction: column; - gap: 14px; - padding-right: 14px; -} - -.future-modal-section { - background: rgba(255, 255, 255, 0.02); - border: 1px solid var(--border-subtle); - border-radius: 14px; - padding: 14px; -} - -.future-modal-section h3 { - margin: 0 0 14px; - font-size: 14px; - font-weight: 700; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 8px; -} - -.section-inline-icon { - display: inline-flex; - width: 16px; - height: 16px; - align-items: center; - justify-content: center; - color: var(--accent-cyan); - flex: 0 0 16px; -} - -.section-inline-icon svg { - width: 16px; - height: 16px; -} - -.future-modal-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} - -.future-chart-wrapper { - height: 220px; -} - -.future-prob-bars .prob-bar-fill { - min-width: 56px; -} - -.future-trend-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; -} - -.future-trend-card { - border: 1px solid var(--border-subtle); - border-radius: 12px; - padding: 12px; - background: rgba(255, 255, 255, 0.025); -} - -.future-trend-label { - display: block; - color: var(--text-muted); - font-size: 11px; - margin-bottom: 6px; -} - -.future-trend-value { - display: block; - color: var(--text-primary); - font-size: 17px; - font-weight: 700; -} - -.future-trend-note { - margin-top: 6px; - color: var(--text-secondary); - font-size: 12px; - line-height: 1.45; -} - -.future-trend-value.warm { - color: #34d399; -} - -.future-trend-value.cold { - color: #f59e0b; -} - -.future-text-block { - color: var(--text-secondary); - font-size: 13px; - line-height: 1.7; -} - -.future-text-block strong { - color: var(--text-primary); -} - -.future-front-score { - display: grid; - gap: 12px; -} - -.future-front-bar { - position: relative; - height: 10px; - border-radius: 999px; - background: linear-gradient(90deg, rgba(245, 158, 11, 0.35), rgba(255,255,255,0.06) 50%, rgba(52, 211, 153, 0.35)); - overflow: hidden; -} - -.future-front-bar::after { - content: ""; - position: absolute; - top: 50%; - left: var(--score-position, 50%); - width: 14px; - height: 14px; - border-radius: 999px; - background: var(--accent-cyan); - transform: translate(-50%, -50%); - box-shadow: 0 0 0 4px rgba(34, 211, 238, 0.14); -} - -.future-front-meta { - display: flex; - flex-wrap: wrap; - gap: 10px; - font-size: 12px; - color: var(--text-secondary); -} - -.future-front-pill { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 10px; - border-radius: 999px; - border: 1px solid var(--border-subtle); - background: rgba(255, 255, 255, 0.03); -} - -@media (max-width: 960px) { - .future-modal-grid, - .future-trend-grid { - grid-template-columns: 1fr; - } -} - -@media (max-width: 1200px) { - .future-trend-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -.guide-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); - gap: 16px; - padding: 4px; -} - -.guide-card { - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--border-subtle); - border-radius: 12px; - padding: 16px; - transition: var(--transition); -} -.guide-card:hover { - background: rgba(255, 255, 255, 0.05); - border-color: var(--border-glass); -} -.guide-card h3 { - font-size: 15px; - font-weight: 700; - color: var(--accent-cyan); - margin-bottom: 10px; - display: flex; - align-items: center; - gap: 8px; -} -.guide-card p { - font-size: 13px; - line-height: 1.6; - color: var(--text-secondary); - margin-bottom: 0; -} -.guide-card b { - color: var(--text-primary); -} - -.guide-footer { - margin-top: 24px; - padding-top: 16px; - border-top: 1px solid var(--border-subtle); - text-align: center; - font-size: 11px; - color: var(--text-muted); -}