// Portfolio component — terminal/retro style
const { useState, useEffect, useRef, useMemo } = React;

// ============ DATA ============
const POSTS = [
  { date: "2026-04-18", title: "Auditando agent harnesses: cómo armé AgentPay y gané la hackathon de Anthropic", readTime: "12 min", words: 2840, tags: ["llms", "agents"] },
  { date: "2026-03-22", title: "Patrones de tool calling y MCP que usamos en Acelera", readTime: "8 min", words: 1620, tags: ["llms", "agents"] },
  { date: "2026-02-09", title: "RAG en producción: del prototipo al agente que entrega valor real", readTime: "15 min", words: 3210, tags: ["rag", "agents"] },
  { date: "2026-01-14", title: "Construyendo un agente que opera mi terminal sin volverse loco", readTime: "11 min", words: 2440, tags: ["llms", "rust"] },
  { date: "2025-12-02", title: "RAG es solo búsqueda vieja con esteroides (y eso está bien)", readTime: "6 min", words: 1180, tags: ["rag"] },
  { date: "2025-10-21", title: "Deployando Whisper.cpp en una Raspberry Pi 5", readTime: "9 min", words: 1890, tags: ["infra"] },
  { date: "2025-09-08", title: "Notas sobre vector DBs después de probar 6 en producción", readTime: "14 min", words: 3050, tags: ["rag", "infra"] },
  { date: "2025-07-30", title: "Cómo entrené un clasificador de spam con 200 ejemplos", readTime: "7 min", words: 1450, tags: ["llms"] },
  { date: "2025-06-12", title: "PyTorch vs JAX: el debate que ya nadie quiere tener", readTime: "10 min", words: 2120, tags: ["llms"] },
  { date: "2025-04-25", title: "Dataset cleaning es el 80% del trabajo y nadie lo quiere admitir", readTime: "5 min", words: 980, tags: ["rag"] },
];

const TAGS = ["llms", "rag", "infra", "rust"];

const PROJECTS = [
  { name: "agentpay", year: "2026", desc: "plugin de seguridad para claude code: intercepta operaciones financieras, detecta skills maliciosas y bloquea mcps. 1er puesto en anthropic & kaszek push to prod hackathon.", link: "github.com/MauroProto", tag: "[1er puesto]",
    longDesc: "AgentPay es un plugin de seguridad para Claude Code que ganó el 1er puesto en el Anthropic & KASZEK Push to Prod Hackathon 2026. La idea: cuando un agente IA empieza a operar con plata, instalar skills nuevas o conectarse a MCPs externos, el riesgo deja de ser teórico. AgentPay se mete entre el agente y el sistema operativo para bloquear lo que no debería pasar y dejar pasar lo que sí.",
    sections: [
      { title: "qué hace", body: "tres capas de defensa que corren en orden. (1) intercepción de operaciones financieras antes de ejecutarse para que el agente nunca pueda mover plata sin pasar por una validación explícita. (2) detección de skills maliciosas instaladas en el harness, comparando contra una lista de patrones conocidos y heurísticas de comportamiento. (3) bloqueo de MCPs sospechosos por reputación, firma o comportamiento sospechoso al conectarse." },
      { title: "stack", body: "claude code · plugins API · MCP · validación de skills · políticas declarativas. distribuido como plugin oficial vía marketplace." },
    ],
  },
  { name: "lain agent", year: "2026", desc: "chat web con ia para optimizar prompts, mantener conversaciones con contexto y analizar imágenes. next.js + react + typescript.", link: "lainagent.com", tag: "[activo]",
    longDesc: "Lain empezó como un chat wrapper y terminó siendo una plataforma con varios modos de razonamiento, sistema de memoria semántica entre conversaciones y un scaffolder que orquesta agentes en paralelo para generar proyectos completos. La idea es que el chat no se olvide de quién sos ni de qué hablaron antes, y que cuando le pidas un proyecto, lo arme.",
    sections: [
      { title: "cómo funciona", body: "el usuario elige entre cuatro modos: chat directo, exploración lateral, optimización iterativa de prompts, y un scaffolder que descompone el proyecto en tareas y las dispara en paralelo a workers especializados. cada modo cambia cómo se planifica y se entrega la respuesta." },
      { title: "memoria", body: "cada conversación genera embeddings vectoriales que se guardan en postgres. cuando hacés una pregunta nueva, el sistema busca por similitud coseno en memorias previas de todos tus chats e inyecta el contexto relevante automáticamente. tres tipos de memoria: dentro del chat, entre chats, y vinculada a generaciones del scaffolder." },
      { title: "stack", body: "next.js · typescript · openai · prisma · postgresql · zustand · nextauth · jwt." },
    ],
  },
  { name: "ml canvas", year: "2025", desc: "plataforma visual de machine learning con nodos interactivos. armás un pipeline arrastrando bloques y se ejecuta en python en el backend.", link: "github.com/MauroProto", tag: "[activo]",
    longDesc: "ML Canvas nació de una frustración: los notebooks de Jupyter son lineales y el código se vuelve un desorden rápido. La solución fue un canvas infinito donde cada paso del pipeline de ML es un nodo visual que se conecta con otros arrastrando cables. Subís un CSV, lo conectás a un nodo de preprocesamiento, ese a un modelo, y el resultado a un nodo de visualización. Todo se ejecuta en Python en el backend, pero el usuario nunca toca código si no quiere.",
    sections: [
      { title: "nodos", body: "trece tipos de nodos organizados en cuatro categorías. datos (subida de csv, split train/test). preprocesamiento (encoding, scaling, normalización, PCA, feature selection, imputación). modelos (logistic regression, random forest, svm, knn, xgboost, gradient boosting, decision tree, mlp, ridge, lasso, elastic net, svr, linear regression). visualización (curvas roc, matrices de confusión, histogramas y scatter plots con plotly)." },
      { title: "ejecución segura", body: "cuando se conecta el pipeline, el backend genera código python dinámicamente y lo corre en un sandbox con whitelist de librerías permitidas (scikit-learn, pandas, numpy, xgboost) y límites de memoria + timeout. los resultados se devuelven al frontend vía api y el canvas se autoguarda cada 30 segundos." },
      { title: "modos y asistente IA", body: "tres modos de trabajo: canvas visual con react flow, formulario paso a paso para principiantes, y editor python para usuarios avanzados. el chat con gpt tiene acceso al estado completo del canvas y memoria semántica entre sesiones para sugerir nodos, explicar métricas o modificar el pipeline." },
    ],
  },
  { name: "huella del fuego", year: "2024", desc: "dashboard de incendios en argentina con datos satelitales nasa firms, mapas interactivos y modelos de ml. 3ra mención en el concurso 'contar con datos'.", link: "github.com/MauroProto", tag: "[3ra mención]",
    longDesc: "Proyecto para la competencia de datos UdeSA. El objetivo era predecir riesgo de incendios forestales en las 21 provincias argentinas usando datos satelitales de NASA FIRMS. Entrené sobre un dataset de 2,184 muestras con 23 features (temperaturas, precipitaciones, NDVI, historial de focos, variables estacionales) y el modelo final fue un Random Forest que alcanzó 0.7947 de test accuracy y 0.790 de F1. El proyecto ganó la 3ra mención del concurso 'Contar con Datos'.",
    sections: [
      { title: "pipeline de datos", body: "el pipeline descarga focos de calor de la api de nasa firms (satélite viirs, últimas 48hs), los cruza con datos meteorológicos históricos y calcula features derivados: lags de 1-3 meses, medias móviles, ciclos estacionales sin/cos, ndvi promedio por provincia y densidad histórica de focos. la actualización corre cada 24hs vía github actions." },
      { title: "modelo y resultados", body: "comparé random forest, xgboost y lightgbm con validación temporal (train hasta 2022, test en 2023). random forest ganó con 0.7947 de accuracy. el output son 252 predicciones (21 provincias × 12 meses) con un nivel de riesgo (bajo, medio, alto). features más importantes: temperatura máxima promedio, focos históricos del mismo mes y ndvi." },
      { title: "dashboard", body: "el frontend tiene cuatro páginas: mapa con leaflet mostrando focos activos en tiempo real, mapa de riesgo por provincia con visualización hexagonal h3, gráficos de tendencias con plotly, y una tabla filtrable por provincia y mes. deployado en vercel con datos actualizándose automáticamente." },
    ],
  },
  { name: "neural network 3d", year: "2024", desc: "visualizador 3d interactivo para explicar capas, pesos, activaciones y forward propagation sobre mnist. three.js + pytorch.", link: "github.com/MauroProto", tag: "[activo]",
    longDesc: "Quería entender visualmente qué pasa adentro de una red neuronal cuando clasifica un dígito. Entrené un MLP en PyTorch con arquitectura 784→128→64→10 sobre MNIST y Fashion-MNIST. Después exporté los pesos y construí un visualizador 3D con Three.js donde cada neurona es una esfera que se ilumina según su activación, y las conexiones entre neuronas son líneas cuya opacidad refleja el peso de la conexión.",
    sections: [
      { title: "training", body: "el modelo se entrena en pytorch con soporte metal (mac) y cuda. en mnist alcanza ~98% accuracy y en fashion-mnist 0.789 de f1. el script de training exporta los pesos como json plano que el frontend parsea para hacer inferencia directamente en el browser sin backend." },
      { title: "visualización", body: "dibujás un número en un canvas html, la imagen se downscalea a 28×28 y pasa por el forward pass del modelo en javascript. en cada capa, las activaciones determinan el color y tamaño de cada neurona en la escena 3d. para no matar el rendimiento, las conexiones renderizadas se limitan a 25k (la capa 784→128 tiene 100k+ conexiones, así que se muestran las top por peso absoluto). cámara con orbit controls de three.js." },
    ],
  },
  { name: "badger", year: "2025", desc: "copilot de seguridad para apps web con ia. escanea repos de github y detecta riesgos de auth, secrets, endpoints de ia, supabase y supply chain. vercel zero to agent.", link: "github.com/MauroProto", tag: "[activo]",
    longDesc: "Badger es un copilot de seguridad pensado para apps web creadas con asistencia de IA, donde el código se genera rápido y no siempre se revisa con la misma rigurosidad que un desarrollo manual. Escanea un repositorio de GitHub sin ejecutar código y detecta riesgos típicos de stacks modernos: configuraciones de auth débiles, secrets expuestos, endpoints de IA mal asegurados, problemas de Supabase y dependencias del supply chain.",
    sections: [
      { title: "escaneo", body: "el usuario conecta un repo de github y badger corre un análisis estático sobre el código sin ejecutarlo. identifica patrones de riesgo y devuelve un reporte priorizado por severidad, con sugerencias accionables y links al lugar exacto del repo donde está el problema." },
      { title: "stack", body: "static analysis · github api · reglas heurísticas para auth, secrets y endpoints de IA · cobertura específica para supabase y supply chain. construido durante el programa vercel zero to agent." },
    ],
  },
  { name: "guard", year: "2026", desc: "supply-chain security cli en go para proyectos pnpm y github actions. integración con claude code para revisar dependencias, workflows, mcps y cambios riesgosos antes de prod.", link: "github.com/MauroProto/guard", tag: "[activo]",
    longDesc: "Guard responde una sola pregunta antes de que un cambio de dependencias o CI entre al repositorio: ¿podemos confiar en este cambio? Es una CLI escrita en Go que combina cinco chequeos distintos en un solo comando: posture de repositorio, revisión de lockfile y dependencias, hardening de workflows, validación de políticas con excepciones, y output machine-readable pensado para CI y automatización.",
    sections: [
      { title: "chequeos", body: "la cli toma el pnpm-lock.yaml y los workflows de .github/ y les aplica una cadena de reglas: paquetes sin 2fa, scripts de postinstall sospechosos, permisos excesivos en github actions, triggers riesgosos como pull_request_target, refs de actions sin pin, y compara los hallazgos contra políticas declaradas. el output es json + sarif para integrarse con github checks o codeql, más un formato legible en terminal." },
      { title: "plugin de claude code", body: "además de la cli headless, guard distribuye un plugin para claude code via marketplace.json. dentro de sesiones de edición el plugin corre scans focalizados (review-pr, baseline, policy lint) sin reemplazar a la cli, que queda como fuente de verdad para ci. arquitectura híbrida: cli first-class para pipelines, plugin para iteración interactiva." },
      { title: "instalación", body: "go install, script curl-pipe, o directamente desde el marketplace de claude code. requiere go 1.23+ para compilar desde fuente. distribución con binarios pre-compilados para macos, linux y windows." },
    ],
  },
  { name: "synthetic", year: "2025", desc: "generador local de datasets sintéticos de q&a desde pdfs. chunking semántico, evaluación con llms y exportación lista para fine-tuning.", link: "github.com/MauroProto", tag: "[activo]",
    longDesc: "Synthetic es una herramienta local para generar datasets sintéticos de Q&A a partir de PDFs. Toma uno o varios documentos, los parte en chunks usando segmentación semántica (no por número fijo de tokens), y le pide a un LLM que genere pares pregunta-respuesta para cada chunk. Después corre una pasada de evaluación con LLM para filtrar pares de baja calidad y exporta el dataset en un formato listo para fine-tuning.",
    sections: [
      { title: "pipeline", body: "chunking semántico → generación de q&a con llm → evaluación automática para filtrar pares débiles → exportación en formato listo para fine-tuning. cada paso es independiente y configurable, así podés cambiar el modelo de generación o de evaluación sin tocar el resto." },
      { title: "uso", body: "todo corre localmente, así que sirve para datos sensibles que no querés mandar a apis externas durante el procesamiento. soporta múltiples pdfs en una sola corrida y permite resumir corridas anteriores si interrumpiste el proceso." },
    ],
  },
  { name: "discord llm bot", year: "2024", desc: "bot opensource para discord con múltiples providers llm, memoria unificada entre chat y voz, elevenlabs stt/tts, streaming de respuestas y soporte mcp.", link: "github.com/MauroProto", tag: "[activo]",
    longDesc: "Un bot open-source para Discord que funciona como puerta de entrada a múltiples providers LLM (OpenAI, Anthropic, etc.) desde el chat o desde un canal de voz. La memoria es unificada: lo que se conversa por texto está disponible en voz y al revés, así una conversación puede saltar de un canal a otro sin perder contexto.",
    sections: [
      { title: "voz y streaming", body: "speech-to-text y text-to-speech con elevenlabs para que la voz suene natural. las respuestas se streamean al canal a medida que el modelo las genera, en lugar de esperar a tener el bloque completo. el usuario empieza a leer/escuchar la respuesta apenas el modelo arranca." },
      { title: "mcp y multi-provider", body: "soporte mcp para extender las capacidades del bot conectándolo con herramientas externas. los providers llm son intercambiables (openai, anthropic, etc.), así una misma conversación puede mezclar modelos según el contexto sin que el usuario tenga que pensar en eso." },
    ],
  },
];

const NOW = [
  { what: "Trabajando en", detail: "Acelera — software factory de IA: agentes, arneses de agentes y productos digitales a medida" },
  { what: "Leyendo", detail: "papers de agent harnesses, evaluación de agentes y patrones de RAG en producción" },
  { what: "Aprendiendo", detail: "cursando la Maestría en IA en la Universidad de San Andrés" },
  { what: "Construyendo", detail: "AgentPay y nuevas integraciones de seguridad para harnesses de agentes" },
  { what: "Ubicación", detail: "Buenos Aires, AR · trabajando remoto con equipos globales" },
];

const USES = {
  hardware: [
    "MacBook Pro como laptop principal para Acelera",
    "Workstation con GPU para entrenar modelos de ML/DL on-prem",
    "Teclado HHKB Professional Hybrid Type-S",
    "Monitor LG UltraFine 4K 27\"",
  ],
  software: [
    "Editor: Cursor + Claude Code para pair programming con agentes",
    "Terminal: Ghostty con tmux y zsh + starship",
    "Lenguajes: Python, TypeScript / JavaScript, SQL, Go",
    "ML/AI stack: PyTorch, scikit-learn, XGBoost, LangChain, LangGraph, MCP",
    "Notas: Obsidian con vault sincronizado por git",
  ],
};

const SOCIALS = [
  { label: "github", handle: "@MauroProto", url: "github.com/MauroProto" },
  { label: "linkedin", handle: "in/mauroprotocassina", url: "linkedin.com/in/mauroprotocassina" },
  { label: "email", handle: "contacto@mauroproto.com", url: "mailto:contacto@mauroproto.com" },
];

// ============ THEMES ============
const THEMES = {
  magenta: {
    bg: "#1a1625",
    fg: "#e8e3f0",
    dim: "#8a8398",
    accent: "#ff2bbd",
    accentSoft: "rgba(255, 43, 189, 0.15)",
    accentText: "#0a0a0f",
    border: "#2a2435",
  },
  phosphor: {
    bg: "#0a1410",
    fg: "#c8e8d0",
    dim: "#6a8870",
    accent: "#33ff66",
    accentSoft: "rgba(51, 255, 102, 0.12)",
    accentText: "#0a1410",
    border: "#1a2820",
  },
  amber: {
    bg: "#1a1208",
    fg: "#f0d8a8",
    dim: "#8a7050",
    accent: "#ffaa33",
    accentSoft: "rgba(255, 170, 51, 0.12)",
    accentText: "#1a1208",
    border: "#2a2018",
  },
  cream: {
    bg: "#f4ede0",
    fg: "#1a1208",
    dim: "#7a6850",
    accent: "#c2410c",
    accentSoft: "rgba(194, 65, 12, 0.12)",
    accentText: "#f4ede0",
    border: "#d8cdb5",
  },
};

// ============ ASCII HELPERS ============
function AsciiBars({ char = "|", count = 80, color }) {
  const bars = useMemo(() => {
    let s = "";
    for (let i = 0; i < count; i++) {
      s += Math.random() > 0.15 ? char : " ";
    }
    return s;
  }, [count, char]);
  return <span style={{ color, letterSpacing: "0.05em", whiteSpace: "nowrap", overflow: "hidden", display: "block" }}>{bars}</span>;
}

function Divider({ color, char = ":" }) {
  return <div style={{ color, opacity: 0.6, lineHeight: 1, marginTop: 8, marginBottom: 8, overflow: "hidden", whiteSpace: "nowrap" }}>{char.repeat(200)}</div>;
}

// ============ TYPING EFFECT ============
function useTyping(text, speed = 40, delay = 0) {
  const [out, setOut] = useState("");
  const [done, setDone] = useState(false);
  useEffect(() => {
    setOut("");
    setDone(false);
    let i = 0;
    const start = setTimeout(() => {
      const id = setInterval(() => {
        i++;
        setOut(text.slice(0, i));
        if (i >= text.length) {
          clearInterval(id);
          setDone(true);
        }
      }, speed);
    }, delay);
    return () => clearTimeout(start);
  }, [text, speed, delay]);
  return [out, done];
}

// ============ KEY SOUND ============
function useKeySound(enabled) {
  const ctxRef = useRef(null);
  return (volume = 0.05) => {
    if (!enabled) return;
    try {
      if (!ctxRef.current) ctxRef.current = new (window.AudioContext || window.webkitAudioContext)();
      const ctx = ctxRef.current;
      if (ctx.state === "suspended") ctx.resume();
      const osc = ctx.createOscillator();
      const gain = ctx.createGain();
      osc.type = "square";
      osc.frequency.value = 600 + Math.random() * 400;
      gain.gain.value = volume;
      gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + 0.05);
      osc.connect(gain);
      gain.connect(ctx.destination);
      osc.start();
      osc.stop(ctx.currentTime + 0.05);
    } catch (e) {}
  };
}

// ============ NEURAL NET (decorative + hidden-layer nodes click → projects) ============
function NeuralNet({ theme, onProjectClick, projects }) {
  const LAYERS = [3, 5, 4, 2];
  const W = 480, H = 180;
  const PAD_X = 36, PAD_Y = 24;
  const [tick, setTick] = useState(0);
  const [hover, setHover] = useState(null); // {layer, idx, projectIdx}

  useEffect(() => {
    const id = setInterval(() => setTick((t) => t + 1), 60);
    return () => clearInterval(id);
  }, []);

  const nodes = useMemo(() => {
    const arr = [];
    LAYERS.forEach((count, li) => {
      const x = PAD_X + (li / (LAYERS.length - 1)) * (W - 2 * PAD_X);
      for (let i = 0; i < count; i++) {
        const y = PAD_Y + ((i + 0.5) / count) * (H - 2 * PAD_Y);
        arr.push({ layer: li, idx: i, x, y });
      }
    });
    return arr;
  }, []);

  // hidden nodes (layers 1 and 2) → mapped to projects in order
  const hiddenNodes = useMemo(() => nodes.filter((n) => n.layer === 1 || n.layer === 2), [nodes]);
  const projectMap = useMemo(() => {
    const map = new Map();
    hiddenNodes.forEach((n, i) => {
      if (i < projects.length) map.set(`${n.layer}-${n.idx}`, i);
    });
    return map;
  }, [hiddenNodes, projects]);

  const stepsPerLayer = 60;
  const cycleLen = (LAYERS.length - 1) * stepsPerLayer;
  const cycleT = tick % cycleLen;
  const activeLayer = Math.floor(cycleT / stepsPerLayer);
  const layerProgress = (cycleT % stepsPerLayer) / stepsPerLayer;

  const edges = useMemo(() => {
    const e = [];
    for (let li = 0; li < LAYERS.length - 1; li++) {
      const from = nodes.filter((n) => n.layer === li);
      const to = nodes.filter((n) => n.layer === li + 1);
      for (const a of from) for (const b of to) e.push({ a, b, layer: li });
    }
    return e;
  }, [nodes]);

  const labels = ["x", "h\u2081", "h\u2082", "\u0177"];

  const isHidden = (n) => n.layer === 1 || n.layer === 2;
  const projectIdx = (n) => projectMap.get(`${n.layer}-${n.idx}`);
  const hoveredProject = hover != null ? projects[hover.projectIdx] : null;

  return (
    <div style={{ width: "100%", maxWidth: 540, position: "relative" }}>
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: "block", overflow: "visible" }}>
        {/* layer labels */}
        {LAYERS.map((_, li) => {
          const x = PAD_X + (li / (LAYERS.length - 1)) * (W - 2 * PAD_X);
          return (
            <text key={li} x={x} y={H - 4} textAnchor="middle" fill={theme.dim}
              style={{ fontFamily: "inherit", fontSize: 10, letterSpacing: "0.05em", opacity: 0.6 }}>
              {labels[li]}
            </text>
          );
        })}

        {/* edges */}
        {edges.map((e, i) => {
          const isActive = e.layer === activeLayer;
          return (
            <line key={i}
              x1={e.a.x} y1={e.a.y} x2={e.b.x} y2={e.b.y}
              stroke={isActive ? theme.accent : theme.dim}
              strokeWidth={isActive ? 0.6 : 0.4}
              opacity={isActive ? 0.32 : 0.10} />
          );
        })}

        {/* pulse dots traveling on the active layer */}
        {edges.filter((e) => e.layer === activeLayer).map((e, i) => {
          const px = e.a.x + (e.b.x - e.a.x) * layerProgress;
          const py = e.a.y + (e.b.y - e.a.y) * layerProgress;
          return <circle key={i} cx={px} cy={py} r={2} fill={theme.accent} />;
        })}

        {/* nodes */}
        {nodes.map((n, i) => {
          const justFired = n.layer === activeLayer + 1 && layerProgress > 0.85;
          const firing = n.layer === activeLayer && layerProgress < 0.15;
          const lit = justFired ? 2 : firing ? 1 : 0;
          const pIdx = projectIdx(n);
          const interactive = pIdx != null;
          const isHovered = hover && hover.layer === n.layer && hover.idx === n.idx;
          const r = isHovered ? 8.5 : 7;
          return (
            <g key={i}
              style={{ cursor: interactive ? "pointer" : "default" }}
              onMouseEnter={() => interactive && setHover({ layer: n.layer, idx: n.idx, projectIdx: pIdx })}
              onMouseLeave={() => setHover(null)}
              onClick={(e) => { if (interactive) { e.stopPropagation(); onProjectClick(pIdx); } }}>
              {interactive && <circle cx={n.x} cy={n.y} r={14} fill="transparent" />}
              <circle cx={n.x} cy={n.y} r={r}
                fill={lit === 2 ? theme.accent : "transparent"}
                stroke={(lit > 0 || isHovered) ? theme.accent : theme.fg}
                strokeWidth={isHovered ? 1.8 : lit > 0 ? 1.6 : 1}
                opacity={lit === 0 && !isHovered ? 0.6 : 1} />
              {lit === 2 && (
                <circle cx={n.x} cy={n.y} r={11} fill="none"
                  stroke={theme.accent} strokeWidth={0.8} opacity={0.4}>
                  <animate attributeName="r" values="7;14" dur="0.5s" />
                  <animate attributeName="opacity" values="0.6;0" dur="0.5s" />
                </circle>
              )}
            </g>
          );
        })}

      </svg>
    </div>
  );
}

// ============ HEADER ============
function SiteHeader({ theme }) {
  const [name, nameDone] = useTyping("mauro proto", 70, 200);
  return (
    <header style={{ marginBottom: 32 }}>
      <div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 24, marginBottom: 12 }}>
        <h1 style={{
          margin: 0,
          fontSize: 40,
          fontWeight: 700,
          letterSpacing: "-0.02em",
          color: theme.fg,
          lineHeight: 1.1,
          whiteSpace: "nowrap",
          flexShrink: 0,
        }}>
          {name}{!nameDone && <Cursor color={theme.accent} />}
        </h1>
        <span style={{ color: theme.dim, fontSize: 13, fontVariantNumeric: "tabular-nums", whiteSpace: "nowrap", textAlign: "right" }}>
          v2026.05 · buenos aires
        </span>
      </div>
      <p style={{
        margin: 0,
        color: theme.accent,
        fontSize: 14,
        letterSpacing: "0.02em",
      }}>
        ai/ml engineer <span style={{ color: theme.dim }}>· writing &amp; building things</span>
      </p>
    </header>
  );
}

// ============ SECTIONS ============
function Home({ theme, playKey, onProjectClick }) {
  const [bioText] = useTyping("construyo agentes, rag pipelines y modelos ml end-to-end. 1er puesto en el anthropic & kaszek hackathon con agentpay.", 18, 600);

  return (
    <div>
      <div style={{
        display: "flex", alignItems: "center", justifyContent: "center",
        padding: "8px 0 16px",
        marginBottom: 20,
        borderBottom: `1px dashed ${theme.border}`,
      }}>
        <NeuralNet
          theme={theme}
          onProjectClick={onProjectClick}
          projects={PROJECTS}
        />
      </div>
      <p style={{ color: theme.fg, lineHeight: 1.7, marginBottom: 32, maxWidth: "70ch", fontSize: 16 }}>
        {bioText}<Cursor color={theme.fg} />
      </p>

      <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: "8px 24px", marginBottom: 40 }}>
        {SOCIALS.map((s) => (
          <React.Fragment key={s.label}>
            <span style={{ color: theme.dim }}>{s.label}:</span>
            <a href={"https://" + s.url} onClick={(e) => { e.preventDefault(); playKey(); }} style={linkStyle(theme)}>{s.handle}</a>
          </React.Fragment>
        ))}
      </div>

      <Divider color={theme.accent} />

      <h2 style={h2Style(theme)}>posts recientes</h2>
      <Divider color={theme.accent} />
      <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
        {POSTS.slice(0, 5).map((p) => (
          <PostRow key={p.title} post={p} theme={theme} playKey={playKey} />
        ))}
      </div>
    </div>
  );
}

function PostRow({ post, theme, playKey }) {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "120px 1fr", gap: 20, alignItems: "baseline" }}>
      <span style={{ color: theme.dim, fontVariantNumeric: "tabular-nums" }}>{post.date}</span>
      <a href="#" onClick={(e) => { e.preventDefault(); playKey(); }} style={{ ...linkStyle(theme), color: theme.fg }}>
        {post.title}
      </a>
    </div>
  );
}

function Blog({ theme, playKey }) {
  return (
    <div>
      <h2 style={h2Style(theme)}>blog</h2>
      <Divider color={theme.accent} />
      <p style={{ color: theme.dim, marginBottom: 24 }}>{POSTS.length} posts :: ordenados por fecha</p>
      <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        {POSTS.map((p) => <PostRow key={p.title} post={p} theme={theme} playKey={playKey} />)}
      </div>
    </div>
  );
}

function ProjectDetail({ theme, project, onBack, playKey }) {
  useEffect(() => {
    window.scrollTo({ top: 0, behavior: "smooth" });
  }, [project && project.name]);
  return (
    <div>
      <a href="#" onClick={(e) => { e.preventDefault(); playKey(); onBack(); }}
        style={{ color: theme.dim, textDecoration: "none", cursor: "pointer", fontSize: 13, display: "inline-block", marginBottom: 24 }}>
        $ cd ..
      </a>
      <h2 style={{ ...h2Style(theme), color: theme.accent, fontSize: 28, marginBottom: 8 }}>{project.name}</h2>
      <p style={{ fontSize: 13, marginTop: 0, marginBottom: 0,
        color: /puesto|mención/.test(project.tag) ? theme.accent : theme.dim,
        fontWeight: /puesto|mención/.test(project.tag) ? 600 : 400 }}>{project.tag}</p>
      <Divider color={theme.accent} />
      <p style={{ color: theme.fg, lineHeight: 1.7, fontSize: 16, marginTop: 16, marginBottom: 32, maxWidth: "70ch" }}>
        {project.longDesc || project.desc}
      </p>
      {project.sections && project.sections.map((s, i) => (
        <div key={i} style={{ marginBottom: 28 }}>
          <h3 style={{ color: theme.accent, fontSize: 16, fontWeight: 700, margin: 0, marginBottom: 8 }}>
            $ {s.title}
          </h3>
          <p style={{ color: theme.fg, opacity: 0.9, lineHeight: 1.7, margin: 0, maxWidth: "70ch" }}>
            {s.body}
          </p>
        </div>
      ))}
      <Divider color={theme.accent} char="-" />
      <div style={{ marginTop: 20, display: "flex", gap: 16, flexWrap: "wrap", alignItems: "center" }}>
        <span style={{ color: theme.dim, fontSize: 13 }}>$ open</span>
        <a href={"https://" + project.link} target="_blank" rel="noreferrer"
          onClick={() => playKey()}
          style={{ ...linkStyle(theme), color: theme.accent, fontWeight: 600 }}>
          {project.link}
        </a>
      </div>
    </div>
  );
}

function Projects({ theme, playKey, highlightIdx, onSelectProject }) {
  const refs = useRef({});
  useEffect(() => {
    if (highlightIdx == null) return;
    const el = refs.current[highlightIdx];
    if (el) {
      el.scrollIntoView ? null : null; // no-op guard
      // Avoid scrollIntoView (forbidden); use window.scrollTo instead
      const rect = el.getBoundingClientRect();
      const y = rect.top + window.scrollY - 80;
      window.scrollTo({ top: y, behavior: "smooth" });
    }
  }, [highlightIdx]);

  return (
    <div>
      <h2 style={h2Style(theme)}>proyectos</h2>
      <Divider color={theme.accent} />
      <p style={{ color: theme.dim, marginBottom: 24 }}>cosas que construí :: en su mayoría open source</p>
      <div style={{ display: "flex", flexDirection: "column", gap: 24 }}>
        {PROJECTS.map((p, i) => {
          const isHi = i === highlightIdx;
          return (
            <div
              key={p.name}
              ref={(el) => { refs.current[i] = el; }}
              style={{
                display: "grid", gridTemplateColumns: "auto 1fr", gap: 16, alignItems: "baseline",
                padding: isHi ? "12px 14px" : "0",
                margin: isHi ? "-12px -14px" : "0",
                border: isHi ? `1px solid ${theme.accent}` : "1px solid transparent",
                background: isHi ? (theme.bg === "#0a0a0a" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)") : "transparent",
                transition: "border-color 0.4s ease, background 0.4s ease",
              }}>
              <a href="#" onClick={(e) => { e.preventDefault(); playKey(); onSelectProject && onSelectProject(i); }} style={{ ...linkStyle(theme), color: theme.accent, fontWeight: 700 }}>
                {p.name}
              </a>
              <span style={{ color: theme.dim, fontSize: 13 }}>{p.tag}</span>
              <p style={{ gridColumn: "1 / -1", color: theme.fg, opacity: 0.85, margin: 0, lineHeight: 1.6 }}>
                {p.desc}
              </p>
              <span style={{ gridColumn: "1 / -1", color: theme.dim, fontSize: 13, display: "flex", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
                <span>→ {p.link}</span>
                <a href="#" onClick={(e) => { e.preventDefault(); playKey(); onSelectProject && onSelectProject(i); }}
                  style={{ color: theme.accent, textDecoration: "none", cursor: "pointer" }}>
                  [ ver detalles ]
                </a>
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function Now({ theme }) {
  return (
    <div>
      <h2 style={h2Style(theme)}>now</h2>
      <Divider color={theme.accent} />
      <p style={{ color: theme.dim, marginBottom: 24 }}>actualizado :: 2026-05-06</p>
      <p style={{ color: theme.fg, lineHeight: 1.7, marginBottom: 32, maxWidth: "65ch" }}>
        esta página está inspirada en <span style={{ color: theme.accent }}>nownownow.com</span>. es lo que estoy haciendo ahora mismo, no hace 3 años.
      </p>
      <div style={{ display: "grid", gridTemplateColumns: "140px 1fr", gap: "16px 24px" }}>
        {NOW.map((n) => (
          <React.Fragment key={n.what}>
            <span style={{ color: theme.accent, fontWeight: 600 }}>{n.what}</span>
            <span style={{ color: theme.fg, lineHeight: 1.6 }}>{n.detail}</span>
          </React.Fragment>
        ))}
      </div>
    </div>
  );
}

function Uses({ theme }) {
  return (
    <div>
      <h2 style={h2Style(theme)}>uses</h2>
      <Divider color={theme.accent} />
      <p style={{ color: theme.dim, marginBottom: 32 }}>setup actual :: lo que uso día a día</p>

      <h3 style={{ color: theme.accent, marginBottom: 12, fontSize: 16 }}>$ hardware</h3>
      <ul style={{ listStyle: "none", padding: 0, margin: 0, marginBottom: 32 }}>
        {USES.hardware.map((h, i) => (
          <li key={i} style={{ color: theme.fg, lineHeight: 1.8 }}>
            <span style={{ color: theme.dim, marginRight: 12 }}>→</span>{h}
          </li>
        ))}
      </ul>

      <h3 style={{ color: theme.accent, marginBottom: 12, fontSize: 16 }}>$ software</h3>
      <ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
        {USES.software.map((s, i) => (
          <li key={i} style={{ color: theme.fg, lineHeight: 1.8 }}>
            <span style={{ color: theme.dim, marginRight: 12 }}>→</span>{s}
          </li>
        ))}
      </ul>
    </div>
  );
}

function Contact({ theme, playKey }) {
  return (
    <div>
      <h2 style={h2Style(theme)}>contacto</h2>
      <Divider color={theme.accent} />
      <p style={{ color: theme.fg, lineHeight: 1.7, marginBottom: 32, maxWidth: "65ch" }}>
        la mejor forma de contactarme es por email o twitter. respondo casi siempre, salvo que sea spam de reclutadores
        (entonces probablemente también, pero más lento).
      </p>
      <div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: "16px 24px", marginBottom: 32 }}>
        {SOCIALS.map((s) => (
          <React.Fragment key={s.label}>
            <span style={{ color: theme.accent, fontWeight: 600 }}>{s.label}</span>
            <a href={"https://" + s.url} onClick={(e) => { e.preventDefault(); playKey(); }} style={linkStyle(theme)}>
              {s.handle}
            </a>
          </React.Fragment>
        ))}
      </div>

      <Divider color={theme.accent} char="-" />
      <p style={{ color: theme.dim, marginTop: 24, fontSize: 13 }}>
        gpg fingerprint :: 4F2A 88B1 9E3D 7C56 0AA4 1D8B 9C2E 6F5A 3D71 8E4C
      </p>
    </div>
  );
}

// ============ HELPERS ============
function Cursor({ color }) {
  return <span style={{
    display: "inline-block",
    width: "0.55em",
    height: "1em",
    background: color,
    marginLeft: 2,
    verticalAlign: "text-bottom",
    animation: "blink 1s steps(2) infinite",
  }} />;
}

function linkStyle(theme) {
  return {
    color: theme.accent,
    textDecoration: "underline",
    textDecorationThickness: 1,
    textUnderlineOffset: 3,
    cursor: "pointer",
  };
}

function h2Style(theme) {
  return {
    color: theme.accent,
    fontSize: 22,
    fontWeight: 700,
    margin: 0,
    marginBottom: 4,
    letterSpacing: "-0.01em",
  };
}

// ============ MAIN ============
const SECTIONS = [
  { id: "inicio", label: "inicio" },
  { id: "blog", label: "blog" },
  { id: "proyectos", label: "proyectos" },
  { id: "now", label: "now" },
  { id: "uses", label: "uses" },
  { id: "contacto", label: "contacto" },
];

function Portfolio({ themeName = "magenta", scanlines = false, sound = false, lang = "es", onThemeChange }) {
  const theme = THEMES[themeName];
  const [section, setSection] = useState("inicio");
  const [projectHighlight, setProjectHighlight] = useState(null);
  const [selectedProject, setSelectedProject] = useState(null);
  const playKey = useKeySound(sound);

  useEffect(() => {
    document.body.style.background = theme.bg;
  }, [theme]);

  const onProjectClick = (idx) => {
    playKey();
    setSelectedProject(null);
    setProjectHighlight(idx);
    setSection("proyectos");
  };

  // clear highlight after a moment
  useEffect(() => {
    if (projectHighlight == null) return;
    const t = setTimeout(() => setProjectHighlight(null), 2400);
    return () => clearTimeout(t);
  }, [projectHighlight]);

  // close project detail when leaving the projects section
  useEffect(() => {
    if (section !== "proyectos" && selectedProject != null) {
      setSelectedProject(null);
    }
  }, [section]);

  const renderSection = () => {
    switch (section) {
      case "inicio": return <Home theme={theme} playKey={playKey} onProjectClick={onProjectClick} />;
      case "blog": return <Blog theme={theme} playKey={playKey} />;
      case "proyectos": return selectedProject != null
        ? <ProjectDetail theme={theme} project={PROJECTS[selectedProject]} onBack={() => setSelectedProject(null)} playKey={playKey} />
        : <Projects theme={theme} playKey={playKey} highlightIdx={projectHighlight} onSelectProject={(i) => setSelectedProject(i)} />;
      case "now": return <Now theme={theme} />;
      case "uses": return <Uses theme={theme} />;
      case "contacto": return <Contact theme={theme} playKey={playKey} />;
      default: return null;
    }
  };

  return (
    <div style={{
      background: theme.bg,
      color: theme.fg,
      minHeight: "100vh",
      fontFamily: "'Geist Mono', 'JetBrains Mono', ui-monospace, monospace",
      fontSize: 15,
      lineHeight: 1.5,
      padding: "48px 32px 80px",
      position: "relative",
      overflow: "hidden",
    }}>
      {scanlines && (
        <div style={{
          position: "fixed", inset: 0, pointerEvents: "none", zIndex: 100,
          background: "repeating-linear-gradient(to bottom, transparent 0, transparent 2px, rgba(0,0,0,0.18) 2px, rgba(0,0,0,0.18) 3px)",
          mixBlendMode: "multiply",
        }} />
      )}

      <div style={{ maxWidth: 760, margin: "0 auto" }}>
        <SiteHeader theme={theme} />

        {/* NAV — clean tabs, no decoration */}
        <nav style={{
          display: "flex", flexWrap: "wrap", gap: 0, alignItems: "center",
          borderTop: `1px solid ${theme.border}`,
          borderBottom: `1px solid ${theme.border}`,
          marginBottom: 40,
        }}>
          {SECTIONS.map((s, i) => (
            <a key={s.id} href={`#${s.id}`} onClick={(e) => { e.preventDefault(); playKey(); setSection(s.id); }}
              style={{
                color: section === s.id ? theme.accent : theme.fg,
                textDecoration: "none",
                cursor: "pointer",
                fontWeight: section === s.id ? 600 : 400,
                padding: "12px 18px",
                borderRight: i < SECTIONS.length - 1 ? `1px solid ${theme.border}` : "none",
                position: "relative",
                fontSize: 14,
              }}>
              {section === s.id && <span style={{ color: theme.accent, marginRight: 6 }}>›</span>}
              {s.label}
            </a>
          ))}
        </nav>

        <main style={{ minHeight: 400 }}>
          {renderSection()}
        </main>

        <footer style={{ marginTop: 80, paddingTop: 24, borderTop: `1px dashed ${theme.border}`, color: theme.dim, fontSize: 13, display: "flex", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
          <span>© 2026 mauro proto · ai/ml engineer</span>
          <span>buenos aires · remoto global</span>
        </footer>
      </div>
    </div>
  );
}

Object.assign(window, { Portfolio, THEMES });
