diff --git a/examples/harkan-abs/README.md b/examples/harkan-abs/README.md
new file mode 100644
index 0000000..a2879d0
--- /dev/null
+++ b/examples/harkan-abs/README.md
@@ -0,0 +1,48 @@
+\# Harkan ABS — Abstract Testnet Game
+
+
+
+Cyberpunk mini‑game inspired by Harkan, built for Abstract Testnet.
+
+
+
+\## Demo
+
+https://abs-game001.vercel.app/
+
+
+
+\## Contract (Abstract Testnet)
+
+0x51F2C923a5307E2701F228DcC4cB3D72B1aAb804
+
+
+
+\## Features
+
+\- AGW login + sponsored tx option
+
+\- On‑chain score submission + leaderboard
+
+\- Static frontend (no build step)
+
+
+
+\## Run locally
+
+python -m http.server 5173
+
+\# then open http://localhost:5173
+
+
+
+\## Usage
+
+\- Switch to Abstract Testnet
+
+\- Connect wallet
+
+\- Play a run
+
+\- Submit on‑chain score
+
diff --git a/examples/harkan-abs/agw-panel.js b/examples/harkan-abs/agw-panel.js
new file mode 100644
index 0000000..2f6277f
--- /dev/null
+++ b/examples/harkan-abs/agw-panel.js
@@ -0,0 +1,213 @@
+import React, { useEffect, useMemo, useState } from "https://esm.sh/react@18.2.0";
+import { createRoot } from "https://esm.sh/react-dom@18.2.0/client";
+import {
+ AbstractWalletProvider,
+ useLoginWithAbstract,
+ useWriteContractSponsored,
+} from "https://esm.sh/@abstract-foundation/agw-react@0.6.0";
+import { useAccount, useWriteContract } from "https://esm.sh/wagmi@2.12.5";
+import {
+ isAddress,
+ parseAbi,
+} from "https://esm.sh/viem@2.21.25";
+import { getGeneralPaymasterInput } from "https://esm.sh/viem@2.21.25/zksync";
+import { abstractTestnet } from "https://esm.sh/viem@2.21.25/chains";
+
+const STORAGE_KEYS = {
+ contract: "harkanContractAddress",
+ paymaster: "harkanPaymasterAddress",
+};
+
+const CONTRACT_ABI = parseAbi([
+ "function submitScore(uint256 score,uint256 duration,uint256 hits,uint256 misses)",
+]);
+
+function getStoredValue(key) {
+ if (typeof localStorage === "undefined") return "";
+ return localStorage.getItem(key) || "";
+}
+
+function setStoredValue(key, value) {
+ if (typeof localStorage === "undefined") return;
+ if (!value) {
+ localStorage.removeItem(key);
+ return;
+ }
+ localStorage.setItem(key, value);
+}
+
+function useLatestScore() {
+ const [latestScore, setLatestScore] = useState(
+ typeof window !== "undefined" ? window.harkanLatestScore || null : null
+ );
+
+ useEffect(() => {
+ const handler = (event) => {
+ setLatestScore(event.detail);
+ };
+ window.addEventListener("harkan:runComplete", handler);
+ return () => window.removeEventListener("harkan:runComplete", handler);
+ }, []);
+
+ return latestScore;
+}
+
+function AgwPanel() {
+ const { login, logout } = useLoginWithAbstract();
+ const { address, status } = useAccount();
+ const { writeContractAsync, isPending: isPendingDirect } = useWriteContract();
+ const { writeContractSponsoredAsync, isPending: isPendingSponsored } =
+ useWriteContractSponsored();
+ const latestScore = useLatestScore();
+
+ const [contractAddress, setContractAddress] = useState(
+ getStoredValue(STORAGE_KEYS.contract)
+ );
+ const [paymasterAddress, setPaymasterAddress] = useState(
+ getStoredValue(STORAGE_KEYS.paymaster)
+ );
+ const [statusMsg, setStatusMsg] = useState("Waiting for run...");
+ const [lastHash, setLastHash] = useState("");
+
+ const isConnected = status === "connected";
+ const scoreSummary = useMemo(() => {
+ if (!latestScore) return "No run data yet";
+ return `${latestScore.score} pts • ${latestScore.hits} hits • ${latestScore.accuracy}%`;
+ }, [latestScore]);
+
+ const handleContractChange = (event) => {
+ const value = event.target.value.trim();
+ setContractAddress(value);
+ setStoredValue(STORAGE_KEYS.contract, value);
+ };
+
+ const handlePaymasterChange = (event) => {
+ const value = event.target.value.trim();
+ setPaymasterAddress(value);
+ setStoredValue(STORAGE_KEYS.paymaster, value);
+ };
+
+ async function submitScore() {
+ if (!latestScore) {
+ setStatusMsg("Play a run first");
+ return;
+ }
+ if (!isAddress(contractAddress)) {
+ setStatusMsg("Set a valid contract address");
+ return;
+ }
+ if (!isConnected) {
+ setStatusMsg("Connect AGW first");
+ return;
+ }
+ try {
+ setStatusMsg("Submitting...");
+ const args = [
+ BigInt(latestScore.score),
+ BigInt(latestScore.duration),
+ BigInt(latestScore.hits),
+ BigInt(latestScore.misses),
+ ];
+
+ let hash = "";
+ if (paymasterAddress) {
+ if (!isAddress(paymasterAddress)) {
+ setStatusMsg("Invalid paymaster address");
+ return;
+ }
+ hash = await writeContractSponsoredAsync({
+ abi: CONTRACT_ABI,
+ address: contractAddress,
+ functionName: "submitScore",
+ args,
+ paymaster: paymasterAddress,
+ paymasterInput: getGeneralPaymasterInput({ innerInput: "0x" }),
+ });
+ } else {
+ hash = await writeContractAsync({
+ abi: CONTRACT_ABI,
+ address: contractAddress,
+ functionName: "submitScore",
+ args,
+ });
+ }
+
+ setLastHash(hash);
+ setStatusMsg("Score submitted");
+ } catch (error) {
+ setStatusMsg("Submission failed");
+ }
+ }
+
+ const isSubmitting = isPendingDirect || isPendingSponsored;
+
+ return (
+
+
+ Status: {isConnected ? "Connected" : "Disconnected"}
+
+
+ Address: {address ? `${address.slice(0, 6)}...${address.slice(-4)}` : "—"}
+
+
Latest run: {scoreSummary}
+
+ Contract Address
+
+
+
+ Paymaster (optional)
+
+
+
+ {!isConnected ? (
+
+ Connect AGW
+
+ ) : (
+
+ Disconnect
+
+ )}
+
+ {isSubmitting ? "Submitting..." : "Submit Score"}
+
+
+
{statusMsg}
+ {lastHash ? (
+
+ ) : null}
+
+ );
+}
+
+function Root() {
+ return (
+
+
+
+ );
+}
+
+const mountNode = document.getElementById("agwMount");
+if (mountNode) {
+ createRoot(mountNode).render( );
+}
diff --git a/examples/harkan-abs/app.js b/examples/harkan-abs/app.js
new file mode 100644
index 0000000..10b1a74
--- /dev/null
+++ b/examples/harkan-abs/app.js
@@ -0,0 +1,898 @@
+let viemModules = null;
+let viemReady = false;
+let viemLoading = null;
+
+const networkBtn = document.getElementById("networkBtn");
+const connectBtn = document.getElementById("connectBtn");
+const walletStatus = document.getElementById("walletStatus");
+const chainBadge = document.getElementById("chainBadge");
+const chainValue = document.getElementById("chainValue");
+const sessionStatus = document.getElementById("sessionStatus");
+const modal = document.getElementById("modal");
+const howBtn = document.getElementById("howBtn");
+const closeModal = document.getElementById("closeModal");
+const enterBtn = document.getElementById("enterBtn");
+
+const scoreValue = document.getElementById("scoreValue");
+const comboValue = document.getElementById("comboValue");
+const timeValue = document.getElementById("timeValue");
+const hitsValue = document.getElementById("hitsValue");
+const missesValue = document.getElementById("missesValue");
+const accuracyValue = document.getElementById("accuracyValue");
+const rankValue = document.getElementById("rankValue");
+
+const startBtn = document.getElementById("startBtn");
+const pauseBtn = document.getElementById("pauseBtn");
+const resetBtn = document.getElementById("resetBtn");
+const saveBtn = document.getElementById("saveBtn");
+const submitBtn = document.getElementById("submitBtn");
+const overlay = document.getElementById("overlay");
+const overlayStart = document.getElementById("overlayStart");
+
+const leaderboardList = document.getElementById("leaderboardList");
+const refreshLeaderboard = document.getElementById("refreshLeaderboard");
+const lastTx = document.getElementById("lastTx");
+
+const playerName = document.getElementById("playerName");
+const contractAddressInput = document.getElementById("contractAddress");
+const contractExplorer = document.getElementById("contractExplorer");
+const bestScoreValue = document.getElementById("bestScoreValue");
+const runsValue = document.getElementById("runsValue");
+const accuracyTotalValue = document.getElementById("accuracyTotalValue");
+const difficultySelect = document.getElementById("difficultySelect");
+const soundToggle = document.getElementById("soundToggle");
+const dailyObjective = document.getElementById("dailyObjective");
+
+const canvas = document.getElementById("gameCanvas");
+const ctx = canvas.getContext("2d");
+
+const DEFAULT_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000";
+const CHAIN_INFO = {
+ testnet: {
+ id: 11124,
+ label: "Abstract Testnet",
+ explorer: "https://sepolia.abscan.org",
+ },
+ mainnet: {
+ id: 2741,
+ label: "Abstract Mainnet",
+ explorer: "https://abscan.org",
+ },
+};
+const NETWORKS = {
+ [CHAIN_INFO.testnet.id]: {
+ label: CHAIN_INFO.testnet.label,
+ explorer: CHAIN_INFO.testnet.explorer,
+ },
+ [CHAIN_INFO.mainnet.id]: {
+ label: CHAIN_INFO.mainnet.label,
+ explorer: CHAIN_INFO.mainnet.explorer,
+ },
+};
+let SCORE_EVENT = null;
+const LOG_BLOCK_RANGE = 50000n;
+const LEADERBOARD_ABI = [
+ {
+ type: "function",
+ name: "submitScore",
+ stateMutability: "nonpayable",
+ inputs: [
+ { name: "score", type: "uint256" },
+ { name: "duration", type: "uint256" },
+ { name: "hits", type: "uint256" },
+ { name: "misses", type: "uint256" },
+ ],
+ outputs: [],
+ },
+];
+
+const STORAGE_KEYS = {
+ profile: "harkanProfile",
+ leaderboard: "harkanLeaderboard",
+ settings: "harkanSettings",
+ daily: "harkanDailyObjective",
+ lastTx: "harkanLastTx",
+ contract: "harkanContractAddress",
+ contractAuto: "harkanContractAuto",
+};
+
+const difficultyMap = {
+ easy: { spawnInterval: 850, ttlMin: 2600, ttlMax: 4200, scoreBoost: 0.9 },
+ standard: { spawnInterval: 700, ttlMin: 2200, ttlMax: 3800, scoreBoost: 1 },
+ hard: { spawnInterval: 550, ttlMin: 1700, ttlMax: 3200, scoreBoost: 1.2 },
+ nightmare: { spawnInterval: 420, ttlMin: 1400, ttlMax: 2600, scoreBoost: 1.35 },
+};
+
+let walletAddress = null;
+let walletClient = null;
+let contractAddress = DEFAULT_CONTRACT_ADDRESS;
+let explorerBase = NETWORKS[CHAIN_INFO.testnet.id].explorer;
+let contractAuto = true;
+let publicClient = null;
+
+const game = {
+ running: false,
+ paused: false,
+ score: 0,
+ combo: 1,
+ timeLeft: 45,
+ hits: 0,
+ misses: 0,
+ targets: [],
+ spawnTimer: null,
+ countdownTimer: null,
+ lastHitTime: 0,
+ runStart: 0,
+ runEnd: 0,
+};
+
+const profile = loadProfile();
+const settings = loadSettings();
+const objective = loadObjective();
+
+let audioContext = null;
+let canSubmitScore = false;
+
+function setStatus(message, isConnected = false) {
+ walletStatus.textContent = message;
+ walletStatus.style.color = isConnected ? "#76ffb3" : "#84f0ff";
+}
+
+function formatChainId(id) {
+ return `0x${id.toString(16)}`;
+}
+
+function isAddressSafe(value) {
+ if (viemReady && viemModules) {
+ return viemModules.isAddress(value);
+ }
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
+}
+
+async function ensureViem() {
+ if (viemReady) return true;
+ if (viemLoading) return viemLoading;
+ viemLoading = (async () => {
+ try {
+ const viem = await import("https://esm.sh/viem@2.21.25");
+ const chains = await import("https://esm.sh/viem@2.21.25/chains");
+ const zksync = await import("https://esm.sh/viem@2.21.25/zksync");
+ viemModules = {
+ ...viem,
+ abstract: chains.abstract,
+ abstractTestnet: chains.abstractTestnet,
+ eip712WalletActions: zksync.eip712WalletActions,
+ };
+ SCORE_EVENT = viemModules.parseAbiItem(
+ "event ScoreSubmitted(address indexed player,uint256 score,uint256 duration,uint256 hits,uint256 misses,uint256 timestamp)"
+ );
+ publicClient = viemModules
+ .createPublicClient({
+ chain: viemModules.abstractTestnet,
+ transport: viemModules.http(),
+ })
+ .extend(viemModules.eip712WalletActions());
+ viemReady = true;
+ return true;
+ } catch (error) {
+ setStatus("Web3 modules blocked");
+ return false;
+ }
+ })();
+ return viemLoading;
+}
+
+function setChainInfo(chainIdHex) {
+ if (!chainIdHex) {
+ if (chainBadge) chainBadge.textContent = "Unknown";
+ if (chainValue) chainValue.textContent = "Unknown";
+ return;
+ }
+ const numericId = Number.parseInt(chainIdHex, 16);
+ const info = NETWORKS[numericId];
+ if (info) {
+ if (chainBadge) chainBadge.textContent = info.label;
+ if (chainValue) chainValue.textContent = info.label;
+ explorerBase = info.explorer;
+ } else {
+ if (chainBadge) chainBadge.textContent = `Chain ${numericId}`;
+ if (chainValue) chainValue.textContent = `Chain ${numericId}`;
+ }
+ applyAutoContract(numericId);
+ refreshLastTx();
+ updateContractExplorer();
+}
+
+function getDefaultContract(chainId) {
+ if (chainId === CHAIN_INFO.testnet.id) {
+ return "0x51F2C923a5307E2701F228DcC4cB3D72B1aAb804";
+ }
+ if (chainId === CHAIN_INFO.mainnet.id) {
+ return "0x51F2C923a5307E2701F228DcC4cB3D72B1aAb804";
+ }
+ return DEFAULT_CONTRACT_ADDRESS;
+}
+
+function applyAutoContract(chainId) {
+ if (!contractAuto) return;
+ const stored = localStorage.getItem(STORAGE_KEYS.contract);
+ if (stored) return;
+ const autoAddress = getDefaultContract(chainId);
+ if (!isAddressSafe(autoAddress)) return;
+ contractAddress = autoAddress;
+ contractAddressInput.value = autoAddress;
+ localStorage.setItem(STORAGE_KEYS.contract, autoAddress);
+}
+
+function updateContractExplorer() {
+ if (!contractExplorer) return;
+ if (!isAddressSafe(contractAddress)) {
+ contractExplorer.setAttribute("aria-disabled", "true");
+ contractExplorer.href = "#";
+ return;
+ }
+ contractExplorer.removeAttribute("aria-disabled");
+ contractExplorer.href = `${explorerBase}/address/${contractAddress}`;
+}
+
+async function switchNetwork() {
+ if (!window.ethereum) {
+ setStatus("Wallet not found");
+ return;
+ }
+
+ try {
+ await window.ethereum.request({
+ method: "wallet_addEthereumChain",
+ params: [
+ {
+ chainId: formatChainId(CHAIN_INFO.testnet.id),
+ chainName: CHAIN_INFO.testnet.label,
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
+ rpcUrls: ["https://api.testnet.abs.xyz"],
+ blockExplorerUrls: [CHAIN_INFO.testnet.explorer],
+ },
+ ],
+ });
+ setStatus("Abstract Testnet ready", true);
+ } catch (error) {
+ setStatus("Network switch rejected");
+ }
+}
+
+async function connectWallet() {
+ if (!window.ethereum) {
+ setStatus("Install a wallet");
+ return;
+ }
+
+ try {
+ const ok = await ensureViem();
+ if (!ok) return;
+ const accounts = await window.ethereum.request({
+ method: "eth_requestAccounts",
+ });
+ walletAddress = accounts[0] || null;
+ if (walletAddress) {
+ const short =
+ walletAddress.slice(0, 6) + "..." + walletAddress.slice(-4);
+ setStatus(`Connected ${short}`, true);
+ walletClient = viemModules
+ .createWalletClient({
+ chain: viemModules.abstractTestnet,
+ transport: viemModules.custom(window.ethereum),
+ })
+ .extend(viemModules.eip712WalletActions());
+ }
+ } catch (error) {
+ setStatus("Connection rejected");
+ }
+}
+
+function openModal() {
+ modal.classList.remove("hidden");
+}
+
+function closeModalView() {
+ modal.classList.add("hidden");
+}
+
+function resetGame() {
+ game.running = false;
+ game.paused = false;
+ game.score = 0;
+ game.combo = 1;
+ game.timeLeft = 45;
+ game.hits = 0;
+ game.misses = 0;
+ game.targets = [];
+ game.runStart = 0;
+ game.runEnd = 0;
+ canSubmitScore = false;
+ clearTimers();
+ updateHud();
+ setScoreButtons();
+ overlay.style.display = "grid";
+ overlay.querySelector("h2").textContent = "NEURAL LINK READY";
+ overlay.querySelector("p").textContent =
+ "Use your mouse or tap to lock onto signals. Each hit boosts your sync score.";
+ sessionStatus.textContent = "Standby";
+ draw();
+}
+
+function startGame() {
+ if (game.running) {
+ return;
+ }
+ game.running = true;
+ game.paused = false;
+ game.runStart = performance.now();
+ sessionStatus.textContent = "Live";
+ overlay.style.display = "none";
+ spawnTargets();
+ startCountdown();
+ requestAnimationFrame(loop);
+}
+
+function pauseGame() {
+ if (!game.running) {
+ return;
+ }
+ game.paused = !game.paused;
+ sessionStatus.textContent = game.paused ? "Paused" : "Live";
+ if (game.paused) {
+ clearTimers();
+ } else {
+ spawnTargets();
+ startCountdown();
+ requestAnimationFrame(loop);
+ }
+}
+
+function endGame() {
+ game.running = false;
+ game.runEnd = performance.now();
+ clearTimers();
+ overlay.style.display = "grid";
+ overlay.querySelector("h2").textContent = "RUN COMPLETE";
+ overlay.querySelector("p").textContent = `Final score: ${game.score}`;
+ sessionStatus.textContent = "Complete";
+ canSubmitScore = true;
+ setScoreButtons();
+ updateObjective();
+ const entry = buildScoreEntry();
+ window.harkanLatestScore = entry;
+ window.dispatchEvent(new CustomEvent("harkan:runComplete", { detail: entry }));
+}
+
+function clearTimers() {
+ if (game.spawnTimer) {
+ clearInterval(game.spawnTimer);
+ game.spawnTimer = null;
+ }
+ if (game.countdownTimer) {
+ clearInterval(game.countdownTimer);
+ game.countdownTimer = null;
+ }
+}
+
+function updateHud() {
+ scoreValue.textContent = game.score;
+ comboValue.textContent = `x${game.combo}`;
+ timeValue.textContent = game.timeLeft;
+ hitsValue.textContent = game.hits;
+ missesValue.textContent = game.misses;
+ accuracyValue.textContent = `${calculateAccuracy(game.hits, game.misses)}%`;
+ rankValue.textContent = calculateRank(game.score);
+}
+
+function getDifficulty() {
+ return difficultyMap[settings.difficulty] || difficultyMap.standard;
+}
+
+function spawnTargets() {
+ const difficulty = getDifficulty();
+ game.spawnTimer = setInterval(() => {
+ if (!game.running || game.paused) return;
+ const radius = 12 + Math.random() * 18;
+ game.targets.push({
+ id: crypto.randomUUID(),
+ x: Math.random() * (canvas.width - radius * 2) + radius,
+ y: Math.random() * (canvas.height - radius * 2) + radius,
+ radius,
+ ttl:
+ difficulty.ttlMin +
+ Math.random() * (difficulty.ttlMax - difficulty.ttlMin),
+ born: performance.now(),
+ });
+ }, difficulty.spawnInterval);
+}
+
+function startCountdown() {
+ game.countdownTimer = setInterval(() => {
+ if (game.paused) return;
+ game.timeLeft -= 1;
+ updateHud();
+ if (game.timeLeft <= 0) {
+ endGame();
+ }
+ }, 1000);
+}
+
+function handleHit() {
+ const difficulty = getDifficulty();
+ const now = performance.now();
+ if (now - game.lastHitTime < 1200) {
+ game.combo += 1;
+ } else {
+ game.combo = 1;
+ }
+ game.lastHitTime = now;
+ const scoreDelta = Math.round(10 * game.combo * difficulty.scoreBoost);
+ game.score += scoreDelta;
+ game.hits += 1;
+ playTone(420);
+ updateHud();
+}
+
+function handleMiss() {
+ game.combo = 1;
+ game.misses += 1;
+ playTone(160);
+ updateHud();
+}
+
+function calculateAccuracy(hits, misses) {
+ const total = hits + misses;
+ if (!total) return 0;
+ return Math.round((hits / total) * 100);
+}
+
+function calculateRank(score) {
+ if (score >= 1200) return "Warden";
+ if (score >= 900) return "Vanguard";
+ if (score >= 650) return "Specter";
+ if (score >= 400) return "Runner";
+ if (score >= 200) return "Scout";
+ return "Cadet";
+}
+
+function drawBackground() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = "rgba(7, 16, 30, 0.65)";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ ctx.strokeStyle = "rgba(93, 244, 255, 0.08)";
+ ctx.lineWidth = 1;
+ for (let i = 0; i < canvas.width; i += 60) {
+ ctx.beginPath();
+ ctx.moveTo(i, 0);
+ ctx.lineTo(i, canvas.height);
+ ctx.stroke();
+ }
+ for (let j = 0; j < canvas.height; j += 60) {
+ ctx.beginPath();
+ ctx.moveTo(0, j);
+ ctx.lineTo(canvas.width, j);
+ ctx.stroke();
+ }
+}
+
+function drawTargets() {
+ const now = performance.now();
+ game.targets = game.targets.filter((target) => {
+ const age = now - target.born;
+ if (age > target.ttl) {
+ handleMiss();
+ return false;
+ }
+ const life = 1 - age / target.ttl;
+ ctx.beginPath();
+ ctx.fillStyle = `rgba(93, 244, 255, ${life})`;
+ ctx.shadowColor = "rgba(93, 244, 255, 0.8)";
+ ctx.shadowBlur = 12;
+ ctx.arc(target.x, target.y, target.radius, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.shadowBlur = 0;
+ return true;
+ });
+}
+
+function loop() {
+ if (!game.running || game.paused) {
+ return;
+ }
+ draw();
+ requestAnimationFrame(loop);
+}
+
+function draw() {
+ drawBackground();
+ drawTargets();
+}
+
+function handleCanvasClick(event) {
+ if (!game.running || game.paused) {
+ return;
+ }
+ const rect = canvas.getBoundingClientRect();
+ const scaleX = canvas.width / rect.width;
+ const scaleY = canvas.height / rect.height;
+ const clickX = (event.clientX - rect.left) * scaleX;
+ const clickY = (event.clientY - rect.top) * scaleY;
+ let hit = false;
+
+ game.targets = game.targets.filter((target) => {
+ const distance = Math.hypot(target.x - clickX, target.y - clickY);
+ if (distance <= target.radius) {
+ hit = true;
+ return false;
+ }
+ return true;
+ });
+
+ if (hit) {
+ handleHit();
+ } else {
+ handleMiss();
+ }
+}
+
+function playTone(frequency) {
+ if (!settings.sound) return;
+ if (!audioContext) {
+ audioContext = new AudioContext();
+ }
+ const oscillator = audioContext.createOscillator();
+ const gainNode = audioContext.createGain();
+ oscillator.frequency.value = frequency;
+ oscillator.type = "sine";
+ gainNode.gain.value = 0.05;
+ oscillator.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+ oscillator.start();
+ oscillator.stop(audioContext.currentTime + 0.08);
+}
+
+function setScoreButtons() {
+ saveBtn.disabled = !canSubmitScore;
+ submitBtn.disabled = !canSubmitScore;
+}
+
+function saveLocalScore() {
+ if (!canSubmitScore) return;
+ const entry = buildScoreEntry();
+ const leaderboard = loadLeaderboard();
+ leaderboard.push(entry);
+ leaderboard.sort((a, b) => b.score - a.score);
+ const trimmed = leaderboard.slice(0, 10);
+ localStorage.setItem(STORAGE_KEYS.leaderboard, JSON.stringify(trimmed));
+ updateProfile(entry);
+ renderLeaderboard();
+ setStatus("Score saved locally", true);
+}
+
+async function submitOnChain() {
+ if (!canSubmitScore) return;
+ const ok = await ensureViem();
+ if (!ok) return;
+ if (!walletClient || !walletAddress) {
+ setStatus("Connect wallet first");
+ return;
+ }
+ if (!isAddressSafe(contractAddress)) {
+ setStatus("Set contract address first");
+ return;
+ }
+
+ try {
+ const chainId = await window.ethereum.request({ method: "eth_chainId" });
+ const numericId = Number.parseInt(chainId, 16);
+ if (![CHAIN_INFO.testnet.id, CHAIN_INFO.mainnet.id].includes(numericId)) {
+ setStatus("Switch to Abstract network");
+ return;
+ }
+
+ const entry = buildScoreEntry();
+ const hash = await walletClient.writeContract({
+ address: contractAddress,
+ abi: LEADERBOARD_ABI,
+ functionName: "submitScore",
+ args: [entry.score, entry.duration, entry.hits, entry.misses],
+ account: walletAddress,
+ });
+
+ localStorage.setItem(STORAGE_KEYS.lastTx, hash);
+ lastTx.innerHTML = `Tx: ${hash.slice(
+ 0,
+ 10
+ )}... `;
+ setStatus("Score submitted", true);
+ } catch (error) {
+ setStatus("Transaction failed");
+ }
+}
+
+function buildScoreEntry() {
+ const duration = Math.round((game.runEnd - game.runStart) / 1000);
+ return {
+ name: profile.name,
+ score: game.score,
+ hits: game.hits,
+ misses: game.misses,
+ accuracy: calculateAccuracy(game.hits, game.misses),
+ duration,
+ timestamp: new Date().toISOString(),
+ };
+}
+
+function renderLeaderboard() {
+ const leaderboard = loadLeaderboard();
+ leaderboardList.innerHTML = "";
+ if (!leaderboard.length) {
+ leaderboardList.innerHTML = "No runs yet. Finish a run to enter. ";
+ return;
+ }
+ leaderboard.forEach((entry, index) => {
+ const item = document.createElement("li");
+ item.innerHTML = `#${index + 1} ${entry.name} ${
+ entry.score
+ } `;
+ leaderboardList.appendChild(item);
+ });
+}
+
+function loadLeaderboard() {
+ const stored = localStorage.getItem(STORAGE_KEYS.leaderboard);
+ return stored ? JSON.parse(stored) : [];
+}
+
+function loadProfile() {
+ const stored = localStorage.getItem(STORAGE_KEYS.profile);
+ return (
+ (stored && JSON.parse(stored)) || {
+ name: "NeuralRunner",
+ bestScore: 0,
+ runs: 0,
+ accuracy: 0,
+ }
+ );
+}
+
+function updateProfile(entry) {
+ profile.runs += 1;
+ profile.bestScore = Math.max(profile.bestScore, entry.score);
+ profile.totalHits = (profile.totalHits || 0) + entry.hits;
+ profile.totalMisses = (profile.totalMisses || 0) + entry.misses;
+ profile.accuracy = calculateAccuracy(profile.totalHits, profile.totalMisses);
+ localStorage.setItem(STORAGE_KEYS.profile, JSON.stringify(profile));
+ updateProfileUI();
+}
+
+function updateProfileUI() {
+ playerName.value = profile.name;
+ bestScoreValue.textContent = profile.bestScore;
+ runsValue.textContent = profile.runs;
+ accuracyTotalValue.textContent = `${profile.accuracy}%`;
+}
+
+function loadSettings() {
+ const stored = localStorage.getItem(STORAGE_KEYS.settings);
+ return (
+ (stored && JSON.parse(stored)) || {
+ difficulty: "standard",
+ sound: true,
+ }
+ );
+}
+
+function saveSettings() {
+ localStorage.setItem(STORAGE_KEYS.settings, JSON.stringify(settings));
+}
+
+function loadObjective() {
+ const stored = localStorage.getItem(STORAGE_KEYS.daily);
+ const todayKey = new Date().toISOString().slice(0, 10);
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ if (parsed.date === todayKey) {
+ return parsed;
+ }
+ }
+ const next = {
+ date: todayKey,
+ targetHits: 20 + Math.floor(Math.random() * 20),
+ targetScore: 300 + Math.floor(Math.random() * 500),
+ };
+ localStorage.setItem(STORAGE_KEYS.daily, JSON.stringify(next));
+ return next;
+}
+
+function updateObjective() {
+ dailyObjective.textContent = `Hit ${objective.targetHits} signals and score ${objective.targetScore}+`;
+}
+
+function refreshLastTx() {
+ const hash = localStorage.getItem(STORAGE_KEYS.lastTx);
+ if (hash) {
+ lastTx.innerHTML = `Tx: ${hash.slice(
+ 0,
+ 10
+ )}... `;
+ }
+}
+
+function handleNameChange() {
+ profile.name = playerName.value.trim() || "NeuralRunner";
+ localStorage.setItem(STORAGE_KEYS.profile, JSON.stringify(profile));
+ renderLeaderboard();
+ refreshOnChainLeaderboard();
+}
+
+function handleDifficultyChange() {
+ settings.difficulty = difficultySelect.value;
+ saveSettings();
+ resetGame();
+}
+
+function handleSoundToggle() {
+ settings.sound = soundToggle.checked;
+ saveSettings();
+}
+
+function loadContractAddress() {
+ const stored = localStorage.getItem(STORAGE_KEYS.contract);
+ if (stored && isAddressSafe(stored)) {
+ return stored;
+ }
+ return DEFAULT_CONTRACT_ADDRESS;
+}
+
+function loadContractAuto() {
+ const stored = localStorage.getItem(STORAGE_KEYS.contractAuto);
+ if (stored === "false") {
+ return false;
+ }
+ return true;
+}
+
+function setContractAuto(value) {
+ contractAuto = value;
+ localStorage.setItem(STORAGE_KEYS.contractAuto, value ? "true" : "false");
+}
+
+function handleContractChange() {
+ const value = contractAddressInput.value.trim();
+ if (!value) {
+ contractAddress = DEFAULT_CONTRACT_ADDRESS;
+ localStorage.removeItem(STORAGE_KEYS.contract);
+ setContractAuto(false);
+ setStatus("Contract cleared");
+ updateContractExplorer();
+ return;
+ }
+ if (!isAddressSafe(value)) {
+ setContractAuto(false);
+ setStatus("Invalid contract address");
+ return;
+ }
+ contractAddress = value;
+ localStorage.setItem(STORAGE_KEYS.contract, value);
+ setContractAuto(false);
+ setStatus("Contract saved", true);
+ updateContractExplorer();
+ refreshOnChainLeaderboard();
+}
+
+async function refreshOnChainLeaderboard() {
+ const ok = await ensureViem();
+ if (!ok) return;
+ if (!publicClient || !SCORE_EVENT) {
+ return;
+ }
+ if (!isAddressSafe(contractAddress)) {
+ return;
+ }
+ try {
+ const latestBlock = await publicClient.getBlockNumber();
+ const fromBlock =
+ latestBlock > LOG_BLOCK_RANGE ? latestBlock - LOG_BLOCK_RANGE : 0n;
+ const logs = await publicClient.getLogs({
+ address: contractAddress,
+ event: SCORE_EVENT,
+ fromBlock,
+ toBlock: "latest",
+ });
+ const bestByPlayer = new Map();
+ logs.forEach((log) => {
+ const player = log.args.player;
+ const score = Number(log.args.score);
+ const hits = Number(log.args.hits);
+ const misses = Number(log.args.misses);
+ const duration = Number(log.args.duration);
+ const existing = bestByPlayer.get(player);
+ if (!existing || score > existing.score) {
+ bestByPlayer.set(player, {
+ player,
+ score,
+ hits,
+ misses,
+ duration,
+ });
+ }
+ });
+ const entries = Array.from(bestByPlayer.values())
+ .sort((a, b) => b.score - a.score)
+ .slice(0, 10);
+ if (entries.length) {
+ leaderboardList.innerHTML = "";
+ entries.forEach((entry, index) => {
+ const item = document.createElement("li");
+ const name =
+ profile.name && entry.player === walletAddress
+ ? profile.name
+ : `${entry.player.slice(0, 6)}...${entry.player.slice(-4)}`;
+ item.innerHTML = `#${index + 1} ${name} ${
+ entry.score
+ } `;
+ leaderboardList.appendChild(item);
+ });
+ }
+ } catch (error) {
+ setStatus("On-chain sync failed");
+ }
+}
+
+networkBtn.addEventListener("click", switchNetwork);
+connectBtn.addEventListener("click", connectWallet);
+howBtn.addEventListener("click", openModal);
+closeModal.addEventListener("click", closeModalView);
+enterBtn.addEventListener("click", () => {
+ overlay.style.display = "grid";
+ overlay.querySelector("h2").textContent = "NEURAL LINK READY";
+ overlay.querySelector("p").textContent =
+ "Use your mouse or tap to lock onto signals. Each hit boosts your sync score.";
+ openModal();
+});
+
+startBtn.addEventListener("click", startGame);
+pauseBtn.addEventListener("click", pauseGame);
+resetBtn.addEventListener("click", resetGame);
+overlayStart.addEventListener("click", startGame);
+canvas.addEventListener("click", handleCanvasClick);
+
+saveBtn.addEventListener("click", saveLocalScore);
+submitBtn.addEventListener("click", submitOnChain);
+refreshLeaderboard.addEventListener("click", renderLeaderboard);
+playerName.addEventListener("change", handleNameChange);
+difficultySelect.addEventListener("change", handleDifficultyChange);
+soundToggle.addEventListener("change", handleSoundToggle);
+contractAddressInput.addEventListener("change", handleContractChange);
+
+function boot() {
+ playerName.value = profile.name;
+ difficultySelect.value = settings.difficulty;
+ soundToggle.checked = settings.sound;
+ contractAuto = loadContractAuto();
+ contractAddress = loadContractAddress();
+ contractAddressInput.value =
+ contractAddress === DEFAULT_CONTRACT_ADDRESS ? "" : contractAddress;
+ updateContractExplorer();
+ if (window.ethereum) {
+ window.ethereum
+ .request({ method: "eth_chainId" })
+ .then(setChainInfo)
+ .catch(() => setChainInfo(null));
+ window.ethereum.on("chainChanged", setChainInfo);
+ } else {
+ setChainInfo(null);
+ }
+ updateProfileUI();
+ renderLeaderboard();
+ refreshOnChainLeaderboard();
+ updateObjective();
+ refreshLastTx();
+ setScoreButtons();
+ resetGame();
+}
+
+boot();
diff --git a/examples/harkan-abs/index.html b/examples/harkan-abs/index.html
new file mode 100644
index 0000000..7d275ca
--- /dev/null
+++ b/examples/harkan-abs/index.html
@@ -0,0 +1,256 @@
+
+
+
+
+
+ Harkan ABS — Neural Arena
+
+
+
+
+
+
+
+
+
+
NEURAL LINK PROTOCOL
+
ENTER HARKAN WORLD
+
+ A cyberpunk arena built for Abstract. Jack in, track signals, and
+ claim your score on-chain.
+
+
+ Enter Simulation
+ How It Works
+
+
+
+
+
NEURAL SIGNALS
+
+
+ Tap signals before they fade. Each hit increases your sync score.
+ Miss too many and the link collapses.
+
+
+ Time limit: 45 seconds
+ Combo bonus for consecutive hits
+ Sync score posted after run
+
+
+
+
+
+
+
+
+
+
+
+
NEURAL LINK READY
+
+ Use your mouse or tap to lock onto signals. Each hit boosts your
+ sync score.
+
+
Begin
+
+
+
+
+
+ Hits
+ 0
+
+
+ Misses
+ 0
+
+
+ Accuracy
+ 0%
+
+
+ Rank
+ Cadet
+
+
+
+
+
+
+
+
ABSTRACT GLOBAL WALLET
+
+
+
+
PLAYER PROFILE
+
+
+ Alias
+
+
+
+ Contract Address
+
+
+
+ Open in explorer
+
+
+
+ Best Score
+ 0
+
+
+ Total Runs
+ 0
+
+
+ Accuracy
+ 0%
+
+
+
+
+ Difficulty
+
+ Easy
+ Standard
+ Hard
+ Nightmare
+
+
+
+
+ Sound FX
+
+
+
+ Daily Objective
+ Calibrating...
+
+
+ Paste your deployed NeuralArena contract to enable on-chain
+ leaderboards and submissions.
+
+
+
+
+
+
+
+
Wallet Login
+
+ Use Abstract Global Wallet or any EIP-1193 wallet to connect and
+ track your runs.
+
+
+
+
On-chain Score
+
+ Scores can be written to a smart contract or leaderboard indexer
+ after each run.
+
+
+
+
Gas Sponsored
+
+ Abstract paymasters let you sponsor gas so players can run for free.
+
+
+
+
+
+
+ ABS NETWORK READY
+ © 2026 Harkan ABS Prototype
+
+
+
+
+
How it works
+
+ This prototype mirrors the Harkan cyberpunk vibe while running on
+ Abstract. Connect your wallet, enter the arena, and hit as many
+ signals as you can before the timer ends.
+
+
+ When you are ready to deploy, wire the score submission to an
+ Abstract smart contract using Viem or AGW.
+
+
Got it
+
+
+
+
+
+
+
diff --git a/examples/harkan-abs/styles.css b/examples/harkan-abs/styles.css
new file mode 100644
index 0000000..43798d7
--- /dev/null
+++ b/examples/harkan-abs/styles.css
@@ -0,0 +1,493 @@
+@import url("https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700&family=Rajdhani:wght@400;600&display=swap");
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: "Rajdhani", sans-serif;
+ color: #d5f5ff;
+ background: radial-gradient(circle at top, #0b1224 0%, #04070f 55%, #02050b 100%);
+ min-height: 100vh;
+ overflow-x: hidden;
+}
+
+a {
+ color: #7ad5ff;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+main {
+ padding: 32px 6vw 64px;
+}
+
+.scanlines {
+ position: fixed;
+ inset: 0;
+ background: repeating-linear-gradient(
+ to bottom,
+ rgba(255, 255, 255, 0.02),
+ rgba(255, 255, 255, 0.02) 1px,
+ transparent 1px,
+ transparent 4px
+ );
+ pointer-events: none;
+ mix-blend-mode: screen;
+ opacity: 0.35;
+ z-index: 1;
+}
+
+.topbar {
+ position: sticky;
+ top: 0;
+ z-index: 10;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 6vw;
+ background: rgba(3, 6, 15, 0.85);
+ border-bottom: 1px solid rgba(96, 225, 255, 0.2);
+ backdrop-filter: blur(8px);
+}
+
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-family: "Orbitron", sans-serif;
+ letter-spacing: 2px;
+ font-size: 14px;
+}
+
+.brand-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ background: #5df4ff;
+ box-shadow: 0 0 12px #5df4ff;
+}
+
+.wallet {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.btn {
+ background: linear-gradient(135deg, #29f1ff, #7a5cff);
+ color: #02101a;
+ border: none;
+ padding: 10px 18px;
+ border-radius: 999px;
+ font-weight: 600;
+ cursor: pointer;
+ font-family: "Rajdhani", sans-serif;
+ letter-spacing: 0.5px;
+ box-shadow: 0 0 18px rgba(41, 241, 255, 0.35);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.btn:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 0 22px rgba(122, 92, 255, 0.35);
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ box-shadow: none;
+}
+
+.btn.ghost {
+ background: transparent;
+ color: #a5f3ff;
+ border: 1px solid rgba(93, 244, 255, 0.4);
+ box-shadow: none;
+}
+
+.btn.large {
+ padding: 14px 24px;
+ font-size: 15px;
+}
+
+.status {
+ font-size: 12px;
+ color: #84f0ff;
+}
+
+.badge {
+ font-size: 11px;
+ padding: 4px 10px;
+ border-radius: 999px;
+ border: 1px solid rgba(93, 244, 255, 0.4);
+ color: #9ee9ff;
+ text-transform: uppercase;
+ letter-spacing: 1px;
+}
+
+.hero {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 32px;
+ align-items: center;
+ padding: 40px 0 48px;
+ position: relative;
+ z-index: 2;
+}
+
+.hero-copy h1 {
+ font-family: "Orbitron", sans-serif;
+ font-size: clamp(32px, 5vw, 56px);
+ margin: 8px 0 12px;
+ text-shadow: 0 0 18px rgba(93, 244, 255, 0.4);
+}
+
+.subtle {
+ font-size: 12px;
+ letter-spacing: 4px;
+ text-transform: uppercase;
+ color: #6ee7ff;
+}
+
+.lead {
+ max-width: 480px;
+ line-height: 1.6;
+ color: #b9e9ff;
+}
+
+.hero-actions {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ margin: 24px 0;
+}
+
+.meta {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: 16px;
+ margin-top: 16px;
+}
+
+.label {
+ display: block;
+ font-size: 11px;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ color: #5bb9d8;
+}
+
+.value {
+ font-size: 16px;
+ font-weight: 600;
+ color: #d5f5ff;
+}
+
+.hero-panel {
+ border: 1px solid rgba(93, 244, 255, 0.35);
+ border-radius: 16px;
+ padding: 24px;
+ background: rgba(10, 24, 45, 0.65);
+ box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.4);
+}
+
+.panel-title {
+ font-family: "Orbitron", sans-serif;
+ font-size: 12px;
+ letter-spacing: 3px;
+ color: #7ad5ff;
+ margin-bottom: 12px;
+}
+
+.panel-body p {
+ margin-top: 0;
+ line-height: 1.6;
+}
+
+.panel-body ul {
+ padding-left: 18px;
+ margin: 12px 0 0;
+ color: #a7d8ef;
+}
+
+.arena {
+ margin-top: 24px;
+ border: 1px solid rgba(93, 244, 255, 0.25);
+ border-radius: 18px;
+ padding: 24px;
+ background: rgba(5, 12, 24, 0.85);
+ position: relative;
+ z-index: 2;
+}
+
+.arena-header {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 18px;
+}
+
+.arena-title {
+ font-family: "Orbitron", sans-serif;
+ letter-spacing: 3px;
+ font-size: 14px;
+}
+
+.stats {
+ display: flex;
+ gap: 20px;
+ flex-wrap: wrap;
+}
+
+.arena-actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.score-actions {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.arena-canvas {
+ position: relative;
+ border-radius: 16px;
+ overflow: hidden;
+ border: 1px solid rgba(93, 244, 255, 0.2);
+}
+
+canvas {
+ width: 100%;
+ height: auto;
+ display: block;
+ background: radial-gradient(circle at bottom, #0f1f34, #07111f 60%, #04070f 100%);
+}
+
+.arena-overlay {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ background: rgba(2, 5, 12, 0.6);
+}
+
+.overlay-card {
+ text-align: center;
+ padding: 24px;
+ border-radius: 16px;
+ background: rgba(8, 18, 32, 0.9);
+ border: 1px solid rgba(93, 244, 255, 0.35);
+ max-width: 320px;
+}
+
+.run-summary {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: 16px;
+ padding: 18px;
+ margin-top: 16px;
+ border-radius: 14px;
+ border: 1px solid rgba(93, 244, 255, 0.18);
+ background: rgba(6, 14, 26, 0.75);
+}
+
+.dashboard {
+ margin-top: 42px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 24px;
+}
+
+.panel {
+ border-radius: 16px;
+ border: 1px solid rgba(93, 244, 255, 0.25);
+ background: rgba(8, 18, 32, 0.75);
+ padding: 22px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+#agwPanel {
+ min-height: 320px;
+}
+
+.agw-card {
+ display: grid;
+ gap: 14px;
+}
+
+.agw-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.agw-status {
+ font-size: 13px;
+ color: #c5f3ff;
+}
+
+.agw-field {
+ display: grid;
+ gap: 6px;
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ color: #6bd8f5;
+}
+
+.panel-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.leaderboard {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: 10px;
+}
+
+.leaderboard li {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px 12px;
+ border-radius: 12px;
+ background: rgba(11, 24, 40, 0.8);
+ border: 1px solid rgba(93, 244, 255, 0.15);
+ font-size: 14px;
+}
+
+.profile {
+ display: grid;
+ gap: 16px;
+}
+
+.field {
+ display: grid;
+ gap: 8px;
+ font-size: 13px;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ color: #6bd8f5;
+}
+
+.field.inline {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 12px;
+}
+
+.input {
+ padding: 10px 12px;
+ border-radius: 12px;
+ border: 1px solid rgba(93, 244, 255, 0.3);
+ background: rgba(5, 10, 18, 0.9);
+ color: #d5f5ff;
+ font-family: "Rajdhani", sans-serif;
+ font-size: 14px;
+}
+
+.profile-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ gap: 12px;
+}
+
+.settings {
+ display: grid;
+ gap: 12px;
+}
+
+.objective {
+ padding: 12px 14px;
+ border-radius: 12px;
+ border: 1px solid rgba(93, 244, 255, 0.25);
+ background: rgba(10, 22, 40, 0.8);
+}
+
+.subtle-text {
+ color: #7fc9e6;
+ font-size: 12px;
+}
+
+.note {
+ line-height: 1.4;
+}
+
+.grid {
+ margin-top: 48px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 20px;
+}
+
+.card {
+ padding: 20px;
+ border-radius: 14px;
+ border: 1px solid rgba(93, 244, 255, 0.2);
+ background: rgba(8, 18, 32, 0.7);
+}
+
+footer {
+ padding: 24px 6vw 40px;
+ display: flex;
+ justify-content: space-between;
+ font-size: 12px;
+ color: #6fd6ff;
+ letter-spacing: 2px;
+ border-top: 1px solid rgba(93, 244, 255, 0.2);
+}
+
+.modal {
+ position: fixed;
+ inset: 0;
+ display: grid;
+ place-items: center;
+ background: rgba(3, 6, 15, 0.7);
+ z-index: 20;
+}
+
+.modal.hidden {
+ display: none;
+}
+
+.modal-card {
+ padding: 24px;
+ max-width: 420px;
+ border-radius: 16px;
+ background: #0b1326;
+ border: 1px solid rgba(93, 244, 255, 0.3);
+}
+
+@media (max-width: 720px) {
+ .topbar {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ }
+
+ .wallet {
+ width: 100%;
+ flex-wrap: wrap;
+ }
+
+ footer {
+ flex-direction: column;
+ gap: 8px;
+ }
+}