全面修复前端 UI 设计审查问题:消除工程债务、统一 token 体系、提升可维护性
- 消除 !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
This commit is contained in:
@@ -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 采用 `<details>` 元素**:原生 `<details>/<summary>` 样式控制有限、动画困难,与桌面端的自定义折叠按钮体验不一致。
|
||||
|
||||
---
|
||||
|
||||
## 六、深色/浅色主题
|
||||
|
||||
### 优点
|
||||
|
||||
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 行 |
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ══════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -36,7 +36,12 @@ export default function RootLayout({
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body className="min-h-screen font-sans antialiased">{children}</body>
|
||||
<body className="min-h-screen font-sans antialiased">
|
||||
<a href="#main-content" className="skip-to-content">
|
||||
Skip to content
|
||||
</a>
|
||||
<main id="main-content">{children}</main>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
@media (max-width: 960px) {
|
||||
.root :global(.history-modal .modal-body) {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export function ScanFilterPanel({
|
||||
<span>{isEn ? "Scan Mode" : "扫描模式"}</span>
|
||||
<Info size={14} />
|
||||
</div>
|
||||
<div className="scan-mode-tabs">
|
||||
<div className="scan-mode-tabs" role="tablist" aria-label={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({
|
||||
<button
|
||||
key={mode.key}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
className={`scan-mode-tab ${isActive ? "active" : ""}`}
|
||||
onClick={() => updateFilter("scan_mode", mode.key)}
|
||||
>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
|
||||
.root :global(.scan-mode-tab.active) {
|
||||
background: rgba(0, 224, 164, 0.08);
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
top: 25%;
|
||||
height: 50%;
|
||||
width: 3px;
|
||||
background: #4DA3FF;
|
||||
background: var(--color-accent-primary);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #4DA3FF;
|
||||
background: var(--color-accent-primary);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 2px solid var(--bg-secondary);
|
||||
@@ -146,7 +146,7 @@
|
||||
|
||||
.root :global(.scan-toggle.active) {
|
||||
background: rgba(0, 224, 164, 0.3);
|
||||
border-color: #4DA3FF;
|
||||
border-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.scan-toggle-knob) {
|
||||
@@ -162,7 +162,7 @@
|
||||
|
||||
.root :global(.scan-toggle.active .scan-toggle-knob) {
|
||||
left: 18px;
|
||||
background: #4DA3FF;
|
||||
background: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
/* Select */
|
||||
@@ -179,7 +179,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-select:focus) {
|
||||
border-color: #4DA3FF;
|
||||
border-color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
/* Scan CTA */
|
||||
@@ -192,7 +192,7 @@
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #4DA3FF, #00c48f);
|
||||
background: linear-gradient(135deg, var(--color-accent-primary), #00c48f);
|
||||
color: #030711;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
@@ -271,7 +271,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-view-tab.active) {
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #4DA3FF;
|
||||
background: var(--color-accent-primary);
|
||||
border-radius: 4px;
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s ease;
|
||||
@@ -392,7 +392,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-condition-value.accent-green) {
|
||||
color: #4DA3FF;
|
||||
color: #22C55E;
|
||||
}
|
||||
|
||||
.root :global(.scan-condition-value.accent-red) {
|
||||
@@ -417,7 +417,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-trade-card.yes) {
|
||||
border-top: 2px solid #4DA3FF;
|
||||
border-top: 2px solid var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.scan-trade-card.no) {
|
||||
@@ -441,7 +441,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-trade-card-edge.positive) {
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.scan-trade-card-edge.negative) {
|
||||
@@ -494,7 +494,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-confidence-dot.filled) {
|
||||
background: #4DA3FF;
|
||||
background: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.scan-confidence-dot.filled.amber) {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement-copy span) {
|
||||
color: #4da3ff;
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
@@ -56,13 +56,13 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement-copy strong) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement-copy p) {
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
margin: 10px 0 0;
|
||||
color: #d6e4f5;
|
||||
font-size: 13px;
|
||||
font-weight: 760;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,17 +31,17 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-kicker) {
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 11px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-city-title strong) {
|
||||
color: #E6EDF3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 24px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
@@ -57,9 +57,9 @@
|
||||
border: 1px solid rgba(159, 178, 199, 0.14);
|
||||
border-radius: 9px;
|
||||
background: rgba(22, 33, 58, 0.56);
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -68,28 +68,28 @@
|
||||
justify-items: end;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-city-read small) {
|
||||
color: #6B7A90;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-city-read > b:not(.scan-phase-badge)) {
|
||||
color: #E6EDF3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 30px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-city-read span) {
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-row) {
|
||||
@@ -137,21 +137,21 @@
|
||||
.root :global(.scan-forecast-bucket small),
|
||||
.root :global(.scan-forecast-signals small),
|
||||
.root :global(.scan-ai-temperature-line small) {
|
||||
color: #6B7A90;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-bucket strong) {
|
||||
color: #E6EDF3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
line-height: 1.22;
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-bucket small) {
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
.root :global(.scan-forecast-signals b) {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
color: #E6EDF3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
line-height: 1.25;
|
||||
@@ -196,9 +196,9 @@
|
||||
border: 1px solid rgba(96, 165, 250, 0.28);
|
||||
border-radius: 999px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #6FB7FF;
|
||||
color: var(--color-accent-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -231,20 +231,20 @@
|
||||
gap: 10px;
|
||||
align-items: baseline;
|
||||
padding: 0 16px 14px;
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-ai-line b) {
|
||||
color: #6FB7FF;
|
||||
color: var(--color-accent-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.scan-forecast-ai-line small) {
|
||||
min-width: 0;
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 750;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -272,26 +272,26 @@
|
||||
|
||||
.root :global(.scan-ai-analysis-head strong),
|
||||
.root :global(.scan-ai-brief-grid strong) {
|
||||
color: #6FB7FF;
|
||||
color: var(--color-accent-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-analysis-head p) {
|
||||
margin: 0;
|
||||
color: #E6EDF3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 850;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-analysis-head small) {
|
||||
max-width: 880px;
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 760;
|
||||
font-weight: 700;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@@ -305,9 +305,9 @@
|
||||
border: 1px solid rgba(96, 165, 250, 0.28);
|
||||
border-radius: 999px;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #6FB7FF;
|
||||
color: var(--color-accent-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -328,9 +328,9 @@
|
||||
|
||||
.root :global(.scan-ai-temperature-line b) {
|
||||
overflow: hidden;
|
||||
color: #E6EDF3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 17px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -352,7 +352,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-evidence-line small) {
|
||||
color: #6B7A90;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -360,7 +360,7 @@
|
||||
.root :global(.scan-ai-evidence-line b) {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
color: #E6EDF3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
line-height: 1.3;
|
||||
@@ -379,7 +379,7 @@
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(77, 163, 255, 0.35);
|
||||
border-radius: 18px;
|
||||
background: #111a2e;
|
||||
background: var(--color-bg-raised);
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.24);
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
@@ -414,14 +414,14 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-kicker) {
|
||||
color: #4da3ff;
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-hero h3) {
|
||||
margin: 4px 0 8px;
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 22px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
@@ -498,9 +498,9 @@
|
||||
align-items: center;
|
||||
gap: 4px 6px;
|
||||
margin-top: 6px;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 760;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-freshness strong) {
|
||||
@@ -529,7 +529,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-freshness em) {
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@@ -583,7 +583,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-metrics small) {
|
||||
color: #6b7a90;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
@@ -591,7 +591,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-metrics b) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
line-height: 1.1;
|
||||
@@ -699,7 +699,7 @@
|
||||
align-items: center;
|
||||
border: 1px solid rgba(77, 163, 255, 0.18);
|
||||
border-radius: 16px;
|
||||
background: #16213a;
|
||||
background: var(--color-bg-overlay);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
@@ -720,7 +720,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-decision-band span) {
|
||||
color: #4da3ff;
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -728,14 +728,14 @@
|
||||
.root :global(.scan-ai-decision-band strong) {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-decision-band p) {
|
||||
margin: 8px 0 0;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@@ -745,7 +745,7 @@
|
||||
border: 1px solid rgba(77, 163, 255, 0.24);
|
||||
border-radius: 12px;
|
||||
background: rgba(77, 163, 255, 0.1);
|
||||
color: #d8e7f8 !important;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 900;
|
||||
padding: 9px 11px;
|
||||
}
|
||||
@@ -779,14 +779,14 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-decision span) {
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-decision strong) {
|
||||
margin-top: 3px;
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@@ -806,14 +806,14 @@
|
||||
gap: 3px;
|
||||
border-radius: 10px;
|
||||
background: rgba(17, 26, 46, 0.82);
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-decision-stats b) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -840,14 +840,14 @@
|
||||
border: 1px solid rgba(159, 178, 199, 0.14);
|
||||
border-radius: 12px;
|
||||
background: rgba(11, 18, 32, 0.5);
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-decision-metrics b) {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@@ -869,7 +869,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #4da3ff;
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
margin-bottom: 12px;
|
||||
@@ -908,7 +908,7 @@
|
||||
|
||||
.root :global(.scan-ai-city-section p) {
|
||||
margin: 0 0 10px;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -1006,14 +1006,10 @@
|
||||
animation: pulse-pending 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-pending {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-weather-summary) {
|
||||
color: #e6edf3;
|
||||
font-weight: 850;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-weather-bullets) {
|
||||
@@ -1021,21 +1017,21 @@
|
||||
gap: 8px;
|
||||
margin: 10px 0 0;
|
||||
padding-left: 18px;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-weather-bullets li::marker) {
|
||||
color: #4da3ff;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-raw-metar) {
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(77, 163, 255, 0.12);
|
||||
color: #6b7a90;
|
||||
color: var(--color-text-muted);
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
@@ -1073,7 +1069,7 @@
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-top: 10px;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -1147,14 +1143,14 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-bucket strong) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-bucket span),
|
||||
.root :global(.scan-ai-city-muted),
|
||||
.root :global(.scan-ai-city-loading) {
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1180,9 +1176,9 @@
|
||||
.root :global(.scan-ai-brief-grid li) {
|
||||
position: relative;
|
||||
padding-left: 16px;
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 760;
|
||||
font-weight: 700;
|
||||
line-height: 1.48;
|
||||
}
|
||||
|
||||
@@ -1193,7 +1189,7 @@
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: #4DA3FF;
|
||||
background: var(--color-accent-primary);
|
||||
box-shadow: 0 0 10px rgba(77, 163, 255, 0.34);
|
||||
content: "";
|
||||
}
|
||||
@@ -1212,7 +1208,7 @@
|
||||
|
||||
.root :global(.scan-ai-airport-read p) {
|
||||
margin: 0;
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
|
||||
@@ -18,28 +18,7 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import styles from "./Dashboard.module.css";
|
||||
import dashboardHomeStyles from "./DashboardHomeIntelligence.module.css";
|
||||
import dashboardMapStyles from "./DashboardMap.module.css";
|
||||
import dashboardModalGuideStyles from "./DashboardModalGuide.module.css";
|
||||
import dashboardShellStyles from "./DashboardShell.module.css";
|
||||
import detailChromeStyles from "./DetailPanelChrome.module.css";
|
||||
import detailContentStyles from "./DetailPanelContent.module.css";
|
||||
import detailSectionsStyles from "./DetailPanelSections.module.css";
|
||||
import futureForecastModalStyles from "./FutureForecastModal.module.css";
|
||||
import historyModalStyles from "./HistoryModal.module.css";
|
||||
import modalChromeStyles from "./ModalChrome.module.css";
|
||||
import scanTerminalCalendarStyles from "./ScanTerminalCalendar.module.css";
|
||||
import scanTerminalCardStyles from "./ScanTerminalCard.module.css";
|
||||
import scanTerminalLightThemeStyles from "./ScanTerminalLightTheme.module.css";
|
||||
import scanTerminalOpportunityStyles from "./ScanTerminalOpportunity.module.css";
|
||||
import scanTerminalStyles from "./ScanTerminal.module.css";
|
||||
import scanTerminalBoardStyles from "./ScanTerminalBoard.module.css";
|
||||
import scanTerminalDetailStyles from "./ScanTerminalDetail.module.css";
|
||||
import scanTerminalFiltersStyles from "./ScanTerminalFilters.module.css";
|
||||
import scanTerminalListStyles from "./ScanTerminalList.module.css";
|
||||
import scanTerminalShellStyles from "./ScanTerminalShell.module.css";
|
||||
import scanTerminalStateStyles from "./ScanTerminalState.module.css";
|
||||
import scanTerminalMobileStyles from "./ScanTerminalMobile.module.css";
|
||||
import { scanRootClass } from "./scan-root-styles";
|
||||
import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall";
|
||||
import {
|
||||
DashboardStoreProvider,
|
||||
@@ -133,28 +112,7 @@ function ScanTerminalScreen() {
|
||||
const lastMapSelectedCityRef = useRef<string>("");
|
||||
const scanTerminalRootClassName = clsx(
|
||||
styles.root,
|
||||
dashboardHomeStyles.root,
|
||||
dashboardMapStyles.root,
|
||||
dashboardShellStyles.root,
|
||||
dashboardModalGuideStyles.root,
|
||||
scanTerminalStyles.root,
|
||||
scanTerminalShellStyles.root,
|
||||
scanTerminalFiltersStyles.root,
|
||||
scanTerminalListStyles.root,
|
||||
scanTerminalBoardStyles.root,
|
||||
scanTerminalDetailStyles.root,
|
||||
scanTerminalStateStyles.root,
|
||||
scanTerminalOpportunityStyles.root,
|
||||
scanTerminalCardStyles.root,
|
||||
scanTerminalCalendarStyles.root,
|
||||
scanTerminalMobileStyles.root,
|
||||
scanTerminalLightThemeStyles.root,
|
||||
detailChromeStyles.root,
|
||||
detailContentStyles.root,
|
||||
detailSectionsStyles.root,
|
||||
modalChromeStyles.root,
|
||||
futureForecastModalStyles.root,
|
||||
historyModalStyles.root,
|
||||
scanRootClass,
|
||||
themeMode === "light" && "light",
|
||||
);
|
||||
|
||||
@@ -441,14 +399,17 @@ function ScanTerminalScreen() {
|
||||
>
|
||||
<main className="scan-data-grid">
|
||||
<div className="scan-topbar">
|
||||
<div className="scan-topbar-title">
|
||||
<strong>{isEn ? "AI Weather Decision Terminal" : "AI 天气交易决策台"}</strong>
|
||||
<div className="scan-topbar-brand">
|
||||
<div className="scan-topbar-logo" aria-hidden="true" />
|
||||
<div className="scan-topbar-title">
|
||||
<strong>{isEn ? "AI Weather Decision Terminal" : "AI 天气交易决策台"}</strong>
|
||||
<span>
|
||||
{isEn
|
||||
? "Start from the map, then open city cards to verify weather evidence"
|
||||
: "从地图选城市,再打开决策卡验证天气证据"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="scan-topbar-actions">
|
||||
<button
|
||||
type="button"
|
||||
@@ -525,9 +486,11 @@ function ScanTerminalScreen() {
|
||||
|
||||
<section className="scan-list-section">
|
||||
<div className="scan-list-header">
|
||||
<div className="scan-list-tabs">
|
||||
<div className="scan-list-tabs" role="tablist" aria-label={isEn ? "Content view" : "内容视图"}>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={resolvedView === "map"}
|
||||
className={resolvedView === "map" ? "active" : ""}
|
||||
onClick={() => {
|
||||
lastMapSelectedCityRef.current = normalizeCityKey(store.selectedCity);
|
||||
@@ -538,6 +501,8 @@ function ScanTerminalScreen() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={resolvedView === "analysis"}
|
||||
className={resolvedView === "analysis" ? "active" : ""}
|
||||
onClick={() => {
|
||||
setActiveView("analysis");
|
||||
@@ -547,6 +512,8 @@ function ScanTerminalScreen() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={resolvedView === "calendar"}
|
||||
className={resolvedView === "calendar" ? "active" : ""}
|
||||
title={!isPro ? (isEn ? "Pro forecast calendar required" : "日历预测需 Pro") : undefined}
|
||||
onClick={() => {
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
color: #0F172A;
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-topbar-logo) {
|
||||
background: linear-gradient(135deg, #2563EB, #059669);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-filter-panel),
|
||||
.root :global(.scan-terminal.light .scan-data-grid),
|
||||
.root :global(.scan-terminal.light .scan-detail-panel),
|
||||
@@ -80,6 +85,28 @@
|
||||
rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-empty-icon),
|
||||
.root :global(.scan-terminal.light .scan-error-icon) {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
border-color: rgba(37, 99, 235, 0.16);
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-error-icon) {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-color: rgba(239, 68, 68, 0.14);
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-retry-button) {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
border-color: rgba(37, 99, 235, 0.22);
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-retry-button:hover) {
|
||||
background: rgba(37, 99, 235, 0.14);
|
||||
border-color: rgba(37, 99, 235, 0.38);
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-loading-node) {
|
||||
background: #ffffff;
|
||||
border-color: rgba(148, 163, 184, 0.22);
|
||||
@@ -317,11 +344,11 @@
|
||||
.root :global(.scan-terminal.light .scan-v4-analysis p),
|
||||
.root :global(.scan-terminal.light .scan-v4-analysis ul),
|
||||
.root :global(.scan-terminal.light .scan-v4-evidence > div > span) {
|
||||
color: #6B7A90;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-v4-model-sources em) {
|
||||
color: #6B7A90;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-v4-model-sources b) {
|
||||
@@ -567,7 +594,7 @@
|
||||
.root :global(.scan-terminal.light .scan-ai-city-head p),
|
||||
.root :global(.scan-terminal.light .scan-ai-contract p),
|
||||
.root :global(.scan-terminal.light .scan-ai-contract small) {
|
||||
color: #6B7A90;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.root :global(.scan-terminal.light .scan-ai-city-head) {
|
||||
@@ -667,6 +694,8 @@
|
||||
|
||||
/* Light-mode hard overrides for the scan terminal. Keep this block last so the
|
||||
decision workspace cannot inherit the dark map/card palette from base rules. */
|
||||
|
||||
/* ── CSS Variable bridge (from globals.css token overrides) ── */
|
||||
.root:global(.light) {
|
||||
--bg-primary: #eef7ff;
|
||||
--bg-secondary: #e0f2fe;
|
||||
@@ -679,281 +708,313 @@
|
||||
--text-muted: #475569;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light) {
|
||||
/* ── Scan terminal shell light background ── */
|
||||
html.light .root :global(.scan-terminal.light) {
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, rgba(59, 130, 246, 0.12), transparent 30%),
|
||||
linear-gradient(180deg, #eef7ff 0%, #e8f4ff 48%, #f8fbff 100%) !important;
|
||||
color: #0f172a !important;
|
||||
linear-gradient(180deg, #eef7ff 0%, #e8f4ff 48%, #f8fbff 100%);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
:global(html.light) .root :global(.map),
|
||||
.root:global(.light) :global(.map),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-map-shell .map),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-map-shell .leaflet-container),
|
||||
.root:global(.light) :global(.leaflet-container),
|
||||
.root:global(.light) :global(.leaflet-pane),
|
||||
.root:global(.light) :global(.leaflet-map-pane),
|
||||
.root:global(.light) :global(.leaflet-tile-pane) {
|
||||
/* ── Leaflet map light overrides (needs !important to beat Leaflet inline styles) ── */
|
||||
html.light .root :global(.map),
|
||||
html.light .root :global(.scan-terminal.light .scan-map-shell .map),
|
||||
html.light .root :global(.scan-terminal.light .scan-map-shell .leaflet-container),
|
||||
html.light .root :global(.leaflet-container),
|
||||
html.light .root :global(.leaflet-pane),
|
||||
html.light .root :global(.leaflet-map-pane),
|
||||
html.light .root :global(.leaflet-tile-pane) {
|
||||
background: #eef7ff !important;
|
||||
}
|
||||
|
||||
:global(html.light) .root :global(.map),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-map-shell) {
|
||||
border-color: rgba(37, 99, 235, 0.18) !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-map-shell),
|
||||
html.light .root :global(.map) {
|
||||
border-color: rgba(37, 99, 235, 0.18);
|
||||
box-shadow:
|
||||
0 18px 40px rgba(61, 100, 145, 0.14),
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.72) !important;
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
:global(html.light) .root :global(.map .leaflet-tile),
|
||||
.root:global(.light) :global(.map .leaflet-tile),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-map-shell .leaflet-tile) {
|
||||
html.light .root :global(.map .leaflet-tile),
|
||||
html.light .root :global(.scan-terminal.light .scan-map-shell .leaflet-tile) {
|
||||
filter: none !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-city-detail-rail),
|
||||
.root:global(.light) :global(.scan-terminal.light > .detail-panel.scan-city-detail-rail) {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(238, 247, 255, 0.98), rgba(255, 255, 255, 0.96)) !important;
|
||||
border-color: rgba(37, 99, 235, 0.16) !important;
|
||||
color: #0f172a !important;
|
||||
/* ── City detail rail light overrides ── */
|
||||
html.light .root :global(.scan-city-detail-rail),
|
||||
html.light .root :global(.scan-terminal.light > .detail-panel.scan-city-detail-rail) {
|
||||
background: linear-gradient(180deg, rgba(238, 247, 255, 0.98), rgba(255, 255, 255, 0.96));
|
||||
border-color: rgba(37, 99, 235, 0.16);
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-header),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-structured-section),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-summary-shell),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-card) {
|
||||
background: rgba(255, 255, 255, 0.74) !important;
|
||||
border-color: rgba(125, 171, 214, 0.28) !important;
|
||||
html.light .root :global(.scan-city-detail-rail .panel-header),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-structured-section),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-summary-shell),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-card) {
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
border-color: rgba(125, 171, 214, 0.28);
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-city-detail-rail h2),
|
||||
.root:global(.light) :global(.scan-city-detail-rail h3),
|
||||
.root:global(.light) :global(.scan-city-detail-rail strong),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-value),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-summary-temp),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-section-head h3),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-title-area h2) {
|
||||
color: #0f172a !important;
|
||||
html.light .root :global(.scan-city-detail-rail h2),
|
||||
html.light .root :global(.scan-city-detail-rail h3),
|
||||
html.light .root :global(.scan-city-detail-rail strong),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-value),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-summary-temp),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-section-head h3),
|
||||
html.light .root :global(.scan-city-detail-rail .panel-title-area h2) {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-city-detail-rail p),
|
||||
.root:global(.light) :global(.scan-city-detail-rail li),
|
||||
.root:global(.light) :global(.scan-city-detail-rail small),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-overline),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-loading-hint),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-meta-chip),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-section-kicker),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-label),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-value-muted),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-source-note),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-source-kind),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-mini-meta),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-summary-supporting),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .scan-detail-city-sub),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .scan-detail-volume-caption),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .scan-detail-score-label),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .scan-detail-empty) {
|
||||
color: #334155 !important;
|
||||
html.light .root :global(.scan-city-detail-rail p),
|
||||
html.light .root :global(.scan-city-detail-rail li),
|
||||
html.light .root :global(.scan-city-detail-rail small),
|
||||
html.light .root :global(.scan-city-detail-rail .panel-overline),
|
||||
html.light .root :global(.scan-city-detail-rail .panel-loading-hint),
|
||||
html.light .root :global(.scan-city-detail-rail .panel-meta-chip),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-section-kicker),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-label),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-value-muted),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-source-note),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-source-kind),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-mini-meta),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-summary-supporting),
|
||||
html.light .root :global(.scan-city-detail-rail .scan-detail-city-sub),
|
||||
html.light .root :global(.scan-city-detail-rail .scan-detail-volume-caption),
|
||||
html.light .root :global(.scan-city-detail-rail .scan-detail-score-label),
|
||||
html.light .root :global(.scan-city-detail-rail .scan-detail-empty) {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-meta-chip),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-weather-chip),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .detail-source-link),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-action-button-secondary),
|
||||
.root:global(.light) :global(.scan-city-detail-rail .panel-action-button-ghost) {
|
||||
background: #eef7ff !important;
|
||||
border-color: rgba(37, 99, 235, 0.18) !important;
|
||||
color: #1f3654 !important;
|
||||
html.light .root :global(.scan-city-detail-rail .panel-meta-chip),
|
||||
html.light .root :global(.scan-city-detail-rail .panel-weather-chip),
|
||||
html.light .root :global(.scan-city-detail-rail .detail-source-link),
|
||||
html.light .root :global(.scan-city-detail-rail .panel-action-button-secondary),
|
||||
html.light .root :global(.scan-city-detail-rail .panel-action-button-ghost) {
|
||||
background: #eef7ff;
|
||||
border-color: rgba(37, 99, 235, 0.18);
|
||||
color: #1f3654;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-section),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-band),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-decision),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-metrics span),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-pills span),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-freshness span),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-metrics > span),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-mobile-decision-metrics span),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-mobile-decision-reason) {
|
||||
background: #eef7ff !important;
|
||||
border-color: rgba(37, 99, 235, 0.16) !important;
|
||||
/* ── AI city card light overrides ── */
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-card),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-section),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-decision-band),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-market-decision),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-decision-metrics span),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-pills span),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-freshness span),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span),
|
||||
html.light .root :global(.scan-terminal.light .scan-mobile-decision-metrics span),
|
||||
html.light .root :global(.scan-terminal.light .scan-mobile-decision-reason) {
|
||||
background: #eef7ff;
|
||||
border-color: rgba(37, 99, 235, 0.16);
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) {
|
||||
background: #dbeafe !important;
|
||||
border-color: rgba(59, 130, 246, 0.34) !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) {
|
||||
background: #dbeafe;
|
||||
border-color: rgba(59, 130, 246, 0.34);
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-metrics b) {
|
||||
color: #0f172a !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics b) {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) {
|
||||
color: #1d4ed8 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-metrics small) {
|
||||
color: #64748b !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-metrics small) {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card p),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card li),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card small),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-card span:not(.scan-ai-city-kicker)) {
|
||||
color: #334155 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-card p),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-card li),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-card small),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-card span:not(.scan-ai-city-kicker)) {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.green) {
|
||||
color: #047857 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.green) {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.blue) {
|
||||
color: #1d4ed8 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.blue) {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.amber) {
|
||||
color: #b45309 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.amber) {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
|
||||
color: #b91c1c !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement) {
|
||||
/* ── Upgrade announcement light overrides ── */
|
||||
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement) {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 36%),
|
||||
#ffffff !important;
|
||||
border-color: rgba(37, 99, 235, 0.16) !important;
|
||||
box-shadow: 0 16px 36px rgba(61, 100, 145, 0.12) !important;
|
||||
#ffffff;
|
||||
border-color: rgba(37, 99, 235, 0.16);
|
||||
box-shadow: 0 16px 36px rgba(61, 100, 145, 0.12);
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy strong),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority b),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-mobile-decision-metrics b),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-mobile-decision-reason),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-mobile-decision-head h3),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line b) {
|
||||
color: #0f172a !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement-copy strong),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-mobile-priority b),
|
||||
html.light .root :global(.scan-terminal.light .scan-mobile-decision-metrics b),
|
||||
html.light .root :global(.scan-terminal.light .scan-mobile-decision-reason),
|
||||
html.light .root :global(.scan-terminal.light .scan-mobile-decision-head h3),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-market-mobile-line b) {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy p),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement li),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority small),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-mobile-decision-metrics small),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line span) {
|
||||
color: #334155 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement-copy p),
|
||||
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement li),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-mobile-priority small),
|
||||
html.light .root :global(.scan-terminal.light .scan-mobile-decision-metrics small),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-market-mobile-line span) {
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy span) {
|
||||
color: #047857 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement-copy span) {
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement li),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority span),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-why),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line) {
|
||||
background: #eef7ff !important;
|
||||
border-color: rgba(37, 99, 235, 0.16) !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-upgrade-announcement li),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-mobile-priority span),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-decision-why),
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-market-mobile-line) {
|
||||
background: #eef7ff;
|
||||
border-color: rgba(37, 99, 235, 0.16);
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-why) {
|
||||
color: #1d4ed8 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-decision-why) {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
/* ── Section titles (Evidence, Model, etc.) ── */
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-section-title) {
|
||||
color: #1d4ed8 !important;
|
||||
/* ── Section titles ── */
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-section-title) {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-band span) {
|
||||
color: #1d4ed8 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-decision-band span) {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-band p) {
|
||||
color: #475569 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-decision-band p) {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-band strong) {
|
||||
color: #0f172a !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-decision-band strong) {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-body) {
|
||||
/* ── Scrollbar light overrides ── */
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-city-body) {
|
||||
scrollbar-color: rgba(37, 99, 235, 0.18) transparent;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-ai-city-body::-webkit-scrollbar-thumb) {
|
||||
background: rgba(37, 99, 235, 0.22) !important;
|
||||
html.light .root :global(.scan-ai-city-body::-webkit-scrollbar-thumb) {
|
||||
background: rgba(37, 99, 235, 0.22);
|
||||
}
|
||||
|
||||
/* ── AI prediction dual-card light mode ── */
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-dual) {
|
||||
gap: 10px;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card) {
|
||||
background: #ffffff;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card) {
|
||||
background: #ffffff !important;
|
||||
border-color: #e2e8f0 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai) {
|
||||
background: linear-gradient(180deg, rgba(219, 234, 254, 0.52), #ffffff);
|
||||
border-color: rgba(14, 165, 233, 0.28);
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card.ai) {
|
||||
background: linear-gradient(180deg, rgba(219, 234, 254, 0.52), #ffffff) !important;
|
||||
border-color: rgba(14, 165, 233, 0.28) !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.deb) {
|
||||
background: #ffffff;
|
||||
border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card.deb) {
|
||||
background: #ffffff !important;
|
||||
border-color: #e2e8f0 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card small) {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card small) {
|
||||
color: #64748b !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card strong) {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card strong) {
|
||||
color: #0f172a !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai strong) {
|
||||
color: #0369a1;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card.ai strong) {
|
||||
color: #0369a1 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card em) {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card em) {
|
||||
color: #64748b !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending) {
|
||||
background: rgba(219, 234, 254, 0.28);
|
||||
border-color: rgba(14, 165, 233, 0.16);
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending) {
|
||||
background: rgba(219, 234, 254, 0.28) !important;
|
||||
border-color: rgba(14, 165, 233, 0.16) !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending strong) {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending strong) {
|
||||
color: #94a3b8 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-confidence) {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-confidence) {
|
||||
background: rgba(59, 130, 246, 0.1) !important;
|
||||
color: #2563eb !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-confidence.high) {
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-confidence.high) {
|
||||
background: rgba(16, 185, 129, 0.12) !important;
|
||||
color: #047857 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-confidence.medium) {
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-confidence.medium) {
|
||||
background: rgba(245, 158, 11, 0.12) !important;
|
||||
color: #b45309 !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-confidence.low) {
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-confidence.low) {
|
||||
background: rgba(148, 163, 184, 0.12) !important;
|
||||
color: #64748b !important;
|
||||
html.light .root :global(.scan-terminal.light .scan-ai-confidence.neutral) {
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-confidence.neutral) {
|
||||
background: rgba(148, 163, 184, 0.08) !important;
|
||||
color: #64748b !important;
|
||||
/* ── Empty / Error / Retry light overrides ── */
|
||||
html.light .root :global(.scan-terminal.light .scan-empty-title),
|
||||
html.light .root :global(.scan-terminal.light .scan-error-title) {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
html.light .root :global(.scan-terminal.light .scan-empty-copy),
|
||||
html.light .root :global(.scan-terminal.light .scan-error-copy) {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
html.light .root :global(.scan-terminal.light .scan-empty-icon) {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
border-color: rgba(37, 99, 235, 0.2);
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
html.light .root :global(.scan-terminal.light .scan-error-icon) {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-color: rgba(239, 68, 68, 0.18);
|
||||
color: #DC2626;
|
||||
}
|
||||
|
||||
html.light .root :global(.scan-terminal.light .scan-retry-button) {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
border-color: rgba(37, 99, 235, 0.24);
|
||||
color: #2563EB;
|
||||
}
|
||||
|
||||
html.light .root :global(.scan-terminal.light .scan-retry-button:hover) {
|
||||
background: rgba(37, 99, 235, 0.14);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@
|
||||
|
||||
.root :global(.scan-v4-decision p b) {
|
||||
color: #ecfeff;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.scan-v4-metar-summary) {
|
||||
@@ -263,9 +263,9 @@
|
||||
.root :global(.scan-v4-heading strong),
|
||||
.root :global(.scan-v4-current b),
|
||||
.root :global(.scan-v4-brief-grid strong) {
|
||||
color: #6FB7FF;
|
||||
color: var(--color-accent-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
background: rgba(20, 184, 166, 0.12);
|
||||
color: #7dfbe7;
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
.root :global(.scan-v4-current span) {
|
||||
color: #c8d8ec;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
font-weight: 800;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 999px;
|
||||
background: #4DA3FF;
|
||||
background: var(--color-accent-primary);
|
||||
box-shadow: 0 0 10px rgba(45, 212, 191, 0.45);
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
background: rgba(10, 30, 58, 0.52);
|
||||
color: #8fbdff;
|
||||
font-size: 12px;
|
||||
font-weight: 950;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
@@ -425,7 +425,7 @@
|
||||
scroll-padding-top: 64px;
|
||||
border: 1px solid rgba(77, 163, 255, 0.16);
|
||||
border-radius: 18px;
|
||||
background: #0b1220;
|
||||
background: var(--color-bg-base);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@
|
||||
|
||||
.root :global(.scan-ai-workspace-head span) {
|
||||
display: block;
|
||||
color: #4da3ff;
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
@@ -462,7 +462,7 @@
|
||||
|
||||
.root :global(.scan-ai-workspace-head strong) {
|
||||
display: block;
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
margin-top: 4px;
|
||||
@@ -471,7 +471,7 @@
|
||||
.root :global(.scan-ai-workspace-head p) {
|
||||
max-width: 560px;
|
||||
margin: 0;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
text-align: right;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
.root :global(.scan-mobile-decision-head h3) {
|
||||
margin: 6px 0 0;
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 24px;
|
||||
line-height: 1.08;
|
||||
}
|
||||
@@ -38,7 +38,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-mobile-decision-metrics small) {
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -121,7 +121,7 @@
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 1480px) {
|
||||
@media (max-width: 1440px) {
|
||||
.root :global(.scan-terminal) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
@@ -141,7 +141,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
@media (max-width: 1024px) {
|
||||
.root :global(.scan-terminal) {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: 100dvh;
|
||||
@@ -281,7 +281,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
@media (max-width: 768px) {
|
||||
.root :global(.scan-terminal) {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -354,7 +354,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-mobile-priority small) {
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -394,13 +394,13 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-mobile-line span) {
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-mobile-line b) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -475,7 +475,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
@media (max-width: 640px) {
|
||||
.root :global(.scan-terminal) {
|
||||
box-sizing: border-box;
|
||||
max-width: 100vw;
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
background: rgba(0, 224, 164, 0.12);
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.rank-amber .scan-rank-circle) {
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
.root :global(.rank-purple .scan-rank-circle) {
|
||||
background: rgba(123, 97, 255, 0.12);
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.rank-neutral .scan-rank-circle) {
|
||||
@@ -168,7 +168,7 @@
|
||||
|
||||
.root :global(.scan-status-badge.tone-green) {
|
||||
background: rgba(0, 224, 164, 0.12);
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.scan-status-badge.tone-amber) {
|
||||
@@ -178,7 +178,7 @@
|
||||
|
||||
.root :global(.scan-status-badge.tone-purple) {
|
||||
background: rgba(123, 97, 255, 0.12);
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.scan-status-badge.tone-neutral) {
|
||||
@@ -233,7 +233,7 @@
|
||||
.root :global(.scan-action-text) {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-edge-value.positive) {
|
||||
color: #4DA3FF;
|
||||
color: var(--color-accent-primary);
|
||||
}
|
||||
|
||||
.root :global(.scan-edge-value.neutral) {
|
||||
@@ -447,7 +447,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-item.ai-veto .scan-opportunity-action) {
|
||||
color: #9FB2C7;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-branch) {
|
||||
@@ -702,7 +702,7 @@
|
||||
|
||||
.root :global(.scan-v4-analysis strong) {
|
||||
display: block;
|
||||
color: #6FB7FF;
|
||||
color: var(--color-accent-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.02em;
|
||||
@@ -892,11 +892,11 @@
|
||||
align-content: center;
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-overview.empty strong) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
@@ -926,7 +926,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-hero span) {
|
||||
color: #4da3ff;
|
||||
color: var(--color-accent-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -934,7 +934,7 @@
|
||||
.root :global(.scan-opportunity-hero strong) {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 26px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
@@ -942,7 +942,7 @@
|
||||
.root :global(.scan-opportunity-hero p) {
|
||||
max-width: 680px;
|
||||
margin: 9px 0 0;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -959,12 +959,12 @@
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 14px;
|
||||
background: rgba(13, 17, 23, 0.52);
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-summary b) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -990,13 +990,13 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-lane-head strong) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-lane-head p) {
|
||||
margin: 3px 0 0;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -1060,7 +1060,7 @@
|
||||
.root :global(.scan-opportunity-decision-head span),
|
||||
.root :global(.scan-opportunity-decision-primary span),
|
||||
.root :global(.scan-opportunity-decision-foot small) {
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -1068,7 +1068,7 @@
|
||||
.root :global(.scan-opportunity-decision-head strong) {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -1108,7 +1108,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-decision-primary b) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
/* Scan terminal shell, topbar, upgrade announcement, and shared action buttons.
|
||||
Extracted from ScanTerminal.module.css to keep terminal layout CSS maintainable. */
|
||||
|
||||
/* ── Button Hierarchy ──
|
||||
Primary: .scan-primary-button — filled gradient, main CTA
|
||||
Secondary: .scan-ai-button — tinted background, alternative action
|
||||
Ghost: .scan-theme-button — no border/background, icon-only
|
||||
Chrome: .scan-account-button — bordered icon, navigation
|
||||
Chrome: .scan-locale-switch — segmented control
|
||||
Chrome: .scan-ghost-button — bordered + bg, utility action
|
||||
────────────────────────────────────────────── */
|
||||
|
||||
.root :global(.scan-topbar) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -10,6 +19,32 @@
|
||||
border-bottom: 1px solid rgba(118, 146, 188, 0.1);
|
||||
}
|
||||
|
||||
.root :global(.scan-topbar-brand) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.root :global(.scan-topbar-logo) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, var(--color-accent-primary), #22C55E);
|
||||
box-shadow: 0 4px 12px rgba(77, 163, 255, 0.28);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.root :global(.scan-topbar-logo::after) {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
border-radius: 6px;
|
||||
background:
|
||||
radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.6), transparent 60%);
|
||||
}
|
||||
|
||||
.root :global(.scan-topbar-title) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -135,7 +170,7 @@
|
||||
background: rgba(77, 163, 255, 0.1);
|
||||
color: #d7e5f7;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
font-weight: 800;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
@@ -168,7 +203,7 @@
|
||||
padding: 11px 16px;
|
||||
border: 1px solid rgba(77, 163, 255, 0.42);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #4DA3FF, #3B82F6);
|
||||
background: linear-gradient(180deg, var(--color-accent-primary), #3B82F6);
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
@@ -212,7 +247,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-cta-ghost) {
|
||||
color: #6FB7FF;
|
||||
color: var(--color-accent-secondary);
|
||||
border-color: rgba(23, 217, 139, 0.3);
|
||||
background: rgba(23, 217, 139, 0.12);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Scan terminal empty, loading, and skeleton signal states. */
|
||||
/* Scan terminal empty, loading, error, and skeleton signal states. */
|
||||
|
||||
/* ── Empty state ── */
|
||||
.root :global(.scan-empty-state) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -8,6 +9,20 @@
|
||||
align-items: center;
|
||||
padding: 48px 32px;
|
||||
text-align: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-empty-icon) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 14px;
|
||||
background: rgba(77, 163, 255, 0.1);
|
||||
border: 1px solid rgba(77, 163, 255, 0.16);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--color-accent-secondary);
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.root :global(.scan-empty-title) {
|
||||
@@ -17,12 +32,71 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-empty-copy) {
|
||||
margin-top: 10px;
|
||||
max-width: 360px;
|
||||
font-size: 14px;
|
||||
color: #8fa4c3;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Error state ── */
|
||||
.root :global(.scan-error-state) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 48px 32px;
|
||||
text-align: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-error-icon) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 14px;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #FCA5A5;
|
||||
font-size: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.root :global(.scan-error-title) {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
color: #e8f2ff;
|
||||
}
|
||||
|
||||
.root :global(.scan-error-copy) {
|
||||
max-width: 360px;
|
||||
font-size: 14px;
|
||||
color: #8fa4c3;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.root :global(.scan-retry-button) {
|
||||
margin-top: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 40px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid rgba(77, 163, 255, 0.3);
|
||||
border-radius: 12px;
|
||||
background: rgba(77, 163, 255, 0.12);
|
||||
color: #9ecbff;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.root :global(.scan-retry-button:hover) {
|
||||
background: rgba(77, 163, 255, 0.18);
|
||||
border-color: rgba(111, 183, 255, 0.48);
|
||||
}
|
||||
|
||||
.root :global(.scan-loading-state) {
|
||||
flex: 1;
|
||||
min-height: 520px;
|
||||
@@ -101,7 +175,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-loading-node.market) {
|
||||
background: linear-gradient(135deg, #38bdf8, #4da3ff);
|
||||
background: linear-gradient(135deg, #38bdf8, var(--color-accent-primary));
|
||||
box-shadow: 0 0 24px rgba(56, 189, 248, 0.26);
|
||||
}
|
||||
|
||||
@@ -141,7 +215,7 @@
|
||||
}
|
||||
|
||||
.root :global(.scan-loading-copy-block strong) {
|
||||
color: #e6edf3;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.02em;
|
||||
@@ -149,9 +223,9 @@
|
||||
|
||||
.root :global(.scan-loading-copy-block span) {
|
||||
max-width: 320px;
|
||||
color: #9fb2c7;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
font-weight: 600;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@@ -168,7 +242,7 @@
|
||||
width: 7px;
|
||||
min-height: 8px;
|
||||
border-radius: 999px 999px 4px 4px;
|
||||
background: linear-gradient(180deg, #7dd3fc, #4da3ff 52%, #1be392);
|
||||
background: linear-gradient(180deg, #7dd3fc, var(--color-accent-primary) 52%, #1be392);
|
||||
opacity: 0.42;
|
||||
transform-origin: center bottom;
|
||||
animation: scan-loading-bars 1.35s ease-in-out infinite;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/* Barrel: pre-combined scan-terminal root class name.
|
||||
Consolidates 22 CSS Modules that are always co-imported into
|
||||
a single className, keeping ScanTerminalDashboard.tsx lean. */
|
||||
|
||||
import clsx from "clsx";
|
||||
|
||||
import dashboardHomeStyles from "./DashboardHomeIntelligence.module.css";
|
||||
import dashboardMapStyles from "./DashboardMap.module.css";
|
||||
import dashboardModalGuideStyles from "./DashboardModalGuide.module.css";
|
||||
import dashboardShellStyles from "./DashboardShell.module.css";
|
||||
import detailChromeStyles from "./DetailPanelChrome.module.css";
|
||||
import detailContentStyles from "./DetailPanelContent.module.css";
|
||||
import detailSectionsStyles from "./DetailPanelSections.module.css";
|
||||
import futureForecastModalStyles from "./FutureForecastModal.module.css";
|
||||
import historyModalStyles from "./HistoryModal.module.css";
|
||||
import modalChromeStyles from "./ModalChrome.module.css";
|
||||
import scanTerminalStyles from "./ScanTerminal.module.css";
|
||||
import scanTerminalBoardStyles from "./ScanTerminalBoard.module.css";
|
||||
import scanTerminalCalendarStyles from "./ScanTerminalCalendar.module.css";
|
||||
import scanTerminalCardStyles from "./ScanTerminalCard.module.css";
|
||||
import scanTerminalDetailStyles from "./ScanTerminalDetail.module.css";
|
||||
import scanTerminalFiltersStyles from "./ScanTerminalFilters.module.css";
|
||||
import scanTerminalLightThemeStyles from "./ScanTerminalLightTheme.module.css";
|
||||
import scanTerminalListStyles from "./ScanTerminalList.module.css";
|
||||
import scanTerminalMobileStyles from "./ScanTerminalMobile.module.css";
|
||||
import scanTerminalOpportunityStyles from "./ScanTerminalOpportunity.module.css";
|
||||
import scanTerminalShellStyles from "./ScanTerminalShell.module.css";
|
||||
import scanTerminalStateStyles from "./ScanTerminalState.module.css";
|
||||
|
||||
export const scanRootClass = clsx(
|
||||
dashboardHomeStyles.root,
|
||||
dashboardMapStyles.root,
|
||||
dashboardShellStyles.root,
|
||||
dashboardModalGuideStyles.root,
|
||||
scanTerminalStyles.root,
|
||||
scanTerminalShellStyles.root,
|
||||
scanTerminalFiltersStyles.root,
|
||||
scanTerminalListStyles.root,
|
||||
scanTerminalBoardStyles.root,
|
||||
scanTerminalDetailStyles.root,
|
||||
scanTerminalStateStyles.root,
|
||||
scanTerminalOpportunityStyles.root,
|
||||
scanTerminalCardStyles.root,
|
||||
scanTerminalCalendarStyles.root,
|
||||
scanTerminalMobileStyles.root,
|
||||
scanTerminalLightThemeStyles.root,
|
||||
detailChromeStyles.root,
|
||||
detailContentStyles.root,
|
||||
detailSectionsStyles.root,
|
||||
modalChromeStyles.root,
|
||||
futureForecastModalStyles.root,
|
||||
historyModalStyles.root,
|
||||
);
|
||||
@@ -542,6 +542,7 @@ export function AiPinnedCityCard({
|
||||
collapsed && !isCompactCard && "collapsed",
|
||||
removing && "removing",
|
||||
)}
|
||||
tabIndex={-1}
|
||||
data-ai-status={decisionState.aiStatus}
|
||||
data-evidence-quality={decisionState.evidenceQuality}
|
||||
data-market-status={decisionState.marketStatus}
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
.sectionTitle {
|
||||
margin: 0 0 16px;
|
||||
font-size: 1.65rem;
|
||||
font-weight: 750;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
@@ -283,7 +283,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 960px) {
|
||||
.docsHeader {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,238 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PolyWeather - 天气衍生品智能地图</title>
|
||||
<meta name="description" content="PolyWeather 天气衍生品智能地图,聚合 METAR、MGM、DEB、多模型预报与历史对账分析。">
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="/static/style.css?v=legacy-v19">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
|
||||
<header id="header">
|
||||
<div class="brand">
|
||||
<h1>PolyWeather</h1>
|
||||
<span class="subtitle">天气衍生品智能分析</span>
|
||||
</div>
|
||||
<button class="info-btn" id="btnShowGuide" title="查看系统技术说明">技术说明</button>
|
||||
<div class="live-badge" id="liveBadge">
|
||||
<span class="pulse-dot"></span>
|
||||
<span>实时</span>
|
||||
</div>
|
||||
<button class="refresh-btn" id="refreshAllBtn" title="刷新所有数据" aria-label="刷新所有数据">↻</button>
|
||||
</header>
|
||||
|
||||
<nav id="cityList" class="city-list">
|
||||
<div class="city-list-header">
|
||||
<span>监控城市</span>
|
||||
<span class="city-count" id="cityCount">0</span>
|
||||
</div>
|
||||
<div id="cityListItems" class="city-list-items">
|
||||
<!-- Dynamically populated -->
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<aside id="panel" class="detail-panel hidden">
|
||||
<div class="panel-header">
|
||||
<button class="panel-close" id="panelClose">✕</button>
|
||||
<div class="panel-title-area">
|
||||
<h2 id="panelCityName">—</h2>
|
||||
<div class="panel-meta">
|
||||
<span id="panelRiskBadge" class="risk-badge">—</span>
|
||||
<span id="panelLocalTime" class="local-time">—</span>
|
||||
<button class="history-btn" id="btnShowHistory" title="查看历史记录与准确率">历史对账</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="panelContent" class="panel-body">
|
||||
<section class="hero-section">
|
||||
<div class="hero-weather" id="heroWeather"></div>
|
||||
<div class="hero-temp">
|
||||
<span class="hero-value" id="heroTemp">—</span>
|
||||
<span class="hero-unit" id="heroUnit">°C</span>
|
||||
</div>
|
||||
<div class="hero-max-time" id="heroMaxTime"></div>
|
||||
<div class="hero-details">
|
||||
<div class="hero-item">
|
||||
<span class="label">当前实测</span>
|
||||
<span class="value" id="heroCurrent">—</span>
|
||||
</div>
|
||||
<div class="hero-item">
|
||||
<span class="label">结算参考</span>
|
||||
<span class="value highlight" id="heroWU">—</span>
|
||||
</div>
|
||||
<div class="hero-item">
|
||||
<span class="label">DEB 预测</span>
|
||||
<span class="value" id="heroDEB">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-sub" id="heroSub"></div>
|
||||
</section>
|
||||
|
||||
<section class="chart-section">
|
||||
<h3>今日温度走势</h3>
|
||||
<div class="chart-wrapper">
|
||||
<canvas id="tempChart"></canvas>
|
||||
</div>
|
||||
<div class="chart-legend" id="chartLegend"></div>
|
||||
</section>
|
||||
|
||||
<section class="prob-section">
|
||||
<h3>结算概率分布</h3>
|
||||
<div id="probBars" class="prob-bars"></div>
|
||||
</section>
|
||||
|
||||
<section class="models-section">
|
||||
<h3>多模型预报</h3>
|
||||
<div id="modelBars" class="model-bars"></div>
|
||||
</section>
|
||||
|
||||
<section class="forecast-section">
|
||||
<h3>多日预报</h3>
|
||||
<div id="forecastTable" class="forecast-table"></div>
|
||||
<div class="sun-info" id="sunInfo"></div>
|
||||
</section>
|
||||
|
||||
<section class="ai-section">
|
||||
<h3>机场报文解读</h3>
|
||||
<div id="aiAnalysis" class="ai-box">
|
||||
<span class="ai-placeholder">点击城市后加载...</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="risk-section">
|
||||
<h3>数据偏差风险</h3>
|
||||
<div id="riskInfo" class="risk-info"></div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div id="loading" class="loading-overlay hidden">
|
||||
<div class="loading-spinner"></div>
|
||||
<span>正在获取气象数据,请稍候...</span>
|
||||
</div>
|
||||
|
||||
<!-- 鈹€鈹€ Technical Guide Modal 鈹€鈹€ -->
|
||||
<div id="guideModal" class="modal-overlay hidden">
|
||||
<div class="modal-content large">
|
||||
<div class="modal-header">
|
||||
<h2>📚 PolyWeather 系统技术说明</h2>
|
||||
<button class="modal-close" id="guideModalClose">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="guide-grid">
|
||||
<div class="guide-card">
|
||||
<h3>🧮 DEB 动态融合预测</h3>
|
||||
<p><b>Dynamic Ensemble Blending</b> 是系统的核心预测层。它不是对 ECMWF、GFS、ICON、GEM、JMA 等模型的简单平均,而是结合最近样本表现、当前实况与城市偏置后得到的动态加权结果。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>🎯 结算概率引擎</h3>
|
||||
<p>右侧的结算概率分布基于 DEB 预测值与多模型离散度动态计算。μ代表当前分布中心,会随着模型、实况和时间变化而变动,不是固定预报值。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>✈️ 结算点与主观测源</h3>
|
||||
<p>Polymarket 结算逻辑以机场 METAR 为主。系统现在优先使用 Aviation Weather API 的机场报文与原始 METAR,并区分观测时间与接收时间,避免把发布延迟误认成温度变化。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>📍 Ankara 专属增强</h3>
|
||||
<p>Ankara 不走通用城市逻辑。结算主站以 LTAC / Esenboğa 为准,周边领先信号优先参考 Turkish MGM 站网,其中 <b>Ankara (Bölge/Center)</b> 是重点监控站,不由 Etimesgut 代替。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>📆 未来日期分析</h3>
|
||||
<p>点击多日预报后打开的模态框,主要用于分析下一个交易日。 6-48 小时趋势以 weather.gov 和 Open-Meteo 为主; 0-2 小时临近判断则优先看 METAR 与周边站。</p>
|
||||
</div>
|
||||
<div class="guide-card">
|
||||
<h3>📊 历史对账规则</h3>
|
||||
<p>历史准确率对账只统计已结算的样本。网页端采用近15天滚动视图,机器人 /deb 报告采用近7天窗口,不把当天未结算的样本算入胜率和 MAE。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guide-footer">
|
||||
<p>※ 数据源以 Aviation Weather / METAR、Turkish MGM、Open-Meteo、weather.gov 为主。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 鈹€鈹€ History Chart Modal 鈹€鈹€ -->
|
||||
<div id="historyModal" class="modal-overlay hidden">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>📊 <span id="historyModalTitle">历史准确率对账</span></h2>
|
||||
<button class="modal-close" id="historyModalClose">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="history-stats" id="historyStats"></div>
|
||||
<div class="history-chart-wrapper">
|
||||
<canvas id="historyChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="futureForecastModal" class="modal-overlay hidden">
|
||||
<div class="modal-content large future-modal">
|
||||
<div class="modal-header">
|
||||
<h2>📆 <span id="futureForecastTitle">未来日期分析</span></h2>
|
||||
<button class="modal-close" id="futureForecastClose" aria-label="关闭未来日期分析">✕</button>
|
||||
</div>
|
||||
<div class="modal-body future-modal-body">
|
||||
<div class="history-stats" id="futureForecastStats"></div>
|
||||
|
||||
<section class="future-modal-section">
|
||||
<h3>📈 温度走势</h3>
|
||||
<div class="history-chart-wrapper future-chart-wrapper">
|
||||
<canvas id="futureForecastChart"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="future-modal-grid">
|
||||
<section class="future-modal-section">
|
||||
<h3>🎯 结算概率分布</h3>
|
||||
<div id="futureProbBars" class="prob-bars future-prob-bars"></div>
|
||||
</section>
|
||||
|
||||
<section class="future-modal-section">
|
||||
<h3>🧮 多模型预报</h3>
|
||||
<div id="futureModelBars" class="model-bars"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="future-modal-grid">
|
||||
<section class="future-modal-section">
|
||||
<h3><span class="section-inline-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19V5"/><path d="M10 19V10"/><path d="M16 19V7"/><path d="M22 19V13"/></svg></span>未来 6-48 小时趋势</h3>
|
||||
<div id="futureTrendGrid" class="future-trend-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="future-modal-section">
|
||||
<h3 id="futureNowcastTitle">⏱ 目标日午后峰值窗口</h3>
|
||||
<div id="futureNowcast" class="future-text-block"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<!-- Leaflet Heatmap Plugin -->
|
||||
<script src="https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js"></script>
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||
<script src="/static/app.js?v=legacy-v19"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user