diff --git a/website/src/components/KeypairGenerator.tsx b/website/src/components/KeypairGenerator.tsx
new file mode 100644
index 00000000..aab8da9e
--- /dev/null
+++ b/website/src/components/KeypairGenerator.tsx
@@ -0,0 +1,58 @@
+import React, { useState } from 'react';
+import { Key, ArrowRight, Fingerprint } from 'lucide-react';
+
+export function KeypairGenerator() {
+ const [privateKey, setPrivateKey] = useState('0x' + Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join(''));
+ const [showPrivate, setShowPrivate] = useState(false);
+
+ const generate = () => {
+ setPrivateKey('0x' + Array.from({ length: 64 }, () => Math.floor(Math.random() * 16).toString(16)).join(''));
+ };
+
+ const publicKey = 'xdc' + privateKey.slice(2, 42);
+
+ return (
+
+
+
+
+
+
+ Private Key
+
+
{showPrivate ? privateKey : '•'.repeat(42)}
+
+
+
+
+ Public Address
+
+
{publicKey}
+
+
+
+
+
+
+
Demo only — never share your real private key.
+
+
+ );
+}
+
+export default KeypairGenerator;
diff --git a/website/src/components/NetworkGlobe.tsx b/website/src/components/NetworkGlobe.tsx
new file mode 100644
index 00000000..4d145021
--- /dev/null
+++ b/website/src/components/NetworkGlobe.tsx
@@ -0,0 +1,347 @@
+import React, { useRef, useMemo, Suspense, useCallback, useEffect } from 'react';
+import { Canvas, useFrame, useThree } from '@react-three/fiber';
+import * as THREE from 'three';
+
+// ─── Data: 108 simulated masternodes spread around the globe ─────────────────
+const MASTERNODE_LOCATIONS = [
+ // Asia-Pacific
+ { lat: 35.6762, lng: 139.6503, label: 'Tokyo', region: 'APAC' },
+ { lat: 22.3193, lng: 114.1694, label: 'Hong Kong', region: 'APAC' },
+ { lat: 1.3521, lng: 103.8198, label: 'Singapore', region: 'APAC' },
+ { lat: 37.5665, lng: 126.9780, label: 'Seoul', region: 'APAC' },
+ { lat: 31.2304, lng: 121.4737, label: 'Shanghai', region: 'APAC' },
+ { lat: 19.0760, lng: 72.8777, label: 'Mumbai', region: 'APAC' },
+ { lat: -33.8688, lng: 151.2093, label: 'Sydney', region: 'APAC' },
+ { lat: 13.7563, lng: 100.5018, label: 'Bangkok', region: 'APAC' },
+ // Europe
+ { lat: 51.5074, lng: -0.1278, label: 'London', region: 'EU' },
+ { lat: 48.8566, lng: 2.3522, label: 'Paris', region: 'EU' },
+ { lat: 52.5200, lng: 13.4050, label: 'Berlin', region: 'EU' },
+ { lat: 41.9028, lng: 12.4964, label: 'Rome', region: 'EU' },
+ { lat: 52.3676, lng: 4.9041, label: 'Amsterdam', region: 'EU' },
+ { lat: 59.3293, lng: 18.0686, label: 'Stockholm', region: 'EU' },
+ { lat: 55.7558, lng: 37.6173, label: 'Moscow', region: 'EU' },
+ { lat: 48.2082, lng: 16.3738, label: 'Vienna', region: 'EU' },
+ // Americas
+ { lat: 40.7128, lng: -74.0060, label: 'New York', region: 'NA' },
+ { lat: 37.7749, lng: -122.4194, label: 'San Francisco', region: 'NA' },
+ { lat: 41.8781, lng: -87.6298, label: 'Chicago', region: 'NA' },
+ { lat: 45.5051, lng: -73.5550, label: 'Montreal', region: 'NA' },
+ { lat: -23.5505, lng: -46.6333, label: 'São Paulo', region: 'SA' },
+ { lat: -34.6037, lng: -58.3816, label: 'Buenos Aires', region: 'SA' },
+ // Middle East & Africa
+ { lat: 25.2048, lng: 55.2708, label: 'Dubai', region: 'MEA' },
+ { lat: 26.8206, lng: 30.8025, label: 'Cairo', region: 'MEA' },
+ { lat: -26.2041, lng: 28.0473, label: 'Johannesburg', region: 'MEA' },
+];
+
+// Fill up to 108 nodes with extras
+const EXTRA_NODES = 108 - MASTERNODE_LOCATIONS.length;
+const allNodes = [...MASTERNODE_LOCATIONS];
+for (let i = 0; i < EXTRA_NODES; i++) {
+ const lat = (Math.random() - 0.5) * 160;
+ const lng = (Math.random() - 0.5) * 360;
+ allNodes.push({ lat, lng, label: `Node-${i + 100}`, region: 'OTHER' });
+}
+
+function latLngToVec3(lat: number, lng: number, r: number): THREE.Vector3 {
+ const phi = (90 - lat) * (Math.PI / 180);
+ const theta = (lng + 180) * (Math.PI / 180);
+ return new THREE.Vector3(
+ -r * Math.sin(phi) * Math.cos(theta),
+ r * Math.cos(phi),
+ r * Math.sin(phi) * Math.sin(theta)
+ );
+}
+
+const GLOBE_R = 2.0;
+const regionColors: { [key: string]: string } = {
+ APAC: '#38bdf8',
+ EU: '#818cf8',
+ NA: '#34d399',
+ SA: '#fb923c',
+ MEA: '#f472b6',
+ OTHER: '#60a5fa',
+};
+
+// ─── Arcs: animated great-circle connections between masternodes ─────────────
+function AnimatedArcs({ time }: { time: number }) {
+ const arcsRef = useRef(null);
+
+ const arcPairs = useMemo(() => {
+ const pairs: Array<{ from: (typeof allNodes)[0]; to: (typeof allNodes)[0] }> = [];
+ // Create 24 animated arcs between named nodes
+ const named = allNodes.filter(n => n.region !== 'OTHER').slice(0, 20);
+ for (let i = 0; i < 24; i++) {
+ const from = named[i % named.length];
+ const to = named[(i + 5) % named.length];
+ if (from !== to) pairs.push({ from, to });
+ }
+ return pairs;
+ }, []);
+
+ return (
+
+ {arcPairs.map((pair, i) => {
+ const progressRef = { current: (time * 0.3 + i * 0.15) % 1 };
+ return (
+
+ );
+ })}
+
+ );
+}
+
+function ArcLine({
+ from,
+ to,
+ progress,
+ color,
+}: {
+ from: THREE.Vector3;
+ to: THREE.Vector3;
+ progress: React.MutableRefObject;
+ color: string;
+}) {
+ const lineRef = useRef(null);
+
+ const { geometry, fullPoints } = useMemo(() => {
+ const mid = from.clone().add(to).multiplyScalar(0.5);
+ const dist = from.distanceTo(to);
+ mid.normalize().multiplyScalar(GLOBE_R + dist * 0.4);
+
+ const curve = new THREE.QuadraticBezierCurve3(from, mid, to);
+ const pts = curve.getPoints(60);
+ return {
+ geometry: new THREE.BufferGeometry().setFromPoints(pts),
+ fullPoints: pts,
+ };
+ }, [from, to]);
+
+ const geoRef = useRef(null);
+
+ useEffect(() => {
+ geoRef.current = geometry;
+ return () => {
+ geoRef.current?.dispose();
+ };
+ }, [geometry]);
+
+ useFrame(() => {
+ if (!lineRef.current || !geoRef.current) return;
+ const segLen = Math.floor(fullPoints.length * 0.2);
+ const start = Math.floor(progress.current * fullPoints.length);
+ const positions = new Float32Array(segLen * 3);
+ for (let j = 0; j < segLen; j++) {
+ const p = fullPoints[(start + j) % fullPoints.length];
+ positions[j * 3] = p.x;
+ positions[j * 3 + 1] = p.y;
+ positions[j * 3 + 2] = p.z;
+ }
+ geoRef.current.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+ geoRef.current.attributes.position.needsUpdate = true;
+ });
+
+ const material = useMemo(
+ () => new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.55, linewidth: 1 }),
+ [color]
+ );
+
+ return (
+
+ );
+}
+
+// ─── Globe sphere + grid ──────────────────────────────────────────────────────
+function GlobeSphere({ isDark }: { isDark: boolean }) {
+ return (
+ <>
+ {/* Translucent core */}
+
+
+
+
+ {/* Wireframe grid */}
+
+
+
+
+ {/* Outer glow ring */}
+
+
+
+
+ >
+ );
+}
+
+// ─── Masternode dots on surface ───────────────────────────────────────────────
+function MasternodePoints() {
+ const { positions, colors, sizes } = useMemo(() => {
+ const pos = new Float32Array(allNodes.length * 3);
+ const col = new Float32Array(allNodes.length * 3);
+ const sz = new Float32Array(allNodes.length);
+
+ allNodes.forEach((node, i) => {
+ const v = latLngToVec3(node.lat, node.lng, GLOBE_R + 0.01);
+ pos[i * 3] = v.x;
+ pos[i * 3 + 1] = v.y;
+ pos[i * 3 + 2] = v.z;
+
+ const c = new THREE.Color(regionColors[node.region] || '#1e90ff');
+ col[i * 3] = c.r;
+ col[i * 3 + 1] = c.g;
+ col[i * 3 + 2] = c.b;
+
+ sz[i] = node.region !== 'OTHER' ? 0.065 : 0.04;
+ });
+
+ return { positions: pos, colors: col, sizes: sz };
+ }, []);
+
+ const geoRef = useRef(null);
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+// ─── Floating flake particles around the globe ────────────────────────────────
+function FloatingFlakes({ count = 180 }: { count?: number }) {
+ const { positions, sizes } = useMemo(() => {
+ const pos = new Float32Array(count * 3);
+ const sz = new Float32Array(count);
+ for (let i = 0; i < count; i++) {
+ const theta = Math.random() * Math.PI * 2;
+ const phi = Math.acos(2 * Math.random() - 1);
+ const r = GLOBE_R + 0.4 + Math.random() * 1.6;
+ pos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
+ pos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
+ pos[i * 3 + 2] = r * Math.cos(phi);
+ sz[i] = 0.02 + Math.random() * 0.04;
+ }
+ return { positions: pos, sizes: sz };
+ }, [count]);
+
+ const geoRef = useRef(null);
+
+ useFrame((state) => {
+ if (!geoRef.current) return;
+ const positions = geoRef.current.attributes.position.array as Float32Array;
+ const time = state.clock.elapsedTime;
+ for (let i = 0; i < count; i++) {
+ const idx = i * 3;
+ positions[idx + 1] += Math.sin(time * 0.5 + i) * 0.001;
+ positions[idx] += Math.cos(time * 0.3 + i) * 0.0008;
+ }
+ geoRef.current.attributes.position.needsUpdate = true;
+ });
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+// ─── Atmosphere halo ──────────────────────────────────────────────────────────
+function Atmosphere() {
+ return (
+
+
+
+
+ );
+}
+
+// ─── Main rotating globe scene ────────────────────────────────────────────────
+function GlobeScene({ isDark }: { isDark: boolean }) {
+ const groupRef = useRef(null);
+ const timeRef = useRef(0);
+ const { gl } = useThree();
+
+ // Touch/mouse tilt
+ const tiltRef = useRef({ x: 0, y: 0 });
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ tiltRef.current = {
+ x: ((e.clientY / window.innerHeight) - 0.5) * 0.3,
+ y: ((e.clientX / window.innerWidth) - 0.5) * 0.3,
+ };
+ };
+ window.addEventListener('mousemove', onMove, { passive: true });
+ return () => window.removeEventListener('mousemove', onMove);
+ }, []);
+
+ useFrame((state, delta) => {
+ if (!groupRef.current) return;
+ timeRef.current += delta;
+ const t = timeRef.current;
+
+ // Slow auto-rotate Y + gentle tilt tracking
+ groupRef.current.rotation.y += delta * 0.06;
+ groupRef.current.rotation.x = THREE.MathUtils.lerp(
+ groupRef.current.rotation.x,
+ tiltRef.current.x,
+ 0.04
+ );
+ });
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+// ─── Exported wrapper ─────────────────────────────────────────────────────────
+export function NetworkGlobeHero({ className }: { className?: string }) {
+ const isDark = typeof document !== 'undefined'
+ ? document.documentElement.getAttribute('data-theme') === 'dark'
+ : true;
+
+ return (
+
+
+
+ );
+}
+
+export default NetworkGlobeHero;
diff --git a/website/src/components/NetworkSelector/index.tsx b/website/src/components/NetworkSelector/index.tsx
new file mode 100644
index 00000000..f615397c
--- /dev/null
+++ b/website/src/components/NetworkSelector/index.tsx
@@ -0,0 +1,30 @@
+import React, { useState } from 'react';
+import styles from './styles.module.css';
+
+export default function NetworkSelector() {
+ const [network, setNetwork] = useState('mainnet');
+ const networks = [
+ { id: 'mainnet', name: 'XDC Mainnet', chainId: 50, color: '#10b981' },
+ { id: 'apothem', name: 'Apothem Testnet', chainId: 51, color: '#f59e0b' },
+ { id: 'devnet', name: 'Devnet', chainId: 551, color: '#6366f1' },
+ ];
+
+ return (
+
+
+ {networks.map((net) => (
+
+ ))}
+
+
+ );
+}
diff --git a/website/src/components/NetworkSelector/styles.module.css b/website/src/components/NetworkSelector/styles.module.css
new file mode 100644
index 00000000..19e92cc8
--- /dev/null
+++ b/website/src/components/NetworkSelector/styles.module.css
@@ -0,0 +1,53 @@
+.networkSelector {
+ perspective: 1000px;
+ margin: 1rem 0;
+}
+
+.cardInner {
+ display: flex;
+ gap: 0.5rem;
+ transform-style: preserve-3d;
+}
+
+.networkButton {
+ position: relative;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: 12px;
+ background: var(--ifm-card-background-color);
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
+ transform-style: preserve-3d;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.networkButton:hover {
+ transform: translateY(-2px) translateZ(10px);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
+}
+
+.networkButton.active {
+ border-color: var(--net-color);
+ box-shadow: 0 0 20px var(--net-color), inset 0 0 20px rgba(255, 255, 255, 0.05);
+ transform: translateZ(20px);
+}
+
+.indicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ box-shadow: 0 0 8px currentColor;
+}
+
+.name {
+ font-weight: 600;
+ font-size: 0.875rem;
+}
+
+.chainId {
+ font-size: 0.75rem;
+ opacity: 0.7;
+ font-family: var(--ifm-font-family-monospace);
+}
diff --git a/website/src/components/NetworkStatusWidget.tsx b/website/src/components/NetworkStatusWidget.tsx
new file mode 100644
index 00000000..fb696e87
--- /dev/null
+++ b/website/src/components/NetworkStatusWidget.tsx
@@ -0,0 +1,68 @@
+import React, { useState } from 'react';
+import { Activity, CheckCircle2, AlertCircle, Clock, Globe, Server } from 'lucide-react';
+
+interface NetworkStatus {
+ name: string;
+ status: 'operational' | 'degraded' | 'maintenance';
+ height: number;
+ latency: number;
+ icon: React.ElementType;
+}
+
+const initialNetworks: NetworkStatus[] = [
+ { name: 'XDC Mainnet', status: 'operational', height: 76543210, latency: 45, icon: Globe },
+ { name: 'Apothem Testnet', status: 'operational', height: 45234123, latency: 62, icon: Server },
+ { name: 'Devnet', status: 'operational', height: 1234567, latency: 28, icon: Activity },
+];
+
+export function NetworkStatusWidget() {
+ const [networks] = useState(initialNetworks);
+
+ const statusConfig = {
+ operational: { color: 'text-emerald-500', bg: 'bg-emerald-500/10', label: 'Operational', icon: CheckCircle2 },
+ degraded: { color: 'text-amber-500', bg: 'bg-amber-500/10', label: 'Degraded', icon: AlertCircle },
+ maintenance: { color: 'text-blue-500', bg: 'bg-blue-500/10', label: 'Maintenance', icon: Clock },
+ };
+
+ return (
+
+
+
+ {networks.map((network) => {
+ const config = statusConfig[network.status];
+ const StatusIcon = config.icon;
+ const NetworkIcon = network.icon;
+ return (
+
+
+
+
+
+
+
{network.name}
+
Block {network.height.toLocaleString()}
+
+
+
+
+
{network.latency}ms latency
+
+
+
+ {config.label}
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+export default NetworkStatusWidget;
diff --git a/website/src/components/NoiseOverlay.tsx b/website/src/components/NoiseOverlay.tsx
new file mode 100644
index 00000000..e0211178
--- /dev/null
+++ b/website/src/components/NoiseOverlay.tsx
@@ -0,0 +1,34 @@
+import React, { useEffect, useState } from 'react';
+
+/**
+ * Subtle film grain / noise overlay.
+ * Adds texture and a premium feel without hurting performance.
+ * Uses a tiny base64 SVG noise pattern.
+ */
+
+const NOISE_PATTERN =
+ 'url("data:image/svg+xml,%3Csvg viewBox=%220 0 200 200%22 xmlns=%22http://www.w3.org/2000/svg%22%3E%3Cfilter id=%22noise%22%3E%3CfeTurbulence type=%22fractalNoise%22 baseFrequency=%220.65%22 numOctaves=%223%22 stitchTiles=%22stitch%22/%3E%3C/filter%3E%3Crect width=%22100%25%22 height=%22100%25%22 filter=%22url(%23noise)%22/%3E%3C/svg%3E")';
+
+export function NoiseOverlay() {
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ if (!mounted) return null;
+
+ return (
+
+ );
+}
+
+export default NoiseOverlay;
diff --git a/website/src/components/RPCEndpoint/index.tsx b/website/src/components/RPCEndpoint/index.tsx
new file mode 100644
index 00000000..965175a5
--- /dev/null
+++ b/website/src/components/RPCEndpoint/index.tsx
@@ -0,0 +1,36 @@
+import React, { useState } from 'react';
+import styles from './styles.module.css';
+
+interface Props {
+ url: string;
+ chainId: number;
+ name: string;
+}
+
+export default function RPCEndpoint({ url, chainId, name }: Props) {
+ const [copied, setCopied] = useState(false);
+
+ const handleCopy = async () => {
+ await navigator.clipboard.writeText(url);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
+ {name}
+ Chain ID: {chainId}
+
+
+ {url}
+
+
+
+ );
+}
diff --git a/website/src/components/RPCEndpoint/styles.module.css b/website/src/components/RPCEndpoint/styles.module.css
new file mode 100644
index 00000000..85b9489e
--- /dev/null
+++ b/website/src/components/RPCEndpoint/styles.module.css
@@ -0,0 +1,86 @@
+.endpoint {
+ position: relative;
+ padding: 1rem;
+ border-radius: 12px;
+ background: var(--ifm-card-background-color);
+ border: 1px solid var(--ifm-color-emphasis-300);
+ transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
+ transform-style: preserve-3d;
+}
+
+.endpoint:hover {
+ transform: translateY(-4px) translateZ(20px);
+ box-shadow: 0 12px 40px rgba(37, 99, 235, 0.15);
+ border-color: rgba(37, 99, 235, 0.3);
+}
+
+.endpoint::before {
+ content: '';
+ position: absolute;
+ inset: -2px;
+ border-radius: 14px;
+ background: linear-gradient(135deg, rgba(37, 99, 235, 0.2), rgba(6, 182, 212, 0.2));
+ z-index: -1;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.endpoint:hover::before {
+ opacity: 1;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.75rem;
+}
+
+.name {
+ font-weight: 600;
+ font-size: 0.875rem;
+}
+
+.chainId {
+ font-size: 0.75rem;
+ opacity: 0.7;
+ font-family: var(--ifm-font-family-monospace);
+}
+
+.urlRow {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+}
+
+.url {
+ flex: 1;
+ padding: 0.5rem 0.75rem;
+ background: var(--ifm-code-background);
+ border-radius: 6px;
+ font-size: 0.8125rem;
+ font-family: var(--ifm-font-family-monospace);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.copyButton {
+ padding: 0.5rem 1rem;
+ border: none;
+ border-radius: 6px;
+ background: var(--ifm-color-primary);
+ color: white;
+ font-size: 0.8125rem;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.copyButton:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
+}
+
+.copied {
+ background: var(--ifm-color-success) !important;
+}
diff --git a/website/src/components/ScrollProgress.tsx b/website/src/components/ScrollProgress.tsx
new file mode 100644
index 00000000..07e1ecb7
--- /dev/null
+++ b/website/src/components/ScrollProgress.tsx
@@ -0,0 +1,44 @@
+import React, { useEffect, useState } from 'react';
+
+/**
+ * Thin scroll progress bar fixed to the top of the viewport.
+ */
+
+export function ScrollProgress() {
+ const [progress, setProgress] = useState(0);
+
+ useEffect(() => {
+ let raf = 0;
+ const onScroll = () => {
+ if (raf) return;
+ raf = requestAnimationFrame(() => {
+ const scrollTop = window.scrollY;
+ const docHeight = document.documentElement.scrollHeight - window.innerHeight;
+ const pct = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
+ setProgress(Math.min(100, Math.max(0, pct)));
+ raf = 0;
+ });
+ };
+
+ window.addEventListener('scroll', onScroll, { passive: true });
+ onScroll();
+ return () => {
+ window.removeEventListener('scroll', onScroll);
+ if (raf) cancelAnimationFrame(raf);
+ };
+ }, []);
+
+ return (
+
+ );
+}
+
+export default ScrollProgress;
diff --git a/website/src/components/ScrollToTop.tsx b/website/src/components/ScrollToTop.tsx
new file mode 100644
index 00000000..3fe40b7c
--- /dev/null
+++ b/website/src/components/ScrollToTop.tsx
@@ -0,0 +1,42 @@
+import React, { useEffect, useState } from 'react';
+import { ArrowUp } from 'lucide-react';
+
+export function ScrollToTop() {
+ const [visible, setVisible] = useState(false);
+
+ useEffect(() => {
+ let raf = 0;
+ const onScroll = () => {
+ if (raf) return;
+ raf = requestAnimationFrame(() => {
+ setVisible(window.scrollY > 300);
+ raf = 0;
+ });
+ };
+
+ window.addEventListener('scroll', onScroll, { passive: true });
+ return () => {
+ window.removeEventListener('scroll', onScroll);
+ if (raf) cancelAnimationFrame(raf);
+ };
+ }, []);
+
+ const scrollToTop = () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ };
+
+ return (
+
+ );
+}
+
+export default ScrollToTop;