const { useState, useEffect, useMemo, useRef } = React;
const {
  CornerDownLeft, Loader2, ChevronLeft, RefreshCw, PieChart, ArrowLeftRight,
  Star, MoreVertical, Plus, Sparkles, X, Sun, Moon, Hand, ClipboardList,
  Building2, Wallet, Search, ChevronRight, Check, ArrowRightLeft, TrendingUp, ArrowUpDown, Layers, Bitcoin, Landmark, Upload, FileSpreadsheet, AlertTriangle, Download, Trash2, User, Lock, Boxes
} = LucideReact;
const Exch = ArrowLeftRight;
const { AreaChart, Area, ResponsiveContainer, YAxis } = Recharts;

// ── Haptics: iOS-like tactile feedback. Synthesises short clicks via Web Audio
//    (iOS Safari blocks the Vibration API) and also fires navigator.vibrate where supported.
const Haptics = (() => {
  let ctx = null,enabled = true;
  const ensure = () => {try {if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();if (ctx.state === "suspended") ctx.resume();} catch (e) {}return ctx;};
  const blip = (freq, dur, type, peak, when) => {
    const c = ensure();if (!c) return;const t = c.currentTime + (when || 0);
    const o = c.createOscillator(),g = c.createGain();
    o.type = type || "sine";o.frequency.setValueAtTime(freq, t);
    g.gain.setValueAtTime(0.0001, t);g.gain.exponentialRampToValueAtTime(peak, t + 0.004);g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    o.connect(g);g.connect(c.destination);o.start(t);o.stop(t + dur + 0.02);
  };
  const vibe = (p) => {try {if (navigator.vibrate) navigator.vibrate(p);} catch (e) {}};
  return {
    setEnabled(v) {enabled = v;},
    resume() {ensure();},
    tap() {if (!enabled) return;blip(540, 0.028, "sine", 0.05);vibe(8);},
    select() {if (!enabled) return;blip(440, 0.03, "triangle", 0.055);vibe(10);},
    toggle() {if (!enabled) return;blip(620, 0.03, "sine", 0.06);vibe(12);},
    success() {if (!enabled) return;blip(660, 0.09, "sine", 0.06, 0);blip(990, 0.12, "sine", 0.055, 0.08);vibe([12, 36, 14]);},
    warn() {if (!enabled) return;blip(180, 0.13, "square", 0.05, 0);blip(150, 0.16, "square", 0.045, 0.09);vibe([18, 40, 18]);}
  };
})();

/* Portfolio tracker — demo. Architecture mirrors the iOS 26 native build. */

const THEME = {
  dark: { bg: "#0A0B0D", panel: "#121315", panel2: "#1B1D20", line: "#26292F", ink: "#F4F5F6", mute: "#8A8F98", faint: "#565B63", accent: "#5B9DFF", accentInk: "#5B9DFF", accentSoft: "rgba(91,157,255,0.16)", up: "#34C77B", down: "#E5564B", glassBg: "rgba(28,30,34,0.92)", glassBorder: "rgba(255,255,255,0.10)", page: "#000000", sideL: "#0C0D10", sideR: "#0C1119" },
  light: { bg: "#F2F3F5", panel: "#FFFFFF", panel2: "#EAECEF", line: "#E2E4E8", ink: "#15171B", mute: "#6B7280", faint: "#9AA0A8", accent: "#2F6BED", accentInk: "#2563EB", accentSoft: "rgba(47,107,237,0.12)", up: "#1E9E5A", down: "#D33A2C", glassBg: "rgba(255,255,255,0.94)", glassBorder: "rgba(0,0,0,0.08)", page: "#FFFFFF", sideL: "#FFFFFF", sideR: "#E9EEF7" }
};

// ── UI versions ── each re-skins the whole app (fonts + palette); 3 of them drop the Insights performance graph.
const SANS = 'Inter, ui-sans-serif, system-ui, -apple-system, sans-serif';
const VARIANTS = {
  draft: {
    label: "v0.1", note: "Baseline iOS concept", allowThemeToggle: true, hidePerf: false,
    body: SANS, head: SANS, mono: SANS, colors: null, css: ""
  },
  editorial: {
    label: "Editorial", note: "Print-led · serif", allowThemeToggle: true, hidePerf: true,
    body: SANS, head: SANS, mono: SANS,
    colors: {
      light: { bg: "#FFFFFF", panel: "#FFFFFF", panel2: "#F2EFE9", line: "#E9E5DD", ink: "#1C1B19", mute: "#6F6D67", faint: "#9C9A93", accent: "#136F63", accentInk: "#136F63", accentSoft: "rgba(19,111,99,0.10)", up: "#18A35A", down: "#D9442C", glassBg: "rgba(253,252,250,0.90)", glassBorder: "rgba(28,27,25,0.10)", page: "#F3ECE0", sideL: "#F3ECE0", sideR: "#EBE2D2" },
      dark: { bg: "#14130F", panel: "#1C1B16", panel2: "#23211A", line: "#302D24", ink: "#F1ECE1", mute: "#A39C8E", faint: "#6E685B", accent: "#3FB68C", accentInk: "#4FC79B", accentSoft: "rgba(63,182,140,0.16)", up: "#4FC79B", down: "#E8705C", glassBg: "rgba(28,27,22,0.86)", glassBorder: "rgba(255,255,255,0.10)", page: "#0D0C09", sideL: "#0D0C09", sideR: "#14120D" }
    },
    css: ".pt-editorial h1,.pt-editorial h2,.pt-editorial .pt-hero{font-weight:600;letter-spacing:-0.015em}.pt-editorial h1{font-size:1.45rem;line-height:1.1}"
  },
  wsb: {
    label: "WallStreetBets", note: "Color wash · loud", allowThemeToggle: true, hidePerf: true,
    body: SANS, head: SANS, mono: SANS,
    colors: {
      dark: { bg: "#070D09", panel: "rgba(255,255,255,0.05)", panel2: "rgba(255,255,255,0.08)", line: "rgba(255,255,255,0.10)", ink: "#EDF4EF", mute: "#9BB0A2", faint: "#6E8276", accent: "#3DDC6A", accentInk: "#4DE87A", accentSoft: "rgba(61,220,106,0.16)", up: "#3DDC6A", down: "#FF5247", glassBg: "rgba(20,28,22,0.55)", glassBorder: "rgba(255,255,255,0.14)", page: "#070D09", sideL: "#070D09", sideR: "#070D09" },
      light: { bg: "#FFFFFF", panel: "#FFFFFF", panel2: "#F1F6F2", line: "#E5EBE7", ink: "#0C1A12", mute: "#5B7366", faint: "#94A99C", accent: "#15A34A", accentInk: "#0F9D43", accentSoft: "rgba(21,163,74,0.12)", up: "#15A34A", down: "#E5483B", glassBg: "rgba(255,255,255,0.80)", glassBorder: "rgba(0,0,0,0.08)", page: "#FFFFFF", sideL: "#FFFFFF", sideR: "#F1F6F2" }
    },
    css: ".pt-wsb h1,.pt-wsb h2{letter-spacing:0.01em;font-weight:800}.pt-wsb .pt-hero{font-weight:800;letter-spacing:-0.03em}"
  },
  data: {
    label: "Data", note: "Terminal · monospace", allowThemeToggle: true, hidePerf: true,
    body: SANS, head: SANS, mono: SANS,
    colors: {
      dark: { bg: "#0A0B0D", panel: "#0F1115", panel2: "#161A20", line: "#232A35", ink: "#D7DEE8", mute: "#7E8896", faint: "#525C6A", accent: "#E8A33D", accentInk: "#F0B44E", accentSoft: "rgba(232,163,61,0.14)", up: "#36C28B", down: "#E5564B", glassBg: "rgba(15,17,21,0.94)", glassBorder: "rgba(232,163,61,0.18)", page: "#050608", sideL: "#08090C", sideR: "#08090C" },
      light: { bg: "#FAFAF6", panel: "#FFFFFF", panel2: "#EFEFE9", line: "#E2E2DA", ink: "#1A1B1E", mute: "#6B6F77", faint: "#9CA0A6", accent: "#B7791F", accentInk: "#996310", accentSoft: "rgba(183,121,31,0.12)", up: "#1F9E5A", down: "#D33A2C", glassBg: "rgba(255,255,255,0.92)", glassBorder: "rgba(0,0,0,0.08)", page: "#FFFFFF", sideL: "#FAFAF6", sideR: "#EFEFE9" }
    },
    css: ".pt-data h1,.pt-data h2{letter-spacing:0.04em;font-weight:600;font-size:1rem}.pt-data .pt-hero{letter-spacing:-0.02em}"
  }
};

const FX = { EUR: 1, USD: 0.92, CHF: 1.04, JPY: 0.0061, GBP: 1.17, CAD: 0.67, AUD: 0.61, SGD: 0.69, DKK: 0.134, NOK: 0.086 };
const PAIRS = ["USD", "GBP", "JPY", "CHF", "CAD", "AUD", "SGD"];
const COLORS = { BTC: "#F7931A", ETH: "#627EEA", SOL: "#14B88A", BNB: "#F3BA2F", XRP: "#23292F", ADA: "#0033AD", DOGE: "#C2A633", AVAX: "#E84142", DOT: "#E6007A", LINK: "#2A5ADA", AAPL: "#3A3A3C", MSFT: "#2F7D32", NVDA: "#76B900", GOOGL: "#4285F4", AMZN: "#FF9900", META: "#0866FF", TSLA: "#E31937", SWDA: "#2D9CDB", CSSPX: "#0B5FAE", VWCE: "#C0392B", UST10Y: "#5B6B9E", UST2Y: "#6B7AA8", UST5Y: "#4F5E91", UST30Y: "#46568C", TIPS10Y: "#7180B0", TBILL3M: "#5E6CA0", BTP10Y: "#5B6B9E", BUND10Y: "#56669A", OAT10Y: "#6675AB", GILT10Y: "#4D5D90", JGB10Y: "#717FB2", AAPL27: "#5A6A9C", MSFT29: "#525F8F", ENEL30: "#6370A6", "EUR.CASH": "#44705C", "USD.CASH": "#4F6E8C", "CHF.CASH": "#8C5A5A", "GBP.CASH": "#6E5A8C" };
const DARKTEXT = new Set(["BNB", "DOGE", "USDC", "USDT", "DAI"]);

const CRYPTO = [
["BTC", "Bitcoin", 68250], ["ETH", "Ethereum", 3520], ["USDT", "Tether", 1.0], ["BNB", "BNB", 584], ["SOL", "Solana", 152.4], ["XRP", "XRP", 0.56], ["USDC", "USD Coin", 1.0], ["ADA", "Cardano", 0.46], ["DOGE", "Dogecoin", 0.13], ["AVAX", "Avalanche", 28.9], ["TRX", "TRON", 0.13], ["LINK", "Chainlink", 14.6], ["DOT", "Polkadot", 6.1], ["MATIC", "Polygon", 0.6], ["TON", "Toncoin", 5.4], ["SHIB", "Shiba Inu", 0.000018], ["LTC", "Litecoin", 74.8], ["BCH", "Bitcoin Cash", 418], ["UNI", "Uniswap", 9.2], ["ICP", "Internet Computer", 11.2], ["DAI", "Dai", 1.0], ["ETC", "Ethereum Classic", 24.1], ["XLM", "Stellar", 0.11], ["NEAR", "NEAR Protocol", 5.8], ["APT", "Aptos", 8.5], ["INJ", "Injective", 22.4], ["OP", "Optimism", 1.9], ["ARB", "Arbitrum", 0.82], ["FIL", "Filecoin", 4.5], ["IMX", "Immutable", 1.6], ["HBAR", "Hedera", 0.082], ["VET", "VeChain", 0.031], ["MKR", "Maker", 1480], ["RNDR", "Render", 7.5], ["GRT", "The Graph", 0.18], ["AAVE", "Aave", 96], ["ALGO", "Algorand", 0.16], ["QNT", "Quant", 94], ["FTM", "Fantom", 0.71], ["SAND", "The Sandbox", 0.41], ["MANA", "Decentraland", 0.39], ["AXS", "Axie Infinity", 6.1], ["THETA", "Theta", 1.48], ["EGLD", "MultiversX", 35], ["XTZ", "Tezos", 0.92], ["CHZ", "Chiliz", 0.078], ["EOS", "EOS", 0.61], ["MINA", "Mina", 0.58], ["GALA", "Gala", 0.028], ["ZEC", "Zcash", 28], ["CRV", "Curve DAO", 0.42], ["LDO", "Lido DAO", 1.85], ["SUI", "Sui", 1.12], ["SEI", "Sei", 0.41], ["PEPE", "Pepe", 0.0000112], ["WIF", "dogwifhat", 2.3], ["BONK", "Bonk", 0.000021], ["JUP", "Jupiter", 0.91], ["TIA", "Celestia", 6.2], ["STX", "Stacks", 1.78], ["RUNE", "THORChain", 4.5], ["FET", "Fetch.ai", 1.32], ["ENA", "Ethena", 0.72], ["WLD", "Worldcoin", 2.4], ["ONDO", "Ondo", 1.1], ["PYTH", "Pyth Network", 0.34], ["JTO", "Jito", 2.1], ["BLUR", "Blur", 0.18], ["ENS", "Ethereum Name Service", 26], ["DYDX", "dYdX", 1.3], ["GMX", "GMX", 28], ["SNX", "Synthetix", 2.1], ["COMP", "Compound", 52], ["1INCH", "1inch", 0.35], ["CAKE", "PancakeSwap", 2.4], ["FLOW", "Flow", 0.62], ["ROSE", "Oasis Network", 0.09], ["KAVA", "Kava", 0.42], ["ZIL", "Zilliqa", 0.018], ["CELO", "Celo", 0.62], ["KSM", "Kusama", 27], ["NEO", "Neo", 11.2], ["IOTA", "IOTA", 0.18], ["DASH", "Dash", 26], ["XMR", "Monero", 168], ["PENDLE", "Pendle", 4.2], ["RPL", "Rocket Pool", 12.8], ["FXS", "Frax Share", 2.6], ["BAL", "Balancer", 2.9], ["YFI", "Yearn Finance", 6800], ["SUSHI", "SushiSwap", 0.82], ["LRC", "Loopring", 0.16], ["ENJ", "Enjin Coin", 0.18], ["BAT", "Basic Attention", 0.21], ["ZRX", "0x Protocol", 0.36], ["FLOKI", "Floki", 0.00018], ["ORDI", "Ordinals", 32], ["AKT", "Akash Network", 3.1], ["AR", "Arweave", 22], ["JASMY", "JasmyCoin", 0.022], ["GMT", "STEPN", 0.13],
["TON", "Toncoin", 5.4], ["HBAR", "Hedera", 0.07], ["ICP", "Internet Computer", 11.5], ["IMX", "Immutable", 1.4], ["INJ", "Injective", 22], ["TIA", "Celestia", 6.2], ["SEI", "Sei", 0.4], ["SUI", "Sui", 1.1], ["RUNE", "THORChain", 4.8], ["AAVE", "Aave", 98], ["MKR", "Maker", 2400], ["LDO", "Lido DAO", 1.6], ["QNT", "Quant", 95], ["EGLD", "MultiversX", 32], ["CHZ", "Chiliz", 0.08], ["GALA", "Gala", 0.025], ["SAND", "The Sandbox", 0.32], ["MANA", "Decentraland", 0.36], ["AXS", "Axie Infinity", 5.2], ["APE", "ApeCoin", 0.9], ["XTZ", "Tezos", 0.78], ["EOS", "EOS", 0.62], ["MINA", "Mina", 0.5], ["NEXO", "Nexo", 1.1], ["KAS", "Kaspa", 0.12], ["BONK", "Bonk", 0.000022], ["WIF", "dogwifhat", 1.8], ["PEPE", "Pepe", 0.0000095], ["TRX", "TRON", 0.13], ["TWT", "Trust Wallet", 1.1], ["CFX", "Conflux", 0.16], ["WOO", "WOO Network", 0.18], ["GRT", "The Graph", 0.18], ["LPT", "Livepeer", 13], ["ANKR", "Ankr", 0.03], ["SKL", "SKALE", 0.04], ["AGIX", "SingularityNET", 0.4], ["RNDR", "Render", 6.8]];

const STOCKS = [
["AAPL", "Apple Inc.", 228.5], ["MSFT", "Microsoft", 432], ["NVDA", "NVIDIA", 134.8], ["GOOGL", "Alphabet", 176], ["AMZN", "Amazon", 186], ["META", "Meta Platforms", 502], ["TSLA", "Tesla", 248], ["AVGO", "Broadcom", 165], ["BRK.B", "Berkshire Hathaway", 415, "USD", "NYSE"], ["LLY", "Eli Lilly", 820, "USD", "NYSE"], ["JPM", "JPMorgan Chase", 205, "USD", "NYSE"], ["V", "Visa", 285, "USD", "NYSE"], ["XOM", "Exxon Mobil", 112, "USD", "NYSE"], ["UNH", "UnitedHealth", 490, "USD", "NYSE"], ["MA", "Mastercard", 460, "USD", "NYSE"], ["JNJ", "Johnson & Johnson", 152, "USD", "NYSE"], ["PG", "Procter & Gamble", 168, "USD", "NYSE"], ["HD", "Home Depot", 360, "USD", "NYSE"], ["COST", "Costco", 870], ["ORCL", "Oracle", 142, "USD", "NYSE"], ["MRK", "Merck", 125, "USD", "NYSE"], ["ABBV", "AbbVie", 175, "USD", "NYSE"], ["CVX", "Chevron", 158, "USD", "NYSE"], ["KO", "Coca-Cola", 62, "USD", "NYSE"], ["PEP", "PepsiCo", 168], ["ADBE", "Adobe", 480], ["CRM", "Salesforce", 255, "USD", "NYSE"], ["NFLX", "Netflix", 680], ["AMD", "AMD", 158], ["WMT", "Walmart", 68, "USD", "NYSE"], ["DIS", "Walt Disney", 100, "USD", "NYSE"], ["INTC", "Intel", 31], ["CSCO", "Cisco", 48], ["MCD", "McDonald's", 260, "USD", "NYSE"], ["QCOM", "Qualcomm", 170], ["TXN", "Texas Instruments", 196], ["PFE", "Pfizer", 28, "USD", "NYSE"], ["NKE", "Nike", 78, "USD", "NYSE"], ["PYPL", "PayPal", 68], ["INTU", "Intuit", 620], ["BA", "Boeing", 180, "USD", "NYSE"], ["UBER", "Uber", 70, "USD", "NYSE"],
["ASML.AS", "ASML Holding", 912, "EUR", "Euronext A'dam"], ["MC.PA", "LVMH", 702, "EUR", "Euronext Paris"], ["OR.PA", "L'Oréal", 380, "EUR", "Euronext Paris"], ["SAP.DE", "SAP", 195, "EUR", "Xetra"], ["SIE.DE", "Siemens", 175, "EUR", "Xetra"], ["ALV.DE", "Allianz", 290, "EUR", "Xetra"], ["AIR.PA", "Airbus", 158, "EUR", "Euronext Paris"], ["SAN.PA", "Sanofi", 95, "EUR", "Euronext Paris"], ["TTE.PA", "TotalEnergies", 62, "EUR", "Euronext Paris"], ["ENEL.MI", "Enel", 6.78, "EUR", "Borsa Italiana"], ["ENI.MI", "Eni", 14.2, "EUR", "Borsa Italiana"], ["ISP.MI", "Intesa Sanpaolo", 3.92, "EUR", "Borsa Italiana"], ["UCG.MI", "UniCredit", 38, "EUR", "Borsa Italiana"], ["RACE.MI", "Ferrari", 398, "EUR", "Borsa Italiana"], ["STLAM.MI", "Stellantis", 12.1, "EUR", "Borsa Italiana"], ["G.MI", "Generali", 26, "EUR", "Borsa Italiana"],
["AZN.L", "AstraZeneca", 110, "GBP", "London"], ["SHEL.L", "Shell", 27, "GBP", "London"],
["APC.DE", "Apple Inc.", 210, "EUR", "Xetra"], ["MSF.DE", "Microsoft", 398, "EUR", "Xetra"], ["NVD.DE", "NVIDIA", 124, "EUR", "Xetra"],
["COIN", "Coinbase", 245, "USD", "NASDAQ"], ["PLTR", "Palantir", 38], ["SHOP", "Shopify", 78, "USD", "NYSE"], ["SNOW", "Snowflake", 128], ["NOW", "ServiceNow", 920, "USD", "NYSE"], ["MU", "Micron", 102], ["PANW", "Palo Alto Networks", 360], ["CRWD", "CrowdStrike", 340], ["DDOG", "Datadog", 122], ["ABNB", "Airbnb", 130], ["BKNG", "Booking Holdings", 4100], ["SBUX", "Starbucks", 96, "USD", "NYSE"], ["GILD", "Gilead Sciences", 78], ["AMGN", "Amgen", 320], ["BMY", "Bristol-Myers Squibb", 52, "USD", "NYSE"], ["T", "AT&T", 22, "USD", "NYSE"], ["VZ", "Verizon", 42, "USD", "NYSE"], ["CMCSA", "Comcast", 40], ["GE", "GE Aerospace", 178, "USD", "NYSE"], ["CAT", "Caterpillar", 360, "USD", "NYSE"], ["DE", "Deere & Co", 410, "USD", "NYSE"], ["HON", "Honeywell", 215], ["UPS", "United Parcel Service", 130, "USD", "NYSE"], ["GS", "Goldman Sachs", 510, "USD", "NYSE"], ["MS", "Morgan Stanley", 105, "USD", "NYSE"], ["BAC", "Bank of America", 42, "USD", "NYSE"], ["WFC", "Wells Fargo", 62, "USD", "NYSE"], ["C", "Citigroup", 65, "USD", "NYSE"], ["AXP", "American Express", 270, "USD", "NYSE"], ["BLK", "BlackRock", 880, "USD", "NYSE"], ["SPGI", "S&P Global", 510, "USD", "NYSE"], ["NEE", "NextEra Energy", 78, "USD", "NYSE"], ["LIN", "Linde", 470], ["F", "Ford Motor", 11, "USD", "NYSE"], ["GM", "General Motors", 48, "USD", "NYSE"], ["RIVN", "Rivian", 13], ["MRNA", "Moderna", 78], ["REGN", "Regeneron", 1050], ["VRTX", "Vertex Pharma", 480], ["ISRG", "Intuitive Surgical", 480], ["MDT", "Medtronic", 86, "USD", "NYSE"], ["TMO", "Thermo Fisher", 580, "USD", "NYSE"], ["DHR", "Danaher", 250, "USD", "NYSE"], ["LOW", "Lowe's", 250, "USD", "NYSE"], ["TGT", "Target", 150, "USD", "NYSE"], ["MDLZ", "Mondelez", 70], ["CL", "Colgate-Palmolive", 98, "USD", "NYSE"], ["MMM", "3M", 130, "USD", "NYSE"], ["LMT", "Lockheed Martin", 460, "USD", "NYSE"], ["RTX", "RTX Corp", 118, "USD", "NYSE"], ["BX", "Blackstone", 138, "USD", "NYSE"],
["TSM", "Taiwan Semiconductor", 175, "USD", "NYSE"], ["BABA", "Alibaba", 82, "USD", "NYSE"], ["TM", "Toyota Motor", 185, "USD", "NYSE"], ["SONY", "Sony Group", 92, "USD", "NYSE"], ["PDD", "PDD Holdings", 130, "USD", "NASDAQ"], ["NIO", "NIO", 5.2, "USD", "NYSE"], ["INFY", "Infosys", 20, "USD", "NYSE"],
["ADYEN.AS", "Adyen", 1380, "EUR", "Euronext A'dam"], ["PRX.AS", "Prosus", 38, "EUR", "Euronext A'dam"], ["DTE.DE", "Deutsche Telekom", 27, "EUR", "Xetra"], ["BAS.DE", "BASF", 46, "EUR", "Xetra"], ["BAYN.DE", "Bayer", 28, "EUR", "Xetra"], ["BMW.DE", "BMW", 78, "EUR", "Xetra"], ["MBG.DE", "Mercedes-Benz", 62, "EUR", "Xetra"], ["VOW3.DE", "Volkswagen", 92, "EUR", "Xetra"], ["IFX.DE", "Infineon", 32, "EUR", "Xetra"], ["DBK.DE", "Deutsche Bank", 16, "EUR", "Xetra"], ["BNP.PA", "BNP Paribas", 62, "EUR", "Euronext Paris"], ["AI.PA", "Air Liquide", 162, "EUR", "Euronext Paris"], ["SU.PA", "Schneider Electric", 228, "EUR", "Euronext Paris"], ["DG.PA", "Vinci", 102, "EUR", "Euronext Paris"], ["EL.PA", "EssilorLuxottica", 215, "EUR", "Euronext Paris"], ["KER.PA", "Kering", 245, "EUR", "Euronext Paris"], ["RMS.PA", "Hermès", 2150, "EUR", "Euronext Paris"], ["SAN.MC", "Banco Santander", 4.5, "EUR", "BME"], ["BBVA.MC", "BBVA", 9.4, "EUR", "BME"], ["ITX.MC", "Inditex", 48, "EUR", "BME"], ["IBE.MC", "Iberdrola", 13, "EUR", "BME"], ["NESN.SW", "Nestlé", 92, "CHF", "SIX"], ["ROG.SW", "Roche", 248, "CHF", "SIX"], ["NOVN.SW", "Novartis", 96, "CHF", "SIX"], ["ABBN.SW", "ABB", 48, "CHF", "SIX"], ["NOVO-B.CO", "Novo Nordisk", 720, "DKK", "Copenhagen"], ["EQNR.OL", "Equinor", 270, "NOK", "Oslo"],
["MONC.MI", "Moncler", 58, "EUR", "Borsa Italiana"], ["LDO.MI", "Leonardo", 22, "EUR", "Borsa Italiana"], ["TRN.MI", "Terna", 7.8, "EUR", "Borsa Italiana"], ["SRG.MI", "Snam", 4.6, "EUR", "Borsa Italiana"], ["BMED.MI", "Banca Mediolanum", 11, "EUR", "Borsa Italiana"], ["PIRC.MI", "Pirelli", 5.4, "EUR", "Borsa Italiana"],
["HSBA.L", "HSBC Holdings", 6.9, "GBP", "London"], ["BP.L", "BP", 3.8, "GBP", "London"], ["ULVR.L", "Unilever", 48, "GBP", "London"], ["GSK.L", "GSK", 14.5, "GBP", "London"], ["RIO.L", "Rio Tinto", 49, "GBP", "London"], ["DGE.L", "Diageo", 25, "GBP", "London"], ["BARC.L", "Barclays", 2.3, "GBP", "London"], ["LLOY.L", "Lloyds Banking", 0.58, "GBP", "London"], ["VOD.L", "Vodafone", 0.70, "GBP", "London"], ["REL.L", "RELX", 36, "GBP", "London"],
["ABBV", "AbbVie", 178], ["ACN", "Accenture", 330, "USD", "NYSE"], ["ADBE", "Adobe", 560], ["ADI", "Analog Devices", 225], ["ADP", "Automatic Data Proc.", 245], ["AEP", "American Electric Power", 96], ["AMAT", "Applied Materials", 205], ["AMD", "Advanced Micro Devices", 165], ["AMT", "American Tower", 195, "USD", "NYSE"], ["ANET", "Arista Networks", 330, "USD", "NYSE"], ["AVGO", "Broadcom", 165], ["AZO", "AutoZone", 3100, "USD", "NYSE"], ["BDX", "Becton Dickinson", 230, "USD", "NYSE"], ["BIIB", "Biogen", 215], ["BKR", "Baker Hughes", 37], ["BSX", "Boston Scientific", 78, "USD", "NYSE"], ["CB", "Chubb", 270, "USD", "NYSE"], ["CCI", "Crown Castle", 105, "USD", "NYSE"], ["CDNS", "Cadence Design", 300], ["CDW", "CDW Corp", 225], ["CEG", "Constellation Energy", 215], ["CHTR", "Charter Comm.", 360], ["CI", "Cigna", 340, "USD", "NYSE"], ["CME", "CME Group", 215], ["CMG", "Chipotle", 58], ["COF", "Capital One", 145, "USD", "NYSE"], ["COP", "ConocoPhillips", 112, "USD", "NYSE"], ["COST", "Costco", 880], ["CSCO", "Cisco", 52], ["CSX", "CSX Corp", 34], ["CTAS", "Cintas", 780], ["CTSH", "Cognizant", 78], ["CVS", "CVS Health", 62, "USD", "NYSE"], ["CVX", "Chevron", 155, "USD", "NYSE"], ["D", "Dominion Energy", 54, "USD", "NYSE"], ["DAL", "Delta Air Lines", 52, "USD", "NYSE"], ["DELL", "Dell Technologies", 118, "USD", "NYSE"], ["DIS", "Walt Disney", 98, "USD", "NYSE"], ["DLR", "Digital Realty", 150, "USD", "NYSE"], ["DOW", "Dow Inc.", 52, "USD", "NYSE"], ["DUK", "Duke Energy", 105, "USD", "NYSE"], ["DXCM", "Dexcom", 78], ["EA", "Electronic Arts", 145], ["EBAY", "eBay", 58], ["ECL", "Ecolab", 235, "USD", "NYSE"], ["EOG", "EOG Resources", 125, "USD", "NYSE"], ["EQIX", "Equinix", 820], ["EW", "Edwards Lifesciences", 68, "USD", "NYSE"], ["EXC", "Exelon", 38], ["FAST", "Fastenal", 70], ["FDX", "FedEx", 270, "USD", "NYSE"], ["FI", "Fiserv", 165, "USD", "NYSE"], ["FTNT", "Fortinet", 62], ["GD", "General Dynamics", 290, "USD", "NYSE"], ["GEHC", "GE HealthCare", 85], ["GIS", "General Mills", 68, "USD", "NYSE"], ["GLW", "Corning", 38, "USD", "NYSE"], ["HCA", "HCA Healthcare", 340, "USD", "NYSE"], ["HUM", "Humana", 320, "USD", "NYSE"], ["IBM", "IBM", 195, "USD", "NYSE"], ["ICE", "Intercontinental Exch.", 145, "USD", "NYSE"], ["IDXX", "IDEXX Labs", 490], ["ILMN", "Illumina", 120], ["INTC", "Intel", 22], ["INTU", "Intuit", 640], ["ISRG", "Intuitive Surgical", 480], ["ITW", "Illinois Tool Works", 250, "USD", "NYSE"], ["JCI", "Johnson Controls", 72, "USD", "NYSE"], ["KDP", "Keurig Dr Pepper", 33], ["KHC", "Kraft Heinz", 32], ["KKR", "KKR & Co", 105, "USD", "NYSE"], ["KLAC", "KLA Corp", 720], ["KMB", "Kimberly-Clark", 135, "USD", "NYSE"], ["KMI", "Kinder Morgan", 22, "USD", "NYSE"], ["KO", "Coca-Cola", 62, "USD", "NYSE"], ["KR", "Kroger", 58, "USD", "NYSE"], ["LRCX", "Lam Research", 950], ["LULU", "Lululemon", 320], ["MAR", "Marriott", 250], ["MCD", "McDonald's", 295, "USD", "NYSE"], ["MCHP", "Microchip", 82], ["MCK", "McKesson", 580, "USD", "NYSE"], ["MCO", "Moody's", 430, "USD", "NYSE"], ["MDLZ", "Mondelez", 70], ["MELI", "MercadoLibre", 1900], ["MET", "MetLife", 78, "USD", "NYSE"], ["META", "Meta Platforms", 505], ["MMC", "Marsh & McLennan", 215, "USD", "NYSE"], ["MNST", "Monster Beverage", 52], ["MO", "Altria", 48, "USD", "NYSE"], ["MPC", "Marathon Petroleum", 165, "USD", "NYSE"], ["MRK", "Merck", 125, "USD", "NYSE"], ["MRVL", "Marvell", 72], ["MSCI", "MSCI Inc.", 560, "USD", "NYSE"], ["MSI", "Motorola Solutions", 430, "USD", "NYSE"], ["NFLX", "Netflix", 680], ["NKE", "Nike", 78, "USD", "NYSE"], ["NOC", "Northrop Grumman", 480, "USD", "NYSE"], ["NSC", "Norfolk Southern", 240, "USD", "NYSE"], ["NUE", "Nucor", 165, "USD", "NYSE"], ["NXPI", "NXP Semiconductors", 235], ["ODFL", "Old Dominion", 195], ["ORCL", "Oracle", 145, "USD", "NYSE"], ["ORLY", "O'Reilly Auto", 1150], ["OXY", "Occidental Petroleum", 58, "USD", "NYSE"], ["PCAR", "PACCAR", 105], ["PEP", "PepsiCo", 170], ["PFE", "Pfizer", 28, "USD", "NYSE"], ["PG", "Procter & Gamble", 165, "USD", "NYSE"], ["PGR", "Progressive", 235, "USD", "NYSE"], ["PH", "Parker-Hannifin", 620, "USD", "NYSE"], ["PLD", "Prologis", 115, "USD", "NYSE"], ["PM", "Philip Morris", 105, "USD", "NYSE"], ["PSA", "Public Storage", 310, "USD", "NYSE"], ["PSX", "Phillips 66", 140, "USD", "NYSE"], ["PYPL", "PayPal", 68], ["QCOM", "Qualcomm", 170], ["ROP", "Roper Technologies", 560], ["ROST", "Ross Stores", 145], ["SBAC", "SBA Communications", 215], ["SCHW", "Charles Schwab", 72, "USD", "NYSE"], ["SLB", "Schlumberger", 48, "USD", "NYSE"], ["SO", "Southern Company", 82, "USD", "NYSE"], ["SPG", "Simon Property", 155, "USD", "NYSE"], ["SRE", "Sempra", 75, "USD", "NYSE"], ["STZ", "Constellation Brands", 250, "USD", "NYSE"], ["SYK", "Stryker", 350, "USD", "NYSE"], ["SYY", "Sysco", 78, "USD", "NYSE"], ["TDG", "TransDigm", 1300, "USD", "NYSE"], ["TEL", "TE Connectivity", 150, "USD", "NYSE"], ["TFC", "Truist Financial", 42, "USD", "NYSE"], ["TJX", "TJX Companies", 115, "USD", "NYSE"], ["TMUS", "T-Mobile US", 195], ["TT", "Trane Technologies", 330, "USD", "NYSE"], ["TXN", "Texas Instruments", 195], ["UNH", "UnitedHealth", 490, "USD", "NYSE"], ["UNP", "Union Pacific", 240, "USD", "NYSE"], ["USB", "U.S. Bancorp", 45, "USD", "NYSE"], ["V", "Visa", 280, "USD", "NYSE"], ["VLO", "Valero Energy", 135, "USD", "NYSE"], ["VRSK", "Verisk Analytics", 265], ["WBA", "Walgreens", 12], ["WBD", "Warner Bros. Discovery", 8], ["WDAY", "Workday", 240], ["WELL", "Welltower", 105, "USD", "NYSE"], ["WM", "Waste Management", 210, "USD", "NYSE"], ["WMB", "Williams Cos.", 42, "USD", "NYSE"], ["WMT", "Walmart", 68, "USD", "NYSE"], ["XEL", "Xcel Energy", 58], ["ZTS", "Zoetis", 175, "USD", "NYSE"]];

const ETF = [
["SWDA.MI", "iShares Core MSCI World", 121.13], ["CSSPX.MI", "iShares Core S&P 500", 684], ["VWCE.MI", "Vanguard FTSE All-World", 128.4], ["IWDA.AS", "iShares Core MSCI World", 110.2, "EUR", "Euronext A'dam"], ["EUNL.DE", "iShares Core MSCI World", 105.6, "EUR", "Xetra"], ["SXR8.DE", "iShares Core S&P 500", 560, "EUR", "Xetra"], ["VUSA.MI", "Vanguard S&P 500", 105], ["EIMI.MI", "iShares Core EM IMI", 35.1], ["AGGH.MI", "iShares Global Aggregate Bond", 4.92], ["VWRL.AS", "Vanguard FTSE All-World dist", 119.8, "EUR", "Euronext A'dam"], ["EQQQ.MI", "Invesco Nasdaq-100", 420], ["SXRV.DE", "iShares Nasdaq 100", 720, "EUR", "Xetra"], ["XDWD.DE", "Xtrackers MSCI World", 105, "EUR", "Xetra"], ["IUSQ.DE", "iShares MSCI ACWI", 88, "EUR", "Xetra"], ["SGLN.MI", "iShares Physical Gold", 48], ["WSML.MI", "iShares MSCI World Small Cap", 8.1], ["INRG.MI", "iShares Global Clean Energy", 6.2], ["VFEM.MI", "Vanguard FTSE Emerging Mkts", 55], ["VHYL.AS", "Vanguard All-World High Div", 62, "EUR", "Euronext A'dam"], ["SUSW.MI", "iShares MSCI World SRI", 30], ["JEGP.MI", "JPM Global Equity Income", 28], ["VAGF.MI", "Vanguard Global Aggregate Bond", 24], ["EXSA.DE", "iShares STOXX Europe 600", 50, "EUR", "Xetra"], ["MEUD.PA", "Amundi Core MSCI Europe", 280, "EUR", "Euronext Paris"], ["CSNDX.MI", "iShares Nasdaq 100", 1100],
["QQQ", "Invesco QQQ Trust", 482, "USD", "NASDAQ"], ["SPY", "SPDR S&P 500", 592, "USD", "NYSE Arca"], ["VOO", "Vanguard S&P 500", 545, "USD", "NYSE Arca"], ["VTI", "Vanguard Total Stock Mkt", 285, "USD", "NYSE Arca"], ["IVV", "iShares Core S&P 500", 595, "USD", "NYSE Arca"], ["VEA", "Vanguard Developed Mkts", 52, "USD", "NYSE Arca"], ["VWO", "Vanguard Emerging Mkts", 46, "USD", "NYSE Arca"], ["AGG", "iShares Core US Agg Bond", 98, "USD", "NYSE Arca"], ["BND", "Vanguard Total Bond", 73, "USD", "NASDAQ"], ["GLD", "SPDR Gold Shares", 230, "USD", "NYSE Arca"], ["IAU", "iShares Gold Trust", 47, "USD", "NYSE Arca"], ["SCHD", "Schwab US Dividend", 28, "USD", "NYSE Arca"],
["XLK", "Technology Select SPDR", 232, "USD", "NYSE Arca"], ["XLF", "Financial Select SPDR", 47, "USD", "NYSE Arca"], ["XLE", "Energy Select SPDR", 92, "USD", "NYSE Arca"], ["XLV", "Health Care Select SPDR", 152, "USD", "NYSE Arca"], ["XLY", "Consumer Disc. Select SPDR", 198, "USD", "NYSE Arca"], ["XLP", "Consumer Staples SPDR", 80, "USD", "NYSE Arca"], ["XLI", "Industrial Select SPDR", 138, "USD", "NYSE Arca"], ["XLU", "Utilities Select SPDR", 78, "USD", "NYSE Arca"], ["XLB", "Materials Select SPDR", 92, "USD", "NYSE Arca"], ["XLRE", "Real Estate Select SPDR", 42, "USD", "NYSE Arca"], ["XLC", "Communication Svcs SPDR", 92, "USD", "NYSE Arca"], ["ARKK", "ARK Innovation", 48, "USD", "NYSE Arca"], ["SOXX", "iShares Semiconductor", 220, "USD", "NASDAQ"], ["SMH", "VanEck Semiconductor", 248, "USD", "NASDAQ"], ["XBI", "SPDR S&P Biotech", 96, "USD", "NYSE Arca"], ["TLT", "iShares 20+ Yr Treasury", 92, "USD", "NASDAQ"], ["IEF", "iShares 7-10 Yr Treasury", 95, "USD", "NASDAQ"], ["SHY", "iShares 1-3 Yr Treasury", 82, "USD", "NASDAQ"], ["LQD", "iShares iBoxx Inv Grade Corp", 108, "USD", "NYSE Arca"], ["HYG", "iShares High Yield Corp", 79, "USD", "NYSE Arca"], ["TIP", "iShares TIPS Bond", 108, "USD", "NYSE Arca"], ["VYM", "Vanguard High Dividend", 122, "USD", "NYSE Arca"], ["VIG", "Vanguard Dividend Appr.", 188, "USD", "NYSE Arca"], ["VNQ", "Vanguard Real Estate", 88, "USD", "NYSE Arca"], ["VGT", "Vanguard Info Tech", 580, "USD", "NYSE Arca"], ["VUG", "Vanguard Growth", 392, "USD", "NYSE Arca"], ["VTV", "Vanguard Value", 168, "USD", "NYSE Arca"], ["IWM", "iShares Russell 2000", 220, "USD", "NYSE Arca"], ["DIA", "SPDR Dow Jones", 432, "USD", "NYSE Arca"], ["EFA", "iShares MSCI EAFE", 82, "USD", "NYSE Arca"], ["EEM", "iShares MSCI Emerging Mkts", 44, "USD", "NYSE Arca"], ["ACWI", "iShares MSCI ACWI", 118, "USD", "NASDAQ"], ["VXUS", "Vanguard Total Intl Stock", 62, "USD", "NASDAQ"],
["VWRA.L", "Vanguard FTSE All-World Acc", 142, "USD", "London"], ["VUAA.MI", "Vanguard S&P 500 Acc", 102], ["SPPW.DE", "SPDR MSCI World", 32, "EUR", "Xetra"], ["SEMI.MI", "iShares MSCI Semiconductors", 9.8], ["IH2O.MI", "iShares Global Water", 62], ["DGTL.MI", "iShares Digitalisation", 11.2], ["RBOT.MI", "iShares Automation & Robotics", 13.8], ["HEAL.MI", "iShares Healthcare Innovation", 9.2], ["ECAR.MI", "iShares Electric Vehicles", 8.4], ["IBGL.MI", "iShares Euro Govt 15-30yr", 5.2], ["IBTM.MI", "iShares USD Treasury 7-10yr", 4.8],
["QQQM", "Invesco Nasdaq 100", 195, "USD", "NASDAQ"], ["QUAL", "iShares MSCI USA Quality", 165, "USD", "NYSE Arca"], ["MTUM", "iShares MSCI Momentum", 215, "USD", "NYSE Arca"], ["USMV", "iShares MSCI Min Vol", 88, "USD", "NYSE Arca"], ["IJR", "iShares Core S&P Small-Cap", 115, "USD", "NYSE Arca"], ["IJH", "iShares Core S&P Mid-Cap", 62, "USD", "NYSE Arca"], ["VB", "Vanguard Small-Cap", 225, "USD", "NYSE Arca"], ["VO", "Vanguard Mid-Cap", 265, "USD", "NYSE Arca"], ["VEA", "Vanguard FTSE Dev. Mkts", 52, "USD", "NYSE Arca"], ["VWO", "Vanguard FTSE Emerging", 45, "USD", "NYSE Arca"], ["BND", "Vanguard Total Bond", 72, "USD", "NASDAQ"], ["BNDX", "Vanguard Total Intl Bond", 49, "USD", "NASDAQ"], ["AGG", "iShares Core US Agg Bond", 98, "USD", "NYSE Arca"], ["GLD", "SPDR Gold Shares", 215, "USD", "NYSE Arca"], ["SLV", "iShares Silver Trust", 26, "USD", "NYSE Arca"], ["IAU", "iShares Gold Trust", 44, "USD", "NYSE Arca"], ["DBC", "Invesco DB Commodity", 22, "USD", "NYSE Arca"], ["JEPI", "JPMorgan Equity Premium", 58, "USD", "NYSE Arca"], ["JEPQ", "JPMorgan Nasdaq Premium", 52, "USD", "NASDAQ"], ["IEMG", "iShares Core MSCI EM", 54, "USD", "NYSE Arca"], ["IXUS", "iShares Core Total Intl", 68, "USD", "NASDAQ"], ["EUNL.DE", "iShares Core MSCI World", 92, "EUR", "Xetra"], ["IWDA.AS", "iShares Core MSCI World Acc", 95, "EUR", "Euronext A'dam"], ["AGGH.MI", "iShares Global Aggregate", 4.9, "EUR", "Borsa Italiana"], ["EIMI.MI", "iShares Core MSCI EM IMI", 32, "EUR", "Borsa Italiana"]];


const CASH = [
["EUR.CASH", "Euro", 1, "EUR", "Cash"], ["USD.CASH", "US Dollar", 1, "USD", "Cash"], ["CHF.CASH", "Swiss Franc", 1, "CHF", "Cash"], ["GBP.CASH", "British Pound", 1, "GBP", "Cash"], ["JPY.CASH", "Japanese Yen", 1, "JPY", "Cash"], ["CAD.CASH", "Canadian Dollar", 1, "CAD", "Cash"], ["AUD.CASH", "Australian Dollar", 1, "AUD", "Cash"], ["SGD.CASH", "Singapore Dollar", 1, "SGD", "Cash"]];

const BONDS = [
["UST10Y", "US Treasury Note 4.25% 2035", 97.62, "USD", "U.S. Treasury"], ["UST2Y", "US Treasury Note 4.50% 2027", 99.45, "USD", "U.S. Treasury"], ["UST5Y", "US Treasury Note 4.375% 2030", 98.30, "USD", "U.S. Treasury"], ["UST30Y", "US Treasury Bond 4.75% 2055", 95.10, "USD", "U.S. Treasury"], ["TIPS10Y", "US Treasury TIPS 2.125% 2035", 101.20, "USD", "U.S. Treasury"], ["TBILL3M", "US T-Bill 13-Week", 98.85, "USD", "U.S. Treasury"],
["BTP10Y", "Italy BTP 4.20% 2034", 99.80, "EUR", "MOT"], ["BUND10Y", "Germany Bund 2.60% 2035", 96.40, "EUR", "Xetra"], ["OAT10Y", "France OAT 3.00% 2034", 97.05, "EUR", "Euronext Paris"], ["GILT10Y", "UK Gilt 4.25% 2034", 96.70, "GBP", "London"], ["JGB10Y", "Japan JGB 1.10% 2034", 99.20, "JPY", "Tokyo"],
["AAPL27", "Apple Inc. 3.85% 2027", 98.10, "USD", "Corporate"], ["MSFT29", "Microsoft 4.10% 2029", 97.55, "USD", "Corporate"], ["ENEL30", "Enel SpA 3.50% 2030", 95.90, "EUR", "Corporate"],
["UST7Y", "US Treasury Note 4.375% 2032", 98.05, "USD", "U.S. Treasury"], ["UST20Y", "US Treasury Bond 4.625% 2045", 96.20, "USD", "U.S. Treasury"], ["TBILL6M", "US T-Bill 26-Week", 97.60, "USD", "U.S. Treasury"], ["TIPS5Y", "US Treasury TIPS 1.75% 2030", 100.40, "USD", "U.S. Treasury"],
["BTP30Y", "Italy BTP 4.50% 2054", 97.20, "EUR", "MOT"], ["BTP5Y", "Italy BTP 3.60% 2030", 100.10, "EUR", "MOT"], ["BUND30Y", "Germany Bund 2.90% 2055", 94.80, "EUR", "Xetra"], ["OAT30Y", "France OAT 3.25% 2055", 93.60, "EUR", "Euronext Paris"], ["BONO10Y", "Spain Bono 3.45% 2034", 98.90, "EUR", "BME"], ["PGB10Y", "Portugal OT 3.30% 2034", 99.40, "EUR", "Euronext Lisbon"], ["GGB10Y", "Greece GGB 3.875% 2034", 100.60, "EUR", "Athens"], ["DSL10Y", "Netherlands DSL 2.50% 2034", 96.10, "EUR", "Euronext A'dam"],
["GILT30Y", "UK Gilt 4.375% 2054", 94.20, "GBP", "London"], ["GILT2Y", "UK Gilt 4.125% 2027", 99.10, "GBP", "London"],
["GOOGL28", "Alphabet 3.625% 2028", 97.80, "USD", "Corporate"], ["AMZN31", "Amazon 3.95% 2031", 96.40, "USD", "Corporate"], ["JPM30", "JPMorgan 4.25% 2030", 97.90, "USD", "Corporate"], ["T28", "AT&T 4.10% 2028", 97.20, "USD", "Corporate"], ["VW28", "Volkswagen 3.75% 2028", 98.30, "EUR", "Corporate"], ["ENI30", "Eni 3.625% 2030", 96.80, "EUR", "Corporate"],
["UST3Y", "US Treasury Note 4.50% 2028", 99.10, "USD", "U.S. Treasury"], ["TBILL1M", "US T-Bill 4-Week", 99.62, "USD", "U.S. Treasury"], ["TBILL1Y", "US T-Bill 52-Week", 96.10, "USD", "U.S. Treasury"], ["BTP3Y", "Italy BTP 3.20% 2028", 99.40, "EUR", "MOT"], ["BTP7Y", "Italy BTP 3.85% 2032", 98.70, "EUR", "MOT"], ["BUND5Y", "Germany Bund 2.20% 2030", 97.30, "EUR", "Xetra"], ["OAT5Y", "France OAT 2.75% 2030", 98.10, "EUR", "Euronext Paris"], ["BONO30Y", "Spain Bono 4.00% 2054", 96.20, "EUR", "BME"], ["GILT5Y", "UK Gilt 4.00% 2030", 98.20, "GBP", "London"], ["JGB5Y", "Japan JGB 0.60% 2030", 99.50, "JPY", "Tokyo"], ["TSLA29", "Tesla 4.30% 2029", 96.90, "USD", "Corporate"], ["NVDA30", "NVIDIA 4.00% 2030", 97.10, "USD", "Corporate"], ["GOOGL31", "Alphabet 3.80% 2031", 96.20, "USD", "Corporate"], ["VOD30", "Vodafone 3.50% 2030", 95.60, "GBP", "Corporate"]];


function build(list, type, defCcy, defExch) {const o = {};const TC = { crypto: "#8A8F98", stock: "#8A7BD8", etf: "#2D9CDB", bond: "#5B6B9E", cash: "#44705C" };list.forEach(([key, name, price, ccy, exch], i) => {const base = key.split(/[._]/)[0];o[key] = { ticker: key, name, type, currency: ccy || defCcy, exchange: exch || defExch, base: price, rank: i + 1, color: COLORS[key] || COLORS[base] || TC[type], dark: DARKTEXT.has(key) };});return o;}
function buildRWA() {const o = {};(typeof window !== "undefined" && window.RWA_TOKENS || []).forEach((t, i) => {o[t.slug] = { ticker: t.slug, name: t.name, type: "token", currency: "USD", exchange: t.platform, base: t.navUsd || 1, rank: i + 1, color: "#2FA8A0", rwa: t };});return o;}
const CATALOG = { ...build(CRYPTO, "crypto", "USD", "Crypto"), ...build(STOCKS, "stock", "USD", "NASDAQ"), ...build(ETF, "etf", "EUR", "Borsa Italiana"), ...build(BONDS, "bond", "USD", "U.S. Treasury"), ...build(CASH, "cash", "EUR", "Cash"), ...buildRWA() };

const N = 1500;
const REFRESH_MS = 5 * 60 * 1000;
function makeMockProvider() {const live = {};Object.entries(CATALOG).forEach(([s, a]) => {live[s] = a.type === "cash" ? a.base : a.base * (1 + (Math.random() - 0.5) * 0.02);});return { tick() {Object.keys(live).forEach((s) => {const c = CATALOG[s];const t = c?.type;const vol = t === "cash" || c?.custom ? 0 : t === "crypto" ? 0.012 : t === "bond" ? 0.0015 : 0.006;live[s] *= 1 + (Math.random() - 0.5) * vol;});return { ...live };}, snapshot() {return { ...live };}, set(s, v) {live[s] = v;} };}
function genHistory(end, type, n = N) {const arr = [];const vol = type === "crypto" ? 0.01 : 0.005;let v = end * (0.78 + Math.random() * 0.06);for (let i = 0; i < n; i++) {v *= 1 + (Math.random() - 0.42) * vol;arr.push(v);}arr[n - 1] = end;return arr;}

function buildRelative() {
  const A = [[0, 0.74], [500, 0.82], [800, 0.877], [1180, 0.95238], [1360, 1.0256], [1470, 0.997], [1499, 1.0]];
  const arr = new Array(N).fill(1);
  for (let s = 0; s < A.length - 1; s++) {
    const [x0, y0] = A[s],[x1, y1] = A[s + 1];const len = x1 - x0;
    const walk = new Array(len + 1);walk[0] = 0;
    for (let i = 1; i <= len; i++) walk[i] = walk[i - 1] + (Math.random() - 0.5);
    const wend = walk[len];const bridge = new Array(len + 1);let maxb = 1e-6;
    for (let i = 0; i <= len; i++) {bridge[i] = walk[i] - wend * (i / len);maxb = Math.max(maxb, Math.abs(bridge[i]));}
    const amp = 0.012 + 0.02 * (1 - s / (A.length - 1));
    for (let i = 0; i <= len; i++) {const trend = y0 + (y1 - y0) * (i / len);const hf = Math.sin((x0 + i) * 0.55) * 0.0015 + Math.sin((x0 + i) * 0.21) * 0.0018;arr[x0 + i] = trend * (1 + bridge[i] / maxb * amp + hf);}
  }
  arr[N - 1] = 1.0;return arr;
}

// Per-asset relative path: tracks the shared market curve (with a per-asset beta)
// plus an idiosyncratic brownian-bridge wiggle. Always ends at 1.0 so the asset's
// current value === valEUR. Summing valEUR*rel over holdings reconstructs the total.
function buildAssetRel(marketRel, beta, amp) {
  const n = marketRel.length;
  const walk = new Array(n);walk[0] = 0;
  for (let i = 1; i < n; i++) walk[i] = walk[i - 1] + (Math.random() - 0.5);
  const wend = walk[n - 1];let maxb = 1e-6;const bridge = new Array(n);
  for (let i = 0; i < n; i++) {bridge[i] = walk[i] - wend * (i / (n - 1));maxb = Math.max(maxb, Math.abs(bridge[i]));}
  const arr = new Array(n);
  for (let i = 0; i < n; i++) {const m = 1 + beta * (marketRel[i] - 1) + bridge[i] / maxb * amp;arr[i] = Math.max(0.15, m);}
  arr[n - 1] = 1.0;return arr;
}

let _id = 0;const nid = () => `t${++_id}`;
const today = () => new Date().toISOString().slice(0, 10);
const Bx = (symbol, quantity, price, currency, date) => ({ id: nid(), action: "buy", symbol, quantity, price, currency, date });
const SEED = [
Bx("SWDA.MI", 120, 64.00, "EUR", "2024-11-12T10:02"), Bx("SWDA.MI", 120, 82.00, "EUR", "2025-03-04T16:20"), Bx("SWDA.MI", 120, 95.00, "EUR", "2025-09-10T11:15"), Bx("SWDA.MI", 122, 113.87, "EUR", "2026-03-05T19:57"),
Bx("CSSPX.MI", 80, 520, "EUR", "2024-08-19T14:30"), Bx("CSSPX.MI", 70, 612, "EUR", "2025-09-18T14:30"),
Bx("VWCE.MI", 200, 102, "EUR", "2025-01-15T09:40"), Bx("VWCE.MI", 150, 118.2, "EUR", "2025-12-12T21:27"),
Bx("AAPL", 120, 175, "USD", "2024-09-05T15:40"), Bx("AAPL", 80, 205, "USD", "2026-01-20T15:40"),
Bx("MSFT", 90, 398, "USD", "2025-06-11T15:00"), Bx("NVDA", 250, 110, "USD", "2024-12-03T15:10"),
Bx("BTC", 0.4, 49000, "USD", "2025-07-22T11:05"), Bx("BTC", 1.03849, 52000, "USD", "2025-10-02T11:05"), Bx("SOL", 0.383964984, 152, "USD", "2026-02-18T13:20"),
Bx("ETH", 14, 3100, "USD", "2025-08-14T12:00"), Bx("ASML.AS", 30, 820, "EUR", "2025-05-06T10:30"),
Bx("ENI.MI", 1500, 13.5, "EUR", "2025-02-10T09:55"),
Bx("UST10Y", 300, 95.40, "USD", "2025-04-22T10:30"), Bx("UST10Y", 205, 97.62, "USD", "2026-05-14T11:00"), Bx("BTP10Y", 150, 98.10, "EUR", "2025-06-30T09:45"), Bx("BUND10Y", 120, 95.20, "EUR", "2025-08-12T14:10"), Bx("AAPL27", 100, 97.40, "USD", "2025-10-05T15:30"),
Bx("EUR.CASH", 25000, 1, "EUR", "2026-01-08T10:00")];


const LOC = "en-US";
const eurOf = (v, ccy) => v * (FX[ccy] ?? 1);
const fromEUR = (vEUR, ccy) => vEUR / (FX[ccy] ?? 1);
const money = (v, ccy) => new Intl.NumberFormat(LOC, { style: "currency", currency: ccy, minimumFractionDigits: 2, maximumFractionDigits: ccy === "JPY" ? 2 : Math.abs(v) < 0.01 && v !== 0 ? 6 : Math.abs(v) < 2 ? 4 : 2 }).format(v);
const dispMoney = (vEUR, ccy) => money(fromEUR(vEUR, ccy), ccy);
const num = (v, d = 2) => new Intl.NumberFormat(LOC, { minimumFractionDigits: d, maximumFractionDigits: d }).format(v);
// Quantity: no forced decimals (whole shares show as "150", fractional crypto keeps up to 6 dp)
const qfmt = (v) => new Intl.NumberFormat(LOC, { minimumFractionDigits: 0, maximumFractionDigits: 6 }).format(v);
// Detail "Owned" quantity — crypto shows fine-grained decimals (>=9 when sub-unit, <=5 otherwise)
const qtyDetail = (q, type) => type === "crypto" ?
new Intl.NumberFormat(LOC, { minimumFractionDigits: q < 1 ? 9 : 0, maximumFractionDigits: q < 1 ? 9 : 5 }).format(q) :
num(q);
const CCY_SYM = { EUR: "€", USD: "$", CHF: "CHF", JPY: "¥", GBP: "£", CAD: "C$" };
// Rolling digits — the entrance animation plays ONCE on mount (page/screen load).
// After that, value changes (chart scrubbing, live ticks) update the number in place,
// with no roll/flicker and no throttle, so the figure tracks the chart value live.
// Entrance roll is a page-load flourish only; disarm it once the app has settled so later
// mounts (variant switches, navigation) render digits at rest with no animation.
let entranceArmed = true;
setTimeout(() => { entranceArmed = false; }, 1400);
function RollDigits({ text, className = "", style }) {
  const t = String(text);
  const [shown, setShown] = useState(t);
  const firstRef = useRef(true);
  useEffect(() => {
    if (t !== shown) setShown(t); // follow the value immediately, no throttle
  }, [t, shown]);
  // The entrance roll is a one-time, initial-load flourish. Two guards stop it from ever
  // hiding a figure: (1) entranceArmed disarms shortly after load, so re-skinning the app
  // (variant switch) mounts fresh digits that render at rest instantly; (2) the flag flips
  // DURING render, never in a passive effect — effects get starved by the post-switch RAF
  // storm, which would otherwise leave the one-shot animation stuck "play-pending" at frame 0.
  const playEntrance = firstRef.current && entranceArmed;
  firstRef.current = false;
  const chars = [...shown];
  return <span className={className} style={{ ...style, display: "inline-flex", whiteSpace: "pre" }}>
    {chars.map((ch, i) =>
    <span key={i + "·" + ch} style={playEntrance ? { display: "inline-block", animation: `digitRoll .42s cubic-bezier(.25,.8,.3,1) ${i * 0.012}s both` } : { display: "inline-block" }}>{ch}</span>
    )}
  </span>;
}
// Reveal animation — eases 0→1 on deps change; multiply a value by it to get a count-up "formatting" effect
function useReveal(deps, dur = 850) {
  const [m, setM] = useState(0);
  useEffect(() => {let raf;const t0 = performance.now();setM(0);const step = (t) => {const p = Math.min(1, (t - t0) / dur);setM(1 - Math.pow(1 - p, 3));if (p < 1) raf = requestAnimationFrame(step);};raf = requestAnimationFrame(step);return () => cancelAnimationFrame(raf);}, deps);
  return m;
}
const fmtDateTime = (s) => {const d = new Date(s);const date = new Intl.DateTimeFormat(LOC, { day: "numeric", month: "short", year: "numeric" }).format(d);const time = new Intl.DateTimeFormat(LOC, { hour: "numeric", minute: "2-digit" }).format(d);return s.includes("T") ? `${date} · ${time}` : date;};
const hashColor = (s) => {let h = 0;for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360;return `hsl(${h},48%,46%)`;};
const lettersOf = (sym) => sym.split(/[._]/)[0].slice(0, 4);
function resolveSymbol(raw) {const s = String(raw).toUpperCase().trim();if (CATALOG[s]) return s;const base = s.replace(/[._].*$/, "");if (CATALOG[base]) return base;const byBase = Object.keys(CATALOG).find((k) => k.replace(/[._].*$/, "") === base);return byBase || s;}

function computeHoldings(transactions, prices) {
  const map = {};
  for (const t of transactions) {const m = map[t.symbol] ??= { symbol: t.symbol, qty: 0, buyQty: 0, buyCost: 0 };if (t.action === "buy") {m.qty += t.quantity;m.buyQty += t.quantity;m.buyCost += t.quantity * t.price;} else m.qty -= t.quantity;}
  return Object.values(map).filter((m) => m.qty > 1e-9).map((m) => {const a = CATALOG[m.symbol] || { ticker: m.symbol, name: m.symbol, currency: "EUR", base: 0, type: "stock", exchange: "—" };const avg = m.buyQty ? m.buyCost / m.buyQty : 0;const px = prices[m.symbol] ?? a.base;const valEUR = eurOf(m.qty * px, a.currency),costEUR = eurOf(m.qty * avg, a.currency);const pl = valEUR - costEUR;return { ...m, ...a, avg, px, valEUR, costEUR, pl, plPct: costEUR ? pl / costEUR * 100 : 0 };}).sort((a, b) => b.valEUR - a.valEUR);
}

// ── Sell validation ── walks base + candidate rows in date order and flags any
// candidate SELL that has no holding to sell from (phantom) or sells more than
// is held at that point (oversell). Returns an array parallel to `rows`:
// { issue: null | "phantom" | "oversell", have: number }. Flagged rows are NOT
// applied to the running position, so they're treated as skipped downstream too.
function auditSells(baseTx, rows) {
  const eps = 1e-9;
  const items = [];
  (baseTx || []).forEach((t, i) => items.push({ src: "base", t, date: t.date || today(), ord: i }));
  (rows || []).forEach((r, i) => items.push({ src: "cand", t: r, date: r.date || today(), ord: 1e6 + i, ci: i }));
  items.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : a.ord - b.ord));
  const pos = {};
  const ann = (rows || []).map(() => ({ issue: null, have: 0 }));
  for (const it of items) {
    const sym = resolveSymbol(it.t.symbol);
    const qty = Number(it.t.quantity) || 0;
    if (it.t.action === "sell") {
      const have = pos[sym] || 0;
      if (it.src === "cand") {
        if (have <= eps) { ann[it.ci] = { issue: "phantom", have: 0 }; continue; }
        if (qty > have + eps) { ann[it.ci] = { issue: "oversell", have }; continue; }
        pos[sym] = have - qty;
      } else {
        pos[sym] = Math.max(0, have - qty);
      }
    } else {
      pos[sym] = (pos[sym] || 0) + qty;
    }
  }
  return ann;
}

const RANGES = { LIVE: 30, "1D": 40, "1W": 140, "1M": 320, YTD: 700, MAX: 1500 };
const RANGE_KEYS = Object.keys(RANGES);
const CLASSES = [{ id: "etf", label: "ETFs" }, { id: "stock", label: "Stocks" }, { id: "crypto", label: "Crypto" }, { id: "bond", label: "Bonds" }, { id: "token", label: "Tokenization Asset" }, { id: "cash", label: "Cash" }];

// ── Total-worth chart: iOS-style press-and-scrub + end-of-line dot ──
function TotalChart({ data, h = 260, pad = 66, C, live, scrub, setScrub, fmt }) {
  const boxRef = useRef(null);
  const vals = data.map((d) => d.v);
  const min = Math.min(...vals),max = Math.max(...vals);
  const maxIdx = vals.indexOf(max),minIdx = vals.indexOf(min);
  const len = Math.max(1, data.length - 1);
  const yOf = (v, H) => pad + (1 - (v - min) / (max - min || 1)) * (H - 2 * pad);
  const pick = (clientX) => {const r = boxRef.current.getBoundingClientRect();const x = Math.min(Math.max(clientX - r.left, 0), r.width);const idx = Math.min(data.length - 1, Math.max(0, Math.round(x / r.width * len)));return { x: idx / len * r.width, y: yOf(data[idx].v, r.height), v: data[idx].v, idx };};
  const onDown = (e) => {boxRef.current.setPointerCapture?.(e.pointerId);setScrub(pick(e.clientX));};
  const onMove = (e) => {if (scrub) setScrub(pick(e.clientX));};
  const onEnd = () => setScrub(null);
  const lastY = yOf(data[data.length - 1]?.v ?? 0, h);
  const pressed = !!scrub; // pressed / held → bigger
  const filled = live || pressed; // live OR pressed → solid green core
  const ds = pressed ? 15 : 11;
  const clampPct = (p) => Math.max(11, Math.min(89, p));
  const maxPct = clampPct(maxIdx / len * 100),minPct = clampPct(minIdx / len * 100);
  return (
    <div ref={boxRef} onPointerDown={onDown} onPointerMove={onMove} onPointerUp={onEnd} onPointerCancel={onEnd} style={{ position: "relative", width: "100%", height: h, touchAction: "none", cursor: "crosshair" }}>
      <div style={{ position: "absolute", inset: 0 }}>
        <ResponsiveContainer width="100%" height="100%">
          <AreaChart data={data} margin={{ top: pad, bottom: pad, left: 0, right: 0 }}>
            <YAxis domain={["dataMin", "dataMax"]} hide />
            <Area type="natural" dataKey="v" stroke={C.up} strokeWidth={2} fill="none" isAnimationActive={false} />
          </AreaChart>
        </ResponsiveContainer>
      </div>
      {/* High / Low levels — subtle dark-grey guide lines + values pinned to the left edge */}
      <div style={{ position: "absolute", top: yOf(max, h), left: 0, right: 0, height: 0, borderTop: `1px dashed ${C.faint}`, opacity: 0.5, pointerEvents: "none", zIndex: 3 }}></div>
      <div style={{ position: "absolute", top: yOf(max, h) - 17, left: 24, color: C.faint, pointerEvents: "none", zIndex: 4 }} className="font-mono tabular-nums text-xs whitespace-nowrap">{fmt(max)}</div>
      <div style={{ position: "absolute", top: yOf(min, h), left: 0, right: 0, height: 0, borderTop: `1px dashed ${C.faint}`, opacity: 0.5, pointerEvents: "none", zIndex: 3 }}></div>
      <div style={{ position: "absolute", top: yOf(min, h) + 5, left: 24, color: C.faint, pointerEvents: "none", zIndex: 4 }} className="font-mono tabular-nums text-xs whitespace-nowrap">{fmt(min)}</div>
      {scrub && <div style={{ position: "absolute", top: pad - 10, bottom: pad - 10, left: scrub.x, width: 1, background: C.line, pointerEvents: "none" }}></div>}
      {/* end-of-line dot — default: green ring on bg; pressed: solid green & larger; live: solid green & pulsing */}
      <div style={{ position: "absolute", top: scrub ? scrub.y : lastY, ...(scrub ? { left: scrub.x } : { right: 0 }), width: 0, height: 0, pointerEvents: "none", zIndex: 5 }}>
        {live && <span className="halo" style={{ position: "absolute", left: -ds, top: -ds, width: ds * 2, height: ds * 2, borderRadius: "50%", background: C.up }}></span>}
        <span style={{ position: "absolute", left: -ds / 2, top: -ds / 2, width: ds, height: ds, borderRadius: "50%", background: filled ? C.up : C.bg, border: `2px solid ${filled ? C.bg : C.up}`, transition: "width .15s ease, height .15s ease, left .15s ease, top .15s ease, background .15s ease" }}></span>
      </div>
    </div>);

}

// ── Launch-flow data + component (module scope — must NOT live inside App, or it remounts every render) ──
  const VERSION_SWATCH = { draft: "#2F6BED", editorial: "#136F63", wsb: "#2DE07A", data: "#E8A33D" };

  // ── Launch flow: 3 questions → matched investor profile → UI version ──
  // Each answer adds points to one or more archetypes. Highest score wins
  // (ties broken by ORDER below). Archetype id === variant id.
  const ARCHETYPES = {
    wsb: { tag: "The Adrenaline Trader", line: "Always watching, always moving — you live for the green days and the rush of the market.", version: "WallStreetBets", verb: "Your wins, loud" },
    data: { tag: "The Numbers Analyst", line: "Metrics, decimals and dashboards. You want the full readout, nothing rounded off.", version: "Data", verb: "Every figure, live" },
    editorial: { tag: "The Long-View Reader", line: "You think in years and care about the story behind the numbers, not the noise of the day.", version: "Editorial", verb: "Your day, as a headline" },
    draft: { tag: "The Calm Holder", line: "Quiet and unbothered. You check rarely, keep it simple, and let it compound.", version: "v0.1", verb: "Calm and clear" }
  };
  const ORDER = ["wsb", "data", "editorial", "draft"];
  const QUIZ = [
    { q: "When you open the app, what are you really after?", hint: "Pick everything that feels like you.", opts: [
      { t: "Am I up or down right now?", s: { wsb: 2, data: 1 } },
      { t: "A calm overview, nothing more", s: { draft: 2 } },
      { t: "What moved today — and why", s: { editorial: 2 } },
      { t: "The exact figures, down to the cent", s: { data: 2 } } ] },
    { q: "How often do you actually check in?", hint: "Be honest — more than one is fine.", opts: [
      { t: "Several times a day, I can't help it", s: { wsb: 2 } },
      { t: "Once in a while, when I remember", s: { draft: 2 } },
      { t: "Weekly, with my coffee and the news", s: { editorial: 2, draft: 1 } },
      { t: "Daily, like a dashboard I review", s: { data: 2 } } ] },
    { q: "What would make you smile in here?", hint: "Choose all that apply.", opts: [
      { t: "The whole screen turning green when I win", s: { wsb: 2 } },
      { t: "Calm and minimal — nothing shouting", s: { draft: 2 } },
      { t: "A line that reads like a headline", s: { editorial: 2 } },
      { t: "A terminal packed with live numbers", s: { data: 2 } } ] }
  ];

  function LaunchFlow({ onPick, C, native, onAccent, cardBg }) {
    const [step, setStep] = useState(0); // 0 welcome · 1-3 questions · 4 result
    const [picks, setPicks] = useState([[], [], []]);
    const [browseAll, setBrowseAll] = useState(false);
    const padTop = native ? "calc(env(safe-area-inset-top, 0px) + 26px)" : 52;
    const padBottom = native ? "calc(env(safe-area-inset-bottom, 0px) + 20px)" : 26;

    const toggle = (qi, oi) => {
      setPicks((p) => { const n = p.map((a) => a.slice()); const arr = n[qi]; const k = arr.indexOf(oi); if (k >= 0) arr.splice(k, 1); else arr.push(oi); return n; });
      Haptics.toggle();
    };
    const result = useMemo(() => {
      const sc = { wsb: 0, data: 0, editorial: 0, draft: 0 };
      picks.forEach((sel, qi) => sel.forEach((oi) => { const s = QUIZ[qi].opts[oi].s; for (const k in s) sc[k] += s[k]; }));
      let win = ORDER[0]; ORDER.forEach((k) => { if (sc[k] > sc[win]) win = k; });
      return win;
    }, [picks]);

    const overline = { fontSize: 11, letterSpacing: "0.16em", textTransform: "uppercase", color: C.faint, fontWeight: 700 };
    const cta = (label, onClick, on = true, Icon = ChevronRight) => (
      <button onClick={onClick} disabled={!on} style={{ background: C.accentInk, color: onAccent, borderRadius: 13, width: "100%", opacity: on ? 1 : 0.35, cursor: on ? "pointer" : "default" }} className="py-3.5 text-sm font-semibold flex items-center justify-center gap-1.5 active:opacity-85 transition-opacity">{label}{Icon && <Icon size={16} />}</button>
    );
    // Step dots (questions only)
    const Progress = () => (
      <div className="flex items-center gap-1.5">{[1, 2, 3].map((i) => (
        <span key={i} style={{ height: 4, borderRadius: 2, flex: 1, background: step >= i ? C.accentInk : C.line, transition: "background .25s ease" }}></span>
      ))}</div>
    );

    const OptionBtn = ({ qi, oi, label }) => {
      const on = picks[qi].indexOf(oi) >= 0;
      return (
        <button onClick={() => toggle(qi, oi)} style={{ width: "100%", textAlign: "left", display: "flex", alignItems: "center", gap: 13, padding: "15px 16px", borderRadius: 15, background: on ? C.accentSoft : cardBg.background, border: `1.5px solid ${on ? C.accentInk : C.line}`, backdropFilter: cardBg.backdropFilter, WebkitBackdropFilter: cardBg.WebkitBackdropFilter, transition: "border-color .15s ease, background .15s ease" }} className="active:scale-[0.99]">
          <span style={{ width: 24, height: 24, borderRadius: 12, flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", background: on ? C.accentInk : "transparent", border: `1.5px solid ${on ? C.accentInk : C.faint}`, transition: "all .15s ease" }}>{on && <Check size={14} style={{ color: onAccent }} />}</span>
          <span style={{ fontSize: 15, fontWeight: on ? 600 : 500, color: C.ink, lineHeight: 1.3 }}>{label}</span>
        </button>
      );
    };

    return (
      <div style={{ position: "absolute", inset: 0, zIndex: 90, background: C.bg, display: "flex", flexDirection: "column" }}>
        {/* header */}
        <div style={{ padding: `${padTop}px 22px 14px`, flexShrink: 0 }}>
          <div className="flex items-center justify-between" style={{ marginBottom: step >= 1 && step <= 3 ? 14 : 0 }}>
            {step >= 1 && step <= 3 ? (
              <button onClick={() => { setStep((s) => s - 1); Haptics.tap(); }} aria-label="Back" style={{ color: C.mute, width: 32, height: 32, marginLeft: -6, borderRadius: 16 }} className="flex items-center justify-center active:opacity-60"><ChevronLeft size={22} /></button>
            ) : <div style={{ ...overline }}>Portfolio Tracker</div>}
            {step >= 1 && step <= 3 && <div style={{ ...overline, fontSize: 11 }}>{step} / 3</div>}
          </div>
          {step >= 1 && step <= 3 && <Progress />}
        </div>

        {/* body */}
        <div key={step} className="flex-1 overflow-y-auto noscroll slideup" style={{ padding: "6px 22px 18px" }}>
          {step === 0 && (
            <div style={{ minHeight: "100%", display: "flex", flexDirection: "column" }}>
              <div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", paddingBottom: 8 }}>
                <div style={{ display: "inline-flex", alignItems: "center", gap: 7, alignSelf: "flex-start", background: C.accentSoft, color: C.accentInk, borderRadius: 999, padding: "6px 12px", marginBottom: 22 }}><Sparkles size={14} /><span style={{ fontSize: 12, fontWeight: 700, letterSpacing: "0.01em" }}>Find your view</span></div>
                <h1 style={{ fontSize: 30, fontWeight: 700, color: C.ink, lineHeight: 1.08, letterSpacing: "-0.02em", marginBottom: 12 }}>One tracker.<br />Built around <span style={{ color: C.accentInk }}>you</span>.</h1>
                <p style={{ fontSize: 15, color: C.mute, lineHeight: 1.5, marginBottom: 26, maxWidth: 300 }}>Three quick questions and we'll match you with the version of Portfolio Tracker that fits how you really invest.</p>
                <div className="flex items-center gap-3" style={{ marginBottom: 4 }}>
                  <div className="flex items-center" style={{ flexShrink: 0 }}>{ORDER.map((id, i) => (<span key={id} style={{ width: 26, height: 26, borderRadius: 13, background: VERSION_SWATCH[id], border: `2px solid ${C.bg}`, marginLeft: i === 0 ? 0 : -8 }}></span>))}</div>
                  <span style={{ fontSize: 13, color: C.faint, fontWeight: 500 }}>4 versions · one fits you</span>
                </div>
              </div>
              <div style={{ paddingTop: 14 }}>
                {cta("Get started", () => { setStep(1); Haptics.tap(); })}
                <p style={{ fontSize: 12, color: C.faint, textAlign: "center", marginTop: 12 }}>Takes about 20 seconds · no account needed</p>
              </div>
            </div>
          )}

          {step >= 1 && step <= 3 && (() => { const Q = QUIZ[step - 1]; const qi = step - 1; return (
            <div>
              <h1 style={{ fontSize: 23, fontWeight: 700, color: C.ink, lineHeight: 1.18, letterSpacing: "-0.015em", marginTop: 14, marginBottom: 6 }}>{Q.q}</h1>
              <p style={{ fontSize: 13.5, color: C.mute, marginBottom: 20 }}>{Q.hint}</p>
              <div className="flex flex-col gap-2.5">{Q.opts.map((o, oi) => <OptionBtn key={oi} qi={qi} oi={oi} label={o.t} />)}</div>
            </div>
          ); })()}

          {step === 4 && (() => { const A = ARCHETYPES[result]; const sw = VERSION_SWATCH[result]; return (
            <div style={{ minHeight: "100%", display: "flex", flexDirection: "column" }}>
              <div style={{ flex: 1, display: "flex", flexDirection: "column", justifyContent: "center", paddingTop: 10, paddingBottom: 8 }}>
                <div style={{ ...overline, color: C.mute, marginBottom: 16 }}>Your match</div>
                <div style={{ marginBottom: 18 }}>
                  <span style={{ width: 52, height: 52, borderRadius: 16, background: sw, display: "inline-flex", alignItems: "center", justifyContent: "center", boxShadow: `0 8px 22px ${sw}55`, marginBottom: 14 }}><TrendingUp size={24} style={{ color: result === "wsb" || result === "data" ? "#0C0C0C" : "#FFFFFF" }} /></span>
                  <h1 style={{ fontSize: 26, fontWeight: 700, color: C.ink, lineHeight: 1.12, letterSpacing: "-0.02em" }}>{A.tag}</h1>
                  <div style={{ fontSize: 13.5, color: C.faint, fontWeight: 500, marginTop: 4 }}>{A.verb}</div>
                </div>
                <p style={{ fontSize: 15, color: C.mute, lineHeight: 1.5, marginBottom: 22 }}>{A.line}</p>
                <div style={{ ...cardBg, borderRadius: 15, padding: "14px 16px", display: "flex", alignItems: "center", gap: 12 }}>
                  <span style={{ width: 10, height: 10, borderRadius: 5, background: sw, flexShrink: 0 }}></span>
                  <div style={{ flex: 1 }}>
                    <div style={{ fontSize: 11, color: C.faint, fontWeight: 600, letterSpacing: "0.04em", textTransform: "uppercase" }}>Matched view</div>
                    <div style={{ fontSize: 15, fontWeight: 600, color: C.ink }}>{A.version}</div>
                  </div>
                  <button onClick={() => setBrowseAll((b) => !b)} style={{ fontSize: 12.5, fontWeight: 600, color: C.accentInk }} className="active:opacity-60">{browseAll ? "Hide" : "Not you?"}</button>
                </div>
                {browseAll && (
                  <div className="flex flex-col gap-1.5 slideup" style={{ marginTop: 10 }}>
                    {ORDER.filter((id) => id !== result).map((id) => (
                      <button key={id} onClick={() => { onPick(id); }} style={{ width: "100%", display: "flex", alignItems: "center", gap: 11, padding: "11px 14px", borderRadius: 13, background: cardBg.background, border: `1px solid ${C.line}`, backdropFilter: cardBg.backdropFilter, WebkitBackdropFilter: cardBg.WebkitBackdropFilter }} className="active:scale-[0.99] text-left">
                        <span style={{ width: 9, height: 9, borderRadius: 5, background: VERSION_SWATCH[id], flexShrink: 0 }}></span>
                        <span style={{ flex: 1, fontSize: 14, fontWeight: 600, color: C.ink }}>{ARCHETYPES[id].version}</span>
                        <span style={{ fontSize: 12.5, color: C.faint }}>{ARCHETYPES[id].tag}</span>
                      </button>
                    ))}
                  </div>
                )}
              </div>
              <div style={{ paddingTop: 16 }}>
                {cta("Start to set your portfolio", () => onPick(result))}
                <button onClick={() => { setPicks([[], [], []]); setBrowseAll(false); setStep(1); Haptics.tap(); }} style={{ fontSize: 13, fontWeight: 600, color: C.mute, width: "100%", marginTop: 12 }} className="py-1 active:opacity-60">Retake the quiz</button>
              </div>
            </div>
          ); })()}
        </div>

        {/* footer CTA for questions */}
        {step >= 1 && step <= 3 && (
          <div style={{ padding: `12px 22px ${padBottom}px`, flexShrink: 0, borderTop: `1px solid ${C.line}`, background: C.bg }}>
            {cta("Continue", () => { setStep((s) => s + 1); if (step === 3) Haptics.success(); else Haptics.tap(); }, picks[step - 1].length > 0)}
          </div>
        )}
      </div>
    );
  }

// ── Insight engine ───────────────────────────────────────────────────────────
// Turns raw portfolio numbers into ONE honest, plain-language read — the "AI as
// translator" idea from the project story: never advice, always the *so what*.
// Each entry in CASES is a rule {id, test(s)→bool, line(s)→string}. The engine
// computes a signal bundle once, then returns the first matching case as the
// headline plus a few supporting signal chips. Cases are evaluated top-to-bottom,
// so the most specific / most urgent ones sit first. Add cases freely.
function buildInsights(ctx) {
  const { value, dayAbs, dayPct, holdings, money, num, overallPl = 0, overallPlPct = 0 } = ctx;
  const up = dayAbs >= 0;
  const signed = (up ? "+" : "\u2212") + money(Math.abs(dayAbs));
  const total = money(value);
  const pctStr = (up ? "+" : "\u2212") + num(Math.abs(dayPct)) + "%";
  const eps = 0.005;

  const byVal = [...holdings].sort((a, b) => b.valEUR - a.valEUR);
  const top = byVal[0] || null;
  const topWeight = top && value ? top.valEUR / value * 100 : 0;
  const movers = holdings.filter((h) => Math.abs(h.dayAbs) > eps).sort((a, b) => Math.abs(b.dayAbs) - Math.abs(a.dayAbs));
  const topMover = movers[0] || null;
  const totalAbsMove = holdings.reduce((s, h) => s + Math.abs(h.dayAbs), 0) || 1;
  const topMoverShare = topMover ? Math.abs(topMover.dayAbs) / totalAbsMove * 100 : 0;
  const gainers = holdings.filter((h) => h.dayAbs > eps).sort((a, b) => b.dayAbs - a.dayAbs);
  const losers = holdings.filter((h) => h.dayAbs < -eps).sort((a, b) => a.dayAbs - b.dayAbs);
  const worst = losers[0] || null;
  const cashW = value ? holdings.filter((h) => h.type === "cash").reduce((s, h) => s + h.valEUR, 0) / value * 100 : 0;
  const classCount = new Set(holdings.filter((h) => h.type !== "cash").map((h) => h.type)).size;
  const absPct = Math.abs(dayPct);
  const overallUp = overallPl >= 0;

  // Which asset theme drove today's move down — used to explain the "why".
  const classAbs = {};
  holdings.forEach((h) => { classAbs[h.type] = (classAbs[h.type] || 0) + h.dayAbs; });
  const worstClass = Object.entries(classAbs).sort((a, b) => a[1] - b[1])[0];
  const DOWN_THEME = {
    crypto: "crypto took the hardest hit today — it always swings widest",
    etf: "the broad market drifted lower, so your index funds followed it down",
    stock: "a few of your individual stocks led the slide",
    bond: "bonds eased back",
    cash: "your holdings gave a little back",
  };
  const driver = worstClass && worstClass[1] < -eps ? (DOWN_THEME[worstClass[0]] || "a few of your positions pulled back") : "the move was spread thinly across your holdings";
  const dragTail = worst ? `, with ${worst.ticker} the biggest single drag (${money(Math.abs(worst.dayAbs))})` : "";
  // Honest grounding line — backed by the real all-time P/L, never invented.
  const ground = overallUp
    ? `Zoom out: you're still ${overallUp ? "+" : "\u2212"}${num(Math.abs(overallPlPct))}% overall. One red day isn't a trend.`
    : "Drawdowns are part of investing — they come and go.";
  const calm = "Stay calm and look long: past moves don't decide the future ones.";

  const s = { value, dayAbs, dayPct, up, absPct, signed, total, pctStr, top, topWeight, topMover, topMoverShare, gainers, losers, worst, cashW, classCount, n: holdings.length, overallUp, overallPlPct, driver, dragTail, ground, calm };

  // Supporting signals (chips) — always factual.
  const signals = [];
  if (topMover) signals.push({ label: "Top mover", value: `${topMover.ticker} ${topMover.dayAbs >= 0 ? "+" : "\u2212"}${money(Math.abs(topMover.dayAbs))}`, tone: topMover.dayAbs >= 0 ? "up" : "down" });
  if (top) signals.push({ label: "Largest position", value: `${top.ticker} \u00B7 ${num(topWeight)}%`, tone: "neutral" });
  if (holdings.length) signals.push({ label: "Spread", value: `${gainers.length} up \u00B7 ${losers.length} down`, tone: "neutral" });
  if (cashW >= 1) signals.push({ label: "Cash", value: `${num(cashW)}%`, tone: "neutral" });
  const overallChip = { label: "All-time", value: `${overallUp ? "+" : "\u2212"}${money(Math.abs(overallPl))} \u00B7 ${overallUp ? "+" : "\u2212"}${num(Math.abs(overallPlPct))}%`, tone: overallUp ? "up" : "down" };
  // On down days, lead the chips with all-time P/L so the reassurance is grounded in a number.
  if (!up) signals.unshift(overallChip); else signals.push(overallChip);

  const CASES = [
    { id: "empty", test: (s) => s.value <= 0,
      line: () => "Your portfolio is empty. Add a position and you'll get your first honest read right here." },

    { id: "up-concentration", test: (s) => s.up && s.topMover && s.topMoverShare >= 55 && s.absPct >= 0.12,
      line: (s) => `You're up ${s.signed} today — but about ${num(s.topMoverShare)}% of it came from ${s.topMover.ticker} alone. Nice, though your gains are leaning on one name.` },

    { id: "down-single-driver", test: (s) => !s.up && s.topMover && s.topMoverShare >= 60 && s.absPct >= 0.12,
      line: (s) => `Almost all of today's ${s.signed} came from ${s.topMover.ticker} alone. The rest of your portfolio barely moved — this is one name, not the whole market. ${s.ground}` },

    { id: "down-heavy", test: (s) => !s.up && s.absPct >= 1.2,
      line: (s) => `A hard day — down ${s.signed} (${s.pctStr}). Here's why: ${s.driver}${s.dragTail}. ${s.ground} ${s.calm}` },

    { id: "concentration", test: (s) => s.topWeight >= 45,
      line: (s) => `${s.top.ticker} is ${num(s.topWeight)}% of everything you hold. It's carrying you on green days and it'll cut just as deep on red ones — your portfolio is concentrated.` },

    { id: "down-meaningful", test: (s) => !s.up && s.absPct >= 0.4,
      line: (s) => `Red day, down ${s.signed} (${s.pctStr}). The reason: ${s.driver}${s.dragTail}. ${s.ground}` },

    { id: "down-noise", test: (s) => !s.up && s.absPct < 0.12,
      line: (s) => `Only ${s.signed} today (${s.pctStr}) — this is noise, not signal. Most days look exactly like this, so there's nothing to do.` },

    { id: "down-mild", test: (s) => !s.up,
      line: (s) => `A soft ${s.signed} (${s.pctStr}) — ${s.driver}. Nothing unusual. ${s.overallUp ? `You're still up ${num(Math.abs(s.overallPlPct))}% all-time.` : "Down days are part of the ride."}` },

    { id: "up-broad", test: (s) => s.up && s.gainers.length >= Math.max(2, s.losers.length * 2) && s.absPct >= 0.12,
      line: (s) => `A clean green day: ${s.gainers.length} of ${s.n} holdings rose together. No single point of failure — that's the healthy kind of up.` },

    { id: "quiet", test: (s) => s.absPct < 0.12,
      line: (s) => `A quiet ${s.signed} today — less than a tenth of a percent. Your portfolio is just ticking along; nothing needs you.` },

    { id: "default", test: () => true,
      line: (s) => `You're ${s.up ? "up" : "down"} ${s.signed} today (${s.pctStr}), now at ${s.total}.${s.overallUp ? ` Still +${num(Math.abs(s.overallPlPct))}% all-time.` : ""}` },
  ];

  const fired = CASES.find((c) => c.test(s)) || CASES[CASES.length - 1];
  return { headline: fired.line(s), caseId: fired.id, signals: signals.slice(0, 3), note: "A read of your own numbers — not advice. Generated on device." };
}

function App({ theme, setTheme, dataApi, native = false, variant = "draft", setVariant }) {
  const providerRef = useRef(null);if (!providerRef.current) providerRef.current = makeMockProvider();
  const histRef = useRef({});
  const relRef = useRef(null);if (!relRef.current) relRef.current = buildRelative();
  const assetRelRef = useRef({});
  const getAssetRel = (symbol) => {
    if (!assetRelRef.current[symbol]) {
      const a = CATALOG[symbol] || { type: "stock" };
      if (a.type === "cash" || a.custom) {assetRelRef.current[symbol] = new Array(N).fill(1);return assetRelRef.current[symbol];}
      const beta = a.type === "crypto" ? 1.7 : a.type === "etf" ? 0.8 : a.type === "bond" ? 0.18 : a.type === "token" ? 0.1 : 1.05;
      let hsh = 0;for (let i = 0; i < symbol.length; i++) hsh = (hsh * 31 + symbol.charCodeAt(i)) % 1000;
      const amp = a.type === "bond" || a.type === "token" ? 0.01 + hsh / 1000 * 0.012 : 0.04 + hsh / 1000 * 0.06; // bonds/tokens: tight 1–2% spread; others 4–10%
      assetRelRef.current[symbol] = buildAssetRel(relRef.current, beta, amp);
    }
    return assetRelRef.current[symbol];
  };
  const liveBaseRef = useRef(null);
  const V = VARIANTS[variant] || VARIANTS.draft;
  const effTheme = theme;
  const pal = V.colors ? (V.colors[effTheme] || V.colors.light || V.colors.dark) : null;
  const C = pal ? { ...THEME[effTheme], ...pal } : THEME[effTheme];
  // Text color to sit on an accent-filled button — depends on both version and theme.
  const onAccent = variant === "wsb" ? (effTheme === "light" ? "#FFFFFF" : "#0C0C0C") : variant === "data" ? (effTheme === "light" ? "#FFFFFF" : C.bg) : "#FFFFFF";

  const [transactions, setTransactions] = useState(() => {try {const s = sessionStorage.getItem("pt-transactions-v4");if (s) {const arr = JSON.parse(s);if (Array.isArray(arr) && arr.length) {arr.forEach((t) => {const m = /^t(\d+)$/.exec(t.id || "");if (m) _id = Math.max(_id, +m[1]);});return arr;}}} catch {}return SEED;});
  useEffect(() => {try {sessionStorage.setItem("pt-transactions-v4", JSON.stringify(transactions));} catch {}}, [transactions]);
  const [dayDown, setDayDown] = useState(false); // demo toggles up/down each load → every 2nd demo is a red day
  const [classModal, setClassModal] = useState(null);
  const [verSheet, setVerSheet] = useState(false);
  const [settingsPage, setSettingsPage] = useState(null);const [ccyOpen, setCcyOpen] = useState(false);
  const [chooser, setChooser] = useState(() => {try {return !sessionStorage.getItem("pt-ui-chosen");} catch (e) {return true;}});
  const [greenHack, setGreenHack] = useState(0);const greenHackTimer = useRef(null);
  const markChosen = () => {try {sessionStorage.setItem("pt-ui-chosen", "1");} catch (e) {}setChooser(false);};
  useEffect(() => {if (dataApi) dataApi.current = { reset: () => {setTransactions([]);setSelected(null);setTab("insights");setDayDown(false);markChosen();}, demo: () => {setTransactions(SEED);setSelected(null);setTab("insights");setDayDown((d) => !d);markChosen();} };});
  // Switching version from the showcase rail counts as choosing it — never bounce the user back through the v0.1-led launch chooser. (Skip the initial mount so a first-time visitor still gets the chooser.)
  const variantTouched = useRef(false);
  useEffect(() => {if (!variantTouched.current) {variantTouched.current = true;return;}markChosen();}, [variant]);
  const [prices, setPrices] = useState(() => providerRef.current.snapshot());
  const [liveStatus, setLiveStatus] = useState(() => ({ crypto: "\u2026", etf: window.PT_PROXY_URL ? "\u2026" : "off" }));
  const [, setFxTick] = useState(0);
  const [tab, setTab] = useState("insights");
  const [heroView, setHeroView] = useState("daily"); // "daily" | "total"
  const [downToast, setDownToast] = useState(false);
  const [greenToast, setGreenToast] = useState(false);const greenToastTimer = useRef(null);
  const heroM = 1; // numbers show final value immediately; appearance is animated via heroPop scale
  const [selected, setSelected] = useState(null);
  const [subtab, setSubtab] = useState("general");
  const [range, setRange] = useState("1D");
  const [refreshing, setRefreshing] = useState(false);
  const [insight, setInsight] = useState(null);const [insightBusy, setInsightBusy] = useState(false);const [insightOpen, setInsightOpen] = useState(false);
  const [scrub, setScrub] = useState(null);const [endDotBig, setEndDotBig] = useState(false);
  const [showAll, setShowAll] = useState(false);const [toast, setToast] = useState(null);
  const [txFilter, setTxFilter] = useState("all");const [txSort, setTxSort] = useState("new");
  const scrollRef = useRef(null);const lastScroll = useRef(0);const [navCollapsed, setNavCollapsed] = useState(false);const [scrolled, setScrolled] = useState(false);
  const toastRef = useRef(null);const showToast = (msg) => {setToast(msg);clearTimeout(toastRef.current);toastRef.current = setTimeout(() => setToast(null), 2600);};
  useEffect(() => {setScrub(null);}, [range]);
  // Haptics — play a subtle tap on any in-app button/control press (delegated)
  useEffect(() => {
    const onDown = (e) => {const tgt = e.target;if (!tgt || !tgt.closest) return;const b = tgt.closest("button, [role=button], input, a");if (b && b.closest(".pt-root")) Haptics.tap();};
    document.addEventListener("pointerdown", onDown, true);
    return () => document.removeEventListener("pointerdown", onDown, true);
  }, []);
  useEffect(() => {if (scrollRef.current) scrollRef.current.scrollTop = 0;setScrolled(false);setNavCollapsed(false);}, [tab, selected]);

  const [counter, setCounter] = useState("USD");const [display, setDisplay] = useState("EUR");const [ccyPop, setCcyPop] = useState(false);
  const lpRef = useRef(null);const lpFired = useRef(false);

  const [text, setText] = useState("");const [busy, setBusy] = useState(false);const [error, setError] = useState("");const [flash, setFlash] = useState(null);
  const [addOpen, setAddOpen] = useState(false);const [addMode, setAddMode] = useState("menu");
  const [manualStep, setManualStep] = useState("class");const [manualClass, setManualClass] = useState("etf");const [assetQuery, setAssetQuery] = useState("");
  const [mForm, setMForm] = useState({ action: "buy", symbol: "SWDA.MI", quantity: "", price: "", date: today() });
  const [cDirect, setCDirect] = useState(false);
  const [cForm, setCForm] = useState({ category: "etf", newCat: false, catName: "", name: "", symbol: "", quantity: "", price: "", currency: "EUR", date: today() });
  const [sheetText, setSheetText] = useState("");const [sheetBusy, setSheetBusy] = useState(false);const [sheetErr, setSheetErr] = useState("");
  const [bulkText, setBulkText] = useState("");const [bulkParsed, setBulkParsed] = useState(null);const [bulkBusy, setBulkBusy] = useState(false);const [bulkErr, setBulkErr] = useState("");
  // File import (CSV / Excel)
  const [impRows, setImpRows] = useState(null);const [impErr, setImpErr] = useState("");const [impName, setImpName] = useState("");const [impMode, setImpMode] = useState("add");const [impConfirm, setImpConfirm] = useState(false);const impInputRef = useRef(null);const [killConfirm, setKillConfirm] = useState(false);

  const getHistory = (symbol, endPx, type) => {if (!histRef.current[symbol]) histRef.current[symbol] = genHistory(endPx ?? CATALOG[symbol]?.base ?? 100, type ?? CATALOG[symbol]?.type);return histRef.current[symbol];};
  useEffect(() => {const ms = range === "LIVE" ? 4000 : REFRESH_MS;const id = setInterval(() => setPrices(providerRef.current.tick()), ms);return () => clearInterval(id);}, [range]);
  // ── Real price feed: CoinGecko (crypto, live EUR/USD) + Twelve Data proxy (ETF, EUR). ──
  useEffect(() => {
    if (!window.LivePrices) return;
    return window.LivePrices.start({
      symbols: Object.keys(CATALOG),
      typeOf: (s) => CATALOG[s]?.type,
      currencyOf: (s) => CATALOG[s]?.currency || "EUR",
      onPrices: (map) => {Object.entries(map).forEach(([s, p]) => providerRef.current.set(s, p));setPrices(providerRef.current.snapshot());},
      setFxUsd: (rate) => {if (rate > 0.5 && rate < 1.5) {FX.USD = rate;setFxTick((t) => t + 1);}},
      setStatus: (k, v) => setLiveStatus((s) => s[k] === v ? s : { ...s, [k]: v })
    });
  }, []);
  const liveOn = liveStatus.crypto === "live" || liveStatus.etf === "live";
  const refresh = () => {setRefreshing(true);setPrices(providerRef.current.tick());setTimeout(() => setRefreshing(false), 500);};

  const holdings = useMemo(() => computeHoldings(transactions, prices), [transactions, prices]);
  const positionOf = (sym) => {const r = resolveSymbol(sym);const h = holdings.find((x) => x.symbol === r);return h ? h.qty : 0;};
  const impAudit = useMemo(() => impRows ? auditSells(impMode === "replace" ? [] : transactions, impRows) : [], [impRows, impMode, transactions]);
  const bulkAudit = useMemo(() => bulkParsed ? auditSells(transactions, bulkParsed) : [], [bulkParsed, transactions]);
  const totals = useMemo(() => {const value = holdings.reduce((s, h) => s + h.valEUR, 0);const cost = holdings.reduce((s, h) => s + h.costEUR, 0);const pl = value - cost;return { value, cost, pl, plPct: cost ? pl / cost * 100 : 0 };}, [holdings]);
  if (liveBaseRef.current == null && totals.value > 0) liveBaseRef.current = totals.value / 1.0056;

  const startIdx = () => Math.max(0, N - RANGES[range]);const rel = relRef.current;
  // Everything derives from per-asset relative paths so the total === sum of holdings.
  const holdingChange = (h) => {const r = getAssetRel(h.symbol);const sf = r[startIdx()] || 1;const abs = h.valEUR * (1 - sf);return { abs, pct: sf ? (1 - sf) / sf * 100 : 0 };};
  const rangeChange = useMemo(() => {let abs = 0,startVal = 0;for (const h of holdings) {const sf = getAssetRel(h.symbol)[startIdx()] || 1;abs += h.valEUR * (1 - sf);startVal += h.valEUR * sf;}return { abs, pct: startVal ? abs / startVal * 100 : 0 };}, [range, holdings]);
  const totalSeries = useMemo(() => {const s = startIdx();const len = N - s;const step = Math.max(1, Math.floor(len / 60));const raw = [];for (let i = s; i < N; i += step) {let v = 0;for (const h of holdings) v += h.valEUR * (getAssetRel(h.symbol)[i] ?? 1);raw.push(v);}const w = 2;const out = raw.map((_, k) => {let sum = 0,c = 0;for (let j = -w; j <= w; j++) {const idx = k + j;if (idx >= 0 && idx < raw.length) {sum += raw[idx];c++;}}return { i: k, v: sum / c };});if (out.length) out[out.length - 1] = { i: out.length - 1, v: totals.value };return out;}, [range, holdings, totals.value]);
  const assetSeries = (h) => {const r = getAssetRel(h.symbol);const s = startIdx();const len = N - s;const step = Math.max(1, Math.floor(len / 120));const out = [];for (let i = s, k = 0; i < N; i += step, k++) out.push({ i: k, v: h.px * r[i] });if (out.length) out[out.length - 1] = { i: out.length - 1, v: h.px };return out;};
  const assetChange = (h) => {const sf = getAssetRel(h.symbol)[startIdx()] || 1;return sf ? (1 - sf) / sf * 100 : 0;};

  // ── Insights: fixed 1-day window. Headline pinned to +1% (demo); categories aggregate per-asset paths ──
  const dayIdx = N - RANGES["1D"];
  const dayShapeRef = useRef(null);
  if (!dayShapeRef.current) {const n = 48;const walk = [0];for (let i = 1; i < n; i++) walk.push(walk[i - 1] + (Math.random() - 0.5));const wend = walk[n - 1];let maxb = 1e-6;const bridge = walk.map((v, i) => {const b = v - wend * (i / (n - 1));maxb = Math.max(maxb, Math.abs(b));return b;});dayShapeRef.current = bridge.map((b, i) => i / (n - 1) + b / maxb * 0.22);}
  const dayPct = dayDown ? -0.71 : 1.65;
  const dayChange = useMemo(() => {const f = dayPct / 100;return { abs: totals.value * (1 - 1 / (1 + f)), pct: dayPct };}, [totals.value, dayPct]);
  const daySeries = useMemo(() => {const f = dayPct / 100;return dayShapeRef.current.map((t, i) => ({ i, v: totals.value / (1 + f) * (1 + f * t) }));}, [totals.value, dayPct]);
  // Daily direction → drives WSB full-screen color wash + deep-tinted chrome
  const dayUp = dayChange.abs >= 0;
  const wsbWash = variant === "wsb";
  // Reassurance toast — slides in from the top when the WSB wash turns red on the Insights screen
  useEffect(() => {
    if (wsbWash && tab === "insights" && !selected && !dayUp) {
      setDownToast(true);
      const t = setTimeout(() => setDownToast(false), 4200);
      return () => clearTimeout(t);
    }
    setDownToast(false);
  }, [wsbWash, tab, selected, dayUp]);
  const lerpHex = (a, b, t) => {const pa = parseInt(a.slice(1), 16),pb = parseInt(b.slice(1), 16);const r = Math.round((pa >> 16 & 255) + ((pb >> 16 & 255) - (pa >> 16 & 255)) * t);const g = Math.round((pa >> 8 & 255) + ((pb >> 8 & 255) - (pa >> 8 & 255)) * t);const bl = Math.round((pa & 255) + ((pb & 255) - (pa & 255)) * t);return "#" + ((1 << 24) + (r << 16) + (g << 8) + bl).toString(16).slice(1);};
  const washP = dayUp ? 1 : greenHack; // 0 = full red, 1 = full green (joke easter-egg on red days)
  const washBg = effTheme === "light"
    ? `radial-gradient(135% 95% at 50% -14%, ${lerpHex("#FFB4AA", "#90E6B4", washP)} 0%, ${lerpHex("#FFDED8", "#D5F6E3", washP)} 46%, #FFFFFF 100%)`
    : `radial-gradient(135% 95% at 50% -12%, ${lerpHex("#7E2C23", "#2F7C49", washP)} 0%, ${lerpHex("#3A1611", "#143E27", washP)} 42%, ${lerpHex("#0C0706", "#070D09", washP)} 100%)`;
  // Easter egg: on a red WSB day, tapping the amount fast nudges the whole UI greener (it snaps back after ~1.4s).
  const bumpGreen = () => {if (!(wsbWash && !dayUp)) return;Haptics.tap();setGreenHack((p) => {const n = Math.min(1, p + 0.06);if (n >= 1 && p < 1) {Haptics.success();setGreenToast(true);clearTimeout(greenToastTimer.current);greenToastTimer.current = setTimeout(() => setGreenToast(false), 3200);}return n;});clearTimeout(greenHackTimer.current);greenHackTimer.current = setTimeout(() => setGreenHack(0), 1400);};
  const washPrimary = effTheme === "light"
    ? (dayUp ? "#0E8C3E" : lerpHex("#D83A2C", "#0E8C3E", greenHack))
    : (dayUp ? "#4ADE80" : lerpHex("#FF6B5E", "#4ADE80", greenHack));
  const washSecondary = effTheme === "light"
    ? (dayUp ? "#4F8163" : lerpHex("#B26056", "#4F8163", greenHack))
    : (dayUp ? "#8FC7A2" : lerpHex("#E0A79E", "#8FC7A2", greenHack));
  // ── iOS-26 Liquid Glass tab bar (translucent, never colored) ──
  const lightTabs = effTheme !== "dark";
  const tabBarStyle = lightTabs ?
  { background: "rgba(255,255,255,0.42)", backdropFilter: "blur(22px) saturate(1.8)", WebkitBackdropFilter: "blur(22px) saturate(1.8)", border: "1px solid rgba(255,255,255,0.60)", boxShadow: "0 10px 30px rgba(0,0,0,0.14), inset 0 1px 1px rgba(255,255,255,0.75)" } :
  { background: "rgba(44,46,50,0.45)", backdropFilter: "blur(22px) saturate(1.8)", WebkitBackdropFilter: "blur(22px) saturate(1.8)", border: "1px solid rgba(255,255,255,0.14)", boxShadow: "0 10px 30px rgba(0,0,0,0.40), inset 0 1px 1px rgba(255,255,255,0.42)" };
  const tabActiveStyle = lightTabs ?
  { background: "rgba(255,255,255,0.90)", boxShadow: "0 2px 8px rgba(0,0,0,0.10)", color: "#1C1B19" } :
  { background: "rgba(255,255,255,0.16)", boxShadow: "0 2px 8px rgba(0,0,0,0.25)", color: C.ink };
  const tabInactiveColor = lightTabs ? "#55534E" : C.mute;
  const fabStyle = { background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, color: C.ink };
  // ── Shared liquid-glass card surface ── white for light versions (Draft/Editorial), dark for WSB/Data ──
  const glassDark = effTheme === "dark";
  const cardBg = glassDark ?
  { background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.08)" } :
  { background: "rgba(255,255,255,0.74)", backdropFilter: "blur(20px) saturate(1.6)", WebkitBackdropFilter: "blur(20px) saturate(1.6)", border: "1px solid rgba(255,255,255,0.95)", boxShadow: "0 4px 14px rgba(0,0,0,0.10), 0 1px 3px rgba(0,0,0,0.07)" };

  async function callClaude(prompt) {if (!(window.claude && window.claude.complete)) throw new Error("AI unavailable");return String(await window.claude.complete(prompt)).trim();}
  const stripJSON = (s) => s.replace(/```json|```/g, "").trim();
  function ensureAsset(symbol, t) {if (!CATALOG[symbol]) {CATALOG[symbol] = { ticker: symbol, name: symbol, currency: t.currency, base: t.price, type: "stock", exchange: "—" };providerRef.current.set(symbol, t.price);}}
  function commit(list) {const clean = list.map((o) => {const symbol = resolveSymbol(o.symbol);const t = { id: nid(), action: o.action === "sell" ? "sell" : "buy", symbol, quantity: Number(o.quantity), price: Number(o.price), currency: o.currency || CATALOG[symbol]?.currency || "EUR", date: o.date || today() };ensureAsset(symbol, t);return t;});setPrices(providerRef.current.snapshot());setTransactions((prev) => [...prev, ...clean]);return clean;}

  async function quickAdd() {const input = text.trim();if (!input || busy) return;setBusy(true);setError("");try {const obj = JSON.parse(stripJSON(await callClaude(`Extract ONE transaction (en/it), reply ONLY JSON: {"action":"buy"|"sell","symbol":"TICKER","quantity":number,"price":number,"currency":"EUR"|"USD"|"GBP","date":"YYYY-MM-DD"}. Missing date=${today()}. Missing action="buy". Text:"${input}"`)));if (!obj.symbol || !obj.quantity || !obj.price) throw 0;if (obj.action === "sell") {const rs = resolveSymbol(obj.symbol);const have = positionOf(obj.symbol);if (have <= 1e-9) {setError(`You don’t hold any ${rs} to sell yet.`);return;}if (Number(obj.quantity) > have + 1e-9) {setError(`You only hold ${num(have)} ${rs} — can’t sell ${num(Number(obj.quantity))}.`);return;}}const [t] = commit([obj]);setText("");setFlash(`${t.action === "buy" ? "Bought" : "Sold"} ${num(t.quantity)} ${t.symbol} @ ${num(t.price)} ${t.currency}`);setTimeout(() => setFlash(null), 3500);} catch {setError("Didn't catch that. Try: \u201cbought 10 @SWDA at 121\u201d.");} finally {setBusy(false);}}
  function explainDay() {
    setInsightOpen(true);
    Haptics.tap();
    if (insightBusy) return;
    setInsight(null);
    setInsightBusy(true);
    const hs = holdings.map((h) => ({ ticker: h.ticker, type: h.type || "stock", valEUR: h.valEUR, dayAbs: holdingChange(h).abs }));
    const ins = buildInsights({ value: totals.value, dayAbs: dayChange.abs, dayPct: dayChange.pct, holdings: hs, money: (v) => dispMoney(v, display), num, overallPl: totals.pl, overallPlPct: totals.plPct });
    // brief "reading" beat so the on-device generation feels deliberate
    setTimeout(() => { setInsight(ins); setInsightBusy(false); }, 430);
  }

  const mention = useMemo(() => {const m = text.match(/(?:^|\s)@(\w*)$/);if (!m) return null;const q = m[1].toLowerCase();const list = Object.entries(CATALOG).filter(([s, a]) => s.toLowerCase().includes(q) || a.name.toLowerCase().includes(q)).sort((a, b) => (a[1].rank || 99) - (b[1].rank || 99)).slice(0, 6);return { q, list };}, [text]);
  const applyMention = (sym) => setText(text.replace(/@(\w*)$/, sym + " "));

  const startLP = () => {lpFired.current = false;lpRef.current = setTimeout(() => {lpFired.current = true;setCcyPop(true);}, 420);};
  const endLP = () => clearTimeout(lpRef.current);
  const tapCcy = () => {if (lpFired.current) {lpFired.current = false;return;}setDisplay((d) => d === "EUR" ? counter : "EUR");};
  const pickPair = (c) => {setCounter(c);setDisplay(c);setCcyPop(false);};

  const up = rangeChange.abs >= 0;const live = range === "LIVE";
  const scrubBase = totalSeries[0]?.v ?? 0;
  const shownVal = scrub ? scrub.v : totals.value;
  const shownChange = scrub ? { abs: scrub.v - scrubBase, pct: scrubBase ? (scrub.v - scrubBase) / scrubBase * 100 : 0 } : range === "1D" ? dayChange : rangeChange;
  const upShown = shownChange.abs >= 0;
  const glass = { background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}` };
  // User / version button — opens the in-app version switcher (top-right on every screen)
  const userBtn = setVariant ? <button onClick={() => setVerSheet(true)} aria-label="Switch version" style={{ color: C.ink, background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, borderRadius: 999, width: 34, height: 34, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.10)" }} className="flex items-center justify-center shrink-0 active:opacity-80 active:scale-95 transition-transform"><User size={17} /></button> : null;

  const Dot = ({ size = 7 }) => <span className="pulse" style={{ width: size, height: size, borderRadius: size / 2, background: C.up, display: "inline-block" }} />;
  // Launch chooser: descriptions + a mini look&feel preview per version
  const VERSION_DESC = {
    draft: "The clean iOS baseline — your whole portfolio at a glance, calm and familiar.",
    editorial: "Print-inspired serif. Your daily change reads like a newspaper headline.",
    wsb: "Add your portfolio and watch the screen turn green when you're up, red when you're down.",
    data: "A terminal-style, numbers-first cockpit in monospace — built for the data-obsessed."
  };
  // Launch-flow data + component moved to module scope (see above) so it never remounts on App re-render.
  function VersionPreview({ id }) {
    const up = !dayDown;
    const g = up ? "#1F9E5A" : "#D8412C";
    const box = { width: 104, height: 70, flexShrink: 0, display: "flex", flexDirection: "column", justifyContent: "center", gap: 3, overflow: "hidden" };
    if (id === "wsb") {
      const wash = up ? "linear-gradient(160deg,#0B3D24 0%,#0E5C34 55%,#0A4A2A 100%)" : "linear-gradient(160deg,#3D0E0E 0%,#5C1414 55%,#4A1010 100%)";
      return <div style={{ ...box, alignItems: "center", textAlign: "center", background: wash, color: "#EDF4EF", gap: 2 }}>
        <div style={{ fontSize: 6, letterSpacing: "0.12em", textTransform: "uppercase", opacity: 0.75 }}>Today</div>
        <div style={{ fontFamily: "Inter, sans-serif", fontSize: 18, fontWeight: 800, letterSpacing: "-0.03em" }}>{up ? "+" : "−"}€9,841</div>
        <div style={{ fontSize: 6.5, opacity: 0.85 }}>{up ? "up +1.65%" : "down −1.4%"}</div>
      </div>;
    }
    if (id === "data") {
      return <div style={{ ...box, background: "#0A0B0D", color: "#D7DEE8", padding: "0 10px" }}>
        <div style={{ fontSize: 5.5, letterSpacing: "0.06em", textTransform: "uppercase", color: "#7E8896", fontFamily: "'JetBrains Mono', monospace" }}>Portfolio is</div>
        <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 16, fontWeight: 700, letterSpacing: "-0.02em" }}>{up ? "+" : "−"}€9,841<span style={{ color: "#525C6A", fontSize: "0.6em" }}>.08</span></div>
        <div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: 7, color: g }}>{up ? "↑ +1.65%" : "↓ −1.40%"}</div>
      </div>;
    }
    if (id === "editorial") {
      return <div style={{ ...box, background: "#FFFFFF", padding: "0 10px", gap: 4 }}>
        <div style={{ fontSize: 5.5, letterSpacing: "0.12em", textTransform: "uppercase", color: "#9C9A93", fontWeight: 600 }}>Sun · 14 Jun</div>
        <div style={{ fontFamily: "'Instrument Serif', Georgia, serif", fontSize: 13, lineHeight: 1.12, color: "#1C1B19" }}>Grew by <span style={{ color: g, fontWeight: 600, borderBottom: `1.5px solid ${up ? "rgba(24,163,90,0.35)" : "rgba(217,68,44,0.35)"}` }}>€8,472</span> today</div>
      </div>;
    }
    return <div style={{ ...box, background: "#F2F3F5", padding: "0 10px" }}>
      <div style={{ fontSize: 6.5, color: "#6B7280" }}>Today</div>
      <div style={{ fontFamily: "Inter, sans-serif", fontSize: 18, fontWeight: 700, color: "#15171B", letterSpacing: "-0.02em" }}>{up ? "+" : "−"}€9,233</div>
      <div style={{ display: "flex", alignItems: "center", gap: 4 }}><span style={{ background: up ? "rgba(31,158,90,0.14)" : "rgba(216,65,44,0.14)", color: g, borderRadius: 5, fontSize: 7, fontWeight: 600, padding: "1px 5px" }}>{up ? "+1.65%" : "−1.40%"}</span></div>
    </div>;
  }
  const Logo = ({ symbol, size = 38 }) => {const a = CATALOG[symbol] || {};const bg = a.color || hashColor(symbol);return <div style={{ width: size, height: size, borderRadius: size / 2, background: bg, flexShrink: 0 }} className="flex items-center justify-center"><span style={{ fontSize: size > 40 ? 11 : 9, color: a.dark ? "#000" : "#fff", fontWeight: 800, letterSpacing: "-0.02em" }} className="font-mono">{lettersOf(symbol)}</span></div>;};
  const Badge = ({ pct }) => {const u = pct >= 0;return <span style={{ background: u ? "rgba(52,160,90,0.16)" : "rgba(211,58,44,0.16)", color: u ? C.up : C.down, borderRadius: 6 }} className="text-xs font-mono font-medium px-1.5 py-0.5">{u ? "+" : ""}{num(pct)}%</span>;};
  const Spark = ({ data, h = 56 }) => <div style={{ width: "100%", height: h }}><ResponsiveContainer><AreaChart data={data} margin={{ top: 4, bottom: 4, left: 0, right: 0 }}><defs><linearGradient id="gUp" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor={C.up} stopOpacity={0.30} /><stop offset="100%" stopColor={C.up} stopOpacity={0} /></linearGradient></defs><YAxis domain={["dataMin", "dataMax"]} hide /><Area type="monotone" dataKey="v" stroke={C.up} strokeWidth={1.7} fill="url(#gUp)" isAnimationActive={!live} /></AreaChart></ResponsiveContainer></div>;
  // Insights mini-chart — gradient area, no interaction, green up / red down
  const MiniPerf = ({ data, up }) => {const col = up ? C.up : C.down;const fade = "linear-gradient(to right, transparent 0%, black 16%, black 84%, transparent 100%)";return <div style={{ width: "100%", height: 148, pointerEvents: "none", WebkitMaskImage: fade, maskImage: fade }}><div style={{ width: "100%", height: "100%", animation: "miniReveal 1.2s cubic-bezier(.33,0,.18,1) both" }}><ResponsiveContainer><AreaChart data={data} margin={{ top: 26, bottom: 22, left: 0, right: 0 }}><defs><linearGradient id="gInsight" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor={col} stopOpacity={0.32} /><stop offset="100%" stopColor={col} stopOpacity={0} /></linearGradient></defs><YAxis domain={["dataMin", "dataMax"]} hide /><Area type="natural" dataKey="v" stroke={col} strokeWidth={2} fill="url(#gInsight)" isAnimationActive={false} /></AreaChart></ResponsiveContainer></div></div>;};
  const RangeBar = () => <div className="noscroll flex gap-1 overflow-x-auto" style={{ scrollbarWidth: "none" }}>{RANGE_KEYS.map((r) => <button key={r} onClick={() => setRange(r)} style={{ background: range === r ? C.panel2 : "transparent", color: range === r ? C.ink : C.faint, borderRadius: 8, flexShrink: 0 }} className="font-mono px-2.5 py-1 text-xs flex items-center gap-1">{r === "LIVE" && <Dot size={6} />}{r}</button>)}</div>;
  const Field = ({ k, v, c }) => <div><div style={{ color: C.faint }} className="text-xs mb-1">{k}</div><div className="font-mono tabular-nums text-sm" style={{ color: c || C.ink }}>{v}</div></div>;
  const Stat = ({ k, v, c, align = "center" }) => <div style={{ textAlign: align }}><div style={{ color: C.mute }} className="text-xs mb-1">{k}</div><div className="font-mono tabular-nums text-sm" style={{ color: c || C.ink }}>{v}</div></div>;
  const Segmented = ({ value, onChange, options }) => <div style={{ background: C.panel2, borderRadius: 9, padding: 3 }} className="flex">{options.map((o) => {const on = value === o.id;return <button key={o.id} onClick={() => onChange(o.id)} style={{ flex: 1, background: on ? C.panel : "transparent", color: on ? o.color || C.ink : C.mute, borderRadius: 7, boxShadow: on ? "0 1px 3px rgba(0,0,0,0.25)" : "none" }} className="py-2 text-sm font-medium transition-all">{o.label}</button>;})}</div>;
  const Hero = ({ vEUR, ccy }) => {const parts = num(fromEUR(vEUR, ccy)).split(".");const s = CCY_SYM[ccy] || ccy;const sp = s.length > 1 ? s + " " : s;return <span className="tabular-nums pt-hero" style={{ letterSpacing: "-0.02em", "--rollY": "-.55em" }}><RollDigits className="font-bold" style={{ fontSize: 32 }} text={sp + parts[0]} /><RollDigits className="font-bold" style={{ fontSize: 20, opacity: 0.55 }} text={"." + parts[1]} /></span>;};

  // Card transazione multi-riga (stile screenshot) — usata nel tab Transactions
  function TxCard({ t }) {
    const buy = t.action === "buy";const px = prices[t.symbol] ?? CATALOG[t.symbol]?.base ?? t.price;
    const cost = t.quantity * t.price,worth = t.quantity * px,delta = cost ? (worth - cost) / cost * 100 : 0,dUp = delta >= 0;
    return (
      <div style={{ ...cardBg, borderRadius: 16 }} className="p-4">
        <div className="flex items-center justify-between mb-4">
          <div className="flex items-center gap-3 min-w-0"><Logo symbol={t.symbol} size={34} /><div className="min-w-0"><div className="font-semibold text-sm truncate">{CATALOG[t.symbol]?.ticker || t.symbol}</div><div style={{ color: C.faint }} className="text-xs">{fmtDateTime(t.date)}</div></div></div>
          <span style={{ border: `1px solid ${buy ? C.up : C.down}`, color: buy ? C.up : C.down, borderRadius: 6 }} className="text-xs font-medium px-2 py-0.5 shrink-0">{buy ? "Buy" : "Sell"}</span>
        </div>
        <div className="grid grid-cols-2 gap-y-3.5 gap-x-4">
          <Field k="Buy Price" v={money(t.price, t.currency)} /><Field k="Amount Added" v={num(t.quantity)} />
          <Field k="Cost (Incl. Fee)" v={money(cost, t.currency)} /><Field k="Worth" v={money(worth, t.currency)} />
          <Field k="Delta" v={`${dUp ? "+" : ""}${num(delta)}%`} c={dUp ? C.up : C.down} />
        </div>
      </div>);

  }
  // Card transazione con timeline (dettaglio di un asset)
  function TxTimelineCard({ t, last }) {
    const buy = t.action === "buy";const px = prices[t.symbol] ?? CATALOG[t.symbol]?.base ?? t.price;
    const cost = t.quantity * t.price,worth = t.quantity * px,delta = cost ? (worth - cost) / cost * 100 : 0,dUp = delta >= 0;
    return <div className="relative pl-7">{!last && <div style={{ position: "absolute", left: 13, top: 24, bottom: -12, width: 2, background: C.line }} />}<div style={{ position: "absolute", left: 7, top: 7, width: 13, height: 13, borderRadius: 7, background: C.bg, border: `2px solid ${buy ? C.up : C.down}` }} /><div className="flex items-center justify-between mb-2"><span style={{ border: `1px solid ${buy ? C.up : C.down}`, color: buy ? C.up : C.down, borderRadius: 6 }} className="text-xs font-medium px-2 py-0.5">{buy ? "Buy" : "Sell"}</span><span style={{ color: C.mute }} className="text-xs">{fmtDateTime(t.date)}</span></div><div style={{ ...cardBg, borderRadius: 16 }} className="p-4 mb-4 grid grid-cols-2 gap-y-3.5 gap-x-4"><Field k="Buy Price" v={money(t.price, t.currency)} /><Field k="Amount Added" v={num(t.quantity)} /><Field k="Cost (Incl. Fee)" v={money(cost, t.currency)} /><Field k="Worth" v={money(worth, t.currency)} /><Field k="Delta" v={`${dUp ? "+" : ""}${num(delta)}%`} c={dUp ? C.up : C.down} /></div></div>;}

  function rwaCardEl(t) {
    if (!t) return null;
    const fmtAUM = (n) => n >= 1e9 ? `$${(n / 1e9).toFixed(2)}B` : n >= 1e6 ? `$${Math.round(n / 1e6)}M` : n >= 1e3 ? `$${Math.round(n / 1e3)}K` : `$${n}`;
    const fmtTicket = (n) => {const m = n / 1e6;if (n >= 1e6) return `$${Number.isInteger(m) ? m : m.toFixed(1)}M`;if (n >= 1e3) return `$${Math.round(n / 1e3)}K`;return `$${n}`;};
    const fmtHolders = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : `${n}`;
    const apy = t.apy7DayPercent != null ? `${t.apy7DayPercent.toFixed(2)}%` : "\u2014";
    const navStr = t.navUsd != null ? `$${Number(t.navUsd.toFixed(4))}` : "\u2014";
    const nets = t.supportedNetworks || [];
    const rows = [["Issuer", t.issuer], ["Platform", t.platform], ["Asset class", t.assetClass], ["Custodian", t.custodian], ["Investor type", t.investorType]].filter(([, v]) => v);
    return <>
      <div style={{ ...cardBg, borderRadius: 18 }} className="px-4 py-4 mb-3">
        <div className="flex items-center justify-between mb-4">
          <span style={{ color: C.mute }} className="text-xs font-medium">Tokenized fund</span>
          {t.badge && <span style={{ background: C.panel2, color: C.accentInk, borderRadius: 999 }} className="text-[10px] font-semibold px-2 py-1 flex items-center gap-1"><Check size={10} />{t.badge}</span>}
        </div>
        <div className="grid grid-cols-2 gap-y-4">
          <Stat k="AUM" v={fmtAUM(t.aumUsd)} align="left" />
          <Stat k="7D APY" v={apy} c={C.up} align="left" />
          <Stat k="NAV" v={navStr} align="left" />
          <Stat k="Min ticket" v={fmtTicket(t.minimumSubscriptionUsd)} align="left" />
          <Stat k="Holders" v={fmtHolders(t.holders)} align="left" />
          <Stat k="Subscription" v={t.subscriptionFrequency || "\u2014"} align="left" />
        </div>
      </div>
      <div style={{ ...cardBg, borderRadius: 18 }} className="px-4 mb-3">
        {rows.map(([k, v], i) => <div key={k} className="flex items-center justify-between gap-3 py-3" style={{ borderTop: i ? `1px solid ${C.line}` : "none" }}><span style={{ color: C.mute }} className="text-sm shrink-0">{k}</span><span className="text-sm font-medium text-right" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", maxWidth: "62%" }}>{v}</span></div>)}
      </div>
      <div className="mb-5">
        <div style={{ color: C.mute }} className="text-xs mb-2 px-1">Available on {nets.length} network{nets.length === 1 ? "" : "s"}</div>
        <div className="flex flex-wrap gap-2">{nets.map((nw, i) => <span key={i} style={{ background: C.panel2, color: C.ink, borderRadius: 999 }} className="text-xs font-medium px-3 py-1.5">{nw}</span>)}</div>
      </div>
    </>;
  }

  function Detail({ h, modal }) {
    if (!h) return null;
    const data = assetSeries(h);const chgPct = assetChange(h);const dUp = chgPct >= 0;const hUp = h.pl >= 0;const custom = !!CATALOG[h.symbol]?.custom;const rwa = CATALOG[h.symbol]?.rwa;
    const txs = transactions.filter((t) => t.symbol === h.symbol).sort((a, b) => new Date(b.date) - new Date(a.date));
    return <div>
      {modal ?
      <div className="flex items-center justify-center mb-3"><Logo symbol={h.symbol} size={30} /></div> :
      <div className="flex items-center justify-between mb-4"><button onClick={() => setSelected(null)} style={{ color: C.accentInk }} className="flex items-center text-sm"><ChevronLeft size={22} />Portfolio</button><Logo symbol={h.symbol} size={30} /><div style={{ width: 78 }}></div></div>}
      <div className="text-center font-semibold leading-tight mb-1" style={{ fontSize: 17, color: C.ink }}>{h.ticker}</div>
      <div style={{ color: C.mute }} className="text-center text-sm mb-1">{h.name}</div>
      <div style={{ color: C.faint }} className="text-center text-xs mb-5">{h.exchange} · {h.currency}</div>
      <div style={{ ...cardBg, borderRadius: 18 }} className="grid grid-cols-2 gap-y-4 px-4 py-4 mb-5"><Stat k="Owned" v={qtyDetail(h.qty, h.type)} align="left" /><Stat k="Sum of Cost" v={dispMoney(h.costEUR, "EUR")} align="left" /><Stat k="Market Value" v={dispMoney(h.valEUR, "EUR")} align="left" /><Stat k="Total Gains" v={`${hUp ? "+" : ""}${dispMoney(h.pl, "EUR")}`} c={hUp ? C.up : C.down} align="left" /><Stat k="Total Fees" v="—" align="left" /><Stat k="Dividends" v="—" align="left" /></div>
      <div style={{ borderBottom: `1px solid ${C.line}` }} className="flex gap-7 mb-5">{[["general", "General"], ["transactions", "Transactions"]].map(([id, label]) => <button key={id} onClick={() => setSubtab(id)} style={{ color: subtab === id ? C.ink : C.faint, borderBottom: `2px solid ${subtab === id ? C.ink : "transparent"}` }} className="pb-3 text-sm font-medium -mb-px">{label}</button>)}</div>
      {subtab === "general" && <>{rwa && rwaCardEl(rwa)}<div className="flex items-end gap-2 mb-3"><span className="text-2xl font-semibold font-mono tabular-nums">{money(h.px, h.currency)}</span>{custom ? <span style={{ color: C.faint, border: `1px solid ${C.line}`, borderRadius: 6 }} className="text-xs mb-1 px-1.5 py-0.5">Frozen · paid price</span> : <><span className="text-sm font-mono tabular-nums mb-0.5" style={{ color: dUp ? C.up : C.down }}>{dUp ? "+" : ""}{num(chgPct)}%</span><span style={{ color: C.faint }} className="text-xs mb-1 flex items-center gap-1">{live && <Dot size={6} />}{range}</span></>}</div>{custom ? <div style={{ ...cardBg, borderRadius: 16 }} className="px-5 py-7 mb-3 text-center"><div style={{ color: C.mute, fontSize: 13, lineHeight: 1.5 }}>No live price for custom assets — the value stays frozen at what you paid. Use the Transactions tab to log more.</div></div> : <div style={{ ...cardBg, borderRadius: 16 }} className="p-3 mb-3"><div className="flex items-center justify-between mb-1 px-1 gap-3"><span style={{ color: C.faint }} className="text-xs shrink-0">Chart</span><RangeBar /></div><Spark data={data} h={150} /></div>}</>}
      {subtab === "transactions" && <><div className="grid grid-cols-3 mb-6"><Stat k="Avg. Buy Price" v={money(h.avg, h.currency)} align="left" /><Stat k="Avg. Sell Price" v="—" align="left" /><Stat k="# Transactions" v={txs.length} align="left" /></div><button onClick={() => {setSelected(null);openAdd("manual", h.symbol);}} className="flex items-center gap-3 mb-6"><span style={{ background: C.ink, color: C.bg, width: 40, height: 40, borderRadius: 20 }} className="flex items-center justify-center"><Plus size={22} /></span><span className="font-medium">New Transaction</span></button><div>{txs.map((t, i) => <TxTimelineCard key={t.id} t={t} last={i === txs.length - 1} />)}</div></>}
    </div>;
  }

  const allTx = [...transactions].sort((a, b) => new Date(b.date) - new Date(a.date) || (b.id > a.id ? 1 : -1));
  const openAdd = (mode = "menu", sym) => {setAddMode(mode);if (mode === "manual") {if (sym) {setMForm((f) => ({ ...f, symbol: sym }));setManualStep("form");} else {setManualStep("class");setAssetQuery("");}}setAddOpen(true);};

  function addManual() {const a = CATALOG[mForm.symbol];const isCash = a?.type === "cash";const price = isCash ? "1" : mForm.price;if (!mForm.quantity || !price) return;commit([{ ...mForm, price, currency: a?.currency || "EUR" }]);showToast(`${mForm.action === "sell" ? "Sold" : "Added"} ${num(Number(mForm.quantity))} ${a?.ticker || mForm.symbol}`);setMForm((f) => ({ ...f, quantity: "", price: "" }));setAddOpen(false);Haptics.success();}
  const CUSTOM_COLORS = ["#6E7787", "#5B8C7B", "#9C6B8E", "#7B6BA8", "#C08A5A", "#5A7FA8"];
  function addCustom() {
    const useNew = cForm.newCat && cForm.catName.trim();
    const catId = useNew ? cForm.catName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "custom" : cForm.category;
    const isCash = !useNew && cForm.category === "cash";
    const price = isCash ? "1" : cForm.price;
    if (!cForm.name.trim() || !cForm.quantity || !isCash && !price || cForm.newCat && !cForm.catName.trim()) return;
    // build a unique symbol from the ticker (or name)
    const raw = (cForm.symbol || cForm.name).toUpperCase().replace(/[^A-Z0-9.]/g, "").slice(0, 12) || "CUSTOM";
    let symbol = raw,i = 1;
    while (CATALOG[symbol] && !CATALOG[symbol].custom) {symbol = raw + i;i++;}
    let hsh = 0;for (let k = 0; k < raw.length; k++) hsh = (hsh * 31 + raw.charCodeAt(k)) % 997;
    CATALOG[symbol] = { ticker: cForm.symbol ? cForm.symbol.toUpperCase() : symbol, name: cForm.name.trim(), currency: cForm.currency, base: Number(price) || 1, type: catId, catLabel: useNew ? cForm.catName.trim() : undefined, exchange: "Custom", custom: true, color: CUSTOM_COLORS[hsh % CUSTOM_COLORS.length], rank: 999 };
    providerRef.current.set(symbol, Number(price) || 1);
    delete assetRelRef.current[symbol];
    commit([{ action: "buy", symbol, quantity: cForm.quantity, price, currency: cForm.currency, date: cForm.date }]);
    showToast(`Added ${cForm.name.trim()}`);
    setCForm({ category: "etf", newCat: false, catName: "", name: "", symbol: "", quantity: "", price: "", currency: "EUR", date: today() });
    setAddOpen(false);Haptics.success();
  }
  async function sheetNL() {const input = sheetText.trim();if (!input || sheetBusy) return;setSheetBusy(true);setSheetErr("");try {const obj = JSON.parse(stripJSON(await callClaude(`Extract ONE transaction (en/it), reply ONLY JSON: {"action":"buy"|"sell","symbol":"TICKER","quantity":number,"price":number,"currency":"EUR"|"USD"|"GBP","date":"YYYY-MM-DD"}. Missing date=${today()}. Text:"${input}"`)));if (!obj.symbol || !obj.quantity || !obj.price) throw 0;if (obj.action === "sell") {const rs = resolveSymbol(obj.symbol);const have = positionOf(obj.symbol);if (have <= 1e-9) {setSheetErr(`You don’t hold any ${rs} to sell yet.`);setSheetBusy(false);return;}if (Number(obj.quantity) > have + 1e-9) {setSheetErr(`You only hold ${num(have)} ${rs} — can’t sell that much.`);setSheetBusy(false);return;}}commit([obj]);setSheetText("");setAddOpen(false);Haptics.success();showToast("Transaction added");} catch {setSheetErr("Couldn't read that.");} finally {setSheetBusy(false);}}
  async function parseBulk() {if (!bulkText.trim() || bulkBusy) return;setBulkBusy(true);setBulkErr("");setBulkParsed(null);try {const arr = JSON.parse(stripJSON(await callClaude(`Extract ALL transactions (one per line/sentence, en/it), reply ONLY a JSON ARRAY. Item: {"action":"buy"|"sell","symbol":"TICKER","quantity":number,"price":number,"currency":"EUR"|"USD"|"GBP","date":"YYYY-MM-DD"}. Missing date=${today()}. Missing action="buy".\n${bulkText}`)));if (!Array.isArray(arr) || !arr.length) throw 0;setBulkParsed(arr);} catch {setBulkErr("Couldn't parse. One per line, e.g. 10 SWDA at 113.87 on Mar 5 2026");} finally {setBulkBusy(false);}}
  function commitBulk() {const audit = auditSells(transactions, bulkParsed);const valid = bulkParsed.filter((_, i) => !audit[i].issue);const skipped = bulkParsed.length - valid.length;if (!valid.length) {Haptics.warn();showToast("Nothing added — those sells have no matching holdings");return;}const n = valid.length;commit(valid);setBulkParsed(null);setBulkText("");setAddOpen(false);Haptics.success();showToast(`${n} transaction${n > 1 ? "s" : ""} added${skipped ? ` · ${skipped} skipped` : ""}`);}
  // ── File import (CSV / Excel) ──
  const resetImport = () => {setImpRows(null);setImpErr("");setImpName("");setImpConfirm(false);setImpMode("add");if (impInputRef.current) impInputRef.current.value = "";};
  const IMP_KEYS = ["symbol", "ticker", "asset", "instrument", "isin", "quantity", "qty", "shares", "units", "amount", "size", "price", "cost", "rate", "fill", "action", "side", "operation", "currency", "ccy", "cur", "date", "time", "when", "executed"];
  const kwHits = (cells) => cells.reduce((n, c) => {const lc = String(c == null ? "" : c).toLowerCase().trim();return n + (IMP_KEYS.some((k) => lc === k || lc.includes(k)) ? 1 : 0);}, 0);
  function normDate(v) {
    if (v == null || v === "") return "";
    if (typeof v === "number") {const d = new Date(Math.round((v - 25569) * 86400 * 1000));return isNaN(d) ? "" : d.toISOString().slice(0, 10);}
    if (v instanceof Date) return isNaN(v) ? "" : v.toISOString().slice(0, 10);
    const s = String(v).trim();
    let m = s.match(/^(\d{4})[\/.\-](\d{1,2})[\/.\-](\d{1,2})/); // ISO yyyy-mm-dd
    if (m) return `${m[1]}-${String(m[2]).padStart(2, "0")}-${String(m[3]).padStart(2, "0")}`;
    m = s.match(/^(\d{1,2})[\/.\-](\d{1,2})[\/.\-](\d{2,4})$/); // d/m/yyyy (European)
    if (m) {let [, a, b, y] = m;if (y.length === 2) y = "20" + y;return `${y}-${String(b).padStart(2, "0")}-${String(a).padStart(2, "0")}`;}
    return s;
  }
  function normalizeRows(raw) {
    // raw = array of objects keyed by header. Map flexible column names → our shape.
    const pick = (o, names) => {for (const k of Object.keys(o)) {const lk = k.toLowerCase().trim();if (names.some((n) => lk === n || lk.includes(n))) return o[k];}return undefined;};
    const out = [];
    for (const o of raw) {
      const sym = pick(o, ["symbol", "ticker", "asset", "instrument", "isin"]);
      if (sym == null || String(sym).trim() === "") continue;
      let act = String(pick(o, ["action", "side", "type", "operation"]) ?? "buy").toLowerCase();
      act = /sell|sale|sold|withdraw|out/.test(act) ? "sell" : "buy";
      const qty = pick(o, ["quantity", "qty", "shares", "units", "amount", "size"]);
      const price = pick(o, ["price", "cost", "unit price", "rate", "fill"]);
      const ccy = pick(o, ["currency", "ccy", "cur"]);
      const date = pick(o, ["date", "time", "when", "executed", "trade date"]);
      const toNum = (x) => Number(String(x == null ? "" : x).replace(/[^0-9.,\-]/g, "").replace(/\.(?=\d{3}(\D|$))/g, "").replace(",", ".")) || 0;
      out.push({ action: act, symbol: String(sym).trim().toUpperCase(), quantity: toNum(qty), price: toNum(price), currency: ccy ? String(ccy).trim().toUpperCase() : "", date: normDate(date) });
    }
    return out;
  }
  function parseCSV(text) {
    const lines = text.replace(/\r/g, "").split("\n").filter((l) => l.trim() !== "");
    if (!lines.length) return [];
    const delimOf = (l) => {const c = { ";": (l.match(/;/g) || []).length, ",": (l.match(/,/g) || []).length, "\t": (l.match(/\t/g) || []).length };return Object.keys(c).sort((a, b) => c[b] - c[a])[0];};
    const splitWith = (l, delim) => {const cells = [];let cur = "",q = false;for (let i = 0; i < l.length; i++) {const c = l[i];if (c === '"') {if (q && l[i + 1] === '"') {cur += '"';i++;} else q = !q;} else if (c === delim && !q) {cells.push(cur);cur = "";} else cur += c;}cells.push(cur);return cells.map((s) => s.trim());};
    // Find the header row — first line that splits into ≥2 recognised column names (skips title rows)
    let hIdx = -1,hDelim = ",";
    for (let i = 0; i < lines.length; i++) {const d = delimOf(lines[i]);if (kwHits(splitWith(lines[i], d)) >= 2) {hIdx = i;hDelim = d;break;}}
    if (hIdx < 0) {hIdx = 0;hDelim = delimOf(lines[0]);}
    const header = splitWith(lines[hIdx], hDelim);
    const raw = [];
    for (let i = hIdx + 1; i < lines.length; i++) {const c = splitWith(lines[i], hDelim);if (c.every((x) => x === "")) continue;const o = {};header.forEach((h, j) => {o[h] = c[j];});raw.push(o);}
    return normalizeRows(raw);
  }
  async function onImportFile(file) {
    resetImport();if (!file) return;setImpName(file.name);
    try {
      const ext = (file.name.split(".").pop() || "").toLowerCase();
      let rows;
      if (ext === "csv" || ext === "txt") {rows = parseCSV(await file.text());} else
      if (window.XLSX) {const buf = await file.arrayBuffer();const wb = window.XLSX.read(buf, { type: "array", cellDates: true });const ws = wb.Sheets[wb.SheetNames[0]];const aoa = window.XLSX.utils.sheet_to_json(ws, { header: 1, defval: "", raw: true });let hIdx = aoa.findIndex((r) => kwHits(r) >= 2);if (hIdx < 0) hIdx = 0;const header = aoa[hIdx].map((h) => String(h));const objs = [];for (let i = hIdx + 1; i < aoa.length; i++) {const r = aoa[i];if (!r || r.every((x) => x === "" || x == null)) continue;const o = {};header.forEach((h, j) => {o[h] = r[j];});objs.push(o);}rows = normalizeRows(objs);} else
      {setImpErr("Excel support is still loading — retry in a second, or use CSV.");return;}
      if (!rows.length) {setImpErr("No valid rows found. Need columns: symbol, quantity, price (action, currency, date optional).");return;}
      const bad = rows.filter((r) => !r.quantity || !(r.price || r.symbol.includes("CASH")));
      setImpRows(rows);if (bad.length) setImpErr(`${bad.length} row${bad.length > 1 ? "s" : ""} look incomplete (missing qty/price) — they'll still import with 0.`);
    } catch (e) {setImpErr("Could not read this file. Make sure it's a valid CSV or Excel export.");}
  }
  function commitImport() {
    if (!impRows || !impConfirm) return;
    const base = impMode === "replace" ? [] : transactions;
    const audit = auditSells(base, impRows);
    const valid = impRows.filter((_, i) => !audit[i].issue);
    const skipped = impRows.length - valid.length;
    if (!valid.length) {Haptics.warn();showToast("Nothing to import — every row was a sell with no holding");return;}
    const clean = valid.map((o) => {const symbol = resolveSymbol(o.symbol);const t = { id: nid(), action: o.action === "sell" ? "sell" : "buy", symbol, quantity: Number(o.quantity), price: Number(o.price), currency: o.currency || CATALOG[symbol]?.currency || "EUR", date: o.date || today() };ensureAsset(symbol, t);return t;});
    setPrices(providerRef.current.snapshot());
    const n = clean.length;
    if (impMode === "replace") {setTransactions(clean);} else {setTransactions((prev) => [...prev, ...clean]);}
    setAddOpen(false);resetImport();
    if (impMode === "replace") Haptics.warn();else Haptics.success();
    showToast((impMode === "replace" ? `Portfolio replaced · ${n} transaction${n > 1 ? "s" : ""}` : `${n} transaction${n > 1 ? "s" : ""} imported`) + (skipped ? ` · ${skipped} skipped` : ""));
  }
  function downloadTemplate() {
    const csv = "action,symbol,quantity,price,currency,date\nbuy,SWDA.MI,10,113.87,EUR,2026-03-05\nbuy,BTC,0.4,52000,USD,2025-10-02\nsell,AAPL,5,232,USD,2026-06-02\n";
    const blob = new Blob([csv], { type: "text/csv" });const url = URL.createObjectURL(blob);const a = document.createElement("a");a.href = url;a.download = "transactions-template.csv";a.click();setTimeout(() => URL.revokeObjectURL(url), 1000);
  }
  function killAll() {resetImport();setKillConfirm(false);Haptics.warn();}
  const sheetBack = () => {if (addMode === "manual") {if (manualStep === "custom") return setManualStep("list");if (manualStep === "form") return setManualStep("list");if (manualStep === "list") return setManualStep("class");return setAddMode("menu");}if (addMode === "import") resetImport();setAddMode("menu");};
  const inputStyle = { background: C.panel2, color: C.ink, border: `1px solid ${C.line}`, borderRadius: 10, outline: "none" };
  const primaryBtn = (active) => ({ background: active ? C.accent : C.panel2, color: active ? onAccent : C.faint, borderRadius: 13 });

  function AddSheet() {
    const impSkip = impAudit.filter((a) => a && a.issue).length;
    const impOk = impRows ? impRows.length - impSkip : 0;
    const bulkSkip = bulkAudit.filter((a) => a && a.issue).length;
    const bulkOk = bulkParsed ? bulkParsed.length - bulkSkip : 0;
    const menuItems = [
    { id: "manual", Icon: Hand, title: "Add Manually", desc: "Pick an instrument and enter the details.", on: true },
    { id: "nl", Icon: Sparkles, title: "Natural Language", desc: "Describe the trade in plain words.", on: true },
    { id: "bulk", Icon: ClipboardList, title: "Bulk / Paste", desc: "Paste many rows and add them at once.", on: true },
    { id: "import", Icon: FileSpreadsheet, title: "Import file", desc: "Upload a CSV or Excel export.", on: true },
    { id: "custom", Icon: Plus, title: "Custom asset", desc: "Add something not in our list.", on: true },
    { id: "stokr", Icon: Boxes, title: "Connect STOKR/RWA", desc: "Import your tokenized securities.", on: false },
    { id: "broker", Icon: Building2, title: "Connect Broker", desc: "~200 brokers and banks.", on: false },
    { id: "exch", Icon: Exch, title: "Connect Exchange", desc: "Binance, Coinbase, Kraken…", on: false },
    { id: "wallet", Icon: Wallet, title: "Connect Wallet", desc: "MetaMask, WalletConnect…", on: false }];

    const sel = CATALOG[mForm.symbol] || {};const q = assetQuery.toLowerCase();
    const assetList = Object.entries(CATALOG).filter(([s, a]) => a.type === manualClass && (q === "" || s.toLowerCase().includes(q) || a.name.toLowerCase().includes(q))).sort((a, b) => (a[1].rank || 99) - (b[1].rank || 99));
    return (
      <>
      <div onClick={() => setAddOpen(false)} style={{ position: "absolute", inset: 0, zIndex: 49, background: "rgba(0,0,0,0.5)", animation: "fadeIn .28s ease both" }}></div>
      <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, top: 60, zIndex: 50, background: C.bg, borderTopLeftRadius: 18, borderTopRightRadius: 18, overflow: "hidden", display: "flex", flexDirection: "column", boxShadow: "0 -12px 50px rgba(0,0,0,0.45)", animation: "sheetUp .42s cubic-bezier(.32,.72,.33,1) both" }}>
          <div><div style={{ width: 36, height: 5, borderRadius: 3, background: glassDark ? "rgba(255,255,255,0.22)" : "rgba(0,0,0,0.16)", margin: "10px auto 0" }}></div></div>
          <div className="flex items-center justify-between px-5 pt-2 pb-3">{(addMode !== "menu" && !(manualStep === "custom" && cDirect)) ? <button onClick={sheetBack} style={{ color: C.accentInk }} className="flex items-center text-sm -ml-1"><ChevronLeft size={20} />Back</button> : <span />}<button onClick={() => setAddOpen(false)} style={{ background: C.panel2, color: C.mute, width: 30, height: 30, borderRadius: 15 }} className="flex items-center justify-center"><X size={17} /></button></div>
          <div className="flex-1 overflow-y-auto noscroll"><div className="px-5 pb-8">
            {addMode === "menu" && <><h2 className="text-2xl font-bold leading-tight mb-5">How would you like to add transactions?</h2><div className="space-y-2">{menuItems.map(({ id, Icon, title, desc, on }) => <button key={id} disabled={!on} onClick={() => on && (id === "manual" ? openAdd("manual") : id === "custom" ? (setAddMode("manual"), setManualStep("custom"), setCDirect(true)) : setAddMode(id))} style={{ ...cardBg, borderRadius: 14, opacity: on ? 1 : 0.5 }} className="w-full text-left p-4 flex items-center gap-4"><span style={{ background: C.panel2, color: C.accentInk, width: 44, height: 44, borderRadius: 12 }} className="flex items-center justify-center shrink-0"><Icon size={20} /></span><span className="flex-1 min-w-0"><span className="font-semibold text-sm flex items-center gap-2 min-w-0"><span className="truncate">{title}</span>{!on && <span style={{ color: C.faint, border: `1px solid ${C.line}`, borderRadius: 5 }} className="shrink-0 text-[10px] font-normal px-1.5 py-0.5">Soon</span>}</span><span style={{ color: C.mute }} className="text-xs block truncate">{desc}</span></span><ChevronRight size={18} style={{ color: C.faint }} className="shrink-0" /></button>)}</div></>}
            {addMode === "manual" && manualStep === "class" && <><h2 className="text-xl font-bold mb-5">What type of instrument?</h2><div className="space-y-2">{CLASSES.map(({ id, label }) => {const M = { etf: { Icon: Layers, color: "#2D9CDB" }, stock: { Icon: Building2, color: "#8A7BD8" }, crypto: { Icon: Bitcoin, color: "#F7931A" }, bond: { Icon: Landmark, color: "#5B6B9E" }, cash: { Icon: Wallet, color: "#44705C" }, token: { Icon: Boxes, color: "#2FA8A0" } }[id] || {};const Ic = M.Icon;return <button key={id} onClick={() => {setManualClass(id);setAssetQuery("");setManualStep("list");}} style={{ ...cardBg, borderRadius: 14 }} className="w-full p-4 flex items-center justify-between"><span className="flex items-center gap-3">{Ic && <span style={{ background: C.panel2, color: M.color, width: 38, height: 38, borderRadius: 11 }} className="flex items-center justify-center shrink-0"><Ic size={19} /></span>}<span className="font-semibold">{label}</span></span><ChevronRight size={18} style={{ color: C.faint }} /></button>;})}</div><button onClick={() => {setCForm((f) => ({ ...f, category: manualClass }));setCDirect(false);setManualStep("custom");}} style={{ ...cardBg, borderRadius: 14, color: C.accentInk }} className="w-full mt-3 py-3.5 text-sm font-semibold flex items-center justify-center gap-1.5"><Plus size={16} /> Add a custom asset</button></>}
            {addMode === "manual" && manualStep === "list" && <><h2 className="text-xl font-bold mb-3">Select {CLASSES.find((c) => c.id === manualClass)?.label}</h2><div style={{ ...inputStyle }} className="flex items-center gap-2 px-3 mb-3"><Search size={16} style={{ color: C.faint }} /><input value={assetQuery} onChange={(e) => setAssetQuery(e.target.value)} placeholder="Search by ticker or name…" style={{ background: "transparent", color: C.ink, outline: "none" }} className="flex-1 py-2.5 text-sm" /></div><div>{assetList.map(([s, a]) => <button key={s} onClick={() => {setMForm((f) => ({ ...f, symbol: s }));setManualStep("form");}} className="w-full text-left px-1 py-2.5 flex items-center justify-between" style={{ borderBottom: `1px solid ${C.line}` }}><div className="flex items-center gap-3 min-w-0"><Logo symbol={s} size={34} /><div className="min-w-0"><div className="font-semibold text-sm">{a.ticker}</div><div style={{ color: C.faint }} className="text-xs truncate">{a.name} · {a.exchange} · {a.currency}</div></div></div><div className="font-mono tabular-nums text-sm whitespace-nowrap pl-2">{money(prices[s] ?? a.base, a.currency)}</div></button>)}{assetList.length === 0 && <div style={{ color: C.mute }} className="text-sm py-6 text-center">No instruments found.</div>}</div><button onClick={() => {setCForm((f) => ({ ...f, category: manualClass }));setCDirect(false);setManualStep("custom");}} style={{ ...cardBg, borderRadius: 12, color: C.accentInk }} className="w-full mt-3 py-3 text-sm font-semibold flex items-center justify-center gap-1.5"><Plus size={16} /> Add a custom asset</button></>}
            {addMode === "manual" && manualStep === "form" && <><h2 className="text-xl font-bold mb-4">Transaction details</h2><button onClick={() => {setManualClass(sel.type || "etf");setManualStep("list");}} style={{ ...cardBg, borderRadius: 12 }} className="w-full p-3 flex items-center justify-between mb-4"><div className="flex items-center gap-3 min-w-0"><Logo symbol={mForm.symbol} size={34} /><div className="text-left min-w-0"><div className="font-semibold text-sm">{sel.ticker || mForm.symbol}</div><div style={{ color: C.faint }} className="text-xs truncate">{sel.name} · {sel.exchange} · {sel.currency}</div></div></div><span style={{ color: C.accentInk }} className="text-xs shrink-0 pl-2">Change</span></button><div className="mb-4"><Segmented value={mForm.action} onChange={(a) => setMForm((f) => ({ ...f, action: a }))} options={sel.type === "cash" ? [{ id: "buy", label: "Deposit", color: C.up }, { id: "sell", label: "Withdraw", color: C.down }] : [{ id: "buy", label: "Buy", color: C.up }, { id: "sell", label: "Sell", color: C.down }]} /></div><div className={sel.type === "cash" ? "grid grid-cols-1 gap-3 mb-4" : "grid grid-cols-2 gap-3 mb-4"}><div><div className="flex items-center mb-1" style={{ height: 18 }}><label style={{ color: C.mute }} className="text-xs">{sel.type === "cash" ? `Amount (${sel.currency})` : "Quantity"}</label></div><input type="text" inputMode="decimal" value={mForm.quantity} onChange={(e) => setMForm((f) => ({ ...f, quantity: e.target.value.replace(/[^0-9.,]/g, "").replace(",", ".") }))} placeholder="10" style={inputStyle} className="w-full p-3 text-sm" /></div><div style={{ display: sel.type === "cash" ? "none" : "block" }}><div className="flex items-center justify-between mb-1" style={{ height: 18 }}><label style={{ color: C.mute }} className="text-xs">Price ({sel.currency})</label><button type="button" onClick={() => setMForm((f) => {const p = prices[mForm.symbol] ?? sel.base ?? 0;const r = p >= 100 ? p.toFixed(2) : p >= 1 ? p.toFixed(3) : p >= 0.01 ? p.toFixed(4) : p.toPrecision(3);return { ...f, price: String(+r) };})} style={{ color: C.accentInk, background: C.panel2, borderRadius: 6 }} className="text-xs font-medium px-1.5 py-0.5 leading-none">Market</button></div><input type="text" inputMode="decimal" value={mForm.price} onChange={(e) => setMForm((f) => ({ ...f, price: e.target.value.replace(/[^0-9.,]/g, "").replace(",", ".") }))} placeholder={num(prices[mForm.symbol] ?? sel.base ?? 0)} style={inputStyle} className="w-full p-3 text-sm" /></div></div><label style={{ color: C.mute }} className="text-xs">Date</label><input type="date" value={mForm.date} onChange={(e) => setMForm((f) => ({ ...f, date: e.target.value }))} style={inputStyle} className="w-full p-3 mt-1 mb-6 text-sm" /><button onClick={addManual} disabled={!mForm.quantity || sel.type !== "cash" && !mForm.price} style={primaryBtn(mForm.quantity && (sel.type === "cash" || mForm.price))} className="w-full py-3.5 font-semibold">{sel.type === "cash" ? "Add cash" : "Add transaction"}</button></>}
            {addMode === "manual" && manualStep === "custom" && <>
              <h2 className="text-xl font-bold mb-1">Custom asset</h2>
              <p style={{ color: C.mute }} className="text-sm mb-4">Add something not in our list. There's no live price, so it stays frozen at what you paid — but you can still log more transactions on it.</p>
              <div className="mb-4"><div className="flex items-center justify-between mb-1.5" style={{ minHeight: 18 }}><label style={{ color: C.mute }} className="text-xs">Category</label><button onClick={() => setCForm((f) => ({ ...f, newCat: !f.newCat }))} aria-label="Toggle new category" style={{ display: "flex", alignItems: "center", gap: 6 }}><span className="text-xs font-medium" style={{ color: cForm.newCat ? C.accentInk : C.mute }}>New category</span><span style={{ width: 38, height: 22, borderRadius: 11, background: cForm.newCat ? C.accentInk : C.panel2, position: "relative", transition: "background .2s" }}><span style={{ position: "absolute", top: 2, left: cForm.newCat ? 18 : 2, width: 18, height: 18, borderRadius: 9, background: "#FFFFFF", transition: "left .2s", boxShadow: "0 1px 2px rgba(0,0,0,0.3)" }}></span></span></button></div>{cForm.newCat ? <input value={cForm.catName} onChange={(e) => setCForm((f) => ({ ...f, catName: e.target.value }))} placeholder="e.g. Real Estate, Watches, Art…" style={inputStyle} className="w-full p-3 text-sm" /> : <div className="noscroll flex gap-1.5 overflow-x-auto" style={{ scrollbarWidth: "none" }}>{CLASSES.map(({ id, label }) => <button key={id} onClick={() => setCForm((f) => ({ ...f, category: id }))} style={{ background: cForm.category === id ? C.accentInk : C.panel2, color: cForm.category === id ? C.bg : C.mute, borderRadius: 999, flexShrink: 0 }} className="text-xs font-semibold px-3.5 py-2">{label}</button>)}</div>}</div>
              <div className="grid grid-cols-2 gap-3 mb-4">
                <div className="col-span-2"><div className="flex items-center mb-1" style={{ height: 18 }}><label style={{ color: C.mute }} className="text-xs">Name</label></div><input value={cForm.name} onChange={(e) => setCForm((f) => ({ ...f, name: e.target.value }))} placeholder="e.g. My Private Fund" style={inputStyle} className="w-full p-3 text-sm" /></div>
                <div><div className="flex items-center mb-1" style={{ height: 18 }}><label style={{ color: C.mute }} className="text-xs">Ticker (optional)</label></div><input value={cForm.symbol} onChange={(e) => setCForm((f) => ({ ...f, symbol: e.target.value.toUpperCase() }))} placeholder="MYF" style={inputStyle} className="w-full p-3 text-sm" /></div>
                <div><div className="flex items-center mb-1" style={{ height: 18 }}><label style={{ color: C.mute }} className="text-xs">Currency</label></div><select value={cForm.currency} onChange={(e) => setCForm((f) => ({ ...f, currency: e.target.value }))} style={{ ...inputStyle, appearance: "none" }} className="w-full p-3 text-sm">{["EUR", "USD", "GBP", "CHF", "JPY", "CAD", "AUD", "SGD"].map((c) => <option key={c} value={c}>{c}</option>)}</select></div>
                <div><div className="flex items-center mb-1" style={{ height: 18 }}><label style={{ color: C.mute }} className="text-xs">{cForm.category === "cash" ? `Amount (${cForm.currency})` : "Quantity"}</label></div><input type="text" inputMode="decimal" value={cForm.quantity} onChange={(e) => setCForm((f) => ({ ...f, quantity: e.target.value.replace(/[^0-9.,]/g, "").replace(",", ".") }))} placeholder="10" style={inputStyle} className="w-full p-3 text-sm" /></div>
                <div style={{ display: cForm.category === "cash" ? "none" : "block" }}><div className="flex items-center mb-1" style={{ height: 18 }}><label style={{ color: C.mute }} className="text-xs">Price paid ({cForm.currency})</label></div><input type="text" inputMode="decimal" value={cForm.price} onChange={(e) => setCForm((f) => ({ ...f, price: e.target.value.replace(/[^0-9.,]/g, "").replace(",", ".") }))} placeholder="100" style={inputStyle} className="w-full p-3 text-sm" /></div>
                <div className="col-span-2"><div className="flex items-center mb-1" style={{ height: 18 }}><label style={{ color: C.mute }} className="text-xs">Date</label></div><input type="date" value={cForm.date} onChange={(e) => setCForm((f) => ({ ...f, date: e.target.value }))} style={inputStyle} className="w-full p-3 text-sm" /></div>
              </div>
              <button onClick={addCustom} disabled={!cForm.name.trim() || !cForm.quantity || cForm.newCat && !cForm.catName.trim() || !cForm.newCat && cForm.category !== "cash" && !cForm.price || cForm.newCat && !cForm.price} style={primaryBtn(cForm.name.trim() && cForm.quantity && (cForm.newCat ? cForm.catName.trim() && cForm.price : cForm.category === "cash" || cForm.price))} className="w-full py-3.5 font-semibold flex items-center justify-center gap-2"><Plus size={16} /> Add custom asset</button>
            </>}
            {addMode === "nl" && <><h2 className="text-xl font-bold mb-2">Natural language</h2><p style={{ color: C.mute }} className="text-sm mb-4">Just say what happened. I extract type, ticker, shares, price and date.</p><textarea value={sheetText} onChange={(e) => {setSheetText(e.target.value);setSheetErr("");}} rows={3} placeholder="yesterday I sold 0.2 BTC at 64,000 dollars" style={inputStyle} className="w-full p-3 text-sm mb-1 resize-none" />{sheetErr && <p style={{ color: C.down }} className="text-xs mb-2">{sheetErr}</p>}<button onClick={sheetNL} disabled={sheetBusy || !sheetText.trim()} style={primaryBtn(sheetText.trim())} className="w-full py-3.5 font-semibold flex items-center justify-center gap-2 mt-2">{sheetBusy ? <Loader2 size={16} className="animate-spin" /> : <Sparkles size={16} />} Parse & add</button></>}
            {addMode === "bulk" && <><h2 className="text-xl font-bold mb-2">Bulk add</h2><p style={{ color: C.mute }} className="text-sm mb-4">Already have a portfolio? Paste every trade, one per line.</p><textarea value={bulkText} onChange={(e) => {setBulkText(e.target.value);setBulkErr("");}} rows={6} placeholder={"10 SWDA at 113.87 on Mar 5 2026\n0.4 BTC at 52000$ on Oct 2 2025\nsold 5 AAPL at 232$ on Jun 2 2026"} style={inputStyle} className="w-full p-3 text-sm mb-1 resize-none font-mono" />{bulkErr && <p style={{ color: C.down }} className="text-xs mb-2">{bulkErr}</p>}{!bulkParsed && <button onClick={parseBulk} disabled={bulkBusy || !bulkText.trim()} style={primaryBtn(bulkText.trim())} className="w-full py-3.5 font-semibold flex items-center justify-center gap-2 mt-2">{bulkBusy ? <Loader2 size={16} className="animate-spin" /> : <ClipboardList size={16} />} Analyze</button>}{bulkParsed && <><div style={{ color: C.mute }} className="text-xs my-3">Found {bulkParsed.length} · <span style={{ color: C.ink, fontWeight: 600 }}>{bulkOk} to add</span>{bulkSkip > 0 ? ` · ${bulkSkip} skipped` : ""}</div>{bulkSkip > 0 && <div style={{ background: "rgba(232,163,61,0.10)", border: "1px solid rgba(232,163,61,0.38)", borderRadius: 10, color: C.ink }} className="text-xs px-3 py-2 mb-2 flex items-start gap-2"><AlertTriangle size={14} className="shrink-0" style={{ marginTop: 1, color: "#C68A2E" }} /><span><b>{bulkSkip} sell{bulkSkip > 1 ? "s" : ""} will be skipped.</b> Nothing to sell — that asset isn’t in the portfolio yet.</span></div>}<div className="space-y-2 mb-4">{bulkParsed.map((o, i) => {const iss = bulkAudit[i] && bulkAudit[i].issue;return <div key={i} style={{ background: iss ? "rgba(232,163,61,0.10)" : C.panel, border: `1px solid ${iss ? "rgba(232,163,61,0.42)" : C.line}`, borderRadius: 10 }} className="p-3 text-sm"><div className="flex items-center justify-between"><span><span style={{ color: o.action === "sell" ? C.down : C.up }} className="font-medium">{o.action === "sell" ? "Sell" : "Buy"}</span> <span className="font-mono">{String(o.symbol).toUpperCase()}</span></span><span className="font-mono tabular-nums" style={{ textDecoration: iss ? "line-through" : "none", color: iss ? C.mute : "inherit" }}>{num(Number(o.quantity))} × {num(Number(o.price))} {o.currency || ""} · {o.date}</span></div>{iss && <div className="flex items-center gap-1 mt-1.5 text-xs" style={{ color: "#C68A2E", fontWeight: 600 }}><AlertTriangle size={11} className="shrink-0" /><span>Will skip · {iss === "phantom" ? `no ${String(o.symbol).toUpperCase()} held` : `only ${num(bulkAudit[i].have)} held`}</span></div>}</div>;})}</div><div className="flex gap-2"><button onClick={() => setBulkParsed(null)} style={{ background: C.panel2, color: C.ink, borderRadius: 13 }} className="flex-1 py-3.5 font-medium">Edit</button><button onClick={commitBulk} disabled={bulkOk === 0} style={{ background: bulkOk > 0 ? C.accent : C.panel2, color: bulkOk > 0 ? onAccent : C.faint, borderRadius: 13 }} className="flex-1 py-3.5 font-semibold">{bulkOk === 0 ? "Nothing to add" : `Add ${bulkOk}`}</button></div></>}</>}
            {addMode === "import" && <>
              <h2 className="text-xl font-bold mb-2">Import file</h2>
              <p style={{ color: C.mute }} className="text-sm mb-4">Upload a CSV or Excel (.xlsx) export. Columns: <span className="font-mono" style={{ color: C.ink }}>symbol, quantity, price</span> — action, currency and date are optional.</p>
              <input ref={impInputRef} type="file" accept=".csv,.xlsx,.xls,.txt" style={{ display: "none" }} onChange={(e) => onImportFile(e.target.files && e.target.files[0])} />
              <button onClick={() => impInputRef.current && impInputRef.current.click()} style={{ border: `1.5px dashed ${C.line}`, background: C.panel, borderRadius: 14 }} className="w-full px-4 py-7 flex flex-col items-center gap-2 mb-3 active:opacity-80">
                <span style={{ background: C.panel2, color: C.accentInk, width: 46, height: 46, borderRadius: 14 }} className="flex items-center justify-center"><Upload size={22} /></span>
                <span className="font-semibold text-sm">{impName || "Choose a file"}</span>
                <span style={{ color: C.faint }} className="text-xs">CSV · XLSX · XLS</span>
              </button>
              <button onClick={downloadTemplate} style={{ color: C.accentInk }} className="text-xs font-medium flex items-center gap-1 mb-4"><Download size={13} /> Download CSV template</button>
              {impErr && <div style={{ background: "rgba(217,68,44,0.10)", color: C.down, border: "1px solid rgba(217,68,44,0.25)", borderRadius: 10 }} className="text-xs px-3 py-2 mb-3 flex items-start gap-2"><AlertTriangle size={14} className="shrink-0" style={{ marginTop: 1 }} />{impErr}</div>}
              {impRows && <>
                <div className="flex items-center justify-between mb-2"><span style={{ color: C.mute }} className="text-xs">Found {impRows.length} · <span style={{ color: C.ink, fontWeight: 600 }}>{impOk} to import</span>{impSkip > 0 ? ` · ${impSkip} skipped` : ""}</span><button onClick={() => setKillConfirm(true)} style={{ color: C.down }} className="text-xs font-semibold flex items-center gap-1 active:opacity-70"><Trash2 size={13} /> Delete all</button></div>
                {impSkip > 0 && <div style={{ background: "rgba(232,163,61,0.10)", border: "1px solid rgba(232,163,61,0.38)", borderRadius: 10, color: C.ink }} className="text-xs px-3 py-2 mb-2 flex items-start gap-2"><AlertTriangle size={14} className="shrink-0" style={{ marginTop: 1, color: "#C68A2E" }} /><span><b>{impSkip} sell row{impSkip > 1 ? "s" : ""} will be skipped.</b> You can’t sell an asset that isn’t in the portfolio yet — there’s nothing to sell. Everything else imports normally.</span></div>}
                <div className="space-y-1.5 mb-4" style={{ maxHeight: 180, overflowY: "auto" }}>{impRows.slice(0, 30).map((o, i) => {const iss = impAudit[i] && impAudit[i].issue;return <div key={i} style={{ background: iss ? "rgba(232,163,61,0.10)" : C.panel, border: `1px solid ${iss ? "rgba(232,163,61,0.42)" : C.line}`, borderRadius: 10 }} className="p-2.5 text-xs"><div className="flex items-center justify-between"><span><span style={{ color: o.action === "sell" ? C.down : C.up }} className="font-medium">{o.action === "sell" ? "Sell" : "Buy"}</span> <span className="font-mono">{o.symbol}</span></span><span className="font-mono tabular-nums" style={{ color: C.mute, textDecoration: iss ? "line-through" : "none" }}>{num(o.quantity)} × {num(o.price)} {o.currency} · {o.date || "—"}</span></div>{iss && <div className="flex items-center gap-1 mt-1.5" style={{ color: "#C68A2E", fontWeight: 600 }}><AlertTriangle size={11} className="shrink-0" /><span>Will skip · {iss === "phantom" ? `no ${o.symbol} held to sell` : `only ${num(impAudit[i].have)} held`}</span></div>}</div>;})}{impRows.length > 30 && <div style={{ color: C.faint }} className="text-xs text-center py-1">+{impRows.length - 30} more</div>}</div>
                <div className="mb-3"><div style={{ color: C.mute }} className="text-xs mb-1.5">How should these be applied?</div><Segmented value={impMode} onChange={(m) => {setImpMode(m);setImpConfirm(false);}} options={[{ id: "add", label: `Add to existing (${transactions.length})` }, { id: "replace", label: "Replace all", color: C.down }]} /></div>
                <button onClick={() => setImpConfirm((c) => !c)} style={{ background: C.panel, border: `1px solid ${impConfirm ? C.accentInk : C.line}`, borderRadius: 12 }} className="w-full p-3 flex items-center gap-3 mb-3 text-left">
                  <span style={{ width: 22, height: 22, borderRadius: 6, background: impConfirm ? C.accentInk : "transparent", border: `1.5px solid ${impConfirm ? C.accentInk : C.faint}` }} className="flex items-center justify-center shrink-0">{impConfirm && <Check size={14} color={onAccent} />}</span>
                  <span className="text-xs" style={{ color: C.ink, lineHeight: 1.4 }}>{impMode === "replace" ? <>I understand this will <b>delete my current {transactions.length} transaction{transactions.length > 1 ? "s" : ""}</b> and replace them with the {impOk} valid one{impOk !== 1 ? "s" : ""} above.</> : <>I confirm adding {impOk} transaction{impOk !== 1 ? "s" : ""} on top of my existing {transactions.length}.</>}</span>
                </button>
                <button onClick={commitImport} disabled={!impConfirm || impOk === 0} style={{ background: impConfirm && impOk > 0 ? impMode === "replace" ? C.down : C.accent : C.panel2, color: impConfirm && impOk > 0 ? impMode === "replace" ? "#FFFFFF" : onAccent : C.faint, borderRadius: 13 }} className="w-full py-3.5 font-semibold flex items-center justify-center gap-2">{impMode === "replace" ? <AlertTriangle size={16} /> : <Upload size={16} />}{impOk === 0 ? "Nothing to import" : impMode === "replace" ? `Replace & upload${impSkip ? ` (${impOk})` : ""}` : `Upload & add${impSkip ? ` (${impOk})` : ""}`}</button>
              </>}
            </>}
          </div>
          </div>
        </div>
      </>);

  }

  // ── Insights daily-message hero — one design treatment per UI version (Editorial=B serif, WSB=D color wash, Data=E number-first) ──
  function AltHero() {
    const u = dayChange.abs >= 0;
    const col = u ? C.up : C.down;
    const absStr = dispMoney(Math.abs(dayChange.abs), display); // "€8,472.16"
    const signed = (u ? "+" : "\u2212") + absStr; // "+€8,472.16"
    const total = dispMoney(totals.value, display);
    const pct = (u ? "+" : "\u2212") + num(Math.abs(dayChange.pct)) + "%"; // "+1.65%"
    const d = new Date();
    const eyebrow = `${d.toLocaleDateString("en-US", { weekday: "long" })} \u00B7 ${d.getDate()} ${d.toLocaleDateString("en-US", { month: "long" })}`;
    // Auto-shrink the big hero figure so long amounts (JPY, large portfolios) never overflow the screen
    const heroFit = (str, base, min) => Math.max(min, base - Math.max(0, str.length - 9) * 1.8);

    if (variant === "editorial") {
      const underline = u ? "rgba(24,163,90,0.35)" : "rgba(217,68,44,0.35)";
      return (
        <div style={{ display: "flex", flexDirection: "column", gap: 15, paddingTop: 64, paddingBottom: 80 }}>
          <div style={{ fontSize: 13, letterSpacing: "0.14em", textTransform: "uppercase", color: C.faint, fontWeight: 500 }}>{eyebrow}</div>
          <div style={{ fontFamily: "'Instrument Serif', Georgia, serif", fontSize: 44, lineHeight: 1.12, color: C.ink, fontWeight: 400 }}>
            {u ? "Your portfolio grew " : "Your portfolio slipped "}
            <span style={{ whiteSpace: "nowrap" }}>{"by "}<span style={{ color: col, fontWeight: 600, borderBottom: `3px solid ${underline}` }}><RollDigits text={absStr} /></span></span>
            {" today"}
          </div>
          <div style={{ fontSize: 16, color: C.mute }}>{totals.value > 0 ? <>That's <span style={{ color: col, fontWeight: 700 }}><RollDigits text={pct} /></span> — now <RollDigits text={total} />.</> : <>Now <RollDigits text={total} />.</>}</div>
          <button onClick={(e) => {e.stopPropagation();explainDay();}} style={{ alignSelf: "flex-start", ...glass, borderRadius: 999, color: insightOpen ? C.accentInk : C.mute, marginTop: 4 }} className="flex items-center gap-1.5 text-xs px-3 py-1.5 active:opacity-80">{insightBusy ? <Loader2 size={13} className="animate-spin" /> : <Sparkles size={13} />} Explain my day</button>
        </div>);
    }

    if (variant === "wsb") {
      const pillBg = effTheme === "light" ? "rgba(12,26,18,0.06)" : "rgba(255,255,255,0.08)";
      return (
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", gap: 10, paddingTop: 64, paddingBottom: 80 }}>
          <div style={{ fontSize: 14, fontWeight: 400, letterSpacing: "0.12em", textTransform: "uppercase", color: washSecondary }}>Today</div>
          <div onClick={bumpGreen} className="pt-hero" style={{ fontSize: heroFit(signed, 48, 26), fontWeight: 800, color: washPrimary, letterSpacing: "-0.03em", lineHeight: 1, whiteSpace: "nowrap", cursor: !u ? "pointer" : "default", userSelect: "none", transition: "color .25s ease" }}><RollDigits text={signed} /></div>
          <div style={{ fontSize: 17, fontWeight: 400, color: washSecondary }}>{totals.value <= 0 ? "your portfolio is waiting to grow" : <>{u ? "your portfolio is up " : "your portfolio is down "}<RollDigits text={pct} /></>}</div>
          <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 8, marginTop: 2 }}>
            <div style={{ background: pillBg, color: washPrimary, fontSize: 14, fontWeight: 600, padding: "7px 15px", borderRadius: 999, whiteSpace: "nowrap", height: 32, display: "flex", alignItems: "center" }}>{`${total} total`}</div>
            <button onClick={(e) => {e.stopPropagation();explainDay();}} aria-label="Explain my day" style={{ width: 32, height: 32, borderRadius: 999, background: washPrimary, color: effTheme === "light" ? "#FFFFFF" : "#0C0C0C", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, boxShadow: insightOpen ? `0 0 0 2px ${washPrimary}55` : "none" }} className="active:scale-90 transition-transform">{insightBusy ? <Loader2 size={16} className="animate-spin" /> : <Sparkles size={16} />}</button>
          </div>
        </div>);
    }

    // data — E · number first
    const [aInt, ...aRest] = signed.split(".");
    const aDec = aRest.length ? "." + aRest.join(".") : "";
    return (
      <div style={{ display: "flex", flexDirection: "column", gap: 12, paddingTop: 64, paddingBottom: 80 }}>
        <div style={{ fontSize: 17, fontWeight: 400, color: C.faint }}>Today your portfolio is</div>
        <div className="pt-hero" style={{ fontSize: heroFit(aInt + aDec, 34, 22), fontWeight: 800, color: C.ink, letterSpacing: "-0.02em", lineHeight: 1, whiteSpace: "nowrap" }}><RollDigits text={aInt} /><span style={{ color: C.faint, fontSize: "0.62em" }}><RollDigits text={aDec} /></span></div>
        <div style={{ display: "flex", alignItems: "center", gap: 5, flexWrap: "wrap" }}>
          {totals.value > 0 && <div style={{ color: col, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 16, fontWeight: 800, flexShrink: 0, lineHeight: 1 }}>{u ? "\u2191" : "\u2193"}</div>}
          {totals.value > 0 && <div style={{ fontSize: 16, fontWeight: 700, color: col, whiteSpace: "nowrap" }}>{u ? "growing " : "down "}<RollDigits text={pct} /></div>}
          <div style={{ fontSize: 16, color: C.faint, whiteSpace: "nowrap" }}>{totals.value > 0 ? <>{"\u00B7 "}<RollDigits text={total} /></> : <RollDigits text={total} />}</div>
        </div>
        <button onClick={(e) => {e.stopPropagation();explainDay();}} style={{ alignSelf: "flex-start", background: C.panel2, border: `1px solid ${C.line}`, borderRadius: 8, color: insightOpen ? C.accentInk : C.mute }} className="font-mono flex items-center gap-1.5 text-xs px-2.5 py-1.5 active:opacity-80">{insightBusy ? <Loader2 size={12} className="animate-spin" /> : <Sparkles size={12} />} explain_my_day()</button>
      </div>);
  }

  return (
    <div className={"pt-root pt-" + variant} style={{ position: "relative", width: "100%", height: "100%", overflow: "hidden", background: wsbWash && tab === "insights" && !selected ? washBg : variant === "editorial" ? (effTheme === "dark" ? "linear-gradient(165deg, #17150F 0%, #131108 55%, #0E0C07 100%)" : "linear-gradient(165deg, #FFFFFF 0%, #FAFAFB 50%, #EFEFF2 100%)") : C.bg, color: C.ink, fontFamily: V.body, "--pt-head": V.head, "--pt-mono": V.mono }}>
      <style>{`.pt-root h1,.pt-root h2,.pt-root .pt-hero{font-family:var(--pt-head)}.pt-root .font-mono{font-family:var(--pt-mono)}${V.css}@keyframes digitRoll{from{transform:translateY(var(--rollY,.55em));filter:blur(2px)}to{transform:none;filter:blur(0)}}@keyframes miniReveal{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}.noscroll::-webkit-scrollbar{display:none}@keyframes pulseDot{0%{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.65)}100%{opacity:1;transform:scale(1)}}.pulse{animation:pulseDot 1.2s ease-in-out infinite}@keyframes popIn{from{opacity:0;transform:translateY(-4px) scale(.97)}to{opacity:1;transform:none}}.pop{animation:popIn .14s ease-out}@keyframes slideUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:none}}.slideup{animation:slideUp .2s cubic-bezier(.2,.8,.2,1)}@keyframes haloPulse{0%{transform:scale(.85);opacity:.5}70%{transform:scale(2.8);opacity:0}100%{transform:scale(2.8);opacity:0}}.halo{animation:haloPulse 1.5s ease-out infinite}@keyframes sheetUp{from{transform:translateY(100%)}to{transform:none}}@keyframes fadeIn{from{opacity:0}to{opacity:1}}`}</style>

      {chooser && setVariant && <LaunchFlow C={C} native={native} onAccent={onAccent} cardBg={cardBg} onPick={(id) => { setVariant(id); if (id === "wsb") setTheme("dark"); markChosen(); Haptics.select(); }} />}

      {!native && <div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 44, zIndex: 25, display: "flex", alignItems: "center", justifyContent: "space-between", padding: "0 24px" }}><span className="text-sm font-semibold">9:41</span><span style={{ color: C.ink }} className="text-xs font-semibold tracking-wide">5G</span></div>}
      {!native && <div style={{ position: "absolute", top: 9, left: "50%", transform: "translateX(-50%)", width: 108, height: 30, borderRadius: 18, background: "#000", zIndex: 30 }} />}

      {/* Reassurance toast — top, only when the WSB wash is red */}
      <div style={{ position: "absolute", top: 52, left: 16, right: 16, zIndex: 40, display: "flex", justifyContent: "center", pointerEvents: "none", opacity: downToast ? 1 : 0, transform: downToast ? "translateY(0)" : "translateY(-16px)", transition: "opacity .4s ease, transform .4s cubic-bezier(.22,1,.36,1)" }}>
        <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 4, maxWidth: "100%", padding: "10px 18px", borderRadius: 18, background: "rgba(20,10,9,0.55)", backdropFilter: "blur(18px) saturate(180%)", WebkitBackdropFilter: "blur(18px) saturate(180%)", border: "1px solid rgba(255,107,94,0.30)", boxShadow: "0 8px 24px rgba(0,0,0,0.35)", textAlign: "center" }}>
          <span style={{ width: 7, height: 7, borderRadius: 4, background: "#FF6B5E", flexShrink: 0, boxShadow: "0 0 8px rgba(255,107,94,0.8)" }}></span>
          <span style={{ fontSize: 13.5, fontWeight: 600, color: "#FFE9E6", lineHeight: 1.35 }}>No worries, keep going!<br /><span style={{ fontWeight: 400, color: "rgba(255,233,230,0.72)" }}>Still up this year</span></span>
        </div>
      </div>

      {/* Easter-egg toast — top, when the user "forces" the WSB market green by tapping fast */}
      <div style={{ position: "absolute", top: 52, left: 16, right: 16, zIndex: 41, display: "flex", justifyContent: "center", pointerEvents: "none", opacity: greenToast ? 1 : 0, transform: greenToast ? "translateY(0)" : "translateY(-16px)", transition: "opacity .4s ease, transform .4s cubic-bezier(.22,1,.36,1)" }}>
        <div style={{ display: "flex", alignItems: "center", gap: 9, maxWidth: "100%", padding: "10px 16px", borderRadius: 999, background: "rgba(8,22,14,0.55)", backdropFilter: "blur(18px) saturate(180%)", WebkitBackdropFilter: "blur(18px) saturate(180%)", border: "1px solid rgba(74,222,128,0.35)", boxShadow: "0 8px 24px rgba(0,0,0,0.35)" }}>
          <span style={{ width: 7, height: 7, borderRadius: 4, background: "#4ADE80", flexShrink: 0, boxShadow: "0 0 8px rgba(74,222,128,0.85)" }}></span>
          <span style={{ fontSize: 13.5, fontWeight: 600, color: "#E7FBEE", whiteSpace: "nowrap" }}>You turned the market green!&nbsp;<span style={{ fontWeight: 400, color: "rgba(231,251,238,0.72)" }}>…for a second 😏</span></span>
        </div>
      </div>

      {/* Sticky header — appears on scroll, gradient fade below */}
      {(() => {const showHdr = scrolled && !(tab === "portfolio" && selected);const u = dayChange.abs >= 0;const col = u ? C.up : C.down;
        const center = tab === "insights" ?
        <span style={{ color: col }} className="font-mono tabular-nums text-sm font-semibold">{u ? "+" : ""}{dispMoney(dayChange.abs, display)} · {u ? "+" : ""}{num(dayChange.pct)}%</span> :
        tab === "portfolio" ?
        <span className="font-mono tabular-nums text-sm font-semibold">{dispMoney(totals.value, display)}</span> :
        <span className="text-sm font-semibold">{txFilter === "all" ? "All" : CLASSES.find((c) => c.id === txFilter)?.label || "All"}</span>;
        return (
          <div style={{ position: "absolute", top: 0, left: 0, right: 0, zIndex: 24, pointerEvents: "none", opacity: showHdr ? 1 : 0, transform: showHdr ? "translateY(0)" : "translateY(-10px)", transition: "opacity .22s ease, transform .22s ease" }}>
          {/* Liquid Glass — translucent tint + blur, fading out toward the bottom edge */}
          <div style={{ position: "absolute", inset: 0, background: effTheme === "dark" ? wsbWash ? "rgba(7,11,9,0.58)" : "rgba(10,11,13,0.52)" : "rgba(242,243,245,0.55)", backdropFilter: "blur(16px) saturate(180%)", WebkitBackdropFilter: "blur(16px) saturate(180%)", WebkitMaskImage: "linear-gradient(to bottom, black 0%, black 58%, transparent 100%)", maskImage: "linear-gradient(to bottom, black 0%, black 58%, transparent 100%)" }}></div>
          <div style={{ position: "relative", paddingTop: native ? "max(12px, env(safe-area-inset-top))" : 44, paddingBottom: 12, display: "flex", alignItems: "center", justifyContent: "center" }}>{center}</div>
          <div style={{ height: 30 }}></div>
        </div>);})()}

      <div ref={scrollRef} onScroll={(e) => {const y = e.currentTarget.scrollTop;setScrolled(y > 64);const last = lastScroll.current;if (y < 36) setNavCollapsed(false);else if (y - last > 3) setNavCollapsed(true);else if (last - y > 3) setNavCollapsed(false);lastScroll.current = y;}} className="noscroll" style={{ position: "absolute", inset: 0, overflowY: "auto", paddingTop: native ? "max(12px, env(safe-area-inset-top))" : 44 }}>
        <div className="px-5 pt-2" style={{ paddingBottom: 116 }}>
          {tab === "insights" && <>
            <div className="flex items-center justify-between mb-6"><h1 className="text-lg font-semibold">Overview</h1><div className="flex items-center gap-1.5"><button onClick={refresh} style={{ color: C.ink, background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, borderRadius: 999, width: 34, height: 34, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.10)" }} className="flex items-center justify-center shrink-0 active:opacity-80 active:scale-95 transition-transform"><RefreshCw size={17} className={refreshing ? "animate-spin" : ""} /></button>{variant !== "draft" && <button onPointerDown={startLP} onPointerUp={endLP} onPointerLeave={endLP} onClick={(e) => {e.stopPropagation();tapCcy();}} style={{ color: C.ink, background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, borderRadius: 999, height: 34, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.10)" }} className="flex items-center gap-1.5 pl-3 pr-2.5 text-xs font-semibold active:opacity-80 active:scale-95 transition-transform"><span className="font-mono">{display}</span><ArrowRightLeft size={12} style={{ color: C.ink }} /></button>}{V.allowThemeToggle && <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")} style={{ color: C.ink, background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, borderRadius: 999, width: 34, height: 34, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.10)" }} className="flex items-center justify-center shrink-0 active:opacity-80 active:scale-95 transition-transform">{theme === "dark" ? <Sun size={17} /> : <Moon size={17} />}</button>}{userBtn}</div></div>
            {variant === "draft" && (() => {
              const u = dayChange.abs >= 0;
              const col = u ? C.up : C.down;
              const bg = u ? "rgba(52,160,90,0.14)" : "rgba(211,58,44,0.14)";
              const sym = CCY_SYM[display] || display;
              const symPre = sym.length > 1 ? sym + " " : sym;
              const split = (vEUR) => num(Math.abs(fromEUR(vEUR, display))).split(".");
              const [dInt, dDec] = split(dayChange.abs);
              const [tInt, tDec] = split(totals.value);
              const daily = heroView === "daily";
              return (
                <div onClick={() => setHeroView((v) => v === "daily" ? "total" : "daily")} className="select-none cursor-pointer">

                  {/* Riga 1 — etichetta + What's going on? a destra */}
                  <div className="flex items-center justify-between mb-1" style={{ minHeight: 26 }}>
                    <span style={{ color: C.mute }} className="text-sm">{daily ? "Today" : "Total worth"}</span>
                    <button onClick={(e) => {e.stopPropagation();explainDay();}} style={{ ...glass, borderRadius: 999, color: insightOpen ? C.accentInk : C.mute }} className="flex items-center gap-1 text-xs px-2.5 py-1">{insightBusy ? <Loader2 size={12} className="animate-spin" /> : <Sparkles size={12} />} Explain my day</button>
                  </div>

                  {/* Riga 2 — cifra grande + selettore valuta allineato a destra */}
                  <div className="flex items-end justify-between">
                    <span className="tabular-nums pt-hero" style={{ letterSpacing: "-0.02em", color: daily ? col : C.ink, "--rollY": daily ? "-.55em" : ".55em" }}>
                      <RollDigits key={heroView + "-i"} className="font-bold" style={{ fontSize: 34, lineHeight: 1 }} text={(daily ? u ? "+" : "−" : "") + symPre + (daily ? dInt : tInt)} />
                      <RollDigits key={heroView + "-d"} className="font-bold" style={{ fontSize: 27, lineHeight: 1, opacity: 0.55 }} text={"." + (daily ? dDec : tDec)} />
                    </span>
                    <button onPointerDown={startLP} onPointerUp={endLP} onPointerLeave={endLP} onClick={(e) => {e.stopPropagation();tapCcy();}} style={{ background: C.panel2, color: C.ink, borderRadius: 9 }} className="flex items-center gap-1 text-sm font-medium px-2.5 py-1.5 select-none"><span className="font-mono">{display}</span><ArrowRightLeft size={13} style={{ color: C.mute }} /></button>
                  </div>

                  {/* Riga 3 — pill % (ferma) + valore secondario (rolling) */}
                  <div className="flex items-center gap-2 mt-3">
                    {totals.value > 0 && <span style={{ background: bg, color: col, borderRadius: 10, fontSize: "14px" }} className="tabular-nums font-semibold px-2.5 py-1 whitespace-nowrap">{u ? "+" : ""}{num(dayChange.pct)}%</span>}
                    {daily ?
                    <span style={{ color: C.ink, "--rollY": "-.55em", fontSize: "14px" }} className="tabular-nums whitespace-nowrap flex items-center"><span style={{ color: C.ink, marginRight: 1 }}>{u ? "↑" : "↓"}</span><RollDigits text={symPre + num(fromEUR(totals.value, display))} /></span> :
                    <span style={{ color: col, "--rollY": ".55em", fontSize: "14px" }} className="tabular-nums whitespace-nowrap"><RollDigits text={(u ? "+" : "−") + symPre + num(Math.abs(fromEUR(dayChange.abs, display)))} /></span>}
                  </div>

                  {/* Dots — affordance: due viste, quella attiva è scura */}
                  <div className="flex gap-1.5 mt-2" style={{ margin: "16px 0px 0px" }}>
                    <span style={{ width: 7, height: 7, borderRadius: 999, background: daily ? C.mute : C.line }}></span>
                    <span style={{ width: 7, height: 7, borderRadius: 999, background: daily ? C.line : C.mute }}></span>
                  </div>
                </div>);

            })()}
            {variant !== "draft" && AltHero()}
            {!V.hidePerf && <div style={{ width: "86%", margin: "24px auto" }}><MiniPerf data={daySeries} up={dayChange.abs >= 0} /></div>}
            <div style={{ color: C.mute }} className="text-xs mb-2 px-1">Asset classes</div>
            {(() => {
              const defs = [{ id: "etf", label: "ETFs", Icon: Layers, color: "#2D9CDB" }, { id: "stock", label: "Stocks", Icon: Building2, color: "#8A7BD8" }, { id: "crypto", label: "Crypto", Icon: Bitcoin, color: "#F7931A" }, { id: "bond", label: "Bonds", Icon: Landmark, color: "#5B6B9E" }, { id: "token", label: "Tokenization", Icon: Boxes, color: "#2FA8A0" }, { id: "cash", label: "Cash", Icon: Wallet, color: "#44705C" }];
              // append any user-created custom categories present in holdings
              const known = new Set(defs.map((d) => d.id));
              holdings.forEach((h) => {const t = h.type || "stock";if (!known.has(t)) {known.add(t);const a = CATALOG[h.symbol];defs.push({ id: t, label: a?.catLabel || t, Icon: Sparkles, color: a?.color || "#8A8F98" });}});
              const rows = defs.map((d) => {
                const hs = holdings.filter((h) => (h.type || "stock") === d.id);
                if (!hs.length) return null;
                let val = 0,absR = 0,startVal = 0;
                for (const h of hs) {const sf = getAssetRel(h.symbol)[dayIdx] || 1;val += h.valEUR;absR += h.valEUR * (1 - sf);startVal += h.valEUR * sf;}
                const pctRaw = startVal ? absR / startVal * 100 : 0;const u = !dayDown;
                return { ...d, hs, val, u, absSigned: (u ? 1 : -1) * Math.abs(absR), pct: (u ? 1 : -1) * Math.abs(pctRaw) };
              }).filter(Boolean);
              const cardStyle = { ...cardBg, borderRadius: 18 };
              const cInk = C.ink,cMute = C.mute,cFaint = C.faint;
              const iconBorder = glassDark ? "1px solid rgba(255,255,255,0.14)" : "1px solid rgba(0,0,0,0.06)";
              const iconBg = glassDark ? "rgba(255,255,255,0.05)" : "#FFFFFF";
              if (!rows.length) return (
                <div style={{ ...cardStyle, padding: "26px 22px", display: "flex", flexDirection: "column", alignItems: "center", textAlign: "center", gap: 12 }}>
                  <span style={{ width: 44, height: 44, borderRadius: 22, background: glassDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)", display: "flex", alignItems: "center", justifyContent: "center" }}><Wallet size={20} color={C.mute} /></span>
                  <div style={{ fontSize: 13, color: cMute, maxWidth: 220, lineHeight: 1.45 }}>Start tracking your investments — add your first transaction to see them here.</div>
                  <button onClick={() => openAdd("menu")} style={{ background: C.accentInk, color: onAccent, borderRadius: 999, fontSize: 13, fontWeight: 600, padding: "9px 18px", display: "inline-flex", alignItems: "center", gap: 6 }} className="active:opacity-80"><Plus size={15} /> Add a transaction</button>
                </div>);
              return (
                <div className="space-y-2.5">
                  {rows.map((r) =>
                  <button key={r.id} onClick={() => setClassModal(r.id)} style={cardStyle} className="w-full text-left px-3.5 py-3 flex items-center gap-3 active:opacity-70">
                      <span style={{ width: 38, height: 38, borderRadius: 19, background: iconBg, border: iconBorder, color: "rgb(7, 13, 9)" }} className="flex items-center justify-center shrink-0"><r.Icon size={17} color={r.color} /></span>
                      <div className="flex-1 min-w-0"><div className="font-semibold leading-tight" style={{ fontSize: 12, color: cInk }}>{r.label}</div><div style={{ color: cMute, fontSize: 11, marginTop: 2 }}>{r.hs.length} asset{r.hs.length === 1 ? "" : "s"}</div></div>
                      <div className="text-right"><div className="font-mono tabular-nums font-semibold" style={{ fontSize: 12, color: cInk }}>{dispMoney(r.val, display)}</div>{r.id === "cash" ? <div style={{ color: cFaint, fontSize: 11, marginTop: 2 }}>No daily change</div> : <div className="flex items-center gap-1 justify-end whitespace-nowrap" style={{ marginTop: 2 }}><span style={{ color: r.u ? C.up : C.down, fontSize: 11 }} className="font-mono tabular-nums">{r.u ? "+" : ""}{dispMoney(r.absSigned, display)}</span><span style={{ background: r.u ? "rgba(52,160,90,0.16)" : "rgba(211,58,44,0.16)", color: r.u ? C.up : C.down, borderRadius: 6, fontSize: 11 }} className="font-mono font-medium px-1.5 py-0.5">{r.u ? "+" : ""}{num(r.pct)}%</span></div>}</div>
                    </button>
                  )}
                </div>);
            })()}
          </>}

          {tab === "portfolio" && <>
            <div className="flex items-center justify-between mb-6"><h1 className="text-lg font-semibold">Portfolio</h1><div className="flex items-center gap-1.5"><button onClick={refresh} style={{ color: C.ink, background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, borderRadius: 999, width: 34, height: 34, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.10)" }} className="flex items-center justify-center shrink-0 active:opacity-80 active:scale-95 transition-transform"><RefreshCw size={17} className={refreshing ? "animate-spin" : ""} /></button>{variant !== "draft" && <button onPointerDown={startLP} onPointerUp={endLP} onPointerLeave={endLP} onClick={(e) => {e.stopPropagation();tapCcy();}} style={{ color: C.ink, background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, borderRadius: 999, height: 34, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.10)" }} className="flex items-center gap-1.5 pl-3 pr-2.5 text-xs font-semibold active:opacity-80 active:scale-95 transition-transform"><span className="font-mono">{display}</span><ArrowRightLeft size={12} style={{ color: C.ink }} /></button>}{V.allowThemeToggle && <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")} style={{ color: C.ink, background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, borderRadius: 999, width: 34, height: 34, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.10)" }} className="flex items-center justify-center shrink-0 active:opacity-80 active:scale-95 transition-transform">{theme === "dark" ? <Sun size={17} /> : <Moon size={17} />}</button>}{userBtn}</div></div>

            <div className="flex items-center gap-2 mb-1" style={{ minHeight: 26 }}><span style={{ color: C.mute }} className="text-sm flex items-center gap-1.5">Total worth</span>{live && <span style={{ color: C.up, background: "rgba(52,160,90,0.14)", borderRadius: 999 }} className="text-[10px] font-semibold tracking-wide px-1.5 py-0.5">LIVE</span>}</div>

            <div className="flex items-end justify-between mb-2">
              {Hero({ vEUR: shownVal, ccy: display })}
              {variant === "draft" && <button onPointerDown={startLP} onPointerUp={endLP} onPointerLeave={endLP} onClick={tapCcy} style={{ background: C.panel2, color: C.ink, borderRadius: 9 }} className="flex items-center gap-1 text-sm font-medium px-2.5 py-1.5 mb-1 select-none"><span className="font-mono">{display}</span><ArrowRightLeft size={13} style={{ color: C.mute }} /></button>}
            </div>

            {totals.value > 0 && <div className="flex items-center gap-2 mb-1"><span style={{ background: upShown ? "rgba(52,160,90,0.14)" : "rgba(211,58,44,0.14)", color: upShown ? C.up : C.down, borderRadius: 10, fontSize: "14px" }} className="tabular-nums font-semibold px-2.5 py-1 whitespace-nowrap">{upShown ? "+" : ""}{num(shownChange.pct)}%</span><span className="tabular-nums whitespace-nowrap" style={{ color: upShown ? C.up : C.down, "--rollY": "-.55em", fontSize: "14px" }}>{scrub ? <>{upShown ? "+" : ""}{dispMoney(shownChange.abs, display)}</> : <RollDigits text={(upShown ? "+" : "") + dispMoney(shownChange.abs, display)} />}</span>{scrub && <span style={{ color: C.faint }} className="text-xs whitespace-nowrap">at cursor</span>}</div>}

            <div className="my-3" style={{ marginLeft: -20, marginRight: -4 }}><TotalChart data={totalSeries} h={260} pad={66} C={C} live={live} scrub={scrub} setScrub={setScrub} fmt={(v) => dispMoney(v, display)} /></div>
            <div className="mb-6"><RangeBar /></div>

            <div style={{ color: C.mute }} className="text-xs mb-2 px-1">Holdings · {holdings.length}</div>
            {holdings.length === 0 && <div style={{ background: C.panel, border: `1px dashed ${C.line}`, borderRadius: 16, color: C.mute }} className="text-sm text-center px-6 py-10">No holdings yet. Type a trade above, or tap + to add your first one.</div>}
            <div className="space-y-1.5">
              {(showAll ? holdings : holdings.slice(0, 10)).map((h) => {const ch = holdingChange(h);const chgPct = ch.pct,chgEUR = ch.abs;const up = chgPct >= 0;const isCash = h.type === "cash";return (
                  <button key={h.symbol} onClick={() => {setSelected(h.symbol);setSubtab("general");}} style={{ ...cardBg, borderRadius: 16 }} className="w-full text-left px-3.5 py-3 flex items-center gap-3 active:opacity-70">
                      <Logo symbol={h.symbol} size={38} />
                      <div className="flex-1 min-w-0"><div className="font-semibold truncate" style={{ fontSize: 13, lineHeight: 1.5, color: C.ink }}>{h.ticker}</div><div className="truncate" style={{ color: C.mute, fontSize: 11, marginTop: 3 }}>{isCash ? h.name : <>{qfmt(h.qty)} · {dispMoney(eurOf(h.px, h.currency), display)}</>}</div></div>
                      <div className="text-right"><div className="font-mono tabular-nums font-semibold" style={{ fontSize: 13, color: C.ink }}>{dispMoney(h.valEUR, display)}</div>{isCash ? <div style={{ color: C.faint, fontSize: 11, marginTop: 2 }}>No daily change</div> : <div className="flex items-center gap-1 justify-end whitespace-nowrap" style={{ marginTop: 2 }}><span style={{ color: up ? C.up : C.down, fontSize: 11 }} className="font-mono tabular-nums">{up ? "+" : ""}{dispMoney(chgEUR, display)}</span><span style={{ background: up ? "rgba(52,160,90,0.16)" : "rgba(211,58,44,0.16)", color: up ? C.up : C.down, borderRadius: 6, fontSize: 11 }} className="font-mono font-medium px-1.5 py-0.5">{up ? "+" : ""}{num(chgPct)}%</span></div>}</div>
                </button>);})}
              {holdings.length > 10 && <button onClick={() => setShowAll((s) => !s)} style={{ ...cardBg, borderRadius: 14, color: C.accentInk }} className="w-full py-2.5 text-sm font-medium flex items-center justify-center gap-1">{showAll ? "Show less" : `See all ${holdings.length}`}<ChevronRight size={15} style={{ transform: showAll ? "rotate(-90deg)" : "rotate(90deg)" }} /></button>}
            </div>
          </>}

          {tab === "transactions" && (() => {
            const typeOfTx = (t) => CATALOG[t.symbol]?.type || "stock";
            const filtered = allTx.filter((t) => txFilter === "all" || typeOfTx(t) === txFilter);
            const shown = txSort === "value" ? [...filtered].sort((a, b) => eurOf(b.quantity * b.price, b.currency) - eurOf(a.quantity * a.price, a.currency)) : txSort === "old" ? [...filtered].reverse() : filtered;
            const SORTS = { new: "Newest", old: "Oldest", value: "Largest" };
            const nextSort = { new: "old", old: "value", value: "new" };
            return <>
              <div className="flex items-start justify-between mb-4"><div><h1 className="text-lg font-semibold mb-1">Transactions</h1><p style={{ color: C.mute }} className="text-xs">{shown.length} transaction{shown.length === 1 ? "" : "s"} · {SORTS[txSort].toLowerCase()} first</p></div>{userBtn}</div>
              <div className="flex items-center justify-between gap-2 mb-4">
                <div className="noscroll flex gap-1 overflow-x-auto" style={{ scrollbarWidth: "none" }}>{[{ id: "all", label: "All" }, ...CLASSES].map(({ id, label }) => <button key={id} onClick={() => setTxFilter(id)} style={{ background: txFilter === id ? C.panel2 : "transparent", color: txFilter === id ? C.ink : C.faint, border: `1px solid ${txFilter === id ? C.line : "transparent"}`, borderRadius: 999, flexShrink: 0 }} className="text-xs font-medium px-3 py-1.5">{label}</button>)}</div>
                <button onClick={() => setTxSort(nextSort[txSort])} style={{ background: C.panel2, color: C.mute, borderRadius: 999 }} className="flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 shrink-0"><ArrowUpDown size={12} />{SORTS[txSort]}</button>
              </div>
              {shown.length === 0 && <div style={{ background: C.panel, border: `1px dashed ${C.line}`, borderRadius: 16, color: C.mute }} className="text-sm text-center px-6 py-10">No {txFilter === "all" ? "" : (CLASSES.find((c) => c.id === txFilter)?.label || "") + " "}transactions yet.</div>}
              <div className="space-y-3">{shown.map((t) => <TxCard key={t.id} t={t} />)}</div>
            </>;})()}
        </div>
      </div>

      {/* What's going on? — floating bottom modal */}
      {insightOpen && (() => {
      const insUp = dayChange.abs >= 0;
      const insInk = insUp ? C.up : C.down;
      const insSoft = insUp ? "rgba(52,160,90,0.14)" : "rgba(211,58,44,0.14)";
      return (
      <>
        <div onClick={() => setInsightOpen(false)} className="slideup" style={{ position: "absolute", inset: 0, zIndex: 45, background: "rgba(0,0,0,0.32)", backdropFilter: "blur(2px)", WebkitBackdropFilter: "blur(2px)" }}></div>
      <div className="slideup" style={{ position: "absolute", left: 0, right: 0, bottom: 0, zIndex: 46, ...glass, borderTopLeftRadius: 22, borderTopRightRadius: 22, boxShadow: "0 -12px 48px rgba(0,0,0,0.5)" }}>
          <div style={{ width: 36, height: 5, borderRadius: 3, background: glassDark ? "rgba(255,255,255,0.22)" : "rgba(0,0,0,0.16)", margin: "10px auto 0" }}></div>
          <div className="p-4" style={{ paddingBottom: native ? "calc(16px + env(safe-area-inset-bottom))" : 22 }}>
            <div className="flex items-center gap-3">
              <span style={{ background: insSoft, color: insInk, width: 30, height: 30, borderRadius: 9 }} className="flex items-center justify-center shrink-0"><Sparkles size={15} /></span>
              <div style={{ color: insInk }} className="text-[11px] font-semibold uppercase tracking-wide flex-1 min-w-0">Today's one thing</div>
              <button onClick={() => setInsightOpen(false)} style={{ color: C.mute }} className="shrink-0"><X size={16} /></button>
            </div>
            {insightBusy && !insight ?
              <div style={{ color: C.mute, marginTop: 14 }} className="text-sm flex items-center gap-2"><Loader2 size={14} className="animate-spin" />Reading your portfolio…</div> :
              <div style={{ color: C.ink, fontSize: 15, fontWeight: 600, lineHeight: 1.4, textWrap: "pretty", marginTop: 14 }}>{insight && insight.headline}</div>}
            {insight && insight.signals && insight.signals.length > 0 &&
              <div className="flex flex-col gap-2" style={{ ...cardBg, borderRadius: 14, padding: "12px 14px", marginTop: 40 }}>
                {insight.signals.map((sg, i) => {
                  const tc = sg.tone === "up" ? C.up : sg.tone === "down" ? C.down : C.ink;
                  return (
                    <div key={i} className="flex items-center justify-between gap-3">
                      <span style={{ color: C.faint }} className="text-xs">{sg.label}</span>
                      <span className="font-mono tabular-nums text-xs font-semibold" style={{ color: tc }}>{sg.value}</span>
                    </div>);
                })}
              </div>}
            {insight && insight.note &&
              <div style={{ color: C.faint, marginTop: 12, display: "flex", alignItems: "center", gap: 6, whiteSpace: "nowrap", overflow: "hidden" }} className="text-[11px]">
                <Lock size={11} style={{ flexShrink: 0 }} /><span style={{ overflow: "hidden", textOverflow: "ellipsis" }}>{insight.note}</span>
              </div>}
          </div>
        </div>
      </>);})()}

      {ccyPop && <><div style={{ position: "absolute", inset: 0, zIndex: 31 }} onClick={() => setCcyPop(false)} /><div className="pop" style={{ position: "absolute", top: 104, ...(tab === "insights" || variant !== "draft" ? { right: 14 } : { left: 120 }), zIndex: 32, ...glass, borderRadius: 14, boxShadow: "0 12px 40px rgba(0,0,0,0.45)", minWidth: 180, maxHeight: 300, overflowY: "auto", overflowX: "hidden" }} className="noscroll"><div style={{ position: "absolute", top: -5, ...(tab === "insights" || variant !== "draft" ? { right: 24 } : { left: 24 }), width: 10, height: 10, background: C.glassBg, borderLeft: `1px solid ${C.glassBorder}`, borderTop: `1px solid ${C.glassBorder}`, transform: "rotate(45deg)" }} /><div style={{ color: C.faint, position: "sticky", top: 0, background: C.glassBg, zIndex: 1 }} className="text-xs px-4 pt-3 pb-1">Display currency</div>{PAIRS.map((c) => <button key={c} onClick={() => pickPair(c)} className="w-full px-4 py-2.5 flex items-center justify-between" style={{ borderTop: `1px solid ${C.line}` }}><span className="text-sm font-medium">EUR · {c}</span>{counter === c && <Check size={16} style={{ color: C.accentInk }} />}</button>)}</div></>}

      {/* iOS 26-style bottom scrim — Liquid Glass: translucent tint + blur, fading upward */}
      <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, height: 128, zIndex: 33, pointerEvents: "none", background: wsbWash && tab === "insights" ? "transparent" : effTheme === "dark" ? "linear-gradient(to top, rgba(10,11,13,0.62) 18%, rgba(10,11,13,0))" : "linear-gradient(to top, rgba(242,243,245,0.65) 18%, rgba(242,243,245,0))", WebkitMaskImage: "linear-gradient(to top, black 45%, transparent)", maskImage: "linear-gradient(to top, black 45%, transparent)", backdropFilter: "blur(14px) saturate(180%)", WebkitBackdropFilter: "blur(14px) saturate(180%)" }}></div>

      {/* unified tabs + add on a single row; the two tabs collapse to the left on scroll (iOS 26) */}
      <div style={{ position: "absolute", left: 14, right: 14, bottom: native ? "max(18px, env(safe-area-inset-bottom))" : 18, zIndex: 35, display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10 }}>
        <div style={{ ...tabBarStyle, borderRadius: 999 }} className="flex items-center gap-1 p-1.5">
          {[{ id: "insights", label: "Overview", Icon: TrendingUp }, { id: "portfolio", label: "Portfolio", Icon: PieChart }, { id: "transactions", label: "Transactions", Icon: ArrowLeftRight }].map(({ id, label, Icon }) => {const on = tab === id;const showL = on && !navCollapsed;return (
              <button key={id} onClick={() => {setTab(id);setSelected(null);}} style={{ ...(on ? tabActiveStyle : { background: "transparent", color: tabInactiveColor }), borderRadius: 999 }} className="flex items-center text-sm px-4 py-2.5">
              <Icon size={17} style={{ flexShrink: 0 }} />
              <span style={{ display: "inline-block", maxWidth: showL ? 120 : 0, opacity: showL ? 1 : 0, marginLeft: showL ? 8 : 0, overflow: "hidden", whiteSpace: "nowrap", fontWeight: on ? 700 : 600, transition: "max-width .34s cubic-bezier(.4,0,.2,1), opacity .22s, margin-left .34s cubic-bezier(.4,0,.2,1)" }}>{label}</span>
            </button>);})}
        </div>
        <button onClick={() => openAdd("menu")} style={{ ...fabStyle, width: 50, height: 50, borderRadius: 25, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 10px 30px rgba(0,0,0,0.18)", flexShrink: 0 }} className="flex items-center justify-center"><Plus size={24} /></button>
      </div>

      {toast && <div className="slideup flex items-center gap-2" style={{ position: "absolute", left: "50%", transform: "translateX(-50%)", bottom: 150, zIndex: 60, padding: "10px 16px", ...glass, borderRadius: 999, boxShadow: "0 10px 30px rgba(0,0,0,0.45)" }}><span style={{ background: C.up, color: "#fff", width: 18, height: 18, borderRadius: 9 }} className="flex items-center justify-center shrink-0"><Check size={12} /></span><span className="text-sm font-medium" style={{ color: C.ink, whiteSpace: "nowrap" }}>{toast}</span></div>}

      {addOpen && AddSheet()}

      {verSheet && <>
        <div onClick={() => {setVerSheet(false);setSettingsPage(null);}} style={{ position: "absolute", inset: 0, zIndex: 69, background: "rgba(0,0,0,0.5)", animation: "fadeIn .28s ease both" }}></div>
        <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, top: 60, zIndex: 70, background: glassDark ? "#0B0B0D" : C.bg, borderTopLeftRadius: 18, borderTopRightRadius: 18, overflow: "hidden", display: "flex", flexDirection: "column", boxShadow: "0 -12px 50px rgba(0,0,0,0.45)", animation: "sheetUp .42s cubic-bezier(.32,.72,.33,1) both" }}>
          {(() => {
            const sLine = glassDark ? "rgba(84,84,88,0.55)" : "rgba(60,60,67,0.18)";
            const cardStyle = { background: glassDark ? "#1C1C1E" : "#FFFFFF", borderRadius: 16, overflow: "hidden" };
            const glassClose = { color: glassDark ? "rgba(235,235,245,0.6)" : "rgba(60,60,67,0.6)", background: glassDark ? "rgba(120,120,128,0.26)" : "rgba(120,120,128,0.14)", borderRadius: 999, width: 30, height: 30 };
            const Row = ({ Icon, color, label, value, onClick, active = true, last = false, control }) => (
              <button onClick={active ? onClick : undefined} disabled={!active} style={{ position: "relative", width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "11px 16px", opacity: active ? 1 : 0.4, cursor: active && onClick ? "pointer" : "default", textAlign: "left" }} className={active && onClick ? "active:opacity-70" : ""}>
                <span style={{ width: 29, height: 29, borderRadius: 7, background: color, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><Icon size={16} color="#FFFFFF" /></span>
                <span style={{ flex: 1, minWidth: 0, fontSize: 16, fontWeight: 400, color: C.ink, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{label}</span>
                {value != null && value !== "" && <span style={{ fontSize: 15, color: C.mute, whiteSpace: "nowrap" }}>{value}</span>}
                {control || (onClick && active ? <ChevronRight size={16} style={{ color: C.faint, flexShrink: 0 }} /> : null)}
                {!last && <span style={{ position: "absolute", left: 57, right: 0, bottom: 0, height: 1, background: sLine }}></span>}
              </button>);
            const GroupLabel = ({ children }) => <div style={{ fontSize: 13, fontWeight: 400, color: C.faint, margin: "4px 6px 7px" }}>{children}</div>;
            const CCY_LIST = [["EUR", "€"], ["USD", "$"], ["GBP", "£"], ["JPY", "¥"], ["CHF", "CHF"], ["CAD", "C$"], ["AUD", "A$"], ["SGD", "S$"]];
            const CCY_POP = [["EUR", "€"], ["USD", "$"], ["GBP", "£"], ["JPY", "¥"]];
            const CCY_ALL = [["CHF", "CHF"], ["CAD", "C$"], ["AUD", "A$"], ["SGD", "S$"], ["DKK", "kr"], ["NOK", "kr"]];
            const pickCcy = (c) => {setDisplay(c);if (c !== "EUR") setCounter(c);setCcyOpen(false);Haptics.select();};
            const CcyOpt = ([c, s], i, arr) => <button key={c} onClick={() => pickCcy(c)} style={{ position: "relative", width: "100%", display: "flex", alignItems: "center", gap: 10, padding: "12px 16px", textAlign: "left" }} className="active:opacity-70"><span style={{ width: 22, color: C.accentInk, flexShrink: 0 }}>{display === c ? <Check size={16} /> : null}</span><span style={{ flex: 1, fontSize: 16, color: C.ink }}>{c} ({s})</span>{i < arr.length - 1 && <span style={{ position: "absolute", left: 48, right: 0, bottom: 0, height: 1, background: sLine }}></span>}</button>;

            if (settingsPage === "currency") {
              return <>
                <div style={{ position: "relative", display: "flex", alignItems: "center", justifyContent: "center", padding: "16px 16px 14px" }}>
                  <button onClick={() => {setSettingsPage(null);setCcyOpen(false);}} aria-label="Back" style={{ ...glassClose, position: "absolute", left: 16 }} className="flex items-center justify-center"><ChevronLeft size={20} /></button>
                  <span style={{ fontSize: 17, fontWeight: 600, color: C.ink }}>Currency</span>
                </div>
                <div className="flex-1 overflow-y-auto noscroll px-4" style={{ paddingBottom: 28 }}>
                  <div style={cardStyle}>
                    <button onClick={() => setCcyOpen((o) => !o)} style={{ width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "14px 16px", textAlign: "left" }} className="active:opacity-70">
                      <span style={{ flex: 1, fontSize: 16, fontWeight: 600, color: C.ink }}>Default Currency</span>
                      <span style={{ fontSize: 15, color: C.mute }}>{display} ({CCY_SYM[display] || display})</span><ArrowUpDown size={15} style={{ color: C.faint }} />
                    </button>
                    {ccyOpen && <div style={{ borderTop: `1px solid ${sLine}` }}><div style={{ fontSize: 13, color: C.faint, padding: "10px 16px 4px" }}>Popular currencies</div>{CCY_POP.map((e, i) => CcyOpt(e, i, CCY_POP))}<div style={{ fontSize: 13, color: C.faint, padding: "10px 16px 4px", borderTop: `1px solid ${sLine}` }}>All currencies</div>{CCY_ALL.map((e, i) => CcyOpt(e, i, CCY_ALL))}</div>}
                  </div>
                  <p style={{ fontSize: 12.5, color: C.mute, lineHeight: 1.45, margin: "10px 6px 22px" }}>This default currency is used in your overview metrics. Every value is converted into this currency.</p>
                  <GroupLabel>Exchange rates</GroupLabel>
                  <div style={cardStyle}>
                    <div style={{ display: "flex", alignItems: "center", padding: "13px 16px", borderBottom: `1px solid ${sLine}` }}><span style={{ flex: 1, fontSize: 15, color: C.ink }}>Last update</span><span style={{ fontSize: 14, color: C.mute }}>{new Date().toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" })}</span></div>
                    <button onClick={() => {refresh();Haptics.tap();}} style={{ width: "100%", display: "flex", alignItems: "center", gap: 8, padding: "13px 16px", textAlign: "left" }} className="active:opacity-70"><RefreshCw size={15} style={{ color: C.accentInk }} className={refreshing ? "animate-spin" : ""} /><span style={{ fontSize: 15, fontWeight: 600, color: C.accentInk }}>Refresh rates</span></button>
                  </div>
                  <p style={{ fontSize: 12.5, color: C.mute, lineHeight: 1.45, margin: "10px 6px 0" }}>Rates refresh automatically every 5 days. You can also refresh manually once per day.</p>
                </div>
              </>;
            }

            return <>
              <div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 90, background: glassDark ? "linear-gradient(180deg, rgba(140,70,40,0.18) 0%, rgba(140,70,40,0) 100%)" : "linear-gradient(180deg, rgba(140,70,40,0.08) 0%, rgba(140,70,40,0) 100%)", pointerEvents: "none" }}></div>
              <div style={{ position: "relative", display: "flex", alignItems: "center", justifyContent: "center", padding: "16px 16px 14px" }}>
                <span style={{ fontSize: 17, fontWeight: 600, color: C.ink }}>Settings</span>
                <button onClick={() => setVerSheet(false)} aria-label="Close" style={{ ...glassClose, position: "absolute", right: 16 }} className="flex items-center justify-center"><X size={18} /></button>
              </div>
              <div className="flex-1 overflow-y-auto noscroll px-4" style={{ paddingBottom: 28 }}>
                <GroupLabel>View</GroupLabel>
                <div style={{ ...cardStyle, marginBottom: 22 }}>
                  {Object.entries(VARIANTS).map(([id, v], i) => {const on = id === variant;const sw = { draft: "#2F6BED", editorial: "#136F63", wsb: "#2DE07A", data: "#E8A33D" }[id];return (
                    <button key={id} onClick={() => {setVariant && setVariant(id);setSelected(null);Haptics.select();}} style={{ position: "relative", width: "100%", display: "flex", alignItems: "center", gap: 12, padding: "11px 16px", textAlign: "left" }} className="active:opacity-70">
                      <span style={{ width: 29, height: 29, borderRadius: 7, background: sw, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><span style={{ width: 11, height: 11, borderRadius: 6, background: "#FFFFFF" }}></span></span>
                      <span style={{ flex: 1, minWidth: 0 }}><span style={{ display: "block", fontSize: 16, fontWeight: 500, color: C.ink, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{ARCHETYPES[id].tag}</span><span style={{ display: "block", fontSize: 12.5, color: C.faint, marginTop: 1 }}>{v.label}</span></span>
                      {on ? <Check size={18} style={{ color: C.accentInk }} /> : <span style={{ width: 18 }}></span>}
                      {i < 3 && <span style={{ position: "absolute", left: 57, right: 0, bottom: 0, height: 1, background: sLine }}></span>}
                    </button>);
                  })}
                </div>

                <div style={{ ...cardStyle, marginBottom: 22 }}>
                  <Row Icon={User} color="#8E8E93" label="Personalization" value="" active={false} />
                  <Row Icon={theme === "dark" ? Moon : Sun} color="#0A84FF" label="Appearance" value={theme === "dark" ? "Dark" : "Light"} onClick={() => {setTheme(theme === "dark" ? "light" : "dark");Haptics.select();}} />
                  <Row Icon={Sparkles} color="#FF453A" label="Notifications" active={false} />
                  <Row Icon={Wallet} color="#5E5CE6" label="Currency" value={`${display} (${CCY_SYM[display] || display})`} onClick={() => setSettingsPage("currency")} last />
                </div>

                <div style={{ ...cardStyle, marginBottom: 22 }}>
                  <Row Icon={Hand} color="#8E8E93" label="Privacy & Security" active={false} />
                  <Row Icon={Upload} color="#0A84FF" label="iCloud & Data" active={false} last />
                </div>

                <div style={{ ...cardStyle, marginBottom: 22 }}>
                  <Row Icon={Search} color="#30D158" label="Support" active={false} />
                  <Row Icon={AlertTriangle} color="#BF5AF2" label="About" active={false} last />
                </div>

                <button onClick={() => {setVerSheet(false);setSettingsPage(null);try {sessionStorage.setItem("pt-ui-chosen", "");} catch (e) {}setChooser(true);setSelected(null);Haptics.warn();}} style={{ color: C.down, border: `1px solid ${C.down}`, borderRadius: 12, background: "transparent" }} className="w-full py-3 text-sm font-semibold flex items-center justify-center gap-1.5"><RefreshCw size={15} /> Restart flow</button>
              </div>
            </>;
          })()}
        </div>
      </>}

      {killConfirm && <div style={{ position: "absolute", inset: 0, zIndex: 70, background: "rgba(0,0,0,0.55)" }} className="flex items-center justify-center p-6" onClick={() => setKillConfirm(false)}>
        <div onClick={(e) => e.stopPropagation()} style={{ background: C.bg, borderRadius: 18, maxWidth: 300 }} className="w-full p-5 text-center">
          <span style={{ background: "rgba(217,68,44,0.12)", color: C.down, width: 48, height: 48, borderRadius: 24 }} className="flex items-center justify-center mx-auto mb-3"><AlertTriangle size={24} /></span>
          <h3 className="font-bold text-lg mb-1" style={{ color: C.ink }}>Discard imported file?</h3>
          <p className="text-sm mb-5" style={{ color: C.mute }}>This clears the {impRows ? impRows.length : 0} transaction{(impRows ? impRows.length : 0) > 1 ? "s" : ""} from the uploaded file. Your existing portfolio stays untouched.</p>
          <div className="flex gap-2"><button onClick={() => setKillConfirm(false)} style={{ background: C.panel2, color: C.ink, borderRadius: 12 }} className="flex-1 py-3 font-semibold">Cancel</button><button onClick={killAll} style={{ background: C.down, color: "#FFFFFF", borderRadius: 12 }} className="flex-1 py-3 font-semibold flex items-center justify-center gap-1.5"><Trash2 size={16} /> Discard</button></div>
        </div>
      </div>}

      {tab === "portfolio" && selected && (() => {const hh = holdings.find((x) => x.symbol === selected);if (!hh) return null;return (
          <>
        <div onClick={() => setSelected(null)} style={{ position: "absolute", inset: 0, zIndex: 69, background: "rgba(0,0,0,0.5)", animation: "fadeIn .28s ease both" }}></div>
        <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, top: 60, zIndex: 70, background: wsbWash ? washBg : C.bg, borderTopLeftRadius: 18, borderTopRightRadius: 18, overflow: "hidden", display: "flex", flexDirection: "column", boxShadow: "0 -12px 50px rgba(0,0,0,0.45)", animation: "sheetUp .42s cubic-bezier(.32,.72,.33,1) both" }}>
          <div><div style={{ width: 36, height: 5, borderRadius: 3, background: glassDark ? "rgba(255,255,255,0.22)" : "rgba(0,0,0,0.16)", margin: "10px auto 0" }}></div></div>
          <button onClick={() => setSelected(null)} aria-label="Close" style={{ position: "absolute", top: 14, right: 14, zIndex: 6, color: C.ink, background: C.glassBg, backdropFilter: "blur(22px) saturate(180%)", WebkitBackdropFilter: "blur(22px) saturate(180%)", border: `1px solid ${C.glassBorder}`, borderRadius: 999, width: 34, height: 34, boxShadow: "inset 0 1px 0 rgba(255,255,255,0.22), 0 1px 3px rgba(0,0,0,0.10)" }} className="flex items-center justify-center active:opacity-80 active:scale-95 transition-transform"><X size={18} /></button>
          <div className="flex-1 overflow-y-auto noscroll px-5" style={{ paddingTop: 6, paddingBottom: 28 }}><Detail h={hh} modal /></div>
        </div>
        </>);
      })()}

      {classModal && (() => {
        const list = holdings.filter((h) => (h.type || "stock") === classModal);
        const meta = { etf: { label: "ETFs", Icon: Layers, color: "#2D9CDB" }, stock: { label: "Stocks", Icon: Building2, color: "#8A7BD8" }, crypto: { label: "Crypto", Icon: Bitcoin, color: "#F7931A" }, bond: { label: "Bonds", Icon: Landmark, color: "#5B6B9E" }, token: { label: "Tokenization", Icon: Boxes, color: "#2FA8A0" }, cash: { label: "Cash", Icon: Wallet, color: "#44705C" } }[classModal] || { label: CATALOG[list[0]?.symbol]?.catLabel || classModal, Icon: Sparkles, color: CATALOG[list[0]?.symbol]?.color || "#8A8F98" };
        const sheetBg = wsbWash ? washBg : C.bg;
        return (
          <>
          <div onClick={() => setClassModal(null)} style={{ position: "absolute", inset: 0, zIndex: 69, background: "rgba(0,0,0,0.5)", animation: "fadeIn .28s ease both" }}></div>
          <div style={{ position: "absolute", left: 0, right: 0, bottom: 0, top: 60, zIndex: 70, background: sheetBg, borderTopLeftRadius: 18, borderTopRightRadius: 18, overflow: "hidden", display: "flex", flexDirection: "column", boxShadow: "0 -12px 50px rgba(0,0,0,0.45)", animation: "sheetUp .42s cubic-bezier(.32,.72,.33,1) both" }}>
            <div><div style={{ width: 36, height: 5, borderRadius: 3, background: glassDark ? "rgba(255,255,255,0.22)" : "rgba(0,0,0,0.16)", margin: "10px auto 0" }}></div></div>
            <div className="px-5 pt-3 pb-4 flex items-center justify-between">
              <div className="flex items-center gap-3">
                <span style={{ width: 40, height: 40, borderRadius: 20, background: glassDark ? "rgba(255,255,255,0.05)" : "#FFFFFF", border: glassDark ? "1px solid rgba(255,255,255,0.16)" : "1px solid rgba(0,0,0,0.06)" }} className="flex items-center justify-center shrink-0"><meta.Icon size={19} color={meta.color} /></span>
                <div><div className="font-semibold" style={{ fontSize: 17, color: C.ink }}>{meta.label}</div><div style={{ color: C.mute, fontSize: 12 }}>{list.length} asset{list.length === 1 ? "" : "s"}</div></div>
              </div>
              <button onClick={() => setClassModal(null)} style={{ background: C.panel2, color: C.mute, width: 32, height: 32, borderRadius: 16 }} className="flex items-center justify-center"><X size={17} /></button>
            </div>
            <div className="flex-1 overflow-y-auto noscroll px-5" style={{ paddingBottom: 28 }}>
              <div className="space-y-2.5">
                {list.map((h) => {
                    const sf = getAssetRel(h.symbol)[dayIdx] || 1;const u = !dayDown;const isCash = h.type === "cash";
                    const chgAbs = (u ? 1 : -1) * Math.abs(h.valEUR * (1 - sf));const chgPct = (u ? 1 : -1) * Math.abs(sf ? (1 - sf) / sf * 100 : 0);
                    return (
                      <div key={h.symbol} style={{ ...cardBg, borderRadius: 16 }} className="w-full text-left px-3.5 py-3 flex items-center gap-3">
                      <Logo symbol={h.symbol} size={38} />
                      <div className="flex-1 min-w-0"><div className="font-semibold truncate" style={{ fontSize: 13, lineHeight: 1.5, color: C.ink }}>{h.ticker}</div><div className="truncate" style={{ color: C.mute, fontSize: 11, marginTop: 3 }}>{isCash ? h.name : <>{qfmt(h.qty)} · {dispMoney(eurOf(h.px, h.currency), display)}</>}</div></div>
                      <div className="text-right"><div className="font-mono tabular-nums font-semibold" style={{ fontSize: 13, color: C.ink }}>{dispMoney(h.valEUR, display)}</div>{isCash ? <div style={{ color: C.faint, fontSize: 11, marginTop: 2 }}>No daily change</div> : <div className="flex items-center gap-1 justify-end whitespace-nowrap" style={{ marginTop: 2 }}><span style={{ color: u ? C.up : C.down, fontSize: 11 }} className="font-mono tabular-nums">{u ? "+" : ""}{dispMoney(chgAbs, display)}</span><span style={{ background: u ? "rgba(52,160,90,0.16)" : "rgba(211,58,44,0.16)", color: u ? C.up : C.down, borderRadius: 6, fontSize: 11 }} className="font-mono font-medium px-1.5 py-0.5">{u ? "+" : ""}{num(chgPct)}%</span></div>}</div>
                    </div>);
                  })}
              </div>
            </div>
          </div>
          </>);
      })()}
    </div>);

}

// Page layout lives in showcase.jsx
Object.assign(window, { App, THEME });