| import React, { useEffect, useState, useRef } from 'react'; |
| import { useAuth } from '../context/AuthContext'; |
| import { Network, Server, Cpu, Database, Activity, ArrowRight, Zap, GitBranch, MessageSquare, TrendingUp, RefreshCw } from 'lucide-react'; |
|
|
| const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'; |
|
|
| |
| function useCountUp(target: number, duration = 1200) { |
| const [val, setVal] = useState(0); |
| const prev = useRef(0); |
| useEffect(() => { |
| if (target === 0) { setVal(0); return; } |
| const start = prev.current; |
| const diff = target - start; |
| const startTime = performance.now(); |
| const tick = (now: number) => { |
| const t = Math.min((now - startTime) / duration, 1); |
| const ease = 1 - Math.pow(1 - t, 3); |
| setVal(Math.round(start + diff * ease)); |
| if (t < 1) requestAnimationFrame(tick); |
| else prev.current = target; |
| }; |
| requestAnimationFrame(tick); |
| }, [target]); |
| return val; |
| } |
|
|
| const StatCounter: React.FC<{ value: number | string; label: string; suffix?: string }> = ({ value, label, suffix = '' }) => { |
| const numVal = typeof value === 'number' ? value : parseInt(String(value)) || 0; |
| const animated = useCountUp(numVal); |
| return ( |
| <div className="hm-stat-block"> |
| <div className="hm-stat-value">{typeof value === 'number' ? animated : value}{suffix}</div> |
| <div className="hm-stat-key">{label}</div> |
| </div> |
| ); |
| }; |
|
|
| const Home: React.FC = () => { |
| const { token, logout, user } = useAuth(); |
| const [health, setHealth] = useState<any>(null); |
| const [stats, setStats] = useState<any>(null); |
| const [myStats, setMyStats] = useState<any>(null); |
| const [loading, setLoading] = useState(true); |
| const [lastRefresh, setLastRefresh] = useState<Date>(new Date()); |
|
|
| const fetchData = async () => { |
| setLoading(true); |
| try { |
| const [healthRes, statsRes, myStatsRes] = await Promise.all([ |
| fetch(`${API_BASE}/system/health`), |
| fetch(`${API_BASE}/system/stats`, { headers: { Authorization: `Bearer ${token}` } }), |
| fetch(`${API_BASE}/system/my-stats`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null), |
| ]); |
| if (statsRes.status === 401) { logout(); return; } |
| if (healthRes.ok) setHealth(await healthRes.json()); |
| if (statsRes.ok) setStats(await statsRes.json()); |
| if (myStatsRes?.ok) setMyStats(await myStatsRes.json()); |
| } catch (err) { |
| console.error('Failed to fetch system data', err); |
| } finally { |
| setLoading(false); |
| setLastRefresh(new Date()); |
| } |
| }; |
|
|
| useEffect(() => { |
| fetchData(); |
| const interval = setInterval(fetchData, 30000); |
| return () => clearInterval(interval); |
| }, [token]); |
|
|
| const isOnline = (v: boolean | undefined) => v === true; |
|
|
| const systemOk = health ? (isOnline(health.neo4j_connected) && isOnline(health.redis_connected)) : false; |
|
|
| return ( |
| <div className="container fade-in max-w-[1300px] pb-12"> |
| |
| {/* ββ Hero βββββββββββββββββββββββββββββββββββββββ */} |
| <div className="hm-hero"> |
| <div className="hm-hero-left"> |
| <div className="hm-hero-label">CORTEX PLATFORM</div> |
| <h1 className="hm-hero-title"> |
| Agentic Knowledge<br /> |
| <span className="hm-hero-accent">Intelligence</span> |
| </h1> |
| <p className="hm-hero-sub"> |
| Production-grade knowledge graph Β· Real-time extraction Β· Multi-hop reasoning |
| </p> |
| <div className="hm-hero-ctas"> |
| <a href="/process" className="hm-cta-primary"> |
| <Database size={16} /> Ingest Documents <ArrowRight size={14} /> |
| </a> |
| <a href="/interact" className="hm-cta-secondary"> |
| <Cpu size={16} /> Query Graph <ArrowRight size={14} /> |
| </a> |
| </div> |
| </div> |
| |
| <div className="hm-hero-right"> |
| <div className={`hm-system-badge ${systemOk ? 'ok' : 'warn'}`}> |
| <span className={`hm-pulse-dot ${systemOk ? 'green' : 'yellow'}`} /> |
| <span className="hm-badge-label"> |
| {loading ? 'CHECKING...' : systemOk ? 'SYSTEM OPERATIONAL' : 'SYSTEM DEGRADED'} |
| </span> |
| <button className="hm-refresh-btn" onClick={fetchData} title="Refresh"> |
| <RefreshCw size={13} className={loading ? 'animate-spin' : ''} /> |
| </button> |
| </div> |
| |
| <div className="hm-infra-stack"> |
| {[ |
| { label: 'NEO4J', ok: isOnline(health?.neo4j_connected) }, |
| { label: 'REDIS', ok: isOnline(health?.redis_connected) }, |
| { label: `${health?.workers_active ?? 0} WORKERS`, ok: true, neutral: true }, |
| { label: 'API', ok: true }, |
| ].map(s => ( |
| <div key={s.label} className="hm-infra-row"> |
| <span className={`hm-dot ${s.neutral ? 'neutral' : s.ok ? 'online' : 'offline'}`} /> |
| <span className="hm-infra-name">{s.label}</span> |
| <span className={`hm-infra-badge ${s.neutral ? 'neutral' : s.ok ? 'ok' : 'fail'}`}> |
| {s.neutral ? 'ACTIVE' : s.ok ? 'CONNECTED' : 'OFFLINE'} |
| </span> |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| |
| {/* ββ Platform Metrics βββββββββββββββββββββββββββ */} |
| <div className="hm-section"> |
| <div className="hm-section-label">PLATFORM METRICS</div> |
| <div className="hm-metrics-row"> |
| <StatCounter value={stats?.documents_count ?? 0} label="DOCUMENTS" /> |
| <div className="hm-metric-divider" /> |
| <StatCounter value={stats?.entities_count ?? 0} label="ENTITIES" /> |
| <div className="hm-metric-divider" /> |
| <StatCounter value={stats?.relationships_count ?? 0} label="RELATIONSHIPS" /> |
| <div className="hm-metric-divider" /> |
| <StatCounter value={stats?.chunks_count ?? 0} label="CHUNKS" /> |
| <div className="hm-metric-divider" /> |
| <div className="hm-stat-block"> |
| <div className="hm-stat-value text-2xl"> |
| {stats?.ontology_version ?? 'β'} |
| </div> |
| <div className="hm-stat-key">ONTOLOGY VER</div> |
| </div> |
| </div> |
| </div> |
| |
| {/* ββ Main Grid ββββββββββββββββββββββββββββββββββ */} |
| <div className="hm-main-grid"> |
| |
| {/* Quick Actions */} |
| <div className="hm-card hm-actions-card"> |
| <div className="hm-card-head"> |
| <Activity size={16} /> QUICK ACTIONS |
| </div> |
| <div className="hm-action-list"> |
| {[ |
| { href: '/process', icon: <Database size={18}/>, label: 'INGEST DOCUMENTS', desc: 'Upload PDFs, text, or crawl URLs' }, |
| { href: '/interact', icon: <Cpu size={18}/>, label: 'QUERY KNOWLEDGE', desc: 'Ask questions across the graph' }, |
| { href: '/simulate', icon: <Network size={18}/>, label: 'EXPLORE NODES', desc: 'Interactive D3 force visualization' }, |
| { href: '/ontology', icon: <Server size={18}/>, label: 'MANAGE ONTOLOGY', desc: 'Edit schema & run AI refinement' }, |
| { href: '/insights', icon: <TrendingUp size={18}/>,label: 'INSIGHTS', desc: 'Quality metrics & AI reports' }, |
| ].map(a => ( |
| <a key={a.href} href={a.href} className="hm-action-item"> |
| <span className="hm-action-icon">{a.icon}</span> |
| <div className="hm-action-text"> |
| <div className="hm-action-label">{a.label}</div> |
| <div className="hm-action-desc">{a.desc}</div> |
| </div> |
| <ArrowRight size={14} className="hm-action-arrow" /> |
| </a> |
| ))} |
| </div> |
| </div> |
| |
| {/* Right column: My Activity + Feature cards */} |
| <div className="flex flex-col gap-6"> |
| |
| {/* User Activity */} |
| <div className="hm-card"> |
| <div className="hm-card-head"> |
| <MessageSquare size={16} /> MY ACTIVITY |
| {user && <span className="ml-auto font-mono text-[0.7rem] text-[#666666]">@{user.username}</span>} |
| </div> |
| <div className="hm-activity-grid"> |
| <div className="hm-activity-chip"> |
| <div className="hm-activity-val">{myStats?.conversation_count ?? 'β'}</div> |
| <div className="hm-activity-key">CONVERSATIONS</div> |
| </div> |
| <div className="hm-activity-chip"> |
| <div className="hm-activity-val">{myStats?.message_count ?? 'β'}</div> |
| <div className="hm-activity-key">QUERIES SENT</div> |
| </div> |
| <div className="hm-activity-chip"> |
| <div className="hm-activity-val">{myStats?.last_active ? new Date(myStats.last_active).toLocaleDateString() : 'β'}</div> |
| <div className="hm-activity-key">LAST ACTIVE</div> |
| </div> |
| </div> |
| {!myStats && ( |
| <div style={{ fontFamily: 'var(--font-mono)', fontSize: '0.78rem', color: 'var(--muted-color)', marginTop: '0.75rem' }}> |
| Start querying the graph to build your activity history. |
| </div> |
| )} |
| </div> |
| |
| {/* Graph intelligence card */} |
| <div className="hm-card hm-graph-card"> |
| <div className="hm-card-head"> |
| <GitBranch size={16} /> KNOWLEDGE GRAPH |
| </div> |
| <div style={{ fontSize: '0.85rem', lineHeight: 1.7, color: 'var(--muted-color)', marginBottom: '1rem' }}> |
| Neo4j-powered semantic knowledge graph. Multi-hop reasoning, entity enrichment, and community detection built in. |
| </div> |
| <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}> |
| {['Entities', 'Relationships', 'Communities', 'Graph Export'].map(tag => ( |
| <span key={tag} className="hm-tag">{tag}</span> |
| ))} |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| {/* ββ Feature Showcase βββββββββββββββββββββββββββ */} |
| <div className="hm-features-section"> |
| <div className="hm-section-label" style={{ marginBottom: '1.5rem' }}>PLATFORM CAPABILITIES</div> |
| <div className="hm-features-grid"> |
| {[ |
| { |
| icon: <Database size={24}/>, |
| title: 'DOCUMENT INGESTION', |
| desc: 'Ingest PDFs, text files, Markdown, and web URLs. Celery workers extract entities and relationships into the knowledge graph automatically via LLM pipelines.', |
| color: '#2563eb', |
| }, |
| { |
| icon: <Network size={24}/>, |
| title: 'GRAPH INTELLIGENCE', |
| desc: 'Neo4j-powered knowledge graph with rich entity relationships. Query across documents globally or per-source with full ontology control.', |
| color: '#7c3aed', |
| }, |
| { |
| icon: <Cpu size={24}/>, |
| title: 'AGENTIC LOGIC', |
| desc: 'Multi-step ReACT reasoning agent that searches the graph, retrieves relevant chunks, and streams answers with confidence scoring in real time.', |
| color: '#059669', |
| }, |
| { |
| icon: <Zap size={24}/>, |
| title: 'LLM-AS-JUDGE', |
| desc: 'Inline faithfulness evaluation using heuristic scoring. Detects hallucination risk, context precision, and answer quality on every response.', |
| color: '#d97706', |
| }, |
| { |
| icon: <Activity size={24}/>, |
| title: 'LIVE SIMULATION', |
| desc: 'Interactive D3 force graph with color-coded entity types, physics controls, fullscreen mode, PNG export, and node detail modals.', |
| color: '#dc2626', |
| }, |
| { |
| icon: <TrendingUp size={24}/>, |
| title: 'ONTOLOGY DRIFT', |
| desc: 'Automated schema drift detection that spots when new data no longer fits the current ontology. Propose and approve schema expansions.', |
| color: '#0891b2', |
| }, |
| ].map(f => ( |
| <div key={f.title} className="hm-feature-card" style={{ '--feature-color': f.color } as any}> |
| <div className="hm-feature-icon" style={{ color: f.color }}>{f.icon}</div> |
| <div className="hm-feature-title">{f.title}</div> |
| <div className="hm-feature-desc">{f.desc}</div> |
| </div> |
| ))} |
| </div> |
| </div> |
| |
| {/* Footer bar */} |
| <div className="hm-footer-bar"> |
| <span className="hm-footer-brand">CORTEX_PLATFORM</span> |
| <span style={{ color: 'var(--muted-color)', fontSize: '0.72rem', fontFamily: 'var(--font-mono)' }}> |
| Last refreshed: {lastRefresh.toLocaleTimeString()} |
| </span> |
| <span style={{ color: 'var(--muted-color)', fontSize: '0.72rem', fontFamily: 'var(--font-mono)' }}> |
| v{health ? '1.0' : 'β'} Β· Neo4j + Redis + Celery |
| </span> |
| </div> |
| |
| <style>{` |
| |
| .hm-hero { |
| display: grid; |
| grid-template-columns: 1fr 320px; |
| gap: 2rem; |
| padding: 2.5rem 0 2rem; |
| border-bottom: 3px solid #000; |
| margin-bottom: 2.5rem; |
| align-items: center; |
| } |
| .hm-hero-label { |
| font-family: var(--font-mono); |
| font-size: 0.72rem; |
| font-weight: 700; |
| letter-spacing: 3px; |
| color: var(--muted-color); |
| margin-bottom: 0.75rem; |
| } |
| .hm-hero-title { |
| font-family: var(--font-display); |
| font-size: clamp(2rem, 4vw, 3rem); |
| font-weight: 800; |
| line-height: 1.1; |
| margin-bottom: 0.75rem; |
| letter-spacing: -0.5px; |
| } |
| .hm-hero-accent { |
| position: relative; |
| display: inline-block; |
| } |
| .hm-hero-accent::after { |
| content: ''; |
| position: absolute; |
| left: 0; bottom: 2px; |
| width: 100%; height: 4px; |
| background: #000; |
| } |
| .hm-hero-sub { |
| color: var(--muted-color); |
| font-size: 0.95rem; |
| line-height: 1.6; |
| margin-bottom: 1.5rem; |
| max-width: 480px; |
| } |
| .hm-hero-ctas { |
| display: flex; |
| gap: 0.75rem; |
| flex-wrap: wrap; |
| } |
| .hm-cta-primary { |
| display: inline-flex; |
| align-items: center; |
| gap: 0.5rem; |
| background: #000; |
| color: #fff; |
| padding: 0.65rem 1.25rem; |
| font-family: var(--font-mono); |
| font-size: 0.8rem; |
| font-weight: 700; |
| letter-spacing: 0.5px; |
| text-decoration: none; |
| transition: background 0.15s, transform 0.15s; |
| border: 2px solid #000; |
| } |
| .hm-cta-primary:hover { |
| background: #333; |
| color: #fff; |
| transform: translateY(-1px); |
| box-shadow: 3px 3px 0 rgba(0,0,0,0.2); |
| } |
| .hm-cta-secondary { |
| display: inline-flex; |
| align-items: center; |
| gap: 0.5rem; |
| background: transparent; |
| color: #000; |
| padding: 0.65rem 1.25rem; |
| font-family: var(--font-mono); |
| font-size: 0.8rem; |
| font-weight: 700; |
| letter-spacing: 0.5px; |
| text-decoration: none; |
| border: 2px solid #000; |
| transition: background 0.15s, transform 0.15s; |
| } |
| .hm-cta-secondary:hover { |
| background: #000; |
| color: #fff; |
| transform: translateY(-1px); |
| } |
| |
| |
| .hm-system-badge { |
| display: flex; |
| align-items: center; |
| gap: 0.6rem; |
| border: 2px solid #000; |
| padding: 0.5rem 0.85rem; |
| margin-bottom: 1rem; |
| font-family: var(--font-mono); |
| font-size: 0.72rem; |
| font-weight: 700; |
| letter-spacing: 1px; |
| } |
| .hm-system-badge.warn { border-color: #d97706; color: #d97706; } |
| .hm-pulse-dot { |
| width: 8px; height: 8px; |
| border-radius: 50%; |
| flex-shrink: 0; |
| animation: pulseGlow 2s ease-in-out infinite; |
| } |
| .hm-pulse-dot.green { background: #16a34a; box-shadow: 0 0 0 0 rgba(22,163,74,0.4); } |
| .hm-pulse-dot.yellow { background: #d97706; box-shadow: 0 0 0 0 rgba(217,119,6,0.4); } |
| @keyframes pulseGlow { |
| 0%, 100% { box-shadow: 0 0 0 0 rgba(22,163,74,0.4); } |
| 50% { box-shadow: 0 0 0 5px rgba(22,163,74,0); } |
| } |
| .hm-badge-label { flex: 1; } |
| .hm-refresh-btn { |
| background: none; |
| border: none; |
| cursor: pointer; |
| padding: 0; |
| color: var(--muted-color); |
| display: flex; |
| align-items: center; |
| transition: color 0.15s; |
| } |
| .hm-refresh-btn:hover { color: #000; background: none; } |
| |
| |
| .hm-infra-stack { display: flex; flex-direction: column; gap: 0.5rem; } |
| .hm-infra-row { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| padding: 0.4rem 0.6rem; |
| border: 1.5px solid #e5e5e5; |
| font-family: var(--font-mono); |
| font-size: 0.78rem; |
| } |
| .hm-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; } |
| .hm-dot.online { background: #16a34a; } |
| .hm-dot.offline { background: #dc2626; } |
| .hm-dot.neutral { background: #9ca3af; } |
| .hm-infra-name { flex: 1; font-weight: 600; color: var(--muted-color); } |
| .hm-infra-badge { |
| font-size: 0.65rem; |
| font-weight: 700; |
| padding: 1px 6px; |
| letter-spacing: 0.5px; |
| } |
| .hm-infra-badge.ok { background: #dcfce7; color: #16a34a; } |
| .hm-infra-badge.fail { background: #fee2e2; color: #dc2626; } |
| .hm-infra-badge.neutral { background: #f3f4f6; color: #6b7280; } |
| |
| |
| .hm-section { margin-bottom: 2.5rem; } |
| .hm-section-label { |
| font-family: var(--font-mono); |
| font-size: 0.68rem; |
| font-weight: 700; |
| letter-spacing: 3px; |
| color: var(--muted-color); |
| margin-bottom: 1rem; |
| } |
| |
| |
| .hm-metrics-row { |
| display: flex; |
| align-items: center; |
| border: 3px solid #000; |
| overflow: hidden; |
| } |
| .hm-stat-block { |
| flex: 1; |
| min-width: 0; |
| padding: 1.25rem 1.5rem; |
| text-align: center; |
| } |
| .hm-stat-value { |
| font-family: var(--font-mono); |
| font-size: 2.2rem; |
| font-weight: 900; |
| line-height: 1; |
| margin-bottom: 0.3rem; |
| } |
| .hm-stat-key { |
| font-family: var(--font-mono); |
| font-size: 0.6rem; |
| color: var(--muted-color); |
| letter-spacing: 1.5px; |
| font-weight: 700; |
| } |
| .hm-metric-divider { |
| width: 1px; |
| height: 60px; |
| background: #000; |
| flex-shrink: 0; |
| } |
| |
| |
| .hm-main-grid { |
| display: grid; |
| grid-template-columns: 1fr 360px; |
| gap: 1.5rem; |
| margin-bottom: 3rem; |
| } |
| |
| |
| .hm-card { |
| border: 2px solid #000; |
| padding: 1.5rem; |
| background: #fff; |
| transition: transform 0.15s, box-shadow 0.15s; |
| } |
| .hm-card:hover { |
| transform: translateY(-2px); |
| box-shadow: 4px 4px 0 #000; |
| } |
| .hm-card-head { |
| display: flex; |
| align-items: center; |
| gap: 0.5rem; |
| font-family: var(--font-mono); |
| font-size: 0.72rem; |
| font-weight: 700; |
| letter-spacing: 1.5px; |
| margin-bottom: 1.25rem; |
| padding-bottom: 0.75rem; |
| border-bottom: 2px solid #000; |
| } |
| |
| |
| .hm-actions-card { |
| background: #fafafa; |
| } |
| .hm-action-list { display: flex; flex-direction: column; gap: 0.4rem; } |
| .hm-action-item { |
| display: flex; |
| align-items: center; |
| gap: 0.75rem; |
| padding: 0.7rem 0.75rem; |
| border: 1.5px solid #e5e5e5; |
| text-decoration: none; |
| color: #000; |
| transition: all 0.15s; |
| background: #fff; |
| } |
| .hm-action-item:hover { |
| background: #000; |
| color: #fff; |
| border-color: #000; |
| transform: translateX(3px); |
| box-shadow: -3px 3px 0 rgba(0,0,0,0.1); |
| } |
| .hm-action-icon { |
| width: 36px; height: 36px; |
| display: flex; align-items: center; justify-content: center; |
| background: #000; |
| color: #fff; |
| flex-shrink: 0; |
| transition: background 0.15s; |
| } |
| .hm-action-item:hover .hm-action-icon { background: #fff; color: #000; } |
| .hm-action-text { flex: 1; min-width: 0; } |
| .hm-action-label { |
| font-family: var(--font-mono); |
| font-size: 0.8rem; |
| font-weight: 700; |
| letter-spacing: 0.5px; |
| } |
| .hm-action-desc { |
| font-size: 0.75rem; |
| color: var(--muted-color); |
| margin-top: 0.1rem; |
| } |
| .hm-action-item:hover .hm-action-desc { color: rgba(255,255,255,0.7); } |
| .hm-action-arrow { flex-shrink: 0; transition: transform 0.15s; } |
| .hm-action-item:hover .hm-action-arrow { transform: translateX(3px); } |
| |
| |
| .hm-activity-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 0.75rem; |
| } |
| .hm-activity-chip { |
| border: 1.5px solid #e5e5e5; |
| padding: 0.75rem 0.5rem; |
| text-align: center; |
| } |
| .hm-activity-val { |
| font-family: var(--font-mono); |
| font-size: 1.3rem; |
| font-weight: 900; |
| line-height: 1; |
| margin-bottom: 0.25rem; |
| } |
| .hm-activity-key { |
| font-family: var(--font-mono); |
| font-size: 0.58rem; |
| color: var(--muted-color); |
| letter-spacing: 1px; |
| font-weight: 700; |
| } |
| |
| |
| .hm-graph-card {} |
| .hm-tag { |
| display: inline-block; |
| background: #f3f4f6; |
| border: 1.5px solid #e5e5e5; |
| font-family: var(--font-mono); |
| font-size: 0.68rem; |
| font-weight: 700; |
| padding: 2px 8px; |
| letter-spacing: 0.5px; |
| } |
| |
| |
| .hm-features-section { |
| border-top: 3px solid #000; |
| padding-top: 2.5rem; |
| margin-bottom: 2rem; |
| } |
| .hm-features-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
| gap: 1rem; |
| } |
| .hm-feature-card { |
| border: 2px solid #e5e5e5; |
| padding: 1.5rem; |
| transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s; |
| position: relative; |
| overflow: hidden; |
| } |
| .hm-feature-card::before { |
| content: ''; |
| position: absolute; |
| left: 0; top: 0; bottom: 0; |
| width: 3px; |
| background: var(--feature-color, #000); |
| opacity: 0; |
| transition: opacity 0.2s; |
| } |
| .hm-feature-card:hover { |
| border-color: #000; |
| box-shadow: 4px 4px 0 #000; |
| transform: translateY(-2px); |
| } |
| .hm-feature-card:hover::before { opacity: 1; } |
| .hm-feature-icon { |
| margin-bottom: 0.85rem; |
| transition: transform 0.2s; |
| } |
| .hm-feature-card:hover .hm-feature-icon { transform: scale(1.1); } |
| .hm-feature-title { |
| font-family: var(--font-mono); |
| font-size: 0.72rem; |
| font-weight: 700; |
| letter-spacing: 1px; |
| margin-bottom: 0.5rem; |
| } |
| .hm-feature-desc { |
| font-size: 0.82rem; |
| color: var(--muted-color); |
| line-height: 1.65; |
| } |
| |
| |
| .hm-footer-bar { |
| display: flex; |
| align-items: center; |
| gap: 1.5rem; |
| border-top: 1px solid #e5e5e5; |
| padding-top: 1.25rem; |
| margin-top: 1rem; |
| } |
| .hm-footer-brand { |
| font-family: var(--font-mono); |
| font-size: 0.72rem; |
| font-weight: 700; |
| letter-spacing: 2px; |
| margin-right: auto; |
| } |
| |
| |
| @media (max-width: 900px) { |
| .hm-hero { grid-template-columns: 1fr; } |
| .hm-hero-right { order: -1; } |
| .hm-main-grid { grid-template-columns: 1fr; } |
| .hm-metrics-row { flex-wrap: wrap; } |
| .hm-metric-divider { display: none; } |
| .hm-stat-block { flex: 0 0 50%; border-bottom: 1px solid #000; } |
| } |
| `}</style> |
| </div> |
| ); |
| }; |
|
|
| export default Home; |
|
|