const { useState, useEffect, useRef, useCallback } = React;

// Initialize Supabase client from CDN-loaded library
window.supabaseClient = window.supabase.createClient(window.SUPABASE_URL, window.SUPABASE_ANON_KEY);

/* ─── SECURITIES DATABASE (unchanged from v6) ─────────────────────── */
const STOCKS = [
  /* ── TECHNOLOGY & MEDIA ── */
  {ticker:"NPN",name:"Naspers",sector:"Technology",type:"Stock",tv:"JSE:NPN"},
  {ticker:"PRX",name:"Prosus",sector:"Technology",type:"Stock",tv:"JSE:PRX"},
  {ticker:"MFL",name:"Metrofile",sector:"Technology",type:"Stock",tv:"JSE:MFL"},
  {ticker:"DTC",name:"Datatec",sector:"Technology",type:"Stock",tv:"JSE:DTC"},
  {ticker:"MCG",name:"MultiChoice Group",sector:"Technology",type:"Stock",tv:"JSE:MCG"},
  {ticker:"CMH",name:"Combined Motor Holdings",sector:"Technology",type:"Stock",tv:"JSE:CMH"},
  {ticker:"EOH",name:"EOH Holdings",sector:"Technology",type:"Stock",tv:"JSE:EOH"},
  {ticker:"ALT",name:"Allied Electronics",sector:"Technology",type:"Stock",tv:"JSE:ALT"},
  /* ── CONSUMER STAPLES ── */
  {ticker:"BTI",name:"British American Tobacco",sector:"Consumer Staples",type:"Stock",tv:"JSE:BTI"},
  {ticker:"AVI",name:"AVI Limited",sector:"Consumer Staples",type:"Stock",tv:"JSE:AVI"},
  {ticker:"TBS",name:"Tiger Brands",sector:"Consumer Staples",type:"Stock",tv:"JSE:TBS"},
  {ticker:"RCL",name:"RCL Foods",sector:"Consumer Staples",type:"Stock",tv:"JSE:RCL"},
  {ticker:"OCE",name:"Oceana Group",sector:"Consumer Staples",type:"Stock",tv:"JSE:OCE"},
  {ticker:"CLS",name:"Clicks Group",sector:"Consumer Staples",type:"Stock",tv:"JSE:CLS"},
  {ticker:"DCP",name:"Dis-Chem Pharmacies",sector:"Consumer Staples",type:"Stock",tv:"JSE:DCP"},
  {ticker:"PIK",name:"Pick n Pay Stores",sector:"Consumer Staples",type:"Stock",tv:"JSE:PIK"},
  {ticker:"SHP",name:"Shoprite Holdings",sector:"Consumer Staples",type:"Stock",tv:"JSE:SHP"},
  {ticker:"SPR",name:"Spar Group",sector:"Consumer Staples",type:"Stock",tv:"JSE:SPR"},
  {ticker:"FBR",name:"Famous Brands",sector:"Consumer Staples",type:"Stock",tv:"JSE:FBR"},
  {ticker:"RFG",name:"Rhodes Food Group",sector:"Consumer Staples",type:"Stock",tv:"JSE:RFG"},
  {ticker:"ADW",name:"AdvTech",sector:"Consumer Staples",type:"Stock",tv:"JSE:ADW"},
  {ticker:"DGH",name:"Distell Group",sector:"Consumer Staples",type:"Stock",tv:"JSE:DGH"},
  {ticker:"AWB",name:"Awilco",sector:"Consumer Staples",type:"Stock",tv:"JSE:AWB"},
  /* ── RETAIL ── */
  {ticker:"WHL",name:"Woolworths Holdings",sector:"Retail",type:"Stock",tv:"JSE:WHL"},
  {ticker:"TFG",name:"The Foschini Group",sector:"Retail",type:"Stock",tv:"JSE:TFG"},
  {ticker:"MRP",name:"Mr Price Group",sector:"Retail",type:"Stock",tv:"JSE:MRP"},
  {ticker:"TRU",name:"Truworths International",sector:"Retail",type:"Stock",tv:"JSE:TRU"},
  {ticker:"LEW",name:"Lewis Group",sector:"Retail",type:"Stock",tv:"JSE:LEW"},
  {ticker:"PPH",name:"Pepkor Holdings",sector:"Retail",type:"Stock",tv:"JSE:PPH"},
  {ticker:"MSM",name:"Massmart Holdings",sector:"Retail",type:"Stock",tv:"JSE:MSM"},
  {ticker:"CSB",name:"Cashbuild",sector:"Retail",type:"Stock",tv:"JSE:CSB"},
  {ticker:"ITE",name:"Italtile",sector:"Retail",type:"Stock",tv:"JSE:ITE"},
  /* ── LUXURY & GLOBAL ── */
  {ticker:"CFR",name:"Richemont",sector:"Luxury & Global",type:"Stock",tv:"JSE:CFR"},
  {ticker:"MNP",name:"Mondi",sector:"Luxury & Global",type:"Stock",tv:"JSE:MNP"},
  {ticker:"BAT",name:"Brait",sector:"Luxury & Global",type:"Stock",tv:"JSE:BAT"},
  /* ── MINING ── */
  {ticker:"BHP",name:"BHP Group",sector:"Mining",type:"Stock",tv:"JSE:BHP"},
  {ticker:"AGL",name:"Anglo American",sector:"Mining",type:"Stock",tv:"JSE:AGL"},
  {ticker:"ARI",name:"African Rainbow Minerals",sector:"Mining",type:"Stock",tv:"JSE:ARI"},
  {ticker:"KIO",name:"Kumba Iron Ore",sector:"Mining",type:"Stock",tv:"JSE:KIO"},
  {ticker:"EXX",name:"Exxaro Resources",sector:"Mining",type:"Stock",tv:"JSE:EXX"},
  {ticker:"S32",name:"South32",sector:"Mining",type:"Stock",tv:"JSE:S32"},
  {ticker:"AFT",name:"Afrimat",sector:"Mining",type:"Stock",tv:"JSE:AFT"},
  {ticker:"SSW",name:"Sibanye-Stillwater",sector:"Mining",type:"Stock",tv:"JSE:SSW"},
  {ticker:"TGA",name:"Thungela Resources",sector:"Mining",type:"Stock",tv:"JSE:TGA"},
  {ticker:"GLN",name:"Glencore",sector:"Mining",type:"Stock",tv:"JSE:GLN"},
  {ticker:"AFE",name:"AECI",sector:"Mining",type:"Stock",tv:"JSE:AFE"},
  {ticker:"PPC",name:"PPC Limited",sector:"Mining",type:"Stock",tv:"JSE:PPC"},
  {ticker:"ARH",name:"Arch Coal",sector:"Mining",type:"Stock",tv:"JSE:ARH"},
  /* ── GOLD MINING ── */
  {ticker:"GFI",name:"Gold Fields",sector:"Gold Mining",type:"Stock",tv:"JSE:GFI"},
  {ticker:"ANG",name:"AngloGold Ashanti",sector:"Gold Mining",type:"Stock",tv:"JSE:ANG"},
  {ticker:"HAR",name:"Harmony Gold",sector:"Gold Mining",type:"Stock",tv:"JSE:HAR"},
  {ticker:"DRD",name:"DRDGold",sector:"Gold Mining",type:"Stock",tv:"JSE:DRD"},
  {ticker:"PAN",name:"Pan African Resources",sector:"Gold Mining",type:"Stock",tv:"JSE:PAN"},
  {ticker:"GLD",name:"Gold One International",sector:"Gold Mining",type:"Stock",tv:"JSE:GLD"},
  /* ── PGMs ── */
  {ticker:"IMP",name:"Impala Platinum",sector:"PGMs",type:"Stock",tv:"JSE:IMP"},
  {ticker:"AMS",name:"Anglo American Platinum",sector:"PGMs",type:"Stock",tv:"JSE:AMS"},
  {ticker:"NHM",name:"Northam Platinum",sector:"PGMs",type:"Stock",tv:"JSE:NHM"},
  {ticker:"RBP",name:"Royal Bafokeng Platinum",sector:"PGMs",type:"Stock",tv:"JSE:RBP"},
  {ticker:"PPH2",name:"Petra Diamonds",sector:"PGMs",type:"Stock",tv:"JSE:PPH"},
  {ticker:"TGS",name:"Trans Hex",sector:"PGMs",type:"Stock",tv:"JSE:TGS"},
  /* ── ENERGY ── */
  {ticker:"SOL",name:"Sasol",sector:"Energy",type:"Stock",tv:"JSE:SOL"},
  {ticker:"ENX",name:"Enx Group",sector:"Energy",type:"Stock",tv:"JSE:ENX"},
  {ticker:"RNI",name:"Renergen",sector:"Energy",type:"Stock",tv:"JSE:RNI"},
  {ticker:"VVO",name:"Vivo Energy",sector:"Energy",type:"Stock",tv:"JSE:VVO"},
  {ticker:"MMP",name:"Montauk Renewables",sector:"Energy",type:"Stock",tv:"JSE:MMP"},
  /* ── BANKS ── */
  {ticker:"CPI",name:"Capitec Bank",sector:"Financials",type:"Stock",tv:"JSE:CPI"},
  {ticker:"FSR",name:"FirstRand",sector:"Financials",type:"Stock",tv:"JSE:FSR"},
  {ticker:"SBK",name:"Standard Bank",sector:"Financials",type:"Stock",tv:"JSE:SBK"},
  {ticker:"NED",name:"Nedbank Group",sector:"Financials",type:"Stock",tv:"JSE:NED"},
  {ticker:"ABG",name:"Absa Group",sector:"Financials",type:"Stock",tv:"JSE:ABG"},
  {ticker:"INL",name:"Investec",sector:"Financials",type:"Stock",tv:"JSE:INL"},
  {ticker:"INP",name:"Investec Plc",sector:"Financials",type:"Stock",tv:"JSE:INP"},
  {ticker:"ARL",name:"African Rainbow Capital",sector:"Financials",type:"Stock",tv:"JSE:ARL"},
  {ticker:"ZED",name:"Zeder Investments",sector:"Financials",type:"Stock",tv:"JSE:ZED"},
  /* ── ASSET MANAGEMENT ── */
  {ticker:"PSG",name:"PSG Group",sector:"Financials",type:"Stock",tv:"JSE:PSG"},
  {ticker:"PPR",name:"PSG Konsult",sector:"Financials",type:"Stock",tv:"JSE:PPR"},
  {ticker:"CML",name:"Coronation Fund Managers",sector:"Financials",type:"Stock",tv:"JSE:CML"},
  {ticker:"NIN",name:"Ninety One",sector:"Financials",type:"Stock",tv:"JSE:NIN"},
  {ticker:"NY1",name:"Ninety One Plc",sector:"Financials",type:"Stock",tv:"JSE:NY1"},
  {ticker:"SFN",name:"Sasfin Holdings",sector:"Financials",type:"Stock",tv:"JSE:SFN"},
  {ticker:"JSEL",name:"JSE Limited",sector:"Financials",type:"Stock",tv:"JSE:JSE"},
  {ticker:"TCP",name:"Transaction Capital",sector:"Financials",type:"Stock",tv:"JSE:TCP"},
  {ticker:"PAY",name:"Purple Group",sector:"Financials",type:"Stock",tv:"JSE:PAY"},
  /* ── INSURANCE ── */
  {ticker:"DSY",name:"Discovery Limited",sector:"Insurance",type:"Stock",tv:"JSE:DSY"},
  {ticker:"SLM",name:"Sanlam",sector:"Insurance",type:"Stock",tv:"JSE:SLM"},
  {ticker:"MTM",name:"Momentum Metropolitan",sector:"Insurance",type:"Stock",tv:"JSE:MTM"},
  {ticker:"OMU",name:"Old Mutual",sector:"Insurance",type:"Stock",tv:"JSE:OMU"},
  {ticker:"LBH",name:"Liberty Holdings",sector:"Insurance",type:"Stock",tv:"JSE:LBH"},
  {ticker:"SNT",name:"Santam",sector:"Insurance",type:"Stock",tv:"JSE:SNT"},
  {ticker:"OUT",name:"OUTsurance Group",sector:"Insurance",type:"Stock",tv:"JSE:OUT"},
  {ticker:"CLI",name:"Clientele",sector:"Insurance",type:"Stock",tv:"JSE:CLI"},
  /* ── HEALTHCARE ── */
  {ticker:"NTC",name:"Netcare",sector:"Healthcare",type:"Stock",tv:"JSE:NTC"},
  {ticker:"MEI",name:"Mediclinic International",sector:"Healthcare",type:"Stock",tv:"JSE:MEI"},
  {ticker:"APN",name:"Aspen Pharmacare",sector:"Healthcare",type:"Stock",tv:"JSE:APN"},
  {ticker:"LHC",name:"Life Healthcare",sector:"Healthcare",type:"Stock",tv:"JSE:LHC"},
  {ticker:"ADH",name:"Adcock Ingram",sector:"Healthcare",type:"Stock",tv:"JSE:ADH"},
  {ticker:"AFR",name:"AfroCentric Investment",sector:"Healthcare",type:"Stock",tv:"JSE:AFR"},
  /* ── TELECOMS ── */
  {ticker:"MTN",name:"MTN Group",sector:"Telecoms",type:"Stock",tv:"JSE:MTN"},
  {ticker:"VOD",name:"Vodacom Group",sector:"Telecoms",type:"Stock",tv:"JSE:VOD"},
  {ticker:"TKG",name:"Telkom SA",sector:"Telecoms",type:"Stock",tv:"JSE:TKG"},
  {ticker:"BLU",name:"Blue Label Telecoms",sector:"Telecoms",type:"Stock",tv:"JSE:BLU"},
  {ticker:"AYO",name:"AYO Technology",sector:"Telecoms",type:"Stock",tv:"JSE:AYO"},
  /* ── INDUSTRIALS ── */
  {ticker:"BVT",name:"Bidvest Group",sector:"Industrials",type:"Stock",tv:"JSE:BVT"},
  {ticker:"BAW",name:"Barloworld",sector:"Industrials",type:"Stock",tv:"JSE:BAW"},
  {ticker:"REM",name:"Remgro",sector:"Industrials",type:"Stock",tv:"JSE:REM"},
  {ticker:"IPL",name:"Imperial Logistics",sector:"Industrials",type:"Stock",tv:"JSE:IPL"},
  {ticker:"KAP",name:"KAP Industrial",sector:"Industrials",type:"Stock",tv:"JSE:KAP"},
  {ticker:"SPG",name:"Super Group",sector:"Industrials",type:"Stock",tv:"JSE:SPG"},
  {ticker:"WBO",name:"Wilson Bayly Holmes",sector:"Industrials",type:"Stock",tv:"JSE:WBO"},
  {ticker:"MUR",name:"Murray & Roberts",sector:"Industrials",type:"Stock",tv:"JSE:MUR"},
  {ticker:"GRP",name:"Grindrod",sector:"Industrials",type:"Stock",tv:"JSE:GRP"},
  {ticker:"HCI",name:"Hosken Consolidated Investments",sector:"Industrials",type:"Stock",tv:"JSE:HCI"},
  {ticker:"NVS",name:"Novus Holdings",sector:"Industrials",type:"Stock",tv:"JSE:NVS"},
  {ticker:"RBX",name:"Raubex Group",sector:"Industrials",type:"Stock",tv:"JSE:RBX"},
  {ticker:"INV",name:"Invicta Holdings",sector:"Industrials",type:"Stock",tv:"JSE:INV"},
  {ticker:"NPH",name:"Northam Holdings",sector:"Industrials",type:"Stock",tv:"JSE:NPH"},
  {ticker:"AEG",name:"Aveng",sector:"Industrials",type:"Stock",tv:"JSE:AEG"},
  {ticker:"CCO",name:"Capco",sector:"Industrials",type:"Stock",tv:"JSE:CCO"},
  /* ── PROPERTY (REITs) ── */
  {ticker:"GRT",name:"Growthpoint Properties",sector:"Property",type:"Stock",tv:"JSE:GRT"},
  {ticker:"RDF",name:"Redefine Properties",sector:"Property",type:"Stock",tv:"JSE:RDF"},
  {ticker:"HYP",name:"Hyprop Investments",sector:"Property",type:"Stock",tv:"JSE:HYP"},
  {ticker:"FFB",name:"Fortress REIT B",sector:"Property",type:"Stock",tv:"JSE:FFB"},
  {ticker:"FFA",name:"Fortress REIT A",sector:"Property",type:"Stock",tv:"JSE:FFA"},
  {ticker:"EMI",name:"Emira Property Fund",sector:"Property",type:"Stock",tv:"JSE:EMI"},
  {ticker:"SAR",name:"SA Corporate Real Estate",sector:"Property",type:"Stock",tv:"JSE:SAR"},
  {ticker:"ATT",name:"Attacq",sector:"Property",type:"Stock",tv:"JSE:ATT"},
  {ticker:"SSS",name:"Stor-Age Property REIT",sector:"Property",type:"Stock",tv:"JSE:SSS"},
  {ticker:"SRE",name:"Sirius Real Estate",sector:"Property",type:"Stock",tv:"JSE:SRE"},
  {ticker:"VKE",name:"Vukile Property Fund",sector:"Property",type:"Stock",tv:"JSE:VKE"},
  {ticker:"L2D",name:"Liberty Two Degrees",sector:"Property",type:"Stock",tv:"JSE:L2D"},
  {ticker:"RES",name:"Resilient REIT",sector:"Property",type:"Stock",tv:"JSE:RES"},
  {ticker:"DIB",name:"Dipula Income Fund B",sector:"Property",type:"Stock",tv:"JSE:DIB"},
  {ticker:"OAS",name:"Oasis Crescent Property",sector:"Property",type:"Stock",tv:"JSE:OAS"},
  {ticker:"EPP",name:"EPP NV",sector:"Property",type:"Stock",tv:"JSE:EPP"},
  {ticker:"NRP",name:"NEPI Rockcastle",sector:"Property",type:"Stock",tv:"JSE:NRP"},
  {ticker:"MAS",name:"MAS Real Estate",sector:"Property",type:"Stock",tv:"JSE:MAS"},
  {ticker:"ACS",name:"Accelerate Property Fund",sector:"Property",type:"Stock",tv:"JSE:ACS"},
  /* ── TOURISM & LEISURE ── */
  {ticker:"TSH",name:"Tsogo Sun Hotels",sector:"Tourism",type:"Stock",tv:"JSE:TSH"},
  {ticker:"SUI",name:"Sun International",sector:"Tourism",type:"Stock",tv:"JSE:SUI"},
  {ticker:"SHG",name:"Spur Corporation",sector:"Tourism",type:"Stock",tv:"JSE:SHG"},
  {ticker:"CCL",name:"City Lodge Hotels",sector:"Tourism",type:"Stock",tv:"JSE:CCL"},
  {ticker:"HMN",name:"Hammerson",sector:"Tourism",type:"Stock",tv:"JSE:HMN"},
  /* ── EDUCATION ── */
  {ticker:"ADV",name:"AdvTech Group",sector:"Education",type:"Stock",tv:"JSE:ADV"},
  {ticker:"CTK",name:"Curro Holdings",sector:"Education",type:"Stock",tv:"JSE:CTK"},
  {ticker:"STA",name:"Stadio Holdings",sector:"Education",type:"Stock",tv:"JSE:STA"},
  /* ── SA TOP 40 ETFs ── */
  {ticker:"STX40",name:"Satrix 40",sector:"SA Sector ETF",type:"ETF",tv:"JSE:STX40"},
  {ticker:"STXFIN",name:"Satrix Financials",sector:"SA Sector ETF",type:"ETF",tv:"JSE:STXFIN"},
  {ticker:"STXIND",name:"Satrix Industrials",sector:"SA Sector ETF",type:"ETF",tv:"JSE:STXIND"},
  {ticker:"STXRES",name:"Satrix Resources",sector:"SA Sector ETF",type:"ETF",tv:"JSE:STXRES"},
  {ticker:"STXPRO",name:"Satrix Property",sector:"SA Property ETF",type:"ETF",tv:"JSE:STXPRO"},
  {ticker:"STXSWX",name:"Satrix Swix Top 40",sector:"SA Sector ETF",type:"ETF",tv:"JSE:STXSWX"},
  {ticker:"STXQUA",name:"Satrix Quality SA",sector:"SA Sector ETF",type:"ETF",tv:"JSE:STXQUA"},
  {ticker:"STXMOM",name:"Satrix Momentum SA",sector:"SA Sector ETF",type:"ETF",tv:"JSE:STXMOM"},
  {ticker:"STXCAP",name:"Satrix Capped SWIX",sector:"SA Sector ETF",type:"ETF",tv:"JSE:STXCAP"},
  /* ── GLOBAL ETFs ── */
  {ticker:"STXNDQ",name:"Satrix Nasdaq 100",sector:"Global ETF",type:"ETF",tv:"JSE:STXNDQ"},
  {ticker:"STXWDM",name:"Satrix MSCI World",sector:"Global ETF",type:"ETF",tv:"JSE:STXWDM"},
  {ticker:"STXEMG",name:"Satrix MSCI Emerging Markets",sector:"Global ETF",type:"ETF",tv:"JSE:STXEMG"},
  {ticker:"STX500",name:"Satrix S&P 500",sector:"Global ETF",type:"ETF",tv:"JSE:STX500"},
  {ticker:"STXCHN",name:"Satrix MSCI China",sector:"Global ETF",type:"ETF",tv:"JSE:STXCHN"},
  {ticker:"STXIDA",name:"Satrix MSCI India",sector:"Global ETF",type:"ETF",tv:"JSE:STXIDA"},
  {ticker:"SYGUS",name:"Sygnia S&P 500",sector:"Global ETF",type:"ETF",tv:"JSE:SYGUS"},
  {ticker:"SYGWD",name:"Sygnia MSCI World",sector:"Global ETF",type:"ETF",tv:"JSE:SYGWD"},
  {ticker:"SYGJP",name:"Sygnia Itrix Japan",sector:"Global ETF",type:"ETF",tv:"JSE:SYGJP"},
  {ticker:"SYGEU",name:"Sygnia Itrix Euro Stoxx",sector:"Global ETF",type:"ETF",tv:"JSE:SYGEU"},
  {ticker:"SYGEM",name:"Sygnia Itrix MSCI EM",sector:"Global ETF",type:"ETF",tv:"JSE:SYGEM"},
  {ticker:"SYG4IR",name:"Sygnia 4th Industrial Rev",sector:"Global ETF",type:"ETF",tv:"JSE:SYG4IR"},
  {ticker:"SYGH",name:"Sygnia Healthcare Innovation",sector:"Global ETF",type:"ETF",tv:"JSE:SYGH"},
  {ticker:"PTXSPY",name:"PureTracks S&P 500",sector:"Global ETF",type:"ETF",tv:"JSE:PTXSPY"},
  {ticker:"ASHGEQ",name:"Ashburton Global 1200",sector:"Global ETF",type:"ETF",tv:"JSE:ASHGEQ"},
  {ticker:"ASHMID",name:"Ashburton MidCap ETF",sector:"SA Sector ETF",type:"ETF",tv:"JSE:ASHMID"},
  {ticker:"ASHWLD",name:"Ashburton Developed World",sector:"Global ETF",type:"ETF",tv:"JSE:ASHWLD"},
  {ticker:"ASHEMG",name:"Ashburton EM Equity",sector:"Global ETF",type:"ETF",tv:"JSE:ASHEMG"},
  /* ── DIVIDEND ETFs ── */
  {ticker:"DIVTRX",name:"Coreshares Dividend ETF",sector:"SA Dividend ETF",type:"ETF",tv:"JSE:DIVTRX"},
  {ticker:"STXDIV",name:"Satrix Dividend Plus",sector:"SA Dividend ETF",type:"ETF",tv:"JSE:STXDIV"},
  {ticker:"DIVX",name:"Satrix Dividend ETF",sector:"SA Dividend ETF",type:"ETF",tv:"JSE:DIVX"},
  {ticker:"STXDPL",name:"Satrix Divi Plus",sector:"SA Dividend ETF",type:"ETF",tv:"JSE:STXDPL"},
  /* ── PROPERTY ETFs ── */
  {ticker:"PROPTRX",name:"Coreshares SA Property",sector:"SA Property ETF",type:"ETF",tv:"JSE:PROPTRX"},
  {ticker:"GLPROP",name:"Grindrod Property ETF",sector:"SA Property ETF",type:"ETF",tv:"JSE:GLPROP"},
  {ticker:"SBPP",name:"Standard Bank Property ETF",sector:"SA Property ETF",type:"ETF",tv:"JSE:SBPP"},
  {ticker:"GLPROP2",name:"Sygnia Itrix SA Listed Property",sector:"SA Property ETF",type:"ETF",tv:"JSE:GLPROP2"},
  /* ── CORESHARES ── */
  {ticker:"CTOP50",name:"CoreShares Top 50",sector:"SA Sector ETF",type:"ETF",tv:"JSE:CTOP50"},
  {ticker:"COCS40",name:"CoreShares S&P SA Comp",sector:"SA Sector ETF",type:"ETF",tv:"JSE:COCS40"},
  {ticker:"CORETOP",name:"CoreShares Equally Weighted Top 40",sector:"SA Sector ETF",type:"ETF",tv:"JSE:CORETOP"},
  {ticker:"CSP500",name:"CoreShares S&P 500",sector:"Global ETF",type:"ETF",tv:"JSE:CSP500"},
  /* ── NEWFUNDS ── */
  {ticker:"NFEMOM",name:"Newfunds Momentum ETF",sector:"SA Sector ETF",type:"ETF",tv:"JSE:NFEMOM"},
  {ticker:"NFEDEF",name:"Newfunds Equity Defensive",sector:"SA Sector ETF",type:"ETF",tv:"JSE:NFEDEF"},
  {ticker:"NFEVAL",name:"Newfunds Value ETF",sector:"SA Sector ETF",type:"ETF",tv:"JSE:NFEVAL"},
  {ticker:"NFEHIY",name:"Newfunds High Income",sector:"SA Bond ETF",type:"ETF",tv:"JSE:NFEHIY"},
  /* ── COMMODITY ETFs ── */
  {ticker:"NEWGLD",name:"Absa NewGold ETF",sector:"Commodity ETF",type:"ETF",tv:"JSE:NEWGLD"},
  {ticker:"ETFGLD",name:"1nvest Gold ETF",sector:"Commodity ETF",type:"ETF",tv:"JSE:ETFGLD"},
  {ticker:"ETFPLT",name:"1nvest Platinum ETF",sector:"Commodity ETF",type:"ETF",tv:"JSE:ETFPLT"},
  {ticker:"ETFPLD",name:"1nvest Palladium ETF",sector:"Commodity ETF",type:"ETF",tv:"JSE:ETFPLD"},
  {ticker:"ETFRHO",name:"1nvest Rhodium ETF",sector:"Commodity ETF",type:"ETF",tv:"JSE:ETFRHO"},
  {ticker:"NEWPLT",name:"NewPlat ETF",sector:"Commodity ETF",type:"ETF",tv:"JSE:NEWPLT"},
  {ticker:"NEWPLD",name:"NewPalladium ETF",sector:"Commodity ETF",type:"ETF",tv:"JSE:NEWPLD"},
  /* ── BOND & MONEY MARKET ETFs ── */
  {ticker:"STXILB",name:"Satrix Inflation Linked Bond",sector:"SA Bond ETF",type:"ETF",tv:"JSE:STXILB"},
  {ticker:"STXGOV",name:"Satrix SA Government Bond",sector:"SA Bond ETF",type:"ETF",tv:"JSE:STXGOV"},
  {ticker:"NFEGOVI",name:"Newfunds GOVI Bond ETF",sector:"SA Bond ETF",type:"ETF",tv:"JSE:NFEGOVI"},
  {ticker:"NFETRACI",name:"Newfunds TRACI Bond ETF",sector:"SA Bond ETF",type:"ETF",tv:"JSE:NFETRACI"},
  {ticker:"STXMMT",name:"Satrix Money Market ETF",sector:"Money Market ETF",type:"ETF",tv:"JSE:STXMMT"},
  {ticker:"NFEMONEY",name:"Newfunds Money Market",sector:"Money Market ETF",type:"ETF",tv:"JSE:NFEMONEY"},
  /* ── MULTI-ASSET ETFs ── */
  {ticker:"STXBAL",name:"Satrix Balanced",sector:"Multi-Asset ETF",type:"ETF",tv:"JSE:STXBAL"},
  {ticker:"STXGRO",name:"Satrix Growth",sector:"Multi-Asset ETF",type:"ETF",tv:"JSE:STXGRO"},
  {ticker:"STXCNS",name:"Satrix Conservative",sector:"Multi-Asset ETF",type:"ETF",tv:"JSE:STXCNS"},
  {ticker:"STXMUL",name:"Satrix Multi-Asset",sector:"Multi-Asset ETF",type:"ETF",tv:"JSE:STXMUL"},
  /* ── COMMODITY ETNs ── */
  {ticker:"SBUL",name:"Standard Bank Platinum ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SBUL"},
  {ticker:"SPAL",name:"Standard Bank Palladium ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SPAL"},
  {ticker:"SGLD",name:"Standard Bank Gold ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SGLD"},
  {ticker:"SSIL",name:"Standard Bank Silver ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SSIL"},
  {ticker:"SCOP",name:"Standard Bank Copper ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SCOP"},
  {ticker:"SOIL",name:"Standard Bank Brent Crude ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SOIL"},
  {ticker:"SCOC",name:"Standard Bank Cocoa ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SCOC"},
  {ticker:"SWHT",name:"Standard Bank Wheat ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SWHT"},
  {ticker:"SCOF",name:"Standard Bank Coffee ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SCOF"},
  {ticker:"SSOY",name:"Standard Bank Soya ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SSOY"},
  {ticker:"SCOR",name:"Standard Bank Corn ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SCOR"},
  {ticker:"SSUG",name:"Standard Bank Sugar ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SSUG"},
  {ticker:"SLEAD",name:"Standard Bank Lead ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SLEAD"},
  {ticker:"SZINC",name:"Standard Bank Zinc ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SZINC"},
  {ticker:"SNICK",name:"Standard Bank Nickel ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SNICK"},
  {ticker:"SALU",name:"Standard Bank Aluminium ETN",sector:"Commodity ETN",type:"ETN",tv:"JSE:SALU"},
];

/* ─── COMPANY LOGO DOMAINS ──────────────────────────────────────────
   Maps JSE tickers to company domains for logo fetching via Logo.dev.
   Falls back to letter icon when domain is unavailable.           */
const LOGO_DOMAINS = {
  NPN:"naspers.com", PRX:"prosus.com", MCG:"multichoice.com",
  DTC:"datatec.com", MTN:"mtn.com", VOD:"vodacom.com", TKG:"telkom.co.za",
  CPI:"capitecbank.co.za", FSR:"firstrand.co.za", SBK:"standardbank.co.za",
  NED:"nedbank.co.za", ABG:"absa.co.za", INL:"investec.com",
  DSY:"discovery.co.za", SLM:"sanlam.co.za", OMU:"oldmutual.com",
  LBH:"libertyholdings.co.za", SNT:"santam.co.za", OUT:"outsurance.co.za",
  GFI:"goldfields.com", ANG:"anglogoldashanti.com", HAR:"harmony.co.za",
  DRD:"drdgold.com", PAN:"panafrican.co.za",
  IMP:"implats.co.za", AMS:"angloplats.com", NHM:"northam.co.za",
  SOL:"sasol.com", RNI:"renergen.co.za",
  AGL:"angloamerican.com", BHP:"bhp.com", EXX:"exxaro.com",
  S32:"south32.net", KIO:"kumba.com", GLN:"glencore.com", TGA:"thungela.com",
  SHP:"shopriteholdings.co.za", SPR:"spar.co.za", PIK:"pnp.co.za",
  CLS:"clicksgroup.co.za", DCP:"dischem.co.za", WHL:"woolworthsholdings.co.za",
  TFG:"tfglimited.co.za", MRP:"mrpricegroup.co.za", TRU:"truworths.co.za",
  PPH:"pepkor.co.za", LEW:"lewisgroup.co.za",
  NTC:"netcare.co.za", MEI:"mediclinic.com", APN:"aspen.co.za",
  LHC:"lifehealthcare.co.za", ADH:"adcock.co.za",
  BVT:"bidvest.co.za", BAW:"barloworld.com", REM:"remgro.com",
  GRP:"grindrod.com", KAP:"kapindustrial.co.za",
  GRT:"growthpoint.co.za", RDF:"redefine.co.za", HYP:"hyprop.co.za",
  ATT:"attacq.co.za", SRE:"sirius-real-estate.com", SSS:"stor-age.com",
  CFR:"richemont.com", BTI:"bat.com", AVI:"avi.co.za",
  TBS:"tigerbrands.com", OCE:"oceana.co.za",
  ADV:"advtech.co.za", CTK:"curro.co.za",
};

function CompanyLogo({ticker, size=44}) {
  const T = useT();
  const [failed, setFailed] = useState(false);
  const domain = LOGO_DOMAINS[ticker];
  const initial = ticker ? ticker.slice(0,1) : "?";
  const col = T.accent;

  if(!domain || failed) {
    return (
      <div style={{
        width:size, height:size, borderRadius:size*0.28,
        background:`linear-gradient(135deg,${T.accent}18,${T.accent}08)`,
        border:`1px solid ${T.border}`,
        display:"flex", alignItems:"center", justifyContent:"center",
        flexShrink:0,
      }}>
        <span style={{fontSize:size*0.4, fontWeight:800, color:T.accent}}>{initial}</span>
      </div>
    );
  }

  return (
    <div style={{
      width:size, height:size, borderRadius:size*0.28,
      border:`1px solid ${T.border}`,
      background:"#fff",
      display:"flex", alignItems:"center", justifyContent:"center",
      flexShrink:0, overflow:"hidden",
    }}>
      <img
        src={`https://img.logo.dev/${domain}?token=pk_X-1ZO13GSgeOoUrIuJ6BeQ`}
        alt={ticker}
        width={size*0.7} height={size*0.7}
        style={{objectFit:"contain"}}
        onError={()=>setFailed(true)}
      />
    </div>
  );
}


const ALL_SECTORS = ["All",...[...new Set(STOCKS.map(s=>s.sector))]];
const TYPES = ["All","Stock","ETF","ETN"];

/* ─── DESIGN TOKENS — Dual Theme System ─────────────────────────────
   Light: Apple.com / Robinhood light — clean, white, trustworthy
   Dark:  Refined dark — soft, premium, not terminal                */
const LIGHT = {
  bg0:"#F2F2F7", bg1:"#FFFFFF", bg2:"#F8F8FA", bg3:"#EFEFF4", bg4:"#E5E5EA",
  border:"#E5E5EA", border2:"#D1D1D6",
  text:"#1C1C1E", text2:"#48484A", text3:"#8E8E93",
  green:"#34C759", greenDim:"#34C75914",
  red:"#FF3B30",   redDim:"#FF3B3012",
  yellow:"#FF9500",yellowDim:"#FF950012",
  orange:"#FF9500",orangeDim:"#FF950012",
  accent:"#00A550",
  cyan:"#007AFF", purple:"#5856D6", gold:"#FF9500", blue:"#007AFF",
  overlay:"rgba(0,0,0,.3)",
  tooltipBg:"#FFFFFF", tooltipText:"#1C1C1E", tooltipBorder:"#E5E5EA",
  tooltipShadow:"0 8px 32px rgba(0,0,0,.12)",
};

const DARK = {
  bg0:"#0D0D0F", bg1:"#141416", bg2:"#1C1C1F", bg3:"#242428", bg4:"#2C2C31",
  border:"#2A2A2F", border2:"#38383F",
  text:"#F5F5F7", text2:"#AEAEB2", text3:"#636366",
  green:"#30D158", greenDim:"#30D15818",
  red:"#FF453A",   redDim:"#FF453A18",
  yellow:"#FFD60A",yellowDim:"#FFD60A18",
  orange:"#FF9F0A",orangeDim:"#FF9F0A18",
  accent:"#30D158",
  cyan:"#64D2FF", purple:"#BF5AF2", gold:"#FFD60A", blue:"#0A84FF",
  overlay:"rgba(0,0,0,.7)",
  tooltipBg:"#1C1C1F", tooltipText:"#F5F5F7", tooltipBorder:"#38383F",
  tooltipShadow:"0 8px 32px rgba(0,0,0,.6)",
};

// T is set dynamically — default to light, overridden by ThemeProvider
let T = {...LIGHT};

const scoreColor = s => s>=8?T.green:s>=6?T.yellow:s>=4?"#FF9800":T.red;
const scoreDim   = s => s>=8?T.greenDim:s>=6?T.yellowDim:s>=4?"#FF980014":T.redDim;
const SIGNALS = {
  "STRONG BUY":{bg:"#00E67614",border:"#00E67640",text:"#00E676",glyph:"▲▲"},
  "BUY":{bg:"#00E67610",border:"#00E67630",text:"#69F0AE",glyph:"▲"},
  "HOLD":{bg:"#FFD60014",border:"#FFD60040",text:"#FFD600",glyph:"◆"},
  "SELL":{bg:"#FF475714",border:"#FF475740",text:"#FF4757",glyph:"▼"},
  "STRONG SELL":{bg:"#FF475710",border:"#FF475730",text:"#FF1744",glyph:"▼▼"},
};
const getSig = s => SIGNALS[s]||SIGNALS["HOLD"];

/* ─── LIVE MACRO DATA via Anthropic web_search ───────────────────────
   Since we can't fetch from arbitrary FX APIs in the browser, we pull
   live prices once on mount via Claude's own web_search tool.
   Fallback values are accurate as of March 10 2026.               */
// Static fallback — used only if all live APIs fail
const MACRO_FALLBACK = {
  xau:"5,095", xauRaw:5095,
  zar:"18.45",  zarRaw:18.45,
  brent:"98.96",brentRaw:98.96,
  alsi:"75,420",
  silver:"83.51",
  platinum:"968",
  updated:"offline fallback",
};

/* ─── LIVE MACRO DATA FETCHER ─────────────────────────────────────
   Fetches live USD/ZAR + metals prices from free CORS-enabled APIs.
   Falls back gracefully to MACRO_FALLBACK on any failure.       */
async function fetchLiveMacro() {
  const result = {...MACRO_FALLBACK};
  const timeout = (ms) => new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms));

  // USD/ZAR from open.er-api.com (free, CORS-enabled, no key required)
  try {
    const r = await Promise.race([
      fetch("https://open.er-api.com/v6/latest/USD").then(r => r.json()),
      timeout(4000)
    ]);
    if (r?.rates?.ZAR) {
      result.zarRaw = r.rates.ZAR;
      result.zar    = r.rates.ZAR.toFixed(2);
    }
  } catch(e) { console.warn("USD/ZAR fetch failed, using fallback"); }

  // Gold + Silver from frankfurter.app (free, CORS-enabled)
  try {
    const r = await Promise.race([
      fetch("https://api.frankfurter.app/latest?from=USD&to=XAU,XAG").then(r => r.json()),
      timeout(4000)
    ]);
    if (r?.rates?.XAU) {
      const goldUsdPerOz = 1 / r.rates.XAU;
      result.xauRaw = goldUsdPerOz;
      result.xau    = goldUsdPerOz.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
    }
    if (r?.rates?.XAG) {
      const silverUsdPerOz = 1 / r.rates.XAG;
      result.silver = silverUsdPerOz.toFixed(2);
    }
  } catch(e) { console.warn("Metals fetch failed, using fallback"); }

  result.updated = new Date().toISOString().slice(0, 16).replace("T", " ") + " UTC";
  return result;
}

/* ─── MACRO CONTEXT BUILDER ───────────────────────────────────────
   Builds a string injected into every analysis prompt so the AI
   has current macro context when scoring securities.            */
function buildMacroCtx(m) {
  return `LIVE MACRO CONTEXT (as of ${m.updated || "now"}):
- Gold (XAU): $${m.xau}/oz
- Silver (XAG): $${m.silver}/oz
- Platinum: $${m.platinum}/oz
- USD/ZAR: R${m.zar}
- Brent Crude: $${m.brent}/bbl
- JSE ALSI: ${m.alsi}

Use these CURRENT values when reasoning about commodity exposure, currency translation, and SA market dynamics.`;
}

/* ─── ANTHROPIC API ─────────────────────────────────────────────────── */
function sanitiseJSON(raw) {
  // Extract outermost { ... }
  const start = raw.indexOf("{");
  const end   = raw.lastIndexOf("}");
  if (start < 0 || end < 0) throw new Error("No JSON object found in model response.");
  let s = raw.slice(start, end + 1);

  // Replace smart/curly quotes with straight quotes
  s = s.replace(/[\u2018\u2019]/g, "'").replace(/[\u201C\u201D]/g, '"');

  // Remove control characters (except \n \r \t which are valid in JSON)
  s = s.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");

  // Remove trailing commas before } or ] — the #1 cause of parse errors
  s = s.replace(/,\s*([}\]])/g, "$1");

  // Attempt parse — if it fails, try stripping anything after the last valid }
  try {
    return JSON.parse(s);
  } catch (e1) {
    // Last-resort: find the last complete top-level } by scanning backwards
    let depth = 0, lastValid = -1;
    for (let i = s.length - 1; i >= 0; i--) {
      if (s[i] === "}") { depth++; }
      if (s[i] === "{") { depth--; if (depth === 0) { lastValid = i; break; } }
    }
    if (lastValid >= 0) {
      try { return JSON.parse(s.slice(lastValid)); } catch(e) {}
    }
    throw new Error(`JSON parse failed: ${e1.message}. Try again — the model returned malformed output.`);
  }
}

/* ── callClaude ────────────────────────────────────────────────────────
   Routes through your Vercel serverless function /api/analyse.
   Your Anthropic key lives in ANTHROPIC_API_KEY env var on Vercel —
   never exposed to the browser. The function also enforces tier limits.
   For local dev / demo mode: set DEMO_MODE=true and pass apiKey directly.
   ──────────────────────────────────────────────────────────────────── */
async function callClaude(authToken, system, userMsg, maxTokens=5000, tier="free") {
  // DEMO MODE: if token looks like an Anthropic key, call direct (dev/testing only)
  const isDemoKey = authToken?.startsWith("sk-ant-");

  async function attempt(attemptNo) {
    if (isDemoKey) {
      const res = await fetch("https://api.anthropic.com/v1/messages", {
        method:"POST",
        headers:{
          "Content-Type":"application/json",
          "x-api-key":authToken,
          "anthropic-version":"2023-06-01",
          "anthropic-dangerous-direct-browser-access":"true"
        },
        body:JSON.stringify({
          model: tier==="institutional" ? "claude-opus-4-7" : tier==="pro" ? "claude-sonnet-4-6" : "claude-haiku-4-5-20251001",
          max_tokens:maxTokens,
          system,
          messages:[{role:"user",content:userMsg}]
        }),
      });
      if(!res.ok){
        const e=await res.json().catch(er=>({}));
        // Retry on rate limit or 5xx
        if((res.status===429||res.status>=500)&&attemptNo<2){
          await new Promise(r=>setTimeout(r,1500*(attemptNo+1)));
          return attempt(attemptNo+1);
        }
        throw new Error(e.error?.message||`API error ${res.status}`);
      }
      const data=await res.json();
      const raw=(data.content?.[0]?.text||"").trim();
      // Detect truncation: if response stopped before closing brace
      const stopReason=data.stop_reason;
      if(stopReason==="max_tokens"&&attemptNo<1){
        // Retry once with even more tokens
        return attempt(attemptNo+1);
      }
      try { return sanitiseJSON(raw); }
      catch(parseErr){
        if(attemptNo<2){
          await new Promise(r=>setTimeout(r,800));
          return attempt(attemptNo+1);
        }
        throw new Error("AI returned malformed data after 3 attempts. Please retry.");
      }
    }

    // PRODUCTION: backend proxy
    let res;
    try {
      res = await fetch("/api/analyse", {
        method:"POST",
        headers:{"Content-Type":"application/json","Authorization":`Bearer ${authToken}`},
        body:JSON.stringify({system, userMsg, maxTokens, tier}),
      });
    } catch(networkErr) {
      throw new Error("Network error — check your connection and try again.");
    }

    // 405 = backend not deployed correctly → tell user to use demo key
    if(res.status === 405) {
      throw new Error("Backend not configured. Use the Developer/Demo mode with your Anthropic key to test analysis.");
    }

    if(!res.ok){
      const e=await res.json().catch(er=>({}));
      if((res.status===429||res.status>=500)&&attemptNo<2){
        await new Promise(r=>setTimeout(r,1500*(attemptNo+1)));
        return attempt(attemptNo+1);
      }
      throw new Error(e.error?.message||`Server error ${res.status}`);
    }
    const data=await res.json();
    try { return sanitiseJSON((data.text||"").trim()); }
    catch(parseErr){
      if(attemptNo<2){
        await new Promise(r=>setTimeout(r,800));
        return attempt(attemptNo+1);
      }
      throw new Error("AI returned malformed data. Please retry the analysis.");
    }
  }

  return attempt(0);
}


const ATLAS_SYS = `You are ATLAS — Senior Macro Analyst, JSE Equity Research Division.

ANALYTICAL FRAMEWORK (you MUST apply ALL of these):

1. DCF VALUATION: Estimate intrinsic value using Discounted Cash Flow.
   - Project 3-year FCF trajectory with explicit growth assumptions
   - Apply WACC appropriate to SA market (typically 12-16% for JSE equities)
   - Calculate terminal value using Gordon Growth Model
   - Express upside/downside as % to intrinsic value estimate

2. FACTOR MODEL SCORING (Fama-French 5-Factor):
   - Size factor (SMB): market cap positioning vs sector peers
   - Value factor (HML): P/B, P/E vs sector averages
   - Profitability factor (RMW): EBITDA margin, ROE trend
   - Investment factor (CMA): capex cycle, balance sheet deployment
   - Momentum factor (WML): 6-12 month price momentum vs ALSI

3. PROBABILITY-WEIGHTED SCENARIO ANALYSIS:
   - Bull case (assign probability %): specific catalyst + 12m target
   - Base case (assign probability %): consensus + 12m target
   - Bear case (assign probability %): specific downside trigger + 12m target
   - Expected value = sum of (probability × return) for each scenario

4. RISK-ADJUSTED METRICS:
   - Estimate beta vs JSE ALSI (market risk)
   - Sharpe ratio proxy: (expected return - risk-free rate) / estimated volatility
   - Risk-free rate = SA 10yr bond yield (~11.5%)
   - Max drawdown risk based on historical sector volatility

5. KELLY CRITERION (position sizing):
   - Kelly % = (Edge × Odds - (1-Edge)) / Odds
   - Where Edge = probability of positive outcome, Odds = win/loss ratio
   - Report suggested portfolio allocation % based on Kelly

6. TECHNICAL CONFIRMATION:
   - RSI momentum signal (overbought >70, oversold <30)
   - 52-week range positioning (where is price vs high/low)
   - Volume trend (accumulation vs distribution)
   - Support/resistance level assessment

PHILOSOPHY: You are bullish on JSE equities structurally. You weight macro tailwinds heavily — commodity supercycles, ZAR dynamics, SA political stability trajectory. Your edge is finding securities where macro tailwinds + cheap valuation + positive momentum align.

CRITICAL: Return ONLY raw JSON. Start { end }. No markdown. No backticks. No preamble. ALL fields MUST be present.

{"overallScore":7.4,"signal":"BUY","conviction":"HIGH",
"headline":"One punchy sentence — the single most important insight right now.",
"summary":"Two sentences. Specific numbers. Specific catalysts. No vague statements.",
"bullCase":"Two sentences. Price target. Specific catalyst. Timeline.",
"bearCase":"Two sentences. Specific downside risk with numbers.",
"keyDrivers":["Driver 1 with specific metric","Driver 2","Driver 3","Driver 4"],
"dcfAnalysis":{
  "intrinsicValueVsCurrent":"e.g. 23% undervalued vs DCF fair value",
  "wacc":"e.g. 14.2%",
  "terminalGrowthRate":"e.g. 4.5%",
  "fcfGrowthAssumption":"e.g. 12% Y1, 9% Y2, 6% Y3",
  "verdict":"UNDERVALUED|FAIRLY VALUED|OVERVALUED"
},
"scenarioAnalysis":{
  "bull":{"probability":35,"return12m":"+45%","catalyst":"Specific catalyst"},
  "base":{"probability":45,"return12m":"+18%","catalyst":"Consensus driver"},
  "bear":{"probability":20,"return12m":"-15%","catalyst":"Specific risk"},
  "expectedValue":"+22.5%"
},
"factorExposures":{
  "sizeSmb":"SMALL|MID|LARGE cap — sector context",
  "valueHml":"CHEAP|FAIR|EXPENSIVE vs sector P/B and P/E",
  "profitabilityRmw":"ROBUST|NEUTRAL|WEAK — EBITDA margin trend",
  "momentumWml":"STRONG|NEUTRAL|WEAK — 6-12 month price trend",
  "overallFactorScore":"e.g. 3 of 5 factors positive"
},
"kellyCriterion":{
  "edge":0.62,
  "suggestedAllocation":"8%",
  "rationale":"One sentence on why this position size makes sense given conviction and volatility"
},
"riskMetrics":{
  "estimatedBeta":"e.g. 1.35 vs JSE ALSI",
  "sharpeProxy":"e.g. 1.8 (attractive risk-adjusted return)",
  "maxDrawdownRisk":"e.g. -28% in severe bear scenario",
  "riskFreeRate":"11.5% (SA 10yr bond)"
},
"technicals":{
  "rsiSignal":"OVERSOLD|NEUTRAL|OVERBOUGHT — estimated RSI ~55",
  "fiftyTwoWeekPosition":"e.g. Trading at 68% of 52-week range",
  "momentumSignal":"BULLISH|NEUTRAL|BEARISH",
  "keyLevelToWatch":"e.g. Support at R145, resistance at R168"
},
"microeconomics":{
  "supplyDemand":"One sentence on supply/demand dynamics.",
  "pricingPower":"One sentence — can this company raise prices?",
  "competitiveMoat":"One sentence — structural advantage.",
  "marginsOutlook":"One sentence on margin trajectory.",
  "demandDrivers":"One sentence on end-market demand."
},
"scores":{"macroMomentum":8,"currencyFlows":7,"sectorRotation":7,"geopoliticalRisk":5,"marketSentiment":8},
"companyMetrics":{
  "peRatio":"Estimated P/E","pbRatio":"P/B","dividendYield":"Yield %",
  "revenueGrowth":"YoY %","ebitdaMargin":"% estimate","netDebtToEbitda":"x ratio",
  "roe":"ROE %","marketCap":"e.g. R280bn","analystConsensus":"e.g. 8 Buy / 3 Hold",
  "lastReported":"e.g. FY2025"
},
"predictions":{
  "threeMonth":{"direction":"UP","low":5,"high":14,"confidence":"HIGH","rationale":"Near-term catalyst."},
  "oneYear":{"direction":"UP","low":18,"high":35,"confidence":"MEDIUM","rationale":"One sentence."},
  "threeYear":{"direction":"UP","low":45,"high":95,"confidence":"MEDIUM","rationale":"One sentence."}
},
"outperformVsAlsi":{"verdict":"OUTPERFORM","probability":68,"rationale":"One sentence vs JSE ALSI."},
"riskFactors":[
  {"risk":"Risk name","severity":"HIGH","detail":"One sentence."},
  {"risk":"Risk 2","severity":"MEDIUM","detail":"One sentence."},
  {"risk":"Risk 3","severity":"LOW","detail":"One sentence."}
],
"peers":[
  {"name":"Peer A","ticker":"XXX","score":8.2,"verdict":"STRONGER","reason":"One sentence."},
  {"name":"Peer B","ticker":"YYY","score":5.1,"verdict":"WEAKER","reason":"One sentence."},
  {"name":"Peer C","ticker":"ZZZ","score":6.9,"verdict":"SIMILAR","reason":"One sentence."}
],
"catalysts":[
  {"event":"Specific upcoming catalyst","timeframe":"Q2 2026","impact":"POSITIVE"},
  {"event":"Second catalyst","timeframe":"H2 2026","impact":"POSITIVE"},
  {"event":"Risk event","timeframe":"Ongoing","impact":"NEGATIVE"}
],
"newsFlow":[
  {"headline":"Realistic specific headline","source":"Bloomberg","date":"Mar 2026","sentiment":"POSITIVE","summary":"One sentence."},
  {"headline":"Second headline","source":"Business Day","date":"Feb 2026","sentiment":"NEUTRAL","summary":"One sentence."},
  {"headline":"Third headline","source":"Reuters","date":"Jan 2026","sentiment":"NEGATIVE","summary":"One sentence."}
]}`;

const ATLAS_LITE_SYS = `You are ATLAS — a JSE macro bull analyst. Precise. Data-specific. No waffle.

PHILOSOPHY: Macro-first. You believe SA equities are mispriced to the upside due to commodity supercycles, ZAR strength, and political stability.

CRITICAL: Return ONLY raw JSON. Start { end }. No markdown. No backticks. No preamble. ALL fields MUST be present.

This is the FREE TIER summary — keep it concise. Do not include companyMetrics, predictions, peers, catalysts, newsFlow, microeconomics, or riskFactors.

{"overallScore":7.4,"signal":"BUY","conviction":"HIGH",
"headline":"One punchy sentence — the single most important reason to own this security right now.",
"summary":"Two sentences max. Specific numbers, specific catalysts. No vague statements.",
"bullCase":"Two sentences. Specific price targets, timelines, catalysts.",
"bearCase":"Two sentences. Specific downside risks with numbers.",
"keyDrivers":["Driver 1 — specific","Driver 2 — specific","Driver 3 — specific"],
"scores":{"macroMomentum":8,"currencyFlows":7,"sectorRotation":7,"geopoliticalRisk":5,"marketSentiment":8}}`;

const MERIDIAN_SYS = `You are MERIDIAN — Senior Risk Analyst, JSE Equity Research Division.

ANALYTICAL FRAMEWORK — RISK-FOCUSED QUANTITATIVE ANALYSIS:

1. DOWNSIDE DCF STRESS TEST:
   - Apply bear-case assumptions: FCF growth -20% below consensus
   - Use stressed WACC (+3% risk premium for tail risks)
   - Calculate maximum justifiable price under stress scenario
   - Express current price vs stressed intrinsic value

2. VALUE-AT-RISK (VaR) ASSESSMENT:
   - Estimate 95% VaR: maximum expected loss in worst 5% of outcomes
   - Historical volatility proxy for this sector/stock type
   - Correlation with ZAR volatility (key SA-specific risk)
   - Black swan scenario: what happens in a -30% ALSI drawdown?

3. MEAN REVERSION ANALYSIS:
   - Is this security extended above its historical mean valuation?
   - Z-score vs 3-year valuation average (P/E, P/B)
   - Reversion risk: how much downside if it reverts to mean?
   - Catalyst that would trigger reversion

4. FACTOR RISK DECOMPOSITION:
   - Which factor exposures create vulnerability?
   - Size risk: small caps have 3x the volatility of large caps
   - Value trap risk: cheap for a reason?
   - Momentum reversal risk: extended rallies historically revert

5. TAIL RISK SCENARIOS:
   - ZAR blowout scenario (-20% ZAR): impact on this stock
   - Commodity cycle reversal scenario: impact
   - SA political risk scenario (Moody's downgrade): impact
   - Global risk-off scenario (EM selloff): impact

PHILOSOPHY: You distrust consensus. You believe markets consistently underprice SA-specific tail risks. You score 0.5-1.5pts below ATLAS on the same security by design. You are not pessimistic — you are precise about risk.

CRITICAL: Return ONLY raw JSON. Start { end }. No markdown. No backticks. ALL fields MUST be present.

{"overallScore":6.1,"signal":"HOLD","conviction":"MEDIUM",
"divergence":"One sharp sentence — where and why you disagree with the bull case. Cite a specific metric.",
"bearCaseDeep":"Two sentences. Most credible bear scenario. Specific triggers, magnitudes, timeframes.",
"hiddenRisks":["Non-obvious structural risk","Regulatory/policy risk","Balance sheet risk"],
"downsideDcf":{
  "stressedWacc":"e.g. 17.5% (base 14.2% + 3.3% risk premium)",
  "stressedFcgGrowth":"e.g. -8% Y1, flat Y2, +3% Y3",
  "stressedIntrinsicValue":"e.g. 34% downside to stressed fair value",
  "currentPriceVsStressed":"STRETCHED|FAIR|DISCOUNTED at current price"
},
"varAssessment":{
  "var95":"e.g. -18% maximum expected loss in worst 5% of months",
  "zarCorrelation":"e.g. HIGH — 0.72 correlation to USD/ZAR moves",
  "alsiDrawdownImpact":"e.g. In -30% ALSI scenario, estimated -42% for this stock",
  "volatilityEstimate":"e.g. 35% annualised volatility estimate"
},
"meanReversionRisk":{
  "peZScore":"e.g. +1.8 standard deviations above 3yr mean P/E",
  "reversionDownside":"e.g. -22% if P/E reverts to 3yr average",
  "reversionCatalyst":"e.g. Earnings miss or rate hike cycle"
},
"tailRiskScenarios":{
  "zarBlowout":"e.g. -15% impact if ZAR weakens 20% from current levels",
  "commodityReversal":"e.g. -28% impact if gold falls to $3,200/oz",
  "politicalRisk":"e.g. -12% impact in Moody's junk downgrade scenario",
  "globalRiskOff":"e.g. -22% in EM selloff comparable to March 2020"
},
"microRisks":{
  "demandDestruction":"One sentence — what could kill demand?",
  "substitutionRisk":"One sentence — can customers switch away?",
  "inputCostRisk":"One sentence — input cost threat to margins?"
},
"scores":{"macroMomentum":6,"currencyFlows":5,"sectorRotation":6,"geopoliticalRisk":3,"marketSentiment":6},
"probabilityWeightedOutlook":"Two sentences. Probability-weighted view acknowledging upside but anchored on realistic base case."}`;



/* ─── TOOLTIP DEFINITIONS ────────────────────────────────────────────── */
const TOOLTIPS = {
  "Macro Momentum":"How strongly the broader macroeconomic environment (GDP growth, interest rates, commodity prices) supports this security right now.",
  "Currency & Flows":"The impact of USD/ZAR moves and foreign capital flows into/out of SA markets on this security's performance.",
  "Sector Rotation":"Whether institutional money is currently rotating INTO this sector vs rotating out to other sectors.",
  "Geopolitical Risk":"Exposure to political instability — SA political risk, EM risk, commodity sanctions, supply chain disruption.",
  "Market Sentiment":"Technical momentum, short-term investor positioning, and news flow sentiment for this security.",
  "P/E Ratio":"Price-to-Earnings: how much investors pay per rand of profit. Lower = potentially cheaper. Market average ~15x.",
  "P/B Ratio":"Price-to-Book: share price vs net asset value. Below 1.0 means trading below book value.",
  "Dividend Yield":"Annual dividend as % of share price. Higher yield = more income, but verify it is sustainable.",
  "Revenue Growth":"Year-over-year sales growth. Positive = expanding business. Negative = shrinking revenues.",
  "EBITDA Margin":"Earnings before interest, tax, depreciation, amortisation as % of revenue. Higher = more profitable operations.",
  "Net Debt/EBITDA":"Debt burden relative to earnings. Below 2x = healthy. Above 4x = high leverage risk.",
  "ROE":"Return on Equity — how efficiently management generates profit from shareholder capital. Above 15% is strong.",
  "Market Cap":"Total value of all shares outstanding. Size matters for liquidity and institutional ownership.",
  "ATLAS Score":"Macro bull analyst — weighted 60% in consensus. Focuses on global tailwinds, commodity cycles, ZAR dynamics.",
  "MERIDIAN Score":"Contrarian risk analyst — weighted 40% in consensus. Stress-tests downside, flags hidden structural risks.",
  "Consensus Score":"Weighted average: ATLAS×60% + MERIDIAN×40%. The final MacroScore rating for this security.",
  "Supply & Demand":"Basic economic principle: when demand exceeds supply, prices rise. Applied to this company's product/service market.",
  "Pricing Power":"Can the company raise prices without losing customers? High pricing power = durable margins.",
  "Competitive Moat":"A structural barrier that protects a company from competition — brand, network effect, scale, patents, switching costs.",
  "Demand Drivers":"The fundamental economic forces creating demand for this company's product or service.",
};

function Tooltip({label, children, dark=false}) {
  const T = useT();
  const [show, setShow] = useState(false);
  const tip = TOOLTIPS[label];
  if(!tip) return children||<span>{label}</span>;
  return (
    <span style={{position:"relative",display:"inline-flex",alignItems:"center",gap:4,cursor:"help"}}
      onMouseEnter={()=>setShow(true)} onMouseLeave={()=>setShow(false)}>
      {children||<span style={{color:T.text2,fontSize:"inherit"}}>{label}</span>}
      <span style={{width:13,height:13,borderRadius:"50%",background:T.border2,border:`1px solid ${T.border2}`,display:"inline-flex",alignItems:"center",justifyContent:"center",fontSize:8,color:T.text3,flexShrink:0,lineHeight:1}}>?</span>
      {show&&<div style={{position:"absolute",bottom:"calc(100% + 8px)",left:0,background:T.tooltipBg,border:`1.5px solid ${T.tooltipBorder}`,borderRadius:12,padding:"12px 14px",width:260,fontSize:12,color:T.tooltipText,lineHeight:1.6,zIndex:9999,boxShadow:T.tooltipShadow,pointerEvents:"none"}}>
        <div style={{fontWeight:700,color:T.accent,fontSize:10,letterSpacing:".06em",textTransform:"uppercase",marginBottom:5}}>{label}</div>
        {tip}
      </div>}
    </span>
  );
}

/* ─── TRADINGVIEW CHART ─────────────────────────────────────────────
   Uses TradingView's widget iframe URL — more reliable than the JS
   embed for JSE symbols. Shows a sign-in prompt if TV returns a login
   wall (detected by iframe load + postMessage where available).
   Users sign into TradingView once in their browser; cookies persist. */
function TVChart({symbol, ticker}) {
  const T = useT();
  const [ready, setReady] = useState(false);
  const [failed, setFailed] = useState(false);
  const tvUrl = `https://www.tradingview.com/widgetembed/?frameElementId=tv_${ticker}&symbol=${encodeURIComponent(symbol)}&interval=D&hidesidetoolbar=0&symboledit=0&saveimage=0&toolbarbg=060A0E&studies=RSI%40tv-basicstudies%2CMACD%40tv-basicstudies&theme=dark&style=1&timezone=Africa%2FJohannesburg&withdateranges=1&hideideas=1&locale=en`;
  const directUrl = `https://www.tradingview.com/chart/?symbol=${encodeURIComponent(symbol)}`;

  return (
    <div>
      <div style={{position:"relative",height:440,borderRadius:8,overflow:"hidden",border:`1px solid ${T.border}`,background:T.bg2}}>
        {!ready && !failed && (
          <div style={{position:"absolute",inset:0,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:8,zIndex:2}}>
            <div style={{width:24,height:24,border:`2px solid ${T.border2}`,borderTop:`2px solid ${T.accent}`,borderRadius:"50%",animation:"spin 1s linear infinite"}}/>
            <span style={{fontSize:11,color:T.text3}}>Loading chart…</span>
            <style>{`@keyframes spin{to{transform:rotate(360deg)}}`}</style>
          </div>
        )}
        {failed && (
          <div style={{position:"absolute",inset:0,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:12,padding:24,zIndex:2}}>
            <div style={{fontSize:11,color:T.text2,textAlign:"center",lineHeight:1.6,marginBottom:4}}>
              Charts for JSE securities require a free TradingView account.<br/>
              Sign in once — charts load automatically on return visits.
            </div>
            <div style={{display:"flex",gap:8}}>
              <a href="https://www.tradingview.com/gopro/?feature=sign_in" target="_blank" rel="noopener noreferrer"
                style={{background:T.accent,color:"#000",fontSize:10,fontWeight:900,padding:"8px 16px",borderRadius:6,textDecoration:"none",letterSpacing:".06em"}}>
                SIGN IN FREE →
              </a>
              <a href={directUrl} target="_blank" rel="noopener noreferrer"
                style={{background:T.bg3,color:T.text2,fontSize:10,fontWeight:700,padding:"8px 14px",borderRadius:6,textDecoration:"none",border:`1px solid ${T.border2}`}}>
                OPEN {ticker} ON TRADINGVIEW
              </a>
            </div>
            <div style={{fontSize:9,color:T.text3}}>After signing in, close this tab and reopen the chart.</div>
          </div>
        )}
        <iframe
          key={symbol}
          src={tvUrl}
          style={{width:"100%",height:"100%",border:"none",opacity:ready?1:0,transition:"opacity .3s"}}
          onLoad={()=>{ setReady(true); setFailed(false); }}
          onError={()=>{ setFailed(true); setReady(false); }}
          sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
          title={`${ticker} chart`}
        />
      </div>
      <div style={{marginTop:6,display:"flex",justifyContent:"space-between",alignItems:"center"}}>
        <span style={{fontSize:9,color:T.text3}}>Powered by TradingView · {symbol} · Daily · RSI + MACD</span>
        <a href={directUrl} target="_blank" rel="noopener noreferrer" style={{fontSize:9,color:T.text3,textDecoration:"none"}}>Full chart →</a>
      </div>
    </div>
  );
}


/* ─── SCORE RING ────────────────────────────────────────────────────── */
function ScoreRing({score,size=80}){
  const T = useT();
  const r=size/2-7,circ=2*Math.PI*r,fill=(score/10)*circ,col=scoreColor(score);
  return(
    <svg width={size} height={size} style={{transform:"rotate(-90deg)"}}>
      <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={T.border2} strokeWidth={5}/>
      <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={col} strokeWidth={5}
        strokeDasharray={`${fill} ${circ}`} strokeLinecap="round"
        style={{transition:"stroke-dasharray 1.2s cubic-bezier(.4,0,.2,1)",filter:`drop-shadow(0 0 5px ${col})`}}/>
      <text x="50%" y="50%" textAnchor="middle" dominantBaseline="central"
        style={{transform:"rotate(90deg)",transformOrigin:"center",fill:col,fontSize:size*.25,fontWeight:900,fontFamily:"monospace"}}>
        {score?.toFixed(1)}
      </text>
    </svg>
  );
}

function FactorBar({label,score,weight,color}){
  const T = useT();
  const col=color||scoreColor(score);
  return(
    <div style={{marginBottom:13}}>
      <div style={{display:"flex",justifyContent:"space-between",marginBottom:5}}>
        <Tooltip label={label}><span style={{fontSize:11,color:T.text2,textTransform:"uppercase",letterSpacing:".05em",fontWeight:500}}>{label}</span></Tooltip>
        <div style={{display:"flex",gap:8}}>
          <span style={{fontSize:10,color:T.text3,fontFamily:"monospace"}}>{weight}</span>
          <span style={{fontSize:13,fontWeight:800,color:col,fontFamily:"monospace",minWidth:32,textAlign:"right"}}>{score}/10</span>
        </div>
      </div>
      <div style={{height:3,background:T.border,borderRadius:99,overflow:"hidden"}}>
        <div style={{height:"100%",width:`${score*10}%`,background:col,borderRadius:99,
          boxShadow:`0 0 8px ${col}70`,transition:"width 1.2s cubic-bezier(.4,0,.2,1)"}}/>
      </div>
    </div>
  );
}

/* ─── ANALYST EXPLAINER — collapsible dropdown ──────────────────────── */
function AnalystExplainer(){
  const T = useT();
  const [open, setOpen] = useState(false);
  return(
    <div style={{marginBottom:16}}>
      <button
        onClick={()=>setOpen(p=>!p)}
        style={{
          width:"100%",background:T.bg2,
          border:`1.5px solid ${T.border}`,
          borderRadius:open?"14px 14px 0 0":14,
          padding:"11px 18px",
          display:"flex",alignItems:"center",justifyContent:"space-between",
          cursor:"pointer",transition:"all .2s",
        }}>
        <div style={{display:"flex",alignItems:"center",gap:10}}>
          <div style={{display:"flex",gap:6}}>
            <span style={{fontSize:11,fontWeight:700,color:T.green,background:T.greenDim,padding:"2px 8px",borderRadius:99}}>ATLAS 60%</span>
            <span style={{fontSize:11,fontWeight:700,color:T.purple,background:`${T.purple}14`,padding:"2px 8px",borderRadius:99}}>MERIDIAN 40%</span>
          </div>
          <span style={{fontSize:12,color:T.text2,fontWeight:500}}>How this score was built</span>
        </div>
        <span style={{fontSize:14,color:T.text3,transform:open?"rotate(180deg)":"rotate(0deg)",transition:"transform .2s",display:"inline-block"}}>▾</span>
      </button>
      {open&&(
        <div style={{background:T.bg2,border:`1.5px solid ${T.border}`,borderTop:"none",borderRadius:"0 0 14px 14px",padding:"16px 18px"}}>
          <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10}}>
            {[
              {name:"ATLAS",col:T.green,weight:"60%",icon:"▲",
               role:"Macro Bull Analyst",
               desc:"Macro-first. Weights global tailwinds, commodity cycles, ZAR dynamics. Runs DCF, factor models, Kelly Criterion. Optimistic by design.",
               edge:"Finds underpriced opportunities the market misses."},
              {name:"MERIDIAN",col:T.purple,weight:"40%",icon:"▼",
               role:"Contrarian Risk Analyst",
               desc:"Sceptic by design. Stress-tests every bull assumption. Runs VaR, tail risk scenarios, DCF stress tests. Scores 0.5–1.5pts below ATLAS.",
               edge:"Flags downside risks before they become losses."},
            ].map(a=>(
              <div key={a.name} style={{background:T.bg1,border:`1.5px solid ${a.col}25`,borderRadius:10,padding:"13px 14px"}}>
                <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:8}}>
                  <span style={{fontSize:14,color:a.col}}>{a.icon}</span>
                  <span style={{fontSize:13,fontWeight:800,color:a.col}}>{a.name}</span>
                  <span style={{fontSize:11,color:T.text3,marginLeft:"auto"}}>{a.weight} weight</span>
                </div>
                <div style={{fontSize:12,fontWeight:600,color:T.text,marginBottom:5}}>{a.role}</div>
                <div style={{fontSize:12,color:T.text2,lineHeight:1.55,marginBottom:6}}>{a.desc}</div>
                <div style={{fontSize:11,color:a.col,fontWeight:600,fontStyle:"italic"}}>{a.edge}</div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}


/* ─── ANALYSIS MODAL ────────────────────────────────────────────────── */
function AnalysisModal({result,stock,onClose,onToggleWatchlist,inWatchlist,isPro=true,onUpgrade}){
  const T = useT();
  const [tab,setTab]=useState("verdict");
  const [expanded,setExpanded]=useState({});
  const at=result.atlas,me=result.meridian,sig=getSig(result.consensusSignal);

  const toggle = (k) => setExpanded(p=>({...p,[k]:!p[k]}));

  const TABS=[
    {id:"verdict",l:"VERDICT"},
    {id:"micro",l:"ECONOMICS"},
    {id:"metrics",l:"METRICS"},
    {id:"chart",l:"CHART"},
    {id:"factors",l:"FACTORS"},
    {id:"outlook",l:"OUTLOOK"},
    {id:"peers",l:"PEERS"},
    {id:"catalysts",l:"CATALYSTS"},
  ];

  return(
    <div style={{position:"fixed",inset:0,background:T.overlay,zIndex:900,display:"flex",alignItems:"center",justifyContent:"center",padding:16,backdropFilter:"blur(10px)"}} onClick={onClose}>
      <div onClick={e=>e.stopPropagation()} style={{background:T.bg1,border:`1px solid ${T.border2}`,borderRadius:16,width:"100%",maxWidth:980,maxHeight:"95vh",display:"flex",flexDirection:"column",overflow:"hidden",boxShadow:"0 40px 100px rgba(0,0,0,.95)"}}>

        {/* Header */}
        <div style={{padding:"18px 22px 0",borderBottom:`1px solid ${T.border}`,flexShrink:0}}>
          <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:16}}>
            <div style={{display:"flex",alignItems:"center",gap:14}}>
              <ScoreRing score={result.consensusScore} size={66}/>
              <div>
                <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:4}}>
                  <span style={{fontSize:19,fontWeight:900,color:T.text}}>{result.name}</span>
                  <span style={{fontSize:10,color:T.text3,fontFamily:"monospace",background:T.bg3,border:`1px solid ${T.border}`,borderRadius:4,padding:"2px 6px"}}>{result.ticker}</span>
                  <span style={{background:sig.bg,border:`1px solid ${sig.border}`,color:sig.text,fontSize:9,fontWeight:800,padding:"3px 9px",borderRadius:4}}>{sig.glyph} {result.consensusSignal}</span>
                </div>
                {at?.headline&&<div style={{fontSize:12,color:T.text2,lineHeight:1.5,maxWidth:520,marginBottom:4}}>{at.headline}</div>}
                <div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
                  {[result.sector,"JSE",result.type].map((v,i,arr)=>(
                    <span key={i} style={{fontSize:10,color:T.text3}}>{v}{i<arr.length-1?" ·":""}</span>
                  ))}
                </div>
              </div>
            </div>
            <div style={{display:"flex",alignItems:"center",gap:7,flexShrink:0}}>
              {[["ATLAS",at?.overallScore,T.green,"60%"],["MERIDIAN",me?.overallScore,T.purple,"40%"]].map(([l,s,col,w])=>(
                <div key={l} style={{textAlign:"center",padding:"7px 12px",background:T.bg3,border:`1px solid ${col}20`,borderRadius:8}}>
                  <Tooltip label={`${l} Score`}>
                    <div style={{fontSize:8,color:col,letterSpacing:".1em",marginBottom:2,fontWeight:800}}>{l} <span style={{color:T.text3}}>×{w}</span></div>
                  </Tooltip>
                  <div style={{fontSize:20,fontWeight:900,color:scoreColor(s),fontFamily:"monospace"}}>{s?.toFixed(1)}</div>
                </div>
              ))}
              <button onClick={()=>onToggleWatchlist(result)} style={{padding:"7px 11px",background:inWatchlist?T.greenDim:T.bg3,border:`1px solid ${inWatchlist?T.accent:T.border}`,borderRadius:7,color:inWatchlist?T.accent:T.text2,fontSize:10,cursor:"pointer",fontWeight:700}}>
                {inWatchlist?"✓ WATCHING":"+ WATCH"}
              </button>
              <button onClick={onClose} style={{width:32,height:32,background:T.bg3,border:`1px solid ${T.border}`,borderRadius:7,cursor:"pointer",color:T.text2,fontSize:16,display:"flex",alignItems:"center",justifyContent:"center"}}>×</button>
            </div>
          </div>
          <div style={{display:"flex",overflowX:"auto",gap:4,padding:"0 4px"}}>
            {TABS.map(t=>{
              const locked = !isPro && t.id !== "verdict";
              return (
                <button key={t.id} onClick={()=>locked?(onUpgrade&&onUpgrade()):setTab(t.id)}
                  style={{
                    background:tab===t.id?T.bg3:"transparent",
                    border:"none",
                    borderBottom:tab===t.id?`2px solid ${T.accent}`:"2px solid transparent",
                    padding:"10px 16px",
                    fontSize:13,fontWeight:tab===t.id?700:500,
                    color:locked?T.text3:tab===t.id?T.text:T.text2,
                    cursor:"pointer",letterSpacing:"-.01em",
                    whiteSpace:"nowrap",transition:"all .15s",
                    opacity:locked?.45:1,
                    borderRadius:"8px 8px 0 0",
                  }}>
                  {t.l}{locked&&<span style={{fontSize:9,marginLeft:4,color:T.gold}}>⊕</span>}
                </button>
              );
            })}
          </div>
        </div>

        {/* Body */}
        <div style={{overflowY:"auto",flex:1,padding:22}}>

          {/* VERDICT */}
          {tab==="verdict"&&(
            <div>
              <AnalystExplainer/>
              <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10,marginBottom:14}}>
                <div style={{background:T.bg2,border:`1px solid ${T.border}`,borderLeft:`3px solid ${T.green}`,borderRadius:10,padding:14}}>
                  <div style={{fontSize:9,color:T.green,fontWeight:800,letterSpacing:".1em",marginBottom:6,textTransform:"uppercase"}}>▲ ATLAS — BULL CASE</div>
                  <p style={{margin:0,fontSize:13,color:T.text2,lineHeight:1.6}}>{at?.bullCase}</p>
                </div>
                <div style={{background:T.bg2,border:`1px solid ${T.border}`,borderLeft:`3px solid ${T.purple}`,borderRadius:10,padding:14}}>
                  <div style={{fontSize:9,color:T.purple,fontWeight:800,letterSpacing:".1em",marginBottom:6,textTransform:"uppercase"}}>▼ MERIDIAN — DEEP BEAR</div>
                  <p style={{margin:0,fontSize:13,color:T.text2,lineHeight:1.6}}>{me?.bearCaseDeep||at?.bearCase}</p>
                </div>
              </div>

              {me?.divergence&&(
                <div style={{background:"#FFD60008",border:`1px solid ${T.yellow}30`,borderRadius:10,padding:"11px 14px",marginBottom:12}}>
                  <div style={{fontSize:9,color:T.yellow,fontWeight:800,letterSpacing:".1em",marginBottom:4,textTransform:"uppercase"}}>⚡ WHERE MERIDIAN DISAGREES</div>
                  <p style={{margin:0,fontSize:13,color:T.text2,lineHeight:1.6}}>{me.divergence}</p>
                </div>
              )}

              {/* Key Drivers — collapsible */}
              <div style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:10,overflow:"hidden",marginBottom:12}}>
                <button onClick={()=>toggle("drivers")} style={{width:"100%",background:"none",border:"none",padding:"11px 14px",display:"flex",justifyContent:"space-between",cursor:"pointer"}}>
                  <span style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",textTransform:"uppercase"}}>KEY MACRO DRIVERS</span>
                  <span style={{color:T.text3,fontSize:11}}>{expanded.drivers?"▲":"▼"}</span>
                </button>
                {expanded.drivers&&<div style={{padding:"0 14px 14px",display:"grid",gridTemplateColumns:"1fr 1fr",gap:8}}>
                  {(at?.keyDrivers||[]).map((d,i)=>(
                    <div key={i} style={{display:"flex",gap:8}}>
                      <div style={{width:4,height:4,borderRadius:"50%",background:T.accent,marginTop:8,flexShrink:0}}/>
                      <span style={{fontSize:12,color:T.text2,lineHeight:1.55}}>{d}</span>
                    </div>
                  ))}
                </div>}
              </div>

              {/* Risk Register — collapsible */}
              {at?.riskFactors&&(
                <div style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:10,overflow:"hidden"}}>
                  <button onClick={()=>toggle("risks")} style={{width:"100%",background:"none",border:"none",padding:"11px 14px",display:"flex",justifyContent:"space-between",cursor:"pointer"}}>
                    <span style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",textTransform:"uppercase"}}>RISK REGISTER</span>
                    <span style={{color:T.text3,fontSize:11}}>{expanded.risks?"▲":"▼"}</span>
                  </button>
                  {expanded.risks&&<div style={{padding:"0 14px 14px"}}>
                    {at.riskFactors.map((r,i)=>{
                      const col=r.severity==="HIGH"?T.red:r.severity==="MEDIUM"?T.yellow:T.text3;
                      return(
                        <div key={i} style={{display:"flex",gap:10,background:T.bg3,border:`1px solid ${T.border}`,borderRadius:8,padding:"9px 13px",marginBottom:6}}>
                          <span style={{fontSize:8,fontWeight:800,color:col,background:col+"18",border:`1px solid ${col}40`,borderRadius:3,padding:"2px 6px",whiteSpace:"nowrap",marginTop:2}}>{r.severity}</span>
                          <div><div style={{fontSize:12,fontWeight:600,color:T.text,marginBottom:2}}>{r.risk}</div><div style={{fontSize:11,color:T.text3}}>{r.detail}</div></div>
                        </div>
                      );
                    })}
                    {me?.hiddenRisks&&<div style={{marginTop:10,background:"#FF475708",border:`1px solid ${T.red}20`,borderRadius:8,padding:"10px 12px"}}>
                      <div style={{fontSize:8,color:T.red,fontWeight:800,letterSpacing:".1em",marginBottom:6,textTransform:"uppercase"}}>MERIDIAN HIDDEN RISKS</div>
                      {me.hiddenRisks.map((r,i)=><div key={i} style={{display:"flex",gap:7,marginBottom:5}}><span style={{color:T.red,flexShrink:0,fontSize:11}}>⚠</span><span style={{fontSize:12,color:T.text2}}>{r}</span></div>)}
                    </div>}
                  </div>}
                </div>
              )}

              {!isPro && (
                <div style={{marginTop:14,background:`linear-gradient(135deg, ${T.accent}10, ${T.cyan}08)`,border:`1px solid ${T.accent}35`,borderRadius:10,padding:"14px 16px",display:"flex",justifyContent:"space-between",alignItems:"center",gap:12,flexWrap:"wrap"}}>
                  <div style={{flex:1,minWidth:200}}>
                    <div style={{fontSize:11,fontWeight:800,color:T.accent,letterSpacing:".06em",marginBottom:4}}>UNLOCK FULL ANALYSIS</div>
                    <div style={{fontSize:11,color:T.text2,lineHeight:1.5}}>Get Economics, Metrics, Charts, Factor Scores, Forecasts, Peer Comparison & Catalyst Calendar — plus unlimited analyses on Claude Sonnet 4.</div>
                  </div>
                  <button onClick={()=>onUpgrade&&onUpgrade()} style={{padding:"9px 18px",background:T.accent,color:"#000",border:"none",borderRadius:7,fontSize:10,fontWeight:900,letterSpacing:".07em",cursor:"pointer",fontFamily:"'DM Mono',monospace",whiteSpace:"nowrap"}}>UPGRADE TO PRO →</button>
                </div>
              )}
            </div>
          )}

          {/* MICROECONOMICS */}
          {tab==="micro"&&(
            <div>
              <div style={{marginBottom:14,padding:"10px 14px",background:T.bg3,border:`1px solid ${T.border}`,borderRadius:9}}>
                <div style={{fontSize:9,color:T.text3,letterSpacing:".12em",marginBottom:4,textTransform:"uppercase",fontWeight:800}}>ABOUT THIS TAB</div>
                <p style={{margin:0,fontSize:12,color:T.text2,lineHeight:1.55}}>Microeconomic analysis examines this company's specific competitive environment — supply/demand for its products, pricing power, competitive position, and margin dynamics. This is different from macro analysis which looks at interest rates and GDP.</p>
              </div>
              {at?.microeconomics&&(
                <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10,marginBottom:14}}>
                  {[
                    ["Supply & Demand",at.microeconomics.supplyDemand,T.cyan],
                    ["Pricing Power",at.microeconomics.pricingPower,T.green],
                    ["Competitive Moat",at.microeconomics.competitiveMoat,T.gold],
                    ["Margins Outlook",at.microeconomics.marginsOutlook,T.yellow],
                    ["Demand Drivers",at.microeconomics.demandDrivers,T.accent],
                  ].map(([label,text,col])=>(
                    <div key={label} style={{background:T.bg2,border:`1px solid ${T.border}`,borderLeft:`3px solid ${col}`,borderRadius:9,padding:14}}>
                      <Tooltip label={label}>
                        <div style={{fontSize:9,color:col,fontWeight:800,letterSpacing:".1em",marginBottom:6,textTransform:"uppercase"}}>{label}</div>
                      </Tooltip>
                      <p style={{margin:0,fontSize:13,color:T.text2,lineHeight:1.6}}>{text||"—"}</p>
                    </div>
                  ))}
                </div>
              )}
              {me?.microRisks&&(
                <div>
                  <div style={{fontSize:9,color:T.purple,fontWeight:800,letterSpacing:".1em",marginBottom:8,textTransform:"uppercase"}}>MERIDIAN — MICRO RISK FACTORS</div>
                  {[
                    ["Demand Destruction",me.microRisks.demandDestruction,T.red],
                    ["Substitution Risk",me.microRisks.substitutionRisk,T.yellow],
                    ["Input Cost Risk",me.microRisks.inputCostRisk,"#FF9800"],
                  ].map(([l,v,col])=>v&&(
                    <div key={l} style={{display:"flex",gap:10,background:T.bg2,border:`1px solid ${T.border}`,borderRadius:8,padding:"10px 14px",marginBottom:7}}>
                      <span style={{fontSize:8,color:col,background:col+"15",border:`1px solid ${col}30`,borderRadius:3,padding:"2px 7px",fontWeight:800,whiteSpace:"nowrap",marginTop:2}}>{l}</span>
                      <span style={{fontSize:13,color:T.text2,lineHeight:1.5}}>{v}</span>
                    </div>
                  ))}
                </div>
              )}
            </div>
          )}

          {/* METRICS */}
          {tab==="metrics"&&(
            <div>
              <div style={{marginBottom:14,padding:"10px 14px",background:T.bg3,border:`1px solid ${T.border}`,borderRadius:9}}>
                <p style={{margin:0,fontSize:12,color:T.text2,lineHeight:1.55}}>Company metrics are AI-estimated using the latest available public financial data. For ETFs, some ratios (P/E, ROE) are replaced with AUM and tracking information. Hover metrics for explanations.</p>
              </div>
              {!at?.companyMetrics&&<div style={{padding:"40px 20px",textAlign:"center",color:T.text3,fontSize:12}}>Metrics not available for this analysis. Re-run the analysis to generate metrics.</div>}
              {at?.companyMetrics&&(
                <div>
                  <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:9}}>
                    {[
                      ["P/E Ratio","peRatio"],["P/B Ratio","pbRatio"],["Dividend Yield","dividendYield"],
                      ["Revenue Growth","revenueGrowth"],["EBITDA Margin","ebitdaMargin"],["Net Debt/EBITDA","netDebtToEbitda"],
                      ["ROE","roe"],["Market Cap","marketCap"],["Analyst Consensus","analystConsensus"],
                    ].map(([label,key])=>(
                      <div key={key} style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:9,padding:"12px 14px"}}>
                        <Tooltip label={label}>
                          <div style={{fontSize:9,color:T.text3,textTransform:"uppercase",letterSpacing:".08em",marginBottom:5,borderBottom:`1px solid ${T.border}`,paddingBottom:5}}>{label}</div>
                        </Tooltip>
                        <div style={{fontSize:16,fontWeight:800,color:T.text,fontFamily:"monospace"}}>{at.companyMetrics[key]||"—"}</div>
                      </div>
                    ))}
                  </div>
                  {at.companyMetrics.lastReported&&(
                    <div style={{marginTop:10,fontSize:10,color:T.text3,textAlign:"right"}}>Last reported period: {at.companyMetrics.lastReported}</div>
                  )}
                </div>
              )}
            </div>
          )}

          {/* CHART */}
          {tab==="chart"&&(
            <div>
              <div style={{marginBottom:10,display:"flex",justifyContent:"space-between",alignItems:"center"}}>
                <div>
                  <span style={{fontSize:12,color:T.text2,fontWeight:600}}>{result.name}</span>
                  <span style={{fontSize:11,color:T.text3}}> · JSE · {result.ticker} · Daily · RSI + MACD</span>
                </div>
                <div style={{fontSize:9,color:T.text3}}>Powered by TradingView</div>
              </div>
              <TVChart symbol={stock?.tv||`JSE:${result.ticker}`} ticker={result.ticker}/>
            </div>
          )}

          {/* FACTORS */}
          {tab==="factors"&&(
            <div>
              <AnalystExplainer/>
              <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:24,marginBottom:18}}>
                {[["ATLAS",at,T.green],["MERIDIAN",me,T.purple]].map(([label,d,col])=>(
                  <div key={label}>
                    <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:14}}>
                      <div style={{display:"flex",alignItems:"center",gap:6}}>
                        <div style={{width:7,height:7,borderRadius:"50%",background:col,boxShadow:`0 0 7px ${col}`}}/>
                        <span style={{fontSize:12,fontWeight:800,color:T.text}}>{label}</span>
                      </div>
                      <span style={{fontSize:22,fontWeight:900,color:scoreColor(d?.overallScore),fontFamily:"monospace"}}>{d?.overallScore?.toFixed(1)}</span>
                    </div>
                    <FactorBar label="Macro Momentum"    score={d?.scores?.macroMomentum||5}    weight="30%" color={col}/>
                    <FactorBar label="Currency & Flows"  score={d?.scores?.currencyFlows||5}    weight="20%" color={col}/>
                    <FactorBar label="Sector Rotation"   score={d?.scores?.sectorRotation||5}   weight="20%" color={col}/>
                    <FactorBar label="Geopolitical Risk" score={d?.scores?.geopoliticalRisk||5} weight="15%" color={col}/>
                    <FactorBar label="Market Sentiment"  score={d?.scores?.marketSentiment||5}  weight="15%" color={col}/>
                  </div>
                ))}
              </div>
              <div style={{background:T.bg3,border:`1px solid ${T.border}`,borderRadius:10,padding:14}}>
                <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8}}>
                  <Tooltip label="Consensus Score">
                    <span style={{fontSize:9,color:T.text3,letterSpacing:".1em",textTransform:"uppercase"}}>CONSENSUS SCORE (ATLAS×60% + MERIDIAN×40%)</span>
                  </Tooltip>
                  <span style={{fontSize:24,fontWeight:900,color:scoreColor(result.consensusScore),fontFamily:"monospace"}}>{result.consensusScore?.toFixed(1)}</span>
                </div>
                <div style={{height:4,background:T.border,borderRadius:99,overflow:"hidden"}}>
                  <div style={{height:"100%",width:`${result.consensusScore*10}%`,background:`linear-gradient(90deg,${T.green},${T.purple})`,borderRadius:99,boxShadow:`0 0 12px ${T.green}60`}}/>
                </div>
              </div>
              {me?.probabilityWeightedOutlook&&(
                <div style={{marginTop:12,background:"#D500F908",border:`1px solid ${T.purple}30`,borderRadius:10,padding:"11px 14px"}}>
                  <div style={{fontSize:9,color:T.purple,fontWeight:800,letterSpacing:".1em",marginBottom:5,textTransform:"uppercase"}}>MERIDIAN — PROBABILITY-WEIGHTED OUTLOOK</div>
                  <p style={{margin:0,fontSize:13,color:T.text2,lineHeight:1.6}}>{me.probabilityWeightedOutlook}</p>
                </div>
              )}
            </div>
          )}

          {/* OUTLOOK */}
          {tab==="outlook"&&(
            <div>
              {!at?.predictions&&(
                <div style={{padding:"40px 20px",textAlign:"center",color:T.text3,fontSize:12}}>Outlook data not available. Re-run the analysis to generate forecasts.</div>
              )}
              {at?.predictions&&(
                <div>
                  <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:10,marginBottom:16}}>
                    {[["3 MONTHS","Short-term",at.predictions.threeMonth],["1 YEAR","Medium-term",at.predictions.oneYear],["3 YEARS","Long-term",at.predictions.threeYear]].map(([lbl,sub,d])=>{
                      if(!d) return null;
                      const up=d.direction==="UP",col=up?T.green:d.direction==="DOWN"?T.red:T.yellow;
                      return(
                        <div key={lbl} style={{background:T.bg2,border:`1px solid ${T.border}`,borderLeft:`3px solid ${col}`,borderRadius:10,padding:15}}>
                          <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:1,textTransform:"uppercase"}}>{lbl}</div>
                          <div style={{fontSize:10,color:T.text3,marginBottom:10}}>{sub} · ATLAS forecast</div>
                          <div style={{fontSize:28,fontWeight:900,color:col,lineHeight:1,marginBottom:5,fontFamily:"monospace"}}>{up?"+":(d.direction==="DOWN"?"−":"~")}{d.low}–{d.high}%</div>
                          <div style={{fontSize:8,color:col,background:col+"15",border:`1px solid ${col}30`,borderRadius:3,padding:"2px 6px",display:"inline-block",marginBottom:8,fontWeight:800}}>{d.confidence} CONFIDENCE</div>
                          <p style={{margin:0,fontSize:11,color:T.text3,lineHeight:1.5}}>{d.rationale}</p>
                        </div>
                      );
                    })}
                  </div>
                  {at?.outperformVsAlsi&&(
                    <div style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:10,padding:15,display:"flex",alignItems:"center",gap:20,marginBottom:14}}>
                      <div style={{textAlign:"center",flexShrink:0}}>
                        <div style={{fontSize:38,fontWeight:900,color:at.outperformVsAlsi.verdict==="OUTPERFORM"?T.green:at.outperformVsAlsi.verdict==="UNDERPERFORM"?T.red:T.yellow,fontFamily:"monospace",lineHeight:1}}>{at.outperformVsAlsi.probability}%</div>
                        <div style={{fontSize:9,color:T.text3,marginTop:3,letterSpacing:".08em"}}>PROBABILITY</div>
                      </div>
                      <div>
                        <div style={{fontSize:13,fontWeight:700,color:at.outperformVsAlsi.verdict==="OUTPERFORM"?T.green:at.outperformVsAlsi.verdict==="UNDERPERFORM"?T.red:T.yellow,marginBottom:5}}>{at.outperformVsAlsi.verdict} VS JSE ALL SHARE</div>
                        <p style={{margin:0,fontSize:12,color:T.text2,lineHeight:1.6}}>{at.outperformVsAlsi.rationale}</p>
                      </div>
                    </div>
                  )}
                </div>
              )}
            </div>
          )}

          {/* PEERS */}
          {tab==="peers"&&(
            <div>
              <div style={{fontSize:9,color:T.text3,letterSpacing:".08em",marginBottom:12,textTransform:"uppercase"}}>RANKED BY CONSENSUS SCORE · JSE SECTOR PEERS</div>
              {[{name:result.name,ticker:result.ticker,score:result.consensusScore,verdict:"THIS STOCK",reason:"Current analysis target."},...(at?.peers||[])].sort((a,b)=>(b.score||0)-(a.score||0)).map((p,i)=>{
                const isThis=p.verdict==="THIS STOCK",col=isThis?T.cyan:p.verdict==="STRONGER"?T.red:p.verdict==="WEAKER"?T.green:T.yellow;
                return(
                  <div key={i} style={{display:"flex",alignItems:"center",gap:12,padding:"11px 14px",borderRadius:8,background:isThis?T.bg4:T.bg2,border:`1px solid ${isThis?T.cyan+"40":T.border}`,marginBottom:6}}>
                    <span style={{fontSize:10,color:T.text3,width:18,textAlign:"right",fontFamily:"monospace"}}>#{i+1}</span>
                    <div style={{width:65,flexShrink:0}}><div style={{fontSize:13,fontWeight:800,color:T.text,fontFamily:"monospace"}}>{p.ticker}</div><div style={{fontSize:10,color:T.text3,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{p.name}</div></div>
                    <div style={{flex:1}}>
                      <div style={{height:3,background:T.border,borderRadius:99,overflow:"hidden",marginBottom:4}}><div style={{height:"100%",width:`${(p.score||0)*10}%`,background:scoreColor(p.score||0),borderRadius:99}}/></div>
                      <div style={{fontSize:10,color:T.text3}}>{p.reason}</div>
                    </div>
                    <div style={{fontSize:19,fontWeight:900,color:scoreColor(p.score||0),width:40,textAlign:"right",fontFamily:"monospace"}}>{(p.score||0).toFixed(1)}</div>
                    <span style={{background:col+"15",border:`1px solid ${col}40`,color:col,fontSize:8,fontWeight:800,padding:"3px 7px",borderRadius:3,width:76,textAlign:"center"}}>{isThis?"◉ TARGET":p.verdict}</span>
                  </div>
                );
              })}
            </div>
          )}

          {/* CATALYSTS */}
          {tab==="catalysts"&&(
            <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:16}}>
              <div>
                <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:10,textTransform:"uppercase"}}>UPCOMING CATALYSTS</div>
                {(at?.catalysts||[]).map((c,i)=>{const col=c.impact==="POSITIVE"?T.green:c.impact==="NEGATIVE"?T.red:T.yellow;return(
                  <div key={i} style={{background:T.bg2,border:`1px solid ${T.border}`,borderLeft:`3px solid ${col}`,borderRadius:8,padding:"11px 14px",marginBottom:7}}>
                    <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:3}}><span style={{fontSize:13,fontWeight:600,color:T.text}}>{c.event}</span><span style={{fontSize:8,color:col,background:col+"15",border:`1px solid ${col}30`,borderRadius:3,padding:"2px 6px",fontWeight:800}}>{c.impact}</span></div>
                    <div style={{fontSize:10,color:T.text3}}>{c.timeframe}</div>
                  </div>
                );})}
              </div>
              <div>
                <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:10,textTransform:"uppercase"}}>NEWS FLOW</div>
                {(at?.newsFlow||[]).map((n,i)=>{const col=n.sentiment==="POSITIVE"?T.green:n.sentiment==="NEGATIVE"?T.red:T.yellow;return(
                  <div key={i} style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:8,padding:"11px 14px",marginBottom:7}}>
                    <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:4}}>
                      <div style={{display:"flex",gap:5,alignItems:"center"}}><div style={{width:4,height:4,borderRadius:"50%",background:col}}/><span style={{fontSize:9,color:T.text3}}>{n.source} · {n.date}</span></div>
                      <span style={{fontSize:8,color:col,fontWeight:800}}>{n.sentiment}</span>
                    </div>
                    <div style={{fontSize:12,fontWeight:600,color:T.text,marginBottom:4,lineHeight:1.4}}>{n.headline}</div>
                    <p style={{margin:0,fontSize:11,color:T.text3,lineHeight:1.5}}>{n.summary}</p>
                  </div>
                );})}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ─── STOCK CARD ────────────────────────────────────────────────────── */
function StockCard({stock,result,loading,onAnalyse,onOpen}){
  const T = useT();
  const isLoading=loading===stock.ticker,sig=result?getSig(result.consensusSignal):null;
  const typeCol=stock.type==="ETF"?T.cyan:stock.type==="ETN"?T.purple:T.green;
  const pred=result?.atlas?.predictions?.threeMonth;
  return(
    <div style={{background:T.bg2,border:`1px solid ${result?scoreColor(result.consensusScore)+"35":T.border}`,borderRadius:10,padding:12,cursor:result?"pointer":"default",transition:"all .2s",position:"relative",overflow:"hidden"}}
      onClick={()=>result&&onOpen(result)}
      onMouseEnter={e=>{e.currentTarget.style.transform="translateY(-2px)";e.currentTarget.style.background=T.bg3;e.currentTarget.style.borderColor=result?scoreColor(result.consensusScore)+"55":T.border2;}}
      onMouseLeave={e=>{e.currentTarget.style.transform="";e.currentTarget.style.background=T.bg2;e.currentTarget.style.borderColor=result?scoreColor(result.consensusScore)+"35":T.border;}}>
      {result&&<div style={{position:"absolute",top:0,left:0,right:0,height:2,background:scoreColor(result.consensusScore),boxShadow:`0 0 8px ${scoreColor(result.consensusScore)}70`}}/>}
      <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:5}}>
        <div style={{flex:1,minWidth:0}}>
          <div style={{fontSize:13,fontWeight:900,color:T.text,fontFamily:"monospace"}}>{stock.ticker}</div>
          <div style={{fontSize:10,color:T.text3,marginTop:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{stock.name}</div>
        </div>
        {result&&sig&&<span style={{background:sig.bg,border:`1px solid ${sig.border}`,color:sig.text,fontSize:7,fontWeight:800,padding:"2px 5px",borderRadius:3,flexShrink:0,marginLeft:5}}>{result.consensusSignal}</span>}
      </div>
      <div style={{display:"flex",gap:3,marginBottom:8}}>
        <span style={{background:T.bg1,fontSize:7,color:T.text3,padding:"1px 5px",borderRadius:3}}>{stock.sector}</span>
        <span style={{background:typeCol+"15",fontSize:7,color:typeCol,padding:"1px 5px",borderRadius:3,fontWeight:700}}>{stock.type}</span>
      </div>
      {result?(
        <div style={{display:"flex",alignItems:"flex-end",justifyContent:"space-between"}}>
          <div style={{fontSize:26,fontWeight:900,color:scoreColor(result.consensusScore),lineHeight:1,fontFamily:"monospace"}}>{result.consensusScore?.toFixed(1)}</div>
          {pred&&<div style={{textAlign:"right"}}><div style={{fontSize:7,color:T.text3,letterSpacing:".06em",marginBottom:1}}>3M EST</div><div style={{fontSize:10,fontWeight:800,color:pred.direction==="UP"?T.green:T.red,fontFamily:"monospace"}}>{pred.direction==="UP"?"▲":"▼"} {pred.low}–{pred.high}%</div></div>}
        </div>
      ):(
        <button onClick={e=>{e.stopPropagation();onAnalyse(stock);}} disabled={!!loading}
          style={{width:"100%",padding:"6px",background:isLoading?T.greenDim:T.bg1,border:`1px solid ${isLoading?T.green+"40":T.border}`,borderRadius:5,color:isLoading?T.green:T.text3,fontSize:9,cursor:loading?"wait":"pointer",fontWeight:700,letterSpacing:".06em",textTransform:"uppercase"}}>
          {isLoading?"ANALYSING...":"ANALYSE →"}
        </button>
      )}
    </div>
  );
}

/* ─── SEARCH ────────────────────────────────────────────────────────── */
function SearchBox({onSelect}){
  const T = useT();
  const [q,setQ]=useState(""),[open,setOpen]=useState(false),ref=useRef(null);
  useEffect(()=>{const h=e=>{if(ref.current&&!ref.current.contains(e.target))setOpen(false);};document.addEventListener("mousedown",h);return()=>document.removeEventListener("mousedown",h);},[]);
  const matches=q.trim().length<1?[]:STOCKS.filter(s=>{const t=q.toLowerCase();return s.ticker.toLowerCase().includes(t)||s.name.toLowerCase().includes(t)||s.sector.toLowerCase().includes(t);}).slice(0,8);
  return(
    <div ref={ref} style={{position:"relative",flex:1,maxWidth:300}}>
      <div style={{display:"flex",alignItems:"center",background:T.bg2,border:`1px solid ${open?T.accent+"50":T.border}`,borderRadius:7,padding:"0 10px",gap:6}}>
        <span style={{color:T.text3,fontSize:12}}>⌕</span>
        <input value={q} onChange={e=>setQ(e.target.value)} onFocus={()=>setOpen(true)} placeholder="Search securities..." style={{background:"none",border:"none",outline:"none",color:T.text,fontSize:12,padding:"8px 0",flex:1,minWidth:0,fontFamily:"inherit"}}/>
        {q&&<button onClick={()=>setQ("")} style={{background:"none",border:"none",color:T.text3,cursor:"pointer",padding:0,fontSize:13}}>×</button>}
      </div>
      {open&&matches.length>0&&(
        <div style={{position:"absolute",top:"calc(100% + 4px)",left:0,right:0,background:T.bg2,border:`1px solid ${T.border2}`,borderRadius:10,zIndex:500,overflow:"hidden",boxShadow:"0 16px 48px rgba(0,0,0,.7)"}}>
          {matches.map(s=>{const tc=s.type==="ETF"?T.cyan:s.type==="ETN"?T.purple:T.green;return(
            <div key={s.ticker} onClick={()=>{onSelect(s);setQ("");setOpen(false);}}
              style={{display:"flex",alignItems:"center",gap:10,padding:"9px 12px",cursor:"pointer",borderBottom:`1px solid ${T.border}`}}
              onMouseEnter={e=>e.currentTarget.style.background=T.bg3} onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
              <div style={{width:30,height:30,borderRadius:5,background:T.bg3,display:"flex",alignItems:"center",justifyContent:"center",fontSize:8,fontWeight:800,color:T.text2,flexShrink:0,fontFamily:"monospace"}}>{s.ticker.slice(0,6)}</div>
              <div style={{flex:1,minWidth:0}}><div style={{fontSize:12,fontWeight:600,color:T.text}}>{s.name}</div><div style={{fontSize:9,color:T.text3}}>{s.ticker} · {s.sector}</div></div>
              <span style={{background:tc+"15",color:tc,fontSize:7,fontWeight:800,padding:"2px 5px",borderRadius:3,flexShrink:0}}>{s.type}</span>
            </div>
          );})}
        </div>
      )}
    </div>
  );
}

/* ─── LOADING OVERLAY ───────────────────────────────────────────────── */
function LoadingOverlay({ticker,name}){
  const T = useT();
  const [step, setStep]   = useState(0);
  const [tipIdx, setTip]  = useState(0);

  const steps = [
    {label:"Initialising AI engine",analyst:"system"},
    {label:"ATLAS loading macro context",analyst:"atlas"},
    {label:"ATLAS running DCF valuation",analyst:"atlas"},
    {label:"ATLAS scoring factor exposures",analyst:"atlas"},
    {label:"ATLAS calculating Kelly position size",analyst:"atlas"},
    {label:"MERIDIAN stress-testing assumptions",analyst:"meridian"},
    {label:"MERIDIAN running Value-at-Risk model",analyst:"meridian"},
    {label:"MERIDIAN flagging tail risk scenarios",analyst:"meridian"},
    {label:"Computing consensus score",analyst:"system"},
    {label:"Building your full report",analyst:"system"},
  ];

  const tips = [
    {icon:"📊", heading:"What is ATLAS?", body:"ATLAS is your macro bull analyst — it identifies tailwinds, runs DCF valuations, and scores securities using Fama-French factor models. Weighted 60% in the final score."},
    {icon:"⚖️", heading:"What is MERIDIAN?", body:"MERIDIAN is your contrarian risk analyst — it stress-tests the bull case, runs Value-at-Risk models, and flags tail risk scenarios you might miss. Weighted 40%."},
    {icon:"📐", heading:"DCF Valuation", body:"Discounted Cash Flow analysis estimates a security's intrinsic value by projecting future free cash flows and discounting them at an appropriate WACC. It's the gold standard of fundamental analysis."},
    {icon:"🎯", heading:"Kelly Criterion", body:"The Kelly Criterion is a mathematical formula used by hedge funds to size positions optimally. MacroScore uses it to tell you how much of a portfolio to allocate — not just whether to buy."},
    {icon:"📉", heading:"Value at Risk (VaR)", body:"VaR tells you the maximum expected loss in the worst 5% of market outcomes. Institutional desks use it to understand downside before they buy — now you can too."},
    {icon:"🌍", heading:"Fama-French Factors", body:"The Fama-French 5-factor model decomposes returns into size, value, profitability, investment, and momentum. Understanding your factor exposure is how quant funds think about risk."},
    {icon:"💡", heading:"Why two analysts?", body:"One bullish, one sceptical — the same way a good investment committee works. ATLAS finds the opportunity. MERIDIAN makes sure you're not blind to the risks. You get both, weighted."},
    {icon:"🏦", heading:"Institutional-grade analysis", body:"The same frameworks MacroScore uses — DCF, factor models, scenario analysis — are used by Goldman Sachs, Bridgewater, and every major JSE fund manager. Now available to every retail investor."},
  ];

  useEffect(()=>{
    const t1 = setInterval(()=>setStep(p=>Math.min(p+1, steps.length-1)), 900);
    const t2 = setInterval(()=>setTip(p=>(p+1)%tips.length), 6000);
    return()=>{clearInterval(t1);clearInterval(t2);};
  },[]);

  const current = steps[step];
  const tip = tips[tipIdx];
  const atlasActive = current.analyst==="atlas";
  const meridianActive = current.analyst==="meridian";
  const progress = ((step+1)/steps.length)*100;

  return(
    <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,.5)",zIndex:999,display:"flex",alignItems:"center",justifyContent:"center",backdropFilter:"blur(12px)",padding:20}}>
      <div style={{background:"#FFFFFF",borderRadius:28,padding:"36px 36px 32px",width:"100%",maxWidth:460,textAlign:"center",boxShadow:"0 40px 80px rgba(0,0,0,.18),0 8px 24px rgba(0,0,0,.10)"}}>

        {/* Company identity */}
        <div style={{marginBottom:24}}>
          <CompanyLogo ticker={ticker} size={56}/>
          <div style={{marginTop:14,fontSize:11,color:"#8E8E93",letterSpacing:".08em",fontWeight:600,textTransform:"uppercase",marginBottom:4}}>Analysing</div>
          <div style={{fontSize:28,fontWeight:800,color:"#1C1C1E",letterSpacing:"-.03em",lineHeight:1}}>{ticker}</div>
          <div style={{fontSize:15,color:"#48484A",marginTop:4,fontWeight:500}}>{name}</div>
        </div>

        {/* Analyst status chips */}
        <div style={{display:"flex",gap:8,justifyContent:"center",marginBottom:20}}>
          {[
            {label:"ATLAS", active:atlasActive||step>=5, col:"#34C759"},
            {label:"MERIDIAN", active:meridianActive||step>=8, col:"#5856D6"},
          ].map(({label,active,col})=>(
            <div key={label} style={{
              background:active?col+"14":"#F2F2F7",
              border:`1.5px solid ${active?col+"40":"#E5E5EA"}`,
              borderRadius:99,padding:"6px 16px",
              display:"flex",alignItems:"center",gap:6,
              transition:"all .5s",
            }}>
              <div style={{
                width:6,height:6,borderRadius:"50%",
                background:active?col:"#C7C7CC",
                boxShadow:active?`0 0 8px ${col}`:"none",
                transition:"all .5s",
              }}/>
              <span style={{fontSize:12,fontWeight:700,color:active?col:"#8E8E93",transition:"color .5s"}}>{label}</span>
            </div>
          ))}
        </div>

        {/* Current step */}
        <div style={{background:"#F2F2F7",borderRadius:10,padding:"10px 16px",marginBottom:20,minHeight:36,display:"flex",alignItems:"center",justifyContent:"center"}}>
          <span style={{fontSize:13,color:"#48484A",fontWeight:500}}>{current.label}…</span>
        </div>

        {/* Progress bar */}
        <div style={{height:3,background:"#E5E5EA",borderRadius:99,overflow:"hidden",marginBottom:28}}>
          <div style={{height:"100%",width:`${progress}%`,background:`linear-gradient(90deg,#34C759,#5856D6)`,borderRadius:99,transition:"width .9s cubic-bezier(.4,0,.2,1)"}}/>
        </div>

        {/* Educational tip — the money maker for retention */}
        <div style={{
          background:"#F8F8FA",border:"1.5px solid #E5E5EA",
          borderRadius:16,padding:"18px 20px",
          textAlign:"left",transition:"all .4s",
          minHeight:100,
        }}>
          <div style={{display:"flex",alignItems:"flex-start",gap:12}}>
            <span style={{fontSize:28,lineHeight:1,flexShrink:0}}>{tip.icon}</span>
            <div>
              <div style={{fontSize:13,fontWeight:700,color:"#1C1C1E",marginBottom:5}}>{tip.heading}</div>
              <div style={{fontSize:13,color:"#48484A",lineHeight:1.6,fontWeight:400}}>{tip.body}</div>
            </div>
          </div>
          {/* Dot indicators */}
          <div style={{display:"flex",gap:4,justifyContent:"center",marginTop:14}}>
            {tips.map((_,i)=>(
              <div key={i} style={{width:i===tipIdx?16:5,height:5,borderRadius:99,background:i===tipIdx?"#00A550":"#D1D1D6",transition:"all .4s"}}/>
            ))}
          </div>
        </div>

        <div style={{marginTop:16,fontSize:11,color:"#8E8E93"}}>
          Institutional-grade analysis · Powered by Claude AI
        </div>
      </div>
    </div>
  );
}

/* ─── SVG LOGO ──────────────────────────────────────────────────────── */
function Logo({size=32}){
  const T = useT();
  return(
    <svg width={size} height={size} viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M20 3L35 11.5V28.5L20 37L5 28.5V11.5L20 3Z" stroke={T.accent} strokeWidth="1.5" fill="none" opacity="0.45"/>
      <path d="M20 8L31 14.5V27.5L20 34L9 27.5V14.5L20 8Z" fill={T.accent} opacity="0.07"/>
      <polyline points="10,27 16,19 21,22 30,11" stroke={T.accent} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
      <circle cx="30" cy="11" r="2.5" fill={T.accent} style={{filter:`drop-shadow(0 0 5px ${T.accent})`}}/>
      <circle cx="10" cy="27" r="1.8" fill={T.accent} opacity="0.45"/>
    </svg>
  );
}



/* ─── THEME SYSTEM ──────────────────────────────────────────────────
   Auto-detects device preference on load.
   Manual toggle in nav bar overrides for the session.             */
const ThemeContext = React.createContext({dark:false, T:LIGHT, toggle:()=>{}});

function useTheme() {
  return React.useContext(ThemeContext);
}

function ThemeProvider({children}) {
  const prefersDark = window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
  const [dark, setDark] = useState(prefersDark);

  // Compute the active theme object as state — all components re-render when this changes
  const theme = dark ? DARK : LIGHT;

  // Keep T in sync so non-context components still work
  Object.assign(T, theme);

  // Update body background directly
  useEffect(()=>{
    document.body.style.background = theme.bg0;
    document.body.style.color = theme.text;
  }, [dark]);

  const toggle = () => setDark(p => !p);

  return (
    <ThemeContext.Provider value={{dark, T:theme, toggle}}>
      <div key={dark?"dark":"light"} style={{minHeight:"100vh"}}>
        {children}
      </div>
    </ThemeContext.Provider>
  );
}

/* Sun/Moon toggle button for nav */
function ThemeToggle() {
  const T = useT();
  const {dark, toggle} = useTheme();
  return (
    <button
      onClick={toggle}
      title={dark ? "Switch to light mode" : "Switch to dark mode"}
      style={{
        width:36, height:36,
        borderRadius:99,
        background:T.bg3,
        border:`1.5px solid ${T.border}`,
        cursor:"pointer",
        display:"flex", alignItems:"center", justifyContent:"center",
        fontSize:16,
        transition:"all .2s",
        flexShrink:0,
      }}>
      {dark ? "☀️" : "🌙"}
    </button>
  );
}

/* Hook that forces a re-render when theme changes — use inside any component */
function useT() {
  const {T:theme} = useTheme();
  return theme;
}


/* ─── AUTH SCREENS ───────────────────────────────────────────────────── */
/* Simulates Supabase email/password auth.
   In production: replace signIn/signUp calls with supabase.auth.signInWithPassword()
   and supabase.auth.signUp(). The session JWT then goes to /api/analyse as Bearer token.
   PayFast webhook updates the user's tier in the DB; /api/analyse reads it server-side. */

/* ─── REAL SUPABASE AUTH ─────────────────────────────────────────
   Uses the Supabase client loaded from CDN in index.html.
   `window.supabaseClient` is initialised at app startup.        */

async function authSignUp(email, password) {
  if(!email.includes("@")) throw new Error("Invalid email address.");
  if(password.length < 8) throw new Error("Password must be at least 8 characters.");
  const sb = window.supabaseClient;
  if(!sb) throw new Error("Auth system not ready. Reload the page.");
  const {data, error} = await sb.auth.signUp({email, password});
  if(error) throw new Error(error.message);
  if(!data.session) throw new Error("Account created. Check your email to verify, then sign in.");
  // Profile auto-created by Supabase trigger
  return {token:data.session.access_token, tier:"free", email:data.user.email, userId:data.user.id};
}

async function authSignIn(email, password) {
  const sb = window.supabaseClient;
  if(!sb) throw new Error("Auth system not ready. Reload the page.");

  // Sign in with 10 second timeout
  const signInPromise = sb.auth.signInWithPassword({email, password});
  const signInTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Sign-in timed out. Please try again.")), 10000));
  const {data, error} = await Promise.race([signInPromise, signInTimeout]);
  if(error) throw new Error(error.message);
  if(!data?.session) throw new Error("No session returned. Check email verification.");

  // Fetch tier with short timeout — don't block sign-in if it fails
  let tier = "free";
  try {
    const queryPromise = sb.from("profiles").select("tier").eq("id", data.user.id).single();
    const queryTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3000));
    const {data:profile} = await Promise.race([queryPromise, queryTimeout]);
    if(profile?.tier) tier = profile.tier;
  } catch(e) {
    console.warn("Tier fetch failed on sign-in, defaulting to free:", e.message);
  }
  return {token:data.session.access_token, tier, email:data.user.email, userId:data.user.id};
}

async function authSignOut() {
  const sb = window.supabaseClient;
  if(sb) await sb.auth.signOut();
}

function AuthScreen({onAuth}) {
  const T = useT();
  const [mode, setMode]     = useState("signin"); // signin | signup | forgot | legal
  const [legalTab, setLegalTab] = useState("terms"); // terms | privacy | disclaimer
  const [email, setEmail]   = useState("");
  const [pass, setPass]     = useState("");
  const [pass2, setPass2]   = useState("");
  const [err, setErr]       = useState("");
  const [loading, setLoading] = useState(false);
  const [agreed, setAgreed] = useState(false);
  const [demoKey, setDemoKey] = useState("");

  const submit = async () => {
    setErr(""); setLoading(true);
    try {
      if(mode === "signup" && !agreed) { setErr("You must accept the Terms of Service to continue."); return; }
      if(mode === "signup" && pass !== pass2) { setErr("Passwords do not match."); return; }

      // DEMO KEY path — direct Anthropic key for dev/testing
      if(demoKey.startsWith("sk-ant-")) {
        onAuth({token: demoKey, tier:"pro", email:"demo@macroscore.co.za", isDev:true});
        return;
      }

      const result = mode === "signup"
        ? await authSignUp(email, pass)
        : await authSignIn(email, pass);
      onAuth(result);
    } catch(e) {
      setErr(e.message);
    } finally {
      setLoading(false);
    }
  };

  const LEGAL = {
    terms: {
      title: "Terms of Service",
      updated: "Effective: 10 March 2026",
      body: [
        ["1. Information Service Only", "MacroScore provides AI-generated investment information and market data for educational and informational purposes only. Nothing on this platform constitutes financial advice, investment advice, trading advice, or any other form of advice as defined under the Financial Advisory and Intermediary Services Act 37 of 2002 (FAIS Act). MacroScore is not licensed as a Financial Services Provider (FSP) by the Financial Sector Conduct Authority (FSCA)."],
        ["2. No Reliance", "You acknowledge that all analysis, scores, signals, and forecasts are generated by artificial intelligence models (Claude by Anthropic) and may be inaccurate, incomplete, or outdated. You agree not to make any investment decision based solely or primarily on content provided by MacroScore. Always conduct independent research and consult a licensed financial advisor before investing."],
        ["3. Subscriptions & Billing", "MacroScore offers Free, Pro (R199/month), and Institutional (R1,999/month) subscription tiers. Subscriptions are billed monthly via PayFast in South African Rand. You may cancel at any time; cancellation takes effect at the end of the current billing period with no refund for the remaining period. MacroScore reserves the right to change pricing with 30 days' notice."],
        ["4. Acceptable Use", "You may not resell, redistribute, or commercially exploit analysis outputs without written permission. Automated scraping, API abuse, or using the platform to build competing services is prohibited. MacroScore may terminate accounts that violate these terms without refund."],
        ["5. Intellectual Property", "All AI-generated analysis, scoring methodologies, UI design, and platform code are the intellectual property of MacroScore. TradingView charts are provided under TradingView's terms of service. JSE ticker data is publicly available market information."],
        ["6. Limitation of Liability", "MacroScore's total liability to you for any claim arising from use of the platform is limited to the subscription fees you paid in the 3 months preceding the claim. MacroScore is not liable for any investment losses, opportunity costs, or consequential damages arising from reliance on platform content."],
        ["7. Governing Law", "These terms are governed by the laws of the Republic of South Africa. Any disputes shall be subject to the jurisdiction of the South Gauteng High Court or the Western Cape High Court, at MacroScore's election."],
      ]
    },
    privacy: {
      title: "Privacy Policy",
      updated: "Effective: 10 March 2026 · POPIA Compliant",
      body: [
        ["1. Information We Collect", "We collect your email address and encrypted password for authentication. We collect usage data (analyses run, securities viewed) to enforce tier limits and improve the service. We do not collect payment card details — all billing is handled by PayFast, a PCI-DSS compliant payment processor."],
        ["2. How We Use Your Information", "Your email is used for account authentication and service communications. Usage data is used to enforce subscription limits and improve AI analysis quality. We do not sell your personal information to third parties."],
        ["3. POPIA Compliance", "MacroScore processes your personal information in compliance with the Protection of Personal Information Act 4 of 2013 (POPIA). You have the right to access, correct, or request deletion of your personal information. Submit requests to privacy@macroscore.co.za."],
        ["4. Data Storage", "Account data is stored in Supabase (PostgreSQL) hosted on AWS infrastructure with SOC 2 Type II certification. Analysis queries are sent to Anthropic's API and are subject to Anthropic's data processing terms. We do not store the content of individual analyses."],
        ["5. Cookies", "We use session cookies for authentication only. We do not use tracking cookies or third-party advertising cookies. TradingView charts may set their own cookies subject to TradingView's privacy policy."],
        ["6. Data Retention", "Account data is retained for the duration of your subscription plus 12 months. You may request account deletion at any time by emailing support@macroscore.co.za."],
        ["7. Contact", "Data queries: privacy@macroscore.co.za · Support: support@macroscore.co.za"],
      ]
    },
    disclaimer: {
      title: "Investment Disclaimer",
      updated: "Important — Read Before Using",
      body: [
        ["Not Financial Advice", "MacroScore is an information platform. ATLAS and MERIDIAN are AI personas — they are not human analysts, not licensed financial advisors, and their outputs are not regulated investment advice under the FAIS Act."],
        ["AI Limitations", "Scores, signals, and forecasts are generated by large language models that may hallucinate, use outdated data, or produce inaccurate financial metrics. Company metrics displayed are model-estimated and not sourced from live financial databases. Always verify against audited financial statements."],
        ["Past Performance", "Any forward-looking statements, price targets, or return estimates are hypothetical projections only. Past performance of securities discussed on the platform does not guarantee future results."],
        ["JSE Securities", "MacroScore covers JSE-listed securities and is designed for South African investors. Securities discussed may not be suitable for all investors depending on their financial situation, risk tolerance, and investment objectives."],
        ["Conflicts of Interest", "MacroScore has no commercial relationship with any JSE-listed company discussed on the platform. Analysis scores are generated independently by AI models and are not influenced by any third party."],
      ]
    }
  };

  if(mode === "legal") {
    const legal = LEGAL[legalTab];
    return (
      <div style={{minHeight:"100vh",background:T.bg0,color:T.text,fontFamily:"'DM Sans','Segoe UI',sans-serif",display:"flex",flexDirection:"column"}}>
        <style>{`@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800;900&family=DM+Mono:wght@400;500;700&display=swap');*{box-sizing:border-box;}body{margin:0;}`}</style>
        <div style={{background:T.bg1,borderBottom:`1px solid ${T.border}`,padding:"0 20px",height:48,display:"flex",alignItems:"center",justifyContent:"space-between",flexShrink:0}}>
          <div style={{display:"flex",alignItems:"center",gap:8}}>
            <Logo size={22}/>
            <span style={{fontSize:11,fontWeight:900,color:T.text,fontFamily:"'DM Mono',monospace"}}>MACROSCORE</span>
            <span style={{fontSize:9,color:T.text3}}>/ Legal</span>
          </div>
          <button onClick={()=>setMode(agreed===false?"signup":"signin")} style={{background:"none",border:`1px solid ${T.border}`,borderRadius:5,padding:"4px 12px",fontSize:9,color:T.text3,cursor:"pointer"}}>← Back</button>
        </div>
        <div style={{maxWidth:760,margin:"0 auto",padding:"28px 20px",flex:1,overflowY:"auto"}}>
          <div style={{display:"flex",gap:4,marginBottom:20}}>
            {[["terms","Terms of Service"],["privacy","Privacy Policy"],["disclaimer","Disclaimer"]].map(([id,label])=>(
              <button key={id} onClick={()=>setLegalTab(id)}
                style={{background:legalTab===id?T.accent:T.bg2,color:legalTab===id?"#000":T.text3,border:`1px solid ${legalTab===id?T.accent:T.border}`,borderRadius:5,padding:"5px 13px",fontSize:9,fontWeight:700,cursor:"pointer",letterSpacing:".05em"}}>
                {label}
              </button>
            ))}
          </div>
          <div style={{fontSize:20,fontWeight:900,color:T.text,marginBottom:4}}>{legal.title}</div>
          <div style={{fontSize:10,color:T.text3,marginBottom:22}}>{legal.updated}</div>
          {legal.body.map(([heading, text], i) => (
            <div key={i} style={{marginBottom:20}}>
              <div style={{fontSize:12,fontWeight:800,color:T.accent,marginBottom:6,letterSpacing:".02em"}}>{heading}</div>
              <p style={{margin:0,fontSize:12,color:T.text2,lineHeight:1.75}}>{text}</p>
            </div>
          ))}
        </div>
      </div>
    );
  }

  const isSignUp = mode === "signup";
  const gridBg = `radial-gradient(circle at 50% 0%, ${T.accent}08 0%, transparent 60%)`;

  const TIERS = [
    {name:"FREE",price:"R0",period:"forever",col:T.text3,
     features:["5 analyses / month","VERDICT tab only","Haiku AI model","Watchlist (5 stocks)"]},
    {name:"PRO",price:"R199",period:"/ month",col:T.accent,badge:"MOST POPULAR",
     features:["Unlimited analyses","All 8 analysis tabs","Claude Sonnet 4.6 AI","Unlimited watchlist","ATLAS + MERIDIAN full depth","My Macrofolio","Microeconomics & Metrics"]},
    {name:"INSTITUTIONAL",price:"R1,999",period:"/ month",col:T.cyan,badge:"ENTERPRISE",
     features:["Claude Opus 4.7 (most powerful AI)","Bulk analysis (10 stocks at once)","CSV / Excel export","My Macrofolio","Priority email support","Team seats (coming soon)","PDF reports (coming soon)","Sector heatmap (coming soon)"]},
  ];

  return (
    <div style={{minHeight:"100vh",background:T.bg0,display:"flex",alignItems:"center",justifyContent:"center",fontFamily:"'Plus Jakarta Sans',sans-serif",position:"relative",overflow:"hidden"}}>
      <style>{`@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800;900&family=DM+Mono:wght@400;500;700&display=swap');*{box-sizing:border-box;}body{margin:0;}input::placeholder{color:${T.text3};}button{font-family:inherit;}`}</style>
      <div style={{position:"absolute",inset:0,backgroundImage:gridBg,backgroundSize:"56px 56px",opacity:.18}}/>
      <div style={{position:"absolute",top:"15%",left:"50%",transform:"translateX(-50%)",width:800,height:800,background:`radial-gradient(circle,${T.accent}05 0%,transparent 60%)`,pointerEvents:"none"}}/>

      <div style={{position:"relative",zIndex:1,width:"100%",maxWidth:720,padding:"20px 20px"}}>

        {/* Logo */}
        <div style={{textAlign:"center",marginBottom:28}}>
          <div style={{display:"inline-flex",alignItems:"center",justifyContent:"center",width:64,height:64,background:T.bg2,border:`1px solid ${T.border2}`,borderRadius:18,marginBottom:14,boxShadow:`0 0 40px ${T.accent}12`}}>
            <Logo size={38}/>
          </div>
          <div style={{display:"flex",alignItems:"baseline",justifyContent:"center",marginBottom:6}}>
            <span style={{fontSize:28,fontWeight:900,color:T.text,letterSpacing:"-.03em",fontFamily:"'DM Mono',monospace"}}>MACRO</span>
            <span style={{fontSize:28,fontWeight:900,color:T.accent,letterSpacing:"-.03em",fontFamily:"'DM Mono',monospace"}}>SCORE</span>
          </div>
          <div style={{fontSize:9,color:T.text3,letterSpacing:".2em",textTransform:"uppercase"}}>JSE AI INTELLIGENCE TERMINAL</div>
        </div>

        {/* Pricing row */}
        <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:7,marginBottom:22}}>
          {TIERS.map(tier=>(
            <div key={tier.name} style={{background:tier.badge?`${tier.col}06`:T.bg2,border:`1px solid ${tier.badge?tier.col+"40":T.border}`,borderRadius:10,padding:"13px 13px",position:"relative"}}>
              {tier.badge&&<div style={{position:"absolute",top:9,right:9,background:tier.col,color:"#000",fontSize:7,fontWeight:900,padding:"2px 6px",borderRadius:3,letterSpacing:".08em"}}>{tier.badge}</div>}
              <div style={{fontSize:8,fontWeight:800,color:tier.col,letterSpacing:".12em",marginBottom:5,textTransform:"uppercase"}}>{tier.name}</div>
              <div style={{display:"flex",alignItems:"baseline",gap:3,marginBottom:10}}>
                <span style={{fontSize:20,fontWeight:900,color:T.text,fontFamily:"'DM Mono',monospace"}}>{tier.price}</span>
                <span style={{fontSize:9,color:T.text3}}>{tier.period}</span>
              </div>
              {tier.features.map((f,i)=>(
                <div key={i} style={{display:"flex",gap:5,alignItems:"flex-start",marginBottom:4}}>
                  <span style={{color:tier.col,fontSize:9,marginTop:1,flexShrink:0}}>✓</span>
                  <span style={{fontSize:10,color:T.text2,lineHeight:1.35}}>{f}</span>
                </div>
              ))}
            </div>
          ))}
        </div>

        {/* Auth card */}
        <div style={{background:T.bg1,border:`1.5px solid ${T.border}`,borderRadius:24,padding:"28px 28px",boxShadow:"0 20px 60px rgba(0,0,0,.12),0 4px 16px rgba(0,0,0,.06)"}}>
          <div style={{display:"flex",gap:3,marginBottom:18,background:T.bg1,border:`1px solid ${T.border}`,borderRadius:7,padding:3}}>
            {[["signin","Sign In"],["signup","Create Account"]].map(([m,l])=>(
              <button key={m} onClick={()=>{setMode(m);setErr("");}}
                style={{flex:1,background:mode===m?T.bg3:"transparent",color:mode===m?T.text:T.text3,border:mode===m?`1px solid ${T.border2}`:"1px solid transparent",borderRadius:5,padding:"7px",fontSize:10,fontWeight:mode===m?700:400,cursor:"pointer",transition:"all .15s"}}>
                {l}
              </button>
            ))}
          </div>

          <div style={{display:"flex",flexDirection:"column",gap:10}}>
            <div>
              <div style={{fontSize:10,color:T.text2,marginBottom:5,fontWeight:600}}>Email</div>
              <input value={email} onChange={e=>setEmail(e.target.value)} type="email" placeholder="you@example.com"
                onKeyDown={e=>e.key==="Enter"&&submit()}
                style={{width:"100%",background:T.bg3,border:`1px solid ${T.border}`,borderRadius:7,padding:"10px 12px",fontSize:12,color:T.text,outline:"none",fontFamily:"inherit"}}/>
            </div>
            <div>
              <div style={{fontSize:10,color:T.text2,marginBottom:5,fontWeight:600}}>Password</div>
              <input value={pass} onChange={e=>setPass(e.target.value)} type="password" placeholder={isSignUp?"At least 8 characters":"Your password"}
                onKeyDown={e=>e.key==="Enter"&&submit()}
                style={{width:"100%",background:T.bg3,border:`1px solid ${T.border}`,borderRadius:7,padding:"10px 12px",fontSize:12,color:T.text,outline:"none",fontFamily:"inherit"}}/>
            </div>
            {isSignUp&&(
              <div>
                <div style={{fontSize:10,color:T.text2,marginBottom:5,fontWeight:600}}>Confirm Password</div>
                <input value={pass2} onChange={e=>setPass2(e.target.value)} type="password" placeholder="Repeat password"
                  onKeyDown={e=>e.key==="Enter"&&submit()}
                  style={{width:"100%",background:T.bg3,border:`1px solid ${T.border}`,borderRadius:7,padding:"10px 12px",fontSize:12,color:T.text,outline:"none",fontFamily:"inherit"}}/>
              </div>
            )}

            {isSignUp&&(
              <div style={{display:"flex",alignItems:"flex-start",gap:9,padding:"10px 12px",background:T.bg3,border:`1px solid ${agreed?T.accent+"40":T.border}`,borderRadius:7,cursor:"pointer"}} onClick={()=>setAgreed(!agreed)}>
                <div style={{width:16,height:16,borderRadius:4,border:`2px solid ${agreed?T.accent:T.border2}`,background:agreed?T.accent:"transparent",flexShrink:0,marginTop:1,display:"flex",alignItems:"center",justifyContent:"center",transition:"all .15s"}}>
                  {agreed&&<span style={{fontSize:10,color:"#000",fontWeight:900,lineHeight:1}}>✓</span>}
                </div>
                <span style={{fontSize:11,color:T.text2,lineHeight:1.55}}>
                  I have read and agree to the{" "}
                  <span onClick={e=>{e.stopPropagation();setMode("legal");setLegalTab("terms");}} style={{color:T.accent,cursor:"pointer",textDecoration:"underline"}}>Terms of Service</span>,{" "}
                  <span onClick={e=>{e.stopPropagation();setMode("legal");setLegalTab("privacy");}} style={{color:T.accent,cursor:"pointer",textDecoration:"underline"}}>Privacy Policy</span>, and{" "}
                  <span onClick={e=>{e.stopPropagation();setMode("legal");setLegalTab("disclaimer");}} style={{color:T.accent,cursor:"pointer",textDecoration:"underline"}}>Investment Disclaimer</span>.
                  I understand MacroScore is not a licensed financial advisor and all analysis is AI-generated information only.
                </span>
              </div>
            )}

            {err&&<div style={{background:T.redDim,border:`1px solid ${T.red}30`,borderRadius:6,padding:"8px 12px",fontSize:11,color:T.red,lineHeight:1.5}}>⚠ {err}</div>}

            <button onClick={submit} disabled={loading}
              style={{width:"100%",padding:"11px",background:T.accent,color:"#000",border:"none",borderRadius:7,fontSize:11,fontWeight:900,cursor:"pointer",letterSpacing:".08em",fontFamily:"'DM Mono',monospace",opacity:loading?.7:1,marginTop:2}}>
              {loading?"PROCESSING...":(isSignUp?"CREATE FREE ACCOUNT →":"SIGN IN →")}
            </button>

            {/* Developer demo key option */}
            <details style={{marginTop:4}}>
              <summary style={{fontSize:9,color:T.text3,cursor:"pointer",letterSpacing:".05em",userSelect:"none"}}>Developer / Demo mode</summary>
              <div style={{marginTop:8}}>
                <div style={{fontSize:9,color:T.text3,marginBottom:5}}>Enter Anthropic API key to test without a backend (calls Claude directly).</div>
                <input value={demoKey} onChange={e=>setDemoKey(e.target.value)} type="password" placeholder="sk-ant-api03-..."
                  style={{width:"100%",background:T.bg3,border:`1px solid ${T.border}`,borderRadius:6,padding:"8px 12px",fontSize:11,color:T.text,outline:"none",fontFamily:"'DM Mono',monospace",marginBottom:7}}/>
                <button onClick={submit} disabled={!demoKey.startsWith("sk-ant-")||loading}
                  style={{width:"100%",padding:"8px",background:demoKey.startsWith("sk-ant-")?T.bg4:"#111",border:`1px solid ${demoKey.startsWith("sk-ant-")?T.border2:T.border}`,borderRadius:6,color:T.text3,fontSize:9,cursor:"pointer",fontWeight:700,letterSpacing:".06em"}}>
                  LAUNCH IN DEMO MODE
                </button>
                <div style={{marginTop:6,fontSize:9,color:T.text3,textAlign:"center"}}>
                  <a href="https://console.anthropic.com/settings/keys" target="_blank" rel="noopener noreferrer" style={{color:T.text3}}>Get key at console.anthropic.com →</a>
                </div>
              </div>
            </details>
          </div>
        </div>

        {/* Legal footer */}
        <div style={{marginTop:14,fontSize:9,color:T.text3,textAlign:"center",lineHeight:1.8,opacity:.6}}>
          AI-generated analysis only · Not financial advice · Not affiliated with JSE Limited or Anthropic<br/>
          <span onClick={()=>{setMode("legal");setLegalTab("terms");}} style={{cursor:"pointer",textDecoration:"underline"}}>Terms</span>
          {" · "}
          <span onClick={()=>{setMode("legal");setLegalTab("privacy");}} style={{cursor:"pointer",textDecoration:"underline"}}>Privacy</span>
          {" · "}
          <span onClick={()=>{setMode("legal");setLegalTab("disclaimer");}} style={{cursor:"pointer",textDecoration:"underline"}}>Disclaimer</span>
        </div>
      </div>
    </div>
  );
}



/* ─── PROMO CODE INPUT ─────────────────────────────────────────────
   Production: validate against Supabase `promo_codes` table.
   For now: hard-coded launch promos. Easy to migrate later.       */
const PROMO_CODES = {
  "LAUNCH50":    {percent:50, label:"Launch promo — 50% off first month", maxUses:100},
  "EARLY100":    {percent:50, label:"Early adopter — first 100 only",     maxUses:100},
  "JSE25":       {percent:25, label:"JSE community discount",              maxUses:null},
  "STUDENT30":   {percent:30, label:"Student discount",                     maxUses:null},
  "FRIENDS":     {percent:75, label:"Friends & family",                     maxUses:50},
  "TWITTER20":   {percent:20, label:"Twitter follower discount",            maxUses:null},
  "REDDIT15":    {percent:15, label:"Reddit community discount",            maxUses:null},
  "BETA":        {percent:50, label:"Beta tester discount",                 maxUses:null},
};

function PromoCodeInput({onApplied, currentDiscount}) {
  const T = useT();
  const [code, setCode] = useState("");
  const [err, setErr]   = useState("");
  const [open, setOpen] = useState(false);

  const apply = () => {
    setErr("");
    const cleaned = code.trim().toUpperCase();
    if (!cleaned) { setErr("Enter a promo code"); return; }
    const promo = PROMO_CODES[cleaned];
    if (!promo) { setErr("Invalid or expired promo code"); return; }
    onApplied({code:cleaned, percent:promo.percent, label:promo.label});
    setCode("");
  };

  const remove = () => {
    onApplied(null);
    setCode("");
    setErr("");
  };

  if (currentDiscount) {
    return (
      <div style={{background:`${T.green}10`,border:`1px solid ${T.green}40`,borderRadius:8,padding:"10px 14px",marginBottom:18,display:"flex",alignItems:"center",gap:10,justifyContent:"space-between"}}>
        <div style={{display:"flex",alignItems:"center",gap:8,flex:1,minWidth:0}}>
          <span style={{fontSize:14,color:T.green,flexShrink:0}}>✓</span>
          <div style={{minWidth:0}}>
            <div style={{fontSize:11,fontWeight:800,color:T.green,fontFamily:"'DM Mono',monospace"}}>{currentDiscount.code}</div>
            <div style={{fontSize:10,color:T.text2,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{currentDiscount.label} · {currentDiscount.percent}% off</div>
          </div>
        </div>
        <button onClick={remove} style={{background:"none",border:`1px solid ${T.border}`,color:T.text3,fontSize:10,padding:"4px 9px",borderRadius:5,cursor:"pointer",flexShrink:0}}>REMOVE</button>
      </div>
    );
  }

  if (!open) {
    return (
      <div style={{textAlign:"center",marginBottom:18}}>
        <button onClick={()=>setOpen(true)} style={{background:"none",border:"none",color:T.text3,fontSize:11,cursor:"pointer",textDecoration:"underline",letterSpacing:".03em"}}>
          Have a promo code?
        </button>
      </div>
    );
  }

  return (
    <div style={{marginBottom:18}}>
      <div style={{display:"flex",gap:6}}>
        <input
          value={code}
          onChange={e=>{setCode(e.target.value.toUpperCase()); setErr("");}}
          onKeyDown={e=>e.key==="Enter"&&apply()}
          placeholder="ENTER PROMO CODE"
          style={{flex:1,background:T.bg3,border:`1px solid ${err?T.red+"50":T.border}`,borderRadius:7,padding:"9px 13px",fontSize:12,color:T.text,outline:"none",fontFamily:"'DM Mono',monospace",letterSpacing:".05em",textTransform:"uppercase"}}/>
        <button onClick={apply} style={{padding:"9px 16px",background:T.accent,color:"#000",border:"none",borderRadius:7,fontSize:10,fontWeight:900,cursor:"pointer",letterSpacing:".07em",fontFamily:"'DM Mono',monospace"}}>
          APPLY
        </button>
      </div>
      {err && <div style={{fontSize:10,color:T.red,marginTop:6,paddingLeft:4}}>⚠ {err}</div>}
    </div>
  );
}


/* ─── MACROFOLIO (Paid tier only) ─────────────────────────────────────
   AI-generated dynamic portfolio using MBILL$ fictional currency.
   Users enter starting capital in MBILL$ (not real money) — AI allocates
   across JSE securities. Strong legal disclaimers throughout.       */

const MACROFOLIO_SYS = `You are an expert JSE portfolio architect building a DIVERSIFIED portfolio of JSE-listed securities.

CRITICAL RULES:
1. You are building an EDUCATIONAL model portfolio — NOT actual investment advice.
2. Only recommend from the provided JSE universe.
3. Allocate percentages that sum to EXACTLY 100.
4. Diversify across 5-10 sectors minimum.
5. Include a mix of stocks, ETFs, and some defensive assets.
6. Match portfolio to the user's stated risk profile and horizon.
7. Rationale per holding must be 1-2 sentences, specific and data-driven.

Return ONLY raw JSON. Start { end }. No markdown, no backticks, no preamble. ALL fields MUST be present.

{"strategy":"One sentence on overall portfolio thesis given current macro environment.",
"riskProfile":"CONSERVATIVE|BALANCED|AGGRESSIVE",
"expectedReturn":"e.g. 12-18% annualised",
"timeHorizon":"e.g. 3-5 years",
"holdings":[
  {"ticker":"GFI","name":"Gold Fields","sector":"Gold Mining","weight":12,"role":"Inflation hedge + gold cycle exposure","rationale":"Gold at $5,095 with supply deficits — GFI offers operational leverage to further price moves."},
  {"ticker":"CPI","name":"Capitec Bank","sector":"Financials","weight":10,"role":"Core SA banking exposure","rationale":"Best-in-class SA retail bank with strong loan growth and resilient NIMs."}
],
"macroContext":"Two sentences on how current macro environment (ZAR, commodities, global flows) shapes this allocation.",
"rebalancingTrigger":"One sentence on when this portfolio should be rebalanced (e.g. quarterly, on specific macro trigger).",
"keyRisks":["Risk 1 — specific","Risk 2 — specific","Risk 3 — specific"]}`;

function Macrofolio({session, macro, isPro, isInstitutional, onOpen, onUpgrade}) {
  const T = useT();
  const [capital, setCapital]           = useState(10000);
  const [riskProfile, setRiskProfile]   = useState("BALANCED");
  const [horizon, setHorizon]           = useState("medium");
  const [portfolio, setPortfolio]       = useState(null);
  const [loading, setLoading]           = useState(false);
  const [error, setError]               = useState(null);
  const [lastGenerated, setLastGenerated] = useState(null);
  const [showSetup, setShowSetup]       = useState(true);

  // Load saved portfolio from localStorage on mount (until backend storage added)
  useEffect(()=>{
    try {
      const key = "macrofolio_" + (session?.userId || "demo");
      const saved = localStorage.getItem(key);
      if(saved) {
        const p = JSON.parse(saved);
        setPortfolio(p.portfolio);
        setCapital(p.capital);
        setRiskProfile(p.riskProfile);
        setHorizon(p.horizon);
        setLastGenerated(p.lastGenerated);
        setShowSetup(false);
      }
    } catch(e) {}
  }, [session?.userId]);

  const savePortfolio = (data) => {
    try {
      const key = "macrofolio_" + (session?.userId || "demo");
      localStorage.setItem(key, JSON.stringify(data));
    } catch(e) {}
  };

  const generatePortfolio = async () => {
    if(!isPro) { onUpgrade(); return; }
    setLoading(true); setError(null);

    const horizonMap = {
      short:  "Short-term: 6-12 months",
      medium: "Medium-term: 2-4 years",
      long:   "Long-term: 5+ years",
    };

    // Give AI a compact universe list
    const universe = STOCKS.slice(0, 80).map(s => s.ticker + " (" + s.name + ", " + s.sector + ", " + s.type + ")").join("; ");

    const userMsg = "STARTING CAPITAL: MBILL$" + capital.toLocaleString() + "\n" +
      "RISK PROFILE: " + riskProfile + "\n" +
      "TIME HORIZON: " + horizonMap[horizon] + "\n\n" +
      buildMacroCtx(macro) + "\n\n" +
      "JSE UNIVERSE AVAILABLE:\n" + universe + "\n\n" +
      "Build a diversified portfolio matching the risk profile and horizon. Return ONLY the JSON object. Start with { end with }.";

    try {
      const result = await callClaude(session?.token, MACROFOLIO_SYS, userMsg, 4000, session?.tier || "pro");
      if(!result.holdings || !Array.isArray(result.holdings)) throw new Error("Invalid portfolio structure");

      // Validate weights sum to ~100
      const totalWeight = result.holdings.reduce((s, h) => s + (h.weight || 0), 0);
      if(Math.abs(totalWeight - 100) > 2) {
        // Normalise
        result.holdings.forEach(h => { h.weight = Math.round((h.weight / totalWeight) * 100 * 10) / 10; });
      }

      const timestamp = new Date().toISOString();
      setPortfolio(result);
      setLastGenerated(timestamp);
      setShowSetup(false);
      savePortfolio({portfolio:result, capital, riskProfile, horizon, lastGenerated:timestamp});
    } catch(e) {
      setError(e.message || "Failed to generate portfolio. Try again.");
    } finally {
      setLoading(false);
    }
  };

  const adjustCapital = (delta) => {
    const newCapital = Math.max(1000, capital + delta);
    setCapital(newCapital);
  };

  const resetPortfolio = () => {
    if(!confirm("Clear current Macrofolio and start over?")) return;
    setPortfolio(null);
    setShowSetup(true);
    setLastGenerated(null);
    try { localStorage.removeItem("macrofolio_" + (session?.userId || "demo")); } catch(e) {}
  };

  const timeSince = (ts) => {
    if(!ts) return "never";
    const diff = Date.now() - new Date(ts).getTime();
    const days = Math.floor(diff / (1000*60*60*24));
    if(days === 0) return "today";
    if(days === 1) return "yesterday";
    if(days < 7) return days + " days ago";
    if(days < 30) return Math.floor(days/7) + " weeks ago";
    return Math.floor(days/30) + " months ago";
  };

  // ── GATE for free users ──
  if(!isPro) {
    return (
      <div>
        <div style={{marginBottom:16}}>
          <div style={{fontSize:8,color:T.text3,letterSpacing:".12em",marginBottom:3,textTransform:"uppercase"}}>MY MACROFOLIO</div>
          <div style={{fontSize:16,fontWeight:900,color:T.text}}>AI-Managed Model Portfolio</div>
        </div>
        <div style={{background:"linear-gradient(135deg,"+T.accent+"08,"+T.cyan+"06)",border:"1px solid "+T.accent+"30",borderRadius:12,padding:"36px 28px",textAlign:"center"}}>
          <div style={{fontSize:48,marginBottom:12,opacity:.6}}>◎</div>
          <div style={{fontSize:18,fontWeight:900,color:T.text,marginBottom:8}}>My Macrofolio is a Pro feature</div>
          <div style={{fontSize:12,color:T.text2,maxWidth:440,margin:"0 auto 20px",lineHeight:1.6}}>
            AI builds a diversified, dynamic model portfolio from the full JSE universe — tailored to your risk profile, time horizon, and MBILL$ starting capital. Rebalances as markets move.
          </div>
          <div style={{display:"flex",gap:10,justifyContent:"center",flexWrap:"wrap",maxWidth:520,margin:"0 auto"}}>
            {["Diversified across 5-10 sectors","Weight rationale per holding","Rebalancing triggers","Risk register","Macro-aware allocation"].map(f => (
              <span key={f} style={{fontSize:10,color:T.text2,background:T.bg2,border:"1px solid "+T.border,borderRadius:6,padding:"5px 11px"}}>✓ {f}</span>
            ))}
          </div>
          <button onClick={onUpgrade} style={{marginTop:22,padding:"11px 26px",background:T.accent,color:"#000",border:"none",borderRadius:8,fontSize:11,fontWeight:900,cursor:"pointer",letterSpacing:".08em",fontFamily:"'DM Mono',monospace"}}>
            UPGRADE TO PRO →
          </button>
        </div>
      </div>
    );
  }

  // ── SETUP view (first time or reset) ──
  if(showSetup || !portfolio) {
    return (
      <div>
        <div style={{marginBottom:16}}>
          <div style={{fontSize:8,color:T.text3,letterSpacing:".12em",marginBottom:3,textTransform:"uppercase"}}>MY MACROFOLIO</div>
          <div style={{fontSize:16,fontWeight:900,color:T.text}}>Build Your AI Model Portfolio</div>
        </div>

        {/* Legal banner */}
        <div style={{background:T.redDim,border:"1px solid "+T.red+"30",borderRadius:9,padding:"11px 14px",marginBottom:16,display:"flex",gap:10,alignItems:"flex-start"}}>
          <span style={{color:T.red,fontSize:14,lineHeight:1,flexShrink:0}}>⚠</span>
          <div style={{fontSize:11,color:T.text2,lineHeight:1.55}}>
            <strong style={{color:T.red}}>EDUCATIONAL TOOL ONLY.</strong> My Macrofolio is a hypothetical model portfolio using fictional <strong>MBILL$</strong> currency (not ZAR). This is NOT investment advice, NOT financial planning, and NOT a recommendation to buy any security. MacroScore is not a licensed FSP. Always consult a registered financial advisor before making real investment decisions.
          </div>
        </div>

        <div style={{background:T.bg2,border:"1px solid "+T.border,borderRadius:12,padding:"24px 24px"}}>
          {/* Starting capital */}
          <div style={{marginBottom:22}}>
            <div style={{fontSize:10,color:T.text3,letterSpacing:".1em",marginBottom:8,textTransform:"uppercase"}}>STARTING CAPITAL (MBILL$)</div>
            <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:10}}>
              <button onClick={()=>adjustCapital(-1000)} style={{width:32,height:32,background:T.bg3,border:"1px solid "+T.border2,borderRadius:7,color:T.text2,fontSize:16,cursor:"pointer",fontFamily:"'DM Mono',monospace"}}>−</button>
              <div style={{flex:1,background:T.bg3,border:"1px solid "+T.border,borderRadius:8,padding:"11px 14px",textAlign:"center"}}>
                <span style={{fontSize:9,color:T.text3,marginRight:4}}>MBILL$</span>
                <span style={{fontSize:22,fontWeight:900,color:T.accent,fontFamily:"'DM Mono',monospace"}}>{capital.toLocaleString()}</span>
              </div>
              <button onClick={()=>adjustCapital(1000)} style={{width:32,height:32,background:T.bg3,border:"1px solid "+T.border2,borderRadius:7,color:T.text2,fontSize:16,cursor:"pointer",fontFamily:"'DM Mono',monospace"}}>+</button>
            </div>
            <div style={{display:"flex",gap:6}}>
              {[10000,25000,50000,100000,250000,1000000].map(preset=>(
                <button key={preset} onClick={()=>setCapital(preset)} style={{flex:1,padding:"6px 4px",background:capital===preset?T.greenDim:T.bg3,color:capital===preset?T.accent:T.text3,border:"1px solid "+(capital===preset?T.accent+"40":T.border),borderRadius:5,fontSize:9,cursor:"pointer",fontWeight:700,fontFamily:"'DM Mono',monospace"}}>
                  {preset >= 1000000 ? "1M" : preset/1000+"k"}
                </button>
              ))}
            </div>
          </div>

          {/* Risk profile */}
          <div style={{marginBottom:22}}>
            <div style={{fontSize:10,color:T.text3,letterSpacing:".1em",marginBottom:8,textTransform:"uppercase"}}>RISK PROFILE</div>
            <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:6}}>
              {[
                {id:"CONSERVATIVE",label:"Conservative",sub:"Lower volatility · more bonds/cash/defensive",col:T.cyan},
                {id:"BALANCED",label:"Balanced",sub:"Equal mix · moderate volatility",col:T.accent},
                {id:"AGGRESSIVE",label:"Aggressive",sub:"Higher growth · higher volatility · more equities",col:"#FF9800"},
              ].map(p=>(
                <button key={p.id} onClick={()=>setRiskProfile(p.id)} style={{padding:"10px 11px",background:riskProfile===p.id?p.col+"12":T.bg3,color:riskProfile===p.id?p.col:T.text2,border:"1px solid "+(riskProfile===p.id?p.col+"50":T.border),borderRadius:7,fontSize:10,cursor:"pointer",fontWeight:700,textAlign:"left",letterSpacing:".03em"}}>
                  <div style={{fontWeight:900,marginBottom:3,fontSize:10}}>{p.label}</div>
                  <div style={{fontSize:9,color:riskProfile===p.id?p.col:T.text3,lineHeight:1.3,fontWeight:400}}>{p.sub}</div>
                </button>
              ))}
            </div>
          </div>

          {/* Time horizon */}
          <div style={{marginBottom:24}}>
            <div style={{fontSize:10,color:T.text3,letterSpacing:".1em",marginBottom:8,textTransform:"uppercase"}}>TIME HORIZON</div>
            <div style={{display:"flex",gap:6}}>
              {[
                {id:"short",label:"Short-term",sub:"6-12 months"},
                {id:"medium",label:"Medium-term",sub:"2-4 years"},
                {id:"long",label:"Long-term",sub:"5+ years"},
              ].map(h=>(
                <button key={h.id} onClick={()=>setHorizon(h.id)} style={{flex:1,padding:"10px 10px",background:horizon===h.id?T.greenDim:T.bg3,color:horizon===h.id?T.accent:T.text2,border:"1px solid "+(horizon===h.id?T.accent+"40":T.border),borderRadius:7,fontSize:10,cursor:"pointer",fontWeight:700,textAlign:"center"}}>
                  <div style={{fontWeight:900,fontSize:10}}>{h.label}</div>
                  <div style={{fontSize:9,color:horizon===h.id?T.accent:T.text3,marginTop:2}}>{h.sub}</div>
                </button>
              ))}
            </div>
          </div>

          {/* Error */}
          {error && (
            <div style={{background:T.redDim,border:"1px solid "+T.red+"40",borderRadius:7,padding:"8px 12px",marginBottom:14,fontSize:11,color:T.red}}>⚠ {error}</div>
          )}

          {/* Generate button */}
          <button onClick={generatePortfolio} disabled={loading}
            style={{width:"100%",padding:"13px",background:loading?T.bg3:T.accent,color:loading?T.text3:"#000",border:"none",borderRadius:8,fontSize:11,fontWeight:900,cursor:loading?"wait":"pointer",letterSpacing:".08em",fontFamily:"'DM Mono',monospace"}}>
            {loading ? "ARCHITECT WORKING..." : "GENERATE MACROFOLIO →"}
          </button>
        </div>
      </div>
    );
  }

  // ── PORTFOLIO VIEW ──
  const SECTOR_COL = {
    "Gold Mining":"#FFD600","PGMs":"#E0E0E0","Mining":"#FF9800",
    "Financials":T.cyan,"Insurance":T.cyan,
    "Technology":T.accent,"Telecoms":T.accent,
    "Consumer Staples":"#8BC34A","Retail":"#8BC34A",
    "Healthcare":"#F06292","Property":"#9575CD",
    "Energy":"#FF7043","Industrials":T.text3,
    "Global ETF":T.cyan,"SA Sector ETF":T.accent,
    "Commodity ETF":"#FFD600","Commodity ETN":"#FFD600",
    "SA Property ETF":"#9575CD","SA Bond ETF":"#90A4AE",
    "SA Dividend ETF":T.green,"Money Market ETF":"#90A4AE",
    "Multi-Asset ETF":"#7986CB","Luxury & Global":"#FFAB00",
    "Tourism":"#FF7043","Education":"#7E57C2",
  };

  const sectorBreakdown = {};
  portfolio.holdings.forEach(h => {
    sectorBreakdown[h.sector] = (sectorBreakdown[h.sector] || 0) + h.weight;
  });
  const sortedSectors = Object.entries(sectorBreakdown).sort((a,b)=>b[1]-a[1]);

  return (
    <div>
      {/* Header */}
      <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:16,flexWrap:"wrap",gap:10}}>
        <div>
          <div style={{fontSize:8,color:T.text3,letterSpacing:".12em",marginBottom:3,textTransform:"uppercase"}}>MY MACROFOLIO · Updated {timeSince(lastGenerated)}</div>
          <div style={{fontSize:16,fontWeight:900,color:T.text}}>AI Model Portfolio — MBILL${capital.toLocaleString()}</div>
        </div>
        <div style={{display:"flex",gap:8}}>
          <button onClick={()=>setShowSetup(true)} style={{padding:"7px 14px",background:T.bg2,border:"1px solid "+T.border2,borderRadius:6,color:T.text2,fontSize:9,cursor:"pointer",fontWeight:700,letterSpacing:".06em"}}>
            ADJUST CAPITAL
          </button>
          <button onClick={generatePortfolio} disabled={loading} style={{padding:"7px 14px",background:T.bg2,border:"1px solid "+T.border2,borderRadius:6,color:T.text2,fontSize:9,cursor:"pointer",fontWeight:700,letterSpacing:".06em"}}>
            {loading ? "..." : "↻ REBALANCE"}
          </button>
          <button onClick={resetPortfolio} style={{padding:"7px 14px",background:T.bg2,border:"1px solid "+T.border2,borderRadius:6,color:T.text3,fontSize:9,cursor:"pointer",fontWeight:700,letterSpacing:".06em"}}>
            RESET
          </button>
        </div>
      </div>

      {/* Compact legal strip */}
      <div style={{background:T.redDim,border:"1px solid "+T.red+"30",borderRadius:7,padding:"7px 12px",marginBottom:14,fontSize:10,color:T.text2,display:"flex",gap:8,alignItems:"center"}}>
        <span style={{color:T.red,flexShrink:0}}>⚠</span>
        <span>Educational model portfolio using fictional <strong>MBILL$</strong>. Not investment advice. Not a recommendation to buy. Always consult a licensed financial advisor.</span>
      </div>

      {/* Strategy header */}
      <div style={{background:T.bg2,border:"1px solid "+T.border,borderLeft:"3px solid "+T.accent,borderRadius:8,padding:"12px 16px",marginBottom:14}}>
        <div style={{display:"flex",gap:8,marginBottom:6,flexWrap:"wrap"}}>
          <span style={{background:T.greenDim,color:T.accent,border:"1px solid "+T.accent+"30",fontSize:8,fontWeight:800,padding:"2px 8px",borderRadius:3,letterSpacing:".08em"}}>{portfolio.riskProfile}</span>
          <span style={{background:T.bg3,color:T.text2,border:"1px solid "+T.border,fontSize:8,fontWeight:800,padding:"2px 8px",borderRadius:3,letterSpacing:".05em"}}>{portfolio.timeHorizon}</span>
          <span style={{background:T.bg3,color:T.cyan,border:"1px solid "+T.cyan+"30",fontSize:8,fontWeight:800,padding:"2px 8px",borderRadius:3,letterSpacing:".05em"}}>TARGET {portfolio.expectedReturn}</span>
        </div>
        <div style={{fontSize:12,color:T.text,lineHeight:1.55,fontStyle:"italic"}}>"{portfolio.strategy}"</div>
      </div>

      <div style={{display:"grid",gridTemplateColumns:"2fr 1fr",gap:14}}>
        {/* Holdings */}
        <div>
          <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:9,textTransform:"uppercase"}}>
            HOLDINGS · {portfolio.holdings.length} positions
          </div>
          {portfolio.holdings.map((h,i)=>{
            const stockInfo = STOCKS.find(s => s.ticker === h.ticker);
            const col = SECTOR_COL[h.sector] || T.text2;
            const mbillAmount = Math.round((h.weight/100) * capital);
            return (
              <div key={i}
                onClick={()=>stockInfo && onOpen && onOpen(stockInfo)}
                style={{display:"flex",alignItems:"center",gap:12,padding:"11px 14px",marginBottom:6,background:T.bg2,border:"1px solid "+T.border,borderLeft:"3px solid "+col,borderRadius:8,cursor:stockInfo?"pointer":"default",transition:"all .15s"}}>
                {/* Weight pill */}
                <div style={{width:48,height:48,flexShrink:0,borderRadius:8,background:col+"14",border:"1px solid "+col+"35",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"}}>
                  <span style={{fontSize:14,fontWeight:900,color:col,fontFamily:"'DM Mono',monospace",lineHeight:1}}>{h.weight}</span>
                  <span style={{fontSize:8,color:col,opacity:.7,marginTop:1}}>%</span>
                </div>
                {/* Info */}
                <div style={{flex:1,minWidth:0}}>
                  <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:2,flexWrap:"wrap"}}>
                    <span style={{fontSize:12,fontWeight:900,color:T.text,fontFamily:"monospace"}}>{h.ticker}</span>
                    <span style={{fontSize:10,color:T.text2,overflow:"hidden",textOverflow:"ellipsis"}}>{h.name}</span>
                    <span style={{background:col+"14",color:col,border:"1px solid "+col+"30",fontSize:7,fontWeight:800,padding:"1px 6px",borderRadius:3,letterSpacing:".05em"}}>{h.sector.toUpperCase()}</span>
                  </div>
                  <div style={{fontSize:10,color:T.accent,fontWeight:700,marginBottom:3,letterSpacing:".02em"}}>{h.role}</div>
                  <div style={{fontSize:10,color:T.text3,lineHeight:1.45}}>{h.rationale}</div>
                </div>
                {/* MBILL$ */}
                <div style={{textAlign:"right",flexShrink:0}}>
                  <div style={{fontSize:8,color:T.text3,letterSpacing:".08em"}}>MBILL$</div>
                  <div style={{fontSize:13,fontWeight:900,color:T.text,fontFamily:"'DM Mono',monospace"}}>{mbillAmount.toLocaleString()}</div>
                </div>
              </div>
            );
          })}
        </div>

        {/* Right sidebar */}
        <div>
          {/* Sector allocation */}
          <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:9,textTransform:"uppercase"}}>SECTOR MIX</div>
          <div style={{background:T.bg2,border:"1px solid "+T.border,borderRadius:8,padding:"10px 12px",marginBottom:12}}>
            {sortedSectors.map(([sec, w])=>(
              <div key={sec} style={{marginBottom:7}}>
                <div style={{display:"flex",justifyContent:"space-between",fontSize:9,marginBottom:2}}>
                  <span style={{color:T.text2}}>{sec}</span>
                  <span style={{color:SECTOR_COL[sec]||T.text2,fontFamily:"'DM Mono',monospace",fontWeight:700}}>{w.toFixed(1)}%</span>
                </div>
                <div style={{height:3,background:T.bg3,borderRadius:99,overflow:"hidden"}}>
                  <div style={{width:w+"%",height:"100%",background:SECTOR_COL[sec]||T.text2,boxShadow:"0 0 4px "+(SECTOR_COL[sec]||T.text2)+"60"}}/>
                </div>
              </div>
            ))}
          </div>

          {/* Macro context */}
          <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:9,textTransform:"uppercase"}}>MACRO THESIS</div>
          <div style={{background:T.bg2,border:"1px solid "+T.border,borderRadius:8,padding:"10px 12px",marginBottom:12,fontSize:10,color:T.text2,lineHeight:1.55}}>
            {portfolio.macroContext}
          </div>

          {/* Rebalancing */}
          <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:9,textTransform:"uppercase"}}>REBALANCING</div>
          <div style={{background:T.bg2,border:"1px solid "+T.border,borderRadius:8,padding:"10px 12px",marginBottom:12,fontSize:10,color:T.text2,lineHeight:1.55}}>
            {portfolio.rebalancingTrigger}
          </div>

          {/* Risks */}
          <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:9,textTransform:"uppercase"}}>KEY RISKS</div>
          <div style={{background:T.bg2,border:"1px solid "+T.border,borderRadius:8,padding:"10px 12px"}}>
            {(portfolio.keyRisks||[]).map((r,i)=>(
              <div key={i} style={{display:"flex",gap:6,marginBottom:5,fontSize:10,color:T.text2,lineHeight:1.5}}>
                <span style={{color:T.red,flexShrink:0}}>▸</span>
                <span>{r}</span>
              </div>
            ))}
          </div>
        </div>
      </div>

      {error && (
        <div style={{background:T.redDim,border:"1px solid "+T.red+"40",borderRadius:7,padding:"8px 12px",marginTop:14,fontSize:11,color:T.red}}>⚠ {error}</div>
      )}
    </div>
  );
}

/* ─── BULK ANALYSIS (Institutional only) ────────────────────────────
   Runs up to 10 stocks in parallel using Promise.allSettled so one
   failure doesn't block the others. Shows a live progress grid.    */
function BulkAnalysis({session, macro, results, setResults, onOpen, onUpgrade}) {
  const T = useT();
  const [selected, setSelected]   = useState([]);
  const [running, setRunning]     = useState(false);
  const [progress, setProgress]   = useState({}); // ticker -> 'pending'|'running'|'done'|'error'
  const [search, setSearch]       = useState("");
  const isInstitutional           = session?.tier === "institutional";

  const filtered = search.trim().length > 1
    ? STOCKS.filter(s => s.ticker.toLowerCase().includes(search.toLowerCase()) || s.name.toLowerCase().includes(search.toLowerCase())).slice(0, 20)
    : [];

  const toggle = (ticker) => {
    if(!isInstitutional) { onUpgrade(); return; }
    setSelected(p => p.includes(ticker) ? p.filter(x => x !== ticker) : p.length >= 10 ? p : [...p, ticker]);
  };

  const runBulk = async () => {
    if(!isInstitutional) { onUpgrade(); return; }
    if(selected.length === 0) return;
    setRunning(true);
    const init = {};
    selected.forEach(t => init[t] = "pending");
    setProgress(init);

    // Run all in parallel — Promise.allSettled so failures don't block
    await Promise.allSettled(selected.map(async ticker => {
      const stock = STOCKS.find(s => s.ticker === ticker);
      if(!stock) return;
      if(results[ticker]) { setProgress(p => ({...p, [ticker]:"done"})); return; }
      setProgress(p => ({...p, [ticker]:"running"}));
      try {
        const macroCtx = buildMacroCtx(macro);
        const userMsg  = `SECURITY: ${ticker} — ${stock.name}
SECTOR: ${stock.sector}
TYPE: ${stock.type}

${macroCtx}

Return ONLY the JSON object. Start with { end with }. No markdown, no backticks, no preamble.`;
        const token    = session?.token;
        const tier     = session?.tier || "free";
        const [atlas, meridian] = await Promise.all([
          callClaude(token, ATLAS_SYS, userMsg, 5000, tier),
          callClaude(token, MERIDIAN_SYS, userMsg, 2000, tier),
        ]);
        const cs  = parseFloat((atlas.overallScore * .6 + meridian.overallScore * .4).toFixed(1));
        const sm  = {"STRONG BUY":5,"BUY":4,"HOLD":3,"SELL":2,"STRONG SELL":1};
        const avg = (sm[atlas.signal]||3)*.6 + (sm[meridian.signal]||3)*.4;
        const sig = avg>=4.5?"STRONG BUY":avg>=3.5?"BUY":avg>=2.5?"HOLD":avg>=1.5?"SELL":"STRONG SELL";
        setResults(p => ({...p, [ticker]: {ticker, name:stock.name, sector:stock.sector, type:stock.type, consensusScore:cs, consensusSignal:sig, atlas, meridian}}));
        setProgress(p => ({...p, [ticker]:"done"}));
      } catch(e) {
        setProgress(p => ({...p, [ticker]:"error"}));
      }
    }));
    setRunning(false);
  };

  const exportCSV = () => {
    const rows = [["Ticker","Name","Sector","Type","Score","Signal","ATLAS","MERIDIAN","3M Low%","3M High%","1Y Low%","1Y High%"]];
    selected.filter(t => results[t]).forEach(t => {
      const r = results[t];
      const p3 = r.atlas?.predictions?.threeMonth;
      const p1 = r.atlas?.predictions?.oneYear;
      rows.push([t, r.name, r.sector, r.type, r.consensusScore, r.consensusSignal,
        r.atlas?.overallScore, r.meridian?.overallScore,
        p3?.low||"", p3?.high||"", p1?.low||"", p1?.high||""]);
    });
    const csv = rows.map(r => r.join(",")).join("\n");
    const a = document.createElement("a");
    a.href = "data:text/csv;charset=utf-8," + encodeURIComponent(csv);
    a.download = `macroscore-bulk-${new Date().toISOString().slice(0,10)}.csv`;
    a.click();
  };

  const doneCount  = Object.values(progress).filter(v => v === "done").length;
  const errorCount = Object.values(progress).filter(v => v === "error").length;

  const statusCol = s => s==="done"?T.green:s==="running"?T.cyan:s==="error"?T.red:T.text3;
  const statusGlyph = s => s==="done"?"✓":s==="running"?"◌":s==="error"?"✗":"·";

  return (
    <div>
      {/* Header */}
      <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:16,flexWrap:"wrap",gap:10}}>
        <div>
          <div style={{fontSize:8,color:T.text3,letterSpacing:".12em",marginBottom:3,textTransform:"uppercase"}}>INSTITUTIONAL · BULK ANALYSIS</div>
          <div style={{fontSize:16,fontWeight:900,color:T.text}}>Run up to 10 stocks simultaneously</div>
        </div>
        <div style={{display:"flex",gap:8}}>
          {selected.length > 0 && results[selected.find(t => results[t])] && (
            <button onClick={exportCSV}
              style={{padding:"7px 14px",background:T.bg2,border:`1px solid ${T.border2}`,borderRadius:6,color:T.text2,fontSize:9,cursor:"pointer",fontWeight:700,letterSpacing:".06em"}}>
              ↓ EXPORT CSV
            </button>
          )}
          <button onClick={runBulk} disabled={running || selected.length === 0}
            style={{padding:"7px 18px",background:selected.length>0&&!running?T.accent:"#111D2A",color:selected.length>0&&!running?"#000":T.text3,border:"none",borderRadius:6,fontSize:10,cursor:selected.length>0&&!running?"pointer":"not-allowed",fontWeight:900,letterSpacing:".07em",fontFamily:"'DM Mono',monospace",transition:"all .2s"}}>
            {running?`ANALYSING ${doneCount}/${selected.length}...`:`ANALYSE ${selected.length} STOCK${selected.length!==1?"S":""} →`}
          </button>
        </div>
      </div>

      {/* Institutional gate */}
      {!isInstitutional && (
        <div style={{background:"#00E5FF08",border:`1px solid ${T.cyan}30`,borderRadius:10,padding:"14px 18px",marginBottom:16,display:"flex",alignItems:"center",justifyContent:"space-between",gap:12}}>
          <div>
            <div style={{fontSize:12,fontWeight:700,color:T.cyan,marginBottom:3}}>Institutional Feature</div>
            <div style={{fontSize:11,color:T.text2}}>Bulk analysis is available on the Institutional plan (R1,999/month). Analyse up to 10 stocks simultaneously and export results to CSV.</div>
          </div>
          <button onClick={onUpgrade} style={{padding:"8px 16px",background:T.cyan,color:"#000",border:"none",borderRadius:6,fontSize:9,cursor:"pointer",fontWeight:900,letterSpacing:".07em",whiteSpace:"nowrap"}}>UPGRADE →</button>
        </div>
      )}

      <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:14}}>
        {/* Left — stock selector */}
        <div>
          <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:8,textTransform:"uppercase"}}>
            SELECT STOCKS ({selected.length}/10)
          </div>

          {/* Selected chips */}
          {selected.length > 0 && (
            <div style={{display:"flex",flexWrap:"wrap",gap:5,marginBottom:10}}>
              {selected.map(t => {
                const st = progress[t];
                const col = statusCol(st||"");
                return (
                  <div key={t} style={{display:"flex",alignItems:"center",gap:5,background:T.bg3,border:`1px solid ${col||T.border}40`,borderRadius:5,padding:"3px 8px"}}>
                    <span style={{fontSize:9,fontWeight:800,color:col||T.text2,fontFamily:"monospace"}}>{statusGlyph(st||"")} {t}</span>
                    {!running && <span onClick={()=>toggle(t)} style={{cursor:"pointer",color:T.text3,fontSize:11,lineHeight:1}}>×</span>}
                  </div>
                );
              })}
            </div>
          )}

          {/* Search */}
          <div style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:7,padding:"8px 12px",display:"flex",gap:8,alignItems:"center",marginBottom:8}}>
            <span style={{color:T.text3,fontSize:12}}>⌕</span>
            <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Search to add stocks..."
              style={{background:"none",border:"none",outline:"none",color:T.text,fontSize:12,flex:1,fontFamily:"inherit"}}/>
          </div>

          {/* Results */}
          {filtered.map(s => {
            const sel = selected.includes(s.ticker);
            const full = selected.length >= 10 && !sel;
            const tc = s.type==="ETF"?T.cyan:s.type==="ETN"?T.purple:T.green;
            return (
              <div key={s.ticker} onClick={()=>!full&&toggle(s.ticker)}
                style={{display:"flex",alignItems:"center",gap:10,padding:"8px 10px",borderRadius:6,background:sel?T.bg4:T.bg2,border:`1px solid ${sel?T.accent+"40":T.border}`,marginBottom:4,cursor:full?"not-allowed":"pointer",opacity:full?.4:1,transition:"all .15s"}}>
                <div style={{width:20,height:20,borderRadius:4,background:sel?T.greenDim:T.bg3,border:`1px solid ${sel?T.accent:T.border}`,display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0}}>
                  {sel&&<span style={{fontSize:9,color:T.accent,fontWeight:900}}>✓</span>}
                </div>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:11,fontWeight:700,color:T.text,fontFamily:"monospace"}}>{s.ticker}</div>
                  <div style={{fontSize:9,color:T.text3,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{s.name}</div>
                </div>
                <span style={{background:tc+"15",color:tc,fontSize:7,fontWeight:800,padding:"1px 5px",borderRadius:3,flexShrink:0}}>{s.type}</span>
                {results[s.ticker] && <span style={{fontSize:11,fontWeight:900,color:scoreColor(results[s.ticker].consensusScore),fontFamily:"monospace"}}>{results[s.ticker].consensusScore.toFixed(1)}</span>}
              </div>
            );
          })}
          {search.trim().length < 2 && <div style={{fontSize:10,color:T.text3,textAlign:"center",padding:"20px 0"}}>Type to search 160+ JSE securities</div>}
        </div>

        {/* Right — results grid */}
        <div>
          <div style={{fontSize:9,color:T.text3,fontWeight:800,letterSpacing:".1em",marginBottom:8,textTransform:"uppercase"}}>
            RESULTS {doneCount > 0 && `· ${doneCount} COMPLETE${errorCount>0?` · ${errorCount} FAILED`:""}`}
          </div>

          {selected.length === 0 ? (
            <div style={{height:200,display:"flex",alignItems:"center",justifyContent:"center",background:T.bg2,border:`1px solid ${T.border}`,borderRadius:10}}>
              <div style={{textAlign:"center"}}>
                <div style={{fontSize:24,opacity:.1,marginBottom:8}}>⊞</div>
                <div style={{fontSize:11,color:T.text3}}>Select stocks on the left to begin</div>
              </div>
            </div>
          ) : (
            <div style={{display:"flex",flexDirection:"column",gap:6}}>
              {selected.map(ticker => {
                const r = results[ticker];
                const st = progress[ticker];
                const sig = r ? getSig(r.consensusSignal) : null;
                const pred = r?.atlas?.predictions?.threeMonth;
                return (
                  <div key={ticker}
                    onClick={()=>r&&onOpen(r)}
                    style={{display:"flex",alignItems:"center",gap:10,padding:"10px 13px",borderRadius:8,background:T.bg2,border:`1px solid ${r?scoreColor(r.consensusScore)+"35":T.border}`,cursor:r?"pointer":"default",transition:"all .15s",position:"relative",overflow:"hidden"}}>
                    {r&&<div style={{position:"absolute",top:0,left:0,right:0,height:2,background:scoreColor(r.consensusScore),boxShadow:`0 0 6px ${scoreColor(r.consensusScore)}60`}}/>}

                    {/* Status / Score */}
                    <div style={{width:44,height:44,borderRadius:8,background:r?scoreDim(r.consensusScore):T.bg3,border:`1px solid ${r?scoreColor(r.consensusScore)+"40":T.border}`,display:"flex",alignItems:"center",justifyContent:"center",flexShrink:0}}>
                      {st==="running" ? (
                        <div style={{width:16,height:16,border:`2px solid ${T.border2}`,borderTop:`2px solid ${T.cyan}`,borderRadius:"50%",animation:"spin 1s linear infinite"}}/>
                      ) : r ? (
                        <span style={{fontSize:14,fontWeight:900,color:scoreColor(r.consensusScore),fontFamily:"monospace"}}>{r.consensusScore.toFixed(1)}</span>
                      ) : st==="error" ? (
                        <span style={{fontSize:14,color:T.red}}>✗</span>
                      ) : (
                        <span style={{fontSize:11,color:T.text3,fontFamily:"monospace"}}>{ticker.slice(0,4)}</span>
                      )}
                    </div>

                    <div style={{flex:1,minWidth:0}}>
                      <div style={{fontSize:12,fontWeight:700,color:T.text,fontFamily:"monospace"}}>{ticker}</div>
                      <div style={{fontSize:9,color:T.text3,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{STOCKS.find(s=>s.ticker===ticker)?.name}</div>
                    </div>

                    {sig&&<span style={{background:sig.bg,border:`1px solid ${sig.border}`,color:sig.text,fontSize:7,fontWeight:800,padding:"2px 6px",borderRadius:3,flexShrink:0}}>{r?.consensusSignal}</span>}
                    {pred&&<span style={{fontSize:10,fontWeight:700,color:pred.direction==="UP"?T.green:T.red,fontFamily:"monospace",flexShrink:0}}>{pred.direction==="UP"?"▲":"▼"}{pred.low}–{pred.high}%</span>}
                    {st==="error"&&<span style={{fontSize:9,color:T.red}}>Failed — retry</span>}
                  </div>
                );
              })}
            </div>
          )}

          {/* Summary stats when done */}
          {doneCount > 0 && (
            <div style={{marginTop:10,display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:7}}>
              {[
                ["AVG SCORE", (selected.filter(t=>results[t]).reduce((s,t)=>s+(results[t]?.consensusScore||0),0)/Math.max(1,doneCount)).toFixed(1), T.text],
                ["BUY SIGNALS", selected.filter(t=>["BUY","STRONG BUY"].includes(results[t]?.consensusSignal)).length, T.green],
                ["SELLS", selected.filter(t=>["SELL","STRONG SELL"].includes(results[t]?.consensusSignal)).length, T.red],
              ].map(([l,v,col])=>(
                <div key={l} style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:7,padding:"8px 10px",textAlign:"center"}}>
                  <div style={{fontSize:7,color:T.text3,letterSpacing:".08em",marginBottom:3,textTransform:"uppercase"}}>{l}</div>
                  <div style={{fontSize:18,fontWeight:900,color:col,fontFamily:"monospace"}}>{v}</div>
                </div>
              ))}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

/* ─── WATCHLIST ─────────────────────────────────────────────────────── */
function WatchlistView({watchlist,results,onRemove,onOpen}){
  const T = useT();
  if(watchlist.length===0) return(
    <div style={{textAlign:"center",padding:"70px 20px"}}>
      <div style={{fontSize:28,marginBottom:10,opacity:.15}}>◎</div>
      <div style={{fontSize:13,fontWeight:600,color:T.text2,marginBottom:5}}>Watchlist is empty</div>
      <div style={{fontSize:11,color:T.text3}}>Analyse a security and click "+ WATCH" to track it here.</div>
    </div>
  );
  const analysed=watchlist.filter(t=>results[t]);
  const avg=analysed.length>0?analysed.reduce((s,t)=>s+(results[t]?.consensusScore||0),0)/analysed.length:0;
  const buys=analysed.filter(t=>["BUY","STRONG BUY"].includes(results[t]?.consensusSignal)).length;
  return(
    <div>
      <div style={{display:"grid",gridTemplateColumns:"repeat(4,1fr)",gap:9,marginBottom:16}}>
        {[["POSITIONS",watchlist.length,T.text],["ANALYSED",analysed.length,T.text],["AVG SCORE",avg>0?avg.toFixed(1):"—",scoreColor(avg)],["BUY SIGNALS",`${buys}/${analysed.length}`,T.green]].map(([l,v,col])=>(
          <div key={l} style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:9,padding:"11px 14px"}}>
            <div style={{fontSize:8,color:T.text3,letterSpacing:".1em",fontWeight:800,marginBottom:5,textTransform:"uppercase"}}>{l}</div>
            <div style={{fontSize:22,fontWeight:900,color:col,fontFamily:"monospace"}}>{v}</div>
          </div>
        ))}
      </div>
      <div style={{background:T.bg2,border:`1px solid ${T.border}`,borderRadius:12,overflow:"hidden"}}>
        {watchlist.map((ticker,i)=>{
          const r=results[ticker],stock=STOCKS.find(s=>s.ticker===ticker),sig=r?getSig(r.consensusSignal):null,pred=r?.atlas?.predictions?.threeMonth;
          return(
            <div key={ticker} style={{display:"flex",alignItems:"center",gap:12,padding:"12px 15px",borderBottom:i<watchlist.length-1?`1px solid ${T.border}`:"none",cursor:r?"pointer":"default",transition:"background .15s"}}
              onClick={()=>r&&onOpen(r)} onMouseEnter={e=>e.currentTarget.style.background=T.bg3} onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
              <div style={{width:40,height:40,borderRadius:7,background:r?scoreDim(r.consensusScore):T.bg3,border:`1px solid ${r?scoreColor(r.consensusScore)+"40":T.border}`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:9,fontWeight:900,color:r?scoreColor(r.consensusScore):T.text3,flexShrink:0,fontFamily:"monospace"}}>{ticker.slice(0,5)}</div>
              <div style={{flex:1,minWidth:0}}><div style={{fontSize:12,fontWeight:600,color:T.text}}>{stock?.name||ticker}</div><div style={{fontSize:9,color:T.text3}}>{stock?.sector} · {stock?.type}</div></div>
              <div style={{fontSize:22,fontWeight:900,color:r?scoreColor(r.consensusScore):T.text3,width:48,textAlign:"center",fontFamily:"monospace"}}>{r?r.consensusScore?.toFixed(1):"—"}</div>
              {sig&&<span style={{background:sig.bg,border:`1px solid ${sig.border}`,color:sig.text,fontSize:8,fontWeight:800,padding:"3px 8px",borderRadius:3}}>{r?.consensusSignal}</span>}
              {pred&&<span style={{fontSize:10,fontWeight:700,color:pred.direction==="UP"?T.green:T.red,width:72,textAlign:"right",fontFamily:"monospace"}}>{pred.direction==="UP"?"▲":"▼"} {pred.low}–{pred.high}%</span>}
              <button onClick={e=>{e.stopPropagation();onRemove(ticker);}} style={{background:"none",border:"none",color:T.text3,cursor:"pointer",fontSize:15,padding:"4px 6px",opacity:.4}}>×</button>
            </div>
          );
        })}
      </div>
    </div>
  );
}

/* ─── MAIN APP ──────────────────────────────────────────────────────── */
/* ─── ERROR BOUNDARY ─────────────────────────────────────────────
   Catches any runtime error in the app and shows a friendly screen
   instead of a blank page. Critical for production reliability.  */
class ErrorBoundary extends React.Component {
  constructor(props){
    super(props);
    this.state = {hasError:false, error:null};
  }
  static getDerivedStateFromError(error){
    return {hasError:true, error};
  }
  componentDidCatch(error, info){
    console.error("ErrorBoundary caught:", error, info);
  }
  render(){
    if(this.state.hasError){
      return (
        <div style={{minHeight:"100vh",background:"#060A0E",color:"#E2EAF4",fontFamily:"'DM Sans',sans-serif",display:"flex",alignItems:"center",justifyContent:"center",padding:24}}>
          <div style={{maxWidth:480,textAlign:"center"}}>
            <div style={{fontSize:48,color:"#FF4757",marginBottom:16}}>⚠</div>
            <div style={{fontSize:18,fontWeight:800,marginBottom:10}}>Something broke</div>
            <div style={{fontSize:13,color:"#7A94B0",marginBottom:20,lineHeight:1.6}}>The app hit an unexpected error. Try reloading the page.</div>
            <div style={{fontSize:11,color:"#3D5470",fontFamily:"monospace",background:"#0A1018",border:"1px solid #1C2D40",borderRadius:8,padding:"10px 14px",marginBottom:20,textAlign:"left",overflowX:"auto"}}>
              {this.state.error?.message || "Unknown error"}
            </div>
            <div style={{display:"flex",gap:8,justifyContent:"center"}}>
              <button onClick={()=>window.location.reload()} style={{padding:"10px 22px",background:"#00E676",color:"#000",border:"none",borderRadius:7,fontSize:11,fontWeight:900,cursor:"pointer",letterSpacing:".06em",fontFamily:"'DM Mono',monospace"}}>RELOAD</button>
              <button onClick={async()=>{try{await window.supabaseClient?.auth?.signOut();}catch(e){} window.location.reload();}} style={{padding:"10px 22px",background:"#0F1824",color:"#7A94B0",border:"1px solid #1C2D40",borderRadius:7,fontSize:11,fontWeight:700,cursor:"pointer",letterSpacing:".06em"}}>SIGN OUT & RELOAD</button>
            </div>
          </div>
        </div>
      );
    }
    return this.props.children;
  }
}

function App(){
  const T = useT();
  // session: {token, tier, email, isDev}
  const [session,setSession]   = useState(null);
  const [view,setView]         = useState("discover");
  const [results,setResults]   = useState({});
  const [loadingTicker,setLoading]   = useState(null);
  const [loadingMeta,setLoadingMeta] = useState(null);
  const [activeResult,setActive]     = useState(null);
  const [watchlist,setWatchlist]     = useState([]);
  const [error,setError]             = useState(null);
  const [sectorFilter,setSector]     = useState("All");
  const [typeFilter,setType]         = useState("All");
  const [macro,setMacro]             = useState(MACRO_FALLBACK);
  const [usageCount,setUsageCount]   = useState(0);
  const [showUpgrade,setShowUpgrade] = useState(false);
  const [promoDiscount,setPromoDiscount] = useState(null);

  // ── Supabase session management ────────────────────────────────────
  useEffect(()=>{
    const sb = window.supabaseClient;
    if(!sb) return;

    // Helper: fetch tier with a hard timeout so we never hang
    const fetchTier = async (userId) => {
      try {
        const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3000));
        const query   = sb.from("profiles").select("tier").eq("id", userId).single();
        const {data:profile} = await Promise.race([query, timeout]);
        return profile?.tier || "free";
      } catch(e) {
        console.warn("Tier fetch failed, defaulting to free:", e.message);
        return "free";
      }
    };

    // Restore existing session on page load
    sb.auth.getSession().then(async ({data})=>{
      if(data?.session?.user){
        const tier = await fetchTier(data.session.user.id);
        setSession({
          token:  data.session.access_token,
          email:  data.session.user.email,
          userId: data.session.user.id,
          tier
        });
      }
    }).catch(e => console.error("Session restore failed:", e));

    // Listen ONLY for sign-out + external auth events (not our own sign-in calls)
    const {data:listener} = sb.auth.onAuthStateChange(async (event, sess)=>{
      if(event === "SIGNED_OUT"){
        setSession(null);
        return;
      }
      // For TOKEN_REFRESHED or SIGNED_IN events from external sources (email verify, OAuth)
      // we only update if there's no current session (avoid racing with manual sign-in)
      if((event === "SIGNED_IN" || event === "TOKEN_REFRESHED") && sess?.user){
        setSession(prev => {
          // If we already have a session for this user, don't re-fetch tier (avoid race)
          if(prev && prev.userId === sess.user.id) {
            return {...prev, token: sess.access_token};
          }
          // New sign-in from external source — fetch tier asynchronously
          fetchTier(sess.user.id).then(tier => {
            setSession(s => s?.userId === sess.user.id ? s : {
              token: sess.access_token,
              email: sess.user.email,
              userId: sess.user.id,
              tier
            });
          });
          // Return immediate session with default tier, tier updates when fetch completes
          return {
            token: sess.access_token,
            email: sess.user.email,
            userId: sess.user.id,
            tier: "free"
          };
        });
      }
    });

    return ()=>{ listener?.subscription?.unsubscribe?.(); };
  },[]);

  // Fetch live macro on login
  useEffect(()=>{
    if(!session) return;
    fetchLiveMacro().then(d => setMacro(d)).catch(e => console.warn("Macro fetch failed:", e));
  },[session]);

  const FREE_LIMIT = 5;
  const isPro = session?.tier === "pro" || session?.tier === "institutional";
  const isInstitutional = session?.tier === "institutional";

  const analyse = useCallback(async(ticker,name,sector,type)=>{
    if(results[ticker]){setActive(results[ticker]);return;}

    // Free tier limit
    if(!isPro && usageCount >= FREE_LIMIT){
      setShowUpgrade(true);
      return;
    }

    setLoading(ticker);setLoadingMeta({ticker,name});setError(null);
    try{
      const macroCtx = buildMacroCtx(macro);
      const userMsg  = `SECURITY: ${ticker} — ${name}\nSECTOR: ${sector}\nTYPE: ${type}\n\n${macroCtx}\n\nReturn ONLY the JSON object. Start with { end with }. No markdown, no backticks, no preamble.`;
      // Free tier: Haiku (cheaper), Pro: Sonnet 4 (full power)
      const token = session?.token;
      const tier  = session?.tier || "free";
      // Free tier gets the LITE ATLAS prompt — verdict-only fields, ~70% fewer tokens
      const atlasPrompt = isPro ? ATLAS_SYS : ATLAS_LITE_SYS;
      const atlasMaxTokens = isPro ? 5000 : 1200;
      const [atlas,meridian]=await Promise.all([
        callClaude(token,atlasPrompt,userMsg,atlasMaxTokens,tier),
        callClaude(token,MERIDIAN_SYS,userMsg,2000,tier),
      ]);
      const cs=parseFloat((atlas.overallScore*.6+meridian.overallScore*.4).toFixed(1));
      const sm={"STRONG BUY":5,"BUY":4,"HOLD":3,"SELL":2,"STRONG SELL":1};
      const avg=(sm[atlas.signal]||3)*.6+(sm[meridian.signal]||3)*.4;
      const sig=avg>=4.5?"STRONG BUY":avg>=3.5?"BUY":avg>=2.5?"HOLD":avg>=1.5?"SELL":"STRONG SELL";
      const r={ticker,name,sector,type,consensusScore:cs,consensusSignal:sig,atlas,meridian,tier};
      setResults(p=>({...p,[ticker]:r}));
      setActive(r);
      if(!isPro) setUsageCount(p=>p+1);
    }catch(e){setError(e.message||`Analysis failed for ${ticker}. Retry.`);}
    finally{setLoading(null);setLoadingMeta(null);}
  },[results,session,macro,isPro,usageCount]);

  if(!session) return <AuthScreen onAuth={s=>setSession(s)}/>;

  const filtered=STOCKS.filter(s=>(sectorFilter==="All"||s.sector===sectorFilter)&&(typeFilter==="All"||s.type===typeFilter));
  const activeStock=activeResult?STOCKS.find(s=>s.ticker===activeResult.ticker):null;

  return(
    <div style={{minHeight:"100vh",background:T.bg0,color:T.text,fontFamily:"'Plus Jakarta Sans',-apple-system,sans-serif"}}>
      <style>{`
  @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400&display=swap');
  *{box-sizing:border-box;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}
  body{margin:0;background:${T.bg0};font-family:'Plus Jakarta Sans',-apple-system,BlinkMacSystemFont,sans-serif;color:${T.text};}
  ::-webkit-scrollbar{width:5px;height:5px;}
  ::-webkit-scrollbar-track{background:transparent;}
  ::-webkit-scrollbar-thumb{background:${T.border2};border-radius:99px;}
  input::placeholder{color:${T.text3};}
  button{font-family:inherit;}
  a{color:${T.blue};}
  @keyframes spin{to{transform:rotate(360deg)}}
  @keyframes fadeUp{from{opacity:0;transform:translateY(16px)}to{opacity:1;transform:translateY(0)}}
  @keyframes fadeIn{from{opacity:0}to{opacity:1}}
  .fade-up{animation:fadeUp .4s cubic-bezier(.16,1,.3,1) forwards;}
  .fade-in{animation:fadeIn .3s ease forwards;}
`}</style>

      {loadingMeta&&<LoadingOverlay ticker={loadingMeta.ticker} name={loadingMeta.name}/>}

      {/* NAV — Apple-style premium top bar */}
      <div style={{background:`${T.bg1}f0`,backdropFilter:"blur(24px) saturate(180%)",borderBottom:`1px solid ${T.border}`,boxShadow:"0 1px 0 rgba(0,0,0,.06)",padding:"0 24px",display:"flex",alignItems:"center",gap:16,height:60,position:"sticky",top:0,zIndex:100}}>

        {/* Logo */}
        <div style={{display:"flex",alignItems:"center",gap:10,flexShrink:0}}>
          <div style={{width:34,height:34,borderRadius:10,background:`linear-gradient(135deg,${T.accent}30,${T.accent}10)`,border:`1px solid ${T.accent}30`,display:"flex",alignItems:"center",justifyContent:"center"}}>
            <Logo size={20}/>
          </div>
          <div>
            <div style={{fontSize:15,fontWeight:800,color:T.text,letterSpacing:"-.02em",lineHeight:1}}>MacroScore</div>
            <div style={{fontSize:10,color:T.text3,letterSpacing:".02em"}}>JSE Intelligence</div>
          </div>
        </div>

        {/* Nav tabs — larger, pill-style */}
        <div style={{display:"flex",gap:4,background:T.bg2,borderRadius:12,padding:4,marginLeft:8}}>
          {[
            {v:"discover", l:"Discover", icon:"◉"},
            {v:"watchlist", l:watchlist.length>0?`Watchlist (${watchlist.length})`:"Watchlist", icon:"★"},
            {v:"macrofolio", l:"Macrofolio", icon:"◎"},
            {v:"bulk", l:"Bulk", icon:"⊞"},
          ].map(({v,l,icon})=>(
            <button key={v} onClick={()=>setView(v)}
              style={{
                background:view===v?T.bg4:"transparent",
                color:view===v?T.text:T.text2,
                border:"none",borderRadius:9,
                padding:"8px 16px",fontSize:13,fontWeight:view===v?700:500,
                cursor:"pointer",transition:"all .2s",
                display:"flex",alignItems:"center",gap:6,whiteSpace:"nowrap",
              }}>
              <span style={{fontSize:11,opacity:view===v?1:.6}}>{icon}</span>
              {l}
            </button>
          ))}
        </div>

        <div style={{flex:1}}/>

        {/* Right side — user info + actions */}
        <div style={{display:"flex",alignItems:"center",gap:10,flexShrink:0}}>
          {/* Usage counter for free users */}
          {!isPro && (
            <div style={{fontSize:12,color:T.text3}}>
              {usageCount}/{FREE_LIMIT} analyses
            </div>
          )}

          {/* Tier badge */}
          <div style={{
            background:isInstitutional?`${T.cyan}20`:isPro?`${T.accent}20`:T.bg3,
            color:isInstitutional?T.cyan:isPro?T.accent:T.text3,
            border:`1px solid ${isInstitutional?T.cyan+"40":isPro?T.accent+"40":T.border}`,
            fontSize:11,fontWeight:700,padding:"4px 10px",borderRadius:99,
          }}>
            {isInstitutional?"Institutional":isPro?"Pro":"Free"}
          </div>

          {/* Upgrade button */}
          {!isPro && (
            <button onClick={()=>setShowUpgrade(true)}
              style={{background:T.accent,color:"#000",border:"none",borderRadius:99,padding:"8px 18px",fontSize:13,fontWeight:700,cursor:"pointer"}}>
              Upgrade
            </button>
          )}

          {/* Theme toggle */}
          <ThemeToggle/>

          {/* Sign out */}
          <button onClick={async()=>{await authSignOut();setSession(null);}}
            style={{background:T.bg2,color:T.text2,border:`1.5px solid ${T.border}`,borderRadius:99,padding:"8px 14px",fontSize:13,fontWeight:500,cursor:"pointer"}}>
            Sign out
          </button>
        </div>
      </div>

      {/* MAIN */}
      <div style={{maxWidth:1100,margin:"0 auto",padding:"24px 20px"}}>
        {error&&<div style={{marginBottom:10,padding:"8px 13px",background:"#FF475712",border:`1px solid ${T.red}35`,borderRadius:6,display:"flex",justifyContent:"space-between",alignItems:"center"}}>
          <span style={{color:T.red,fontSize:11,fontFamily:"monospace"}}>⚠ {error}</span>
          <button onClick={()=>setError(null)} style={{background:"none",border:"none",color:T.red,cursor:"pointer",fontSize:14}}>×</button>
        </div>}

        {view==="discover"&&(
          <div className="fade-up">

            {/* Search bar */}
            <div style={{position:"relative",marginBottom:32}}>
              <span style={{position:"absolute",left:18,top:"50%",transform:"translateY(-50%)",color:T.text3,fontSize:18,pointerEvents:"none"}}>⌕</span>
              <input
                value={sectorFilter==="search"?typeFilter:""}
                onChange={e=>{setSector("search");setType(e.target.value);}}
                onFocus={e=>{setSector("search");e.target.style.borderColor=T.accent;e.target.style.boxShadow=`0 0 0 3px ${T.accent}20`;}}
                onBlur={e=>{e.target.style.borderColor=T.border;e.target.style.boxShadow="none";}}
                placeholder="Search stocks, ETFs, sectors…"
                style={{
                  width:"100%",background:T.bg1,
                  border:`1.5px solid ${T.border}`,
                  borderRadius:16,padding:"18px 20px 18px 52px",
                  fontSize:17,color:T.text,outline:"none",
                  fontFamily:"'Plus Jakarta Sans',sans-serif",
                  boxShadow:"0 1px 3px rgba(0,0,0,.06)",
                  transition:"border .2s, box-shadow .2s",
                }}
              />
            </div>

            {/* Featured section */}
            {sectorFilter !== "search" && (
              <div>
                {/* Section header + category filters */}
                <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-end",marginBottom:20,flexWrap:"wrap",gap:12}}>
                  <div>
                    <div style={{fontSize:26,fontWeight:800,color:T.text,letterSpacing:"-.03em",lineHeight:1}}>Featured</div>
                    <div style={{fontSize:15,color:T.text3,marginTop:4,fontWeight:400}}>Handpicked for today's market</div>
                  </div>
                  <div style={{display:"flex",gap:6,flexWrap:"wrap"}}>
                    {["All","Gold Mining","Financials","Technology","Property","Mining"].map(cat=>(
                      <button key={cat}
                        onClick={()=>setSector(cat==="All"?"All":cat)}
                        style={{
                          padding:"8px 16px",
                          background:(!sectorFilter&&cat==="All")||sectorFilter===cat?T.accent:T.bg1,
                          color:(!sectorFilter&&cat==="All")||sectorFilter===cat?"#fff":T.text2,
                          border:`1.5px solid ${(!sectorFilter&&cat==="All")||sectorFilter===cat?T.accent:T.border}`,
                          borderRadius:99,fontSize:13,fontWeight:600,
                          cursor:"pointer",transition:"all .15s",whiteSpace:"nowrap",
                          boxShadow:(!sectorFilter&&cat==="All")||sectorFilter===cat?`0 2px 8px ${T.accent}30`:"none",
                        }}>
                        {cat}
                      </button>
                    ))}
                  </div>
                </div>

                {/* Stock cards — 2-column grid */}
                <div style={{display:"grid",gridTemplateColumns:"repeat(2,1fr)",gap:14}}>
                  {STOCKS
                    .filter(s => !sectorFilter||sectorFilter==="All" ? [
                      "GFI","CPI","NPN","SOL","AMS","FSR","MTN","AGL","SHP","DSY",
                      "STX40","STXNDQ","HAR","IMP","TFG","GRT","VOD","APN","CFR","BTI"
                    ].includes(s.ticker) : s.sector===sectorFilter)
                    .slice(0,20)
                    .map(stock=>{
                      const hasResult = results[stock.ticker];
                      const sig = hasResult ? getSig(hasResult.consensusSignal) : null;
                      const isLoading = loadingTicker===stock.ticker;
                      const typeCol = stock.type==="ETF"?T.blue:stock.type==="ETN"?T.purple:T.accent;

                      return (
                        <div key={stock.ticker}
                          onClick={()=>analyse(stock.ticker,stock.name,stock.sector,stock.type)}
                          style={{
                            background:T.bg1,
                            border:`1.5px solid ${hasResult?scoreColor(hasResult.consensusScore)+"30":T.border}`,
                            borderRadius:20,padding:"20px 20px",
                            cursor:"pointer",transition:"all .2s",
                            boxShadow:"0 1px 3px rgba(0,0,0,.06)",
                            display:"flex",alignItems:"center",gap:16,
                            position:"relative",overflow:"hidden",
                          }}
                          onMouseEnter={e=>{e.currentTarget.style.boxShadow="0 8px 24px rgba(0,0,0,.10)";e.currentTarget.style.transform="translateY(-2px)";}}
                          onMouseLeave={e=>{e.currentTarget.style.boxShadow="0 1px 3px rgba(0,0,0,.06)";e.currentTarget.style.transform="translateY(0)";}}>

                          {/* Score strip on left edge */}
                          {hasResult&&<div style={{position:"absolute",left:0,top:0,bottom:0,width:3,background:scoreColor(hasResult.consensusScore),borderRadius:"20px 0 0 20px"}}/>}

                          {/* Company Logo */}
                          <CompanyLogo ticker={stock.ticker} size={52}/>

                          {/* Info */}
                          <div style={{flex:1,minWidth:0}}>
                            <div style={{fontSize:18,fontWeight:800,color:T.text,letterSpacing:"-.02em",marginBottom:2}}>{stock.ticker}</div>
                            <div style={{fontSize:14,color:T.text2,fontWeight:500,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",marginBottom:8}}>{stock.name}</div>
                            <div style={{display:"flex",alignItems:"center",gap:6}}>
                              <span style={{fontSize:12,color:typeCol,background:`${typeCol}14`,padding:"3px 9px",borderRadius:99,fontWeight:600}}>{stock.type}</span>
                              <span style={{fontSize:12,color:T.text3}}>{stock.sector}</span>
                            </div>
                          </div>

                          {/* Right: score or loading or arrow */}
                          <div style={{flexShrink:0,textAlign:"right"}}>
                            {isLoading ? (
                              <div style={{width:24,height:24,border:`2.5px solid ${T.border}`,borderTop:`2.5px solid ${T.accent}`,borderRadius:"50%",animation:"spin 1s linear infinite"}}/>
                            ) : hasResult ? (
                              <div>
                                <div style={{fontSize:24,fontWeight:900,color:scoreColor(hasResult.consensusScore),lineHeight:1,marginBottom:3}}>{hasResult.consensusScore.toFixed(1)}</div>
                                <div style={{fontSize:11,fontWeight:700,color:sig?.text}}>{sig?.glyph} {hasResult.consensusSignal}</div>
                              </div>
                            ) : (
                              <div style={{fontSize:20,color:T.text3,fontWeight:300}}>›</div>
                            )}
                          </div>
                        </div>
                      );
                    })
                  }
                </div>
              </div>
            )}

            {/* Search results */}
            {sectorFilter==="search" && typeFilter && (
              <div>
                <div style={{fontSize:16,color:T.text2,marginBottom:20,fontWeight:500}}>
                  Results for "<span style={{color:T.text,fontWeight:700}}>{typeFilter}</span>"
                </div>
                <div style={{display:"grid",gridTemplateColumns:"repeat(2,1fr)",gap:14}}>
                  {STOCKS
                    .filter(s=>
                      s.ticker.toLowerCase().includes(typeFilter.toLowerCase())||
                      s.name.toLowerCase().includes(typeFilter.toLowerCase())||
                      s.sector.toLowerCase().includes(typeFilter.toLowerCase())
                    )
                    .slice(0,24)
                    .map(stock=>{
                      const hasResult = results[stock.ticker];
                      const typeCol = stock.type==="ETF"?T.blue:stock.type==="ETN"?T.purple:T.accent;
                      return (
                        <div key={stock.ticker}
                          onClick={()=>analyse(stock.ticker,stock.name,stock.sector,stock.type)}
                          style={{background:T.bg1,border:`1.5px solid ${T.border}`,borderRadius:20,padding:"20px 20px",cursor:"pointer",transition:"all .2s",boxShadow:"0 1px 3px rgba(0,0,0,.06)",display:"flex",alignItems:"center",gap:16}}
                          onMouseEnter={e=>{e.currentTarget.style.boxShadow="0 8px 24px rgba(0,0,0,.10)";e.currentTarget.style.transform="translateY(-2px)";}}
                          onMouseLeave={e=>{e.currentTarget.style.boxShadow="0 1px 3px rgba(0,0,0,.06)";e.currentTarget.style.transform="translateY(0)";}}>
                          <CompanyLogo ticker={stock.ticker} size={52}/>
                          <div style={{flex:1,minWidth:0}}>
                            <div style={{fontSize:18,fontWeight:800,color:T.text,marginBottom:2}}>{stock.ticker}</div>
                            <div style={{fontSize:14,color:T.text2,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",marginBottom:8}}>{stock.name}</div>
                            <span style={{fontSize:12,color:typeCol,background:`${typeCol}14`,padding:"3px 9px",borderRadius:99,fontWeight:600}}>{stock.type}</span>
                          </div>
                          {hasResult&&<div style={{fontSize:22,fontWeight:900,color:scoreColor(hasResult.consensusScore)}}>{hasResult.consensusScore.toFixed(1)}</div>}
                          {!hasResult&&<div style={{fontSize:20,color:T.text3}}>›</div>}
                        </div>
                      );
                    })
                  }
                </div>
              </div>
            )}

            {/* Error */}
            {error&&(
              <div style={{background:"#FFF2F2",border:`1.5px solid ${T.red}30`,borderRadius:14,padding:"16px 20px",marginTop:20,fontSize:14,color:T.red,display:"flex",gap:10,alignItems:"center"}}>
                <span style={{fontSize:18}}>⚠</span><span>{error}</span>
                <button onClick={()=>setError(null)} style={{marginLeft:"auto",background:"none",border:"none",color:T.red,cursor:"pointer",fontSize:20,lineHeight:1}}>×</button>
              </div>
            )}
          </div>
        )}

{view==="watchlist"&&(
          <div>
            <div style={{marginBottom:16}}><div style={{fontSize:8,color:T.text3,letterSpacing:".12em",marginBottom:3,textTransform:"uppercase"}}>PORTFOLIO TRACKER</div><div style={{fontSize:16,fontWeight:900,color:T.text}}>My Watchlist</div></div>
            <WatchlistView watchlist={watchlist} results={results} onRemove={t=>setWatchlist(p=>p.filter(x=>x!==t))} onOpen={setActive}/>
          </div>
        )}

        {view==="macrofolio"&&(
          <Macrofolio
            session={session}
            macro={macro}
            isPro={isPro}
            isInstitutional={isInstitutional}
            onOpen={(stock)=>{
              if(results[stock.ticker]) setActive(results[stock.ticker]);
              else analyse(stock.ticker, stock.name, stock.sector, stock.type);
            }}
            onUpgrade={()=>setShowUpgrade(true)}
          />
        )}

        {view==="bulk"&&(
          <BulkAnalysis
            session={session}
            macro={macro}
            results={results}
            setResults={setResults}
            onOpen={setActive}
            onUpgrade={()=>setShowUpgrade(true)}
          />
        )}
      </div>

      {activeResult&&<AnalysisModal result={activeResult} stock={activeStock} onClose={()=>setActive(null)}
        onToggleWatchlist={r=>{const t=r.ticker;setWatchlist(p=>p.includes(t)?p.filter(x=>x!==t):[...p,t]);}}
        inWatchlist={watchlist.includes(activeResult.ticker)}
        isPro={isPro}
        onUpgrade={()=>setShowUpgrade(true)}/>}

      {showUpgrade&&(
        <div style={{position:"fixed",inset:0,background:T.overlay,zIndex:950,display:"flex",alignItems:"center",justifyContent:"center",padding:20,backdropFilter:"blur(12px)"}}
          onClick={()=>setShowUpgrade(false)}>
          <div onClick={e=>e.stopPropagation()} style={{background:T.bg1,border:`1px solid ${T.border2}`,borderRadius:16,padding:"28px 28px",width:"100%",maxWidth:520,boxShadow:"0 40px 80px rgba(0,0,0,.9)",maxHeight:"90vh",overflowY:"auto"}}>
            <div style={{textAlign:"center",marginBottom:22}}>
              <div style={{fontSize:18,fontWeight:900,color:T.text,marginBottom:6}}>Upgrade MacroScore</div>
              {!isPro&&usageCount>=FREE_LIMIT
                ? <div style={{fontSize:12,color:T.red}}>You've used all {FREE_LIMIT} free analyses this month.</div>
                : <div style={{fontSize:12,color:T.text2}}>Unlock unlimited analyses and full depth reports.</div>
              }
            </div>

            {/* Promo Code Input */}
            <PromoCodeInput onApplied={(d)=>setPromoDiscount(d)} currentDiscount={promoDiscount}/>

            <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10,marginBottom:18}}>
              {[
                {name:"PRO",basePrice:199,period:"/month",col:T.accent,badge:"RECOMMENDED",
                 features:["Unlimited analyses","All 8 tabs","Claude Sonnet 4.6","ATLAS + MERIDIAN full depth","My Macrofolio","Unlimited watchlist"]},
                {name:"INSTITUTIONAL",basePrice:1999,period:"/month",col:T.cyan,
                 features:["Claude Opus 4.7 AI","Bulk analysis (10 at once)","CSV export","My Macrofolio","Priority support","Team seats (coming soon)","PDF reports (coming soon)"]},
              ].map(tier=>{
                const finalPrice = promoDiscount?.percent ? Math.round(tier.basePrice*(1-promoDiscount.percent/100)) : tier.basePrice;
                const hasDiscount = promoDiscount && finalPrice < tier.basePrice;
                const payfastUrl = `https://www.payfast.co.za/eng/process?merchant_id=YOUR_ID&merchant_key=YOUR_KEY&amount=${finalPrice}.00&item_name=MacroScore+${tier.name}${promoDiscount?`&custom_str1=${promoDiscount.code}`:""}`;
                return (
                  <div key={tier.name} style={{background:`${tier.col}06`,border:`1px solid ${tier.col}35`,borderRadius:12,padding:"16px 15px",position:"relative"}}>
                    {tier.badge&&<div style={{position:"absolute",top:-8,left:"50%",transform:"translateX(-50%)",background:tier.col,color:"#000",fontSize:7,fontWeight:900,padding:"2px 8px",borderRadius:3,whiteSpace:"nowrap",letterSpacing:".08em"}}>{tier.badge}</div>}
                    <div style={{fontSize:9,fontWeight:800,color:tier.col,letterSpacing:".12em",marginBottom:5,textTransform:"uppercase",marginTop:8}}>{tier.name}</div>
                    <div style={{display:"flex",alignItems:"baseline",gap:5,marginBottom:12,flexWrap:"wrap"}}>
                      <span style={{fontSize:26,fontWeight:900,color:T.text,fontFamily:"'DM Mono',monospace"}}>R{finalPrice}</span>
                      <span style={{fontSize:10,color:T.text3}}>{tier.period}</span>
                      {hasDiscount&&<span style={{fontSize:10,color:T.red,textDecoration:"line-through",fontFamily:"'DM Mono',monospace"}}>R{tier.basePrice}</span>}
                    </div>
                    {hasDiscount&&<div style={{fontSize:9,color:T.green,fontWeight:700,marginBottom:8,letterSpacing:".05em"}}>✓ {promoDiscount.percent}% OFF APPLIED</div>}
                    {tier.features.map((f,i)=>(
                      <div key={i} style={{display:"flex",gap:6,marginBottom:4}}>
                        <span style={{color:tier.col,fontSize:9,flexShrink:0}}>✓</span>
                        <span style={{fontSize:10,color:T.text2,lineHeight:1.4}}>{f}</span>
                      </div>
                    ))}
                    <a href={payfastUrl} target="_blank" rel="noopener noreferrer"
                      style={{display:"block",marginTop:14,padding:"9px",background:tier.col,color:"#000",borderRadius:7,textDecoration:"none",fontSize:10,fontWeight:900,textAlign:"center",letterSpacing:".07em"}}>
                      SUBSCRIBE VIA PAYFAST →
                    </a>
                  </div>
                );
              })}
            </div>
            <div style={{fontSize:9,color:T.text3,textAlign:"center",lineHeight:1.7}}>
              Payments processed securely by PayFast · Cancel anytime · ZAR billing<br/>
              After payment, email <span style={{color:T.accent}}>support@macroscore.co.za</span> to activate your tier
            </div>
            <div style={{marginTop:10,fontSize:9,color:T.text3,textAlign:"center",opacity:.6}}>
              MacroScore is an information service, not a financial advisor · All analysis is AI-generated
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(
  <ThemeProvider>
    <ErrorBoundary>
      <App />
    </ErrorBoundary>
  </ThemeProvider>
);
