/**
 * <folk-wallet-viewer> — multichain Safe wallet visualization.
 *
 * Enter a Safe address to see balances across chains, transfer history,
 * and flow visualizations. Authenticated users can link external wallets
 * via EIP-6963 + SIWE.
 */

import { transformToTimelineData, transformToSankeyData, transformToMultichainData, explorerLink, txExplorerLink } from "../lib/data-transform";
import type { TimelineEntry, SankeyData, MultichainData, TransferRecord } from "../lib/data-transform";
import { loadD3, renderTimeline, renderFlowChart, renderSankey } from "../lib/wallet-viz";
import { DEMO_TIMELINE_DATA, DEMO_SANKEY_DATA, DEMO_MULTICHAIN_DATA, DEMO_YIELD_RATES } from "../lib/wallet-demo-data";
import { buildProtocolComparisons, computeYieldAtDays, resolveApy } from "../lib/yield-sandbox";
import type { ProtocolComparison, SandboxAsset } from "../lib/yield-sandbox";
import { TourEngine } from "../../../shared/tour-engine";
import { WalletLocalFirstClient } from "../local-first-client";
import type { WalletDoc, WatchedAddress } from "../schemas";
import { walletSchema, walletDocId } from "../schemas";
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import type { DocumentId } from "../../../shared/local-first/document";
import { ViewHistory } from "../../../shared/view-history.js";

interface ChainInfo {
	chainId: string;
	name: string;
	prefix: string;
	color: string;
}

interface BalanceItem {
	tokenAddress: string | null;
	token: { name: string; symbol: string; decimals: number } | null;
	balance: string;
	fiatBalance: string;
	fiatConversion: string;
}

interface LinkedWallet {
	id: string;
	address: string;
	type: "eoa" | "safe";
	label: string;
	providerName?: string;
	providerRdns?: string;
	safeInfo?: {
		threshold: number;
		ownerCount: number;
		isEncryptIdOwner: boolean;
	};
}

interface DiscoveredProvider {
	uuid: string;
	name: string;
	icon: string;
	rdns: string;
}

const CHAIN_COLORS: Record<string, string> = {
	"1": "#627eea",
	"10": "#ff0420",
	"100": "#04795b",
	"137": "#8247e5",
	"8453": "#0052ff",
	"42161": "#28a0f0",
	"42220": "#35d07f",
	"43114": "#e84142",
	"56": "#f3ba2f",
	"324": "#8c8dfc",
	"11155111": "#f59e0b",
	"84532": "#f59e0b",
	"421614": "#9ca3af",
	"11155420": "#ff6680",
	"80002": "#a855f7",
	"43113": "#fb923c",
	"97": "#fbbf24",
	"59144": "#0E76FD",
	"59141": "#3b82f6",
	"local": "#22c55e",
};

const COLORS_TX = { cyan: "#00d4ff", green: "#4ade80", red: "#f87171" };

const CHAIN_NAMES: Record<string, string> = {
	"1": "Ethereum", "10": "Optimism", "100": "Gnosis", "137": "Polygon",
	"8453": "Base", "42161": "Arbitrum", "42220": "Celo", "43114": "Avalanche",
	"56": "BSC", "324": "zkSync", "59144": "Linea", "local": "CRDT",
};

const EXAMPLE_WALLETS = [
	{ name: "TEC Commons Fund", address: "0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1", type: "Safe" },
	{ name: "Gitcoin Treasury", address: "0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6", type: "Safe" },
	{ name: "Vitalik.eth", address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", type: "EOA" },
];

type ViewTab = "balances" | "budget" | "flows";

interface YieldRate {
	protocol: string;
	chainId: string;
	asset: string;
	assetAddress: string;
	vaultAddress: string;
	apy: number;
	apyBase?: number;
	apy7d?: number;
	apy30d?: number;
	tvl?: number;
	vaultName?: string;
}

interface YieldPositionData {
	protocol: string;
	chainId: string;
	asset: string;
	vaultAddress: string;
	shares: string;
	underlying: string;
	decimals: number;
	apy: number;
	dailyEarnings: number;
	annualEarnings: number;
}

interface YieldSuggestionData {
	type: "deposit" | "rebalance";
	priority: "high" | "medium" | "low";
	to: YieldRate;
	amount: string;
	amountUSD: number;
	reason: string;
	estimatedGasCostUSD: number;
}

interface AllChainBalanceEntry {
	chainId: string;
	chainName: string;
	balances: BalanceItem[];
}

class FolkWalletViewer extends HTMLElement {
	private shadow: ShadowRoot;
	private space = "";
	private address = "";
	private detectedChains: ChainInfo[] = [];
	private selectedChain: string | null = null;
	private balances: BalanceItem[] = [];
	private allChainBalances: Map<string, AllChainBalanceEntry> = new Map();
	private chainFilter: string | null = null; // null = show all
	private loading = false;
	private error = "";
	private isDemo = false;
	private walletType: "safe" | "eoa" | "" = "";
	private includeTestnets = true;

	// Linked wallets state
	private isAuthenticated = false;
	private passKeyEOA = "";
	private userDID = "";
	private linkedWallets: LinkedWallet[] = [];
	private showProviderPicker = false;
	private discoveredProviders: DiscoveredProvider[] = [];
	private linkingInProgress = false;
	private linkError = "";

	// CRDT token balances
	private crdtBalances: Array<{ tokenId: string; name: string; symbol: string; decimals: number; icon: string; color: string; balance: number }> = [];
	private crdtLoading = false;

	// DeFi positions (Zerion)
	private defiPositions: Array<{ protocol: string; type: string; chain: string; chainId: string; tokens: Array<{ symbol: string; amount: number; valueUSD: number }>; totalValueUSD: number }> = [];
	private defiTotalUSD = 0;
	private defiLoading = false;

	private myWalletBalances: Map<string, Array<{ chainId: string; chainName: string; balances: BalanceItem[] }>> = new Map();
	private myWalletsLoading = false;

	// Yield state
	private yieldRates: YieldRate[] = [];
	private yieldPositions: YieldPositionData[] = [];
	private yieldSuggestions: YieldSuggestionData[] = [];
	private yieldLoading = false;
	private yieldTotalIdleUSD = 0;
	private yieldTotalDepositedUSD = 0;
	private yieldWeightedAPY = 0;
	private yieldDepositInProgress = false;
	private yieldError = "";

	// Sandbox simulator state
	private sandboxActive = false;
	private sandboxPrincipal = 10000;
	private sandboxAsset: SandboxAsset = "USDC";
	private sandboxChain: "1" | "8453" | "" = "";
	private sandboxDays = 365;
	private sandboxComparisons: ProtocolComparison[] = [];

	// UX state
	private addressBarExpanded = false;
	private detailsModalOpen = false;

	// Visualization state
	private activeView: ViewTab = "budget";
	private _history = new ViewHistory<ViewTab>("budget", "rwallet");
	private transfers: Map<string, any> | null = null;
	private transfersLoading = false;
	private d3Ready = false;
	private vizData: {
		timeline?: TimelineEntry[];
		sankey?: SankeyData;
		multichain?: MultichainData;
	} = {};

	// Flows scrubber state
	private flowsScrubberPos = -1; // -1 = show all
	private flowsPlayInterval: ReturnType<typeof setInterval> | null = null;

	// Guided tour
	private _tour!: TourEngine;
	private static readonly TOUR_STEPS = [
		{ target: '#address-input', title: "Enter Address", message: "Paste any wallet or Safe multisig address to load balances across all supported chains.", advanceOnClick: false },
		{ target: '#testnet-toggle', title: "Testnet Toggle", message: "Include testnet chains in the scan. Useful for checking Safe deployments on Sepolia or Goerli.", advanceOnClick: true },
		{ target: '.chain-btn', title: "Chain Selector", message: "Click a chain to load balances for that specific network. Active chains are highlighted.", advanceOnClick: false },
		{ target: '.view-tab', title: "Dashboard Views", message: "Switch between balance view, transfer timeline, and flow visualisations for deeper analysis.", advanceOnClick: false },
	];

	// Multiplayer state
	private lfClient: WalletLocalFirstClient | null = null;
	private _lfcUnsub: (() => void) | null = null;
	private _subscribedDocIds: string[] = [];
	private watchedAddresses: WatchedAddress[] = [];
	private _stopPresence: (() => void) | null = null;
	// Membrane — caller's role for visibility filtering
	private _myRole: string = 'viewer';

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open" });
		this._tour = new TourEngine(
			this.shadow,
			FolkWalletViewer.TOUR_STEPS,
			"rwallet_tour_done",
			() => this.shadow.host as HTMLElement,
		);
	}

	connectedCallback() {
		// Read initial-view attribute from server route
		const initialView = this.getAttribute("initial-view");
		if (initialView && ["balances", "budget", "flows"].includes(initialView)) {
			this.activeView = initialView as ViewTab;
		}

		const space = this.getAttribute("space") || "";
		this.space = space;
		if (space === "demo") {
			this.loadDemoData();
		} else {
			const params = new URLSearchParams(window.location.search);
			this.address = params.get("address") || "";
			this.checkAuthState();
			this.initWalletSync(space);
			this.loadMyRole();

			// Auto-load address from passkey or linked wallet
			if (!this.address && this.passKeyEOA) {
				this.address = this.passKeyEOA;
			}

			this.render();
			if (this.address) this.detectChains();
		}
		if (!localStorage.getItem("rwallet_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rwallet', context: 'Wallet' }));
		window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
	}

	disconnectedCallback() {
		this._history.destroy();
		window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
		this._stopPresence?.();
		if (this.flowsPlayInterval) {
			clearInterval(this.flowsPlayInterval);
			this.flowsPlayInterval = null;
		}
		if (this._quoteTimer) { clearTimeout(this._quoteTimer); this._quoteTimer = null; }
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime) {
			for (const id of this._subscribedDocIds) runtime.unsubscribe(id);
		}
		this._subscribedDocIds = [];
	}

	private _onViewRestored = (e: CustomEvent) => {
		if (e.detail?.moduleId !== 'rwallet') return;
		this.activeView = e.detail.view;
		this.render();
		if (this.activeView !== "balances") {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}
	};

	// ── Membrane ──

	private _isVisibleTo(vis: string | undefined | null, role: string): boolean {
		const levels: Record<string, number> = { viewer: 0, member: 1, moderator: 2, admin: 3 };
		return (levels[role] ?? 0) >= (levels[vis ?? 'viewer'] ?? 0);
	}

	private async loadMyRole() {
		if (this.isDemo || this.space === 'demo') return;
		const token = localStorage.getItem('encryptid-token');
		if (!token) return;
		try {
			const res = await fetch(`/api/space-access/${encodeURIComponent(this.space)}`, {
				headers: { Authorization: `Bearer ${token}` },
			});
			if (res.ok) {
				const data = await res.json();
				this._myRole = data.role || 'viewer';
			}
		} catch {}
	}

	private checkAuthState() {
		try {
			const session = localStorage.getItem("encryptid_session");
			if (session) {
				const parsed = JSON.parse(session);
				if (parsed.claims?.exp > Math.floor(Date.now() / 1000)) {
					this.isAuthenticated = true;
					// Default to visualizer tab — user's wallet auto-populates the address
					this.passKeyEOA = parsed.claims?.eid?.walletAddress || "";
					this.userDID = parsed.claims?.did || "";
					this.loadLinkedWallets().then(() => {
						this.loadMyWalletBalances();
						// If no passkey EOA but linked wallets exist, use first linked wallet
						if (!this.address && !this.passKeyEOA && this.linkedWallets.length > 0) {
							this.address = this.linkedWallets[0].address;
							this.render();
							this.detectChains();
						}
					});
					this.loadCRDTBalances();
				}
			}
		} catch {}
	}

	private async initWalletSync(space: string) {
		try {
			this.lfClient = new WalletLocalFirstClient(space);
			await this.lfClient.init();
			await this.lfClient.subscribe();

			this._lfcUnsub = this.lfClient.onChange((doc) => {
				this.extractWatchlist(doc);
				this.render();
			});

			const doc = this.lfClient.getDoc();
			if (doc) this.extractWatchlist(doc);
		} catch (err) {
			console.warn('[rWallet] Local-first init failed:', err);
		}
		this.subscribeCollabOverlay();
	}

	private async subscribeCollabOverlay() {
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (!runtime?.isInitialized) return;
		try {
			const docId = walletDocId(this.space) as DocumentId;
			await runtime.subscribe(docId, walletSchema);
			this._subscribedDocIds.push(docId);
		} catch { /* runtime unavailable */ }
	}

	private extractWatchlist(doc: WalletDoc) {
		this.watchedAddresses = doc.watchedAddresses
			? Object.values(doc.watchedAddresses).sort((a, b) => b.addedAt - a.addedAt)
			: [];
	}

	private addToWatchlist(address: string, chain: string, label: string) {
		if (!this.lfClient) return;
		const did = this.userDID || null;
		this.lfClient.addWatchAddress(address, chain, label, did);
	}

	private removeFromWatchlist(key: string) {
		if (!this.lfClient) return;
		this.lfClient.removeWatchAddress(key);
	}

	private getAuthToken(): string | null {
		try {
			const session = localStorage.getItem("encryptid_session");
			if (!session) return null;
			const parsed = JSON.parse(session);
			return parsed.accessToken || null;
		} catch {
			return null;
		}
	}

	private async loadLinkedWallets() {
		const token = this.getAuthToken();
		if (!token) return;

		try {
			const res = await fetch("/encryptid/api/wallet-link/list", {
				headers: { "Authorization": `Bearer ${token}` },
			});
			if (!res.ok) return;

			const data = await res.json();
			// Server decrypts at rest and returns entry data directly
			this.linkedWallets = (data.wallets || []).map((w: any) => ({
				id: w.id,
				address: w.address || "",
				type: w.type || "eoa",
				label: w.label || "",
				providerName: w.providerName,
				providerRdns: w.providerRdns,
				safeInfo: w.safeInfo,
			}));
			this.render();
		} catch {}
	}

	private async loadCRDTBalances() {
		const token = this.getAuthToken();
		if (!token) return;
		this.crdtLoading = true;
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/crdt-tokens/my-balances`, {
				headers: { "Authorization": `Bearer ${token}` },
			});
			if (res.ok) {
				const data = await res.json();
				this.crdtBalances = data.balances || [];
			}
		} catch {}
		this.crdtLoading = false;
		this.render();
	}

	private async loadDefiPositions(address?: string) {
		const addr = address || this.address;
		if (!addr) return;
		this.defiLoading = true;
		this.render();
		try {
			const base = this.getApiBase();
			const res = await fetch(`${base}/api/defi/${addr}/positions`);
			if (res.ok) {
				const data = await res.json();
				this.defiPositions = data.positions || [];
				this.defiTotalUSD = data.totalUSD || 0;
			}
		} catch {}
		this.defiLoading = false;
		this.render();
	}

	// ── Yield data loading ──

	private async loadYieldData() {
		this.yieldLoading = true;
		this.yieldError = "";
		this.render();

		const base = this.getApiBase();

		try {
			// Always fetch rates (public)
			const ratesRes = await fetch(`${base}/api/yield/rates`);
			if (ratesRes.ok) {
				const data = await ratesRes.json();
				this.yieldRates = data.rates || [];
			}

			// Fetch positions if we have an address
			if (this.address) {
				const posRes = await fetch(`${base}/api/yield/${this.address}/positions`);
				if (posRes.ok) {
					const data = await posRes.json();
					this.yieldPositions = data.positions || [];
				}
			}

			// Fetch strategy if authenticated
			const token = this.getAuthToken();
			if (token && this.address) {
				const stratRes = await fetch(`${base}/api/yield/${this.address}/strategy`, {
					headers: { "Authorization": `Bearer ${token}` },
				});
				if (stratRes.ok) {
					const data = await stratRes.json();
					const s = data.strategy || {};
					this.yieldSuggestions = s.suggestions || [];
					this.yieldTotalIdleUSD = s.totalIdleUSD || 0;
					this.yieldTotalDepositedUSD = s.totalDepositedUSD || 0;
					this.yieldWeightedAPY = s.weightedAPY || 0;
				}
			}
		} catch (err) {
			this.yieldError = "Failed to load yield data";
			console.warn("yield load error", err);
		}

		this.yieldLoading = false;
		if (this.sandboxActive) this.recomputeSandbox();
		this.render();
	}

	private async handleYieldDeposit(suggestion: YieldSuggestionData) {
		const token = this.getAuthToken();
		if (!token) {
			this.yieldError = "Authentication required for deposits";
			this.render();
			return;
		}

		this.yieldDepositInProgress = true;
		this.yieldError = "";
		this.render();

		const base = this.getApiBase();
		try {
			const res = await fetch(
				`${base}/api/yield/${suggestion.to.chainId}/${this.address}/build-deposit`,
				{
					method: "POST",
					headers: {
						"Content-Type": "application/json",
						"Authorization": `Bearer ${token}`,
					},
					body: JSON.stringify({
						protocol: suggestion.to.protocol,
						asset: suggestion.to.asset,
						amount: suggestion.amount,
						vaultAddress: suggestion.to.vaultAddress,
					}),
				},
			);

			if (!res.ok) {
				const err = await res.json();
				this.yieldError = err.error || "Failed to build deposit transaction";
			} else {
				const data = await res.json();
				// Transaction built — show info to user (would connect to Safe propose flow)
				this.yieldError = "";
				alert(
					`Transaction ready for Safe proposal:\n\n${data.transaction.description}\n\nSteps:\n${data.transaction.steps.map((s: string, i: number) => `${i + 1}. ${s}`).join("\n")}\n\nSubmit this via your Safe multisig to execute.`,
				);
			}
		} catch {
			this.yieldError = "Network error building deposit transaction";
		}

		this.yieldDepositInProgress = false;
		this.render();
	}

	private async loadMyWalletBalances() {
		const addresses: Array<{ address: string; type: "eoa" | "safe" }> = [];

		if (this.passKeyEOA) {
			addresses.push({ address: this.passKeyEOA, type: "eoa" });
		}
		for (const w of this.linkedWallets) {
			addresses.push({ address: w.address, type: w.type });
		}

		if (addresses.length === 0) return;

		this.myWalletsLoading = true;
		this.render();

		const base = this.getApiBase();
		const tn = this.includeTestnets ? "" : "?testnets=false";

		await Promise.allSettled(
			addresses.map(async ({ address, type }) => {
				try {
					const endpoint = type === "safe"
						? `${base}/api/safe/${address}/all-balances${tn}`
						: `${base}/api/eoa/${address}/all-balances${tn}`;
					const res = await fetch(endpoint);
					if (!res.ok) return;
					const data = await res.json();
					if (data.chains && data.chains.length > 0) {
						this.myWalletBalances.set(address.toLowerCase(), data.chains);
					}
				} catch {}
			}),
		);

		this.myWalletsLoading = false;
		this.render();
	}

	private renderLocalTokens(): string {
		if (!this.isAuthenticated) return "";
		if (this.crdtLoading) {
			return `<div class="local-tokens-section"><span class="spinner"></span> Loading local tokens...</div>`;
		}
		if (this.crdtBalances.length === 0) return "";

		const rows = this.crdtBalances.map((t) => {
			const formatted = (t.balance / Math.pow(10, t.decimals)).toFixed(2);
			return `<tr>
				<td style="display:flex;align-items:center;gap:6px">
					<span style="font-size:1.2em">${t.icon || '🪙'}</span>
					<strong>${this.esc(t.symbol)}</strong>
				</td>
				<td>${this.esc(t.name)}</td>
				<td style="text-align:right;font-family:monospace;font-weight:600">${formatted}</td>
			</tr>`;
		}).join('');

		return `
		<div class="local-tokens-section">
			<h3 style="margin:0 0 8px;font-size:1rem;color:#e0e0e0">Local Tokens (CRDT)</h3>
			<table style="width:100%;border-collapse:collapse">
				<thead>
					<tr style="color:#999;font-size:0.8rem;text-transform:uppercase">
						<th style="text-align:left;padding:4px 8px">Token</th>
						<th style="text-align:left;padding:4px 8px">Name</th>
						<th style="text-align:right;padding:4px 8px">Balance</th>
					</tr>
				</thead>
				<tbody>${rows}</tbody>
			</table>
		</div>`;
	}

	private loadDemoData() {
		this.isDemo = true;
		this.address = "0x29567BdBcC92aCF37AC6B56B69180857bB69f7D1";
		this.detectedChains = [
			{ chainId: "1", name: "Ethereum", prefix: "eth", color: "#627eea" },
			{ chainId: "10", name: "Optimism", prefix: "oeth", color: "#ff0420" },
			{ chainId: "100", name: "Gnosis", prefix: "gno", color: "#04795b" },
			{ chainId: "137", name: "Polygon", prefix: "pol", color: "#8247e5" },
			{ chainId: "8453", name: "Base", prefix: "base", color: "#0052ff" },
			{ chainId: "42161", name: "Arbitrum", prefix: "arb1", color: "#28a0f0" },
			{ chainId: "43114", name: "Avalanche", prefix: "avax", color: "#e84142" },
		];
		this.selectedChain = "100";
		this.balances = [
			{ tokenAddress: null, token: { name: "xDAI", symbol: "xDAI", decimals: 18 }, balance: "45230000000000000000000", fiatBalance: "45230", fiatConversion: "1" },
			{ tokenAddress: "0x5dF8339c5E282ee48c0c7cE252A7842F74e378b2", token: { name: "Token Engineering Commons", symbol: "TEC", decimals: 18 }, balance: "1250000000000000000000000", fiatBalance: "12500", fiatConversion: "0.01" },
			{ tokenAddress: "0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1", token: { name: "Wrapped Ether", symbol: "WETH", decimals: 18 }, balance: "8500000000000000000", fiatBalance: "28050", fiatConversion: "3300" },
			{ tokenAddress: "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", token: { name: "USD Coin", symbol: "USDC", decimals: 6 }, balance: "15750000000", fiatBalance: "15750", fiatConversion: "1" },
			{ tokenAddress: "0x4f4F9b8D5B4d0Dc10506e5551B0513B61fD59e75", token: { name: "Giveth", symbol: "GIV", decimals: 18 }, balance: "500000000000000000000000", fiatBalance: "2500", fiatConversion: "0.005" },
		];
		// Pre-load demo viz data
		this.vizData = {
			timeline: DEMO_TIMELINE_DATA,
			sankey: DEMO_SANKEY_DATA,
			multichain: DEMO_MULTICHAIN_DATA,
		};
		// Seed sandbox with demo yield rates
		this.yieldRates = DEMO_YIELD_RATES.map((r) => ({
			protocol: r.protocol,
			chainId: r.chainId,
			asset: r.asset,
			assetAddress: r.assetAddress,
			vaultAddress: r.vaultAddress,
			apy: r.apy,
			tvl: r.tvl,
			vaultName: r.vaultName,
		}));
		this.render();
	}

	private getApiBase(): string {
		const path = window.location.pathname;
		const match = path.match(/^(\/[^/]+)?\/rwallet/);
		return match ? match[0] : "";
	}

	private async detectChains() {
		if (this.isDemo) return;
		if (!this.address || !/^0x[a-fA-F0-9]{40}$/.test(this.address)) {
			this.error = "Please enter a valid Ethereum address (0x...)";
			this.render();
			return;
		}

		this.loading = true;
		this.error = "";
		this.detectedChains = [];
		this.balances = [];
		this.allChainBalances = new Map();
		this.chainFilter = null;
		this.walletType = "";
		this.transfers = null;
		this.vizData = {};
		this.render();

		try {
			const base = this.getApiBase();
			const tn = this.includeTestnets ? "" : "?testnets=false";

			// Try Safe all-balances first
			const safeRes = await fetch(`${base}/api/safe/${this.address}/all-balances${tn}`);
			const safeData = await safeRes.json();

			if (safeData.chains && safeData.chains.length > 0) {
				this.walletType = "safe";
				this.populateFromAllBalances(safeData.chains);
			} else {
				// Fall back to EOA all-balances
				const eoaRes = await fetch(`${base}/api/eoa/${this.address}/all-balances${tn}`);
				const eoaData = await eoaRes.json();

				if (eoaData.chains && eoaData.chains.length > 0) {
					this.walletType = "eoa";
					this.populateFromAllBalances(eoaData.chains);
				} else {
					this.error = "No balances found for this address on any supported chain.";
				}
			}
		} catch (e) {
			this.error = "Failed to detect wallet. Check the address and try again.";
		}

		this.loading = false;
		this.addressBarExpanded = false;

		// Fire DeFi positions fetch in background (non-blocking)
		if (this.address && this.hasData()) {
			this.loadDefiPositions();
		}

		// Default to budget view and auto-load transfers when wallet data is available
		if (this.hasData()) {
			const showViz = this.walletType === "safe" || this.isDemo || (this.isAuthenticated && this.crdtBalances.length > 0);
			if (showViz && this.activeView !== "balances") {
				if (this.activeView !== "budget" && this.activeView !== "flows") {
					this.activeView = "budget";
				}
				this.render();
				this.loadTransfers();
				return;
			}
		}

		this.render();
	}

	private populateFromAllBalances(chains: Array<{ chainId: string; chainName: string; balances: BalanceItem[] }>) {
		this.allChainBalances = new Map();
		this.detectedChains = [];

		for (const ch of chains) {
			this.allChainBalances.set(ch.chainId, {
				chainId: ch.chainId,
				chainName: ch.chainName,
				balances: ch.balances,
			});
			this.detectedChains.push({
				chainId: ch.chainId,
				name: ch.chainName,
				prefix: "",
				color: CHAIN_COLORS[ch.chainId] || "#888",
			});
		}

		// Set selectedChain for viz compatibility (first chain)
		if (this.detectedChains.length > 0) {
			this.selectedChain = this.detectedChains[0].chainId;
		}

		// Build flattened balances for backward compat (viz data, etc.)
		this.balances = this.getFilteredBalances();
	}

	/** Get balances respecting current chainFilter */
	private getFilteredBalances(): BalanceItem[] {
		const result: BalanceItem[] = [];
		for (const [chainId, entry] of this.allChainBalances) {
			if (this.chainFilter && this.chainFilter !== chainId) continue;
			result.push(...entry.balances);
		}
		return result;
	}

	/** Get all balances (including CRDT) for the unified table, with chain info attached.
	 *  Pass ignoreFilter=true to get all chains regardless of current filter (for stats). */
	private getUnifiedBalances(ignoreFilter = false): Array<BalanceItem & { chainId: string; chainName: string }> {
		const result: Array<BalanceItem & { chainId: string; chainName: string }> = [];

		for (const [chainId, entry] of this.allChainBalances) {
			if (!ignoreFilter && this.chainFilter && this.chainFilter !== chainId) continue;
			for (const b of entry.balances) {
				result.push({ ...b, chainId, chainName: entry.chainName });
			}
		}

		// Merge CRDT tokens (when showing all or filtering to "local")
		if (this.isAuthenticated && this.crdtBalances.length > 0 && (ignoreFilter || !this.chainFilter || this.chainFilter === "local")) {
			for (const t of this.crdtBalances) {
				result.push({
					tokenAddress: `crdt:${t.tokenId}`,
					token: { name: t.name, symbol: t.symbol, decimals: t.decimals },
					balance: t.balance.toString(),
					fiatBalance: "0",
					fiatConversion: "0",
					chainId: "local",
					chainName: "Local",
				});
			}
		}

		return result;
	}

	private async loadBalances() {
		if (this.isDemo) return;
		if (!this.selectedChain) return;
		try {
			const base = this.getApiBase();
			const apiPath = this.walletType === "safe"
				? `${base}/api/safe/${this.selectedChain}/${this.address}/balances`
				: `${base}/api/eoa/${this.selectedChain}/${this.address}/balances`;
			const res = await fetch(apiPath);
			if (res.ok) {
				this.balances = await res.json();
			}
		} catch {
			this.balances = [];
		}
	}

	// ── Transfer Loading for Visualizations ──

	private async loadTransfers() {
		if (this.isDemo || this.transfers || this.transfersLoading) return;

		this.transfersLoading = true;
		this.render();

		try {
			const base = this.getApiBase();
			const chainDataMap = new Map<string, any>();

			// Fan out requests to all detected chains (Safe wallets only)
			if (this.walletType === "safe") {
				await Promise.allSettled(
					this.detectedChains.map(async (ch) => {
						try {
							const res = await fetch(`${base}/api/safe/${ch.chainId}/${this.address}/transfers?limit=2000`);
							if (!res.ok) return;
							const data = await res.json();
							const incoming: any[] = [];
							const outgoing: any[] = [];
							const results = data.results || [];
							const addr = this.address.toLowerCase();
							for (const tx of results) {
								// Outgoing: Safe-initiated multisig transactions
								if (tx.txType === "MULTISIG_TRANSACTION") {
									outgoing.push(tx);
								}
								// Incoming: external ETH/token transfers to Safe
								if (tx.txType === "ETHEREUM_TRANSACTION") {
									if (tx.from && tx.value && tx.value !== "0") {
										incoming.push({
											type: "ETHER_TRANSFER",
											from: tx.from,
											to: this.address,
											value: tx.value,
											executionDate: tx.executionDate,
											blockTimestamp: tx.executionDate,
										});
									}
								}
								// Embedded transfers (both tx types may have these)
								if (tx.transfers) {
									for (const t of tx.transfers) {
										if (t.to?.toLowerCase() === addr && t.from?.toLowerCase() !== addr) {
											incoming.push(t);
										}
									}
								}
							}
							chainDataMap.set(ch.chainId, { incoming, outgoing, chainId: ch.chainId });
						} catch {}
					}),
				);
			}

			// Fetch CRDT token transfers (available for all wallet types when authenticated)
			if (this.isAuthenticated && this.userDID) {
				try {
					const res = await fetch(`${base}/api/crdt-tokens/transfers?did=${encodeURIComponent(this.userDID)}`);
					if (res.ok) {
						const data = await res.json();
						const entries = data.entries || [];
						if (entries.length > 0) {
							const incoming: any[] = [];
							const outgoing: any[] = [];
							const walletAddr = this.address.toLowerCase();

							// Map DIDs to stable pseudo-addresses for the transform layer
							// The current user's DID → this.address (so transforms recognize it)
							// Other DIDs → deterministic short hex from the DID string
							const didToAddr = (did: string): string => {
								if (did === this.userDID) return walletAddr;
								if (!did || did === 'system') return '0x0000000000000000000000000000000000000000';
								// Deterministic pseudo-address from DID
								let hash = 0;
								for (let i = 0; i < did.length; i++) hash = ((hash << 5) - hash + did.charCodeAt(i)) | 0;
								const hex = Math.abs(hash).toString(16).padStart(8, '0');
								return `0x${hex}${'0'.repeat(32)}`.slice(0, 42);
							};

							// Build a label lookup for DID short names
							const didLabels = new Map<string, string>();
							for (const entry of entries) {
								if (entry.toLabel) didLabels.set(entry.to, entry.toLabel);
							}

							for (const entry of entries) {
								const fromAddr = entry.type === 'mint' ? didToAddr(entry.issuedBy || 'system') : didToAddr(entry.from);
								const toAddr = didToAddr(entry.to);
								const fromLabel = entry.type === 'mint' ? 'Mint' : (didLabels.get(entry.from) || entry.from?.slice(-12) || 'system');
								const toLabel = entry.toLabel || entry.to?.slice(-12) || '';

								const shaped = {
									type: "ERC20_TRANSFER",
									transferType: "ERC20_TRANSFER",
									from: fromAddr,
									to: toAddr,
									value: String(entry.amount),
									tokenInfo: {
										symbol: entry.tokenSymbol,
										name: entry.tokenSymbol,
										decimals: entry.tokenDecimals,
									},
									executionDate: new Date(entry.timestamp).toISOString(),
									blockTimestamp: new Date(entry.timestamp).toISOString(),
									_fromLabel: fromLabel,
									_toLabel: toLabel,
								};

								if (entry.to === this.userDID) {
									incoming.push(shaped);
								}
								if (entry.from === this.userDID) {
									outgoing.push({
										txType: "MULTISIG_TRANSACTION",
										isExecuted: true,
										executionDate: shaped.executionDate,
										value: "0",
										to: toAddr,
										transfers: [{ ...shaped, from: walletAddr }],
										dataDecoded: null,
									});
								}
							}

							chainDataMap.set("local", { incoming, outgoing, chainId: "local" });
						}
					}
				} catch {}
			}

			this.transfers = chainDataMap;
			this.computeVizData();
		} catch {
			this.transfers = new Map();
		}

		this.transfersLoading = false;
		this.render();
		if (this.activeView !== "balances") {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}
	}

	private computeVizData() {
		if (!this.transfers) return;

		this.vizData.timeline = transformToTimelineData(this.transfers, this.address, CHAIN_NAMES);

		// Sankey for selected chain
		if (this.selectedChain && this.transfers.has(this.selectedChain)) {
			this.vizData.sankey = transformToSankeyData(
				this.transfers.get(this.selectedChain),
				this.address,
				this.selectedChain,
			);
		}

		this.vizData.multichain = transformToMultichainData(this.transfers, this.address, CHAIN_NAMES);
	}

	private async drawActiveVisualization() {
		const container = this.shadow.querySelector("#viz-container") as HTMLElement;
		if (!container) return;

		// Lazy-load D3
		if (!this.d3Ready) {
			container.innerHTML = '<div class="loading"><span class="spinner"></span> Loading visualization library...</div>';
			try {
				await loadD3();
				this.d3Ready = true;
			} catch {
				container.innerHTML = '<div class="error">Failed to load D3 visualization library.</div>';
				return;
			}
		}

		// Build chain color map for flow chart
		const chainColorMap: Record<string, string> = {};
		for (const ch of this.detectedChains) {
			chainColorMap[ch.name.toLowerCase()] = ch.color;
		}
		chainColorMap["crdt"] = "#22c55e";

		switch (this.activeView) {
			case "budget":
				if (this.vizData.timeline && this.vizData.timeline.length > 0) {
					renderTimeline(container, this.vizData.timeline, { chainColors: chainColorMap });
				} else {
					container.innerHTML = '<div class="empty">No timeline data available. Transfer data may still be loading.</div>';
				}
				break;

			case "flows":
				this.drawFlowsWithScrubber(container, chainColorMap);
				break;
		}
	}

	private drawFlowsWithScrubber(container: HTMLElement, chainColorMap: Record<string, string>) {
		const timeline = this.vizData.timeline;
		if (!timeline || timeline.length === 0) {
			// Fall back to sankey-only if no timeline but sankey data exists
			if (this.vizData.sankey && this.vizData.sankey.links.length > 0) {
				renderSankey(container, this.vizData.sankey);
			} else {
				container.innerHTML = '<div class="empty">No flow data available. Transfer data may still be loading.</div>';
			}
			return;
		}

		container.innerHTML = "";

		// Sankey container
		const sankeyDiv = document.createElement("div");
		sankeyDiv.id = "flows-sankey";
		container.appendChild(sankeyDiv);

		// Scrubber controls
		const scrubberWrap = document.createElement("div");
		scrubberWrap.className = "flows-scrubber";
		scrubberWrap.innerHTML = `
			<div class="scrubber-row">
				<button class="scrubber-btn" id="flows-play" title="Play/Pause">▶</button>
				<input type="range" id="flows-range" min="-1" max="${timeline.length - 1}" value="${this.flowsScrubberPos}" class="scrubber-range">
				<span class="scrubber-label" id="flows-date-label">${this.flowsScrubberPos < 0 ? "All transactions" : ""}</span>
			</div>
			<div class="scrubber-detail" id="flows-tx-detail"></div>
		`;
		container.appendChild(scrubberWrap);

		const rangeInput = scrubberWrap.querySelector("#flows-range") as HTMLInputElement;
		const dateLabel = scrubberWrap.querySelector("#flows-date-label") as HTMLElement;
		const txDetail = scrubberWrap.querySelector("#flows-tx-detail") as HTMLElement;
		const playBtn = scrubberWrap.querySelector("#flows-play") as HTMLButtonElement;

		const renderAtPosition = (pos: number) => {
			this.flowsScrubberPos = pos;
			const slicedTimeline = pos < 0 ? timeline : timeline.slice(0, pos + 1);

			// Rebuild sankey data from sliced timeline
			const sankeyData = this.buildSankeyFromTimeline(slicedTimeline);
			sankeyDiv.innerHTML = "";

			if (sankeyData && sankeyData.links.length > 0) {
				renderSankey(sankeyDiv, sankeyData);
			} else {
				sankeyDiv.innerHTML = '<div class="empty" style="padding:40px;text-align:center;color:var(--rs-text-muted,#666);">Move the scrubber to include transactions.</div>';
			}

			// Update label
			if (pos < 0) {
				dateLabel.textContent = `All transactions (${timeline.length})`;
				txDetail.innerHTML = "";
			} else {
				const tx = timeline[pos];
				const dateStr = tx.date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
				dateLabel.textContent = `${pos + 1} of ${timeline.length} — ${dateStr}`;

				const color = tx.type === "in" ? "#4ade80" : "#f87171";
				const sign = tx.type === "in" ? "+" : "-";
				const usd = tx.usd >= 1000 ? Math.round(tx.usd).toLocaleString() : tx.usd.toFixed(2);
				const peer = tx.type === "in" ? (tx.from || "Unknown") : (tx.to || "Unknown");
				const peerLabel = tx.type === "in" ? "From" : "To";
				txDetail.innerHTML = `
					<span style="color:${color};font-weight:600;">${sign}$${usd}</span>
					<span style="color:var(--rs-text-secondary,#888);margin:0 8px;">·</span>
					<span>${tx.amount.toLocaleString(undefined, { maximumFractionDigits: 4 })} ${tx.token}</span>
					<span style="color:var(--rs-text-secondary,#888);margin:0 8px;">·</span>
					<span style="color:var(--rs-text-secondary,#888);">${peerLabel}: <span style="font-family:monospace;font-size:0.8em;">${peer.length > 14 ? peer.slice(0, 6) + "..." + peer.slice(-4) : peer}</span></span>
				`;
			}
		};

		// Initial render
		renderAtPosition(this.flowsScrubberPos);

		// Scrubber input handler
		rangeInput.addEventListener("input", () => {
			const pos = parseInt(rangeInput.value, 10);
			renderAtPosition(pos);
		});

		// Play/pause handler
		playBtn.addEventListener("click", () => {
			if (this.flowsPlayInterval) {
				clearInterval(this.flowsPlayInterval);
				this.flowsPlayInterval = null;
				playBtn.textContent = "▶";
				return;
			}

			playBtn.textContent = "⏸";
			// Start from beginning if at end or showing all
			if (this.flowsScrubberPos >= timeline.length - 1 || this.flowsScrubberPos < 0) {
				this.flowsScrubberPos = -1;
			}

			this.flowsPlayInterval = setInterval(() => {
				const next = this.flowsScrubberPos + 1;
				if (next >= timeline.length) {
					clearInterval(this.flowsPlayInterval!);
					this.flowsPlayInterval = null;
					playBtn.textContent = "▶";
					return;
				}
				rangeInput.value = String(next);
				renderAtPosition(next);
			}, 600);
		});
	}

	private buildSankeyFromTimeline(entries: TimelineEntry[]): SankeyData | null {
		if (entries.length === 0) return null;

		const nodeMap = new Map<string, number>();
		const links: { source: number; target: number; value: number; token: string }[] = [];

		const getIdx = (name: string) => {
			if (!nodeMap.has(name)) nodeMap.set(name, nodeMap.size);
			return nodeMap.get(name)!;
		};

		const walletLabel = "Wallet";
		getIdx(walletLabel);

		for (const tx of entries) {
			if (tx.type === "in") {
				const fromLabel = tx.from ? (tx.from.length > 14 ? tx.from.slice(0, 6) + "..." + tx.from.slice(-4) : tx.from) : "Unknown";
				const src = getIdx(fromLabel);
				const tgt = getIdx(walletLabel);
				links.push({ source: src, target: tgt, value: Math.max(tx.usd, 0.01), token: tx.token });
			} else {
				const toLabel = tx.to ? (tx.to.length > 14 ? tx.to.slice(0, 6) + "..." + tx.to.slice(-4) : tx.to) : "Unknown";
				const src = getIdx(walletLabel);
				const tgt = getIdx(toLabel);
				links.push({ source: src, target: tgt, value: Math.max(tx.usd, 0.01), token: tx.token });
			}
		}

		// Aggregate duplicate links (same source→target)
		const linkKey = (l: typeof links[0]) => `${l.source}->${l.target}`;
		const aggregated = new Map<string, typeof links[0]>();
		for (const l of links) {
			const k = linkKey(l);
			if (aggregated.has(k)) {
				const existing = aggregated.get(k)!;
				existing.value += l.value;
			} else {
				aggregated.set(k, { ...l });
			}
		}

		const nodes = Array.from(nodeMap.entries()).map(([name, _idx]) => ({
			name,
			type: name === walletLabel ? "wallet" as const : (entries.some(tx => tx.type === "in" && ((tx.from || "Unknown").startsWith(name.slice(0, 6)))) ? "source" as const : "target" as const),
			address: name,
		}));

		return { nodes, links: Array.from(aggregated.values()) };
	}

	private renderTransactionTables(): string {
		const mc = this.vizData.multichain;
		if (!mc || (!mc.allTransfers.incoming.length && !mc.allTransfers.outgoing.length)) return "";

		const renderTable = (transfers: TransferRecord[], direction: "in" | "out") => {
			if (!transfers.length) return "<p style='color:#666;padding:12px;'>None</p>";
			const rows = transfers.slice(0, 500).map((tx) => {
				const date = tx.date ? new Date(tx.date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "2-digit" }) : "—";
				const addr = direction === "in" ? (tx.from || "—") : (tx.to || "—");
				const addrShort = direction === "in" ? (tx.fromShort || "—") : (tx.toShort || "—");
				const usd = tx.usd > 0 ? `$${tx.usd >= 1000 ? Math.round(tx.usd).toLocaleString() : tx.usd.toFixed(2)}` : "—";
				const amount = tx.amount >= 1000 ? Math.round(tx.amount).toLocaleString() : tx.amount.toFixed(tx.amount < 1 ? 4 : 2);
				const explorer = addr !== "—" ? explorerLink(addr, tx.chainId) : "#";
				return `<tr>
					<td>${date}</td>
					<td><span class="chain-dot-sm" style="background:var(--rs-accent,#14b8a6)"></span>${tx.chainName || tx.chainId}</td>
					<td><a href="${explorer}" target="_blank" rel="noopener" title="${addr}" style="color:${COLORS_TX.cyan};text-decoration:none;font-family:monospace;font-size:0.8rem;">${addrShort}</a></td>
					<td>${tx.token}</td>
					<td style="text-align:right">${amount}</td>
					<td style="text-align:right">${usd}</td>
				</tr>`;
			}).join("");
			return `<div class="tx-table-wrap"><table class="tx-table">
				<thead><tr><th>Date</th><th>Chain</th><th>${direction === "in" ? "From" : "To"}</th><th>Token</th><th style="text-align:right">Amount</th><th style="text-align:right">USD</th></tr></thead>
				<tbody>${rows}</tbody>
			</table></div>`;
		};

		return `
		<div class="tx-tables">
			<details class="tx-section" open>
				<summary>Incoming (${mc.allTransfers.incoming.length})</summary>
				${renderTable(mc.allTransfers.incoming, "in")}
			</details>
			<details class="tx-section">
				<summary>Outgoing (${mc.allTransfers.outgoing.length})</summary>
				${renderTable(mc.allTransfers.outgoing, "out")}
			</details>
		</div>`;
	}

	private formatBalance(balance: string, decimals: number): string {
		const val = Number(balance) / Math.pow(10, decimals);
		if (val >= 1000000) return `${(val / 1000000).toFixed(2)}M`;
		if (val >= 1000) return `${(val / 1000).toFixed(2)}K`;
		if (val >= 1) return val.toFixed(2);
		if (val >= 0.0001) return val.toFixed(4);
		return val.toExponential(2);
	}

	private formatUSD(val: string): string {
		const n = parseFloat(val);
		if (isNaN(n) || n === 0) return "$0";
		if (n >= 1000000) return `$${(n / 1000000).toFixed(2)}M`;
		if (n >= 1000) return `$${(n / 1000).toFixed(1)}K`;
		return `$${n.toFixed(2)}`;
	}

	private shortenAddress(addr: string): string {
		return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
	}

	private handleSubmit(e: Event) {
		e.preventDefault();
		const input = this.shadow.querySelector("#address-input") as HTMLInputElement;
		if (input) {
			this.address = input.value.trim();
			this.addressBarExpanded = false;
			const url = new URL(window.location.href);
			url.searchParams.set("address", this.address);
			window.history.replaceState({}, "", url.toString());
			this.detectChains();
		}
	}

	private handleChainSelect(chainId: string) {
		if (this.isDemo) {
			this.selectedChain = chainId;
			this.render();
			return;
		}

		// Toggle filter: click same chain again to show all
		if (this.chainFilter === chainId) {
			this.chainFilter = null;
		} else {
			this.chainFilter = chainId;
		}

		// Update selectedChain for viz compatibility
		this.selectedChain = chainId;
		this.balances = this.getFilteredBalances();

		// Recompute sankey for selected chain
		if (this.transfers && this.transfers.has(chainId)) {
			this.vizData.sankey = transformToSankeyData(
				this.transfers.get(chainId),
				this.address,
				chainId,
			);
		}

		this.render();
		if (this.activeView !== "balances") {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}
	}

	private handleViewTabClick(view: ViewTab) {
		if (this.activeView === view) return;
		this._history.push(view);
		this.activeView = view;

		if (view !== "balances" && !this.transfers && !this.isDemo) {
			this.loadTransfers();
		}

		this.render();

		if (view !== "balances") {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}
	}

	private hasData(): boolean {
		return this.detectedChains.length > 0 || this.crdtBalances.length > 0;
	}

	// ── EIP-6963 Provider Discovery ──

	private sanitizeIconUri(uri: string): string {
		if (!uri) return "";
		// Allow https: URLs and safe data: image types (no SVG — can contain scripts)
		if (/^https:\/\//i.test(uri)) return uri;
		if (/^data:image\/(png|jpeg|gif|webp);base64,/i.test(uri)) return uri;
		return ""; // Block javascript:, data:image/svg+xml, etc.
	}

	private startProviderDiscovery() {
		this.discoveredProviders = [];
		this.showProviderPicker = true;
		this.linkError = "";
		this.render();

		const handler = (e: Event) => {
			const detail = (e as CustomEvent).detail;
			if (!detail?.info?.uuid || !detail?.provider) return;
			const exists = this.discoveredProviders.some(p => p.uuid === detail.info.uuid);
			if (!exists) {
				this.discoveredProviders.push({
					uuid: detail.info.uuid,
					name: detail.info.name,
					icon: this.sanitizeIconUri(detail.info.icon || ""),
					rdns: detail.info.rdns,
				});
				this.render();
			}
		};

		window.addEventListener("eip6963:announceProvider", handler);
		window.dispatchEvent(new Event("eip6963:requestProvider"));

		// Store handler for cleanup
		(this as any)._eip6963Handler = handler;

		// If no providers found after 2s, show message
		setTimeout(() => {
			if (this.discoveredProviders.length === 0 && this.showProviderPicker) {
				this.linkError = "No browser wallets detected. Install MetaMask or another EIP-6963 compatible wallet.";
				this.render();
			}
		}, 2000);
	}

	private stopProviderDiscovery() {
		const handler = (this as any)._eip6963Handler;
		if (handler) {
			window.removeEventListener("eip6963:announceProvider", handler);
			delete (this as any)._eip6963Handler;
		}
		this.showProviderPicker = false;
		this.discoveredProviders = [];
		this.linkError = "";
	}

	private async handleProviderSelect(uuid: string) {
		const provider = this.discoveredProviders.find(p => p.uuid === uuid);
		if (!provider) return;

		this.linkingInProgress = true;
		this.linkError = "";
		this.render();

		try {
			// Get the actual EIP-1193 provider reference
			let eip1193Provider: any = null;
			const getProvider = new Promise<any>((resolve) => {
				const handler = (e: Event) => {
					const detail = (e as CustomEvent).detail;
					if (detail?.info?.uuid === uuid) {
						window.removeEventListener("eip6963:announceProvider", handler);
						resolve(detail.provider);
					}
				};
				window.addEventListener("eip6963:announceProvider", handler);
				window.dispatchEvent(new Event("eip6963:requestProvider"));
				setTimeout(() => resolve(null), 3000);
			});
			eip1193Provider = await getProvider;
			if (!eip1193Provider) throw new Error("Could not get wallet provider");

			// 1. Request accounts
			const accounts = await eip1193Provider.request({ method: "eth_requestAccounts" });
			if (!accounts || accounts.length === 0) throw new Error("No accounts returned");
			const walletAddress = accounts[0] as string;

			// 2. Get nonce from server
			const token = this.getAuthToken();
			if (!token) throw new Error("Not authenticated");

			const nonceRes = await fetch("/encryptid/api/wallet-link/nonce", {
				method: "POST",
				headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json" },
			});
			if (!nonceRes.ok) throw new Error("Failed to get nonce");
			const { nonce } = await nonceRes.json();

			// 3. Build SIWE message
			const domain = window.location.host;
			const origin = window.location.origin;
			const issuedAt = new Date().toISOString();
			const siweMessage = [
				`${domain} wants you to sign in with your Ethereum account:`,
				walletAddress,
				"",
				"Link this wallet to your EncryptID identity",
				"",
				`URI: ${origin}`,
				`Version: 1`,
				`Chain ID: 1`,
				`Nonce: ${nonce}`,
				`Issued At: ${issuedAt}`,
			].join("\n");

			// 4. Sign with external wallet
			const signature = await eip1193Provider.request({
				method: "personal_sign",
				params: [siweMessage, walletAddress],
			});

			// 5. Hash the address for dedup (salted with user ID from session)
			const addressHash = await this.hashAddress(walletAddress);

			// 6. Build entry data — server encrypts at rest with AES-256-GCM
			const entry = {
				address: walletAddress,
				type: "eoa",
				label: provider.name,
				providerRdns: provider.rdns,
				providerName: provider.name,
			};

			// 7. Verify with server (server handles encryption at rest)
			const verifyRes = await fetch("/encryptid/api/wallet-link/verify", {
				method: "POST",
				headers: {
					"Authorization": `Bearer ${token}`,
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					message: siweMessage,
					signature,
					addressHash,
					walletType: "eoa",
					entry,
				}),
			});

			if (!verifyRes.ok) {
				const err = await verifyRes.json();
				throw new Error(err.error || "Verification failed");
			}

			const result = await verifyRes.json();

			// 8. Add to local state
			const newWallet: LinkedWallet = {
				id: result.id,
				address: walletAddress,
				type: "eoa",
				label: provider.name,
				providerName: provider.name,
				providerRdns: provider.rdns,
			};
			this.linkedWallets.push(newWallet);

			this.stopProviderDiscovery();
		} catch (err: any) {
			this.linkError = err?.message || "Failed to link wallet";
		}

		this.linkingInProgress = false;
		this.render();
	}

	private async handleUnlinkWallet(id: string) {
		const token = this.getAuthToken();
		if (!token) return;

		try {
			const res = await fetch(`/encryptid/api/wallet-link/${id}`, {
				method: "DELETE",
				headers: { "Authorization": `Bearer ${token}` },
			});
			if (res.ok) {
				this.linkedWallets = this.linkedWallets.filter(w => w.id !== id);
				this.render();
			}
		} catch {}
	}

	private async handleViewLinkedWallet(address: string) {
		this.address = address;
		const input = this.shadow.querySelector("#address-input") as HTMLInputElement;
		if (input) input.value = address;
		const url = new URL(window.location.href);
		url.searchParams.set("address", address);
		window.history.replaceState({}, "", url.toString());
		await this.detectChains();
	}

	private async hashAddress(address: string): Promise<string> {
		// Salt with user ID to prevent cross-user address correlation
		const session = localStorage.getItem("encryptid_session");
		const userId = session ? (JSON.parse(session).claims?.sub || "") : "";
		const normalized = userId + ":" + address.toLowerCase();
		const encoded = new TextEncoder().encode(normalized);
		const hash = await crypto.subtle.digest("SHA-256", encoded);
		const bytes = new Uint8Array(hash);
		return btoa(String.fromCharCode(...bytes))
			.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
	}

	// ── Render Methods ──

	private renderStyles(): string {
		return `
		<style>
			:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); -webkit-tap-highlight-color: transparent; }
			* { box-sizing: border-box; }
			button, a, input, select, textarea, [role="button"] { touch-action: manipulation; }

			/* ── Hero ── */
			.hero {
				text-align: center; padding: 32px 16px 24px;
				border-bottom: 1px solid var(--rs-border-subtle); margin-bottom: 24px;
			}
			.hero-title {
				font-size: 28px; font-weight: 700; margin: 0 0 6px;
				background: linear-gradient(135deg, var(--rs-accent), #4ade80);
				-webkit-background-clip: text; -webkit-text-fill-color: transparent;
				background-clip: text;
			}
			.hero-subtitle { font-size: 14px; color: var(--rs-text-secondary); margin: 0 0 20px; }

			/* ── Address bar ── */
			.address-bar { display: flex; gap: 8px; margin-bottom: 12px; max-width: 640px; margin-left: auto; margin-right: auto; }
			.address-bar input {
				flex: 1; padding: 12px 16px; border-radius: 10px;
				border: 1px solid var(--rs-input-border); background: var(--rs-bg-surface); color: var(--rs-text-primary);
				font-family: monospace; font-size: 14px;
			}
			.address-bar input:focus { border-color: var(--rs-accent); outline: none; box-shadow: 0 0 0 2px rgba(20,184,166,0.15); }
			.address-bar button {
				padding: 12px 24px; border-radius: 10px; border: none;
				background: linear-gradient(135deg, var(--rs-accent), var(--rs-accent-hover)); color: #000;
				font-weight: 600; cursor: pointer; font-size: 14px; transition: opacity 0.2s;
			}
			.address-bar button:hover { opacity: 0.9; }

			/* ── Controls row ── */
			.controls-row {
				display: flex; align-items: center; justify-content: space-between;
				max-width: 640px; margin: 0 auto 24px; flex-wrap: wrap; gap: 8px;
			}
			.testnet-toggle {
				display: flex; align-items: center; gap: 8px;
				cursor: pointer; user-select: none; font-size: 13px; color: var(--rs-text-secondary);
			}
			.testnet-toggle:hover { color: var(--rs-text-primary); }
			.toggle-track {
				width: 34px; height: 18px; border-radius: 9px; background: var(--rs-border);
				position: relative; transition: background 0.2s;
			}
			.testnet-toggle.active .toggle-track { background: #f59e0b; }
			.toggle-thumb {
				width: 14px; height: 14px; border-radius: 50%; background: var(--rs-text-muted);
				position: absolute; top: 2px; left: 2px; transition: all 0.2s;
			}
			.testnet-toggle.active .toggle-thumb { left: 18px; background: #fff; }
			.wallet-badge {
				display: inline-flex; align-items: center; gap: 6px;
				padding: 4px 12px; border-radius: 6px; font-size: 12px; font-weight: 600;
				text-transform: uppercase; letter-spacing: 0.5px;
			}
			.wallet-badge.safe { background: rgba(74,222,128,0.12); color: #4ade80; border: 1px solid rgba(74,222,128,0.25); }
			.wallet-badge.eoa { background: rgba(20,184,166,0.12); color: var(--rs-accent); border: 1px solid rgba(20,184,166,0.25); }
			.wallet-badge.encryptid { background: rgba(168,85,247,0.12); color: #a855f7; border: 1px solid rgba(168,85,247,0.25); }

			/* ── My Wallets section ── */
			.my-wallets {
				max-width: 640px; margin: 0 auto 24px;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 12px;
				padding: 16px;
			}
			.my-wallets-header {
				display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
			}
			.my-wallets-title { font-size: 14px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--rs-text-secondary); }
			.link-wallet-btn {
				padding: 6px 14px; border-radius: 8px; border: 1px dashed var(--rs-border);
				background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 12px;
				transition: all 0.2s;
			}
			.link-wallet-btn:hover { border-color: var(--rs-accent); color: var(--rs-accent); background: rgba(20,184,166,0.05); }
			.wallet-list { display: flex; flex-direction: column; gap: 6px; }
			.wallet-item {
				display: flex; align-items: center; gap: 10px; padding: 10px 12px;
				border-radius: 8px; background: var(--rs-bg-hover); cursor: pointer;
				transition: all 0.15s;
			}
			.wallet-item:hover { background: var(--rs-bg-surface); box-shadow: 0 0 0 1px var(--rs-border); }
			.wallet-item-info { flex: 1; min-width: 0; }
			.wallet-item-label { font-size: 13px; font-weight: 500; color: var(--rs-text-primary); }
			.wallet-item-addr { font-size: 11px; color: var(--rs-text-muted); font-family: monospace; }
			.wallet-item-actions { display: flex; gap: 4px; }
			.wallet-item-actions button {
				padding: 4px 8px; border-radius: 4px; border: none; cursor: pointer; font-size: 11px;
				background: transparent; color: var(--rs-text-muted); transition: all 0.15s;
			}
			.wallet-item-actions button:hover { color: var(--rs-text-primary); background: var(--rs-bg-surface); }
			.wallet-item-actions .unlink-btn:hover { color: var(--rs-error); }

			/* ── Provider picker ── */
			.provider-picker {
				margin-top: 12px; padding: 12px; border-radius: 8px;
				background: var(--rs-bg-hover); border: 1px solid var(--rs-border-subtle);
			}
			.provider-picker-title { font-size: 12px; color: var(--rs-text-secondary); margin-bottom: 8px; }
			.provider-list { display: flex; flex-wrap: wrap; gap: 8px; }
			.provider-item {
				display: flex; align-items: center; gap: 8px; padding: 8px 14px;
				border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface);
				cursor: pointer; transition: all 0.2s; font-size: 13px; color: var(--rs-text-primary);
			}
			.provider-item:hover { border-color: var(--rs-accent); background: rgba(20,184,166,0.05); }
			.provider-item img { width: 20px; height: 20px; border-radius: 4px; }
			.provider-cancel {
				margin-top: 8px; padding: 4px 10px; border-radius: 6px; border: none;
				background: transparent; color: var(--rs-text-muted); cursor: pointer; font-size: 12px;
			}
			.provider-cancel:hover { color: var(--rs-text-primary); }

			/* ── Supported chains ── */
			.supported-chains {
				display: flex; flex-wrap: wrap; gap: 8px; justify-content: center;
				margin-bottom: 28px; max-width: 640px; margin-left: auto; margin-right: auto;
			}
			.supported-chain {
				display: flex; align-items: center; gap: 5px;
				padding: 4px 10px; border-radius: 6px; background: var(--rs-bg-surface);
				border: 1px solid var(--rs-border-subtle); font-size: 12px; color: var(--rs-text-secondary);
			}
			.supported-chain .dot { width: 6px; height: 6px; border-radius: 50%; }

			/* ── Feature cards ── */
			.features {
				display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
				gap: 16px; margin-bottom: 28px;
			}
			.feature-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 12px;
				padding: 20px; text-align: center; transition: border-color 0.2s;
			}
			.feature-card:hover { border-color: var(--rs-border-strong); }
			.feature-icon { font-size: 28px; margin-bottom: 10px; }
			.feature-card h3 { font-size: 14px; font-weight: 600; margin: 0 0 6px; color: var(--rs-text-primary); }
			.feature-card p { font-size: 12px; color: var(--rs-text-secondary); margin: 0; line-height: 1.5; }

			/* ── Example wallets ── */
			.examples {
				max-width: 640px; margin: 0 auto 20px;
			}
			.examples-label { font-size: 11px; color: var(--rs-text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 10px; }
			.example-list { display: flex; flex-direction: column; gap: 6px; }
			.example-item {
				display: flex; align-items: center; gap: 10px; padding: 10px 14px;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle); border-radius: 10px;
				cursor: pointer; transition: all 0.2s;
			}
			.example-item:hover { border-color: var(--rs-accent); background: var(--rs-bg-hover); }
			.example-name { font-size: 13px; font-weight: 500; color: var(--rs-text-primary); }
			.example-addr { font-size: 11px; color: var(--rs-text-muted); font-family: monospace; }
			.example-type {
				margin-left: auto; font-size: 10px; font-weight: 600;
				padding: 2px 8px; border-radius: 4px; text-transform: uppercase;
			}
			.example-type.safe { background: rgba(74,222,128,0.1); color: #4ade80; }
			.example-type.eoa { background: rgba(20,184,166,0.1); color: var(--rs-accent); }

			/* ── Dashboard: chains ── */
			.chains { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
			.chain-bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; margin-bottom: 16px; }
			.chain-bar-seg { transition: width 0.3s ease; min-width: 3px; }
			.chain-btn {
				padding: 6px 14px; border-radius: 8px; border: 2px solid var(--rs-border);
				background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 13px;
				display: flex; align-items: center; gap: 6px; transition: all 0.2s;
			}
			.chain-btn:hover { border-color: var(--rs-border-strong); }
			.chain-btn.active { border-color: var(--chain-color); background: var(--rs-bg-hover); }
			.chain-dot { width: 8px; height: 8px; border-radius: 50%; }

			/* ── Dashboard: stats ── */
			.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; }
			.stat-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px;
				padding: 14px; text-align: center;
			}
			.stat-label { font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase; margin-bottom: 6px; }
			.stat-value { font-size: 20px; font-weight: 700; color: var(--rs-accent); }

			/* ── View Tabs ── */
			.view-tabs {
				display: flex; gap: 4px; margin-bottom: 20px;
				border-bottom: 2px solid var(--rs-border-subtle); padding-bottom: 0;
			}
			.view-tab {
				padding: 10px 18px; border: none; background: transparent;
				color: var(--rs-text-secondary); cursor: pointer; font-size: 13px; font-weight: 500;
				border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.2s;
			}
			.view-tab:hover { color: var(--rs-text-primary); background: var(--rs-bg-hover); }
			.view-tab.active {
				color: var(--rs-accent); border-bottom-color: var(--rs-accent);
			}

			/* ── Viz container ── */
			.viz-container { min-height: 200px; }

			/* ── Flows scrubber ── */
			.flows-scrubber {
				margin-top: 16px; padding: 14px 18px;
				background: var(--rs-bg-surface, rgba(255,255,255,0.03));
				border: 1px solid var(--rs-border-subtle, rgba(255,255,255,0.1));
				border-radius: 10px;
			}
			.scrubber-row {
				display: flex; align-items: center; gap: 12px;
			}
			.scrubber-btn {
				width: 36px; height: 36px; border-radius: 50%;
				border: 1px solid var(--rs-border, rgba(255,255,255,0.15));
				background: var(--rs-bg-hover, rgba(255,255,255,0.06));
				color: var(--rs-text-primary, #e0e0e0);
				font-size: 14px; cursor: pointer; display: flex;
				align-items: center; justify-content: center;
				transition: background 0.2s;
			}
			.scrubber-btn:hover { background: var(--rs-bg-active, rgba(255,255,255,0.12)); }
			.scrubber-range {
				flex: 1; height: 6px; -webkit-appearance: none; appearance: none;
				background: var(--rs-border-subtle, rgba(255,255,255,0.1));
				border-radius: 3px; outline: none; cursor: pointer;
			}
			.scrubber-range::-webkit-slider-thumb {
				-webkit-appearance: none; width: 18px; height: 18px;
				border-radius: 50%; background: var(--rs-accent, #14b8a6);
				border: 2px solid var(--rs-bg-surface, #1a1a2e);
				cursor: grab;
			}
			.scrubber-range::-moz-range-thumb {
				width: 18px; height: 18px; border-radius: 50%;
				background: var(--rs-accent, #14b8a6);
				border: 2px solid var(--rs-bg-surface, #1a1a2e);
				cursor: grab;
			}
			.scrubber-label {
				font-size: 0.82rem; color: var(--rs-text-secondary, #888);
				min-width: 180px; text-align: right; white-space: nowrap;
			}
			.scrubber-detail {
				margin-top: 8px; font-size: 0.85rem; min-height: 1.5em;
				color: var(--rs-text-primary, #e0e0e0); display: flex;
				align-items: center; flex-wrap: wrap;
			}

			/* ── Transaction tables ── */
			.tx-tables { margin-top: 20px; }
			.tx-section { background: var(--rs-bg-surface, rgba(255,255,255,0.03)); border: 1px solid var(--rs-border-subtle, rgba(255,255,255,0.1)); border-radius: 10px; margin-bottom: 12px; }
			.tx-section summary { padding: 12px 16px; cursor: pointer; font-weight: 600; font-size: 0.9rem; color: var(--rs-text-primary, #e0e0e0); user-select: none; }
			.tx-section summary:hover { color: #00d4ff; }
			.tx-table-wrap { overflow-x: auto; max-height: 400px; overflow-y: auto; }
			.tx-table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
			.tx-table th { position: sticky; top: 0; background: var(--rs-bg-surface, #1a1a2e); padding: 8px 10px; text-align: left; color: #888; font-size: 0.7rem; text-transform: uppercase; border-bottom: 1px solid rgba(255,255,255,0.1); }
			.tx-table td { padding: 6px 10px; border-bottom: 1px solid rgba(255,255,255,0.05); color: #ccc; white-space: nowrap; }
			.tx-table tr:hover td { background: rgba(255,255,255,0.03); }
			.chain-dot-sm { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: middle; }

			/* ── Dashboard: balance table ── */
			.balance-table { width: 100%; border-collapse: collapse; }
			.balance-table th {
				text-align: left; padding: 10px 8px; border-bottom: 2px solid var(--rs-border);
				color: var(--rs-text-secondary); font-size: 11px; text-transform: uppercase;
			}
			.balance-table td { padding: 10px 8px; border-bottom: 1px solid var(--rs-border-subtle); }
			.balance-table tr:hover td { background: var(--rs-bg-hover); }
			.token-symbol { font-weight: 600; color: var(--rs-text-primary); }
			.token-name { font-size: 12px; color: var(--rs-text-secondary); }
			.amount-cell { text-align: right; font-family: monospace; }
			.fiat { color: var(--rs-success); }

			/* ── Chain cell in balance table ── */
			.chain-cell {
				display: flex; align-items: center; gap: 6px;
				font-size: 12px; color: var(--rs-text-secondary); white-space: nowrap;
			}
			.chain-dot-sm { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }

			/* ── States ── */
			.empty { text-align: center; color: var(--rs-text-muted); padding: 40px; }
			.loading { text-align: center; color: var(--rs-text-secondary); padding: 40px; }
			.loading .spinner {
				display: inline-block; width: 20px; height: 20px;
				border: 2px solid var(--rs-border); border-top-color: var(--rs-accent); border-radius: 50%;
				animation: spin 0.8s linear infinite; margin-right: 8px; vertical-align: middle;
			}
			@keyframes spin { to { transform: rotate(360deg); } }
			.error {
				text-align: center; color: var(--rs-error); padding: 16px;
				background: rgba(239,83,80,0.08); border: 1px solid rgba(239,83,80,0.2);
				border-radius: 10px; margin-bottom: 16px;
			}
			.local-tokens-section {
				background: var(--rs-surface, #1a1a2e);
				border: 1px solid #2775ca44;
				border-left: 3px solid #2775ca;
				border-radius: 10px;
				padding: 16px 20px;
				margin-bottom: 16px;
			}
			.local-tokens-section table td {
				padding: 6px 8px;
				border-bottom: 1px solid rgba(255,255,255,0.06);
				color: #e0e0e0;
			}
			.local-tokens-section table tr:last-child td { border-bottom: none; }

			/* ── DeFi Positions ── */
			.defi-section {
				margin-top: 20px;
				padding: 0 4px;
			}
			.defi-header {
				display: flex;
				justify-content: space-between;
				align-items: center;
				margin-bottom: 12px;
			}
			.defi-header h3 {
				margin: 0;
				font-size: 1rem;
				color: #e0e0e0;
			}
			.defi-total {
				font-size: 1.1rem;
				font-weight: 700;
				color: #4ade80;
				font-family: monospace;
			}
			.defi-protocol-card {
				background: rgba(255,255,255,0.04);
				border: 1px solid rgba(255,255,255,0.08);
				border-radius: 10px;
				padding: 12px;
				margin-bottom: 10px;
			}
			.defi-protocol-header {
				display: flex;
				justify-content: space-between;
				align-items: center;
				margin-bottom: 8px;
				font-size: 0.9rem;
			}
			.defi-proto-total {
				font-family: monospace;
				font-weight: 600;
				color: #ccc;
			}
			.defi-position-row {
				display: flex;
				align-items: center;
				gap: 8px;
				padding: 6px 0;
				border-top: 1px solid rgba(255,255,255,0.05);
				font-size: 0.82rem;
				flex-wrap: wrap;
			}
			.defi-type-badge {
				font-size: 0.7rem;
				padding: 2px 8px;
				border-radius: 4px;
				font-weight: 600;
				text-transform: uppercase;
				white-space: nowrap;
			}
			.defi-chain {
				color: #999;
				font-size: 0.78rem;
			}
			.defi-tokens {
				flex: 1;
				display: flex;
				gap: 6px;
				flex-wrap: wrap;
			}
			.defi-token {
				font-family: monospace;
				font-size: 0.78rem;
				color: #bbb;
			}
			.defi-value {
				font-family: monospace;
				font-weight: 600;
				margin-left: auto;
				white-space: nowrap;
			}

			/* ── Payment Actions (Buy/Swap/Withdraw) ── */
			.payment-actions {
				display: grid;
				grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
				gap: 16px;
				margin-top: 20px;
			}
			.action-card {
				background: var(--rs-surface, #1a1a2e);
				border: 1px solid rgba(255,255,255,0.08);
				border-radius: 12px;
				padding: 20px;
				transition: border-color 0.2s;
			}
			.action-card:hover { border-color: rgba(255,255,255,0.15); }
			.action-header {
				display: flex;
				align-items: center;
				gap: 8px;
				margin-bottom: 8px;
			}
			.action-header h4 {
				margin: 0;
				font-size: 1rem;
				color: #e0e0e0;
				flex: 1;
			}
			.action-icon { font-size: 1.3em; }
			.action-badge {
				font-size: 0.7rem;
				padding: 2px 8px;
				border-radius: 20px;
				background: rgba(39,117,202,0.15);
				color: #6bb5ff;
				border: 1px solid rgba(39,117,202,0.3);
			}
			.action-badge.curve {
				background: rgba(34,197,94,0.12);
				color: #4ade80;
				border-color: rgba(34,197,94,0.3);
			}
			.action-desc {
				font-size: 0.82rem;
				color: #999;
				margin: 0 0 12px;
				line-height: 1.4;
			}
			.provider-hints {
				display: flex;
				flex-wrap: wrap;
				gap: 6px;
				margin-bottom: 12px;
			}
			.provider-tag {
				font-size: 0.7rem;
				padding: 2px 8px;
				border-radius: 12px;
				background: rgba(255,255,255,0.04);
				color: #aaa;
				border: 1px solid rgba(255,255,255,0.08);
			}
			.provider-tag.eu { color: #6bb5ff; border-color: rgba(39,117,202,0.25); }
			.provider-tag.us { color: #a78bfa; border-color: rgba(167,139,250,0.25); }
			.provider-tag.small { color: #fbbf24; border-color: rgba(251,191,36,0.25); }
			.swap-balances {
				display: flex;
				gap: 16px;
				font-size: 0.82rem;
				color: #bbb;
				margin-bottom: 12px;
			}
			.swap-balances strong { color: #e0e0e0; }
			.swap-tabs {
				display: flex;
				gap: 4px;
				margin-bottom: 12px;
				background: rgba(255,255,255,0.04);
				border-radius: 8px;
				padding: 3px;
			}
			.swap-tab {
				flex: 1;
				padding: 6px;
				font-size: 0.82rem;
				border: none;
				border-radius: 6px;
				background: transparent;
				color: #999;
				cursor: pointer;
				transition: all 0.2s;
			}
			.swap-tab.active {
				background: rgba(34,197,94,0.15);
				color: #4ade80;
			}
			.swap-quote {
				font-size: 0.82rem;
				color: #bbb;
				padding: 8px;
				margin-bottom: 8px;
				background: rgba(255,255,255,0.02);
				border-radius: 6px;
				min-height: 20px;
			}
			.action-form {
				margin-top: 12px;
			}
			.form-row {
				margin-bottom: 10px;
			}
			.form-row label {
				display: block;
				font-size: 0.75rem;
				color: #999;
				margin-bottom: 4px;
				text-transform: uppercase;
				letter-spacing: 0.05em;
			}
			.action-input {
				width: 100%;
				padding: 8px 10px;
				font-size: 0.88rem;
				border: 1px solid rgba(255,255,255,0.1);
				border-radius: 6px;
				background: rgba(0,0,0,0.2);
				color: #e0e0e0;
				box-sizing: border-box;
			}
			.action-input:focus {
				outline: none;
				border-color: var(--rs-accent, #14b8a6);
			}
			.action-btn {
				width: 100%;
				padding: 10px;
				font-size: 0.88rem;
				font-weight: 600;
				border: none;
				border-radius: 8px;
				cursor: pointer;
				transition: all 0.2s;
			}
			.action-btn.primary {
				background: var(--rs-accent, #14b8a6);
				color: #000;
			}
			.action-btn.primary:hover { opacity: 0.9; }
			.action-btn.outline {
				background: transparent;
				border: 1px solid rgba(255,255,255,0.15);
				color: #ccc;
			}
			.action-btn.outline:hover {
				background: rgba(255,255,255,0.05);
				border-color: rgba(255,255,255,0.25);
			}
			.action-status {
				font-size: 0.82rem;
				margin-top: 8px;
				min-height: 20px;
			}
			.action-status.success { color: #4ade80; }
			.action-status.error { color: #f87171; }
			.action-status.loading { color: #fbbf24; }

			.link-error {
				color: var(--rs-error); font-size: 12px; margin-top: 8px;
				padding: 6px 10px; background: rgba(239,83,80,0.08); border-radius: 6px;
			}

			/* ── Wallet cards (My Wallets tab) ── */
			.my-wallets-grid { display: flex; flex-direction: column; gap: 16px; max-width: 720px; margin: 0 auto; }
			.wallet-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle);
				border-radius: 12px; padding: 16px; transition: border-color 0.2s;
			}
			.wallet-card:hover { border-color: var(--rs-border-strong); }
			.wallet-card-header {
				display: flex; align-items: center; justify-content: space-between;
				margin-bottom: 12px; flex-wrap: wrap; gap: 8px;
			}
			.wallet-card-addr {
				font-family: monospace; font-size: 12px; color: var(--rs-text-muted); margin-left: 8px;
			}
			.wallet-card-actions { display: flex; align-items: center; gap: 8px; }
			.wallet-card-total {
				font-size: 18px; font-weight: 700; color: var(--rs-accent); font-family: monospace;
			}
			.view-flows-btn {
				padding: 5px 12px; border-radius: 6px; border: 1px solid var(--rs-border);
				background: transparent; color: var(--rs-text-secondary); cursor: pointer; font-size: 12px;
				transition: all 0.2s; white-space: nowrap;
			}
			.view-flows-btn:hover { border-color: var(--rs-accent); color: var(--rs-accent); background: rgba(20,184,166,0.05); }
			.unlink-btn-card {
				padding: 4px 8px; border-radius: 4px; border: none; cursor: pointer;
				font-size: 12px; background: transparent; color: var(--rs-text-muted); transition: all 0.15s;
			}
			.unlink-btn-card:hover { color: var(--rs-error); }
			.balance-table.compact th { padding: 6px 8px; font-size: 10px; }
			.balance-table.compact td { padding: 6px 8px; font-size: 12px; }

			/* ── CRDT section in wallet card ── */
			.crdt-section {
				margin-top: 12px; padding: 10px 12px; border-radius: 8px;
				background: rgba(39,117,202,0.06); border: 1px solid rgba(39,117,202,0.15);
			}
			.crdt-label { font-size: 11px; color: #5b9bd5; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
			.crdt-row {
				display: flex; align-items: center; gap: 6px; padding: 4px 0;
				font-size: 13px; color: var(--rs-text-primary);
			}

			/* ── EncryptID identity card ── */
			.encryptid-card { border-color: rgba(168,85,247,0.25); }
			.encryptid-wallets { display: flex; flex-direction: column; gap: 12px; }
			.encryptid-wallet-section {
				padding: 10px 12px; border-radius: 8px;
				background: var(--rs-bg-hover); border: 1px solid var(--rs-border-subtle);
			}
			.encryptid-wallet-label {
				display: flex; align-items: center; gap: 6px;
				font-size: 12px; font-weight: 600; text-transform: uppercase;
				letter-spacing: 0.5px; color: var(--rs-text-secondary); margin-bottom: 8px;
			}
			.wallet-type-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
			.wallet-type-addr {
				font-family: monospace; font-weight: 400; font-size: 11px;
				color: var(--rs-text-muted); margin-left: auto; text-transform: none; letter-spacing: 0;
			}

			/* ── Aggregate stats ── */
			.aggregate-stats {
				display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
				gap: 12px; max-width: 720px; margin: 20px auto 0;
			}

			/* ── My wallets tab link button ── */
			.my-wallets-tab-link-btn {
				display: block; max-width: 720px; margin: 20px auto 0; text-align: center;
			}

			/* ── Yield header ── */
			.yield-header { margin-bottom: 24px; }
			.yield-header-row {
				display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
			}
			.yield-header-title {
				font-size: 20px; font-weight: 700; color: var(--rs-text-primary); margin: 0 0 6px;
			}
			.yield-header-desc {
				font-size: 13px; color: var(--rs-text-secondary); margin: 0; line-height: 1.5; max-width: 560px;
			}
			.yield-back-link {
				padding: 8px 16px; border-radius: 8px; border: 1px solid var(--rs-border);
				color: var(--rs-text-secondary); text-decoration: none; font-size: 13px; font-weight: 500;
				white-space: nowrap; transition: all 0.2s;
			}
			.yield-back-link:hover { border-color: var(--rs-accent); color: var(--rs-accent); }

			/* ── Yield tab ── */
			.yield-summary {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle);
				border-radius: 12px; padding: 16px 20px; margin-bottom: 20px;
			}
			.yield-summary-row {
				display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;
			}
			.yield-stat { display: flex; flex-direction: column; align-items: center; }
			.yield-stat-label { font-size: 11px; color: var(--rs-text-secondary); text-transform: uppercase; margin-bottom: 4px; }
			.yield-stat-value { font-size: 20px; font-weight: 700; color: var(--rs-accent); font-family: monospace; }
			.yield-section { margin-bottom: 24px; }
			.yield-section-title {
				font-size: 14px; font-weight: 600; color: var(--rs-text-primary);
				margin: 0 0 12px; padding-bottom: 6px; border-bottom: 1px solid var(--rs-border-subtle);
			}
			.yield-positions { display: flex; flex-direction: column; gap: 10px; }
			.yield-position-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle);
				border-radius: 10px; padding: 14px;
			}
			.yield-pos-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
			.yield-pos-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
			.yield-pos-label { display: block; font-size: 10px; color: var(--rs-text-muted); text-transform: uppercase; }
			.yield-pos-value { font-size: 14px; font-weight: 600; color: var(--rs-text-primary); font-family: monospace; }
			.yield-protocol-badge {
				font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 4px; text-transform: uppercase;
			}
			.yield-protocol-badge.aave-v3 { background: rgba(180,128,255,0.12); color: #b480ff; }
			.yield-protocol-badge.morpho-blue { background: rgba(0,148,255,0.12); color: #0094ff; }
			.yield-chain-badge {
				font-size: 10px; padding: 2px 6px; border-radius: 4px;
				background: var(--rs-bg-hover); color: var(--rs-text-secondary);
			}
			.yield-suggestion {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle);
				border-radius: 10px; padding: 14px; margin-bottom: 8px;
			}
			.yield-prio-badge {
				font-size: 10px; font-weight: 700; padding: 2px 6px; border-radius: 4px;
				text-transform: uppercase; margin-right: 6px;
			}
			.prio-high { background: rgba(239,83,80,0.15); color: #ef5350; }
			.prio-med { background: rgba(255,167,38,0.15); color: #ffa726; }
			.prio-low { background: rgba(102,187,106,0.15); color: #66bb6a; }
			.yield-suggestion-type {
				font-size: 12px; font-weight: 600; color: var(--rs-text-primary); text-transform: uppercase;
			}
			.yield-suggestion-reason {
				font-size: 13px; color: var(--rs-text-secondary); margin: 8px 0; line-height: 1.4;
			}
			.yield-suggestion-footer {
				display: flex; align-items: center; justify-content: space-between;
			}
			.yield-suggestion-gas { font-size: 11px; color: var(--rs-text-muted); }
			.yield-action-btn {
				padding: 6px 16px; border-radius: 6px; border: 1px solid var(--rs-accent);
				background: transparent; color: var(--rs-accent); cursor: pointer; font-size: 12px;
				font-weight: 600; transition: all 0.2s;
			}
			.yield-action-btn:hover { background: rgba(20,184,166,0.1); }
			.yield-action-btn:disabled { opacity: 0.5; cursor: not-allowed; }

			/* ── Yield rates table ── */
			.yield-rates-table { width: 100%; border-collapse: collapse; table-layout: fixed; }
			.yield-rates-table th {
				text-align: left; padding: 10px 8px; border-bottom: 2px solid var(--rs-border);
				color: var(--rs-text-secondary); font-size: 11px; text-transform: uppercase;
			}
			.yield-rates-table td {
				padding: 12px 8px; border-bottom: 1px solid var(--rs-border-subtle);
				vertical-align: middle;
			}
			.yield-rates-table tr:hover td { background: var(--rs-bg-hover); }
			.yield-rates-table .col-protocol { width: 22%; }
			.yield-rates-table .col-chain { width: 14%; }
			.yield-rates-table .col-asset { width: 12%; }
			.yield-rates-table .col-num { width: 17%; text-align: right; font-family: monospace; }

			/* ── Sandbox panel ── */
			.sandbox-panel {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle);
				border-radius: 12px; padding: 16px 20px; margin-bottom: 20px;
			}
			.sandbox-toggle {
				display: inline-flex; align-items: center; gap: 6px;
				padding: 6px 14px; border-radius: 8px; border: 1px solid var(--rs-accent);
				background: transparent; color: var(--rs-accent); cursor: pointer;
				font-size: 12px; font-weight: 600; transition: all 0.2s; margin-bottom: 12px;
			}
			.sandbox-toggle:hover { background: rgba(20,184,166,0.1); }
			.sandbox-toggle.active { background: var(--rs-accent); color: #000; }
			.sandbox-inputs {
				display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 14px;
			}
			.sandbox-inputs input, .sandbox-inputs select {
				padding: 6px 10px; border-radius: 6px; border: 1px solid var(--rs-border);
				background: var(--rs-bg); color: var(--rs-text-primary); font-size: 13px;
				font-family: monospace;
			}
			.sandbox-inputs input { width: 120px; }
			.sandbox-inputs select { min-width: 80px; }
			.sandbox-slider-row {
				display: flex; align-items: center; gap: 12px; margin-bottom: 14px;
			}
			.sandbox-slider-row input[type="range"] {
				flex: 1; accent-color: var(--rs-accent); cursor: pointer;
			}
			.sandbox-slider-label {
				font-size: 12px; color: var(--rs-text-secondary); min-width: 60px; text-align: right;
			}
			.sandbox-slider-ends {
				font-size: 10px; color: var(--rs-text-muted); white-space: nowrap;
			}
			.sandbox-compare {
				display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px;
			}
			.sandbox-compare-row {
				display: grid; grid-template-columns: 1fr 80px 50px 100px 100px;
				gap: 8px; align-items: center; padding: 8px 10px; border-radius: 8px;
				background: var(--rs-bg); font-size: 13px;
			}
			.sandbox-compare-row:first-child { background: rgba(20,184,166,0.08); }
			.sandbox-compare-label { font-weight: 500; color: var(--rs-text-primary); }
			.sandbox-compare-apy { font-family: monospace; font-weight: 700; color: var(--rs-success); text-align: right; }
			.sandbox-live-badge {
				font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px;
				text-transform: uppercase; text-align: center;
			}
			.sandbox-live-badge.live { background: rgba(102,187,106,0.15); color: #66bb6a; }
			.sandbox-live-badge.est { background: rgba(255,167,38,0.15); color: #ffa726; }
			.sandbox-earnings { font-family: monospace; text-align: right; color: var(--rs-success); }
			.sandbox-balance { font-family: monospace; text-align: right; color: var(--rs-text-secondary); }
			.sandbox-milestones {
				display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; text-align: center;
			}
			.sandbox-milestone {
				display: flex; flex-direction: column; gap: 2px; padding: 6px 4px;
				border-radius: 6px; background: var(--rs-bg);
			}
			.sandbox-milestone-label { font-size: 10px; color: var(--rs-text-muted); text-transform: uppercase; }
			.sandbox-milestone-value { font-size: 12px; font-weight: 600; color: var(--rs-success); font-family: monospace; }
			@media (max-width: 640px) {
				.sandbox-milestones { grid-template-columns: repeat(4, 1fr); }
				.sandbox-compare-row { grid-template-columns: 1fr 60px 40px 80px; }
				.sandbox-balance { display: none; }
			}

			/* ── Compact address bar ── */
			.compact-address-bar {
				display: flex; align-items: center; gap: 8px; margin-bottom: 12px;
				max-width: 640px; margin-left: auto; margin-right: auto;
				padding: 8px 14px; border-radius: 10px;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-subtle);
			}
			.compact-addr-chip {
				display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0;
				font-family: monospace; font-size: 14px; font-weight: 600; color: var(--rs-text-primary);
			}
			.compact-btn {
				padding: 5px 12px; border-radius: 6px; border: 1px solid var(--rs-border);
				background: transparent; color: var(--rs-text-secondary); cursor: pointer;
				font-size: 12px; font-weight: 500; transition: all 0.2s; white-space: nowrap;
			}
			.compact-btn:hover { border-color: var(--rs-accent); color: var(--rs-accent); }

			/* ── Watchlist chips ── */
			.watchlist-bar {
				display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
				margin-bottom: 16px; max-width: 640px; margin-left: auto; margin-right: auto;
			}
			.watchlist-chip {
				display: inline-flex; align-items: center; gap: 4px;
				padding: 4px 10px; border-radius: 16px;
				border: 1px solid var(--rs-border-subtle); background: var(--rs-bg-surface);
				color: var(--rs-text-secondary); cursor: pointer; font-size: 12px;
				transition: all 0.2s; white-space: nowrap;
			}
			.watchlist-chip:hover { border-color: var(--rs-accent); color: var(--rs-accent); }
			.watchlist-chip.active { border-color: var(--rs-accent); background: rgba(20,184,166,0.1); color: var(--rs-accent); }
			.watchlist-chip.suggested { border-style: dashed; color: var(--rs-text-muted); }
			.watchlist-chip.suggested:hover { border-style: solid; }
			.watchlist-chip.add-chip { padding: 4px 8px; font-size: 14px; font-weight: 700; }
			.chip-label { font-weight: 500; }
			.chip-addr { font-family: monospace; font-size: 10px; color: var(--rs-text-muted); }
			.chip-remove {
				font-size: 14px; line-height: 1; padding: 0 2px; margin-left: 2px;
				color: var(--rs-text-muted); cursor: pointer;
			}
			.chip-remove:hover { color: var(--rs-error); }

			/* ── Details modal ── */
			.details-modal-overlay {
				position: fixed; inset: 0; z-index: 1000;
				background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
				display: flex; align-items: center; justify-content: center;
				padding: 20px;
			}
			.details-modal {
				background: var(--rs-bg, #0f0f1a); border: 1px solid var(--rs-border-subtle);
				border-radius: 16px; max-width: 900px; width: 100%;
				max-height: 80vh; display: flex; flex-direction: column;
				box-shadow: 0 20px 60px rgba(0,0,0,0.5);
			}
			.details-modal-header {
				display: flex; align-items: center; justify-content: space-between;
				padding: 16px 20px; border-bottom: 1px solid var(--rs-border-subtle);
			}
			.details-modal-header h3 { margin: 0; font-size: 16px; color: var(--rs-text-primary); }
			.details-modal-header button {
				background: none; border: none; color: var(--rs-text-muted); cursor: pointer;
				font-size: 20px; padding: 4px 8px; border-radius: 6px;
			}
			.details-modal-header button:hover { color: var(--rs-text-primary); background: var(--rs-bg-hover); }
			.details-modal-body { overflow-y: auto; padding: 20px; }

			/* ── Viz wrapper with details button ── */
			.viz-wrapper { position: relative; }
			.details-btn {
				position: absolute; top: 8px; right: 8px; z-index: 5;
				padding: 5px 14px; border-radius: 16px;
				border: 1px solid var(--rs-border); background: var(--rs-bg-surface);
				color: var(--rs-text-secondary); cursor: pointer; font-size: 12px;
				font-weight: 500; transition: all 0.2s; backdrop-filter: blur(4px);
			}
			.details-btn:hover { border-color: var(--rs-accent); color: var(--rs-accent); }

			@media (max-width: 768px) {
				.hero-title { font-size: 22px; }
				.balance-table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
				.address-bar { flex-wrap: wrap; }
				.address-bar input { min-width: 0; }
				.chains { flex-wrap: wrap; }
				.features { grid-template-columns: 1fr 1fr; }
				.view-tabs { overflow-x: auto; }
				.wallet-card-header { flex-direction: column; align-items: flex-start; }
			}
			@media (max-width: 640px) {
				.view-tab { padding: 6px 10px; font-size: 12px; }
				.compact-address-bar { flex-wrap: wrap; }
				.watchlist-bar { gap: 4px; }
			}
			@media (max-width: 480px) {
				.features { grid-template-columns: 1fr; }
				.hero-title { font-size: 18px; }
				.hero-subtitle { font-size: 13px; }
				.view-tabs { gap: 2px; }
				.aggregate-stats { grid-template-columns: 1fr 1fr; gap: 8px; }
				.wallet-item { padding: 8px; }
				.my-wallets { padding: 12px; }
			}
		</style>`;
	}

	private renderHero(): string {
		if (this.hasData()) return "";
		return `
		<div class="hero">
			<div class="hero-title">rWallet</div>
			<div class="hero-subtitle">Multichain treasury visualization — Safe multisigs and EOA wallets</div>
		</div>`;
	}

	private renderMyWallets(): string {
		if (!this.isAuthenticated) return "";

		const hasWallets = this.passKeyEOA || this.linkedWallets.length > 0;

		return `
		<div class="my-wallets">
			<div class="my-wallets-header">
				<div class="my-wallets-title">My Wallets</div>
				<button class="link-wallet-btn" id="link-wallet-btn">+ Link Wallet</button>
			</div>
			<div class="wallet-list">
				${this.passKeyEOA ? `
					<div class="wallet-item" data-view-address="${this.esc(this.passKeyEOA)}" data-collab-id="wallet:${this.esc(this.passKeyEOA)}">
						<div class="wallet-item-info">
							<div class="wallet-item-label">
								<span class="wallet-badge encryptid">EncryptID</span>
								Passkey EOA
							</div>
							<div class="wallet-item-addr">${this.shortenAddress(this.passKeyEOA)}</div>
						</div>
					</div>
				` : ""}
				${this.linkedWallets.map(w => `
					<div class="wallet-item" data-view-address="${this.esc(w.address)}" data-collab-id="wallet:${this.esc(w.address)}">
						<div class="wallet-item-info">
							<div class="wallet-item-label">
								<span class="wallet-badge ${w.type}">${this.esc(w.providerName || w.type.toUpperCase())}</span>
								${this.esc(w.label)}
								${w.safeInfo && !w.safeInfo.isEncryptIdOwner ? '<span style="font-size:10px;color:var(--rs-text-muted)">(not co-signer)</span>' : ""}
							</div>
							<div class="wallet-item-addr">${this.shortenAddress(w.address)}</div>
						</div>
						<div class="wallet-item-actions">
							<button class="unlink-btn" data-unlink="${this.esc(w.id)}" title="Unlink wallet">&#10005;</button>
						</div>
					</div>
				`).join("")}
				${!hasWallets ? '<div style="font-size:12px;color:var(--rs-text-muted);padding:8px;">No wallets linked yet. Click "Link Wallet" to connect a browser wallet.</div>' : ""}
			</div>
			${this.renderProviderPicker()}
		</div>`;
	}

	private renderProviderPicker(): string {
		if (!this.showProviderPicker) return "";

		return `
		<div class="provider-picker">
			<div class="provider-picker-title">
				${this.linkingInProgress ? '<span class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px"></span> Connecting...' : 'Select a wallet to link:'}
			</div>
			${!this.linkingInProgress ? `
				<div class="provider-list">
					${this.discoveredProviders.map(p => `
						<div class="provider-item" data-provider-uuid="${this.esc(p.uuid)}">
							${p.icon ? `<img src="${this.esc(p.icon)}" alt="" onerror="this.style.display='none'">` : ""}
							${this.esc(p.name)}
						</div>
					`).join("")}
					${this.discoveredProviders.length === 0 ? '<div style="font-size:12px;color:var(--rs-text-muted);padding:4px;">Searching for wallets...</div>' : ""}
				</div>
				<button class="provider-cancel" id="provider-cancel">Cancel</button>
			` : ""}
			${this.linkError ? `<div class="link-error">${this.esc(this.linkError)}</div>` : ""}
		</div>`;
	}

	private renderSupportedChains(): string {
		if (this.hasData() || this.loading) return "";
		return `
		<div class="supported-chains">
			${Object.entries(CHAIN_NAMES).map(([id, name]) => `
				<div class="supported-chain">
					<div class="dot" style="background: ${CHAIN_COLORS[id]}"></div>
					${name}
				</div>
			`).join("")}
		</div>`;
	}

	private renderFeatures(): string {
		if (this.hasData() || this.loading) return "";
		return `
		<div class="features">
			<div class="feature-card">
				<div class="feature-icon">&#9939;</div>
				<h3>Safe Multisig</h3>
				<p>Visualize Gnosis Safe balances, signers, and thresholds across all chains.</p>
			</div>
			<div class="feature-card">
				<div class="feature-icon">&#128279;</div>
				<h3>Any Wallet (EOA)</h3>
				<p>Paste any 0x address — works for regular wallets too, not just Safes.</p>
			</div>
			<div class="feature-card">
				<div class="feature-icon">&#127760;</div>
				<h3>10+ Chains</h3>
				<p>Ethereum, Base, Polygon, Gnosis, Arbitrum, Optimism, and more in one view.</p>
			</div>
			<div class="feature-card">
				<div class="feature-icon">&#128274;</div>
				<h3>No Custody Risk</h3>
				<p>Read-only. rWallet never holds keys or moves funds — just visualizes.</p>
			</div>
		</div>`;
	}

	private renderExamples(): string {
		if (this.hasData() || this.loading) return "";
		return `
		<div class="examples">
			<div class="examples-label">Try an example</div>
			<div class="example-list">
				${EXAMPLE_WALLETS.map((w) => `
					<div class="example-item" data-address="${w.address}">
						<div>
							<div class="example-name">${w.name}</div>
							<div class="example-addr">${w.address.slice(0, 6)}...${w.address.slice(-4)}</div>
						</div>
						<div class="example-type ${w.type.toLowerCase()}">${w.type}</div>
					</div>
				`).join("")}
			</div>
		</div>`;
	}

	private renderWatchlist(): string {
		// Client-side membrane filter (defense-in-depth)
		const visibleAddresses = this.watchedAddresses.filter(w => this._isVisibleTo(w.visibility, this._myRole));
		if (visibleAddresses.length === 0 && !this.lfClient) return '';
		const isLive = this.lfClient?.isConnected ?? false;
		return `
		<div class="watchlist" style="margin-top:1.5rem">
			<div style="display:flex;align-items:center;gap:8px;margin-bottom:0.75rem">
				<span style="font-size:0.75rem;text-transform:uppercase;letter-spacing:0.05em;color:var(--rs-text-muted);font-weight:600">Shared Watchlist</span>
				${isLive ? '<span style="font-size:0.65rem;padding:2px 6px;border-radius:999px;background:rgba(34,197,94,0.15);color:#22c55e;font-weight:500">LIVE</span>' : ''}
				<button class="watch-add-btn" style="margin-left:auto;font-size:0.75rem;padding:3px 10px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-family:inherit" data-action="add-watch">+ Watch</button>
			</div>
			${visibleAddresses.length === 0
				? '<div style="text-align:center;padding:1rem;color:var(--rs-text-muted);font-size:0.8rem">No watched addresses. Click "Watch" to add one.</div>'
				: `<div style="display:flex;flex-direction:column;gap:4px">${visibleAddresses.map(w => {
					const key = `${w.chain}:${w.address.toLowerCase()}`;
					const wLock = w.visibility && w.visibility !== 'viewer' ? '<span class="membrane-lock" title="Restricted: ' + w.visibility + ' only">&#128274;</span> ' : '';
					return `<div class="watch-item" style="display:flex;align-items:center;gap:8px;padding:0.5rem 0.75rem;background:var(--rs-bg-surface);border:1px solid var(--rs-border);border-radius:8px;cursor:pointer" data-watch-address="${w.address}">
						<span style="font-weight:600;font-size:0.875rem;color:var(--rs-text-primary);flex:1">${wLock}${this.esc(w.label || w.address.slice(0, 8) + '...')}</span>
						<span style="font-size:0.75rem;color:var(--rs-text-muted)">${w.address.slice(0, 6)}...${w.address.slice(-4)}</span>
						<button class="watch-remove" data-watch-key="${key}" style="font-size:0.65rem;color:var(--rs-text-muted);border:none;background:none;cursor:pointer;padding:2px 4px">&times;</button>
					</div>`;
				}).join('')}</div>`}
		</div>`;
	}

	private renderMyWalletsTab(): string {
		if (!this.isAuthenticated) {
			return `<div class="empty">
				<p>Sign in to view your wallets</p>
				<a href="/auth" style="color:var(--rs-accent)">Sign in with EncryptID &rarr;</a>
			</div>`;
		}

		if (this.myWalletsLoading) {
			return `<div class="loading"><span class="spinner"></span> Loading wallet balances...</div>`;
		}

		let html = '<div class="my-wallets-grid">';

		// EncryptID identity card — always shown for authenticated users
		// Every EncryptID identity has both a CRDT wallet (DID) and an EVM wallet (passkey-derived)
		html += this.renderEncryptIdCard();

		// Linked wallet cards (external wallets)
		for (const w of this.linkedWallets) {
			html += this.renderWalletCard(w.address, w.providerName || w.type.toUpperCase(), w.type, false, w.id);
		}

		html += '</div>';

		// Aggregate total
		html += this.renderAggregateTotal();

		// Link wallet button + provider picker
		html += `
		<div class="my-wallets-tab-link-btn">
			<button class="link-wallet-btn" id="link-wallet-btn" style="padding:10px 24px;font-size:14px">+ Link Wallet</button>
		</div>
		${this.renderProviderPicker()}`;

		return html;
	}

	private renderEncryptIdCard(): string {
		// ── CRDT Wallet (DID) ──
		const didShort = this.userDID
			? `${this.userDID.slice(0, 16)}...${this.userDID.slice(-6)}`
			: "loading...";

		let crdtRows = "";
		if (this.crdtLoading) {
			crdtRows = `<div style="padding:8px;color:var(--rs-text-muted);font-size:12px"><span class="spinner" style="width:12px;height:12px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px"></span> Loading...</div>`;
		} else if (this.crdtBalances.length > 0) {
			crdtRows = this.crdtBalances.map(t => {
				const formatted = (t.balance / Math.pow(10, t.decimals)).toFixed(2);
				return `<div class="crdt-row">
					<span style="font-size:1.1em">${t.icon || '\u{1FA99}'}</span>
					<strong>${this.esc(t.symbol)}</strong>
					<span class="token-name" style="margin-left:4px">${this.esc(t.name)}</span>
					<span style="margin-left:auto;font-family:monospace;font-weight:600">${formatted}</span>
				</div>`;
			}).join("");
		} else {
			crdtRows = `<div style="padding:8px;color:var(--rs-text-muted);font-size:12px">No CRDT token balances</div>`;
		}

		// ── EVM Wallet (passkey-derived) ──
		const chainBalances = this.passKeyEOA
			? (this.myWalletBalances.get(this.passKeyEOA.toLowerCase()) || [])
			: [];

		const allBals: Array<BalanceItem & { chainId: string; chainName: string }> = [];
		for (const ch of chainBalances) {
			for (const b of ch.balances) {
				allBals.push({ ...b, chainId: ch.chainId, chainName: ch.chainName });
			}
		}

		const sorted = allBals
			.filter(b => {
				const fiat = parseFloat(b.fiatBalance || "0");
				if (fiat > 0.01) return true;
				if (b.chainId === "local" || b.tokenAddress?.startsWith("crdt:")) return true;
				if (!b.tokenAddress && BigInt(b.balance || "0") > 0n) return true;
				return false;
			})
			.sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"));

		const totalUSD = sorted.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);

		let evmBalanceRows = "";
		if (sorted.length > 0) {
			evmBalanceRows = sorted.slice(0, 10).map(b => {
				const color = CHAIN_COLORS[b.chainId] || "#888";
				return `<tr>
					<td><div class="chain-cell"><span class="chain-dot-sm" style="background:${color}"></span>${this.esc(b.chainName)}</div></td>
					<td><span class="token-symbol">${this.esc(b.token?.symbol || "ETH")}</span></td>
					<td class="amount-cell">${this.formatBalance(b.balance, b.token?.decimals || 18)}</td>
					<td class="amount-cell fiat">${this.formatUSD(b.fiatBalance)}</td>
				</tr>`;
			}).join("");
			if (sorted.length > 10) {
				evmBalanceRows += `<tr><td colspan="4" style="text-align:center;color:var(--rs-text-muted);font-size:11px;padding:8px">+ ${sorted.length - 10} more tokens</td></tr>`;
			}
		}

		return `
		<div class="wallet-card encryptid-card">
			<div class="wallet-card-header">
				<div>
					<span class="wallet-badge encryptid">EncryptID</span>
					<span class="wallet-card-total" style="margin-left:12px">${this.formatUSD(String(totalUSD))}</span>
				</div>
				${this.passKeyEOA ? `<button class="view-flows-btn" data-view-in-viz="${this.esc(this.passKeyEOA)}">View Flows &rarr;</button>` : ""}
			</div>

			<div class="encryptid-wallets">
				<!-- CRDT Wallet -->
				<div class="encryptid-wallet-section">
					<div class="encryptid-wallet-label">
						<span class="wallet-type-dot" style="background:#2775ca"></span>
						CRDT Wallet
						<span class="wallet-type-addr">${this.esc(didShort)}</span>
					</div>
					${crdtRows}
				</div>

				<!-- EVM Wallet -->
				<div class="encryptid-wallet-section">
					<div class="encryptid-wallet-label">
						<span class="wallet-type-dot" style="background:#627eea"></span>
						EVM Wallet
						<span class="wallet-type-addr">${this.passKeyEOA ? this.shortenAddress(this.passKeyEOA) : "not derived"}</span>
					</div>
					${evmBalanceRows ? `
					<table class="balance-table compact">
						<thead>
							<tr><th>Chain</th><th>Token</th><th class="amount-cell">Balance</th><th class="amount-cell">USD</th></tr>
						</thead>
						<tbody>${evmBalanceRows}</tbody>
					</table>` : `<div style="padding:8px;color:var(--rs-text-muted);font-size:12px">No on-chain balances found</div>`}
				</div>
			</div>
		</div>`;
	}

	private renderWalletCard(address: string, label: string, badgeClass: string, _isEncryptId: boolean, walletId?: string): string {
		const chainBalances = this.myWalletBalances.get(address.toLowerCase()) || [];

		// Flatten all balances for this wallet
		const allBals: Array<BalanceItem & { chainId: string; chainName: string }> = [];
		for (const ch of chainBalances) {
			for (const b of ch.balances) {
				allBals.push({ ...b, chainId: ch.chainId, chainName: ch.chainName });
			}
		}

		const sorted = allBals
			.filter(b => {
				const fiat = parseFloat(b.fiatBalance || "0");
				if (fiat > 0.01) return true;
				if (b.chainId === "local" || b.tokenAddress?.startsWith("crdt:")) return true;
				if (!b.tokenAddress && BigInt(b.balance || "0") > 0n) return true;
				return false;
			})
			.sort((a, b) => parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0"));

		const totalUSD = sorted.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);

		let balanceRows = "";
		if (sorted.length > 0) {
			balanceRows = sorted.slice(0, 10).map(b => {
				const color = CHAIN_COLORS[b.chainId] || "#888";
				return `<tr>
					<td><div class="chain-cell"><span class="chain-dot-sm" style="background:${color}"></span>${this.esc(b.chainName)}</div></td>
					<td><span class="token-symbol">${this.esc(b.token?.symbol || "ETH")}</span></td>
					<td class="amount-cell">${this.formatBalance(b.balance, b.token?.decimals || 18)}</td>
					<td class="amount-cell fiat">${this.formatUSD(b.fiatBalance)}</td>
				</tr>`;
			}).join("");
			if (sorted.length > 10) {
				balanceRows += `<tr><td colspan="4" style="text-align:center;color:var(--rs-text-muted);font-size:11px;padding:8px">+ ${sorted.length - 10} more tokens</td></tr>`;
			}
		}

		return `
		<div class="wallet-card">
			<div class="wallet-card-header">
				<div>
					<span class="wallet-badge ${badgeClass}">${this.esc(label)}</span>
					<span class="wallet-card-addr">${this.shortenAddress(address)}</span>
				</div>
				<div class="wallet-card-actions">
					<span class="wallet-card-total">${this.formatUSD(String(totalUSD))}</span>
					<button class="view-flows-btn" data-view-in-viz="${this.esc(address)}">View Flows &rarr;</button>
					${walletId ? `<button class="unlink-btn-card" data-unlink="${this.esc(walletId)}" title="Unlink">&#10005;</button>` : ""}
				</div>
			</div>
			${sorted.length > 0 ? `
			<table class="balance-table compact">
				<thead>
					<tr><th>Chain</th><th>Token</th><th class="amount-cell">Balance</th><th class="amount-cell">USD</th></tr>
				</thead>
				<tbody>${balanceRows}</tbody>
			</table>` : `<div style="padding:12px;text-align:center;color:var(--rs-text-muted);font-size:12px">No on-chain balances found</div>`}
		</div>`;
	}

	private renderAggregateTotal(): string {
		let grandTotal = 0;
		let totalTokens = 0;
		const totalChains = new Set<string>();

		for (const [, chains] of this.myWalletBalances) {
			for (const ch of chains) {
				totalChains.add(ch.chainId);
				for (const b of ch.balances) {
					const fiat = parseFloat(b.fiatBalance || "0");
					if (fiat > 0.01 || (!b.tokenAddress && BigInt(b.balance || "0") > 0n)) {
						grandTotal += fiat;
						totalTokens++;
					}
				}
			}
		}

		// Include DeFi positions in aggregate
		grandTotal += this.defiTotalUSD;

		if (grandTotal === 0 && totalTokens === 0) return "";

		const walletCount = (this.passKeyEOA ? 1 : 0) + this.linkedWallets.length;
		const defiLabel = this.defiTotalUSD > 0 ? ` (incl. ${this.formatUSD(String(this.defiTotalUSD))} DeFi)` : "";

		return `
		<div class="aggregate-stats">
			<div class="stat-card">
				<div class="stat-label">Total Portfolio${defiLabel}</div>
				<div class="stat-value">${this.formatUSD(String(grandTotal))}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Wallets</div>
				<div class="stat-value">${walletCount}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Tokens</div>
				<div class="stat-value">${totalTokens}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Chains</div>
				<div class="stat-value">${totalChains.size}</div>
			</div>
		</div>`;
	}

	private renderViewTabs(): string {
		if (!this.hasData()) return "";
		const tabs: { id: ViewTab; label: string }[] = [
			{ id: "balances", label: "Token Balances" },
			{ id: "budget", label: "Budget Visualization" },
			{ id: "flows", label: "Flows" },
		];
		// Show viz tabs for Safe wallets, demo, or when CRDT tokens exist
		const showViz = this.walletType === "safe" || this.isDemo || (this.isAuthenticated && this.crdtBalances.length > 0);
		const visibleTabs = showViz ? tabs : [tabs[0]];

		return `
		<div class="view-tabs">
			${visibleTabs.map(t => `
				<button class="view-tab ${this.activeView === t.id ? "active" : ""}" data-view="${t.id}">${t.label}</button>
			`).join("")}
		</div>`;
	}

	private renderYieldStandaloneHeader(): string {
		// Derive the wallets link from current URL
		const basePath = window.location.pathname.replace(/\/yield\/?$/, "");
		return `
		<div class="yield-header">
			<div class="yield-header-row">
				<div>
					<h2 class="yield-header-title">Stablecoin Yield</h2>
					<p class="yield-header-desc">Compare APY rates across Aave V3 and Morpho Blue vaults on Ethereum and Base. Deposit idle USDC, USDT, or DAI through your Safe multisig.</p>
				</div>
				<a href="${basePath}/wallets" class="yield-back-link">Back to Wallets</a>
			</div>
		</div>`;
	}

	private recomputeSandbox() {
		const liveRates = this.yieldRates.map((r) => ({
			protocol: r.protocol,
			chainId: r.chainId,
			asset: r.asset,
			apy: r.apy,
		}));
		this.sandboxComparisons = buildProtocolComparisons(
			this.sandboxAsset,
			this.sandboxPrincipal,
			this.sandboxDays,
			liveRates.length ? liveRates : undefined,
		);
	}

	private formatDaysLabel(days: number): string {
		if (days < 7) return `${days} day${days > 1 ? "s" : ""}`;
		if (days < 30) return `${Math.round(days / 7)} week${days >= 14 ? "s" : ""}`;
		if (days < 365) return `${Math.round(days / 30)} month${days >= 60 ? "s" : ""}`;
		const y = days / 365;
		return y === Math.floor(y) ? `${y} year${y > 1 ? "s" : ""}` : `${y.toFixed(1)} years`;
	}

	private renderSandboxPanel(): string {
		let html = `<button class="sandbox-toggle ${this.sandboxActive ? "active" : ""}" data-sandbox-toggle>
			${this.sandboxActive ? "Close Sandbox" : "Try Sandbox"}
		</button>`;

		if (!this.sandboxActive) return html;

		// Inputs row
		html += `<div class="sandbox-inputs">
			<label style="font-size:12px;color:var(--rs-text-secondary)">Amount</label>
			<input type="number" id="sandbox-amount" value="${this.sandboxPrincipal}" min="1" step="100">
			<label style="font-size:12px;color:var(--rs-text-secondary)">Asset</label>
			<select id="sandbox-asset">
				<option value="USDC" ${this.sandboxAsset === "USDC" ? "selected" : ""}>USDC</option>
				<option value="USDT" ${this.sandboxAsset === "USDT" ? "selected" : ""}>USDT</option>
				<option value="DAI" ${this.sandboxAsset === "DAI" ? "selected" : ""}>DAI</option>
			</select>
			<label style="font-size:12px;color:var(--rs-text-secondary)">Chain</label>
			<select id="sandbox-chain">
				<option value="" ${this.sandboxChain === "" ? "selected" : ""}>All</option>
				<option value="1" ${this.sandboxChain === "1" ? "selected" : ""}>Ethereum</option>
				<option value="8453" ${this.sandboxChain === "8453" ? "selected" : ""}>Base</option>
			</select>
		</div>`;

		// Time slider
		html += `<div class="sandbox-slider-row">
			<span class="sandbox-slider-ends">1d</span>
			<input type="range" id="sandbox-days" min="1" max="1825" value="${this.sandboxDays}">
			<span class="sandbox-slider-ends">5yr</span>
			<span class="sandbox-slider-label">${this.formatDaysLabel(this.sandboxDays)}</span>
		</div>`;

		// Protocol comparison
		const comparisons = this.sandboxChain
			? this.sandboxComparisons.filter((c) => c.chainId === this.sandboxChain)
			: this.sandboxComparisons;

		if (comparisons.length > 0) {
			html += `<div class="sandbox-compare">`;
			for (const c of comparisons) {
				html += `<div class="sandbox-compare-row">
					<span class="sandbox-compare-label">${this.esc(c.label)}</span>
					<span class="sandbox-compare-apy">${c.apy.toFixed(2)}%</span>
					<span class="sandbox-live-badge ${c.isLive ? "live" : "est"}">${c.isLive ? "LIVE" : "EST"}</span>
					<span class="sandbox-earnings">+$${c.earnings < 1 ? c.earnings.toFixed(2) : c.earnings.toFixed(0)}</span>
					<span class="sandbox-balance">$${c.balance.toLocaleString("en-US", { maximumFractionDigits: 0 })}</span>
				</div>`;
			}
			html += `</div>`;
		}

		// Milestones (use top comparison APY or first available)
		const bestApy = comparisons[0]?.apy || this.sandboxComparisons[0]?.apy || 4;
		const milestonePoints = [1, 7, 30, 90, 180, 365, 730, 1825];
		const milestoneLabels = ["1d", "7d", "30d", "90d", "6mo", "1yr", "2yr", "5yr"];

		html += `<div class="sandbox-milestones">`;
		for (let i = 0; i < milestonePoints.length; i++) {
			const pt = computeYieldAtDays(this.sandboxPrincipal, bestApy, milestonePoints[i]);
			const val = pt.earnings < 1 ? `$${pt.earnings.toFixed(2)}` : `$${pt.earnings.toFixed(0)}`;
			html += `<div class="sandbox-milestone">
				<span class="sandbox-milestone-label">${milestoneLabels[i]}</span>
				<span class="sandbox-milestone-value">${val}</span>
			</div>`;
		}
		html += `</div>`;

		return `<div class="sandbox-panel">${html}</div>`;
	}

	private renderYieldTab(): string {
		if (this.yieldLoading) {
			return '<div class="loading"><span class="spinner"></span> Loading yield data...</div>';
		}
		if (this.yieldError) {
			return `<div class="error">${this.esc(this.yieldError)}</div>`;
		}

		const chainNames: Record<string, string> = { "1": "Ethereum", "8453": "Base" };
		let html = this.renderSandboxPanel();

		// ── Strategy summary banner ──
		if (this.yieldSuggestions.length > 0 || this.yieldTotalDepositedUSD > 0) {
			html += `<div class="yield-summary">
				<div class="yield-summary-row">
					<div class="yield-stat">
						<span class="yield-stat-label">Idle Stablecoins</span>
						<span class="yield-stat-value">${this.formatUSD(String(this.yieldTotalIdleUSD))}</span>
					</div>
					<div class="yield-stat">
						<span class="yield-stat-label">Earning Yield</span>
						<span class="yield-stat-value">${this.formatUSD(String(this.yieldTotalDepositedUSD))}</span>
					</div>
					<div class="yield-stat">
						<span class="yield-stat-label">Weighted APY</span>
						<span class="yield-stat-value" style="color:var(--rs-success)">${this.yieldWeightedAPY.toFixed(2)}%</span>
					</div>
				</div>
			</div>`;
		}

		// ── Positions ──
		if (this.yieldPositions.length > 0) {
			html += `<div class="yield-section">
				<h3 class="yield-section-title">Active Positions</h3>
				<div class="yield-positions">`;
			for (const pos of this.yieldPositions) {
				const value = Number(BigInt(pos.underlying)) / 10 ** pos.decimals;
				html += `
				<div class="yield-position-card">
					<div class="yield-pos-header">
						<span class="yield-protocol-badge ${pos.protocol}">${pos.protocol === "aave-v3" ? "Aave V3" : "Morpho"}</span>
						<span class="yield-chain-badge">${chainNames[pos.chainId] || pos.chainId}</span>
						<span style="margin-left:auto;font-weight:600;color:var(--rs-text-primary)">${pos.asset}</span>
					</div>
					<div class="yield-pos-stats">
						<div>
							<span class="yield-pos-label">Deposited</span>
							<span class="yield-pos-value">${value.toLocaleString("en-US", { style: "currency", currency: "USD" })}</span>
						</div>
						<div>
							<span class="yield-pos-label">APY</span>
							<span class="yield-pos-value" style="color:var(--rs-success)">${pos.apy.toFixed(2)}%</span>
						</div>
						<div>
							<span class="yield-pos-label">Daily</span>
							<span class="yield-pos-value">${pos.dailyEarnings.toFixed(2)}</span>
						</div>
						<div>
							<span class="yield-pos-label">Annual</span>
							<span class="yield-pos-value">${pos.annualEarnings.toFixed(2)}</span>
						</div>
					</div>
				</div>`;
			}
			html += `</div></div>`;
		}

		// ── Suggestions ──
		if (this.yieldSuggestions.length > 0) {
			html += `<div class="yield-section">
				<h3 class="yield-section-title">Suggestions</h3>`;
			for (let i = 0; i < this.yieldSuggestions.length; i++) {
				const s = this.yieldSuggestions[i];
				const prioClass = s.priority === "high" ? "prio-high" : s.priority === "medium" ? "prio-med" : "prio-low";
				html += `
				<div class="yield-suggestion">
					<span class="yield-prio-badge ${prioClass}">${s.priority}</span>
					<span class="yield-suggestion-type">${s.type === "deposit" ? "Deposit" : "Rebalance"}</span>
					<p class="yield-suggestion-reason">${this.esc(s.reason)}</p>
					<div class="yield-suggestion-footer">
						<span class="yield-suggestion-gas">Est. gas: ${s.estimatedGasCostUSD.toFixed(2)}</span>
						${this.isAuthenticated && this.walletType === "safe" ? `<button class="yield-action-btn" data-yield-deposit="${i}" ${this.yieldDepositInProgress ? "disabled" : ""}>
							${this.yieldDepositInProgress ? "Building..." : "Execute"}
						</button>` : ""}
					</div>
				</div>`;
			}
			html += `</div>`;
		}

		// ── Rates table ──
		if (this.yieldRates.length > 0) {
			// Deduplicate: keep highest-APY entry per protocol+chain+asset
			const deduped = new Map<string, YieldRate>();
			const sorted = [...this.yieldRates].sort((a, b) => b.apy - a.apy);
			for (const r of sorted) {
				const key = `${r.protocol}:${r.chainId}:${r.asset}`;
				if (!deduped.has(key)) deduped.set(key, r);
			}
			const rates = [...deduped.values()];

			html += `<div class="yield-section">
				<h3 class="yield-section-title">Available Rates</h3>
				<table class="yield-rates-table">
					<colgroup>
						<col class="col-protocol">
						<col class="col-chain">
						<col class="col-asset">
						<col class="col-num">
						<col class="col-num">
						<col class="col-num">
					</colgroup>
					<thead><tr>
						<th>Protocol</th>
						<th>Chain</th>
						<th>Asset</th>
						<th class="col-num">APY</th>
						<th class="col-num">30d Avg</th>
						<th class="col-num">TVL</th>
					</tr></thead>
					<tbody>`;
			for (const r of rates) {
				const protocolLabel = r.protocol === "aave-v3" ? "Aave V3" : (r.vaultName || "Morpho");
				const chainColor = r.chainId === "1" ? "#627eea" : "#0052ff";
				const apyColor = r.apy >= 3 ? "var(--rs-success)" : r.apy >= 1.5 ? "#ffa726" : "var(--rs-text-secondary)";
				html += `<tr>
					<td><span class="yield-protocol-badge ${r.protocol}">${protocolLabel}</span></td>
					<td><span class="yield-chain-badge" style="background:${chainColor}22;color:${chainColor}">${chainNames[r.chainId] || r.chainId}</span></td>
					<td><span class="token-symbol">${this.esc(r.asset)}</span></td>
					<td class="col-num" style="color:${apyColor};font-weight:700">${r.apy.toFixed(2)}%</td>
					<td class="col-num" style="color:var(--rs-text-secondary)">${r.apy30d != null ? r.apy30d.toFixed(2) + "%" : "-"}</td>
					<td class="col-num" style="color:var(--rs-text-secondary)">${r.tvl ? "$" + (r.tvl >= 1e9 ? (r.tvl / 1e9).toFixed(1) + "B" : (r.tvl / 1e6).toFixed(0) + "M") : "-"}</td>
				</tr>`;
			}
			html += `</tbody></table></div>`;
		}

		if (!html) {
			html = '<div class="empty">No yield data available. Yield is supported on Ethereum and Base for USDC, USDT, and DAI.</div>';
		}

		return html;
	}

	private renderBalanceTable(): string {
		const unified = this.getUnifiedBalances();
		if (unified.length === 0) return '<div class="empty">No token balances found.</div>';

		const sorted = unified
			.filter((b) => {
				const fiat = parseFloat(b.fiatBalance || "0");
				if (fiat > 0.01) return true;
				if (b.chainId === "local" || b.tokenAddress?.startsWith("crdt:")) return true;
				if (!b.tokenAddress && BigInt(b.balance || "0") > 0n) return true;
				return false;
			})
			.sort((a, b) => {
				const fiatDiff = parseFloat(b.fiatBalance || "0") - parseFloat(a.fiatBalance || "0");
				if (fiatDiff !== 0) return fiatDiff;
				return Number(BigInt(b.balance || "0") - BigInt(a.balance || "0"));
			});

		if (sorted.length === 0) return '<div class="empty">No token balances found.</div>';

		return `
			<table class="balance-table">
				<thead>
					<tr><th>Chain</th><th>Token</th><th class="amount-cell">Balance</th><th class="amount-cell">USD Value</th></tr>
				</thead>
				<tbody>
					${sorted.map((b) => {
						const color = CHAIN_COLORS[b.chainId] || (b.chainId === "local" ? "#2775ca" : "#888");
						return `
							<tr>
								<td><div class="chain-cell"><span class="chain-dot-sm" style="background:${color}"></span>${this.esc(b.chainName)}</div></td>
								<td>
									<span class="token-symbol">${this.esc(b.token?.symbol || "ETH")}</span>
									<span class="token-name">${this.esc(b.token?.name || "Ether")}</span>
								</td>
								<td class="amount-cell">${this.formatBalance(b.balance, b.token?.decimals || 18)}</td>
								<td class="amount-cell fiat">${this.formatUSD(b.fiatBalance)}</td>
							</tr>`;
					}).join("")}
				</tbody>
			</table>`;
	}

	private renderDefiPositions(): string {
		if (this.defiLoading) {
			return `<div class="defi-section"><span class="spinner"></span> Loading DeFi positions...</div>`;
		}
		if (this.defiPositions.length === 0) return "";

		// Filter by selected chain
		const filtered = this.chainFilter
			? this.defiPositions.filter(p => p.chainId === this.chainFilter)
			: this.defiPositions;
		if (filtered.length === 0) return "";

		// Group by protocol
		const byProtocol = new Map<string, Array<typeof this.defiPositions[0]>>();
		for (const p of filtered) {
			const existing = byProtocol.get(p.protocol) || [];
			existing.push(p);
			byProtocol.set(p.protocol, existing);
		}

		const TYPE_BADGES: Record<string, { label: string; color: string }> = {
			deposit: { label: "Deposit", color: "#4ade80" },
			loan: { label: "Loan", color: "#f87171" },
			staked: { label: "Staked", color: "#a78bfa" },
			locked: { label: "Locked", color: "#fbbf24" },
			reward: { label: "Reward", color: "#38bdf8" },
		};

		let cards = "";
		for (const [protocol, positions] of byProtocol) {
			const protoTotal = positions.reduce((s, p) => s + p.totalValueUSD, 0);
			const rows = positions.map(p => {
				const badge = TYPE_BADGES[p.type] || { label: p.type, color: "#888" };
				const tokenList = p.tokens.map(t =>
					`<span class="defi-token">${this.esc(t.symbol)} ${t.amount.toFixed(4)} (${this.formatUSD(String(t.valueUSD))})</span>`
				).join("");
				return `<div class="defi-position-row">
					<span class="defi-type-badge" style="background:${badge.color}20;color:${badge.color};border:1px solid ${badge.color}40">${badge.label}</span>
					<span class="defi-chain">${this.esc(p.chain)}</span>
					<span class="defi-tokens">${tokenList || this.formatUSD(String(p.totalValueUSD))}</span>
					<span class="defi-value">${this.formatUSD(String(p.totalValueUSD))}</span>
				</div>`;
			}).join("");

			cards += `
			<div class="defi-protocol-card">
				<div class="defi-protocol-header">
					<strong>${this.esc(protocol)}</strong>
					<span class="defi-proto-total">${this.formatUSD(String(protoTotal))}</span>
				</div>
				${rows}
			</div>`;
		}

		return `
		<div class="defi-section">
			<div class="defi-header">
				<h3>DeFi Positions</h3>
				<span class="defi-total">${this.formatUSD(String(filtered.reduce((s, p) => s + p.totalValueUSD, 0)))}</span>
			</div>
			${cards}
		</div>`;
	}

	private renderPaymentActions(): string {
		if (!this.isAuthenticated) return "";

		// Get cUSDC and $MYCO balances from CRDT
		const cusdcBal = this.crdtBalances.find(b => b.symbol === 'cUSDC');
		const mycoBal = this.crdtBalances.find(b => b.symbol === '$MYCO');
		const cusdcFormatted = cusdcBal ? (cusdcBal.balance / Math.pow(10, cusdcBal.decimals)).toFixed(2) : '0.00';
		const mycoFormatted = mycoBal ? (mycoBal.balance / Math.pow(10, mycoBal.decimals)).toFixed(2) : '0.00';

		return `
		<div class="payment-actions">
			<!-- Buy cUSDC Section -->
			<div class="action-card" id="buy-cusdc-card">
				<div class="action-header">
					<span class="action-icon">💵</span>
					<h4>Buy cUSDC</h4>
					<span class="action-badge">Fiat → CRDT</span>
				</div>
				<p class="action-desc">Deposit fiat via bank transfer or card. Auto-routed to cheapest provider.</p>
				<div class="provider-hints">
					<span class="provider-tag eu">EU: Mollie (SEPA/iDEAL)</span>
					<span class="provider-tag us">US: Stripe (ACH/Card)</span>
					<span class="provider-tag small">< $50: Mt Pelerin (no KYC)</span>
				</div>
				<div class="action-form" id="buy-cusdc-form" style="display:none">
					<div class="form-row">
						<label>Amount (USD)</label>
						<input type="number" id="buy-amount" min="1" step="0.01" placeholder="100.00" class="action-input">
					</div>
					<div class="form-row">
						<label>Country</label>
						<select id="buy-country" class="action-input">
							<option value="">Auto-detect</option>
							<option value="US">United States</option>
							<option value="DE">Germany</option>
							<option value="NL">Netherlands</option>
							<option value="FR">France</option>
							<option value="GB">United Kingdom</option>
							<option value="CH">Switzerland</option>
						</select>
					</div>
					<button class="action-btn primary" id="btn-buy-cusdc">Deposit via HyperSwitch</button>
					<div id="buy-cusdc-status" class="action-status"></div>
				</div>
				<button class="action-btn outline" id="btn-toggle-buy">Buy cUSDC</button>
			</div>

			<!-- $MYCO Swap Section -->
			<div class="action-card" id="myco-swap-card">
				<div class="action-header">
					<span class="action-icon">🌱</span>
					<h4>$MYCO Swap</h4>
					<span class="action-badge curve">Bonding Curve</span>
				</div>
				<p class="action-desc">Swap cUSDC ↔ $MYCO on the CRDT bonding curve. Price rises with supply.</p>
				<div class="swap-balances">
					<span>cUSDC: <strong>${cusdcFormatted}</strong></span>
					<span>$MYCO: <strong>${mycoFormatted}</strong></span>
				</div>
				<div class="action-form" id="myco-swap-form" style="display:none">
					<div class="swap-tabs">
						<button class="swap-tab active" data-swap="buy">Buy $MYCO</button>
						<button class="swap-tab" data-swap="sell">Sell $MYCO</button>
					</div>
					<div class="form-row">
						<label id="swap-input-label">cUSDC Amount</label>
						<input type="number" id="swap-amount" min="0.01" step="0.01" placeholder="10.00" class="action-input">
					</div>
					<div id="swap-quote" class="swap-quote"></div>
					<button class="action-btn primary" id="btn-swap-execute">Swap</button>
					<div id="swap-status" class="action-status"></div>
				</div>
				<button class="action-btn outline" id="btn-toggle-swap">Swap cUSDC ↔ $MYCO</button>
			</div>

			<!-- Withdraw Section -->
			<div class="action-card" id="withdraw-card">
				<div class="action-header">
					<span class="action-icon">🏦</span>
					<h4>Withdraw</h4>
					<span class="action-badge">CRDT → Fiat</span>
				</div>
				<p class="action-desc">Convert cUSDC back to fiat. Routed via SEPA (EU) or ACH (US).</p>
				<div class="action-form" id="withdraw-form" style="display:none">
					<div class="form-row">
						<label>cUSDC Amount</label>
						<input type="number" id="withdraw-amount" min="10" step="0.01" placeholder="100.00" class="action-input"
							max="${cusdcFormatted}">
					</div>
					<div class="form-row">
						<label>Bank Country</label>
						<select id="withdraw-country" class="action-input">
							<option value="US">United States (ACH ~3 days)</option>
							<option value="NL">Netherlands (SEPA ~1 day)</option>
							<option value="DE">Germany (SEPA ~1 day)</option>
							<option value="FR">France (SEPA ~1 day)</option>
						</select>
					</div>
					<div class="form-row">
						<label>IBAN / Account</label>
						<input type="text" id="withdraw-iban" placeholder="NL91ABNA0417164300" class="action-input">
					</div>
					<button class="action-btn primary" id="btn-withdraw-execute">Withdraw to Bank</button>
					<div id="withdraw-status" class="action-status"></div>
				</div>
				<button class="action-btn outline" id="btn-toggle-withdraw">Withdraw to Bank</button>
			</div>
		</div>`;
	}

	private renderDashboard(): string {
		if (!this.hasData()) return "";

		// Stats reflect current filter (all chains when no filter active)
		const allBalances = this.getUnifiedBalances();
		const totalUSD = allBalances.reduce((sum, b) => sum + parseFloat(b.fiatBalance || "0"), 0);
		const totalTokens = allBalances.filter((b) => {
			const fiat = parseFloat(b.fiatBalance || "0");
			if (fiat > 0.01) return true;
			if (b.chainId === "local" || b.tokenAddress?.startsWith("crdt:")) return true;
			if (!b.tokenAddress && BigInt(b.balance || "0") > 0n) return true;
			return false;
		}).length;

		// Compute per-chain USD totals for proportional bar
		const allBalancesForBar = this.getUnifiedBalances(true);
		const chainTotals = new Map<string, number>();
		for (const b of allBalancesForBar) {
			const cid = b.chainId;
			chainTotals.set(cid, (chainTotals.get(cid) || 0) + parseFloat(b.fiatBalance || "0"));
		}
		const grandTotal = Array.from(chainTotals.values()).reduce((s, v) => s + v, 0) || 1;

		// Build chain buttons with "All" filter
		const chainButtons = this.detectedChains.map((ch) => {
			const isActive = this.chainFilter === ch.chainId;
			const chainUSD = chainTotals.get(ch.chainId) || 0;
			const pct = grandTotal > 0 ? ((chainUSD / grandTotal) * 100).toFixed(1) : "0";
			return `
				<div class="chain-btn ${isActive ? "active" : ""}"
					data-chain="${ch.chainId}" style="--chain-color: ${ch.color}"
					title="${ch.name}: ${this.formatUSD(String(chainUSD))} (${pct}%)">
					<div class="chain-dot" style="background: ${ch.color}"></div>
					${ch.name}
				</div>`;
		}).join("");

		// Add "Local" filter button if CRDT tokens exist
		const localBtn = (this.isAuthenticated && this.crdtBalances.length > 0) ? `
			<div class="chain-btn ${this.chainFilter === "local" ? "active" : ""}"
				data-chain="local" style="--chain-color: #2775ca">
				<div class="chain-dot" style="background: #2775ca"></div>
				Local
			</div>` : "";

		// Proportional chain bar
		const barSegments = this.detectedChains.map((ch) => {
			const pct = ((chainTotals.get(ch.chainId) || 0) / grandTotal) * 100;
			if (pct < 0.5) return "";
			return `<div class="chain-bar-seg" style="width:${pct}%;background:${ch.color}" title="${ch.name}: ${pct.toFixed(1)}%"></div>`;
		}).join("");

		const isVizView = this.activeView !== "balances";

		return `
		<div class="chains">
			<div class="chain-btn ${this.chainFilter === null ? "active" : ""}"
				data-chain="all" style="--chain-color: var(--rs-accent, #14b8a6)">
				All
			</div>
			${chainButtons}
			${localBtn}
		</div>
		${barSegments ? `<div class="chain-bar">${barSegments}</div>` : ""}

		${!isVizView ? `<div class="stats">
			<div class="stat-card">
				<div class="stat-label">Total Value</div>
				<div class="stat-value">${this.formatUSD(String(totalUSD))}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Tokens</div>
				<div class="stat-value">${totalTokens}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Chains</div>
				<div class="stat-value">${this.allChainBalances.size}</div>
			</div>
			<div class="stat-card">
				<div class="stat-label">Address</div>
				<div class="stat-value" style="font-size:13px;font-family:monospace">${this.shortenAddress(this.address)}</div>
			</div>
		</div>` : ""}

		${this.activeView === "balances"
			? this.renderBalanceTable() + this.renderDefiPositions() + this.renderPaymentActions()
			: `<div class="viz-wrapper">
				<button class="details-btn" data-action="open-details">Details</button>
				<div class="viz-container" id="viz-container">
					${this.transfersLoading ? '<div class="loading"><span class="spinner"></span> Loading transfer data...</div>' : ""}
				</div>
			</div>`
		}`;
	}

	private renderCompactAddressBar(): string {
		const typeLabel = this.walletType === "safe" ? "Safe Multisig" : this.walletType === "eoa" ? "EOA" : "";
		const isWatched = this.watchedAddresses.some(w => w.address.toLowerCase() === this.address.toLowerCase());
		return `
		<div class="compact-address-bar">
			<span class="compact-addr-chip">
				<span class="compact-addr-text">${this.shortenAddress(this.address)}</span>
				${typeLabel ? `<span class="wallet-badge ${this.walletType}" style="margin-left:6px;font-size:10px;padding:2px 6px">${typeLabel}</span>` : ""}
			</span>
			${!isWatched ? `<button class="compact-btn" data-action="save-wallet" title="Save to watchlist">+ Save</button>` : ""}
			<button class="compact-btn" data-action="change-wallet">Change</button>
		</div>`;
	}

	private renderWatchlistChips(): string {
		// Client-side membrane filter (defense-in-depth)
		const visibleChipAddresses = this.watchedAddresses.filter(w => this._isVisibleTo(w.visibility, this._myRole));
		const chips = visibleChipAddresses.map(w => {
			const isActive = this.address && w.address.toLowerCase() === this.address.toLowerCase();
			const chipLock = w.visibility && w.visibility !== 'viewer' ? '<span class="membrane-lock" title="Restricted: ' + w.visibility + ' only">&#128274;</span> ' : '';
			return `<button class="watchlist-chip ${isActive ? "active" : ""}" data-watch-address="${w.address}" title="${w.address}">
				<span class="chip-label">${chipLock}${this.esc(w.label || w.address.slice(0, 8) + '...')}</span>
				<span class="chip-addr">${w.address.slice(0, 6)}...${w.address.slice(-4)}</span>
				<span class="chip-remove" data-watch-key="${w.chain}:${w.address.toLowerCase()}">&times;</span>
			</button>`;
		}).join('');

		// Show example wallets as suggestions when watchlist is empty and no wallet loaded
		const showSuggestions = visibleChipAddresses.length === 0 && !this.hasData() && !this.loading;
		const suggestions = showSuggestions ? EXAMPLE_WALLETS.map(w => `
			<button class="watchlist-chip suggested" data-address="${w.address}" title="${w.name}">
				<span class="chip-label">${w.name}</span>
				<span class="chip-addr">${w.address.slice(0, 6)}...${w.address.slice(-4)}</span>
			</button>
		`).join('') : '';

		if (!chips && !suggestions) {
			if (!this.hasData()) return `<div class="watchlist-bar"><span style="font-size:0.8rem;color:var(--rs-text-muted)">Save wallets to quickly switch between them</span></div>`;
			return '';
		}

		return `
		<div class="watchlist-bar">
			${chips}${suggestions}
			<button class="watchlist-chip add-chip" data-action="add-watch" title="Watch a new address">+</button>
		</div>`;
	}

	private renderDetailsModal(): string {
		if (!this.detailsModalOpen) return '';
		return `
		<div class="details-modal-overlay" data-action="close-details">
			<div class="details-modal" data-stop-propagation>
				<div class="details-modal-header">
					<h3>Wallet Details</h3>
					<button data-action="close-details">&times;</button>
				</div>
				<div class="details-modal-body">
					${this.renderBalanceTable()}
					${this.renderDefiPositions()}
					${this.renderTransactionTables()}
				</div>
			</div>
		</div>`;
	}

	private renderVisualizerTab(): string {
		const hasData = this.hasData();
		const showFullAddressBar = !hasData || this.addressBarExpanded;

		return `
		${this.renderHero()}

		${showFullAddressBar ? `
			<form class="address-bar" id="address-form">
				<input id="address-input" type="text" placeholder="Enter any wallet or Safe address (0x...)"
					value="${this.address}" spellcheck="false">
				<button type="submit">Load</button>
			</form>

			<div class="controls-row">
				<div class="testnet-toggle ${this.includeTestnets ? "active" : ""}" id="testnet-toggle">
					<div class="toggle-track"><div class="toggle-thumb"></div></div>
					<span>Include testnets</span>
				</div>
				<button class="view-tab" id="btn-tour" style="margin-left:auto;font-size:0.78rem;padding:4px 10px">Tour</button>
			</div>
		` : this.renderCompactAddressBar()}

		${this.renderWatchlistChips()}

		${this.renderSupportedChains()}

		${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
		${this.loading ? '<div class="loading"><span class="spinner"></span> Detecting wallet across chains...</div>' : ""}

		${!hasData && !this.loading && this.watchedAddresses.length === 0 ? this.renderFeatures() : ""}
		${this.renderDashboard()}
		${this.renderDetailsModal()}
		`;
	}

	private attachVisualizerListeners() {
		const form = this.shadow.querySelector("#address-form");
		form?.addEventListener("submit", (e) => this.handleSubmit(e));

		this.shadow.querySelector("#testnet-toggle")?.addEventListener("click", () => {
			this.includeTestnets = !this.includeTestnets;
			if (this.address) this.detectChains();
			else this.render();
		});

		this.shadow.querySelectorAll(".chain-btn").forEach((btn) => {
			btn.addEventListener("click", () => {
				const chainId = (btn as HTMLElement).dataset.chain!;
				if (chainId === "all") {
					this.chainFilter = null;
					this.balances = this.getFilteredBalances();
					this.render();
				} else {
					this.handleChainSelect(chainId);
				}
			});
		});

		// Compact address bar actions
		this.shadow.querySelector('[data-action="change-wallet"]')?.addEventListener('click', () => {
			this.addressBarExpanded = true;
			this.render();
			const input = this.shadow.querySelector("#address-input") as HTMLInputElement;
			if (input) { input.value = this.address; input.focus(); input.select(); }
		});

		this.shadow.querySelector('[data-action="save-wallet"]')?.addEventListener('click', () => {
			const label = prompt('Label (optional):') || '';
			const chain = this.selectedChain || '1';
			this.addToWatchlist(this.address, chain, label);
		});

		// Watchlist chips: click to load address
		this.shadow.querySelectorAll<HTMLElement>('.watchlist-chip[data-watch-address]').forEach(el => {
			el.addEventListener('click', (e) => {
				// Don't navigate if clicking the remove button
				if ((e.target as HTMLElement).classList.contains('chip-remove')) return;
				const addr = el.dataset.watchAddress;
				if (!addr) return;
				this.address = addr;
				const url = new URL(window.location.href);
				url.searchParams.set('address', addr);
				window.history.replaceState({}, '', url.toString());
				this.detectChains();
			});
		});

		// Watchlist chips: suggested examples
		this.shadow.querySelectorAll<HTMLElement>('.watchlist-chip.suggested[data-address]').forEach(el => {
			el.addEventListener('click', () => {
				const addr = el.dataset.address;
				if (!addr) return;
				this.address = addr;
				const url = new URL(window.location.href);
				url.searchParams.set('address', addr);
				window.history.replaceState({}, '', url.toString());
				this.detectChains();
			});
		});

		// Watchlist: remove chip
		this.shadow.querySelectorAll<HTMLElement>('.chip-remove').forEach(btn => {
			btn.addEventListener('click', (e) => {
				e.stopPropagation();
				const key = btn.dataset.watchKey;
				if (key) this.removeFromWatchlist(key);
			});
		});

		// Watchlist: add
		this.shadow.querySelector('[data-action="add-watch"]')?.addEventListener('click', () => {
			const addr = prompt('Wallet address to watch:');
			if (!addr) return;
			const label = prompt('Label (optional):') || '';
			const chain = this.selectedChain || '1';
			this.addToWatchlist(addr, chain, label);
		});

		// View tab listeners no longer needed — shell subnav handles navigation

		this.shadow.querySelector("#btn-tour")?.addEventListener("click", () => this.startTour());

		// Details modal
		this.shadow.querySelector('[data-action="open-details"]')?.addEventListener('click', () => {
			this.detailsModalOpen = true;
			this.render();
		});
		this.shadow.querySelectorAll('[data-action="close-details"]').forEach(el => {
			el.addEventListener('click', (e) => {
				// Don't close when clicking inside the modal body
				if ((e.target as HTMLElement).closest('[data-stop-propagation]') && !(e.target as HTMLElement).hasAttribute('data-action')) return;
				this.detailsModalOpen = false;
				this.render();
			});
		});

		// Sandbox listeners
		this.shadow.querySelector("[data-sandbox-toggle]")?.addEventListener("click", () => {
			this.sandboxActive = !this.sandboxActive;
			if (this.sandboxActive) this.recomputeSandbox();
			this.render();
		});
		this.shadow.querySelector("#sandbox-amount")?.addEventListener("input", (e) => {
			const val = parseFloat((e.target as HTMLInputElement).value);
			if (!isNaN(val) && val > 0) {
				this.sandboxPrincipal = val;
				this.recomputeSandbox();
				this.render();
			}
		});
		this.shadow.querySelector("#sandbox-asset")?.addEventListener("change", (e) => {
			this.sandboxAsset = (e.target as HTMLSelectElement).value as SandboxAsset;
			this.recomputeSandbox();
			this.render();
		});
		this.shadow.querySelector("#sandbox-chain")?.addEventListener("change", (e) => {
			this.sandboxChain = (e.target as HTMLSelectElement).value as "1" | "8453" | "";
			this.render();
		});
		this.shadow.querySelector("#sandbox-days")?.addEventListener("input", (e) => {
			this.sandboxDays = parseInt((e.target as HTMLInputElement).value, 10);
			this.recomputeSandbox();
			this.render();
		});

		// Yield deposit buttons
		this.shadow.querySelectorAll("[data-yield-deposit]").forEach((btn) => {
			btn.addEventListener("click", () => {
				const idx = parseInt((btn as HTMLElement).dataset.yieldDeposit!, 10);
				if (this.yieldSuggestions[idx]) {
					this.handleYieldDeposit(this.yieldSuggestions[idx]);
				}
			});
		});

		// Draw visualization if active
		if (this.activeView !== "balances" && this.hasData()) {
			requestAnimationFrame(() => this.drawActiveVisualization());
		}
	}

	private attachMyWalletsListeners() {
		// "View Flows →" buttons
		this.shadow.querySelectorAll("[data-view-in-viz]").forEach((btn) => {
			btn.addEventListener("click", () => {
				const addr = (btn as HTMLElement).dataset.viewInViz!;
				this.address = addr;
				const url = new URL(window.location.href);
				url.searchParams.set("address", addr);
				window.history.replaceState({}, "", url.toString());
				this.render();
				this.detectChains();
			});
		});

		// Link wallet
		this.shadow.querySelector("#link-wallet-btn")?.addEventListener("click", () => {
			this.startProviderDiscovery();
		});

		this.shadow.querySelector("#provider-cancel")?.addEventListener("click", () => {
			this.stopProviderDiscovery();
			this.render();
		});

		this.shadow.querySelectorAll(".provider-item").forEach((item) => {
			item.addEventListener("click", () => {
				const uuid = (item as HTMLElement).dataset.providerUuid!;
				this.handleProviderSelect(uuid);
			});
		});

		// Unlink buttons
		this.shadow.querySelectorAll("[data-unlink]").forEach((btn) => {
			btn.addEventListener("click", (e) => {
				e.stopPropagation();
				const id = (btn as HTMLElement).dataset.unlink!;
				if (confirm("Unlink this wallet?")) {
					this.handleUnlinkWallet(id);
				}
			});
		});
	}

	private _swapAction: 'buy' | 'sell' = 'buy';

	private attachPaymentListeners() {
		// Toggle form visibility
		const toggleBtn = (toggleId: string, formId: string) => {
			const btn = this.shadow.querySelector(`#${toggleId}`);
			const form = this.shadow.querySelector(`#${formId}`) as HTMLElement;
			if (btn && form) {
				btn.addEventListener('click', () => {
					const visible = form.style.display !== 'none';
					form.style.display = visible ? 'none' : 'block';
					(btn as HTMLElement).style.display = visible ? 'block' : 'none';
				});
			}
		};

		toggleBtn('btn-toggle-buy', 'buy-cusdc-form');
		toggleBtn('btn-toggle-swap', 'myco-swap-form');
		toggleBtn('btn-toggle-withdraw', 'withdraw-form');

		// Buy cUSDC
		this.shadow.querySelector('#btn-buy-cusdc')?.addEventListener('click', () => this.handleBuyCUSDC());

		// $MYCO Swap tabs
		this.shadow.querySelectorAll('.swap-tab').forEach(tab => {
			tab.addEventListener('click', () => {
				this._swapAction = (tab as HTMLElement).dataset.swap as 'buy' | 'sell';
				this.shadow.querySelectorAll('.swap-tab').forEach(t => t.classList.remove('active'));
				tab.classList.add('active');
				const label = this.shadow.querySelector('#swap-input-label');
				if (label) label.textContent = this._swapAction === 'buy' ? 'cUSDC Amount' : '$MYCO Amount';
				this.updateSwapQuote();
			});
		});

		// Live quote on input
		this.shadow.querySelector('#swap-amount')?.addEventListener('input', () => this.updateSwapQuote());

		// Execute swap
		this.shadow.querySelector('#btn-swap-execute')?.addEventListener('click', () => this.handleSwapExecute());

		// Withdraw
		this.shadow.querySelector('#btn-withdraw-execute')?.addEventListener('click', () => this.handleWithdraw());
	}

	private _quoteTimer: ReturnType<typeof setTimeout> | null = null;

	private async updateSwapQuote() {
		const input = this.shadow.querySelector('#swap-amount') as HTMLInputElement;
		const quoteEl = this.shadow.querySelector('#swap-quote');
		if (!input || !quoteEl) return;

		const amount = parseFloat(input.value);
		if (!amount || amount <= 0) {
			quoteEl.textContent = '';
			return;
		}

		// Debounce quote requests
		if (this._quoteTimer) clearTimeout(this._quoteTimer);
		this._quoteTimer = setTimeout(async () => {
			await this._fetchQuote(amount, quoteEl);
		}, 300);
	}

	private async _fetchQuote(amount: number, quoteEl: Element) {
		try {
			const base = this.getApiBase();
			const resp = await fetch(`${base}/api/crdt-tokens/myco/quote?action=${this._swapAction}&amount=${amount}`);
			const data = await resp.json();
			if (data.error) {
				quoteEl.innerHTML = `<span style="color:#f87171">${data.error}</span>`;
				return;
			}
			const outToken = this._swapAction === 'buy' ? '$MYCO' : 'cUSDC';
			const feeStr = data.fee ? ` | Fee: ${data.fee.amount.toFixed(4)} cUSDC` : '';
			quoteEl.innerHTML = `You receive: <strong>${data.output.amount.toFixed(4)} ${outToken}</strong> | Price: ${data.pricePerToken.toFixed(6)} cUSDC/${this._swapAction === 'buy' ? '$MYCO' : 'cUSDC'} | Impact: ${data.priceImpact}%${feeStr}`;
		} catch {
			quoteEl.innerHTML = '<span style="color:#f87171">Quote unavailable</span>';
		}
	}

	private async handleBuyCUSDC() {
		const amountInput = this.shadow.querySelector('#buy-amount') as HTMLInputElement;
		const countrySelect = this.shadow.querySelector('#buy-country') as HTMLSelectElement;
		const statusEl = this.shadow.querySelector('#buy-cusdc-status')!;
		const amount = parseFloat(amountInput?.value || '0');

		if (!amount || amount < 1) {
			statusEl.innerHTML = '<span class="error">Enter amount >= $1.00</span>';
			return;
		}

		statusEl.innerHTML = '<span class="loading">Creating payment...</span>';

		try {
			const resp = await fetch(`/api/onramp/hs/create-intent`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({
					amount: Math.round(amount * 100), // cents
					currency: 'usd',
					did: this.getDid(),
					label: this.getLabel(),
					billingCountry: countrySelect?.value || undefined,
				}),
			});
			const data = await resp.json();

			if (data.error) {
				statusEl.innerHTML = `<span class="error">${data.error}</span>`;
				return;
			}

			statusEl.innerHTML = `<span class="success">Payment created! Redirecting to checkout...</span>`;
			setTimeout(() => {
				statusEl.innerHTML = `<span class="success">Payment ID: ${data.paymentId}. Complete payment at pay.rspace.online</span>`;
			}, 1500);
		} catch {
			statusEl.innerHTML = `<span class="error">Failed to create payment</span>`;
		}
	}

	private async handleSwapExecute() {
		const input = this.shadow.querySelector('#swap-amount') as HTMLInputElement;
		const statusEl = this.shadow.querySelector('#swap-status')!;
		const amount = parseFloat(input?.value || '0');

		if (!amount || amount <= 0) {
			statusEl.innerHTML = '<span class="error">Enter a valid amount</span>';
			return;
		}

		statusEl.innerHTML = '<span class="loading">Processing swap...</span>';

		try {
			const base = this.getApiBase();
			const endpoint = this._swapAction === 'buy'
				? `${base}/api/crdt-tokens/myco/buy`
				: `${base}/api/crdt-tokens/myco/sell`;
			const body = this._swapAction === 'buy'
				? { cUSDCAmount: amount }
				: { mycoAmount: amount };

			const token = this.getAuthToken();
			const resp = await fetch(endpoint, {
				method: 'POST',
				headers: {
					'Content-Type': 'application/json',
					...(token ? { 'Authorization': `Bearer ${token}` } : {}),
				},
				body: JSON.stringify(body),
			});
			const data = await resp.json();

			if (data.error) {
				statusEl.innerHTML = `<span class="error">${data.error}</span>`;
				return;
			}

			statusEl.innerHTML = `<span class="success">Swapped! Received ${data.received.amount.toFixed(4)} ${data.received.token}</span>`;
			this.loadCRDTBalances();
		} catch {
			statusEl.innerHTML = `<span class="error">Swap failed</span>`;
		}
	}

	private async handleWithdraw() {
		const amountInput = this.shadow.querySelector('#withdraw-amount') as HTMLInputElement;
		const countrySelect = this.shadow.querySelector('#withdraw-country') as HTMLSelectElement;
		const ibanInput = this.shadow.querySelector('#withdraw-iban') as HTMLInputElement;
		const statusEl = this.shadow.querySelector('#withdraw-status')!;
		const amount = parseFloat(amountInput?.value || '0');

		if (!amount || amount < 10) {
			statusEl.innerHTML = '<span class="error">Minimum withdrawal: $10.00</span>';
			return;
		}
		if (!ibanInput?.value) {
			statusEl.innerHTML = '<span class="error">Enter bank account details</span>';
			return;
		}

		statusEl.innerHTML = '<span class="loading">Initiating withdrawal...</span>';

		try {
			const resp = await fetch(`/api/offramp/hs/initiate`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({
					did: this.getDid(),
					label: this.getLabel(),
					amount,
					currency: 'usd',
					bankCountry: countrySelect?.value || 'US',
					bankDetails: { iban: ibanInput.value },
				}),
			});
			const data = await resp.json();

			if (data.error) {
				statusEl.innerHTML = `<span class="error">${data.error}</span>`;
				return;
			}

			statusEl.innerHTML = `<span class="success">Withdrawal initiated! Est. arrival: ${new Date(data.estimatedArrival).toLocaleDateString()}</span>`;
			this.loadCRDTBalances();
		} catch {
			statusEl.innerHTML = `<span class="error">Withdrawal failed</span>`;
		}
	}

	private getDid(): string {
		try {
			const token = this.getAuthToken();
			if (!token) return '';
			const payload = JSON.parse(atob(token.split('.')[1]));
			return payload.did || payload.sub || '';
		} catch { return ''; }
	}

	private getLabel(): string {
		try {
			const token = this.getAuthToken();
			if (!token) return '';
			const payload = JSON.parse(atob(token.split('.')[1]));
			return payload.username || payload.name || 'Unknown';
		} catch { return 'Unknown'; }
	}

	private render() {
		this.shadow.innerHTML = `
		${this.renderStyles()}
		${this.renderVisualizerTab()}
		`;

		this.attachVisualizerListeners();
		this.attachPaymentListeners();

		this._tour.renderOverlay();
	}

	startTour() {
		this._tour.start();
	}

	private esc(s: string): string {
		const d = document.createElement("div");
		d.textContent = s || "";
		return d.innerHTML;
	}
}

customElements.define("folk-wallet-viewer", FolkWalletViewer);
