// Hero — animated particle-network background (full-width, 20% opacity) const SFHero = ({ lang, accent, setPage }) => { const t = (en, zh) => sfT(lang, en, zh); return (
{/* Full-width animated background */} {/* Subtle dot-grid on top of canvas */}
{/* Foreground content */}
{t('System Integrator · Hong Kong', '系統整合商 · 香港')}

{t( <>We design,{' '} supply,{' '} install, and{' '} support IT infrastructure., <>為香港企業{' '} 設計、 供應、 安裝、並{' '} 持續支援 IT 基建。 )}

{t( 'Structured cabling, fiber backbone, Wi-Fi 7, IP telephony, CCTV, access control, server rooms, cybersecurity, and AI workflow — for offices, retail, warehouses, schools, and commercial sites.', '由佈線、光纖、Wi-Fi 7、IP 電話、閉路電視、門禁、伺服器室到資安與 AI 流程 — 覆蓋辦公室、零售、倉庫、學校及商業場地。' )}

{/* Right column: live stats block */}
INFRASTRUCTURE · LIVE STATUS
{[ { n:'500+', en:'Projects delivered across HK & Macau', zh:'於港澳完成的項目' }, { n:'20+', en:'Years of hands-on field experience', zh:'年實地工程經驗' }, { n:'24/7', en:'SOC monitoring on every handover', zh:'每個交付場地持續監察' }, { n:'4hr', en:'On-site response — standard, not premium', zh:'上門回應 — 標準服務,非額外收費' }, ].map((s,i)=>(
{s.n}
{t(s.en, s.zh)}
))}
); }; // (SFNetworkCanvas is now a global defined in icons.jsx) const _SFNetworkCanvas_UNUSED = ({ accent }) => { const canvasRef = React.useRef(null); const frameRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let W, H, nodes; // Resolve CSS variable colors at runtime const style = getComputedStyle(document.documentElement); const fgMuted = style.getPropertyValue('--fg-muted').trim() || '#56627A'; const bgDefault = style.getPropertyValue('--bg-default').trim() || '#F6F8FB'; const NODE_COUNT = 42; const CONNECT_DIST = 160; const SPEED = 0.28; const NODE_LABELS = [ 'CORE-SW','EDGE-01','EDGE-02','NGFW','AP-1F','AP-2F','AP-3F', 'NVR','SRV-01','SRV-02','UPS','PDU','VPN','SIEM','EDR', 'CAM-01','CAM-02','PBX','VLAN10','VLAN20','BGP','OSPF', ]; const makeNode = (i) => ({ x: Math.random() * W, y: Math.random() * H, vx: (Math.random() - 0.5) * SPEED, vy: (Math.random() - 0.5) * SPEED, r: Math.random() < 0.18 ? 3.5 : 2, // some nodes are "hubs" label: Math.random() < 0.35 ? NODE_LABELS[i % NODE_LABELS.length] : null, }); const resize = () => { W = canvas.offsetWidth; H = canvas.offsetHeight; canvas.width = W * devicePixelRatio; canvas.height = H * devicePixelRatio; ctx.scale(devicePixelRatio, devicePixelRatio); nodes = Array.from({ length: NODE_COUNT }, (_, i) => makeNode(i)); }; const draw = () => { ctx.clearRect(0, 0, W, H); // Move nodes, bounce off edges for (const n of nodes) { n.x += n.vx; n.y += n.vy; if (n.x < 0 || n.x > W) n.vx *= -1; if (n.y < 0 || n.y > H) n.vy *= -1; n.x = Math.max(0, Math.min(W, n.x)); n.y = Math.max(0, Math.min(H, n.y)); } // Draw edges for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const dx = nodes[i].x - nodes[j].x; const dy = nodes[i].y - nodes[j].y; const dist = Math.sqrt(dx*dx + dy*dy); if (dist < CONNECT_DIST) { const alpha = 1 - dist / CONNECT_DIST; ctx.beginPath(); ctx.moveTo(nodes[i].x, nodes[i].y); ctx.lineTo(nodes[j].x, nodes[j].y); ctx.strokeStyle = fgMuted; ctx.globalAlpha = alpha * 0.5; ctx.lineWidth = 0.7; ctx.stroke(); } } } // Draw nodes + labels for (const n of nodes) { const isHub = n.r > 2.5; // Node circle ctx.beginPath(); ctx.arc(n.x, n.y, n.r, 0, Math.PI * 2); ctx.fillStyle = isHub ? accent : fgMuted; ctx.globalAlpha = isHub ? 0.7 : 0.45; ctx.fill(); // Hub ring if (isHub) { ctx.beginPath(); ctx.arc(n.x, n.y, n.r + 3, 0, Math.PI * 2); ctx.strokeStyle = accent; ctx.globalAlpha = 0.25; ctx.lineWidth = 0.8; ctx.stroke(); } // Label if (n.label) { ctx.font = `500 9px "JetBrains Mono", monospace`; ctx.fillStyle = fgMuted; ctx.globalAlpha = 0.55; ctx.fillText(n.label, n.x + n.r + 4, n.y + 3); } } ctx.globalAlpha = 1; frameRef.current = requestAnimationFrame(draw); }; const ro = new ResizeObserver(resize); ro.observe(canvas); resize(); // Pause when not visible const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { frameRef.current = requestAnimationFrame(draw); } else { cancelAnimationFrame(frameRef.current); } }); io.observe(canvas); return () => { cancelAnimationFrame(frameRef.current); ro.disconnect(); io.disconnect(); }; }, [accent]); return ( ); }; window.SFHero = SFHero;