/* =========================================================
   VALIDQ · TICKER DETAIL · PRO MODE
   MRK · Merck & Co. · score 58 · HC SWEET SPOT
   ========================================================= */
/* === D1-C4e.4 engine + analysis labels === */

/* === D1-C4e.3 lifecycle dot fix + residuals === */

/* === D1-C4e.2 motor v3 signal patterns === */

/* === D1-C4e.1 signal verdict + pattern translation === */

/* === D1-C4e JSX runtime i18n (EN-native + T() + DICT EN-ES) === */

const { useState, useEffect, useRef } = React;
const T_DICT_EN_ES = {"Search another ticker — JNJ, KO, NESN.SW…": "Buscar otro ticker — JNJ, KO, NESN.SW…", "You need an account to analyse tickers. Free plan includes 5 analyses per month.": "Necesitas una cuenta para analizar tickers. El plan Gratuito incluye 5 análisis al mes.", "Intrinsic value vs current price": "Valor intrínseco vs precio actual", "Tactical & NLP signals": "Señales tácticas y NLP", "Sector sweet-spot": "Zona óptima del sector", "Composite score": "Puntuación compuesta", "Score breakdown": "Desglose de puntuación", "Sign in required": "Inicio de sesión requerido", "Current drivers": "Drivers actuales", "Intrinsic value": "Valor intrínseco", "Current price": "Precio actual", "Current score": "Puntuación actual", "Mapping error": "Error de mapeo", "Toggle theme": "Cambiar tema", "Back to home": "Volver al inicio", "Bad request": "Solicitud inválida", "Methodology": "Metodología", "Significance": "Significancia", "Agent hive": "Colmena de agentes", "Sweet spot": "Zona óptima", "Lifecycle": "Ciclo de vida", "Confidence": "Confianza", "Key stats": "Datos clave", "Analysing": "Analizando", "Sign out": "Cerrar sesión", "Sign in": "Iniciar sesión", "Account": "Cuenta", "Alerts": "Alertas", "Retry": "Reintentar", "Terms": "Términos", "Close": "Cerrar", "Upside": "Potencial", "Win rate": "Tasa de aciertos", "Agent": "Agente", "News": "Noticias", "Sell-side revisions 30d": "Revisiones del sell-side 30d", "Healthcare relative": "Relativo Sanidad", "Consensus rating": "Rating de consenso", "ETF inflows 30d": "Entradas ETF 30d", "Earnings momentum": "Momentum de beneficios", "Net Debt/EBITDA": "Deuda neta/EBITDA", "Moat (Damodaran)": "Foso (Damodaran)", "FY25 EPS guide": "Guía EPS FY25", "Operating margin": "Margen operativo", "Earnings yield": "Rentabilidad por beneficio", "Buyers/Sellers": "Compradores/Vendedores", "FCF yield TTM": "Rentabilidad FCF TTM", "Last 3 calls": "Últimas 3 calls", "Short interest": "Interés corto", "Switching cost": "Coste de cambio", "Sector NLP avg": "Media NLP del sector", "Brand equity": "Valor de marca", "Change vs Q1": "Cambio vs T1", "Yield curve": "Curva de tipos", "Sector cycle": "Ciclo del sector", "Avg maturity": "Vencimiento medio", "Avg target": "Precio objetivo medio", "Debt/Equity": "Deuda/Patrimonio", "CAPEX/Sales": "CAPEX/Ventas", "DCF upside": "Potencial DCF", "IP / patents": "PI / patentes", "Cost moat": "Foso de costes", "WC days": "Días WC", "Net 90d": "Neto 90d", "Threshold": "Umbral", "Prep tone": "Tono preparado", "Strong (Keytruda 2028)": "Fuerte (Keytruda 2028)", "Late-cycle defensive": "Defensivo de ciclo tardío", "Bullish-neutral": "Alcista-neutral", "+0.42 (bullish)": "+0.42 (alcista)", "4.2 (safe zone)": "4.2 (zona segura)", "−2.41 (clean)": "−2.41 (limpio)", "Buy (4.1/5)": "Compra (4.1/5)", "+0.05 (raise)": "+0.05 (sube)", "Medium-high": "Medio-alto", "0.04 (low)": "0.04 (bajo)", "Steepening": "Empinándose", "1.2% float": "1.2% del float", "8.4 years": "8.4 años", "Medium": "Medio", "Back to feed": "Volver al feed", "In watchlist": "En watchlist", "Add to watchlist": "Añadir a watchlist", "Re-analyse": "Reanalizar", "Analyse another": "Analizar otro", "analysed": "analizado", "cache 14m": "caché 14m", "calibrated against SSRN paper 6735820": "calibrado con paper SSRN 6735820", "vs SPX · 36 months": "vs SPX · 36 meses", "annualised": "anualizado", "positive cohorts": "cohortes positivas", "Backtest 2012–2014, 2017–2019 · 4 107 ticker-years": "Backtest 2012–2014, 2017–2019 · 4 107 años-ticker", "7 academic models · click for detail": "7 modelos académicos · clic para detalle", "7 agents · click for detail": "7 agentes · clic para detalle", "12-month horizon": "horizonte 12 meses", "Start-up": "Inicio", "Young Growth": "Crec. joven", "High Growth": "Crec. alto", "Mature G.": "Mad. crec.", "Mature Stable": "Madurez estable", "Decline": "Declive", "article": "artículo", "articles": "artículos", "active": "activas", "no alerts": "sin alertas", "No alerts yet.": "Aún no hay alertas.", "Earnings + score deltas will appear here.": "Variaciones de earnings y score aparecerán aquí.", "CAGR α": "TCAC α", "2-stage Mauboussin · WACC 7.2% · g 2.5%": "Mauboussin 2-etapas · WACC 7.2% · g 2.5%", "Accounting integrity": "Integridad contable", "Bankruptcy risk": "Riesgo de quiebra", "Business quality": "Calidad del negocio", "Valuation": "Valoración", "Debt structure": "Estructura de deuda", "Free Cash Flow Momentum": "Momentum de Free Cash Flow", "Guidance Reliability": "Fiabilidad de la guía", "Insider Activity": "Actividad de insiders", "ATLAS · Intangibles & Moat": "ATLAS · Intangibles y foso", "Sector timing": "Timing del sector", "CHRONOS · Cycle timing": "CHRONOS · Timing del ciclo", "CLIO · Business model": "CLIO · Modelo de negocio", "ARGOS · Consensus & flows": "ARGOS · Consenso y flujos", "HIGH": "ALTA", "MEDIUM": "MEDIA", "LOW": "BAJA", "Signal · quantitative verdict": "Señal · veredicto cuantitativo", "Alpha 3y": "Alfa 3a", "view detail": "ver detalle", "Valuation · reverse-DCF (Mauboussin)": "Valoración · reverse-DCF (Mauboussin)", "Valuation band": "Banda de valoración", "· overvalued": "· sobrevalorada", "· undervalued": "· infravalorada", "current": "actual", "fair value": "valor justo", "undervalued": "infravalorada", "fair": "justa", "expensive": "cara", "range": "rango", "inside": "dentro", "above range": "por encima", "below range": "por debajo", "EODHD · adjusted close": "EODHD · cierre ajustado", "My account": "Mi cuenta", "Profile & details": "Perfil y detalles", "Manage tickers": "Gestionar tickers", "History": "Historial", "Plan & billing": "Plan y facturación", "Help": "Ayuda", "Docs & support": "Docs y soporte", "this month": "este mes", "Layer": "Capa", "C1–C5 are the \"pure validation\" layers (SSRN paper 6735820). C6–C7 add NLP and intangibles signals. Recomputed after a new 10-K/10-Q or manual re-analysis.": "C1–C5 son las capas de \"validación pura\" (paper SSRN 6735820). C6–C7 añaden señales NLP e intangibles. Se recomputan tras un nuevo 10-K/10-Q o reanálisis manual.", "current score": "score actual", "Updated 14m ago · EODHD + SEC EDGAR data. Re-evaluated every 24h or after a new 10-Q/8-K.": "Actualizado hace 14m · Datos EODHD + SEC EDGAR. Re-evaluado cada 24h o tras nuevo 10-Q/8-K.", "confidence": "confianza", "Monthly quota reached": "Cuota mensual alcanzada", "You've used all your analyses this month. Upgrade your plan to continue.": "Has usado todos tus análisis del mes. Sube de plan para continuar.", "Plan upgrade required": "Mejora de plan requerida", "This feature requires a higher plan": "Esta función requiere un plan superior", "Ticker not found": "Ticker no encontrado", "Temporary error": "Error temporal", "Please try again": "Inténtalo de nuevo", "Watchlist error": "Error de watchlist", "removed from watchlist": "eliminado de watchlist", "added to watchlist": "añadido a watchlist", "AVOID": "EVITAR", "BUY": "COMPRAR", "WAIT": "ESPERAR", "REVIEW": "REVISAR", "QUALITY TRAP": "TRAMPA DE CALIDAD", "VALUE TRAP": "TRAMPA DE VALOR", "MOMENTUM TRAP": "TRAMPA DE MOMENTUM", "DEEP VALUE": "VALOR PROFUNDO", "GROWTH TRAP": "TRAMPA DE CRECIMIENTO", "QUALITY MISS": "CALIDAD INSUFICIENTE", "VALUE MISS": "INFRAVALORACIÓN AUSENTE", "HIGH QUALITY": "ALTA CALIDAD", "WIDE MOAT": "FOSO AMPLIO", "FAIR VALUE": "VALOR JUSTO", "CLEAR BUY": "COMPRA CLARA", "DEEP VALUE TRAP": "TRAMPA VALOR PROFUNDO", "PREMIUM PRICED": "PRECIO PREMIUM", "VALUE SWEET SPOT": "SWEET SPOT DE VALOR", "VALUE SWEET SPOT HC": "SWEET SPOT DE VALOR HC", "VALUE SWEET SPOT LITE": "SWEET SPOT DE VALOR LITE", "HC SWEET SPOT": "SWEET SPOT HC", "1 year · USD": "1 año · USD", "52w position": "Posición 52s", "GICS industry": "Industria GICS", "Engine": "Motor", "Analysis": "Análisis", "Engine v6.1 · 7 layers · 9 agents": "Motor v6.1 · 7 capas · 9 agentes", "Investment thesis": "Tesis de inversión", "Powered by": "Generado por", "Plan": "Plan"};
const T = (s) => {
  if (typeof s !== "string") return s;
  try {
    const lang = (typeof localStorage !== "undefined" && localStorage.getItem("vq_lang")) || "es";
    if (lang === "es" && T_DICT_EN_ES[s]) return T_DICT_EN_ES[s];
  } catch (e) {}
  return s;
};


/* ---------- Lucide-style icons (inline SVG, line-only) ---------- */
const I = ({ d, size = 16, sw = 1.6, children, ...rest }) =>
<svg viewBox="0 0 24 24" width={size} height={size} fill="none"
stroke="currentColor" strokeWidth={sw}
strokeLinecap="round" strokeLinejoin="round" {...rest}>
    {children || <path d={d} />}
  </svg>;

const Icon = {
  ArrowLeft: (p) => <I {...p}><path d="M19 12H5" /><path d="m12 19-7-7 7-7" /></I>,
  Search: (p) => <I {...p}><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></I>,
  Sun: (p) => <I {...p}><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" /></I>,
  Moon: (p) => <I {...p}><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" /></I>,
  Star: (p) => <I {...p}><path d="m12 2 3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" /></I>,
  Refresh: (p) => <I {...p}><path d="M3 12a9 9 0 0 1 15-6.7L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-15 6.7L3 16" /><path d="M8 16H3v5" /></I>,
  Share: (p) => <I {...p}><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><path d="m8.6 13.5 6.8 4M15.4 6.5l-6.8 4" /></I>,
  Info: (p) => <I {...p}><circle cx="12" cy="12" r="10" /><path d="M12 16v-4M12 8h.01" /></I>,
  Check: (p) => <I {...p}><path d="M20 6 9 17l-5-5" /></I>,
  X: (p) => <I {...p}><path d="M18 6 6 18M6 6l12 12" /></I>,
  ChevronDown: (p) => <I {...p}><path d="m6 9 6 6 6-6" /></I>,
  Flask: (p) => <I {...p}><path d="M10 2v7.31" /><path d="M14 9.3V2" /><path d="M8.5 2h7" /><path d="M14 9.3a6.5 6.5 0 1 1-4 0" /><path d="M5.52 16h12.96" /></I>,
  Scale: (p) => <I {...p}><path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" /><path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z" /><path d="M7 21h10M12 3v18M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2" /></I>,
  Gem: (p) => <I {...p}><path d="M6 3h12l4 6-10 13L2 9Z" /><path d="M11 3 8 9l4 13 4-13-3-6" /><path d="M2 9h20" /></I>,
  Tag: (p) => <I {...p}><path d="M12.59 2.59A2 2 0 0 0 11.17 2H4a2 2 0 0 0-2 2v7.17a2 2 0 0 0 .59 1.41l8.83 8.83a2 2 0 0 0 2.83 0l7.17-7.17a2 2 0 0 0 0-2.83z" /><path d="M7 7h.01" /></I>,
  Landmark: (p) => <I {...p}><path d="M3 22h18M5 22V11M19 22V11M3 11h18M12 4 3 9h18l-9-5z" /></I>,
  Home: (p) => <I {...p}><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /><path d="M9 22V12h6v10" /></I>,
  Wave: (p) => <I {...p}><path d="M2 12c2 0 2-4 4-4s2 8 4 8 2-12 4-12 2 16 4 16 2-8 4-8" /></I>,
  Trending: (p) => <I {...p}><path d="m22 7-8.5 8.5-5-5L2 17" /><path d="M16 7h6v6" /></I>,
  Cpu: (p) => <I {...p}><rect x="4" y="4" width="16" height="16" rx="2" /><rect x="9" y="9" width="6" height="6" /><path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" /></I>,
  User: (p) => <I {...p}><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></I>,
  Eye: (p) => <I {...p}><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7z" /><circle cx="12" cy="12" r="3" /></I>,
  Mic: (p) => <I {...p}><rect x="9" y="2" width="6" height="11" rx="3" /><path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v3" /></I>,
  Clock: (p) => <I {...p}><circle cx="12" cy="12" r="10" /><path d="M12 6v6l4 2" /></I>,
  Layers: (p) => <I {...p}><path d="m12 2 9 5-9 5-9-5z" /><path d="m3 12 9 5 9-5" /><path d="m3 17 9 5 9-5" /></I>,
  Calendar: (p) => <I {...p}><rect x="3" y="4" width="18" height="18" rx="2" /><path d="M16 2v4M8 2v4M3 10h18" /></I>,
  Bell: (p) => <I {...p}><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /><path d="M10 21a2 2 0 0 0 4 0" /></I>,
  Newspaper: (p) => <I {...p}><path d="M4 22h14a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0V9h4" /><path d="M18 14h-8M18 18h-8M18 6h-4M18 10h-4" /></I>,
  Activity: (p) => <I {...p}><path d="M22 12h-4l-3 9L9 3l-3 9H2" /></I>
};

/* ---------- DEMO DATA · MRK ---------- */
const TICKER = {
  symbol: "MRK", market: "NYSE", flag: "🇺🇸",
  name: "Merck & Co., Inc.",
  sector: "Healthcare", sub: "Pharmaceuticals",
  lifecycle: "Mature Stable", lifecycleConfidence: 79,
  price: 111.38, change: 0.82, changePct: 0.74,
  currency: "USD", asOf: "May 15, 2026 · 19:25 CET", lastAnalysis: "14m ago",
  score_display: 58, score_c1c5: 58, score_with_agents: 63,
  sectorSweetSpot: { lo: 45, hi: 58, label: "Healthcare · Mature stable" },
  signal: {
    verdict: "BUY",
    pattern: "HC SWEET SPOT",
    confidence: 3,
    confLabel: T("HIGH"),
    desc: "Healthcare mature-stable score 45–58. The engine identifies this pattern with empirical magnitude documented across 6 cohorts (2012–2014, 2017–2019).",
    alpha3y: "+18.6pp",
    cagrAlpha: "+6.20pp",
    winRate: "6/6",
    pValue: "p<0.001"
  },
  layers: [
  { code: "C1", name: T("Accounting integrity"), icon: "Flask", pass: true, score: null, max: null,
    detail: "Beneish M-Score −2.41 · no manipulation detected",
    methodology: "Beneish M-Score detects accounting manipulation from 8 ratios (DSRI, GMI, AQI, SGI, DEPI, SGAI, LVGI, TATA). Threshold −1.78. Validated across >70 SEC cases.",
    drivers: [["M-Score", T("−2.41 (clean)")], ["DSRI", "1.04"], ["AQI", "0.98"], ["TATA", "−0.04"], [T("Threshold"), "−1.78"]] },
  { code: "C2", name: T("Bankruptcy risk"), icon: "Scale", pass: true, score: null, max: null,
    detail: "Altman Z = 4.2 · Ohlson P = 0.04",
    methodology: "Altman Z-Score (5 ratios) + Ohlson O-Score as cross-check. Z>3 = safe zone. P<0.5 = low probability of bankruptcy within 12 months.",
    drivers: [["Altman Z", T("4.2 (safe zone)")], ["Ohlson P", T("0.04 (low)")], ["WC/TA", "0.18"], ["RE/TA", "0.41"], ["EBIT/TA", "0.16"]] },
  { code: "C3", name: T("Business quality"), icon: "Gem", pass: null, score: 28, max: 35,
    detail: "Piotroski 7/9 · Moat 16/22 · ROIC 18.4%",
    methodology: "Combines Piotroski F-Score (9 binary signals across profitability/leverage/efficiency), Damodaran moat scoring and ROIC vs WACC. Weighted by persistence.",
    drivers: [["Piotroski F", "7/9"], [T("Moat (Damodaran)"), "16/22"], ["ROIC", "18.4%"], ["ROIC − WACC", "+11.2pp"], [T("Operating margin"), "31.5%"]] },
  { code: "C4", name: T("Valuation"), icon: "Tag", pass: null, score: 14, max: 25, warn: true,
    detail: "P/E 18.4× · DCF +12% upside · EV/EBITDA 14×",
    methodology: "Triangulation: relative multiples (P/E, EV/EBITDA, P/B) vs sector + 2-stage reverse DCF (Mauboussin). Adjusted for lifecycle stage.",
    drivers: [["P/E TTM", "18.4× (sector 19.2×)"], ["EV/EBITDA", "14.1×"], [T("DCF upside"), "+12.0%"], ["P/B", "5.2×"], [T("Earnings yield"), "5.4%"]] },
  { code: "C5", name: T("Debt structure"), icon: "Landmark", pass: null, score: 18, max: 25,
    detail: "EV/EBITDA 14× · Coverage 18× · ND/EBITDA 1.4×",
    methodology: "Assesses leverage (ND/EBITDA), debt-service capacity (interest coverage) and maturity ladder. Penalises short-term maturity concentration.",
    drivers: [[T("Net Debt/EBITDA"), "1.4×"], ["Interest coverage", "18×"], [T("Debt/Equity"), "0.62"], [T("Avg maturity"), T("8.4 years")], ["Credit rating", "AA-"]] },
  { code: "C6", name: "ATLAS · Intangibles", icon: "Home", pass: null, score: 12, max: 20,
    detail: "5 signals · solid moat · strong IP",
    methodology: "NLP + structured agent over 10-K filings. 5 dimensions: IP, switching costs, network effects, scale moat, brand. Same engine as the ATLAS agent, integrated as a layer.",
    drivers: [[T("IP / patents"), T("Strong (Keytruda 2028)")], [T("Switching cost"), T("Medium")], [T("Brand equity"), "8.4 / top 25%"], ["Network effects", "Low"], [T("Cost moat"), T("Medium-high")]] },
  { code: "C7", name: "HERMES · NLP", icon: "Wave", pass: null, score: 14, max: 30, warn: true,
    detail: "Management tone neutral-bullish · 3 earnings calls",
    methodology: "NLP over transcripts (Loughran-McDonald + in-house finetune). Compares prepared vs Q&A tone, hedging, guidance clarity. Last 3 calls weighted.",
    drivers: [[T("Prep tone"), T("+0.42 (bullish)")], ["Q&A tone", "+0.18 (neutral)"], ["Hedging", "low"], [T("Last 3 calls"), T("Bullish-neutral")], [T("Sector NLP avg"), "+0.12"]] }],

  agents: [
  { name: "FCF-MOM", v: "+5", d: "/15", sign: "pos", tag: "CFO yield expansion",
    full: T("Free Cash Flow Momentum"),
    methodology: "Detects FCF / CFO yield acceleration over 4–8 quarters. Signals margin expansion and pricing power.",
    drivers: [[T("FCF yield TTM"), "5.20%"], ["FCF yield (-1Y)", "4.45%"], [T("CAPEX/Sales"), "-12 bp QoQ"], [T("WC days"), "-3 d YoY"]] },
  { name: "GUIDANCE", v: "+3", d: "/15", sign: "pos", tag: "No FY25 cuts",
    full: T("Guidance Reliability"),
    methodology: "Evaluates the consistency of quarterly guidance and consensus revisions. Penalises recent cuts.",
    drivers: [[T("FY25 EPS guide"), "$8.10-8.25"], [T("Change vs Q1"), T("+0.05 (raise)")], ["Beat rate (8Q)", "7/8"], [T("Sell-side revisions 30d"), "+0.4%"]] },
  { name: "INSIDER", v: "−2", d: "/12", sign: "neg", tag: "Moderate selling",
    full: T("Insider Activity"),
    methodology: "SEC Form 4. Net buying / selling weighted by management-signal vs stock-options-vesting noise.",
    drivers: [[T("Net 90d"), "-$4.2M"], [T("Buyers/Sellers"), "1 / 4"], ["% executive signal", "38%"], ["Cluster activity", "No"]] },
  { name: "ATLAS", v: "+12", d: "/20", sign: "pos", tag: "Moat · 5 signals",
    full: T("ATLAS · Intangibles & Moat"),
    methodology: "NLP + structured over 10-K. Detects IP, switching costs, network effects, scale, brand. Damodaran moat scoring.",
    drivers: [[T("IP / patents"), T("Strong (Keytruda 2028)")], [T("Switching cost"), T("Medium")], [T("Brand equity"), "8.4 / top 25%"], ["Network effects", "Low"], [T("Cost moat"), T("Medium-high")]] },
  { name: "HERMES", v: "+14", d: "/30", sign: "pos", tag: "Bullish neutral",
    full: "HERMES · NLP earnings calls",
    methodology: "Sentiment + management tone over earnings-call transcripts (Loughran-McDonald + in-house finetune). Compares Q&A vs prepared remarks.",
    drivers: [[T("Prep tone"), T("+0.42 (bullish)")], ["Q&A tone", "+0.18 (neutral)"], ["Hedging frequency", "low"], [T("Last 3 calls"), T("Bullish-neutral")]] },
  { name: "CHRONOS", v: "+3", d: "/10", sign: "amb", tag: "Cycle neutral",
    full: T("CHRONOS · Cycle timing"),
    methodology: "Macro + sector cycle positioning (yield curve, PMI, sector rotation). Signals timing relative to the cycle.",
    drivers: [[T("Sector cycle"), T("Late-cycle defensive")], [T("Yield curve"), T("Steepening")], [T("Healthcare relative"), "+0.3 sigma"], [T("Earnings momentum"), "Neutral"]] },
  { name: "ARGOS", v: "+4", d: "/8", sign: "pos", tag: "Consensus buy",
    full: T("ARGOS · Consensus & flows"),
    methodology: "Wisdom of crowds: sell-side consensus + ETF flows + short interest. Filters pure-momentum noise.",
    drivers: [[T("Consensus rating"), T("Buy (4.1/5)")], [T("Avg target"), "$132 (+18%)"], [T("Short interest"), T("1.2% float")], [T("ETF inflows 30d"), "+$240M"]] }],

  dcf: {
    current: 111.38, intrinsic: 124.77, upside: 12.0,
    method: T("2-stage Mauboussin · WACC 7.2% · g 2.5%"),
    range: { lo: 90, hi: 150 }
  },
  stats: [
  { l: "Market cap", v: "$282.4B" },
  { l: "P/E (TTM)", v: "18.4×" },
  { l: "EV/EBITDA", v: "14.1×" },
  { l: "ROIC", v: "18.4%", g: true },
  { l: "FCF yield", v: "5.2%", g: true },
  { l: "Dividend yield", v: "2.94%" },
  { l: "Payout ratio", v: "42%" },
  { l: "Beta (5Y)", v: "0.41" },
  { l: T("GICS industry"), v: "Pharmaceuticals" },
  { l: T("Brand equity"), v: "8.4 · top 25%" },
  { l: T("Engine"), v: "v3.2" },
  { l: T("Analysis"), v: "248 ms" }],

  news: [
  { src: "Reuters", time: "2h", title: "Trump says unaware of plan to fire FDA commissioner Marty Makary" },
  { src: "Bloomberg", time: "5h", title: "How Hantavirus made Moderna's mRNA platform a target for Merck partnership talks" },
  { src: "WSJ", time: "1d", title: "Merck faces Gardasil trial after plaintiffs allege concealed side effects" },
  { src: "FT", time: "2d", title: "Keytruda subcutaneous formulation cleared for late-2026 launch in EU" }],

  alerts: [
  { kind: "up", text: "Score up +2 points vs previous analysis", meta: "14 days ago · 56 → 58" },
  { kind: "info", text: "Next earnings: July 18 · pre-market", meta: "Q2 2026 · estimated EPS $2.21" }]

};

/* ---------- TICKER SEARCH (autocomplete-ready) ---------- */
const TICKER_DB = [
  { sym: "MRK", name: "Merck & Co.", mkt: "NYSE" },
  { sym: "JNJ", name: "Johnson & Johnson", mkt: "NYSE" },
  { sym: "PFE", name: "Pfizer Inc.", mkt: "NYSE" },
  { sym: "LLY", name: "Eli Lilly", mkt: "NYSE" },
  { sym: "NVO", name: "Novo Nordisk", mkt: "NYSE" },
  { sym: "KO", name: "Coca-Cola", mkt: "NYSE" },
  { sym: "PEP", name: "PepsiCo", mkt: "NASDAQ" },
  { sym: "AAPL", name: "Apple Inc.", mkt: "NASDAQ" },
  { sym: "MSFT", name: "Microsoft", mkt: "NASDAQ" },
  { sym: "GOOGL", name: "Alphabet (A)", mkt: "NASDAQ" },
  { sym: "NESN.SW", name: "Nestlé", mkt: "SWX" },
  { sym: "ROG.SW", name: "Roche Holding", mkt: "SWX" },
  { sym: "NOVN.SW", name: "Novartis", mkt: "SWX" },
  { sym: "SAN.PA", name: "Sanofi", mkt: "EPA" },
  { sym: "ITX.MC", name: "Inditex", mkt: "BME" },
  { sym: "IBE.MC", name: "Iberdrola", mkt: "BME" },
  { sym: "BRK.B", name: "Berkshire Hathaway", mkt: "NYSE" },
  { sym: "V", name: "Visa", mkt: "NYSE" },
  { sym: "MA", name: "Mastercard", mkt: "NYSE" },
  { sym: "PG", name: "Procter & Gamble", mkt: "NYSE" }
];
// TODO: replace with /api/tickers/search?q= (DB)
async function searchTickers(q) {
  const Q = q.trim().toUpperCase();
  if (!Q) return [];
  return TICKER_DB.filter(t => t.sym.includes(Q) || t.name.toUpperCase().includes(Q)).slice(0, 8);
}

function TickerSearch() {
  const [q, setQ] = useState("");
  const [open, setOpen] = useState(false);
  const [results, setResults] = useState([]);
  const [active, setActive] = useState(0);
  const wrapRef = useRef(null);
  useEffect(() => {
    let alive = true;
    searchTickers(q).then(r => alive && setResults(r));
    return () => { alive = false; };
  }, [q]);
  useEffect(() => {
    const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);
  const showList = open && q.length > 0;
  const onKey = (e) => {
    if (!showList) return;
    if (e.key === "ArrowDown") { e.preventDefault(); setActive((active + 1) % Math.max(1, results.length)); }
    if (e.key === "ArrowUp") { e.preventDefault(); setActive((active - 1 + results.length) % Math.max(1, results.length)); }
    if (e.key === "Enter" && results[active]) { setQ(results[active].sym); setOpen(false); }
    if (e.key === "Escape") setOpen(false);
  };
  return (
    <div className="nav-search" ref={wrapRef}>
      <span className="icn"><Icon.Search size={14} /></span>
      <input
        placeholder={T("Search another ticker — JNJ, KO, NESN.SW…")}
        value={q}
        onChange={(e) => { setQ(e.target.value); setOpen(true); setActive(0); }}
        onFocus={() => setOpen(true)}
        onKeyDown={onKey}
      />
      <span className="kbd">⌘K</span>
      {showList && (
        <div className="ts-pop">
          {results.length === 0 && <div className="ts-empty">No matches in the database</div>}
          {results.map((t, i) => (
            <button key={t.sym} className={"ts-row " + (i === active ? T("active") : "")}
              onMouseEnter={() => setActive(i)}
              onMouseDown={(e) => { e.preventDefault(); setQ(t.sym); setOpen(false); }}
              type="button">
              <span className="ts-sym">{t.sym}</span>
              <span className="ts-name">{t.name}</span>
              <span className="ts-mkt">{t.mkt}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
}

/* ---------- USER MENU ---------- */
function UserMenu() {
  const [open, setOpen] = useState(false);
  const [me, setMe] = useState(null);
  const ref = useRef(null);

  useEffect(() => {
    const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener("mousedown", onDoc);
    return () => document.removeEventListener("mousedown", onDoc);
  }, []);

  // Fetch real user info
  useEffect(() => {
    if (!window.vqApi || !vqApi.isLoggedIn()) return;
    vqApi.me()
      .then(u => setMe(u))
      .catch(() => setMe(null));
  }, []);

  // If not logged → show Sign in button
  if (!window.vqApi || !vqApi.isLoggedIn()){
    return (
      <button className="btn btn-secondary btn-sm" onClick={() => document.getElementById("modal-login").showModal()}>
        {T("Sign in")}
      </button>
    );
  }

  const email = (me && me.email) || "...";
  const userPart = email.split("@")[0].split(".")[0];
  const displayName = userPart.charAt(0).toUpperCase() + userPart.slice(1);
  const avatarLetter = (email[0] || "?").toUpperCase();
  const plan = (me && me.plan) || "—";
  const queriesUsed = (me && me.queries_this_month) || 0;
  const queriesLimit = (me && me.queries_limit) || "∞";

  const handleSignOut = () => {
    if (window.vqApi) vqApi.logout("/");
  };

  const items = [
    { l: T("My account"), s: T("Profile & details"), href: "/cuenta.html" },
    { l: "Watchlist", s: T("Manage tickers"), href: "/home.html#watchlist" },
    { l: T("History"), s: `${queriesUsed} analyses this month`, href: "/home.html#history" },
    { l: T("Plan & billing"), s: `Plan ${plan}`, href: "/billing.html" },
    { l: T("Help"), s: T("Docs & support"), href: "/legal/metodologia.html" }
  ];

  return (
    <div className="um-wrap" ref={ref}>
      <button className="user-chip" aria-label={T("Account")} onClick={() => setOpen(!open)}>
        <span className="av">{avatarLetter}</span>
        <span className="um-mail-inline">{displayName}</span>
        <Icon.ChevronDown size={11} />
      </button>
      {open && (
        <div className="um-pop">
          <div className="um-head">
            <div className="um-av">{avatarLetter}</div>
            <div className="um-meta">
              <div className="um-name">{displayName}</div>
              <div className="um-mail">{email}</div>
              <div className="um-plan">Plan <b>{plan}</b> · {queriesUsed}/{queriesLimit} {T("this month")}</div>
            </div>
          </div>
          <div className="um-list">
            {items.map(it => (
              <a key={it.l} href={it.href} className="um-row">
                <span className="um-row-l">{it.l}</span>
                <span className="um-row-s">{it.s}</span>
              </a>
            ))}
          </div>
          <button className="um-logout" type="button" onClick={handleSignOut}>{T("Sign out")}</button>
        </div>
      )}
    </div>
  );
}

/* ---------- NAV ---------- */
function Nav({ theme, setTheme }) {
  return (
    <nav>
      <a className="logo" href="landing.html" aria-label="Validq">
        <span className="wm">valid<span className="q">q</span></span>
        <span className="ver">v3 · analysis</span>
      </a>
      <TickerSearch />
      <div className="nav-right">
        <button className="icn-btn" aria-label={T("Toggle theme")} onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
          {theme === "dark" ? <Icon.Sun size={16} /> : <Icon.Moon size={16} />}
        </button>
        <UserMenu />
      </div>
    </nav>);}

/* ---------- TICKER HEADER ---------- */
function TickerHeader({ t, watch, setWatch }) {
  const pos = t.change >= 0;
  return (
    <>
      <a className="crumb" href="home.html">
        <Icon.ArrowLeft size={13} /> {T("Back to feed")}
      </a>
      <div className="tk-head">
        <div className="tk-id">
          <div className="tk-id-row">
            <span className="tk-flag">{t.flag}</span>
            <span className="tk-sym">{t.symbol}</span>
            <span className="tk-mkt">{t.market}</span>
            <span className="tk-name">· {t.name}</span>
          </div>
          <div className="tk-meta">
            <span className="pip"><Icon.Layers size={11} />{t.sector} · {t.sub}</span>
            <span className="pip lc"><Icon.Activity size={11} />{t.lifecycle}</span>
            <span className="pip"><Icon.Cpu size={11} />Engine v3.2</span>
            <span className="pip"><Icon.Clock size={11} />{T("cache 14m")}</span>
          </div>
        </div>
        <div className="tk-px">
          <div className="tk-pn">${t.price.toFixed(2)}</div>
          <div className="tk-px-row">
            <span className="ts">{t.currency} · {t.asOf} · {T("analysed")} {t.lastAnalysis}</span>
          </div>
          <div className="tk-act-row">
            <button className={"btn btn-secondary"} onClick={() => setWatch(!watch)}>
              <span className="icn"><Icon.Star size={14} /></span>
              {watch ? T("In watchlist") : T("Add to watchlist")}
            </button>
            <button className="btn btn-secondary" onClick={() => {
              // Re-analyse: force a fresh analysis bypassing cache
              const url = new URL(window.location.href);
              url.searchParams.set("refresh", "1");
              window.location.href = url.toString();
            }}>
              <span className="icn"><Icon.Refresh size={14} /></span>
              {T("Re-analyse")}
            </button>
            <button className="btn btn-primary" onClick={() => {
              const dlg = document.getElementById("modal-analyse-another");
              if (dlg) {
                dlg.showModal();
                setTimeout(() => {
                  const inp = document.getElementById("aa-ticker-input");
                  if (inp){ inp.value = ""; inp.focus(); }
                }, 50);
              }
            }}>
              {T("Analyse another")}
            </button>
          </div>
        </div>
      </div>
    </>);

}

/* ---------- SCORE HERO (Pro) ---------- */
function ScoreHero({ t }) {
  const score = t.score_c1c5;
  const tone = score >= 55 ? "" : score >= 40 ? "amber" : "red";
  const ss = t.sectorSweetSpot;
  // Sweet spot is calibrated on C1-C5 pure score, so the pin uses score_c1c5
  const pinScore = t.score_c1c5;
  const pinPct = pinScore; // axis 0..100
  const ssLeft = ss.lo;
  const ssWidth = ss.hi - ss.lo;
  return (
    <div className="card score-hero">
      <div className="sh-num">
        <div className={"sh-doto " + tone}>{score}</div>
        <div className="sh-denom">/ 100</div>
        <div className="sh-zone">{T("Sweet spot")}</div>
        <div className="sh-pure-tag" style={{ marginTop: 8 }}>C1–C5 pure</div>
      </div>
      <div className="sh-detail">
        <div className="sh-detail-h">
          <span className="t">{T("Composite score")}</span>
          <span className="s">{T("calibrated against SSRN paper 6735820")}</span>
        </div>
        <div className="sh-bar-block">
          <div className="sh-bar-h sh-bar-h-stack">
            <div className="sh-bar-eyebrow">{T("Sector sweet-spot")}</div>
            <div className="sh-bar-row">
              <b className="sh-bar-label">{ss.label}</b>
              <span className={"sh-bar-r " + (ss.position === "in" ? "pos" : "amb")}>
                {T("range")} {ss.lo}–{ss.hi} · {ss.position === "in" ? "inside ✓" : ss.position === "above" ? "above range ⚠" : ss.position === "below" ? "below range ⚠" : "—"}
              </span>
            </div>
          </div>
          <div className="sh-bar">
            <div className="sh-bar-zone" style={{ left: ssLeft + "%", width: ssWidth + "%" }}>
              <span className="sh-bar-zone-lbl" style={{ left: 0 }}>{ss.lo}</span>
              <span className="sh-bar-zone-lbl" style={{ right: 0 }}>{ss.hi}</span>
            </div>
            <div className="sh-bar-pin" style={{ left: pinPct + "%" }} data-v={pinScore}></div>
          </div>
          <div className="sh-bar-tk">
            <span>0</span><span>20</span><span>40</span><span>60</span><span>80</span><span>100</span>
          </div>
        </div>
        {/* S12_FIX: bloque sh-split eliminado (redundante con resto del hero) */}
      </div>
      {/* S12_MNEMO_UI_v1: MNEMO narrative block */}
      {t.narrative && t.narrative.available && (t.narrative.textEs || t.narrative.textEn) && (
        <div style={{
          gridColumn: "1 / -1",
          width: "100%",
          marginTop: 20,
          paddingTop: 20,
          borderTop: "1px solid rgba(255,255,255,0.06)"
        }}>
          <div style={{
            borderLeft: "3px solid var(--green, #00E87A)",
            paddingLeft: 18,
            marginBottom: 16
          }}>
            <div style={{
              fontSize: 9,
              letterSpacing: ".18em",
              color: "var(--green, #00E87A)",
              textTransform: "uppercase",
              fontWeight: 600,
              marginBottom: 6
            }}>{T("Investment thesis")}</div>
            <div style={{
              fontSize: 16,
              color: "var(--text-primary, #E8EAE6)",
              fontWeight: 400,
              fontStyle: "italic",
              lineHeight: 1.5,
              fontFamily: "'Unbounded', sans-serif"
            }}>{'"' + t.narrative.oneLiner + '"'}</div>
          </div>
          <p style={{
            fontSize: 13.5,
            color: "var(--text-secondary, #B8BCB4)",
            lineHeight: 1.7,
            margin: 0,
            textAlign: "justify"
          }}>{t.narrative.textEs || t.narrative.textEn}</p>
          <div style={{
            marginTop: 18,
            paddingTop: 12,
            borderTop: "1px solid rgba(255,255,255,0.05)",
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            fontSize: 10,
            color: "var(--text-tertiary, #777)",
            letterSpacing: ".04em",
            textTransform: "uppercase",
            flexWrap: "wrap",
            gap: 8
          }}>
            <div style={{display: "flex", gap: 14, flexWrap: "wrap"}}>
              <span>{T("Plan")} · {t.narrative.plan}</span>
              <span>·</span>
              <span>{t.narrative.textEs ? "ES" : "EN"}</span>
              <span>·</span>
              <span>{t.narrative.lifecycleStage}</span>
              {t.narrative.degraded && (
                <>
                  <span>·</span>
                  <span style={{color: "#FFB347"}}>FALLBACK</span>
                </>
              )}
            </div>
            <div style={{display: "flex", alignItems: "center", gap: 6}}>
              <span>{T("Powered by")}</span>
              <span style={{
                color: "var(--green, #00E87A)",
                fontWeight: 600,
                letterSpacing: ".08em"
              }}>MNEMO</span>
            </div>
          </div>
        </div>
      )}
    </div>);

}

/* ---------- SIGNAL ---------- */
function SignalCard({ t }) {
  const s = t.signal;
  const dots = Array.from({ length: 3 }).map((_, i) =>
  <span key={i} className={"d " + (i < s.confidence ? "on" : "")} />
  );
  return (
    <div className="card compra-stripe">
      <div className="signal-h">
        <div className="signal-title">
          <div className="eyebrow g">{T("Signal · quantitative verdict")}</div>
          <div className="signal-name">
            <span className="verdict">{T(s.verdict)}</span>
            <span className="sep">·</span>
            <span className="pattern">{T(s.pattern)}</span>
          </div>
        </div>
        <div className="signal-conf">
          {T("confidence")}
          <span className="conf-dots">{dots}</span>
          <span className="conf-label">{s.confLabel}</span>
        </div>
      </div>
      <p className="signal-desc">{s.desc}</p>
      <div className="alpha-grid">
        <div className="alpha-cell">
          <div className="lbl">{T("Alpha 3y")}</div>
          <div className="val">{s.alpha3y}</div>
          <div className="sub">{T("vs SPX · 36 months")}</div>
        </div>
        <div className="alpha-cell">
          <div className="lbl">{T("CAGR α")}</div>
          <div className="val">{s.cagrAlpha}</div>
          <div className="sub">{T("annualised")}</div>
        </div>
        <div className="alpha-cell">
          <div className="lbl">{T("Win rate")}</div>
          <div className="val neutral">{s.winRate}</div>
          <div className="sub">{T("positive cohorts")}</div>
        </div>
        <div className="alpha-cell">
          <div className="lbl">{T("Significance")}</div>
          <div className="val neutral">{s.pValue}</div>
          <div className="sub">95% CI · t-test</div>
        </div>
      </div>
      <div className="signal-foot">
        <Icon.Info size={11} />
        {T("Backtest 2012–2014, 2017–2019 · 4 107 ticker-years")} ·{" "}
        <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=6735820" target="_blank" rel="noopener">SSRN paper 6735820 ↗</a>
      </div>
    </div>);

}

/* ---------- LAYERS C1-C7 ---------- */
function LayersCard({ layers }) {
  const [open, setOpen] = useState(null);
  return (
    <div className="card">
      <div className="card-h">
        <div>
          <div className="eyebrow">Analysis · layers C1–C7</div>
          <div className="title" style={{ marginTop: 6 }}>{T("Score breakdown")}</div>
        </div>
        <div className="sub">{T("7 academic models · click for detail")}</div>
      </div>
      <div className="layers">
        {layers.map((L) => {
          const isPass = L.pass === true;
          const pct = L.score != null ? Math.round(L.score / L.max * 100) : 100;
          const cls = "layer layer-btn " + (isPass ? "pass " : "") + (L.warn ? "warn " : "");
          const Ico = Icon[L.icon] || Icon.Layers;
          return (
            <button key={L.code} className={cls} type="button" onClick={() => setOpen(L)}>
              <div className="layer-icn"><Ico size={16} /></div>
              <div className="layer-body">
                <div className="layer-row1">
                  <span className="layer-code">{L.code}</span>
                  <span className="layer-name">{L.name}</span>
                </div>
                <div className="layer-detail">{L.detail}</div>
              </div>
              <div className="layer-side">
                {isPass ?
                <span className="layer-pass-pill"><Icon.Check size={10} /> PASS</span> :

                <>
                    <div className="layer-bar">
                      <div className="layer-bar-fill" style={{ width: pct + "%" }} />
                    </div>
                    <span className="layer-val">{L.score}/{L.max}</span>
                  </>
                }
                <span className="layer-chev"><Icon.ChevronDown size={12} style={{transform:"rotate(-90deg)"}}/></span>
              </div>
            </button>);

        })}
      </div>
      {open && <LayerModal layer={open} onClose={() => setOpen(null)} />}
    </div>);

}

function LayerModal({ layer, onClose }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
  }, [onClose]);
  const L = layer;
  const isPass = L.pass === true;
  const Ico = Icon[L.icon] || Icon.Layers;
  const pct = L.score != null ? Math.round(L.score / L.max * 100) : 100;
  return (
    <div className="vq-modal-wrap" onClick={onClose}>
      <div className="vq-modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <button className="vq-modal-close" onClick={onClose} aria-label={T("Close")}><Icon.X size={16} /></button>
        <div className="vq-modal-h">
          <div className="eyebrow g">{T("Layer")} {L.code}</div>
          <div className="vq-modal-title" style={{display:"flex",alignItems:"center",gap:12}}>
            <span className={"layer-icn " + (isPass?"pass":"") + (L.warn?" warn":"")} style={{width:36,height:36,borderRadius:10,background:"var(--bg-elevated)",border:"1px solid var(--border)",display:"inline-flex",alignItems:"center",justifyContent:"center"}}><Ico size={16}/></span>
            {L.name}
          </div>
          <div className="vq-modal-sub">
            {isPass ? <b className="pos">PASS</b> : <>{T("Current score")} <b className={L.warn?"amb":"pos"}>{L.score}/{L.max}</b> ({pct}%)</>}
          </div>
        </div>
        <div className="vq-modal-section">
          <div className="eyebrow">{T("Methodology")}</div>
          <p className="vq-modal-text">{L.methodology}</p>
        </div>
        <div className="vq-modal-section">
          <div className="eyebrow">{T("Current drivers")}</div>
          <div className="vq-driver-list">
            {L.drivers.map(([k, v], i) => (
              <div className="vq-driver" key={i}>
                <span className="k">{k}</span>
                <span className="v">{v}</span>
              </div>
            ))}
          </div>
        </div>
        <div className="vq-modal-foot">
          <Icon.Info size={11} />
          {T("C1–C5 are the \"pure validation\" layers (SSRN paper 6735820). C6–C7 add NLP and intangibles signals. Recomputed after a new 10-K/10-Q or manual re-analysis.")}
        </div>
      </div>
    </div>
  );
}

/* ---------- AGENTS HIVE ---------- */
function AgentsCard({ agents }) {
  const [open, setOpen] = useState(null);
  return (
    <div className="card">
      <div className="card-h">
        <div>
          <div className="eyebrow">{T("Agent hive")}</div>
          <div className="title" style={{ marginTop: 6 }}>{T("Tactical & NLP signals")}</div>
        </div>
        <div className="sub">{T("7 agents · click for detail")}</div>
      </div>
      <div className="agents">
        {agents.map((a) =>
        <button className="agent agent-btn" key={a.name} onClick={() => setOpen(a)} type="button">
            <div className="agent-h">
              <div className="agent-name">{a.name}</div>
              <div className={"agent-sign " + a.sign}>
                {a.sign === "pos" ? "▲" : a.sign === "neg" ? "▼" : "≈"}
              </div>
            </div>
            <div className="agent-val">
              <span className={"agent-v " + a.sign}>{a.v}</span>
              <span className="agent-d">{a.d}</span>
            </div>
            <div className="agent-tag">{a.tag}</div>
            <div className="agent-cta">{T("view detail")} <Icon.ChevronDown size={10} style={{transform:"rotate(-90deg)"}}/></div>
          </button>
        )}
      </div>
      {open && <AgentModal agent={open} onClose={() => setOpen(null)} />}
    </div>);

}

function AgentModal({ agent, onClose }) {
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("keydown", onKey);
    document.body.style.overflow = "hidden";
    return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
  }, [onClose]);
  const a = agent;
  return (
    <div className="vq-modal-wrap" onClick={onClose}>
      <div className="vq-modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <button className="vq-modal-close" onClick={onClose} aria-label={T("Close")}><Icon.X size={16} /></button>
        <div className="vq-modal-h">
          <div className="eyebrow g">{T("Agent")}</div>
          <div className="vq-modal-title">{a.full}</div>
          <div className="vq-modal-sub">{a.name} · {T("current score")} <b className={a.sign === "neg" ? "neg" : a.sign === "amb" ? "amb" : "pos"}>{a.v}{a.d}</b></div>
        </div>
        <div className="vq-modal-section">
          <div className="eyebrow">{T("Methodology")}</div>
          <p className="vq-modal-text">{a.methodology}</p>
        </div>
        <div className="vq-modal-section">
          <div className="eyebrow">{T("Current drivers")}</div>
          <div className="vq-driver-list">
            {a.drivers.map(([k, v], i) => (
              <div className="vq-driver" key={i}>
                <span className="k">{k}</span>
                <span className="v">{v}</span>
              </div>
            ))}
          </div>
        </div>
        <div className="vq-modal-foot">
          <Icon.Info size={11} />
          {T("Updated 14m ago · EODHD + SEC EDGAR data. Re-evaluated every 24h or after a new 10-Q/8-K.")}
        </div>
      </div>
    </div>
  );
}

/* ---------- DCF ---------- */
function DcfCard({ d }) {
  // Build a scale that ALWAYS includes current price + intrinsic + range
  const allVals = [d.range.lo, d.range.hi, d.current, d.intrinsic].filter(v => v && v > 0);
  const rawMin = Math.min(...allVals);
  const rawMax = Math.max(...allVals);
  // Add 8% padding on each side so pins never touch the edges
  const padding = (rawMax - rawMin) * 0.08 || 1;
  const scaleMin = rawMin - padding;
  const scaleMax = rawMax + padding;
  const pct = (v) => Math.max(2, Math.min(98, (v - scaleMin) / (scaleMax - scaleMin) * 100));
  const lo = d.range.lo, hi = d.range.hi;
  // Verdict: is the stock cheap or expensive vs intrinsic?
  const isOvervalued = d.current > d.intrinsic;
  return (
    <div className="card">
      <div className="card-h">
        <div>
          <div className="eyebrow">{T("Valuation · reverse-DCF (Mauboussin)")}</div>
          <div className="title" style={{ marginTop: 6 }}>{T("Intrinsic value vs current price")}</div>
        </div>
        <div className="sub">{d.method}</div>
      </div>
      <div className="dcf-grid">
        <div className="dcf-cell">
          <div className="lbl">{T("Current price")}</div>
          <div className="v">${d.current.toFixed(2)}</div>
          <div className="sub">USD · spot</div>
        </div>
        <div className="dcf-cell">
          <div className="lbl">{T("Intrinsic value")}</div>
          <div className="v g">${d.intrinsic.toFixed(2)}</div>
          <div className="sub">2-stage · WACC 7.2%</div>
        </div>
        <div className="dcf-cell">
          <div className="lbl">{T("Upside")}</div>
          <div className={"v " + (d.upside >= 0 ? "g" : "neg")}>{d.upside >= 0 ? "+" : ""}{d.upside.toFixed(1)}%</div>
          <div className="sub">{T("12-month horizon")}</div>
        </div>
      </div>
      <div className="dcf-bar-wrap">
        <div className="dcf-bar-h">
          <span className="t">{T("Valuation band")} {isOvervalued ? T("· overvalued") : T("· undervalued")}</span>
          <span className="r">bear ${lo.toFixed(0)} — bull ${hi.toFixed(0)}</span>
        </div>
        <div className="dcf-bar-track">
          {/* Range zone bear-bull as a shaded band */}
          <div className="dcf-range-band" style={{ left: pct(lo) + "%", width: (pct(hi) - pct(lo)) + "%" }}></div>
          {/* Current price pin - label BELOW */}
          <div className="dcf-pin dcf-pin-below" style={{ left: pct(d.current) + "%" }}>
            <span className="lbl">${d.current.toFixed(2)}</span>
            <span className="lbl-sub">{T("current")}</span>
          </div>
          {/* Intrinsic pin - label ABOVE */}
          <div className="dcf-pin intrinsic dcf-pin-above" style={{ left: pct(d.intrinsic) + "%" }}>
            <span className="lbl">${d.intrinsic.toFixed(2)}</span>
            <span className="lbl-sub">{T("fair value")}</span>
          </div>
        </div>
        <div className="dcf-bar-tk">
          <span>{T("undervalued")}</span><span>{T("fair")}</span><span>{T("expensive")}</span>
        </div>
      </div>
    </div>);

}

/* ---------- PRICE CHART (interactive) ---------- */
function PriceChart({ ticker, currentPrice }) {
  const ranges = ["1M", "3M", "6M", "1Y", "3Y", "5Y"];
  const [active, setActive] = useState("1Y");
  const [hover, setHover] = useState(null);
  const wrapRef = useRef(null);

  // Range config: days span + number of points
  const rangeCfg = {
    "1M": { days: 30,   n: 30,  vol: 0.6 },
    "3M": { days: 90,   n: 45,  vol: 0.9 },
    "6M": { days: 180,  n: 60,  vol: 1.1 },
    "1Y": { days: 365,  n: 80,  vol: 1.4 },
    "3Y": { days: 1095, n: 120, vol: 2.2 },
    "5Y": { days: 1825, n: 150, vol: 3.0 }
  };
  const cfg = rangeCfg[active] || rangeCfg["1Y"];

  // Deterministic series ending at currentPrice (so the chart "lands" on real price)
  const endPrice = currentPrice || 100;
  const pts = [];
  let y = endPrice * 0.78;  // start ~22% below current
  for (let i = 0; i < cfg.n; i++) {
    y += Math.sin(i * 0.42) * cfg.vol + Math.cos(i * 0.17) * (cfg.vol * 0.6) + (endPrice * 0.22 / cfg.n);
    pts.push(y);
  }
  // Force last point to be the real current price
  if (pts.length) pts[pts.length - 1] = endPrice;

  const now = new Date();
  const startDate = new Date(now.getTime() - cfg.days * 86400000);
  const dayStep = cfg.days / Math.max(1, pts.length - 1);
  const dates = pts.map((_, i) => new Date(startDate.getTime() + i * dayStep * 86400000));
  const min = Math.min(...pts), max = Math.max(...pts);
  const W = 800, H = 200, pad = 8;
  const norm = (v) => H - pad - (v - min) / (max - min) * (H - pad * 2);
  const stepX = (W - pad * 2) / (pts.length - 1);
  const xAt = (i) => pad + i * stepX;
  const path = pts.map((v, i) => (i === 0 ? "M" : "L") + xAt(i).toFixed(2) + " " + norm(v).toFixed(2)).join(" ");
  const area = path + ` L ${xAt(pts.length - 1)} ${H} L ${pad} ${H} Z`;

  const onMove = (e) => {
    const wrap = wrapRef.current;
    if (!wrap) return;
    const r = wrap.getBoundingClientRect();
    const x = e.clientX - r.left;
    const ratio = Math.max(0, Math.min(1, x / r.width));
    const i = Math.round(ratio * (pts.length - 1));
    setHover({ i, xPct: (xAt(i) / W) * 100, yPct: (norm(pts[i]) / H) * 100 });
  };
  const onLeave = () => setHover(null);
  const fmtDate = (d) => d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
  const hovDate = hover ? dates[hover.i] : null;
  const hovPx = hover ? pts[hover.i] : null;

  return (
    <div className="card chart-wrap" style={{ padding: 0 }}>
      <div className="card-h" style={{ padding: "20px 22px 0" }}>
        <div>
          <div className="eyebrow">Price</div>
          <div className="title" style={{ marginTop: 6 }}>{T("1 year · USD")}</div>
        </div>
        <div className="sub">{T("EODHD · adjusted close")}</div>
      </div>
      <div className="chart-tabs" style={{ paddingTop: 14 }}>
        {ranges.map((r) =>
        <button key={r} className={"chart-tab " + (r === active ? T("active") : "")} onClick={() => setActive(r)}>
            {r}
          </button>
        )}
        <div style={{ flex: 1 }} />
        {hover && (
          <div className="chart-hov-inline">
            <span className="d">{fmtDate(hovDate)}</span>
            <span className="p">${hovPx.toFixed(2)}</span>
          </div>
        )}
      </div>
      <div className="chart-svg-wrap" ref={wrapRef} onMouseMove={onMove} onMouseLeave={onLeave}>
        <svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none">
          <defs>
            <linearGradient id="grad" x1="0" x2="0" y1="0" y2="1">
              <stop offset="0%" stopColor="var(--green)" stopOpacity="0.20" />
              <stop offset="100%" stopColor="var(--green)" stopOpacity="0" />
            </linearGradient>
          </defs>
          {[0.25, 0.5, 0.75].map((g, i) =>
          <line key={i} x1={pad} x2={W - pad} y1={H * g} y2={H * g}
          stroke="var(--border)" strokeWidth="1" strokeDasharray="2 4" />
          )}
          <path d={area} fill="url(#grad)" />
          <path d={path} fill="none" stroke="var(--green)" strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round" />
          {hover && (
            <g>
              <line x1={xAt(hover.i)} x2={xAt(hover.i)} y1={pad} y2={H - pad} stroke="var(--text-secondary)" strokeWidth="1" strokeDasharray="3 3" opacity="0.6" />
              <circle cx={xAt(hover.i)} cy={norm(pts[hover.i])} r="5" fill="var(--green)" stroke="var(--bg-card)" strokeWidth="2" />
            </g>
          )}
        </svg>
        {hover && (
          <div className="chart-tip" style={{ left: `calc(${hover.xPct}% + 14px)`, top: `calc(${hover.yPct}% - 12px)` }}>
            <div className="chart-tip-d">{fmtDate(hovDate)}</div>
            <div className="chart-tip-p">${hovPx.toFixed(2)}</div>
          </div>
        )}
      </div>
      <div className="chart-axis">
        <span>May 2025</span><span>Aug</span><span>Nov</span><span>Feb 2026</span><span>May 2026</span>
      </div>
    </div>);

}

/* ---------- SIDEBAR cards ---------- */
function LifecycleCard({ t }) {
  const stages = [T("Start-up"), T("Young Growth"), T("High Growth"), T("Mature G."), T("Mature Stable"), T("Decline")];
  const activeIdx = 4;
  return (
    <div className="side-card">
      <div className="side-h">
        <div className="title"><Icon.Activity size={13} /> {T("Lifecycle")}</div>
        <div className="meta">Damodaran</div>
      </div>
      <div className="lc-ladder">
        {stages.map((s, i) =>
        <div key={s} className={"lc-step " + (i === activeIdx ? "active" : i < activeIdx ? "past" : "")}>
            <div className="lc-dot" />
            <div className="lc-lbl">{s}</div>
          </div>
        )}
      </div>
      <div className="lc-desc">
        Established company with stable growth, predictable dividend
        and low operational volatility.
      </div>
      <div className="lc-conf">
        <span className="lc-conf-l">{T("Confidence")}</span>
        <span className="lc-conf-bar"><span className="lc-conf-fill" style={{ width: t.lifecycleConfidence + "%" }} /></span>
        <span className="lc-conf-v">{t.lifecycleConfidence}% · {T("HIGH")}</span>
      </div>
    </div>);

}

function StatsCard({ stats }) {
  return (
    <div className="side-card">
      <div className="side-h">
        <div className="title"><Icon.Layers size={13} /> {T("Key stats")}</div>
        <div className="meta">EODHD · TTM</div>
      </div>
      <div className="stat-list">
        {stats.map((s) =>
        <div className="stat-row" key={s.l}>
            <span className="stat-l">{s.l}</span>
            <span className={"stat-v " + (s.g ? "g" : "")}>{s.v}</span>
          </div>
        )}
      </div>
    </div>);

}

function publisherFromUrl(url){
  if (!url) return "";
  try {
    const host = new URL(url).hostname.replace(/^www\./, "");
    // Map common domains to friendly publisher names
    const map = {
      "finance.yahoo.com": "Yahoo Finance",
      "yahoo.com": "Yahoo Finance",
      "reuters.com": "Reuters",
      "bloomberg.com": "Bloomberg",
      "cnbc.com": "CNBC",
      "marketwatch.com": "MarketWatch",
      "investing.com": "Investing.com",
      "seekingalpha.com": "Seeking Alpha",
      "fool.com": "Motley Fool",
      "barrons.com": "Barron's",
      "ft.com": "Financial Times",
      "wsj.com": "WSJ",
      "benzinga.com": "Benzinga",
      "thestreet.com": "TheStreet",
      "businesswire.com": "Business Wire",
      "prnewswire.com": "PR Newswire",
      "globenewswire.com": "GlobeNewswire"
    };
    return map[host] || host.split(".")[0].charAt(0).toUpperCase() + host.split(".")[0].slice(1);
  } catch(e){ return ""; }
}

function newsDateStr(iso){
  if (!iso) return "";
  try {
    const d = new Date(iso);
    const ms = Date.now() - d.getTime();
    const h = Math.floor(ms / 3600000);
    if (h < 1) return Math.max(1, Math.floor(ms/60000)) + "m ago";
    if (h < 24) return h + "h ago";
    const days = Math.floor(h / 24);
    if (days < 7) return days + "d ago";
    return d.toLocaleDateString("en-GB", { day: "numeric", month: "short" });
  } catch(e){ return ""; }
}

function NewsCard({ news }) {
  return (
    <div className="side-card">
      <div className="side-h">
        <div className="title"><Icon.Newspaper size={13} /> {T("News")}</div>
        <div className="meta">{news.length} {news.length === 1 ? T("article") : T("articles")}</div>
      </div>
      <div className="news-list">
        {news.length === 0 ? (
          <div style={{padding:"14px 12px",textAlign:"center",fontFamily:"'DM Mono',monospace",fontSize:11,color:"var(--text-tertiary)"}}>
            No recent news.
          </div>
        ) : (
          news.map((n, i) => {
            const publisher = publisherFromUrl(n.url);
            const when = newsDateStr(n.date);
            return (
              <a key={i} href={n.url || "#"} target="_blank" rel="noopener noreferrer" className="news-item news-item-link">
                <div className="news-meta">
                  {publisher && <span className="news-src">{publisher}</span>}
                  {publisher && when && <span className="news-dot">·</span>}
                  {when && <span className="news-time">{when}</span>}
                </div>
                <div className="news-title">{n.title}</div>
              </a>
            );
          })
        )}
      </div>
    </div>
  );
}

function AlertsCard({ alerts }) {
  const count = alerts.length;
  return (
    <div className="side-card">
      <div className="side-h">
        <div className="title"><Icon.Bell size={13} /> {T("Alerts")}</div>
        <div className="meta">{count > 0 ? `${count} ${T("active")}` : T("no alerts")}</div>
      </div>
      <div className="alert-list">
        {count === 0 ? (
          <div style={{padding:"14px 12px",textAlign:"center",fontFamily:"'DM Mono',monospace",fontSize:11,color:"var(--text-tertiary)",letterSpacing:".04em",lineHeight:1.5}}>
            {T("No alerts yet.")}<br/>
            <span style={{fontSize:10}}>{T("Earnings + score deltas will appear here.")}</span>
          </div>
        ) : (
          alerts.map((a, i) => {
            const icon = a.kind === "up" ? <Icon.Trending size={12} /> :
                         a.kind === "down" ? <Icon.Trending size={12} style={{transform:"rotate(180deg)"}} /> :
                         <Icon.Calendar size={12} />;
            const cls = "alert-item " + (a.kind === "up" ? "up" : a.kind === "down" ? "down" : "");
            return (
              <div key={i} className={cls}>
                <div className="alert-icn">{icon}</div>
                <div className="alert-text">
                  {a.text}
                  {a.meta && <span className="meta">{a.meta}</span>}
                </div>
              </div>
            );
          })
        )}
      </div>
    </div>
  );
}

/* ---------- DISCLAIMER ---------- */
function Disclaimer() {
  return (
    <div className="disclaimer">
      <span className="icn"><Icon.Info size={14} /></span>
      <span>
        Validq is a quantitative analysis tool. It does not constitute personalised
        financial advice or a buy/sell recommendation. Investment decisions are the
        user's responsibility. MiFID II compliant. Data sourced from EODHD/SEC EDGAR.
        Backtest results do not guarantee future outcomes.{" "}
        <a href="methodology.html">{T("Methodology")}</a> · <a href="terms.html">{T("Terms")}</a>.
      </span>
    </div>);

}

/* ---------- APP ---------- */
/* =========================================================
   MAPPING: backend /analyze/{ticker} → t (component shape)
   ========================================================= */
function flagForMarket(ticker){
  const m = {".US":"🇺🇸",".SW":"🇨🇭",".MC":"🇪🇸",".PA":"🇫🇷",".DE":"🇩🇪",".L":"🇬🇧",".MI":"🇮🇹",".AS":"🇳🇱",".CO":"🇩🇰",".BR":"🇧🇪",".HE":"🇫🇮",".OL":"🇳🇴",".ST":"🇸🇪",".LS":"🇵🇹",".HK":"🇭🇰",".TO":"🇨🇦",".AX":"🇦🇺",".SA":"🇧🇷",".T":"🇯🇵",".NS":"🇮🇳"};
  if (!ticker) return "🌐";
  for (const k in m){ if (ticker.endsWith(k)) return m[k]; }
  return "🌐";
}
function currencyFor(ticker){
  const m = {".US":"USD",".SW":"CHF",".MC":"EUR",".PA":"EUR",".DE":"EUR",".L":"GBP",".MI":"EUR",".AS":"EUR",".CO":"DKK",".BR":"EUR",".HE":"EUR",".OL":"NOK",".ST":"SEK",".LS":"EUR",".HK":"HKD",".TO":"CAD",".AX":"AUD",".SA":"BRL",".T":"JPY",".NS":"INR"};
  if (!ticker) return "USD";
  for (const k in m){ if (ticker.endsWith(k)) return m[k]; }
  return "USD";
}
function symbolFromTicker(t){ return t ? t.split(".")[0] : ""; }
function marketCodeFromTicker(t){
  if (!t) return "";
  const ext = t.split(".")[1] || "";
  const map = {"US":"NYSE","SW":"SIX","MC":"BME","PA":"EPA","DE":"XETRA","L":"LSE","MI":"BIT","AS":"AMS","HK":"HKEX","TO":"TSX","AX":"ASX","T":"TSE","NS":"NSE"};
  return map[ext] || ext;
}
function timeAgoStr(iso){
  if (!iso) return "—";
  try {
    const ms = Date.now() - new Date(iso).getTime();
    const mins = Math.floor(ms/60000);
    if (mins<60) return mins + "m ago";
    const h = Math.floor(mins/60);
    if (h<24) return h + "h ago";
    const d = Math.floor(h/24);
    return d + "d ago";
  } catch(e){ return "—"; }
}
function parseSignal(sig){
  if (!sig) return {verdict:"REVIEW", pattern:""};
  const parts = sig.replace(/_/g," ").trim().split(/ +/);
  if (parts.length === 0) return {verdict:"REVIEW", pattern:""};
  const verdictMap = {
    "COMPRA":"BUY","COMPRA_CLARA":"BUY","BUY":"BUY",
    "EVITAR":"AVOID","AVOID":"AVOID","NO_INVERTIR":"AVOID",
    "ESPERAR":"WAIT","ESPERAR_MEJOR_PRECIO":"WAIT","WAIT":"WAIT",
    "REVISAR":"REVIEW","REVIEW":"REVIEW"
  };
  const first = parts[0].toUpperCase();
  return {
    verdict: verdictMap[first] || first,
    pattern: parts.slice(1).join(" ").toUpperCase()
  };
}

// Constants for layer/agent metadata
const LAYER_META = {
  "1": {code:"C1", name:T("Accounting integrity"), icon:"Flask", methodology:"Beneish M-Score detects accounting manipulation from 8 ratios (DSRI, GMI, AQI, SGI, DEPI, SGAI, LVGI, TATA). Threshold −1.78. Validated across >70 SEC cases."},
  "2": {code:"C2", name:T("Bankruptcy risk"),      icon:"Scale", methodology:"Altman Z-Score (5 ratios) + Ohlson O-Score as cross-check. Z>3 = safe zone. P<0.5 = low probability of bankruptcy within 12 months."},
  "3": {code:"C3", name:T("Business quality"),     icon:"Gem",   methodology:"Combines Piotroski F-Score (9 binary signals across profitability/leverage/efficiency), Damodaran moat scoring and ROIC vs WACC. Weighted by persistence."},
  "4": {code:"C4", name:T("Valuation"),            icon:"Tag",   methodology:"Triangulation: relative multiples (P/E, EV/EBITDA, P/B) vs sector + 2-stage reverse DCF (Mauboussin). Adjusted for lifecycle stage."},
  "5": {code:"C5", name:T("Debt structure"),       icon:"Landmark", methodology:"Assesses leverage (ND/EBITDA), debt-service capacity (interest coverage) and maturity ladder. Penalises short-term maturity concentration."}
};

const AGENT_META = {
  fcf_momentum:    {name:"FCF-MOM",  full:T("Free Cash Flow Momentum"),   max:15, methodology:"Detects FCF / CFO yield acceleration over 4–8 quarters. Signals margin expansion and pricing power."},
  guidance_tracker:{name:"GUIDANCE", full:T("Guidance Reliability"),      max:15, methodology:"Evaluates the consistency of quarterly guidance and consensus revisions. Penalises recent cuts."},
  insider_flow:    {name:"INSIDER",  full:T("Insider Activity"),          max:12, methodology:"SEC Form 4 + EODHD insider transactions. Net buying / selling weighted by management-signal vs stock-options-vesting noise."},
  atlas:           {name:"ATLAS",    full:T("ATLAS · Intangibles & Moat"),max:20, methodology:"NLP + structured over 10-K. Detects IP, switching costs, network effects, scale, brand. Damodaran moat scoring."},
  hermes:          {name:"HERMES",   full:"HERMES · NLP earnings calls",max:30, methodology:"Sentiment + management tone over earnings-call transcripts (Loughran-McDonald + in-house finetune). Compares Q&A vs prepared remarks."},
  sector_agent:    {name:"SECTOR",   full:T("Sector timing"),             max:10, methodology:"Sector cycle positioning + relative momentum. Adjusts for sector rotation flows."},
  chronos:         {name:"CHRONOS",  full:T("CHRONOS · Cycle timing"),    max:10, methodology:"Macro + sector cycle positioning (yield curve, PMI, sector rotation). Signals timing relative to the cycle."},
  clio:            {name:"CLIO",     full:T("CLIO · Business model"),     max:25, methodology:"LLM analysis of unit economics, customer concentration, scalability, competitive rivalry. CLIO Lite (Pro) vs CLIO Full (Trader+)."},
  argos:           {name:"ARGOS",    full:T("ARGOS · Consensus & flows"), max:8,  methodology:"Wisdom of crowds: sell-side consensus + ETF flows + short interest. Filters pure-momentum noise."}
};

function mapBackendToTicker(d){
  if (!d) return null;

  const sigParsed = parseSignal(d.action_signal && d.action_signal.signal);
  const confMap = {"ALTA":3,"HIGH":3,"MEDIA":2,"MEDIUM":2,"BAJA":1,"LOW":1};
  const conf = ((d.action_signal && d.action_signal.confidence) || "").toUpperCase();
  const confidence = confMap[conf] || 2;
  const confLabel = ({3:T("HIGH"),2:T("MEDIUM"),1:T("LOW")})[confidence] || T("MEDIUM");

  const ticker = d.ticker || "";
  const cur = currencyFor(ticker);

  // Sweet spot
  let sectorSweetSpot = d.sectorSweetSpot || null;
  if (!sectorSweetSpot && d.score_c1c5 !== undefined){
    const sc = d.score_c1c5;
    sectorSweetSpot = {
      lo: Math.max(0, Math.floor(sc * 0.85)),
      hi: Math.min(100, Math.ceil(sc * 1.15)),
      label: ((d.grupo||"").replace(/_/g," ") + " · " + (d.lifecycle_stage||"").replace(/_/g," ")) || "Sector range",
      _derived: true
    };
  }

  // Layers C1-C5
  const layers = ["1","2","3","4","5"].map(k => {
    const c = (d.capas || {})[k] || {};
    const meta = LAYER_META[k];
    const max = c.score_max;
    const score = c.score;
    return {
      code: meta.code, name: meta.name, icon: meta.icon,
      pass: c.passed === true,
      score: (score !== null && score !== undefined) ? Math.round(score) : null,
      max: max || null,
      warn: (score !== null && score !== undefined && max && score < max * 0.5),
      detail: (c.summary || "").substring(0, 90),
      methodology: meta.methodology,
      drivers: (c.corrections || []).slice(0,5).map(x => {
        if (Array.isArray(x)) return x;
        if (typeof x === "object" && x !== null){
          const k = x.descripcion || x.description || x.label || x.name || x.key || JSON.stringify(x).substring(0,40);
          const v = x.score !== undefined ? (x.score > 0 ? "+" : "") + x.score : (x.value || x.detail || "");
          return [String(k).substring(0,60), String(v).substring(0,30)];
        }
        return [String(x).substring(0,40), ""];
      })
    };
  });

  // Layers C6/C7 from ATLAS/HERMES agents (paper exposes 7 layers)
  if (d.agentes){
    if (d.agentes.atlas){
      layers.push({
        code:"C6", name:"ATLAS · Intangibles", icon:"Home",
        pass:null, score: Math.round(d.agentes.atlas.score || 0), max:20,
        warn: (d.agentes.atlas.score || 0) < 10,
        detail: (d.agentes.atlas.summary || "").substring(0,90),
        methodology: AGENT_META.atlas.methodology,
        drivers: []
      });
    }
    if (d.agentes.hermes){
      layers.push({
        code:"C7", name:"HERMES · NLP", icon:"Wave",
        pass:null, score: Math.round(d.agentes.hermes.score || 0), max:30,
        warn: (d.agentes.hermes.score || 0) < 15,
        detail: (d.agentes.hermes.summary || "").substring(0,90),
        methodology: AGENT_META.hermes.methodology,
        drivers: []
      });
    }
  }

  // Agents
  const agentKeys = ["fcf_momentum","guidance_tracker","insider_flow","atlas","hermes","sector_agent","chronos","clio","argos"];
  const agents = agentKeys.filter(k => d.agentes && d.agentes[k]).map(k => {
    const a = d.agentes[k];
    const meta = AGENT_META[k];
    const sc = a.score;
    const sign = sc > 0 ? "pos" : (sc < 0 ? "neg" : "amb");
    return {
      name: meta.name, full: meta.full,
      v: (sc > 0 ? "+" : "") + ((sc !== null && sc !== undefined) ? Math.round(sc) : "—"),
      d: "/" + meta.max,
      sign: sign,
      tag: (a.summary || "").substring(0,40),
      methodology: meta.methodology,
      drivers: (a.signals || []).slice(0,5).map(s => {
        if (Array.isArray(s)) return s;
        if (typeof s === "object" && s !== null){
          const k = s.descripcion || s.description || s.label || s.name || s.key || s.signal || JSON.stringify(s).substring(0,40);
          const v = s.score !== undefined ? (s.score > 0 ? "+" : "") + s.score : (s.value || s.detail || s.peso || "");
          return [String(k).substring(0,60), String(v).substring(0,30)];
        }
        return [String(s).substring(0,40), ""];
      })
    };
  });

  // DCF
  const dcf = d.dcf ? {
    current: d.dcf.current_price || (d.precio && d.precio.price) || 0,
    intrinsic: d.dcf.intrinsic_value_ps || 0,
    upside: d.dcf.margin_of_safety_pct || 0,
    method: "2-stage Mauboussin · WACC " + (d.dcf.wacc_pct ? d.dcf.wacc_pct.toFixed(1)+"%" : "—") + " · g " + (d.dcf.growth_rate_pct ? d.dcf.growth_rate_pct.toFixed(1)+"%" : "—"),
    range: { lo: d.dcf.intrinsic_value_bear || 0, hi: d.dcf.intrinsic_value_bull || 0 }
  } : null;

  // Stats
  const p = d.precio || {};
  const stats = [];
  if (p.pe_current) stats.push({l:"P/E (TTM)", v: p.pe_current.toFixed(1) + "×"});
  if (p.pe_fwd) stats.push({l:"P/E forward", v: p.pe_fwd.toFixed(1) + "×"});
  if (p.pffo_ttm) stats.push({l:"P/FFO (TTM)", v: p.pffo_ttm.toFixed(1) + "×"});
  if (p.rsi_14) stats.push({l:"RSI (14)", v: Math.round(p.rsi_14).toString()});
  if (p.range_52w_pos_pct !== undefined && p.range_52w_pos_pct !== null) {
    // Backend already returns a percentage (0-100), not a fraction
    let pos52 = Number(p.range_52w_pos_pct);
    if (pos52 <= 1) pos52 = pos52 * 100;  // safety: handle fraction format too
    stats.push({l:T("52w position"), v: pos52.toFixed(1) + "%"});
  }
  if (p.epv_ratio) stats.push({l:"EPV ratio", v: p.epv_ratio.toFixed(2) + "×"});
  if (d.gics_industry) stats.push({l:T("GICS industry"), v: d.gics_industry});
  if (d.brand_equity){
    const be = typeof d.brand_equity === "object" && d.brand_equity.score
      ? d.brand_equity.score.toFixed(1)
      : (typeof d.brand_equity === "number" ? d.brand_equity.toFixed(1) : "—");
    stats.push({l:T("Brand equity"), v: be, g: true});
  }
  stats.push({l:T("Engine"), v: "v6.1"});
  if (d.analysis_ms) stats.push({l:T("Analysis"), v: Math.round(d.analysis_ms) + " ms"});

  // Signal
  const signal = {
    verdict: sigParsed.verdict,
    pattern: sigParsed.pattern,
    confidence: confidence,
    confLabel: confLabel,
    desc: (d.action_signal && d.action_signal.rationale) || (d.action_signal && d.action_signal.headline) || (d.veredicto || ""),
    alpha3y: (d.action_signal && d.action_signal.alpha_3y) || "—",
    cagrAlpha: (d.action_signal && d.action_signal.cagr_alpha) || "—",
    winRate: (d.action_signal && d.action_signal.win_rate) || "—",
    pValue: (d.action_signal && d.action_signal.p_value) || "—"
  };

  return {
    symbol: symbolFromTicker(ticker),
    market: marketCodeFromTicker(ticker),
    flag: flagForMarket(ticker),
    name: d.company_name || ticker,
    sector: (d.grupo || "").replace(/_/g," "),
    sub: d.gics_industry || "",
    lifecycle: (d.lifecycle_stage || "").replace(/_/g," "),
    lifecycleConfidence: d.lifecycle_conf ? Math.round(d.lifecycle_conf * 100) : null,
    price: (p.price !== undefined && p.price !== null) ? p.price : 0,
    change: p.change || 0,
    changePct: p.change_pct || 0,
    currency: cur,
    asOf: d.generated_at ? new Date(d.generated_at).toLocaleString("en-GB", {day:"numeric",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"}) : "—",
    lastAnalysis: timeAgoStr(d.generated_at),
    score_display: Math.round(d.score_display || d.score || 0),
    score_c1c5: Math.round(d.score_c1c5 || 0),
    score_with_agents: Math.round(d.score || 0),
    sectorSweetSpot: sectorSweetSpot,
    signal: signal,
    layers: layers,
    agents: agents,
    dcf: dcf,
    stats: stats,
    news: d._news || [],
    alerts: d._alerts || [],
    // S12_MNEMO_UI_v1: MNEMO narrative
    narrative: d.narrative ? {
      available: d.narrative.data_available !== false,
      oneLiner: d.narrative.summary_one_liner || "",
      textEn: d.narrative.narrative_en || "",
      textEs: d.narrative.narrative_es || "",
      lifecycleStage: (d.narrative.lifecycle_focus_applied || "unknown").toUpperCase(),
      depth: d.narrative.depth || "lite",
      degraded: d.narrative.degraded === true,
      plan: (d.narrative.plan || "basic")
    } : null
  };
}

/* =========================================================
   APP · with real backend fetch + auth + login modal
   ========================================================= */
function App() {
  const [theme, setTheme] = useState(localStorage.getItem("vq_theme") || "dark");
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [watch, setWatch] = useState(false);
  const [tickerArg, setTickerArg] = useState("");
  const [isDemo, setIsDemo] = useState(false);
  // === D2-C-React polling ===
  const [retryAttempt, setRetryAttempt] = useState(0);

  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("vq_theme", theme);
  }, [theme]);

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const t = (params.get("t") || "").toUpperCase().trim();
    const demo = params.get("demo") === "1";
    setTickerArg(t);
    setIsDemo(demo);

    if (!t){
      setLoading(false);
      setError({status: 400, message: "Missing ?t=TICKER in URL"});
      return;
    }

    if (!demo && !vqApi.isLoggedIn()){
      setLoading(false);
      setError({status: 401, message: T("Sign in required")});
      setTimeout(() => {
        const dlg = document.getElementById("modal-login");
        if (dlg && !dlg.open) dlg.showModal();
      }, 100);
      return;
    }

    const forceRefresh = params.get("refresh") === "1";
    let endpoint = demo ? "/demo/" + encodeURIComponent(t) : "/analyze/" + encodeURIComponent(t);
    if (forceRefresh && !demo) endpoint += "?refresh=1";
    let cancelled = false;

    // === D2-C-React polling ===
    // Reintenta si Cloudflare corta a 100s con 524 (motor lento).
    // Cada reintento aprovecha caches intermedias del motor.
    const REQ_TIMEOUT_MS = 95000;   // <100s de Cloudflare
    const POLL_DELAY_MS  = 60000;  // S12_FIX_COST: 60s entre retries (era 30s)
    const MAX_RETRIES    = 2;       // S12_FIX_COST: 3 intentos max (era 7), reduce coste 4x

    async function fetchWithTimeout(path, ms){
      const ctl = new AbortController();
      const tid = setTimeout(() => ctl.abort(), ms);
      try { return await vqApi.get(path, {signal: ctl.signal}); }
      finally { clearTimeout(tid); }
    }

    async function loadMainWithRetry(){
      let lastErr;
      for (let attempt = 0; attempt <= MAX_RETRIES; attempt++){
        if (cancelled) throw new Error('cancelled');
        if (attempt > 0) setRetryAttempt(attempt);
        try {
          return await fetchWithTimeout(endpoint, REQ_TIMEOUT_MS);
        } catch(err){
          lastErr = err;
          const status = err && err.status;
          const isRetriable = (
            (err && (err.isAbort || err.isNetwork)) ||
            status === 524 || status === 502 || status === 503 || status === 504
          );
          if (attempt >= MAX_RETRIES || !isRetriable) throw err;
          console.info('[loadAll] retry', attempt+1, 'in', POLL_DELAY_MS/1000, 's:',
                        (err && err.message || '').slice(0, 80));
          await new Promise(r => setTimeout(r, POLL_DELAY_MS));
        }
      }
      throw lastErr;
    }

    async function loadAll(){
      try {
        const main = await loadMainWithRetry();
        if (cancelled) return;

        let news = [], alerts = [];
        if (!demo){
          try {
            const newsRes = await vqApi.get("/news/" + encodeURIComponent(t));
            news = newsRes.news || newsRes.items || [];
          } catch(e){ /* silent */ }
          try {
            const alertsRes = await vqApi.get("/ticker/" + encodeURIComponent(t) + "/alerts");
            alerts = alertsRes.alerts || alertsRes.items || [];
          } catch(e){ /* silent */ }
        }
        main._news = news;
        main._alerts = alerts;

        if (!cancelled){
          setData(main);
          setLoading(false);
        }

        if (!demo && vqApi.isLoggedIn()){
          try {
            const w = await vqApi.get("/watchlist");
            const items = w.watchlist || w.items || [];
            if (!cancelled) setWatch(items.some(it => (it.ticker || it) === t));
          } catch(e){ /* silent */ }
        }
      } catch(err){
        if (cancelled) return;
        setLoading(false);
        setError({status: err.status || 500, message: err.message || "Network error"});
      }
    }

    loadAll();
    return () => { cancelled = true; };
  }, []);

  async function toggleWatch(){
    if (!vqApi.isLoggedIn()){
      const dlg = document.getElementById("modal-login");
      if (dlg) dlg.showModal();
      return;
    }
    try {
      if (watch){
        await vqApi.del("/watchlist/" + encodeURIComponent(tickerArg));
        setWatch(false);
        vqApi.toast(tickerArg + " " + T("removed from watchlist"), "info");
      } else {
        // Backend expects 'ticker' as a query param, not JSON body
        await vqApi.post("/watchlist?ticker=" + encodeURIComponent(tickerArg), {});
        setWatch(true);
        vqApi.toast(tickerArg + " " + T("added to watchlist"), "ok");
      }
    } catch(err){
      vqApi.toast(err.message || T("Watchlist error"), "err");
    }
  }

  if (loading){
    return (
      <>
        <Nav theme={theme} setTheme={setTheme} />
        <div className="page" style={{display:"flex",justifyContent:"center",alignItems:"center",minHeight:"60vh"}}>
          <div style={{textAlign:"center"}}>
            <div style={{width:42,height:42,border:"3px solid var(--border)",borderTopColor:"var(--green)",borderRadius:"50%",animation:"vqspin 0.7s linear infinite",margin:"0 auto 18px"}}></div>
            <div style={{fontFamily:"'Unbounded',sans-serif",fontWeight:600,fontSize:16,letterSpacing:"-.2px",color:"var(--text-primary)"}}>{T("Analysing")} <span style={{color:"var(--green)"}}>{tickerArg}</span></div>
            <div style={{fontFamily:"'DM Mono',monospace",fontSize:11,color:"var(--text-tertiary)",letterSpacing:".04em",marginTop:6}}>{T("Engine v6.1 · 7 layers · 9 agents")}</div>
            {/* === D2-C-React polling === */}
            {retryAttempt > 0 && (
              <div style={{fontFamily:"'DM Mono',monospace",fontSize:11,color:"var(--text-tertiary)",letterSpacing:".04em",marginTop:14,maxWidth:340}}>
                Esto puede tardar 2–3 min para tickers nuevos.<br/>
                Intento {retryAttempt + 1}/7 · siguientes consultas serán instantáneas (caché 24h).
              </div>
            )}
          </div>
        </div>
        <style>{"@keyframes vqspin{to{transform:rotate(360deg)}}"}</style>
      </>
    );
  }

  if (error || !data){
    const msgMap = {
      400: [T("Bad request"), "Missing ?t=TICKER in URL"],
      401: [T("Sign in required"), T("You need an account to analyse tickers. Free plan includes 5 analyses per month.")],
      403: [T("Plan upgrade required"), (error && error.message) || T("This feature requires a higher plan")],
      404: [T("Ticker not found"), "We couldn't find analysis for \"" + tickerArg + "\". Check the symbol (e.g. MRK.US, ITX.MC, NESN.SW)."],
      429: [T("Monthly quota reached"), T("You've used all your analyses this month. Upgrade your plan to continue.")],
      500: [T("Temporary error"), (error && error.message) || T("Please try again")]
    };
    const arr = msgMap[(error && error.status) || 500] || msgMap[500];
    const title = arr[0], msg = arr[1];
    return (
      <>
        <Nav theme={theme} setTheme={setTheme} />
        <div className="page" style={{display:"flex",justifyContent:"center",alignItems:"center",minHeight:"60vh"}}>
          <div style={{textAlign:"center",maxWidth:520,padding:"0 24px"}}>
            <div style={{fontSize:48,color:"var(--amber)",marginBottom:8}}>⚠</div>
            <div style={{fontFamily:"'Unbounded',sans-serif",fontWeight:700,fontSize:24,letterSpacing:"-.6px",color:"var(--text-primary)"}}>{title}</div>
            <div style={{fontSize:14,color:"var(--text-secondary)",lineHeight:1.5,marginTop:8}}>{msg}</div>
            <div style={{display:"flex",gap:10,justifyContent:"center",marginTop:18}}>
              <a href="/" className="btn btn-secondary">{T("Back to home")}</a>
              {error && error.status === 401 ?
                <button className="btn btn-primary" onClick={() => document.getElementById("modal-login").showModal()}>{T("Sign in")}</button> :
                <button className="btn btn-primary" onClick={() => location.reload()}>{T("Retry")}</button>
              }
            </div>
          </div>
        </div>
      </>
    );
  }

  const t = mapBackendToTicker(data);
  if (!t) return <div>{T("Mapping error")}</div>;

  return (
    <>
      <Nav theme={theme} setTheme={setTheme} />
      <div className="page">
        <TickerHeader t={t} watch={watch} setWatch={toggleWatch} />
        <div className="tk-grid">
          <div className="tk-main">
            <ScoreHero t={t} />
            <SignalCard t={t} />
            <PriceChart ticker={t.symbol} currentPrice={t.price} />
            <LayersCard layers={t.layers} />
            <AgentsCard agents={t.agents} />
            {t.dcf && <DcfCard d={t.dcf} />}
          </div>
          <div className="tk-side">
            <LifecycleCard t={t} />
            <StatsCard stats={t.stats} />
            <NewsCard news={t.news || []} />
            <AlertsCard alerts={t.alerts || []} />
          </div>
        </div>
        <Disclaimer />
      </div>
    </>
  );
}

window.vqToggleLoginMode = (function(){
  let mode = "login";
  return function(){
    mode = mode === "login" ? "register" : "login";
    document.getElementById("login-title").textContent = mode === "login" ? T("Sign in") : "Create account";
    document.getElementById("login-submit").textContent = mode === "login" ? T("Sign in") : "Create account";
    document.getElementById("login-name-wrap").style.display = mode === "register" ? "block" : "none";
    document.getElementById("login-toggle-text").textContent = mode === "login" ? "No account yet? " : "Already have one? ";
    document.getElementById("login-toggle-btn").textContent = mode === "login" ? "Create account" : T("Sign in");
    document.getElementById("login-err").style.display = "none";
    return mode;
  };
})();

document.addEventListener("DOMContentLoaded", () => {
  const form = document.getElementById("login-form");
  if (form){
    form.addEventListener("submit", async (e) => {
      e.preventDefault();
      const btn = document.getElementById("login-submit");
      const err = document.getElementById("login-err");
      err.style.display = "none";
      btn.disabled = true;
      const orig = btn.textContent;
      btn.textContent = "...";
      try {
        const isReg = document.getElementById("login-name-wrap").style.display !== "none";
        const email = form.email.value.trim();
        const password = form.password.value;
        if (isReg){
          const name = form.name.value.trim();
          await vqApi.register(email, password, name || null);
        } else {
          await vqApi.login(email, password);
        }
        vqApi.toast(isReg ? "Account created!" : "Welcome!", "ok");
        document.getElementById("modal-login").close();
        setTimeout(() => location.reload(), 400);
      } catch(ex){
        err.textContent = ex.message || "Authentication error";
        err.style.display = "block";
        btn.disabled = false;
        btn.textContent = orig;
      }
    });
  }

  // Anti-autofill for ticker search
  setTimeout(() => {
    const inputs = document.querySelectorAll(".nav-search input, input[type='search']");
    inputs.forEach(el => {
      el.setAttribute("autocomplete", "off");
      el.setAttribute("data-form-type", "other");
      el.setAttribute("data-lpignore", "true");
      el.setAttribute("data-1p-ignore", "true");
      const clear = () => { if (el.value && document.activeElement !== el) el.value = ""; };
      clear();
      setTimeout(clear, 200);
      setTimeout(clear, 500);
    });
  }, 300);
});

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

