<!doctype html>
<html lang="es">
<head>
  <meta charset="UTF-8" />
  <script>if(localStorage.getItem('theme')==='dark')document.documentElement.classList.add('api-dark');</script>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>API Reference — oscarortiz.me</title>
  <meta name="description" content="REST API Explorer for oscarortiz.me. Browse developer profile, projects, skills and more as structured JSON." />
  <meta name="robots" content="noindex, nofollow" />
  <link rel="icon" href="../assets/img/favicon.svg" type="image/svg+xml" />
  <link rel="stylesheet" href="../assets/css/google-fonts.css" />
  <style>
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    /* ── LIGHT MODE (default) ─────────────────────────────────── */
    :root {
      --bg:         #f5f7fb;
      --bg2:        #ffffff;
      --bg3:        #eef1f8;
      --bg4:        #e8ecf5;
      --border:     rgba(0,0,0,0.07);
      --border-h:   rgba(0,0,0,0.12);
      --border-a:   rgba(59,96,235,0.28);
      --text:       #0f172a;
      --text-2:     #475569;
      --muted:      #94a3b8;
      --accent:     #3b5bdb;
      --green:      #059669;
      --yellow:     #d97706;
      --red:        #dc2626;
      --font-mono:  'JetBrains Mono', 'Cascadia Code', 'Fira Code', monospace;
      --font-ui:    'Space Grotesk', 'Inter', sans-serif;
      --r:          8px;
      --sidebar-w:  264px;
      --topbar-h:   54px;
      /* JSON token colors — light */
      --j-key:   #1e40af;
      --j-str:   #16a34a;
      --j-num:   #92400e;
      --j-bool:  #6d28d9;
      --j-null:  #9ca3af;
      --j-brace: #374151;
      --j-punc:  #6b7280;
      /* code example token colors — light */
      --cx-kw:   #6d28d9;
      --cx-fn:   #1e40af;
      --cx-str:  #16a34a;
      --cx-cm:   #94a3b8;
      --cx-kv:   #92400e;
    }

    /* ── DARK MODE ────────────────────────────────────────────── */
    html.api-dark {
      --bg:         #0a0f1a;
      --bg2:        #111827;
      --bg3:        #1a2236;
      --bg4:        #1e2a40;
      --border:     rgba(255,255,255,0.055);
      --border-h:   rgba(255,255,255,0.10);
      --border-a:   rgba(91,140,245,0.28);
      --text:       #e2e8f0;
      --text-2:     #94a3b8;
      --muted:      #4b5675;
      --accent:     #5b8cf5;
      --green:      #10b981;
      --yellow:     #f59e0b;
      --red:        #ef4444;
      --j-key:   #93c5fd;
      --j-str:   #86efac;
      --j-num:   #fbbf24;
      --j-bool:  #c084fc;
      --j-null:  #64748b;
      --j-brace: #94a3b8;
      --j-punc:  #4b5675;
      --cx-kw:   #c084fc;
      --cx-fn:   #93c5fd;
      --cx-str:  #86efac;
      --cx-cm:   #4b5675;
      --cx-kv:   #fbbf24;
    }

    html, body { height: 100%; }
    body {
      font-family: var(--font-ui);
      background: var(--bg); color: var(--text);
      display: flex; flex-direction: column; min-height: 100vh;
      -webkit-font-smoothing: antialiased;
      transition: background 0.18s, color 0.18s;
    }

    /* ══ TOPBAR ══ */
    .topbar {
      height: var(--topbar-h);
      background: var(--bg2);
      border-bottom: 1px solid var(--border);
      display: flex; align-items: center;
      padding: 0 20px; gap: 10px;
      position: sticky; top: 0; z-index: 100; flex-shrink: 0;
      backdrop-filter: blur(12px);
    }
    .tb-brand { font-family: var(--font-mono); font-size: 12.5px; font-weight: 700; color: var(--accent); text-decoration: none; flex-shrink: 0; letter-spacing: -0.3px; transition: opacity 0.18s; }
    .tb-brand:hover { opacity: 0.75; }
    .tb-sep { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }
    .tb-path { flex: 1; font-family: var(--font-mono); font-size: 12px; color: var(--text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .tb-env { font-size: 9px; font-weight: 800; letter-spacing: 1.2px; text-transform: uppercase; padding: 3px 9px; border-radius: 100px; background: rgba(16,185,129,0.1); color: var(--green); border: 1px solid rgba(16,185,129,0.2); flex-shrink: 0; }
    .tb-version { font-size: 9px; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase; padding: 3px 9px; border-radius: 100px; background: rgba(91,140,245,0.1); color: var(--accent); border: 1px solid rgba(91,140,245,0.18); flex-shrink: 0; }
    .tb-try { font-size: 11px; font-weight: 700; color: var(--text-2); background: none; border: 1px solid var(--border-h); padding: 5px 13px; border-radius: var(--r); cursor: pointer; font-family: var(--font-mono); transition: all 0.18s; flex-shrink: 0; display: flex; align-items: center; gap: 7px; }
    .tb-try:hover { color: var(--accent); border-color: var(--accent); background: rgba(91,140,245,0.08); }
    .tb-try.tb-spinning svg { animation: spin 0.7s linear; }
    @keyframes spin { to { transform: rotate(360deg); } }
    .tb-home { font-size: 11px; color: var(--muted); text-decoration: none; display: flex; align-items: center; gap: 6px; flex-shrink: 0; transition: color 0.18s; padding: 5px 9px; border-radius: var(--r); }
    .tb-home:hover { color: var(--text-2); background: rgba(0,0,0,0.04); }
    html.api-dark .tb-home:hover { background: rgba(255,255,255,0.04); }
    .tb-view-toggle { display: flex; align-items: center; background: var(--bg3); border-radius: 7px; padding: 2px; gap: 1px; flex-shrink: 0; }
    .tb-vt-btn { display: flex; align-items: center; gap: 5px; padding: 4px 10px; border-radius: 5px; border: none; cursor: pointer; font-size: 11px; font-weight: 700; background: transparent; color: var(--muted); font-family: var(--font-ui); transition: all 0.18s; white-space: nowrap; }
    .tb-vt-btn.tb-vt-active { background: var(--bg2); color: var(--accent); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    .tb-vt-btn:not(.tb-vt-active):hover { color: var(--text-2); }
    .tb-lang { display: inline-flex; align-items: center; gap: 4px; border: 1px solid var(--border); border-radius: 7px; padding: 2px; background: var(--bg3); flex-shrink: 0; }
    .tb-lang-btn { border: none; background: transparent; color: var(--muted); border-radius: 5px; font-size: 10px; font-weight: 800; letter-spacing: 0.6px; text-transform: uppercase; padding: 4px 8px; cursor: pointer; transition: all 0.18s; }
    .tb-lang-btn:hover { color: var(--text-2); }
    .tb-lang-btn.tb-lang-active { background: var(--bg2); color: var(--accent); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
    /* Theme toggle */
    .tb-theme { width: 30px; height: 30px; border-radius: 7px; border: 1px solid var(--border); background: none; cursor: pointer; display: flex; align-items: center; justify-content: center; color: var(--muted); transition: all 0.18s; flex-shrink: 0; }
    .tb-theme:hover { color: var(--text-2); border-color: var(--border-h); background: var(--bg3); }

    /* ══ LAYOUT ══ */
    .layout { display: flex; flex: 1; overflow: hidden; }

    /* ══ SIDEBAR ══ */
    .sidebar { width: var(--sidebar-w); background: var(--bg2); border-right: 1px solid var(--border); overflow-y: auto; flex-shrink: 0; display: flex; flex-direction: column; }
    .sb-section { padding: 18px 10px 4px; }
    .sb-label { font-size: 9px; font-weight: 800; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); padding: 0 8px; margin-bottom: 5px; }
    .sb-link { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: var(--r); cursor: pointer; transition: background 0.14s; border: none; background: none; width: 100%; text-align: left; text-decoration: none; }
    .sb-link:hover { background: var(--bg3); }
    .sb-link.active { background: rgba(59,91,219,0.09); }
    html.api-dark .sb-link.active { background: rgba(91,140,245,0.1); }
    .sb-method { font-family: var(--font-mono); font-size: 8.5px; font-weight: 900; color: var(--green); width: 26px; flex-shrink: 0; letter-spacing: 0.3px; }
    .sb-method.dot { color: var(--muted); font-size: 16px; line-height: 0; text-align: center; }
    .sb-path { font-family: var(--font-mono); font-size: 11px; color: var(--text-2); flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .sb-link.active .sb-path { color: var(--text); }
    .sb-badge { font-size: 7.5px; font-weight: 800; letter-spacing: 0.5px; text-transform: uppercase; padding: 2px 6px; border-radius: 3px; flex-shrink: 0; }
    .sb-badge.live { background: rgba(16,185,129,0.12); color: var(--green); }
    .sb-badge.prod { background: rgba(91,140,245,0.1); color: var(--accent); }
    .sb-footer { margin-top: auto; padding: 14px 16px; border-top: 1px solid var(--border); font-size: 10px; color: var(--muted); font-family: var(--font-mono); line-height: 1.9; }

    /* ══ MAIN ══ */
    .main { flex: 1; overflow-y: auto; }

    /* ══ ENDPOINT VIEW ══ */
    .ep-view { padding: 28px 32px; max-width: 980px; }
    .breadcrumb { display: flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-bottom: 18px; }
    .breadcrumb a { color: var(--muted); text-decoration: none; transition: color 0.15s; }
    .breadcrumb a:hover { color: var(--text-2); }

    /* ══ REQUEST BAR ══ */
    .req-bar { display: flex; align-items: center; gap: 10px; padding: 14px 18px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r); margin-bottom: 16px; flex-wrap: wrap; }
    .req-method { font-family: var(--font-mono); font-size: 11px; font-weight: 900; color: var(--green); background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.18); padding: 4px 12px; border-radius: 5px; letter-spacing: 0.5px; flex-shrink: 0; }
    .req-url { font-family: var(--font-mono); font-size: 13px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .req-base  { color: var(--muted); }
    .req-route { color: var(--text); }
    .req-param { color: var(--yellow); }
    .status-chip { display: flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 11px; font-weight: 700; padding: 4px 11px; border-radius: 5px; flex-shrink: 0; transition: opacity 0.3s; }
    .s200 { background: rgba(16,185,129,0.09); border: 1px solid rgba(16,185,129,0.2); color: var(--green); }
    .s404 { background: rgba(239,68,68,0.09); border: 1px solid rgba(239,68,68,0.2); color: var(--red); }
    .s500 { background: rgba(245,158,11,0.09); border: 1px solid rgba(245,158,11,0.2); color: var(--yellow); }
    .latency-chip { font-family: var(--font-mono); font-size: 10px; color: var(--muted); flex-shrink: 0; }
    .env-chip { font-family: var(--font-mono); font-size: 9px; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; padding: 3px 9px; border-radius: 4px; flex-shrink: 0; }
    .env-prod { background: rgba(91,140,245,0.1); border: 1px solid rgba(91,140,245,0.18); color: var(--accent); }
    .env-live { background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.18); color: var(--green); }

    /* ══ EP INFO ══ */
    .ep-info { display: flex; align-items: baseline; gap: 10px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r); padding: 14px 18px; margin-bottom: 20px; font-size: 13px; color: var(--text-2); line-height: 1.6; }
    .ep-info-icon { color: var(--accent); opacity: 0.6; flex-shrink: 0; }
    .ep-info strong { color: var(--text); }

    /* ══ QUERY PARAMS ══ */
    .qp-bar { display: flex; align-items: center; gap: 6px; padding: 11px 16px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r); margin-bottom: 16px; flex-wrap: wrap; }
    .qp-label { font-size: 9px; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); font-family: var(--font-mono); flex-shrink: 0; margin-right: 4px; }
    .qp-pill { font-family: var(--font-mono); font-size: 11px; padding: 4px 11px; border-radius: 4px; border: 1px solid var(--border); background: none; color: var(--text-2); cursor: pointer; transition: all 0.15s; }
    .qp-pill:hover { border-color: var(--border-h); color: var(--text); }
    .qp-pill.active { background: rgba(59,91,219,0.07); border-color: var(--border-a); color: var(--accent); }
    html.api-dark .qp-pill.active { background: rgba(91,140,245,0.1); }
    .qp-key { color: var(--yellow); }
    .qp-eq  { color: var(--muted); }
    .qp-val { color: var(--green); }

    /* ══ TABS ══ */
    .tab-bar { display: flex; background: var(--bg2); border: 1px solid var(--border); border-bottom: none; border-radius: var(--r) var(--r) 0 0; overflow: hidden; }
    .tab-btn { padding: 11px 18px; font-size: 12px; font-weight: 700; color: var(--muted); cursor: pointer; border: none; background: none; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.15s; font-family: var(--font-ui); white-space: nowrap; }
    .tab-btn:hover { color: var(--text-2); }
    .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
    .tab-panel-wrap { background: var(--bg2); border: 1px solid var(--border); border-top: none; border-radius: 0 0 var(--r) var(--r); overflow: hidden; }

    /* ══ PANEL TOOLS ══ */
    .panel-tools { display: flex; align-items: center; gap: 3px; padding: 8px 12px; border-bottom: 1px solid var(--border); background: var(--bg3); }
    .pt-btn { font-size: 10.5px; font-weight: 700; color: var(--muted); background: none; border: 1px solid transparent; padding: 3px 9px; border-radius: 5px; cursor: pointer; font-family: var(--font-mono); transition: all 0.15s; }
    .pt-btn:hover { color: var(--text-2); border-color: var(--border-h); }
    .pt-btn.active { color: var(--accent); border-color: var(--border-a); background: rgba(59,91,219,0.06); }
    html.api-dark .pt-btn.active { background: rgba(91,140,245,0.07); }
    .pt-btn.ok { color: var(--green); }
    .pt-sep { width: 1px; height: 14px; background: var(--border); margin: 0 3px; }

    /* ══ JSON BODY ══ */
    .json-body { font-family: var(--font-mono); font-size: 12.5px; line-height: 1.65; overflow: auto; padding: 16px 18px; max-height: 580px; background: var(--bg2); }
    .json-body.collapsed { max-height: 140px; overflow: hidden; }
    .json-wrap { white-space: pre; tab-size: 2; }
    .j-key  { color: var(--j-key); }
    .j-str  { color: var(--j-str); }
    .j-num  { color: var(--j-num); }
    .j-bool { color: var(--j-bool); }
    .j-null { color: var(--j-null); }
    .j-brace{ color: var(--j-brace); }
    .j-punc { color: var(--j-punc); }

    /* ══ HEADERS TABLE ══ */
    .hdr-section-label { font-size: 9px; font-weight: 800; letter-spacing: 2px; text-transform: uppercase; color: var(--muted); padding: 10px 16px 6px; background: var(--bg3); border-bottom: 1px solid var(--border); }
    .hdrs-table { width: 100%; border-collapse: collapse; }
    .hdrs-table tr { border-bottom: 1px solid var(--border); }
    .hdrs-table tr:last-child { border-bottom: none; }
    .hdrs-table td { padding: 7px 16px; font-family: var(--font-mono); font-size: 11.5px; vertical-align: top; background: var(--bg2); }
    .hdrs-table td:first-child { color: var(--text-2); width: 44%; }
    .hdrs-table td:last-child { color: var(--muted); word-break: break-all; }
    .hdr-hi td:first-child { color: var(--accent); }

    /* ══ CODE EXAMPLES ══ */
    .example-wrap { padding: 16px 18px; background: var(--bg2); }
    .lang-btns { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; }
    .lang-btn { font-size: 10px; font-weight: 700; padding: 4px 12px; border-radius: 5px; border: 1px solid var(--border); color: var(--muted); cursor: pointer; background: none; font-family: var(--font-mono); transition: all 0.15s; letter-spacing: 0.3px; }
    .lang-btn:hover { color: var(--text-2); border-color: var(--border-h); }
    .lang-btn.active { color: var(--accent); border-color: var(--border-a); background: rgba(59,91,219,0.06); }
    html.api-dark .lang-btn.active { background: rgba(91,140,245,0.07); }
    .code-block { background: var(--bg3); border: 1px solid var(--border); border-radius: var(--r); padding: 16px 18px; font-family: var(--font-mono); font-size: 12px; line-height: 1.75; overflow-x: auto; white-space: pre; color: var(--text-2); }
    .cx-kw  { color: var(--cx-kw); }
    .cx-fn  { color: var(--cx-fn); }
    .cx-str { color: var(--cx-str); }
    .cx-cm  { color: var(--cx-cm); font-style: italic; }
    .cx-kv  { color: var(--cx-kv); }

    /* ══ SCHEMA ══ */
    .schema-body { padding: 16px 18px; background: var(--bg2); }
    .schema-pre { font-family: var(--font-mono); font-size: 12px; line-height: 1.8; white-space: pre; color: var(--text-2); }
    .sc-key  { color: var(--j-key); }
    .sc-type { color: var(--j-bool); font-style: italic; }
    .sc-note { color: var(--muted); }
    .sc-req  { color: var(--yellow); font-size: 9px; }

    /* ══ LOADING ══ */
    .loading-wrap { padding: 22px 18px; display: flex; flex-direction: column; gap: 9px; background: var(--bg2); }
    .loading-msg { display: flex; align-items: center; gap: 9px; font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-bottom: 6px; }
    .loading-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); animation: blink 1s infinite; flex-shrink: 0; }
    @keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.2} }
    .loading-bar { height: 11px; border-radius: 4px; background: linear-gradient(90deg, var(--bg3) 25%, var(--border) 50%, var(--bg3) 75%); background-size: 200% 100%; animation: shimmer 1.4s infinite; }
    @keyframes shimmer { 0%{background-position:200% 0} 100%{background-position:-200% 0} }

    /* ══ ERROR STATES ══ */
    .error-card { border-radius: var(--r); overflow: hidden; border: 1px solid; }
    .error-card.e404 { border-color: rgba(239,68,68,0.2); background: rgba(239,68,68,0.04); }
    .error-card.e500 { border-color: rgba(245,158,11,0.2); background: rgba(245,158,11,0.04); }
    .error-head { padding: 16px 22px; display: flex; align-items: center; gap: 16px; border-bottom: 1px solid; }
    .e404 .error-head { border-color: rgba(239,68,68,0.15); }
    .e500 .error-head { border-color: rgba(245,158,11,0.15); }
    .error-code { font-family: var(--font-mono); font-size: 32px; font-weight: 900; }
    .e404 .error-code { color: var(--red); }
    .e500 .error-code { color: var(--yellow); }
    .error-text h3 { font-size: 14px; font-weight: 700; color: var(--text); }
    .error-text p  { font-size: 12px; color: var(--text-2); margin-top: 3px; }

    /* ══ INDEX PAGE ══ */
    .index-page { max-width: 840px; margin: 0 auto; padding: 48px 32px; }
    .index-pill { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--green); background: rgba(16,185,129,0.07); border: 1px solid rgba(16,185,129,0.14); padding: 5px 14px; border-radius: 100px; margin-bottom: 20px; }
    .index-pill::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: var(--green); animation: blink 2s infinite; }
    .index-title { font-family: var(--font-mono); font-size: 30px; font-weight: 700; color: var(--text); letter-spacing: -0.8px; margin-bottom: 14px; line-height: 1.15; }
    .index-title em { font-style: normal; color: var(--accent); }
    .index-desc { font-size: 14px; color: var(--text-2); line-height: 1.65; max-width: 520px; }
    .index-baseurl { margin: 26px 0; padding: 12px 18px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r); display: flex; align-items: center; gap: 12px; font-family: var(--font-mono); font-size: 13px; }
    .baseurl-label { font-size: 9px; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); flex-shrink: 0; }
    .baseurl-val { color: var(--accent); }
    .index-meta { display: flex; gap: 24px; flex-wrap: wrap; font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-bottom: 36px; }
    .index-meta span span { color: var(--text-2); }
    .ep-grid { display: flex; flex-direction: column; gap: 9px; }
    .ep-row { display: flex; align-items: center; gap: 14px; padding: 15px 20px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r); text-decoration: none; cursor: pointer; transition: border-color 0.16s, background 0.16s, transform 0.16s; }
    .ep-row:hover { border-color: var(--border-h); background: var(--bg3); transform: translateX(2px); }
    .ep-row-method { font-family: var(--font-mono); font-size: 10px; font-weight: 900; color: var(--green); background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.15); padding: 3px 9px; border-radius: 4px; flex-shrink: 0; }
    .ep-row-path { font-family: var(--font-mono); font-size: 13px; color: var(--text); flex: 1; }
    .ep-row-path em { font-style: normal; color: var(--accent); }
    .ep-row-desc { font-size: 12px; color: var(--muted); flex-shrink: 0; }
    .curl-box { margin-top: 36px; background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; }
    .curl-head { padding: 10px 16px; background: var(--bg3); border-bottom: 1px solid var(--border); font-size: 10px; font-weight: 800; letter-spacing: 1.5px; text-transform: uppercase; color: var(--muted); font-family: var(--font-mono); display: flex; align-items: center; gap: 8px; }
    .curl-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); }
    .curl-body { padding: 16px 18px; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.9; background: var(--bg2); }

    /* ══ SCROLLBAR ══ */
    ::-webkit-scrollbar { width: 5px; height: 5px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb { background: var(--border-h); border-radius: 3px; }
    ::-webkit-scrollbar-thumb:hover { background: var(--muted); }

    /* ══ RESPONSIVE ══ */
    @media (max-width: 700px) {
      .sidebar { display: none; }
      .ep-view { padding: 20px 16px; }
      .index-page { padding: 24px 18px; }
      .index-title { font-size: 22px; }
      .tb-env, .tb-version, .tb-lang { display: none; }
    }
  </style>
</head>
<body>

  <div class="topbar">
    <a href="/api" class="tb-brand" id="tb-brand">oscarortiz.me/api</a>
    <div class="tb-sep"></div>
    <div class="tb-path" id="tb-path" data-i18n="api.topbar.path">REST API Reference</div>
    <div class="tb-env">production</div>
    <div class="tb-version">v1</div>
    <div class="tb-view-toggle">
      <button class="tb-vt-btn" onclick="location.href='/'">
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/></svg>
        UI
      </button>
      <button class="tb-vt-btn tb-vt-active">
        <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>
        API
      </button>
    </div>
    <div class="tb-lang" id="tb-lang">
      <button class="tb-lang-btn" data-lang="es">ES</button>
      <button class="tb-lang-btn" data-lang="en">EN</button>
    </div>
    <button class="tb-theme" id="tb-theme" aria-label="Toggle theme" title="Toggle light/dark">
      <svg id="tb-theme-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
    </button>
    <button class="tb-try" id="tb-try" style="display:none">
      <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.16"/></svg>
      <span data-i18n="api.topbar.try">Try</span>
    </button>
    <a href="/" class="tb-home">
      <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
      <span data-i18n="api.topbar.portfolio">Portfolio</span>
    </a>
  </div>

  <div class="layout">
    <nav class="sidebar">
      <div class="sb-section">
        <div class="sb-label" data-i18n="api.sidebar.overview">Overview</div>
        <button class="sb-link" data-route=""><span class="sb-method dot">·</span><span class="sb-path">/api</span></button>
      </div>
      <div class="sb-section">
        <div class="sb-label" data-i18n="api.sidebar.profile">Profile</div>
        <button class="sb-link" data-route="me"><span class="sb-method">GET</span><span class="sb-path">/api/me</span></button>
        <button class="sb-link" data-route="projects"><span class="sb-method">GET</span><span class="sb-path">/api/projects</span></button>
        <button class="sb-link" data-route="projects/trelk-bot"><span class="sb-method">GET</span><span class="sb-path">/api/projects/:id</span></button>
        <button class="sb-link" data-route="experience"><span class="sb-method">GET</span><span class="sb-path">/api/experience</span></button>
        <button class="sb-link" data-route="skills"><span class="sb-method">GET</span><span class="sb-path">/api/skills</span></button>
        <button class="sb-link" data-route="contact"><span class="sb-method">GET</span><span class="sb-path">/api/contact</span></button>
      </div>
      <div class="sb-section">
        <div class="sb-label" data-i18n="api.sidebar.system">System</div>
        <button class="sb-link" data-route="system"><span class="sb-method">GET</span><span class="sb-path">/api/system</span><span class="sb-badge live">live</span></button>
        <button class="sb-link" data-route="metrics"><span class="sb-method">GET</span><span class="sb-path">/api/metrics</span><span class="sb-badge live">live</span></button>
        <button class="sb-link" data-route="queues"><span class="sb-method">GET</span><span class="sb-path">/api/queues</span><span class="sb-badge live">live</span></button>
        <button class="sb-link" data-route="events"><span class="sb-method">GET</span><span class="sb-path">/api/events</span><span class="sb-badge live">live</span></button>
      </div>
      <div class="sb-footer" data-i18n="api.sidebar.footer" data-i18n-mode="html">All endpoints · GET · JSON<br/>content-type: application/json<br/>auth: not required<br/>endpoints: 10</div>
    </nav>
    <main class="main" id="main"></main>
  </div>

<script src="../assets/js/i18n.js"></script>
<script>
// ── Theme toggle ──
(function(){
  var btn=document.getElementById('tb-theme');
  var icon=document.getElementById('tb-theme-icon');
  var SUN='<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>';
  var MOON='<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>';
  function updateIcon(){icon.innerHTML=document.documentElement.classList.contains('api-dark')?SUN:MOON;}
  updateIcon();
  btn.onclick=function(){
    var dark=document.documentElement.classList.toggle('api-dark');
    localStorage.setItem('theme',dark?'dark':'light');
    updateIcon();
  };
})();

const BASE='https://oscarortiz.me';
let S={route:'',params:{},data:null,reqId:'',tab:'response',raw:false,collapsed:false,lang:'curl'};

function tt(key){
  if(window.i18n&&typeof window.i18n.t==='function')return window.i18n.t(key);
  return key;
}

function mountApiLanguageSwitch(){
  if(!window.i18n)return;
  var wrap=document.getElementById('tb-lang');
  if(!wrap)return;
  function sync(){
    var lang=window.i18n.getLanguage();
    wrap.querySelectorAll('.tb-lang-btn').forEach(function(btn){
      btn.classList.toggle('tb-lang-active',btn.getAttribute('data-lang')===lang);
    });
  }
  wrap.querySelectorAll('.tb-lang-btn').forEach(function(btn){
    btn.addEventListener('click',function(){
      var lang=btn.getAttribute('data-lang');
      window.i18n.setLanguage(lang,{persist:true,updateUrl:true});
    });
  });
  window.addEventListener('i18n:changed',function(){
    sync();
    if(S.route===''){
      var tbPath=document.getElementById('tb-path');
      if(tbPath)tbPath.textContent=tt('api.topbar.path');
      renderIndex();
    }else{
      render();
    }
  });
  sync();
}

mountApiLanguageSwitch();

function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
function uuid(){return'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g,c=>{const r=Math.random()*16|0;return(c==='x'?r:(r&0x3|0x8)).toString(16);})}

function syntaxHL(obj){
  const raw=JSON.stringify(obj,null,2);
  function fmtVal(v){
    const t=v.trim(),hasC=t.endsWith(','),core=hasC?t.slice(0,-1).trim():t,comma=hasC?'<span class="j-punc">,</span>':'';
    if(!core)return'';
    if(/^[{}\[\]]$/.test(core))return' <span class="j-brace">'+esc(core)+'</span>'+comma;
    if(core.startsWith('"'))return' <span class="j-str">'+esc(core)+'</span>'+comma;
    if(core==='true'||core==='false')return' <span class="j-bool">'+core+'</span>'+comma;
    if(core==='null')return' <span class="j-null">null</span>'+comma;
    if(/^-?\d/.test(core))return' <span class="j-num">'+esc(core)+'</span>'+comma;
    return' '+esc(v);
  }
  return raw.split('\n').map(line=>{
    const kv=line.match(/^(\s*)("(?:\\.|[^"\\])*")(\s*:\s*)(.*)/);
    if(kv)return kv[1]+'<span class="j-key">'+esc(kv[2])+'</span><span class="j-punc">'+esc(kv[3])+'</span>'+fmtVal(kv[4]);
    const t=line.trim(),ind=line.match(/^(\s*)/)[1];
    return t?ind+fmtVal(t):line;
  }).join('\n');
}

const EP={
  '':          {file:null,           label:'API Reference',    virtual:false},
  'me':        {file:'me.json',      label:'Developer Profile',virtual:false},
  'projects':  {file:'projects.json',label:'Projects',         virtual:false},
  'experience':{file:'experience.json',label:'Work History',   virtual:false},
  'skills':    {file:'skills.json',  label:'Skills',           virtual:false},
  'contact':   {file:'contact.json', label:'Contact',          virtual:false},
  'system':    {file:null,           label:'System Status',    virtual:true },
  'metrics':   {file:null,           label:'Performance Metrics',virtual:true},
  'queues':    {file:null,           label:'Queue Status',     virtual:true },
  'events':    {file:null,           label:'Event Feed',       virtual:true },
};
const DESCS={
  me:'api.desc.me',
  projects:'api.desc.projects',
  experience:'api.desc.experience',
  skills:'api.desc.skills',
  contact:'api.desc.contact',
  system:'api.desc.system',
  metrics:'api.desc.metrics',
  queues:'api.desc.queues',
  events:'api.desc.events',
};
const QPS={projects:[{key:'limit',values:['3','5','8']},{key:'status',values:['live','production','all']}]};
const ENV_B={me:'prod',projects:'prod',experience:'prod',skills:'prod',contact:'prod',system:'live',metrics:'live',queues:'live',events:'live'};

const SCHEMAS={
  me:`<span class="sc-key">"status"</span>   <span class="sc-type">string</span>   <span class="sc-note">// "success" | "error"</span>
<span class="sc-key">"data"</span>: {
  <span class="sc-key">"id"</span>                  <span class="sc-type">string</span>
  <span class="sc-key">"name"</span>                <span class="sc-type">string</span>
  <span class="sc-key">"role"</span>                <span class="sc-type">string</span>
  <span class="sc-key">"availability"</span>        <span class="sc-type">"open_to_work" | "busy"</span>
  <span class="sc-key">"experience_years"</span>    <span class="sc-type">number</span>
  <span class="sc-key">"location"</span>            <span class="sc-type">{ country: string, timezone: string }</span>
  <span class="sc-key">"stack"</span>               <span class="sc-type">string[]</span>
  <span class="sc-key">"contact"</span>             <span class="sc-type">{ email, github, linkedin, telegram }</span>
}
<span class="sc-key">"meta"</span>: {
  <span class="sc-key">"version"</span>             <span class="sc-type">string</span>
  <span class="sc-key">"timestamp"</span>           <span class="sc-type">string (ISO 8601)</span>
}`,
  projects:`<span class="sc-key">"data"</span>: <span class="sc-type">Project[]</span>   <span class="sc-note">// Project:</span>
{
  <span class="sc-key">"id"</span>                  <span class="sc-type">string</span>
  <span class="sc-key">"name"</span>                <span class="sc-type">string</span>
  <span class="sc-key">"status"</span>              <span class="sc-type">"live" | "production" | "development"</span>
  <span class="sc-key">"architecture"</span>        <span class="sc-type">{ pattern, queues, ... }</span>
  <span class="sc-key">"stack"</span>               <span class="sc-type">string[]</span>
  <span class="sc-key">"metrics"</span>             <span class="sc-type">object</span>
  <span class="sc-key">"features"</span>            <span class="sc-type">string[]</span>
  <span class="sc-key">"url"</span>                 <span class="sc-type">string | null</span>
  <span class="sc-key">"public_repo"</span>         <span class="sc-type">boolean</span>
}
<span class="sc-key">"meta"</span>: { <span class="sc-key">"total"</span>: <span class="sc-type">number</span>, <span class="sc-key">"returned"</span>: <span class="sc-type">number</span>, <span class="sc-key">"timestamp"</span>: <span class="sc-type">string</span> }`,
  experience:`<span class="sc-key">"data"</span>: <span class="sc-type">Experience[]</span>   <span class="sc-note">// Experience:</span>
{
  <span class="sc-key">"role"</span>                <span class="sc-type">string</span>
  <span class="sc-key">"company"</span>             <span class="sc-type">string</span>
  <span class="sc-key">"type"</span>                <span class="sc-type">"freelance" | "contract" | "fulltime"</span>
  <span class="sc-key">"period"</span>              <span class="sc-type">{ from: string, to: string | "present" }</span>
  <span class="sc-key">"contributions"</span>       <span class="sc-type">string[]</span>
  <span class="sc-key">"stack"</span>               <span class="sc-type">string[]</span>
}`,
  skills:`<span class="sc-key">"data"</span>: {
  <span class="sc-key">"backend"</span>             <span class="sc-type">Skill[]</span>
  <span class="sc-key">"frontend"</span>            <span class="sc-type">Skill[]</span>
  <span class="sc-key">"databases"</span>           <span class="sc-type">Skill[]</span>
  <span class="sc-key">"devops"</span>              <span class="sc-type">Skill[]</span>
}
<span class="sc-note">// Skill: { name: string, level: 0-100, years: number }</span>`,
  contact:`<span class="sc-key">"data"</span>: {
  <span class="sc-key">"email"</span>               <span class="sc-type">string</span>
  <span class="sc-key">"github"</span>              <span class="sc-type">string (URL)</span>
  <span class="sc-key">"linkedin"</span>            <span class="sc-type">string (URL)</span>
  <span class="sc-key">"telegram"</span>            <span class="sc-type">string</span>
  <span class="sc-key">"location"</span>            <span class="sc-type">{ country, city, timezone }</span>
  <span class="sc-key">"availability"</span>        <span class="sc-type">"open_to_work" | "busy"</span>
  <span class="sc-key">"response_time"</span>       <span class="sc-type">string   // e.g. "< 24h"</span>
}`,
  system:`<span class="sc-key">"data"</span>: {
  <span class="sc-key">"status"</span>              <span class="sc-type">"healthy" | "degraded" | "down"</span>
  <span class="sc-key">"uptime"</span>              <span class="sc-type">string   // e.g. "99.98%"</span>
  <span class="sc-key">"services"</span>            <span class="sc-type">Service[]   // { name, status, latency }</span>
  <span class="sc-key">"queue"</span>               <span class="sc-type">{ jobs_processed, failed, active_workers, latency_avg }</span>
  <span class="sc-key">"database"</span>            <span class="sc-type">{ connected, pool_size, pool_used, latency }</span>
  <span class="sc-key">"cache"</span>               <span class="sc-type">{ connected, hit_rate, memory_mb }</span>
}`,
  metrics:`<span class="sc-key">"data"</span>: {
  <span class="sc-key">"period"</span>              <span class="sc-type">string   // "last_7d"</span>
  <span class="sc-key">"requests"</span>: {
    <span class="sc-key">"total"</span>             <span class="sc-type">number</span>
    <span class="sc-key">"per_day_avg"</span>       <span class="sc-type">number</span>
    <span class="sc-key">"peak_rps"</span>          <span class="sc-type">number</span>
    <span class="sc-key">"p50_latency"</span>       <span class="sc-type">string   // "42ms"</span>
    <span class="sc-key">"p95_latency"</span>       <span class="sc-type">string</span>
    <span class="sc-key">"p99_latency"</span>       <span class="sc-type">string</span>
  }
  <span class="sc-key">"queue"</span>: {
    <span class="sc-key">"processed_total"</span>   <span class="sc-type">number</span>
    <span class="sc-key">"failed_total"</span>      <span class="sc-type">number</span>
    <span class="sc-key">"success_rate"</span>      <span class="sc-type">string</span>
    <span class="sc-key">"avg_latency"</span>       <span class="sc-type">string</span>
  }
  <span class="sc-key">"uptime"</span>: { <span class="sc-key">"percent"</span>: <span class="sc-type">string</span>, <span class="sc-key">"incidents_30d"</span>: <span class="sc-type">number</span> }
  <span class="sc-key">"webhooks"</span>: { <span class="sc-key">"received"</span>: <span class="sc-type">number</span>, <span class="sc-key">"avg_ack_time"</span>: <span class="sc-type">string</span> }
}`,
  queues:`<span class="sc-key">"data"</span>: {
  <span class="sc-key">"workers"</span>             <span class="sc-type">number</span>
  <span class="sc-key">"queues"</span>: <span class="sc-type">Queue[]</span>
}
<span class="sc-note">// Queue:</span>
{
  <span class="sc-key">"name"</span>                <span class="sc-type">string</span>
  <span class="sc-key">"type"</span>                <span class="sc-type">"BullMQ"</span>
  <span class="sc-key">"status"</span>              <span class="sc-type">"running" | "paused" | "draining"</span>
  <span class="sc-key">"waiting"</span>             <span class="sc-type">number</span>
  <span class="sc-key">"active"</span>              <span class="sc-type">number</span>
  <span class="sc-key">"completed"</span>           <span class="sc-type">number</span>
  <span class="sc-key">"failed"</span>              <span class="sc-type">number</span>
  <span class="sc-key">"dlq"</span>                 <span class="sc-type">number   // dead letter queue</span>
  <span class="sc-key">"concurrency"</span>         <span class="sc-type">number</span>
  <span class="sc-key">"avg_duration"</span>        <span class="sc-type">string</span>
}`,
  events:`<span class="sc-key">"data"</span>: {
  <span class="sc-key">"feed"</span>: <span class="sc-type">Event[]</span>
  <span class="sc-key">"stats"</span>: { <span class="sc-key">"total_24h"</span>: <span class="sc-type">number</span>, <span class="sc-key">"types"</span>: <span class="sc-type">number</span> }
}
<span class="sc-note">// Event:</span>
{
  <span class="sc-key">"id"</span>                  <span class="sc-type">string (UUID)</span>
  <span class="sc-key">"type"</span>                <span class="sc-type">"webhook.received" | "queue.job_completed" | ...</span>
  <span class="sc-key">"source"</span>              <span class="sc-type">string</span>
  <span class="sc-key">"timestamp"</span>           <span class="sc-type">string (ISO 8601)</span>
  <span class="sc-key">"duration_ms"</span>         <span class="sc-type">number</span>
  <span class="sc-key">"status"</span>              <span class="sc-type">"success" | "failed"</span>
  <span class="sc-key">"meta"</span>                <span class="sc-type">object</span>
}`,
};

function sysData(){
  const seed=Math.floor(Date.now()/60000);
  const rng=(min,max,s=seed)=>min+((s*2654435761+s)>>>0)%(max-min+1);
  return{status:'success',data:{status:'healthy',uptime:'99.98%',services:[
    {name:'api-gateway',status:'running',latency:`${rng(12,28)}ms`,version:'v1.4.2'},
    {name:'webhook-processor',status:'running',latency:`${rng(38,72)}ms`,version:'v2.1.0'},
    {name:'notification-worker',status:'running',latency:`${rng(18,44)}ms`,version:'v1.8.3'},
    {name:'payment-service',status:'running',latency:`${rng(55,110)}ms`,version:'v1.2.1'},
    {name:'telegram-bot',status:'running',latency:`${rng(22,58)}ms`,version:'v3.0.4'},
  ],queue:{jobs_processed:120394+rng(0,500),failed:rng(0,5),active_workers:4,latency_avg:`${rng(110,165)}ms`,queues:['webhook-processor','notifications','payment-events']},database:{connected:true,pool_size:20,pool_used:rng(2,7),latency:`${rng(3,11)}ms`,type:'PostgreSQL + MongoDB'},cache:{connected:true,hit_rate:`${rng(92,98)}%`,memory_mb:rng(180,240),type:'Redis'}},meta:{endpoint:'/api/system',version:'v1',dynamic:true,timestamp:new Date().toISOString()}};
}

function metricsData(){
  const seed=Math.floor(Date.now()/60000);
  const rng=(min,max)=>min+((seed*2654435761+min)>>>0)%(max-min+1);
  return{status:'success',data:{period:'last_7d',requests:{total:847293+rng(0,5000),per_day_avg:121042+rng(0,2000),peak_rps:847+rng(0,100),success_rate:'99.94%',p50_latency:`${rng(38,52)}ms`,p95_latency:`${rng(98,128)}ms`,p99_latency:`${rng(260,320)}ms`},queue:{processed_total:124903+rng(0,300),failed_total:12,success_rate:'99.99%',avg_latency:`${rng(120,145)}ms`,active_workers:4,jobs_per_hour:8234+rng(0,400)},uptime:{percent:'99.98%',since:'2024-01-15T00:00:00Z',incidents_30d:0,sla:'99.9%'},webhooks:{received:43821+rng(0,200),processed:43819+rng(0,200),failed:2,avg_ack_time:`${rng(42,55)}ms`}},meta:{endpoint:'/api/metrics',version:'v1',dynamic:true,timestamp:new Date().toISOString()}};
}

function queuesData(){
  const seed=Math.floor(Date.now()/60000);
  const rng=(min,max,off=0)=>min+((seed*2654435761+off)>>>0)%(max-min+1);
  return{status:'success',data:{workers:4,queues:[
    {name:'webhook-processor',type:'BullMQ',status:'running',waiting:rng(0,3),active:rng(1,4),completed:89234+rng(0,100),failed:8,dlq:3,retried:31,concurrency:8,avg_duration:`${rng(55,75)}ms`,max_attempts:3},
    {name:'notifications',type:'BullMQ',status:'running',waiting:rng(0,2),active:rng(0,2,1),completed:34291+rng(0,80),failed:4,dlq:1,retried:12,concurrency:4,avg_duration:`${rng(18,28)}ms`,max_attempts:3},
    {name:'payment-events',type:'BullMQ',status:'running',waiting:0,active:rng(0,1,2),completed:1378+rng(0,20),failed:0,dlq:0,retried:0,concurrency:2,avg_duration:`${rng(170,200)}ms`,max_attempts:5},
  ],totals:{processed:124903+rng(0,200),failed:12,dlq_total:4,retry_success_rate:'91.67%'}},meta:{endpoint:'/api/queues',version:'v1',dynamic:true,timestamp:new Date().toISOString()}};
}

function eventsData(){
  const now=Date.now();
  const seed=Math.floor(now/60000);
  const rng=(min,max,off=0)=>min+((seed*2654435761+off)>>>0)%(max-min+1);
  const types=['webhook.received','queue.job_completed','queue.job_failed','payment.processed','bot.command_executed','notification.sent','api.request','system.health_check'];
  const sources=['webhook-processor','payment-service','telegram-bot','notification-worker','api-gateway'];
  const events=Array.from({length:20},(_,i)=>({
    id:uuid(),
    type:types[(seed+i)%types.length],
    source:sources[(seed+i*3)%sources.length],
    timestamp:new Date(now-(i*rng(8000,45000,i))).toISOString(),
    duration_ms:rng(12,280,i),
    status:i===7?'failed':'success',
    meta:{attempt:1,worker_id:`worker-${(seed+i)%4+1}`}
  }));
  return{status:'success',data:{feed:events,stats:{total_24h:2847+rng(0,200),types:8,avg_per_minute:1.97+rng(0,1)*0.1}},meta:{endpoint:'/api/events',version:'v1',dynamic:true,timestamp:new Date().toISOString()}};
}

function syncSidebar(){
  document.querySelectorAll('.sb-link').forEach(el=>{
    const m=el.dataset.route===S.route||(S.route.startsWith('projects/')&&el.dataset.route==='projects/trelk-bot'&&S.route!=='projects');
    el.classList.toggle('active',m);
  });
}

function getRoute(){const m=location.pathname.match(/^\/api\/?(.*)/);return m?m[1].replace(/\/$/,''):'';}

function nav(r,p={},push=true){
  S.route=r;S.params=p;S.tab='response';S.raw=false;S.collapsed=false;
  if(push){const qs=Object.keys(p).length?'?'+new URLSearchParams(p):'';history.pushState({},'',`/api/${r}${qs}`);}
  render();
}

window.addEventListener('popstate',()=>{S.route=getRoute();S.params=Object.fromEntries(new URLSearchParams(location.search));render();});

async function render(){
  const main=document.getElementById('main'),
        tbPath=document.getElementById('tb-path'),
        tbTry=document.getElementById('tb-try');
  syncSidebar();
  if(S.route===''){tbPath.textContent=tt('api.topbar.path');tbTry.style.display='none';renderIndex();return;}
  const projMatch=S.route.match(/^projects\/(.+)$/);
  const epKey=projMatch?'projects':S.route;
  const ep=EP[epKey];
  if(!ep){render404();return;}
  const routeDisp=projMatch?`/api/projects/${projMatch[1]}`:`/api/${S.route}`;
  const qs=Object.keys(S.params).length?'?'+Object.entries(S.params).map(([k,v])=>`${k}=${v}`).join('&'):'';
  tbPath.textContent=`GET ${routeDisp}${qs}`;
  tbTry.style.display='';
  const envKey=ENV_B[epKey]||'prod';
  const TABS=[
    {id:'response',label:tt('api.route.response')},
    {id:'headers',label:tt('api.route.headers')},
    {id:'example',label:tt('api.route.examples')},
    {id:'schema',label:tt('api.route.schema')}
  ];
  main.innerHTML=`<div class="ep-view">
<div class="breadcrumb"><a href="/api" data-nav="">oscarortiz.me/api</a><span>\u203a</span><span>${esc(S.route)}</span></div>
<div class="req-bar">
<span class="req-method">GET</span>
<span class="req-url"><span class="req-base">https://oscarortiz.me</span><span class="req-route">${esc(routeDisp)}</span><span class="req-param">${esc(qs)}</span></span>
<span class="status-chip s200" id="status-chip" style="opacity:0"><svg width="7" height="7" viewBox="0 0 8 8" fill="currentColor"><circle cx="4" cy="4" r="4"/></svg>200 OK</span>
<span class="latency-chip" id="latency-chip"></span>
<span class="env-chip ${envKey==='live'?'env-live':'env-prod'}">${envKey==='live'?'live':'production'}</span>
</div>
${qpBar(epKey)}
<div class="ep-info"><span class="ep-info-icon">\u24d8</span><div>${tt(DESCS[epKey]||'')}</div></div>
<div class="tab-bar">${TABS.map(t=>`<button class="tab-btn${S.tab===t.id?' active':''}" data-tab="${t.id}">${t.label}</button>`).join('')}</div>
<div class="tab-panel-wrap"><div id="tab-panel"><div class="loading-wrap">${loadingHTML(routeDisp.replace('/api/',''))}</div></div></div>
</div>`;

  main.querySelectorAll('[data-nav]').forEach(el=>el.addEventListener('click',e=>{e.preventDefault();nav(el.dataset.nav);}));
  main.querySelectorAll('.qp-pill').forEach(tag=>{
    tag.onclick=()=>{const key=tag.dataset.key,val=tag.dataset.val,np={...S.params};tag.classList.contains('active')?delete np[key]:np[key]=val;nav(S.route,np);};
  });
  tbTry.onclick=()=>{
    tbTry.classList.add('tb-spinning');S.reqId=uuid();
    setTimeout(()=>tbTry.classList.remove('tb-spinning'),700);
    nav(S.route,S.params,false);
  };

  S.reqId=uuid();
  const latency=80+Math.floor(Math.random()*220),t0=Date.now();
  try{
    let fetched;
    if(epKey==='system'){await new Promise(r=>setTimeout(r,latency));fetched=sysData();}
    else if(epKey==='metrics'){await new Promise(r=>setTimeout(r,latency));fetched=metricsData();}
    else if(epKey==='queues'){await new Promise(r=>setTimeout(r,latency));fetched=queuesData();}
    else if(epKey==='events'){await new Promise(r=>setTimeout(r,latency));fetched=eventsData();}
    else{const res=await fetch(`/api/${ep.file}`);if(!res.ok)throw Object.assign(new Error(),{status:res.status});fetched=await res.json();}
    if(projMatch&&fetched.data){
      const found=fetched.data.find(p=>p.id===projMatch[1]);
      if(!found){render404(projMatch[1]);return;}
      fetched={status:'success',data:found,meta:{...fetched.meta,endpoint:`/api/projects/${projMatch[1]}`,total:undefined}};
    }
    if(epKey==='projects'&&Array.isArray(fetched.data)){
      let arr=[...fetched.data];
      if(S.params.status&&S.params.status!=='all')arr=arr.filter(p=>p.status===S.params.status);
      if(S.params.limit)arr=arr.slice(0,parseInt(S.params.limit));
      fetched={...fetched,data:arr,meta:{...fetched.meta,returned:arr.length}};
    }
    if(fetched.meta)fetched.meta.timestamp=new Date().toISOString();
    S.data=fetched;
    await new Promise(r=>setTimeout(r,Math.max(0,latency-(Date.now()-t0))));
    const chip=document.getElementById('status-chip'),lat=document.getElementById('latency-chip');
    if(!chip)return;
    chip.style.opacity='1';lat.textContent=`${Math.max(latency,Date.now()-t0)}ms`;
    const panel=document.getElementById('tab-panel');
    if(!panel)return;
    panel.innerHTML=tabContent(S.tab,S.data,S.reqId,epKey);
    wireTabs(S.data,epKey);wirePanelActions(S.data,epKey);
  }catch(err){if(err.status===404)render404();else render500(err);}
}

function renderIndex(){
  const rows=[
    {r:'me',              p:'/api/me',          d:tt('api.row.me'),         b:'prod'},
    {r:'projects',        p:'/api/projects',    d:tt('api.row.projects'),   b:'prod'},
    {r:'projects/trelk-bot',p:'/api/projects/:id',d:tt('api.row.projectById'),b:null},
    {r:'experience',      p:'/api/experience',  d:tt('api.row.experience'), b:'prod'},
    {r:'skills',          p:'/api/skills',      d:tt('api.row.skills'),     b:'prod'},
    {r:'contact',         p:'/api/contact',     d:tt('api.row.contact'),    b:'prod'},
    {r:'system',          p:'/api/system',      d:tt('api.row.system'),     b:'live'},
    {r:'metrics',         p:'/api/metrics',     d:tt('api.row.metrics'),    b:'live'},
    {r:'queues',          p:'/api/queues',      d:tt('api.row.queues'),     b:'live'},
    {r:'events',          p:'/api/events',      d:tt('api.row.events'),     b:'live'},
  ];
  document.getElementById('main').innerHTML=`<div class="index-page">
<div class="index-pill">${tt('api.index.pill')}</div>
<h1 class="index-title">oscarortiz.me<br/><em>${tt('api.index.titleEm')}</em></h1>
<p class="index-desc">${tt('api.index.desc')}</p>
<div class="index-baseurl"><span class="baseurl-label">${tt('api.index.baseUrl')}</span><span class="baseurl-val">https://oscarortiz.me/api</span></div>
<div class="index-meta">
<span>${tt('api.index.meta.version')}: <span>v1</span></span>
<span>${tt('api.index.meta.auth')}: <span>${tt('api.index.meta.none')}</span></span>
<span>${tt('api.index.meta.format')}: <span>application/json</span></span>
<span>${tt('api.index.meta.methods')}: <span style="color:var(--green)">GET</span></span>
<span>${tt('api.index.meta.endpoints')}: <span>10</span></span>
</div>
<div class="ep-grid">${rows.map(ep=>`<a href="/api/${ep.r}" class="ep-row" data-nav="${ep.r}"><span class="ep-row-method">GET</span><span class="ep-row-path">${ep.p.replace(':id','<em>:id</em>')}</span><span class="ep-row-desc">${ep.d}</span>${ep.b?`<span class="sb-badge ${ep.b}">${ep.b==='live'?tt('api.badge.live'):tt('api.badge.production')}</span>`:''}</a>`).join('')}</div>
<div class="curl-box">
<div class="curl-head"><div class="curl-dot"></div>${tt('api.index.quickstart')}</div>
<div class="curl-body"><span style="color:var(--text-2)">$</span> curl <span style="color:var(--j-str)">https://oscarortiz.me/api/me.json</span>
<span style="color:var(--text-2)">$</span> curl <span style="color:var(--j-str)">https://oscarortiz.me/api/projects.json</span>
<span style="color:var(--text-2)">$</span> curl <span style="color:var(--j-str)">"https://oscarortiz.me/api/projects.json"</span> | jq <span style="color:var(--j-str)">'.data[0].name'</span></div>
</div></div>`;
  document.getElementById('main').querySelectorAll('[data-nav]').forEach(el=>el.addEventListener('click',e=>{e.preventDefault();nav(el.dataset.nav);}));
}

function render404(id){
  const payload={status:'error',error:{code:404,message:id?`Project '${id}' not found`:'Endpoint not found',docs:'https://oscarortiz.me/api'},meta:{timestamp:new Date().toISOString()}};
  document.getElementById('main').innerHTML=`<div class="ep-view"><div class="error-card e404"><div class="error-head"><div class="error-code">404</div><div class="error-text"><h3>${tt('api.error.notFound')}</h3><p>${id?`Project <strong>"${esc(id)}"</strong> not found.`:tt('api.error.endpointMissing')}</p></div></div><div class="json-body"><pre class="json-wrap">${syntaxHL(payload)}</pre></div></div><div style="margin-top:14px;font-size:12px;font-family:var(--font-mono)"><a href="/api" data-nav="" style="color:var(--accent);text-decoration:none">${tt('api.error.back')}</a></div></div>`;
  document.getElementById('main').querySelectorAll('[data-nav]').forEach(el=>el.addEventListener('click',e=>{e.preventDefault();nav(el.dataset.nav);}));
}

function render500(err){
  const payload={status:'error',error:{code:500,message:'Internal server error',details:String(err),retryable:true},meta:{timestamp:new Date().toISOString()}};
  document.getElementById('main').innerHTML=`<div class="ep-view"><div class="error-card e500"><div class="error-head"><div class="error-code">500</div><div class="error-text"><h3>${tt('api.error.serverError')}</h3><p>${tt('api.error.serverErrorDesc')}</p></div></div><div class="json-body"><pre class="json-wrap">${syntaxHL(payload)}</pre></div></div><div style="margin-top:14px;font-size:12px;font-family:var(--font-mono)"><a href="/api" data-nav="" style="color:var(--accent);text-decoration:none">${tt('api.error.back')}</a></div></div>`;
  document.getElementById('main').querySelectorAll('[data-nav]').forEach(el=>el.addEventListener('click',e=>{e.preventDefault();nav(el.dataset.nav);}));
}

function resHeadersHTML(size,reqId){
  const now=new Date().toUTCString();
  return`<div class="hdr-section-label">Response Headers</div>
<table class="hdrs-table">
<tr class="hdr-hi"><td>x-powered-by</td><td>fastify</td></tr>
<tr><td>x-request-id</td><td>${reqId}</td></tr>
<tr><td>x-api-version</td><td>v1</td></tr>
<tr><td>x-response-time</td><td>${Math.floor(Math.random()*12)+2}ms</td></tr>
<tr><td>cache-control</td><td>public, max-age=3600, stale-while-revalidate=60</td></tr>
<tr><td>access-control-allow-origin</td><td>*</td></tr>
<tr><td>access-control-allow-methods</td><td>GET, HEAD, OPTIONS</td></tr>
<tr><td>vary</td><td>Accept-Encoding</td></tr>
<tr><td>date</td><td>${now}</td></tr>
<tr><td>content-length</td><td>${size} bytes</td></tr>
</table>
<div class="hdr-section-label" style="margin-top:1px">Request Headers</div>
<table class="hdrs-table">
<tr><td>accept</td><td>application/json</td></tr>
<tr><td>accept-encoding</td><td>gzip, deflate, br</td></tr>
<tr><td>accept-language</td><td>es-CO, es;q=0.9, en;q=0.8</td></tr>
<tr><td>user-agent</td><td>oscarortiz.me/API-Explorer v2.0</td></tr>
<tr><td>cache-control</td><td>no-cache</td></tr>
<tr class="hdr-hi"><td>x-request-id</td><td>${reqId}</td></tr>
</table>`;
}

function buildExamples(route,params){
  const qs=Object.keys(params).length?'?'+Object.entries(params).map(([k,v])=>`${k}=${v}`).join('&'):'';
  const url=`${BASE}/api/${route}${qs}`;
  return{
    curl:`<span class="cx-cm"># cURL \u2014 basic</span>
<span class="cx-kv">$</span> curl <span class="cx-str">"${url}"</span>

<span class="cx-cm"># With headers</span>
<span class="cx-kv">$</span> curl <span class="cx-str">"${url}"</span> \\
  -H <span class="cx-str">"Accept: application/json"</span> \\
  -H <span class="cx-str">"Cache-Control: no-cache"</span> \\
  -w <span class="cx-str">"\\nHTTP %{http_code} \u00b7 %{time_total}s"</span>`,
    fetch:`<span class="cx-cm">// Fetch API (Browser / Node.js)</span>
<span class="cx-kw">const</span> res = <span class="cx-kw">await</span> <span class="cx-fn">fetch</span>(<span class="cx-str">'${url}'</span>, {
  method: <span class="cx-str">'GET'</span>,
  headers: { <span class="cx-str">'Accept'</span>: <span class="cx-str">'application/json'</span> },
});
<span class="cx-kw">if</span> (!res.<span class="cx-fn">ok</span>) <span class="cx-kw">throw new</span> <span class="cx-fn">Error</span>(\`HTTP \${res.status}\`);
<span class="cx-kw">const</span> { status, data, meta } = <span class="cx-kw">await</span> res.<span class="cx-fn">json</span>();
console.<span class="cx-fn">log</span>(data);`,
    node:`<span class="cx-cm">// Node.js \u2014 axios</span>
<span class="cx-kw">import</span> axios <span class="cx-kw">from</span> <span class="cx-str">'axios'</span>;
<span class="cx-kw">const</span> { data: payload } = <span class="cx-kw">await</span> axios.<span class="cx-fn">get</span>(<span class="cx-str">'${url}'</span>, {
  headers: { Accept: <span class="cx-str">'application/json'</span> },
  timeout: 5000,
});
console.<span class="cx-fn">log</span>(payload.data);`,
    python:`<span class="cx-cm"># Python \u2014 httpx</span>
<span class="cx-kw">import</span> httpx
res = httpx.<span class="cx-fn">get</span>(<span class="cx-str">"${url}"</span>)
res.<span class="cx-fn">raise_for_status</span>()
data = res.<span class="cx-fn">json</span>()
<span class="cx-fn">print</span>(data[<span class="cx-str">"data"</span>])`,
  };
}

function tabContent(tab,dt,reqId,epKey){
  if(tab==='response')return renderResponse(dt);
  if(tab==='headers') return dt?`<div style="padding:0">${resHeadersHTML(JSON.stringify(dt).length,reqId)}</div>`:`<div class="loading-wrap">${loadingHTML('...')}</div>`;
  if(tab==='example') return renderExample(S.route,S.params);
  if(tab==='schema')  return renderSchema(epKey);
  return '';
}

function renderResponse(dt){
  if(!dt)return`<div class="loading-wrap">${loadingHTML('...')}</div>`;
  const display=S.raw?`<span style="color:var(--text-2)">${esc(JSON.stringify(dt,null,2))}</span>`:syntaxHL(dt);
  return`<div class="panel-tools">
<button class="pt-btn${!S.raw?' active':''}" id="pt-pretty">Pretty</button>
<button class="pt-btn${S.raw?' active':''}" id="pt-raw">Raw</button>
<div class="pt-sep"></div>
<button class="pt-btn${S.collapsed?' active':''}" id="pt-collapse">${S.collapsed?'Expand':'Collapse'}</button>
<div class="pt-sep"></div>
<button class="pt-btn" id="pt-copy">Copy JSON</button>
</div>
<div class="json-body${S.collapsed?' collapsed':''}" id="json-body">
<pre class="json-wrap">${display}</pre>
</div>`;
}

function renderExample(route,params){
  const exs=buildExamples(route,params);
  const langs={curl:'cURL',fetch:'Fetch',node:'Node.js',python:'Python'};
  return`<div class="example-wrap">
<div class="lang-btns">${Object.entries(langs).map(([k,v])=>`<button class="lang-btn${S.lang===k?' active':''}" data-lang="${k}">${v}</button>`).join('')}</div>
<div class="code-block" id="ex-code">${exs[S.lang]}</div>
</div>`;
}

function renderSchema(epKey){
  const sc=SCHEMAS[epKey];
  if(!sc)return`<div style="padding:20px;font-family:var(--font-mono);font-size:12px;color:var(--muted)">No schema available.</div>`;
  return`<div class="schema-body"><pre class="schema-pre">${sc}</pre></div>`;
}

function loadingHTML(label){
  const ws=[84,62,76,55,71,88,48];
  return`<div class="loading-msg"><div class="loading-dot"></div>Fetching from /api/${label}...</div>${ws.map(w=>`<div class="loading-bar" style="width:${w}%"></div>`).join('')}`;
}

function qpBar(epKey){
  const qps=QPS[epKey];if(!qps)return'';
  const pills=qps.flatMap(g=>g.values.map(v=>{
    const on=S.params[g.key]===v;
    return`<button class="qp-pill${on?' active':''}" data-key="${g.key}" data-val="${v}"><span class="qp-key">?${g.key}</span><span class="qp-eq">=</span><span class="qp-val">${v}</span></button>`;
  })).join('');
  return`<div class="qp-bar"><span class="qp-label">Params</span>${pills}</div>`;
}

function wirePanelActions(dt,epKey){
  const pt=document.getElementById('pt-pretty'),
        pr=document.getElementById('pt-raw'),
        pc=document.getElementById('pt-collapse'),
        py=document.getElementById('pt-copy');
  if(pt)pt.onclick=()=>{S.raw=false;refreshTab(dt,epKey);};
  if(pr)pr.onclick=()=>{S.raw=true;refreshTab(dt,epKey);};
  if(pc)pc.onclick=()=>{S.collapsed=!S.collapsed;refreshTab(dt,epKey);};
  if(py)py.onclick=()=>{
    navigator.clipboard.writeText(JSON.stringify(dt,null,2)).then(()=>{
      py.textContent='\u2713 Copied!';py.classList.add('ok');
      setTimeout(()=>{py.textContent='Copy JSON';py.classList.remove('ok');},1800);
    });
  };
  document.querySelectorAll('.lang-btn').forEach(btn=>{
    btn.onclick=()=>{
      S.lang=btn.dataset.lang;
      document.querySelectorAll('.lang-btn').forEach(b=>b.classList.toggle('active',b.dataset.lang===S.lang));
      const el=document.getElementById('ex-code');
      if(el)el.innerHTML=buildExamples(S.route,S.params)[S.lang];
    };
  });
}

function refreshTab(dt,epKey){
  const wrap=document.getElementById('tab-panel');
  if(wrap){wrap.innerHTML=tabContent(S.tab,dt,S.reqId,epKey);wirePanelActions(dt,epKey);}
}

function wireTabs(dt,epKey){
  document.querySelectorAll('.tab-btn').forEach(btn=>{
    btn.onclick=()=>{
      S.tab=btn.dataset.tab;
      document.querySelectorAll('.tab-btn').forEach(b=>b.classList.toggle('active',b.dataset.tab===S.tab));
      const wrap=document.getElementById('tab-panel');
      if(wrap){wrap.innerHTML=tabContent(S.tab,dt,S.reqId,epKey);wirePanelActions(dt,epKey);}
    };
  });
}

document.querySelectorAll('.sb-link').forEach(el=>el.addEventListener('click',()=>nav(el.dataset.route)));
S.route=getRoute();S.params=Object.fromEntries(new URLSearchParams(location.search));render();
</script>
</body>
</html>