diff --git a/frontend/app/error.tsx b/frontend/app/error.tsx new file mode 100644 index 00000000..be9e6295 --- /dev/null +++ b/frontend/app/error.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { RefreshCw } from "lucide-react"; +import { useEffect } from "react"; + +export default function ErrorPage({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error("Unhandled page error:", error); + }, [error]); + + return ( +
+
+ +
+

+ 页面出错了 +

+

+ 数据处理时遇到了意外问题,请尝试刷新页面。 +

+ {error.digest ? ( + + {error.digest} + + ) : null} + +
+ ); +} diff --git a/frontend/app/global-error.tsx b/frontend/app/global-error.tsx new file mode 100644 index 00000000..18504229 --- /dev/null +++ b/frontend/app/global-error.tsx @@ -0,0 +1,115 @@ +"use client"; + +import { RefreshCw } from "lucide-react"; +import { useEffect } from "react"; + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error("Unhandled root error:", error); + }, [error]); + + return ( + + + + PolyWeather — 出错了 + + +
+
+ +
+

+ 页面出错了 +

+

+ PolyWeather 遇到了严重错误,请尝试刷新页面。如果问题持续出现,请联系我们。 +

+ {error.digest ? ( + + {error.digest} + + ) : null} + +
+ + + ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 50aa93b8..1fff22d0 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -50,10 +50,9 @@ --shadow-glow-secondary: 0 0 20px rgba(111, 183, 255, 0.22); /* ── Typography ── */ - --font-data: - "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --font-display: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace; + --font-data: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-display: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: var(--font-jetbrains-mono), "Fira Code", "SF Mono", monospace; /* ── Spacing (4px grid) ── */ --space-1: 4px; diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 80c95d37..b1d5f850 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,6 +1,22 @@ import type { Metadata } from "next"; +import { Inter, JetBrains_Mono } from "next/font/google"; +import { RegisterSW } from "@/components/dashboard/RegisterSW"; import "./globals.css"; +const inter = Inter({ + subsets: ["latin"], + display: "swap", + variable: "--font-inter", + weight: ["300", "400", "500", "600", "700", "800"], +}); + +const jetbrainsMono = JetBrains_Mono({ + subsets: ["latin"], + display: "swap", + variable: "--font-jetbrains-mono", + weight: ["400", "500", "600", "700"], +}); + export const metadata: Metadata = { title: "PolyWeather | Weather Intelligence", description: @@ -23,25 +39,19 @@ export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode }>) { return ( - + - - - Skip to content
{children}
+ ); diff --git a/frontend/components/dashboard/DetailPanel.tsx b/frontend/components/dashboard/DetailPanel.tsx index 17494fd5..8ba0690b 100644 --- a/frontend/components/dashboard/DetailPanel.tsx +++ b/frontend/components/dashboard/DetailPanel.tsx @@ -12,6 +12,7 @@ import { useProAccess, } from "@/hooks/useDashboardStore"; import { useI18n } from "@/hooks/useI18n"; +import { useRelativeTime } from "@/hooks/useRelativeTime"; import { getOfficialSourceLinks } from "@/lib/dashboard-official-sources"; import { trackAppEvent } from "@/lib/app-analytics"; import { getTodayPolymarketUrl } from "@/lib/polymarket-market-links"; @@ -60,6 +61,19 @@ export function DetailPanel({ const isPro = proAccess.subscriptionActive; const isAuthenticated = proAccess.authenticated; const panelRef = useRef(null); + const lastDetailFetchedAtRef = useRef(0); + + useEffect(() => { + if (detail?.name) { + lastDetailFetchedAtRef.current = Date.now(); + } + }, [detail?.name]); + + const detailAgeText = useRelativeTime( + lastDetailFetchedAtRef.current + ? new Date(lastDetailFetchedAtRef.current).toISOString() + : null, + ); const [heavyContentReady, setHeavyContentReady] = useState(false); const isOverlayOpen = Boolean(modal.futureModalDate) || history.historyState.isOpen; @@ -298,6 +312,19 @@ export function DetailPanel({ {getRiskBadgeLabel(panelRiskLevel, locale)} + {detailAgeText ? ( + + {locale === "en-US" ? "Updated " : "更新于 "} + {detailAgeText} + + ) : null} {heroSettlementLabel} diff --git a/frontend/components/dashboard/IntradaySignalScene.tsx b/frontend/components/dashboard/IntradaySignalScene.tsx index a9af6080..8cb16a8d 100644 --- a/frontend/components/dashboard/IntradaySignalScene.tsx +++ b/frontend/components/dashboard/IntradaySignalScene.tsx @@ -1,7 +1,6 @@ "use client"; -import { useEffect, useRef } from "react"; -import * as THREE from "three"; +import { CSSProperties, useEffect, useRef } from "react"; import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion"; export interface IntradaySignalMetric { @@ -24,6 +23,42 @@ function getToneColor(tone: string) { return "#9FB2C7"; } +function hexToRgba(hex: string, alpha: number) { + const sanitized = hex.replace("#", ""); + const numeric = Number.parseInt(sanitized.padEnd(6, "0"), 16); + const r = (numeric >> 16) & 255; + const g = (numeric >> 8) & 255; + const b = numeric & 255; + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +/* ── Draw a rounded-rect path (polyfill-safe) ── */ + +function roundRect( + ctx: CanvasRenderingContext2D, + x: number, + y: number, + w: number, + h: number, + r: number | [number, number, number, number], +) { + const corners = typeof r === "number" ? [r, r, r, r] : r; + const [tl, tr, br, bl] = corners; + ctx.beginPath(); + ctx.moveTo(x + tl, y); + ctx.lineTo(x + w - tr, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + tr); + ctx.lineTo(x + w, y + h - br); + ctx.quadraticCurveTo(x + w, y + h, x + w - br, y + h); + ctx.lineTo(x + bl, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - bl); + ctx.lineTo(x, y + tl); + ctx.quadraticCurveTo(x, y, x + tl, y); + ctx.closePath(); +} + +/* ── Component ── */ + export function IntradaySignalScene({ metrics, score, @@ -38,163 +73,173 @@ export function IntradaySignalScene({ const host = containerRef.current; if (!host) return; - const renderer = new THREE.WebGLRenderer({ - alpha: true, - antialias: true, - powerPreference: "low-power", - }); - renderer.setClearColor(0x000000, 0); - renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5)); + const canvas = document.createElement("canvas"); + canvas.style.cssText = "width:100%;height:100%;display:block;"; + host.appendChild(canvas); - const scene = new THREE.Scene(); - const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100); - camera.position.set(0, 2.8, 7.2); - camera.lookAt(0, 1.2, 0); - - const ambient = new THREE.AmbientLight(0xbfe8ff, 1.25); - const keyLight = new THREE.PointLight(0x67e8f9, 22, 18, 2); - keyLight.position.set(-3.8, 5.6, 4.8); - const warmLight = new THREE.PointLight(0xf59e0b, 12, 16, 2); - warmLight.position.set(4.2, 2.8, 4); - scene.add(ambient, keyLight, warmLight); - - const stage = new THREE.Group(); - scene.add(stage); - - const floor = new THREE.Mesh( - new THREE.CylinderGeometry(3.3, 3.8, 0.12, 48), - new THREE.MeshStandardMaterial({ - color: new THREE.Color(score >= 0 ? "#10263b" : "#2c1d12"), - emissive: new THREE.Color(score >= 0 ? "#0e7490" : "#b45309"), - emissiveIntensity: 0.18 + clamp(Math.abs(score) / 8, 0, 0.24), - metalness: 0.2, - roughness: 0.78, - }), - ); - floor.position.y = -0.12; - stage.add(floor); - - const ring = new THREE.Mesh( - new THREE.TorusGeometry(2.8, 0.03, 18, 100), - new THREE.MeshBasicMaterial({ - color: new THREE.Color(score >= 0 ? "#4DA3FF" : "#F59E0B"), - transparent: true, - opacity: 0.5, - }), - ); - ring.rotation.x = Math.PI / 2; - ring.position.y = 0.03; - stage.add(ring); - - const barGeometry = new THREE.BoxGeometry(0.8, 1, 0.8); - const capGeometry = new THREE.SphereGeometry(0.16, 16, 16); - const bars: Array<{ - mesh: THREE.Mesh; - cap: THREE.Mesh; - glow: THREE.Mesh; - baseY: number; - targetHeight: number; - }> = []; - - const xPositions = [-1.8, -0.6, 0.6, 1.8]; - metrics.slice(0, 4).forEach((metric, index) => { - const height = 0.5 + ((metric.fill ?? 20) / 100) * 2.8; - const color = new THREE.Color(getToneColor(metric.tone)); - const material = new THREE.MeshStandardMaterial({ - color, - emissive: color, - emissiveIntensity: 0.22, - metalness: 0.14, - roughness: 0.38, - }); - const mesh = new THREE.Mesh(barGeometry, material); - mesh.position.set(xPositions[index] || 0, height / 2, 0); - mesh.scale.y = height; - stage.add(mesh); - - const cap = new THREE.Mesh( - capGeometry, - new THREE.MeshBasicMaterial({ - color, - transparent: true, - opacity: 0.95, - }), - ); - cap.position.set(mesh.position.x, height + 0.2, 0); - stage.add(cap); - - const glow = new THREE.Mesh( - new THREE.CylinderGeometry(0.46, 0.58, 0.08, 32), - new THREE.MeshBasicMaterial({ - color, - transparent: true, - opacity: 0.22, - }), - ); - glow.position.set(mesh.position.x, 0.06, 0); - stage.add(glow); - - bars.push({ - mesh, - cap, - glow, - baseY: cap.position.y, - targetHeight: height, - }); - }); + const ctx = canvas.getContext("2d")!; + let animationId = 0; const resize = () => { - const width = Math.max(host.clientWidth, 1); - const height = Math.max(host.clientHeight, 1); - renderer.setSize(width, height, false); - camera.aspect = width / height; - camera.updateProjectionMatrix(); + const rect = host.getBoundingClientRect(); + const dpr = Math.min(window.devicePixelRatio || 1, 1.5); + canvas.width = rect.width * dpr; + canvas.height = rect.height * dpr; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; resize(); - host.appendChild(renderer.domElement); - - const clock = new THREE.Clock(); - let frameId = 0; - - const renderFrame = () => { - frameId = window.requestAnimationFrame(renderFrame); - const elapsed = clock.getElapsedTime(); - stage.rotation.y = Math.sin(elapsed * 0.35) * 0.16; - ring.material.opacity = 0.38 + Math.sin(elapsed * 0.8) * 0.08; - - bars.forEach((bar, index) => { - const pulse = prefersReducedMotion - ? 0 - : Math.sin(elapsed * 1.5 + index * 0.8) * 0.08; - bar.cap.position.y = bar.baseY + pulse; - bar.glow.scale.x = 1 + Math.sin(elapsed * 1.2 + index) * 0.06; - bar.glow.scale.z = 1 + Math.sin(elapsed * 1.2 + index) * 0.06; - }); - - renderer.render(scene, camera); - }; const observer = new ResizeObserver(resize); observer.observe(host); - frameId = window.requestAnimationFrame(renderFrame); + + let startTime = performance.now(); + + const render = (now: number) => { + animationId = requestAnimationFrame(render); + + const W = host.clientWidth; + const H = host.clientHeight; + if (W < 1 || H < 1) return; + + ctx.clearRect(0, 0, W, H); + + const elapsed = prefersReducedMotion ? 0 : (now - startTime) / 1000; + + /* ── Layout ── */ + const cx = W / 2; + const floorY = Math.max(H - 28, H * 0.78); + const rx = clamp(Math.min(W / 2 - 24, 160), 60, 160); + const ry = clamp(rx * 0.28, 12, 26); + + const isPositive = score >= 0; + const accentColor = isPositive ? "#4DA3FF" : "#F59E0B"; + const floorGlowColor = isPositive ? "#0e7490" : "#b45309"; + const floorBaseColor = isPositive ? "#10263b" : "#2c1d12"; + + /* ── Floor ── */ + const floorGrad = ctx.createRadialGradient(cx, floorY, 0, cx, floorY, rx); + floorGrad.addColorStop(0, hexToRgba(floorGlowColor, 0.55)); + floorGrad.addColorStop(0.5, floorBaseColor); + floorGrad.addColorStop(1, "#060e1a"); + + ctx.beginPath(); + ctx.ellipse(cx, floorY, rx, ry, 0, 0, Math.PI * 2); + ctx.fillStyle = floorGrad; + ctx.fill(); + + /* Floor glow overlay */ + const floorGlow = ctx.createRadialGradient(cx, floorY, 0, cx, floorY, rx * 0.6); + floorGlow.addColorStop(0, hexToRgba(accentColor, 0.08)); + floorGlow.addColorStop(1, "transparent"); + ctx.beginPath(); + ctx.ellipse(cx, floorY, rx * 0.6, ry * 0.6, 0, 0, Math.PI * 2); + ctx.fillStyle = floorGlow; + ctx.fill(); + + /* ── Ring (oscillating opacity) ── */ + const ringOpacity = 0.38 + Math.sin(elapsed * 0.8) * 0.1; + ctx.beginPath(); + ctx.ellipse(cx, floorY, rx - 10, Math.max(ry - 3, 8), 0, 0, Math.PI * 2); + ctx.strokeStyle = hexToRgba(accentColor, ringOpacity); + ctx.lineWidth = 2.5; + ctx.stroke(); + + /* ── Bar positions ── */ + const barSpacing = rx * 0.48; + const xPositions = [ + cx - barSpacing, + cx - barSpacing / 3, + cx + barSpacing / 3, + cx + barSpacing, + ]; + const barWidth = Math.min(26, rx * 0.16); + const limitedMetrics = metrics.slice(0, 4); + + /* ── Draw bars ── */ + limitedMetrics.forEach((metric, i) => { + const fill = metric.fill ?? 20; + const height = Math.max(12, 14 + (fill / 100) * (floorY - 55)); + const bx = xPositions[i]; + const by = floorY - height; + const color = getToneColor(metric.tone); + const baseGlow = prefersReducedMotion + ? 1 + : 1 + Math.sin(elapsed * 1.2 + i) * 0.06; + + /* Base glow ellipse */ + const glowGrad = ctx.createRadialGradient(bx, floorY, 0, bx, floorY, barWidth * 1.2); + glowGrad.addColorStop(0, hexToRgba(color, 0.35)); + glowGrad.addColorStop(1, "transparent"); + ctx.beginPath(); + ctx.ellipse(bx, floorY, barWidth * baseGlow, 7 * baseGlow, 0, 0, Math.PI * 2); + ctx.fillStyle = glowGrad; + ctx.fill(); + + /* Bar body (rounded top) */ + const barGrad = ctx.createLinearGradient(bx, by, bx, floorY); + barGrad.addColorStop(0, color); + barGrad.addColorStop(0.55, color); + barGrad.addColorStop(1, hexToRgba(color, 0.3)); + roundRect(ctx, bx - barWidth / 2, by, barWidth, height, [4, 4, 0, 0]); + ctx.fillStyle = barGrad; + ctx.fill(); + + /* Bar glow outline */ + ctx.strokeStyle = hexToRgba(color, 0.15); + ctx.lineWidth = 1; + roundRect(ctx, bx - barWidth / 2 - 1, by - 1, barWidth + 2, height + 2, [5, 5, 0, 0]); + ctx.stroke(); + + /* Cap (pulse offset per bar) */ + const pulse = prefersReducedMotion + ? 0 + : Math.sin(elapsed * 1.5 + i * 0.8) * 3; + const capY = by + pulse; + const capR = Math.max(5, barWidth * 0.22); + + /* Cap outer glow */ + const capGlow = ctx.createRadialGradient(bx, capY, 0, bx, capY, capR * 3); + capGlow.addColorStop(0, hexToRgba(color, 0.2)); + capGlow.addColorStop(1, "transparent"); + ctx.beginPath(); + ctx.arc(bx, capY, capR * 3, 0, Math.PI * 2); + ctx.fillStyle = capGlow; + ctx.fill(); + + /* Cap body */ + ctx.beginPath(); + ctx.arc(bx, capY, capR, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.shadowColor = color; + ctx.shadowBlur = capR * 2; + ctx.fill(); + ctx.shadowBlur = 0; + }); + + /* ── Ambient sparkle dots (only when not reduced motion) ── */ + if (!prefersReducedMotion) { + for (let i = 0; i < 6; i++) { + const angle = elapsed * 0.25 + i * 1.05; + const dist = rx * 0.7 + Math.sin(elapsed * 0.15 + i * 2.3) * 10; + const sx = cx + Math.cos(angle) * dist; + const sy = floorY - ry * 0.4 + Math.sin(angle) * dist * 0.35; + const sparkleSize = 1.5 + Math.sin(elapsed * 2 + i * 1.7) * 0.8; + ctx.beginPath(); + ctx.arc(sx, sy, Math.max(0.5, sparkleSize), 0, Math.PI * 2); + ctx.fillStyle = hexToRgba(accentColor, 0.12 + Math.sin(elapsed * 1.3 + i) * 0.06); + ctx.fill(); + } + } + } + + animationId = requestAnimationFrame(render); return () => { observer.disconnect(); - window.cancelAnimationFrame(frameId); - stage.traverse((child) => { - if (child instanceof THREE.Mesh) { - child.geometry.dispose(); - if (Array.isArray(child.material)) { - child.material.forEach((material) => material.dispose()); - } else { - child.material.dispose(); - } - } - }); - renderer.dispose(); - if (renderer.domElement.parentNode === host) { - host.removeChild(renderer.domElement); + cancelAnimationFrame(animationId); + if (canvas.parentNode === host) { + host.removeChild(canvas); } }; }, [metrics, prefersReducedMotion, score]); @@ -211,7 +256,7 @@ export function IntradaySignalScene({
{metric.label} diff --git a/frontend/components/dashboard/RegisterSW.tsx b/frontend/components/dashboard/RegisterSW.tsx new file mode 100644 index 00000000..70dda4fd --- /dev/null +++ b/frontend/components/dashboard/RegisterSW.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { useEffect } from "react"; + +export function RegisterSW() { + useEffect(() => { + if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("/sw.js").catch(() => {}); + } + }, []); + return null; +} diff --git a/frontend/components/dashboard/ScanTerminal.module.css b/frontend/components/dashboard/ScanTerminal.module.css index a744d103..acf7bc44 100644 --- a/frontend/components/dashboard/ScanTerminal.module.css +++ b/frontend/components/dashboard/ScanTerminal.module.css @@ -554,3 +554,983 @@ min-height: calc(100vh - 32px); max-height: calc(100vh - 32px); } + +/* ══════════════════════════════════════════════════════════════ + LIGHT THEME OVERRIDES — merged from ScanTerminalLightTheme.module.css + Uses CSS custom properties so colors switch automatically when + html.light is toggled via useScanTerminalTheme(). + ══════════════════════════════════════════════════════════════ */ + +.root :global(.scan-terminal.light) { + background: + radial-gradient(circle at top, rgba(59, 130, 246, 0.1), transparent 34%), + linear-gradient(180deg, #F7F9FC 0%, #EEF2F7 100%); + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-filter-panel), +.root :global(.scan-terminal.light .scan-data-grid), +.root :global(.scan-terminal.light .scan-detail-panel), +.root :global(.scan-terminal.light > .detail-panel.scan-city-detail-rail) { + border-color: var(--border-glass); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.96)); + box-shadow: 0 16px 34px rgba(40, 70, 110, 0.12); +} + +.root :global(.scan-terminal.light .scan-sidebar-brand-name), +.root :global(.scan-terminal.light .scan-topbar-tab.active), +.root :global(.scan-terminal.light .scan-hero h1), +.root :global(.scan-terminal.light .scan-topbar-title strong), +.root :global(.scan-terminal.light .scan-city-name), +.root :global(.scan-terminal.light .scan-opportunity-city strong), +.root :global(.scan-terminal.light .panel-title-area h2), +.root :global(.scan-terminal.light .detail-summary-temp), +.root :global(.scan-terminal.light .scan-detail-city-name), +.root :global(.scan-terminal.light .scan-empty-title), +.root :global(.scan-terminal.light .scan-detail-section-title), +.root :global(.scan-terminal.light .scan-detail-volume-big), +.root :global(.scan-terminal.light .scan-kv strong), +.root :global(.scan-terminal.light .scan-time-main), +.root :global(.scan-terminal.light .scan-summary-value), +.root :global(.scan-terminal.light .scan-distribution-card strong), +.root :global(.scan-terminal.light .scan-table-header), +.root :global(.scan-terminal.light .scan-list-tabs button.active) { + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-filter-heading), +.root :global(.scan-terminal.light .scan-topbar-actions), +.root :global(.scan-terminal.light .scan-topbar-time), +.root :global(.scan-terminal.light .scan-hero p), +.root :global(.scan-terminal.light .scan-topbar-title span), +.root :global(.scan-terminal.light .scan-city-sub), +.root :global(.scan-terminal.light .scan-opportunity-phase), +.root :global(.scan-terminal.light .scan-opportunity-phase em), +.root :global(.scan-terminal.light .panel-overline), +.root :global(.scan-terminal.light .detail-summary-supporting), +.root :global(.scan-terminal.light .detail-value-muted), +.root :global(.scan-terminal.light .detail-empty-state), +.root :global(.scan-terminal.light .scan-time-remaining), +.root :global(.scan-terminal.light .scan-detail-city-sub), +.root :global(.scan-terminal.light .scan-detail-volume-caption), +.root :global(.scan-terminal.light .scan-empty-copy), +.root :global(.scan-terminal.light .scan-loading-copy-block span), +.root :global(.scan-terminal.light .scan-kv span:first-child), +.root :global(.scan-terminal.light .scan-chart-label), +.root :global(.scan-terminal.light .scan-trade-sub), +.root :global(.scan-terminal.light .scan-trade-note) { + color: var(--text-muted); +} + +.root :global(.scan-terminal.light .scan-loading-copy-block strong) { + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-loading-signal) { + border-color: #dbeafe; + background: + radial-gradient(circle at 18% 18%, rgba(59, 130, 246, 0.12), transparent 34%), + linear-gradient(145deg, rgba(255, 255, 255, 0.96), rgba(241, 245, 249, 0.9)); + box-shadow: 0 16px 34px rgba(40, 70, 110, 0.1); +} + +.root :global(.scan-terminal.light .scan-loading-signal.compact) { + background: + radial-gradient(circle at 18% 18%, rgba(59, 130, 246, 0.1), transparent 34%), + 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: var(--bg-card); + border-color: rgba(148, 163, 184, 0.22); +} + +.root :global(.scan-terminal.light .scan-loading-node.hot) { + background: linear-gradient(135deg, #f97316, #fde047); +} + +.root :global(.scan-terminal.light .scan-loading-node.market) { + background: linear-gradient(135deg, #38bdf8, #2563eb); +} + +.root :global(.scan-terminal.light .scan-loading-node.action) { + background: linear-gradient(135deg, #16a34a, #22c55e); +} + +.root :global(.scan-terminal.light .scan-list-tabs button), +.root :global(.scan-terminal.light .scan-mode-tab-sub), +.root :global(.scan-terminal.light .scan-opportunity-models em), +.root :global(.scan-terminal.light .scan-opportunity-stat small), +.root :global(.scan-terminal.light .panel-meta-chip), +.root :global(.scan-terminal.light .panel-weather-chip), +.root :global(.scan-terminal.light .scan-distribution-card), +.root :global(.scan-terminal.light .scan-chart-legend) { + color: #94A3B8; +} + +.root :global(.scan-terminal.light .scan-distribution-line em) { + color: #243b5a; +} + +.root :global(.scan-terminal.light .scan-distribution-line b) { + color: #6f86a4; +} + +.root :global(.scan-terminal.light .scan-mode-tab), +.root :global(.scan-terminal.light .scan-table-shell), +.root :global(.scan-terminal.light .scan-table-header), +.root :global(.scan-terminal.light .scan-summary-card), +.root :global(.scan-terminal.light .scan-settings-card), +.root :global(.scan-terminal.light .scan-distribution-card), +.root :global(.scan-terminal.light .scan-opportunity-group), +.root :global(.scan-terminal.light .detail-summary-shell), +.root :global(.scan-terminal.light .detail-structured-section), +.root :global(.scan-terminal.light .detail-card), +.root :global(.scan-terminal.light .scan-trade-card) { + border-color: var(--border-glass); + background: var(--bg-card); +} + +.root :global(.scan-terminal.light .scan-mode-tab.active) { + color: #1D4ED8; + background: #DBEAFE; + border-color: rgba(59, 130, 246, 0.34); + box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.16); +} + +.root :global(.scan-terminal.light .scan-mode-tab.active .scan-mode-tab-label) { + color: #1D4ED8; +} + +.root :global(.scan-terminal.light .scan-mode-tab.active .scan-mode-tab-sub) { + color: var(--text-muted); +} + +.root :global(.scan-terminal.light .scan-mode-tab.active .scan-mode-icon) { + background: rgba(16, 185, 129, 0.16); + color: #069668; +} + +.root :global(.scan-terminal.light .scan-table-row.selected) { + background: linear-gradient(180deg, rgba(228, 252, 242, 0.92), rgba(241, 250, 247, 0.96)); + border-color: rgba(10, 160, 100, 0.34); + box-shadow: inset 0 0 0 1px rgba(10, 160, 100, 0.18); +} + +.root :global(.scan-terminal.light .scan-table-row) { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(247, 250, 254, 0.9)); + border-bottom-color: rgba(35, 72, 118, 0.08); +} + +.root :global(.scan-terminal.light .scan-table-row:hover) { + background: linear-gradient(180deg, rgba(248, 252, 255, 0.96), rgba(238, 247, 255, 0.96)); +} + +.root :global(.scan-terminal.light .scan-opportunity-phase > span) { + color: #14253a; +} + +.root :global(.scan-terminal.light .scan-opportunity-group-head) { + background: rgba(246, 250, 255, 0.78); + border-bottom-color: rgba(35, 72, 118, 0.1); +} + +.root :global(.scan-terminal.light .scan-opportunity-item) { + background: rgba(255, 255, 255, 0.62); + color: #405977; + border-bottom-color: rgba(35, 72, 118, 0.08); + border-color: rgba(35, 72, 118, 0.1); +} + +.root :global(.scan-terminal.light .scan-opportunity-item:hover) { + background: rgba(230, 242, 255, 0.76); +} + +.root :global(.scan-terminal.light .scan-opportunity-item.selected) { + background: linear-gradient(180deg, rgba(222, 250, 238, 0.9), rgba(241, 250, 247, 0.96)); + border-color: rgba(10, 160, 100, 0.34); +} + +.root :global(.scan-terminal.light .scan-opportunity-group.selected) { + border-color: rgba(10, 160, 100, 0.34); + box-shadow: inset 0 0 0 1px rgba(10, 160, 100, 0.18); +} + +.root :global(.scan-terminal.light .scan-opportunity-models span) { + border-color: rgba(35, 72, 118, 0.1); + background: rgba(255, 255, 255, 0.68); +} + +.root :global(.scan-terminal.light .scan-opportunity-stat) { + border-color: rgba(35, 72, 118, 0.1); + background: rgba(248, 252, 255, 0.76); +} + +.root :global(.scan-terminal.light .scan-opportunity-models b), +.root :global(.scan-terminal.light .scan-opportunity-stat b) { + color: #14253a; +} + +.root :global(.scan-terminal.light .scan-opportunity-branch::before), +.root :global(.scan-terminal.light .scan-opportunity-branch i) { + background: rgba(56, 86, 126, 0.24); +} + +.root :global(.scan-terminal.light .panel-header), +.root :global(.scan-terminal.light .panel-body section) { + border-color: rgba(35, 72, 118, 0.1); +} + +.root :global(.scan-terminal.light .panel-action-button-secondary), +.root :global(.scan-terminal.light .panel-action-button-ghost) { + border-color: rgba(35, 72, 118, 0.14); + background: rgba(255, 255, 255, 0.76); +} + +.root :global(.scan-terminal.light .detail-summary-shell-live) { + background: linear-gradient(180deg, rgba(224, 247, 255, 0.82), rgba(241, 248, 255, 0.92)); +} + +.root :global(.scan-terminal.light .scan-locale-switch), +.root :global(.scan-terminal.light .scan-ghost-button), +.root :global(.scan-terminal.light .scan-theme-button), +.root :global(.scan-terminal.light .scan-sort-pill), +.root :global(.scan-terminal.light .scan-icon-pill), +.root :global(.scan-terminal.light .scan-account-button), +.root :global(.scan-terminal.light .scan-detail-action-button) { + border-color: rgba(35, 72, 118, 0.14); + background: rgba(255, 255, 255, 0.78); + color: #1f3654; +} + +.root :global(.scan-terminal.light .scan-primary-button) { + color: #FFFFFF; + border-color: rgba(59, 130, 246, 0.34); + background: linear-gradient(180deg, #3B82F6, #2563EB); +} + +.root :global(.scan-terminal.light .scan-ai-button) { + color: #1D4ED8; + border-color: rgba(59, 130, 246, 0.28); + background: #DBEAFE; +} + +.root :global(.scan-terminal.light .scan-opportunity-ai) { + border-color: rgba(35, 72, 118, 0.1); + background: rgba(248, 252, 255, 0.76); +} + +.root :global(.scan-terminal.light .scan-opportunity-ai small) { + color: #647a98; +} + +.root :global(.scan-terminal.light .scan-opportunity-strength) { + border-color: rgba(35, 72, 118, 0.12); + background: rgba(248, 252, 255, 0.78); +} + +.root :global(.scan-terminal.light .scan-opportunity-expand) { + color: #1d4ed8; + border-color: rgba(37, 99, 235, 0.18); + background: rgba(239, 246, 255, 0.84); +} + +.root :global(.scan-terminal.light .scan-v4-analysis), +.root :global(.scan-terminal.light .scan-v4-analysis section), +.root :global(.scan-terminal.light .scan-v4-evidence > div > span), +.root :global(.scan-terminal.light .scan-v4-model-sources span) { + border-color: rgba(35, 72, 118, 0.12); + background: rgba(255, 255, 255, 0.76); +} + +.root :global(.scan-terminal.light .scan-v4-analysis strong) { + color: #057a6b; +} + +.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), +.root :global(.scan-terminal.light .scan-v4-model-sources em) { + color: var(--text-muted); +} + +.root :global(.scan-terminal.light .scan-v4-model-sources b) { + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-forecast-city-card), +.root :global(.scan-terminal.light .scan-forecast-row), +.root :global(.scan-terminal.light .scan-ai-analysis) { + border-color: var(--border-glass); + background: var(--bg-card); +} + +.root :global(.scan-terminal.light .scan-forecast-city-head) { + background: var(--bg-secondary); +} + +.root :global(.scan-terminal.light .scan-forecast-city-card.selected), +.root :global(.scan-terminal.light .scan-forecast-city-card:has(.scan-forecast-row.selected)) { + border-color: rgba(59, 130, 246, 0.38); + box-shadow: + inset 0 0 0 1px rgba(59, 130, 246, 0.12), + 0 16px 34px rgba(40, 70, 110, 0.12); +} + +.root :global(.scan-terminal.light .scan-forecast-row.selected), +.root :global(.scan-terminal.light .scan-forecast-row.expanded) { + border-color: rgba(59, 130, 246, 0.38); + background: + linear-gradient(180deg, rgba(219, 234, 254, 0.62), rgba(255, 255, 255, 0.98)), + var(--bg-card); + box-shadow: inset 4px 0 0 #3B82F6; +} + +.root :global(.scan-terminal.light .scan-forecast-city-title strong), +.root :global(.scan-terminal.light .scan-forecast-city-read > b:not(.scan-phase-badge)), +.root :global(.scan-terminal.light .scan-forecast-bucket strong), +.root :global(.scan-terminal.light .scan-forecast-signals b), +.root :global(.scan-terminal.light .scan-ai-temperature-line b) { + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-forecast-city-chips span), +.root :global(.scan-terminal.light .scan-forecast-signals span), +.root :global(.scan-terminal.light .scan-ai-evidence-line span) { + border-color: var(--border-glass); + background: var(--bg-secondary); +} + +.root :global(.scan-terminal.light .scan-forecast-city-chips span), +.root :global(.scan-terminal.light .scan-forecast-city-read span), +.root :global(.scan-terminal.light .scan-forecast-bucket small), +.root :global(.scan-terminal.light .scan-forecast-ai-line small), +.root :global(.scan-terminal.light .scan-ai-brief-grid li), +.root :global(.scan-terminal.light .scan-ai-airport-read p) { + color: var(--text-muted); +} + +.root :global(.scan-terminal.light .scan-ai-analysis-head p) { + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-ai-analysis-head small), +.root :global(.scan-terminal.light .scan-ai-evidence-line b) { + color: var(--text-muted); +} + +.root :global(.scan-terminal.light .scan-ai-workspace) { + background: #f7f9fc; + border-color: #e2e8f0; +} + +.root :global(.scan-terminal.light .scan-opportunity-overview) { + background: #f7f9fc; + border-color: #e2e8f0; +} + +.root :global(.scan-terminal.light .scan-opportunity-hero > div:first-child), +.root :global(.scan-terminal.light .scan-opportunity-summary span), +.root :global(.scan-terminal.light .scan-opportunity-lane), +.root :global(.scan-terminal.light .scan-opportunity-decision-card), +.root :global(.scan-terminal.light .scan-opportunity-decision-primary span) { + background: var(--bg-card); + border-color: var(--border-glass); +} + +.root :global(.scan-terminal.light .scan-opportunity-hero strong), +.root :global(.scan-terminal.light .scan-opportunity-summary b), +.root :global(.scan-terminal.light .scan-opportunity-lane-head strong), +.root :global(.scan-terminal.light .scan-opportunity-decision-head strong), +.root :global(.scan-terminal.light .scan-opportunity-decision-primary b) { + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-opportunity-hero p), +.root :global(.scan-terminal.light .scan-opportunity-summary span), +.root :global(.scan-terminal.light .scan-opportunity-lane-head p), +.root :global(.scan-terminal.light .scan-opportunity-decision-card p), +.root :global(.scan-terminal.light .scan-opportunity-decision-head span), +.root :global(.scan-terminal.light .scan-opportunity-decision-primary span), +.root :global(.scan-terminal.light .scan-opportunity-decision-foot small) { + color: #64748b; +} + +.root :global(.scan-terminal.light .scan-ai-city-card), +.root :global(.scan-terminal.light .scan-ai-city-hero) { + background: var(--bg-card); + border-color: var(--border-glass); +} + +.root :global(.scan-terminal.light .scan-ai-city-section), +.root :global(.scan-terminal.light .scan-ai-decision-band), +.root :global(.scan-terminal.light .scan-ai-market-decision), +.root :global(.scan-terminal.light .scan-ai-market-decision-stats small), +.root :global(.scan-terminal.light .scan-ai-decision-metrics span), +.root :global(.scan-terminal.light .scan-ai-market-bucket), +.root :global(.scan-terminal.light .scan-ai-city-pills span), +.root :global(.scan-terminal.light .scan-ai-city-freshness span), +.root :global(.scan-terminal.light .scan-ai-city-metrics > span) { + background: var(--bg-secondary); + border-color: var(--border-glass); +} + +.root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) { + background: #dbeafe; + border-color: rgba(59, 130, 246, 0.28); +} + +.root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) { + color: #1d4ed8; +} + +.root :global(.scan-terminal.light .scan-ai-city-metrics small) { + color: #64748b; +} + +.root :global(.scan-terminal.light .scan-ai-workspace-head strong), +.root :global(.scan-terminal.light .scan-ai-city-hero h3), +.root :global(.scan-terminal.light .scan-ai-city-metrics b), +.root :global(.scan-terminal.light .scan-ai-decision-band strong), +.root :global(.scan-terminal.light .scan-ai-decision-metrics b), +.root :global(.scan-terminal.light .scan-ai-market-bucket strong), +.root :global(.scan-terminal.light .scan-ai-weather-summary), +.root :global(.scan-terminal.light .scan-ai-market-decision strong), +.root :global(.scan-terminal.light .scan-ai-market-decision-stats b) { + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-ai-workspace-head p), +.root :global(.scan-terminal.light .scan-ai-city-section p), +.root :global(.scan-terminal.light .scan-ai-decision-band p), +.root :global(.scan-terminal.light .scan-ai-city-pills span), +.root :global(.scan-terminal.light .scan-ai-city-freshness), +.root :global(.scan-terminal.light .scan-ai-decision-metrics span), +.root :global(.scan-terminal.light .scan-ai-market-bucket span), +.root :global(.scan-terminal.light .scan-ai-market-decision span), +.root :global(.scan-terminal.light .scan-ai-market-decision p), +.root :global(.scan-terminal.light .scan-ai-market-decision-stats small), +.root :global(.scan-terminal.light .scan-ai-city-muted), +.root :global(.scan-terminal.light .scan-ai-city-loading), +.root :global(.scan-terminal.light .scan-ai-city-chart-legend), +.root :global(.scan-terminal.light .scan-ai-weather-bullets) { + color: var(--text-muted); +} + +.root :global(.scan-terminal.light .scan-ai-raw-metar) { + border-top-color: var(--border-glass); + color: #64748b; +} + +.root :global(.scan-terminal.light .scan-ai-city-chart-placeholder) { + border-color: #cbd5e1; + background: #f8fafc; + color: #64748b; +} + +.root :global(.scan-terminal.light .scan-ai-log-panel) { + border-color: rgba(35, 72, 118, 0.12); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(246, 250, 255, 0.9)); +} + +.root :global(.scan-terminal.light .scan-ai-log-panel.compact) { + background: rgba(255, 255, 255, 0.82); +} + +.root :global(.scan-terminal.light .scan-ai-log-panel.compact .scan-ai-log-summary span) { + color: var(--text-secondary); +} + +.root :global(.scan-terminal.light .scan-ai-log-panel.compact .scan-ai-log-summary strong) { + color: #2563eb; +} + +.root :global(.scan-terminal.light .scan-ai-log-head strong), +.root :global(.scan-terminal.light .scan-ai-log-item b) { + color: #122033; +} + +.root :global(.scan-terminal.light .scan-ai-log-head span), +.root :global(.scan-terminal.light .scan-ai-log-time), +.root :global(.scan-terminal.light .scan-ai-log-empty) { + color: #647a98; +} + +.root :global(.scan-terminal.light .scan-ai-log-item) { + border-color: rgba(35, 72, 118, 0.12); + background: rgba(255, 255, 255, 0.72); +} + +.root :global(.scan-terminal.light .scan-ai-log-item small) { + color: #58708f; +} + +.root :global(.scan-terminal.light .scan-ai-log-item.info) { + border-color: rgba(14, 165, 233, 0.24); +} + +.root :global(.scan-terminal.light .scan-ai-log-item.success) { + border-color: rgba(10, 160, 100, 0.28); +} + +.root :global(.scan-terminal.light .scan-ai-log-item.warning) { + border-color: rgba(217, 119, 6, 0.28); +} + +.root :global(.scan-terminal.light .scan-ai-log-item.error) { + border-color: rgba(220, 38, 38, 0.28); +} + +.root :global(.scan-terminal.light .scan-ai-summary-card), +.root :global(.scan-terminal.light .scan-ai-city-card) { + background: rgba(255, 255, 255, 0.92); + border-color: rgba(148, 163, 184, 0.28); +} + +.root :global(.scan-terminal.light .scan-ai-summary-card strong), +.root :global(.scan-terminal.light .scan-ai-city-head strong), +.root :global(.scan-terminal.light .scan-ai-contract b) { + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-ai-summary-card p), +.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: var(--text-muted); +} + +.root :global(.scan-terminal.light .scan-ai-city-head) { + background: #f8fafc; +} + +.root :global(.scan-terminal.light .scan-ai-cluster-note) { + color: var(--text-secondary); + background: #f8fafc; + border-color: rgba(148, 163, 184, 0.26); +} + +.root :global(.scan-terminal.light .scan-ai-contract) { + border-top-color: rgba(148, 163, 184, 0.2); +} + +.root :global(.scan-terminal.light .scan-ai-inline-button) { + color: #064e3b; + background: #d1fae5; + border-color: rgba(16, 185, 129, 0.28); +} + +.root :global(.scan-terminal.light .scan-cta-ghost), +.root :global(.scan-terminal.light .scan-detail-analysis-button) { + border-color: rgba(7, 160, 100, 0.24); + background: rgba(11, 188, 116, 0.12); + color: #057a4d; +} + +.root :global(.scan-terminal.light .scan-score-ring::after) { + background: #f5f9fe; +} + +.root :global(.scan-terminal.light .scan-score-ring span) { + color: #122033; +} + +.root :global(.scan-terminal.light .scan-trade-card p) { + color: #405977; +} + +.root :global(.scan-terminal.light .scan-map-shell) { + background: + radial-gradient(circle at 18% 18%, rgba(59, 130, 246, 0.12), transparent 34%), + linear-gradient(180deg, #f8fbff, #eaf3ff); + border-color: rgba(37, 99, 235, 0.16); + box-shadow: 0 16px 34px rgba(40, 70, 110, 0.12); +} + +.root :global(.scan-terminal.light .scan-map-shell .map), +.root :global(.scan-terminal.light .scan-map-shell .leaflet-container) { + background: #eaf3ff !important; +} + +.root :global(.scan-terminal.light .scan-map-shell .leaflet-tile) { + filter: saturate(1.06) contrast(0.98) brightness(1.04) !important; +} + +.root :global(.scan-terminal.light .scan-map-caption) { + color: var(--text-muted); +} + +/* ── Variable bridge: override dashboard-level CSS vars when + the scan terminal root itself carries the .light class ── */ +.root:global(.light) { + --bg-primary: #eef7ff; + --bg-secondary: #e0f2fe; + --bg-card: #ffffff; + --bg-glass: rgba(255, 255, 255, 0.92); + --border-glass: #cfe8ff; + --border-subtle: rgba(125, 171, 214, 0.34); + --text-primary: #0f172a; + --text-secondary: #334155; + --text-muted: #475569; +} + +/* ── Scan terminal shell light background ── */ +.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%); + color: var(--text-primary); +} + +.root :global(.scan-terminal.light .scan-map-shell), +.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); +} + +.root :global(.scan-terminal.light .scan-map-shell .leaflet-tile), +.root :global(.scan-terminal.light .scan-map-shell .map .leaflet-tile) { + filter: none !important; + opacity: 1 !important; +} + +/* ── Leaflet map light overrides (needs !important to beat Leaflet inline styles) ── */ +.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; +} + +/* ── 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: var(--text-primary); +} + +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); +} + +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: var(--text-primary); +} + +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: var(--text-secondary); +} + +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; +} + +/* ── 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); +} + +html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) { + background: #dbeafe; + border-color: rgba(59, 130, 246, 0.34); +} + +html.light .root :global(.scan-terminal.light .scan-ai-city-metrics b) { + color: var(--text-primary); +} + +html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) { + color: #1d4ed8; +} + +html.light .root :global(.scan-terminal.light .scan-ai-city-metrics small) { + color: #64748b; +} + +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: var(--text-secondary); +} + +html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.green) { + color: #047857; +} + +html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.blue) { + color: #1d4ed8; +} + +html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.amber) { + color: #b45309; +} + +html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.red) { + color: #b91c1c; +} + +/* ── 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%), + var(--bg-card); + border-color: rgba(37, 99, 235, 0.16); + box-shadow: 0 16px 36px rgba(61, 100, 145, 0.12); +} + +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: var(--text-primary); +} + +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: var(--text-secondary); +} + +html.light .root :global(.scan-terminal.light .scan-upgrade-announcement-copy span) { + color: #047857; +} + +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); +} + +html.light .root :global(.scan-terminal.light .scan-ai-decision-why) { + color: #1d4ed8; +} + +/* ── Section titles ── */ +html.light .root :global(.scan-terminal.light .scan-ai-city-section-title) { + color: #1d4ed8; +} + +html.light .root :global(.scan-terminal.light .scan-ai-decision-band span) { + color: #1d4ed8; +} + +html.light .root :global(.scan-terminal.light .scan-ai-decision-band p) { + color: var(--text-muted); +} + +html.light .root :global(.scan-terminal.light .scan-ai-decision-band strong) { + color: var(--text-primary); +} + +/* ── Scrollbar light overrides ── */ +html.light .root :global(.scan-terminal.light .scan-ai-city-body) { + scrollbar-color: rgba(37, 99, 235, 0.18) transparent; +} + +html.light .root :global(.scan-ai-city-body::-webkit-scrollbar-thumb) { + background: rgba(37, 99, 235, 0.22); +} + +/* ── AI prediction dual-card light mode ── */ +html.light .root :global(.scan-terminal.light .scan-ai-prediction-card) { + background: var(--bg-card); + border-color: var(--border-glass); +} + +html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai) { + background: linear-gradient(180deg, rgba(219, 234, 254, 0.52), var(--bg-card)); + border-color: rgba(14, 165, 233, 0.28); +} + +html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.deb) { + background: var(--bg-card); + border-color: var(--border-glass); +} + +html.light .root :global(.scan-terminal.light .scan-ai-prediction-card small) { + color: #64748b; +} + +html.light .root :global(.scan-terminal.light .scan-ai-prediction-card strong) { + color: var(--text-primary); +} + +html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai strong) { + color: #0369a1; +} + +html.light .root :global(.scan-terminal.light .scan-ai-prediction-card em) { + color: #64748b; +} + +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); +} + +html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending strong) { + color: #94a3b8; +} + +html.light .root :global(.scan-terminal.light .scan-ai-confidence) { + background: rgba(59, 130, 246, 0.1); + color: #2563eb; +} + +html.light .root :global(.scan-terminal.light .scan-ai-confidence.high) { + background: rgba(16, 185, 129, 0.12); + color: #047857; +} + +html.light .root :global(.scan-terminal.light .scan-ai-confidence.medium) { + background: rgba(245, 158, 11, 0.12); + color: #b45309; +} + +html.light .root :global(.scan-terminal.light .scan-ai-confidence.low) { + background: rgba(148, 163, 184, 0.12); + color: #64748b; +} + +html.light .root :global(.scan-terminal.light .scan-ai-confidence.neutral) { + background: rgba(148, 163, 184, 0.08); + color: #64748b; +} + +/* ── 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: var(--text-primary); +} + +html.light .root :global(.scan-terminal.light .scan-empty-copy), +html.light .root :global(.scan-terminal.light .scan-error-copy) { + color: var(--text-muted); +} + +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); +} + +/* ── AMOS runway panel light overrides ── */ +html.light .root :global(.scan-terminal.light .scan-amos-runway-panel) { + background: rgba(37, 99, 235, 0.03); + border-color: rgba(37, 99, 235, 0.12); +} + +html.light .root :global(.scan-terminal.light .scan-amos-runway-card) { + background: var(--bg-card); + border-color: rgba(37, 99, 235, 0.08); +} + +html.light .root :global(.scan-terminal.light .scan-amos-runway-label) { + color: #2563EB; +} + +html.light .root :global(.scan-terminal.light .scan-amos-runway-temp) { + color: var(--text-primary); +} + +html.light .root :global(.scan-terminal.light .scan-amos-source-tag) { + background: rgba(37, 99, 235, 0.08); + color: #2563EB; +} + +html.light .root :global(.scan-terminal.light .scan-amos-runway-detail) { + color: #64748B; +} diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index d2eba62f..6c01cacf 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -45,6 +45,7 @@ import { useScanTerminalTheme, useUserLocalClock, } from "@/components/dashboard/scan-terminal/use-scan-terminal-ui-state"; +import { useRelativeTime } from "@/hooks/useRelativeTime"; const MonitorPanel = dynamic( () => import("@/components/dashboard/monitoring/MonitorPanel"), { ssr: false }, @@ -113,6 +114,20 @@ function ScanTerminalScreen() { const userLocalTime = useUserLocalClock(); const { setThemeMode, themeMode } = useScanTerminalTheme(); const lastMapSelectedCityRef = useRef(""); + const lastFetchedAtRef = useRef(0); + const serverAgeText = useRelativeTime(terminalData?.generated_at ?? null); + const localAgeText = useRelativeTime( + lastFetchedAtRef.current + ? new Date(lastFetchedAtRef.current).toISOString() + : null, + ); + + useEffect(() => { + if (terminalData?.generated_at) { + lastFetchedAtRef.current = Date.now(); + } + }, [terminalData?.generated_at]); + const scanTerminalRootClassName = clsx( styles.root, scanRootClass, @@ -446,20 +461,13 @@ function ScanTerminalScreen() {
{terminalData?.generated_at ? ( - - {isEn ? "Updated" : "已更新"}{" "} - {new Date(terminalData.generated_at).toLocaleTimeString( - isEn ? "en-US" : "zh-CN", - { - hour: "2-digit", - minute: "2-digit", - }, - )} + + {isEn ? "Updated" : "已更新"} {serverAgeText || ""} ) : null} - {terminalData?.stale ? ( + {terminalData?.stale && localAgeText ? ( - {isEn ? "Delayed snapshot" : "延迟快照"} + {isEn ? "Local fetch " : "本地下发 "}{localAgeText} ) : null} {isPro ? ( diff --git a/frontend/components/dashboard/ScanTerminalLightTheme.module.css b/frontend/components/dashboard/ScanTerminalLightTheme.module.css deleted file mode 100644 index d9422462..00000000 --- a/frontend/components/dashboard/ScanTerminalLightTheme.module.css +++ /dev/null @@ -1,1012 +0,0 @@ -/* Scan terminal light theme overrides. Keep imported after other scan CSS modules. */ - -.root :global(.scan-terminal.light) { - background: - radial-gradient(circle at top, rgba(59, 130, 246, 0.1), transparent 34%), - linear-gradient(180deg, #F7F9FC 0%, #EEF2F7 100%); - color: #0F172A; -} - -.root :global(.scan-terminal.light .scan-filter-panel), -.root :global(.scan-terminal.light .scan-data-grid), -.root :global(.scan-terminal.light .scan-detail-panel), -.root :global(.scan-terminal.light > .detail-panel.scan-city-detail-rail) { - border-color: #E2E8F0; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.96)); - box-shadow: 0 16px 34px rgba(40, 70, 110, 0.12); -} - -.root :global(.scan-terminal.light .scan-sidebar-brand-name), -.root :global(.scan-terminal.light .scan-topbar-tab.active), -.root :global(.scan-terminal.light .scan-hero h1), -.root :global(.scan-terminal.light .scan-topbar-title strong), -.root :global(.scan-terminal.light .scan-city-name), -.root :global(.scan-terminal.light .scan-opportunity-city strong), -.root :global(.scan-terminal.light .panel-title-area h2), -.root :global(.scan-terminal.light .detail-summary-temp), -.root :global(.scan-terminal.light .scan-detail-city-name), -.root :global(.scan-terminal.light .scan-empty-title), -.root :global(.scan-terminal.light .scan-detail-section-title), -.root :global(.scan-terminal.light .scan-detail-volume-big), -.root :global(.scan-terminal.light .scan-kv strong), -.root :global(.scan-terminal.light .scan-time-main), -.root :global(.scan-terminal.light .scan-summary-value), -.root :global(.scan-terminal.light .scan-distribution-card strong), -.root :global(.scan-terminal.light .scan-table-header), -.root :global(.scan-terminal.light .scan-list-tabs button.active) { - color: #0F172A; -} - -.root :global(.scan-terminal.light .scan-filter-heading), -.root :global(.scan-terminal.light .scan-topbar-actions), -.root :global(.scan-terminal.light .scan-topbar-time), -.root :global(.scan-terminal.light .scan-hero p), -.root :global(.scan-terminal.light .scan-topbar-title span), -.root :global(.scan-terminal.light .scan-city-sub), -.root :global(.scan-terminal.light .scan-opportunity-phase), -.root :global(.scan-terminal.light .scan-opportunity-phase em), -.root :global(.scan-terminal.light .panel-overline), -.root :global(.scan-terminal.light .detail-summary-supporting), -.root :global(.scan-terminal.light .detail-value-muted), -.root :global(.scan-terminal.light .detail-empty-state), -.root :global(.scan-terminal.light .scan-time-remaining), -.root :global(.scan-terminal.light .scan-detail-city-sub), -.root :global(.scan-terminal.light .scan-detail-volume-caption), -.root :global(.scan-terminal.light .scan-empty-copy), -.root :global(.scan-terminal.light .scan-loading-copy-block span), -.root :global(.scan-terminal.light .scan-kv span:first-child), -.root :global(.scan-terminal.light .scan-chart-label), -.root :global(.scan-terminal.light .scan-trade-sub), -.root :global(.scan-terminal.light .scan-trade-note) { - color: #475569; -} - -.root :global(.scan-terminal.light .scan-loading-copy-block strong) { - color: #0f172a; -} - -.root :global(.scan-terminal.light .scan-loading-signal) { - border-color: #dbeafe; - background: - radial-gradient(circle at 18% 18%, rgba(59, 130, 246, 0.12), transparent 34%), - linear-gradient(145deg, rgba(255, 255, 255, 0.96), rgba(241, 245, 249, 0.9)); - box-shadow: 0 16px 34px rgba(40, 70, 110, 0.1); -} - -.root :global(.scan-terminal.light .scan-loading-signal.compact) { - background: - radial-gradient(circle at 18% 18%, rgba(59, 130, 246, 0.1), transparent 34%), - 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); -} - -.root :global(.scan-terminal.light .scan-loading-node.hot) { - background: linear-gradient(135deg, #f97316, #fde047); -} - -.root :global(.scan-terminal.light .scan-loading-node.market) { - background: linear-gradient(135deg, #38bdf8, #2563eb); -} - -.root :global(.scan-terminal.light .scan-loading-node.action) { - background: linear-gradient(135deg, #16a34a, #22c55e); -} - -.root :global(.scan-terminal.light .scan-list-tabs button), -.root :global(.scan-terminal.light .scan-mode-tab-sub), -.root :global(.scan-terminal.light .scan-opportunity-models em), -.root :global(.scan-terminal.light .scan-opportunity-stat small), -.root :global(.scan-terminal.light .panel-meta-chip), -.root :global(.scan-terminal.light .panel-weather-chip), -.root :global(.scan-terminal.light .scan-distribution-card), -.root :global(.scan-terminal.light .scan-chart-legend) { - color: #94A3B8; -} - -.root :global(.scan-terminal.light .scan-distribution-line em) { - color: #243b5a; -} - -.root :global(.scan-terminal.light .scan-distribution-line b) { - color: #6f86a4; -} - -.root :global(.scan-terminal.light .scan-mode-tab), -.root :global(.scan-terminal.light .scan-table-shell), -.root :global(.scan-terminal.light .scan-table-header), -.root :global(.scan-terminal.light .scan-summary-card), -.root :global(.scan-terminal.light .scan-settings-card), -.root :global(.scan-terminal.light .scan-distribution-card), -.root :global(.scan-terminal.light .scan-opportunity-group), -.root :global(.scan-terminal.light .detail-summary-shell), -.root :global(.scan-terminal.light .detail-structured-section), -.root :global(.scan-terminal.light .detail-card), -.root :global(.scan-terminal.light .scan-trade-card) { - border-color: #E2E8F0; - background: #FFFFFF; -} - -.root :global(.scan-terminal.light .scan-mode-tab.active) { - color: #1D4ED8; - background: #DBEAFE; - border-color: rgba(59, 130, 246, 0.34); - box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.16); -} - -.root :global(.scan-terminal.light .scan-mode-tab.active .scan-mode-tab-label) { - color: #1D4ED8; -} - -.root :global(.scan-terminal.light .scan-mode-tab.active .scan-mode-tab-sub) { - color: #475569; -} - -.root :global(.scan-terminal.light .scan-mode-tab.active .scan-mode-icon) { - background: rgba(16, 185, 129, 0.16); - color: #069668; -} - -.root :global(.scan-terminal.light .scan-table-row.selected) { - background: linear-gradient(180deg, rgba(228, 252, 242, 0.92), rgba(241, 250, 247, 0.96)); - border-color: rgba(10, 160, 100, 0.34); - box-shadow: inset 0 0 0 1px rgba(10, 160, 100, 0.18); -} - -.root :global(.scan-terminal.light .scan-table-row) { - background: linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(247, 250, 254, 0.9)); - border-bottom-color: rgba(35, 72, 118, 0.08); -} - -.root :global(.scan-terminal.light .scan-table-row:hover) { - background: linear-gradient(180deg, rgba(248, 252, 255, 0.96), rgba(238, 247, 255, 0.96)); -} - -.root :global(.scan-terminal.light .scan-opportunity-phase > span) { - color: #14253a; -} - -.root :global(.scan-terminal.light .scan-opportunity-group-head) { - background: rgba(246, 250, 255, 0.78); - border-bottom-color: rgba(35, 72, 118, 0.1); -} - -.root :global(.scan-terminal.light .scan-opportunity-item) { - background: rgba(255, 255, 255, 0.62); - color: #405977; - border-bottom-color: rgba(35, 72, 118, 0.08); - border-color: rgba(35, 72, 118, 0.1); -} - -.root :global(.scan-terminal.light .scan-opportunity-item:hover) { - background: rgba(230, 242, 255, 0.76); -} - -.root :global(.scan-terminal.light .scan-opportunity-item.selected) { - background: linear-gradient(180deg, rgba(222, 250, 238, 0.9), rgba(241, 250, 247, 0.96)); - border-color: rgba(10, 160, 100, 0.34); -} - -.root :global(.scan-terminal.light .scan-opportunity-group.selected) { - border-color: rgba(10, 160, 100, 0.34); - box-shadow: inset 0 0 0 1px rgba(10, 160, 100, 0.18); -} - -.root :global(.scan-terminal.light .scan-opportunity-models span) { - border-color: rgba(35, 72, 118, 0.1); - background: rgba(255, 255, 255, 0.68); -} - -.root :global(.scan-terminal.light .scan-opportunity-stat) { - border-color: rgba(35, 72, 118, 0.1); - background: rgba(248, 252, 255, 0.76); -} - -.root :global(.scan-terminal.light .scan-opportunity-models b), -.root :global(.scan-terminal.light .scan-opportunity-stat b) { - color: #14253a; -} - -.root :global(.scan-terminal.light .scan-opportunity-branch::before), -.root :global(.scan-terminal.light .scan-opportunity-branch i) { - background: rgba(56, 86, 126, 0.24); -} - -.root :global(.scan-terminal.light .panel-header), -.root :global(.scan-terminal.light .panel-body section) { - border-color: rgba(35, 72, 118, 0.1); -} - -.root :global(.scan-terminal.light .panel-action-button-secondary), -.root :global(.scan-terminal.light .panel-action-button-ghost) { - border-color: rgba(35, 72, 118, 0.14); - background: rgba(255, 255, 255, 0.76); -} - -.root :global(.scan-terminal.light .detail-summary-shell-live) { - background: linear-gradient(180deg, rgba(224, 247, 255, 0.82), rgba(241, 248, 255, 0.92)); -} - -.root :global(.scan-terminal.light .scan-locale-switch), -.root :global(.scan-terminal.light .scan-ghost-button), -.root :global(.scan-terminal.light .scan-theme-button), -.root :global(.scan-terminal.light .scan-sort-pill), -.root :global(.scan-terminal.light .scan-icon-pill), -.root :global(.scan-terminal.light .scan-account-button), -.root :global(.scan-terminal.light .scan-detail-action-button) { - border-color: rgba(35, 72, 118, 0.14); - background: rgba(255, 255, 255, 0.78); - color: #1f3654; -} - -.root :global(.scan-terminal.light .scan-primary-button) { - color: #FFFFFF; - border-color: rgba(59, 130, 246, 0.34); - background: linear-gradient(180deg, #3B82F6, #2563EB); -} - -.root :global(.scan-terminal.light .scan-ai-button) { - color: #1D4ED8; - border-color: rgba(59, 130, 246, 0.28); - background: #DBEAFE; -} - -.root :global(.scan-terminal.light .scan-opportunity-ai) { - border-color: rgba(35, 72, 118, 0.1); - background: rgba(248, 252, 255, 0.76); -} - -.root :global(.scan-terminal.light .scan-opportunity-ai small) { - color: #647a98; -} - -.root :global(.scan-terminal.light .scan-opportunity-strength) { - border-color: rgba(35, 72, 118, 0.12); - background: rgba(248, 252, 255, 0.78); -} - -.root :global(.scan-terminal.light .scan-opportunity-expand) { - color: #1d4ed8; - border-color: rgba(37, 99, 235, 0.18); - background: rgba(239, 246, 255, 0.84); -} - -.root :global(.scan-terminal.light .scan-v4-analysis), -.root :global(.scan-terminal.light .scan-v4-analysis section), -.root :global(.scan-terminal.light .scan-v4-evidence > div > span), -.root :global(.scan-terminal.light .scan-v4-model-sources span) { - border-color: rgba(35, 72, 118, 0.12); - background: rgba(255, 255, 255, 0.76); -} - -.root :global(.scan-terminal.light .scan-v4-analysis strong) { - color: #057a6b; -} - -.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: var(--color-text-muted); -} - -.root :global(.scan-terminal.light .scan-v4-model-sources em) { - color: var(--color-text-muted); -} - -.root :global(.scan-terminal.light .scan-v4-model-sources b) { - color: #0f172a; -} - -.root :global(.scan-terminal.light .scan-forecast-city-card), -.root :global(.scan-terminal.light .scan-forecast-row), -.root :global(.scan-terminal.light .scan-ai-analysis) { - border-color: #E2E8F0; - background: #FFFFFF; -} - -.root :global(.scan-terminal.light .scan-forecast-city-head) { - background: #EEF2F7; -} - -.root :global(.scan-terminal.light .scan-forecast-city-card.selected), -.root :global(.scan-terminal.light .scan-forecast-city-card:has(.scan-forecast-row.selected)) { - border-color: rgba(59, 130, 246, 0.38); - box-shadow: - inset 0 0 0 1px rgba(59, 130, 246, 0.12), - 0 16px 34px rgba(40, 70, 110, 0.12); -} - -.root :global(.scan-terminal.light .scan-forecast-row.selected), -.root :global(.scan-terminal.light .scan-forecast-row.expanded) { - border-color: rgba(59, 130, 246, 0.38); - background: - linear-gradient(180deg, rgba(219, 234, 254, 0.62), rgba(255, 255, 255, 0.98)), - #FFFFFF; - box-shadow: inset 4px 0 0 #3B82F6; -} - -.root :global(.scan-terminal.light .scan-forecast-city-title strong), -.root :global(.scan-terminal.light .scan-forecast-city-read > b:not(.scan-phase-badge)), -.root :global(.scan-terminal.light .scan-forecast-bucket strong), -.root :global(.scan-terminal.light .scan-forecast-signals b), -.root :global(.scan-terminal.light .scan-ai-temperature-line b) { - color: #0F172A; -} - -.root :global(.scan-terminal.light .scan-forecast-city-chips span), -.root :global(.scan-terminal.light .scan-forecast-signals span), -.root :global(.scan-terminal.light .scan-ai-evidence-line span) { - border-color: #E2E8F0; - background: #EEF2F7; -} - -.root :global(.scan-terminal.light .scan-forecast-city-chips span), -.root :global(.scan-terminal.light .scan-forecast-city-read span), -.root :global(.scan-terminal.light .scan-forecast-bucket small), -.root :global(.scan-terminal.light .scan-forecast-ai-line small), -.root :global(.scan-terminal.light .scan-ai-brief-grid li), -.root :global(.scan-terminal.light .scan-ai-airport-read p) { - color: #475569; -} - -.root :global(.scan-terminal.light .scan-ai-analysis-head p) { - color: #0F172A; -} - -.root :global(.scan-terminal.light .scan-ai-analysis-head small), -.root :global(.scan-terminal.light .scan-ai-evidence-line b) { - color: #475569; -} - -.root :global(.scan-terminal.light .scan-ai-workspace) { - background: #f7f9fc; - border-color: #e2e8f0; -} - -.root :global(.scan-terminal.light .scan-opportunity-overview) { - background: #f7f9fc; - border-color: #e2e8f0; -} - -.root :global(.scan-terminal.light .scan-opportunity-hero > div:first-child), -.root :global(.scan-terminal.light .scan-opportunity-summary span), -.root :global(.scan-terminal.light .scan-opportunity-lane), -.root :global(.scan-terminal.light .scan-opportunity-decision-card), -.root :global(.scan-terminal.light .scan-opportunity-decision-primary span) { - background: #ffffff; - border-color: #e2e8f0; -} - -.root :global(.scan-terminal.light .scan-opportunity-hero strong), -.root :global(.scan-terminal.light .scan-opportunity-summary b), -.root :global(.scan-terminal.light .scan-opportunity-lane-head strong), -.root :global(.scan-terminal.light .scan-opportunity-decision-head strong), -.root :global(.scan-terminal.light .scan-opportunity-decision-primary b) { - color: #0f172a; -} - -.root :global(.scan-terminal.light .scan-opportunity-hero p), -.root :global(.scan-terminal.light .scan-opportunity-summary span), -.root :global(.scan-terminal.light .scan-opportunity-lane-head p), -.root :global(.scan-terminal.light .scan-opportunity-decision-card p), -.root :global(.scan-terminal.light .scan-opportunity-decision-head span), -.root :global(.scan-terminal.light .scan-opportunity-decision-primary span), -.root :global(.scan-terminal.light .scan-opportunity-decision-foot small) { - color: #64748b; -} - -.root :global(.scan-terminal.light .scan-ai-city-card), -.root :global(.scan-terminal.light .scan-ai-city-hero) { - background: #ffffff; - border-color: #e2e8f0; -} - -.root :global(.scan-terminal.light .scan-ai-city-section), -.root :global(.scan-terminal.light .scan-ai-decision-band), -.root :global(.scan-terminal.light .scan-ai-market-decision), -.root :global(.scan-terminal.light .scan-ai-market-decision-stats small), -.root :global(.scan-terminal.light .scan-ai-decision-metrics span), -.root :global(.scan-terminal.light .scan-ai-market-bucket), -.root :global(.scan-terminal.light .scan-ai-city-pills span), -.root :global(.scan-terminal.light .scan-ai-city-freshness span), -.root :global(.scan-terminal.light .scan-ai-city-metrics > span) { - background: #eef2f7; - border-color: #e2e8f0; -} - -.root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) { - background: #dbeafe; - border-color: rgba(59, 130, 246, 0.28); -} - -.root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) { - color: #1d4ed8; -} - -.root :global(.scan-terminal.light .scan-ai-city-metrics small) { - color: #64748b; -} - -.root :global(.scan-terminal.light .scan-ai-workspace-head strong), -.root :global(.scan-terminal.light .scan-ai-city-hero h3), -.root :global(.scan-terminal.light .scan-ai-city-metrics b), -.root :global(.scan-terminal.light .scan-ai-decision-band strong), -.root :global(.scan-terminal.light .scan-ai-decision-metrics b), -.root :global(.scan-terminal.light .scan-ai-market-bucket strong), -.root :global(.scan-terminal.light .scan-ai-weather-summary), -.root :global(.scan-terminal.light .scan-ai-market-decision strong), -.root :global(.scan-terminal.light .scan-ai-market-decision-stats b) { - color: #0f172a; -} - -.root :global(.scan-terminal.light .scan-ai-workspace-head p), -.root :global(.scan-terminal.light .scan-ai-city-section p), -.root :global(.scan-terminal.light .scan-ai-decision-band p), -.root :global(.scan-terminal.light .scan-ai-city-pills span), -.root :global(.scan-terminal.light .scan-ai-city-freshness), -.root :global(.scan-terminal.light .scan-ai-decision-metrics span), -.root :global(.scan-terminal.light .scan-ai-market-bucket span), -.root :global(.scan-terminal.light .scan-ai-market-decision span), -.root :global(.scan-terminal.light .scan-ai-market-decision p), -.root :global(.scan-terminal.light .scan-ai-market-decision-stats small), -.root :global(.scan-terminal.light .scan-ai-city-muted), -.root :global(.scan-terminal.light .scan-ai-city-loading), -.root :global(.scan-terminal.light .scan-ai-city-chart-legend), -.root :global(.scan-terminal.light .scan-ai-weather-bullets) { - color: #475569; -} - -.root :global(.scan-terminal.light .scan-ai-raw-metar) { - border-top-color: #e2e8f0; - color: #64748b; -} - -.root :global(.scan-terminal.light .scan-ai-city-chart-placeholder) { - border-color: #cbd5e1; - background: #f8fafc; - color: #64748b; -} - -.root :global(.scan-terminal.light .scan-ai-log-panel) { - border-color: rgba(35, 72, 118, 0.12); - background: linear-gradient(180deg, rgba(255, 255, 255, 0.8), rgba(246, 250, 255, 0.9)); -} - -.root :global(.scan-terminal.light .scan-ai-log-panel.compact) { - background: rgba(255, 255, 255, 0.82); -} - -.root :global(.scan-terminal.light .scan-ai-log-panel.compact .scan-ai-log-summary span) { - color: #334155; -} - -.root :global(.scan-terminal.light .scan-ai-log-panel.compact .scan-ai-log-summary strong) { - color: #2563eb; -} - -.root :global(.scan-terminal.light .scan-ai-log-head strong), -.root :global(.scan-terminal.light .scan-ai-log-item b) { - color: #122033; -} - -.root :global(.scan-terminal.light .scan-ai-log-head span), -.root :global(.scan-terminal.light .scan-ai-log-time), -.root :global(.scan-terminal.light .scan-ai-log-empty) { - color: #647a98; -} - -.root :global(.scan-terminal.light .scan-ai-log-item) { - border-color: rgba(35, 72, 118, 0.12); - background: rgba(255, 255, 255, 0.72); -} - -.root :global(.scan-terminal.light .scan-ai-log-item small) { - color: #58708f; -} - -.root :global(.scan-terminal.light .scan-ai-log-item.info) { - border-color: rgba(14, 165, 233, 0.24); -} - -.root :global(.scan-terminal.light .scan-ai-log-item.success) { - border-color: rgba(10, 160, 100, 0.28); -} - -.root :global(.scan-terminal.light .scan-ai-log-item.warning) { - border-color: rgba(217, 119, 6, 0.28); -} - -.root :global(.scan-terminal.light .scan-ai-log-item.error) { - border-color: rgba(220, 38, 38, 0.28); -} - -.root :global(.scan-terminal.light .scan-ai-summary-card), -.root :global(.scan-terminal.light .scan-ai-city-card) { - background: rgba(255, 255, 255, 0.92); - border-color: rgba(148, 163, 184, 0.28); -} - -.root :global(.scan-terminal.light .scan-ai-summary-card strong), -.root :global(.scan-terminal.light .scan-ai-city-head strong), -.root :global(.scan-terminal.light .scan-ai-contract b) { - color: #0f172a; -} - -.root :global(.scan-terminal.light .scan-ai-summary-card p), -.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: var(--color-text-muted); -} - -.root :global(.scan-terminal.light .scan-ai-city-head) { - background: #f8fafc; -} - -.root :global(.scan-terminal.light .scan-ai-cluster-note) { - color: #334155; - background: #f8fafc; - border-color: rgba(148, 163, 184, 0.26); -} - -.root :global(.scan-terminal.light .scan-ai-contract) { - border-top-color: rgba(148, 163, 184, 0.2); -} - -.root :global(.scan-terminal.light .scan-ai-inline-button) { - color: #064e3b; - background: #d1fae5; - border-color: rgba(16, 185, 129, 0.28); -} - -.root :global(.scan-terminal.light .scan-cta-ghost), -.root :global(.scan-terminal.light .scan-detail-analysis-button) { - border-color: rgba(7, 160, 100, 0.24); - background: rgba(11, 188, 116, 0.12); - color: #057a4d; -} - -.root :global(.scan-terminal.light .scan-score-ring::after) { - background: #f5f9fe; -} - -.root :global(.scan-terminal.light .scan-score-ring span) { - color: #122033; -} - -.root :global(.scan-terminal.light .scan-trade-card p) { - color: #405977; -} - -.root :global(.scan-terminal.light .scan-map-shell) { - background: - radial-gradient(circle at 18% 18%, rgba(59, 130, 246, 0.12), transparent 34%), - linear-gradient(180deg, #f8fbff, #eaf3ff); - border-color: rgba(37, 99, 235, 0.16); - box-shadow: 0 16px 34px rgba(40, 70, 110, 0.12); -} - -.root :global(.scan-terminal.light .scan-map-shell .map), -.root :global(.scan-terminal.light .scan-map-shell .leaflet-container) { - background: #eaf3ff !important; -} - -.root :global(.scan-terminal.light .scan-map-shell .leaflet-tile) { - filter: saturate(1.06) contrast(0.98) brightness(1.04) !important; -} - -.root :global(.scan-terminal.light .scan-map-caption) { - color: #475569; -} - -.root :global(.scan-terminal.light .scan-list-tabs button), -.root :global(.scan-terminal.light .scan-mode-tab-sub), -.root :global(.scan-terminal.light .scan-opportunity-models em), -.root :global(.scan-terminal.light .scan-opportunity-stat small), -.root :global(.scan-terminal.light .panel-meta-chip), -.root :global(.scan-terminal.light .panel-weather-chip), -.root :global(.scan-terminal.light .scan-distribution-card), -.root :global(.scan-terminal.light .scan-chart-legend), -.root :global(.scan-terminal.light .scan-opportunity-ai small), -.root :global(.scan-terminal.light .scan-ai-log-head span), -.root :global(.scan-terminal.light .scan-ai-log-time), -.root :global(.scan-terminal.light .scan-ai-log-empty), -.root :global(.scan-terminal.light .scan-ai-log-item small), -.root :global(.scan-terminal.light .scan-ai-summary-card p), -.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), -.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), -.root :global(.scan-terminal.light .scan-v4-model-sources em), -.root :global(.scan-terminal.light .scan-ai-raw-metar), -.root :global(.scan-terminal.light .scan-opportunity-hero p), -.root :global(.scan-terminal.light .scan-opportunity-summary span), -.root :global(.scan-terminal.light .scan-opportunity-lane-head p), -.root :global(.scan-terminal.light .scan-opportunity-decision-card p), -.root :global(.scan-terminal.light .scan-opportunity-decision-head span), -.root :global(.scan-terminal.light .scan-opportunity-decision-primary span), -.root :global(.scan-terminal.light .scan-opportunity-decision-foot small) { - color: #475569; -} - -/* 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; - --bg-card: #ffffff; - --bg-glass: rgba(255, 255, 255, 0.92); - --border-glass: #cfe8ff; - --border-subtle: rgba(125, 171, 214, 0.34); - --text-primary: #0f172a; - --text-secondary: #334155; - --text-muted: #475569; -} - -/* ── 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%); - color: #0f172a; -} - -/* ── 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; -} - -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); -} - -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; -} - -/* ── 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; -} - -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); -} - -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; -} - -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; -} - -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; -} - -/* ── 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); -} - -html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary) { - background: #dbeafe; - border-color: rgba(59, 130, 246, 0.34); -} - -html.light .root :global(.scan-terminal.light .scan-ai-city-metrics b) { - color: #0f172a; -} - -html.light .root :global(.scan-terminal.light .scan-ai-city-metrics > span.primary b) { - color: #1d4ed8; -} - -html.light .root :global(.scan-terminal.light .scan-ai-city-metrics small) { - color: #64748b; -} - -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; -} - -html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.green) { - color: #047857; -} - -html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.blue) { - color: #1d4ed8; -} - -html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.amber) { - color: #b45309; -} - -html.light .root :global(.scan-terminal.light .scan-ai-city-status-tag.red) { - color: #b91c1c; -} - -/* ── 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; - border-color: rgba(37, 99, 235, 0.16); - box-shadow: 0 16px 36px rgba(61, 100, 145, 0.12); -} - -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; -} - -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; -} - -html.light .root :global(.scan-terminal.light .scan-upgrade-announcement-copy span) { - color: #047857; -} - -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); -} - -html.light .root :global(.scan-terminal.light .scan-ai-decision-why) { - color: #1d4ed8; -} - -/* ── Section titles ── */ -html.light .root :global(.scan-terminal.light .scan-ai-city-section-title) { - color: #1d4ed8; -} - -html.light .root :global(.scan-terminal.light .scan-ai-decision-band span) { - color: #1d4ed8; -} - -html.light .root :global(.scan-terminal.light .scan-ai-decision-band p) { - color: #475569; -} - -html.light .root :global(.scan-terminal.light .scan-ai-decision-band strong) { - color: #0f172a; -} - -/* ── Scrollbar light overrides ── */ -html.light .root :global(.scan-terminal.light .scan-ai-city-body) { - scrollbar-color: rgba(37, 99, 235, 0.18) transparent; -} - -html.light .root :global(.scan-ai-city-body::-webkit-scrollbar-thumb) { - background: rgba(37, 99, 235, 0.22); -} - -/* ── AI prediction dual-card light mode ── */ -html.light .root :global(.scan-terminal.light .scan-ai-prediction-card) { - background: #ffffff; - border-color: #e2e8f0; -} - -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); -} - -html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.deb) { - background: #ffffff; - border-color: #e2e8f0; -} - -html.light .root :global(.scan-terminal.light .scan-ai-prediction-card small) { - color: #64748b; -} - -html.light .root :global(.scan-terminal.light .scan-ai-prediction-card strong) { - color: #0f172a; -} - -html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai strong) { - color: #0369a1; -} - -html.light .root :global(.scan-terminal.light .scan-ai-prediction-card em) { - color: #64748b; -} - -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); -} - -html.light .root :global(.scan-terminal.light .scan-ai-prediction-card.ai.pending strong) { - color: #94a3b8; -} - -html.light .root :global(.scan-terminal.light .scan-ai-confidence) { - background: rgba(59, 130, 246, 0.1); - color: #2563eb; -} - -html.light .root :global(.scan-terminal.light .scan-ai-confidence.high) { - background: rgba(16, 185, 129, 0.12); - color: #047857; -} - -html.light .root :global(.scan-terminal.light .scan-ai-confidence.medium) { - background: rgba(245, 158, 11, 0.12); - color: #b45309; -} - -html.light .root :global(.scan-terminal.light .scan-ai-confidence.low) { - background: rgba(148, 163, 184, 0.12); - color: #64748b; -} - -html.light .root :global(.scan-terminal.light .scan-ai-confidence.neutral) { - background: rgba(148, 163, 184, 0.08); - color: #64748b; -} - -/* ── 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); -} - -/* ── AMOS runway panel light overrides ── */ -html.light .root :global(.scan-terminal.light .scan-amos-runway-panel) { - background: rgba(37, 99, 235, 0.03); - border-color: rgba(37, 99, 235, 0.12); -} - -html.light .root :global(.scan-terminal.light .scan-amos-runway-card) { - background: #ffffff; - border-color: rgba(37, 99, 235, 0.08); -} - -html.light .root :global(.scan-terminal.light .scan-amos-runway-label) { - color: #2563EB; -} - -html.light .root :global(.scan-terminal.light .scan-amos-runway-temp) { - color: #0F172A; -} - -html.light .root :global(.scan-terminal.light .scan-amos-source-tag) { - background: rgba(37, 99, 235, 0.08); - color: #2563EB; -} - -html.light .root :global(.scan-terminal.light .scan-amos-runway-detail) { - color: #64748B; -} diff --git a/frontend/components/dashboard/WeatherAuraLayer.tsx b/frontend/components/dashboard/WeatherAuraLayer.tsx index 57bd2fa4..a5c116c9 100644 --- a/frontend/components/dashboard/WeatherAuraLayer.tsx +++ b/frontend/components/dashboard/WeatherAuraLayer.tsx @@ -1,11 +1,15 @@ "use client"; -import { CSSProperties, useEffect, useRef, useState } from "react"; -import * as THREE from "three"; +import { CSSProperties, useEffect, useMemo, useState } from "react"; import { useDashboardSelection } from "@/hooks/useDashboardStore"; import { usePrefersReducedMotion } from "@/hooks/usePrefersReducedMotion"; import { getWeatherAuraProfile } from "@/lib/weather-aura"; -import styles from "./Dashboard.module.css"; + +/* ────────────────────────────────────────────────────────────── + Pure-CSS weather-aura particle layer + Replaces the former Three.js WebGL implementation with + CSS @keyframes animations — no canvas, no WebGL. + ────────────────────────────────────────────────────────────── */ function hexToRgba(hex: string, alpha: number) { const sanitized = hex.replace("#", ""); @@ -23,453 +27,390 @@ function hexToRgba(hex: string, alpha: number) { return `rgba(${r}, ${g}, ${b}, ${alpha})`; } +/* ── Injection-safe style IDs ── */ + +const AURA_STYLE_ID = "poly-aura-keyframes"; + +/* ── CSS @keyframes (injected once via