{"id":1103,"date":"2025-11-10T20:04:54","date_gmt":"2025-11-10T20:04:54","guid":{"rendered":"https:\/\/tree-of-mylife.com\/?page_id=1103"},"modified":"2026-01-01T19:31:26","modified_gmt":"2026-01-01T19:31:26","slug":"family-tree","status":"publish","type":"page","link":"https:\/\/tree-of-mylife.com\/es\/family-tree\/","title":{"rendered":"Family Tree"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" \/>\n  <title>Private \u00b7 Family Tree<\/title>\n<style>\n    \/* ========= COLORES Y BASE ========= *\/\n    :root{\n      --gold:#e5c86a;\n      --gold-2:#caa43e;\n      --glass:rgba(12,14,20,.55);\n      --glass-strong:rgba(12,14,20,.75);\n      --text:#f7f7fb;\n      --muted:#d7d9df;\n      --accent:#7bd8ff;\n      --shadow:0 12px 30px rgba(0,0,0,.45);\n    }\n\n    html,body{\n      margin:0;\n      padding:0;\n      height:100%;\n    }\n\n    body{\n      font-family: system-ui, -apple-system, Segoe UI, Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif;\n      color:var(--text);\n      background:#000;\n      overflow:auto;\n    }\n\n    \/* Fondo: \u00e1rbol a pantalla completa *\/\n    body::before{\n      content:\"\";\n      position:fixed;\n      inset:0;\n      z-index:-2;\n      background:center \/ cover no-repeat url(\"https:\/\/tree-of-mylife.com\/wp-content\/uploads\/2025\/11\/ChatGPT-Image-Nov-10-2025-01_12_24-PM.png\");\n    }\n\n    body::after{\n      content:\"\";\n      position:fixed;\n      inset:0;\n      z-index:-1;\n      pointer-events:none;\n      background:\n        radial-gradient(120% 120% at 50% 60%, transparent 45%, rgba(0,0,0,.32) 80%),\n        radial-gradient(80% 60% at 50% 20%, rgba(255,204,64,.10), transparent 70%);\n    }\n\n    main.tree-page{\n      min-height:1600px;\n      position:relative;\n      padding:12px;\n    }\n\n    \/* ========= HUD SUPERIOR ========= *\/\n    .hud{\n      position:sticky;\n      top:10px;\n      right:10px;\n      margin-left:auto;\n      max-width:900px;\n      display:flex;\n      justify-content:flex-end;\n      z-index:5;\n    }\n\n    .hud-panel{\n      background:var(--glass-strong);\n      border-radius:16px;\n      border:1px solid rgba(229,200,106,.7);\n      padding:10px 14px;\n      backdrop-filter:blur(12px) saturate(130%);\n      -webkit-backdrop-filter:blur(12px) saturate(130%);\n      box-shadow:0 10px 26px rgba(0,0,0,.5);\n    }\n\n    .hud-row{\n      display:flex;\n      flex-wrap:wrap;\n      gap:8px;\n      align-items:center;\n    }\n\n    .hud label{\n      font-size:12px;\n      color:var(--muted);\n      margin-right:6px;\n      white-space:nowrap;\n    }\n\n    .hud input[type=\"text\"],\n    .hud select{\n      border-radius:10px;\n      border:1px solid rgba(255,255,255,.35);\n      padding:7px 9px;\n      background:rgba(8,10,20,.9);\n      color:var(--text);\n      font-size:13px;\n    }\n\n    .hud select option{\n      background:#111827;\n      color:#f9fafb;\n    }\n\n    .hud input[type=\"checkbox\"]{\n      width:auto;\n      height:auto;\n      margin-left:2px;\n      cursor:pointer;\n    }\n\n    .btn{\n      font:inherit;\n      font-size:12px;\n      border-radius:999px;\n      border:0;\n      padding:6px 12px;\n      cursor:pointer;\n      background:linear-gradient(180deg,#ffeaa0,#e6c868);\n      color:#111;\n      box-shadow:inset 0 0 0 1px var(--gold-2), 0 4px 12px rgba(0,0,0,.35);\n      white-space:nowrap;\n    }\n\n    .btn.ghost{\n      background:rgba(255,255,255,.12);\n      color:var(--text);\n      box-shadow:inset 0 0 0 1px rgba(255,255,255,.35);\n    }\n\n    \/* ========= ESCENARIO Y NODOS ========= *\/\n    .stage{\n      position:relative;\n      margin-top:10px;\n      min-height:1500px;\n      border-radius:18px;\n      border:1px solid rgba(255,255,255,.18);\n      box-shadow:0 18px 40px rgba(0,0,0,.6);\n      overflow:visible;\n    }\n\n    #links{\n      position:absolute;\n      inset:0;\n      width:100%;\n      height:100%;\n      pointer-events:none;\n    }\n\n    .link{\n      stroke:rgba(255,255,255,.45);\n      stroke-width:1.8;\n      stroke-linecap:round;\n      fill:none;\n      filter:url(#glow);\n    }\n\n    body.delete-lines #links{\n      pointer-events:auto;\n    }\n    body.delete-lines .link{\n      cursor:pointer;\n      stroke:rgba(255,120,120,.95);\n    }\n\n    \/* ==== TARJETAS NODOS ==== *\/\n    .node{\n      position:absolute;\n      min-width:190px;\n      max-width:260px;\n      padding:10px 10px 8px;\n      background:var(--glass);\n      border-radius:16px;\n      border:1px solid rgba(229,200,106,.75);\n      box-shadow:var(--shadow);\n      backdrop-filter:blur(10px) saturate(130%);\n      -webkit-backdrop-filter:blur(10px) saturate(130%);\n      cursor:grab;\n      user-select:none;\n      transition:box-shadow .15s ease, transform .15s ease;\n    }\n\n    .node.dragging{\n      cursor:grabbing;\n      box-shadow:0 14px 30px rgba(0,0,0,.7);\n      transform:translateZ(0) scale(1.02);\n    }\n\n    .node-row{\n      display:flex;\n      gap:10px;\n      align-items:center;\n    }\n\n    .avatar{\n      width:52px;\n      height:52px;\n      border-radius:999px;\n      background:#111 center\/cover no-repeat;\n      border:1px solid rgba(229,200,106,.85);\n      box-shadow:inset 0 0 0 1px rgba(0,0,0,.45);\n      flex-shrink:0;\n      cursor:pointer;\n    }\n\n    .node h3{\n      margin:0 0 2px;\n      font-size:17px;\n      letter-spacing:.3px;\n      color:#ffffff;\n      -webkit-text-stroke:0.7px rgba(255,255,255,.35);\n      text-shadow:\n        0 0 4px rgba(255,255,255,.35),\n        0 0 10px rgba(255,255,255,.25);\n    }\n\n    .node .small{\n      font-size:12px;\n      color:rgba(255,255,255,.85);\n      line-height:1.2;\n      text-shadow:0 0 3px rgba(0,0,0,.4);\n    }\n\n    .node-actions{\n      margin-top:7px;\n      display:flex;\n      flex-wrap:wrap;\n      gap:4px;\n    }\n\n    .node .btn{\n      font-size:10px;\n      padding:3px 7px;\n    }\n\n    .node.pending-connect{\n      box-shadow:0 0 0 2px var(--accent), var(--shadow);\n    }\n\n    \/* ==== TAMA\u00d1OS ==== *\/\n    .node.node-small{\n      min-width:140px;\n      max-width:180px;\n      padding:6px 6px 4px;\n    }\n    .node.node-small h3{ font-size:14px; }\n\n    .node.node-tiny{\n      min-width:110px;\n      max-width:150px;\n      padding:4px 4px 3px;\n    }\n    .node.node-tiny h3{ font-size:11px; }\n\n    \/* ==== SUPERMINI MUY PEQUE\u00d1O: SOLO NOMBRE Y PARENTESCO ==== *\/\n    .node.node-supermini{\n      min-width:65px;\n      max-width:90px;\n      padding:1px 2px 1px;\n      background:transparent;\n      border:none;\n      box-shadow:none;\n    }\n    \/* Sin foto *\/\n    .node.node-supermini .avatar{\n      display:none;\n    }\n    \/* Contenido muy compacto *\/\n    .node.node-supermini .node-row{\n      gap:2px;\n    }\n    \/* Nombre muy peque\u00f1o y pegadito *\/\n    .node.node-supermini h3{\n      font-size:9px;\n      line-height:1.05;\n      margin:0;\n      -webkit-text-stroke:0;\n      text-shadow:\n        0 0 3px rgba(217,217,217,.25),\n        0 0 6px rgba(217,217,217,.2);\n    }\n    \/* Parentesco debajo, tambi\u00e9n mini *\/\n    .node.node-supermini .small{\n      font-size:7px;\n      line-height:1.05;\n      margin:0;\n      -webkit-text-stroke:0;\n      text-shadow:\n        0 0 3px rgba(217,217,217,.25),\n        0 0 6px rgba(217,217,217,.2);\n    }\n    \/* Acciones ocultas para no ocupar espacio *\/\n    .node.node-supermini .node-actions{\n      margin-top:1px;\n      max-height:0;\n      overflow:hidden;\n      opacity:0;\n      pointer-events:none;\n    }\n    \/* Al pasar el mouse, aparecen los botones (por si quieres editar o cambiar tama\u00f1o) *\/\n    .node.node-supermini:hover .node-actions{\n      max-height:200px;\n      opacity:1;\n      pointer-events:auto;\n    }\n\n    .helper{\n      position:fixed;\n      left:14px;\n      bottom:14px;\n      font-size:12px;\n      color:#f9fafb;\n      background:rgba(0,0,0,.55);\n      border-radius:10px;\n      border:1px solid rgba(255,255,255,.25);\n      padding:6px 10px;\n      z-index:6;\n      max-width:260px;\n    }\n\n    @media (max-width:900px){\n      .stage{ min-height:1800px; }\n      .node{ min-width:170px; }\n    }\n\n    \/* ========= OVERLAYS ========= *\/\n    .overlay{\n      position:fixed;\n      inset:0;\n      background:rgba(0,0,0,.75);\n      display:flex;\n      align-items:center;\n      justify-content:center;\n      z-index:9999;\n    }\n    .overlay.hidden{ display:none; }\n\n    .overlay-inner{\n      background:#020617;\n      padding:8px;\n      border-radius:12px;\n      border:1px solid rgba(229,200,106,.6);\n      max-width:90vw;\n      max-height:80vh;\n      box-shadow:0 18px 40px rgba(0,0,0,.8);\n    }\n\n    .overlay-header{\n      display:flex;\n      justify-content:space-between;\n      align-items:center;\n      gap:8px;\n      margin-bottom:6px;\n      font-size:14px;\n    }\n    .overlay-header button{\n      border:none;\n      background:transparent;\n      color:#f9fafb;\n      font-size:18px;\n      cursor:pointer;\n    }\n\n    #videoPlayer{\n      max-width:100%;\n      max-height:70vh;\n      border-radius:10px;\n      background:#000;\n    }\n\n    \/* ========= GALER\u00cdA DE FOTOS ========= *\/\n    #photoView{\n      max-width:100%;\n      max-height:55vh;\n      border-radius:10px;\n      background:#000;\n      display:block;\n      margin:0 auto 6px;\n    }\n\n    .photo-nav{\n      display:flex;\n      justify-content:space-between;\n      align-items:center;\n      margin-bottom:6px;\n      gap:8px;\n    }\n    .photo-nav button{\n      border-radius:999px;\n      border:1px solid rgba(255,255,255,.3);\n      background:rgba(15,23,42,.9);\n      color:#f9fafb;\n      padding:3px 10px;\n      font-size:12px;\n      cursor:pointer;\n    }\n    .photo-nav button:disabled{\n      opacity:.4;\n      cursor:default;\n    }\n    .photo-counter{\n      font-size:12px;\n      opacity:.8;\n    }\n\n    .photo-actions{\n      display:flex;\n      justify-content:space-between;\n      align-items:center;\n      margin-bottom:6px;\n      gap:8px;\n    }\n    .photo-actions button{\n      border-radius:999px;\n      border:1px solid rgba(255,255,255,.3);\n      background:rgba(15,23,42,.9);\n      color:#f9fafb;\n      padding:4px 10px;\n      font-size:12px;\n      cursor:pointer;\n    }\n\n    .photo-thumbs{\n      display:flex;\n      flex-wrap:wrap;\n      gap:4px;\n      max-height:100px;\n      overflow-y:auto;\n      padding-top:4px;\n    }\n    .photo-thumbs img{\n      width:48px;\n      height:48px;\n      object-fit:cover;\n      border-radius:6px;\n      cursor:pointer;\n      opacity:.6;\n      border:1px solid transparent;\n    }\n    .photo-thumbs img.active{\n      opacity:1;\n      border-color:var(--gold);\n    }\n\n    \/* ========= LISTA TIPO ARCHIVOS (files, audio, video) ========= *\/\n    .file-actions{\n      display:flex;\n      justify-content:space-between;\n      align-items:center;\n      gap:8px;\n      margin-bottom:8px;\n    }\n    .file-actions button{\n      border-radius:999px;\n      border:1px solid rgba(255,255,255,.3);\n      background:rgba(15,23,42,.9);\n      color:#f9fafb;\n      padding:4px 10px;\n      font-size:12px;\n      cursor:pointer;\n    }\n\n    .file-list{\n      list-style:none;\n      padding:0;\n      margin:0;\n      max-height:220px;\n      overflow-y:auto;\n      font-size:13px;\n    }\n    .file-item{\n      display:flex;\n      justify-content:space-between;\n      align-items:center;\n      gap:8px;\n      padding:6px 8px;\n      border-radius:8px;\n      background:rgba(15,23,42,.8);\n      margin-bottom:4px;\n    }\n    .file-main{\n      display:flex;\n      flex-direction:column;\n      gap:2px;\n    }\n    .file-name{\n      font-weight:500;\n    }\n    .file-meta{\n      font-size:11px;\n      color:rgba(226,232,240,.8);\n    }\n    .file-buttons{\n      display:flex;\n      gap:4px;\n    }\n    .file-buttons button,\n    .file-buttons a{\n      border-radius:999px;\n      border:1px solid rgba(255,255,255,.3);\n      background:rgba(15,23,42,.9);\n      color:#f9fafb;\n      padding:3px 8px;\n      font-size:11px;\n      cursor:pointer;\n      text-decoration:none;\n      white-space:nowrap;\n    }\n<\/style>\n\n<\/head>\n<body>\n  <main class=\"tree-page\">\n    <!-- HUD -->\n    <div class=\"hud\">\n      <div class=\"hud-panel\">\n        <div class=\"hud-row\">\n          <label for=\"nameInput\">Add member:<\/label>\n          <input id=\"nameInput\" type=\"text\" placeholder=\"Full name\" \/>\n          <select id=\"relInput\" title=\"Relationship\">\n            <option value=\"You\">You<\/option>\n            <option value=\"Mother\">Mother<\/option>\n            <option value=\"Father\">Father<\/option>\n            <option value=\"Spouse\">Spouse<\/option>\n            <option value=\"Partner\">Partner<\/option>\n\n            <option value=\"Son\">Son<\/option>\n            <option value=\"Daughter\">Daughter<\/option>\n            <option value=\"Stepson\">Stepson<\/option>\n            <option value=\"Stepdaughter\">Stepdaughter<\/option>\n\n            <option value=\"Brother\">Brother<\/option>\n            <option value=\"Sister\">Sister<\/option>\n            <option value=\"Half-brother\">Half-brother<\/option>\n            <option value=\"Half-sister\">Half-sister<\/option>\n            <option value=\"Stepbrother\">Stepbrother<\/option>\n            <option value=\"Stepsister\">Stepsister<\/option>\n\n            <option value=\"Grandmother (Maternal)\">Grandmother (Maternal)<\/option>\n            <option value=\"Grandfather (Maternal)\">Grandfather (Maternal)<\/option>\n            <option value=\"Grandmother (Paternal)\">Grandmother (Paternal)<\/option>\n            <option value=\"Grandfather (Paternal)\">Grandfather (Paternal)<\/option>\n\n            <option value=\"Great-grandfather\">Great-grandfather<\/option>\n            <option value=\"Great-grandmother\">Great-grandmother<\/option>\n\n            <option value=\"Uncle\">Uncle<\/option>\n            <option value=\"Aunt\">Aunt<\/option>\n            <option value=\"Nephew\">Nephew<\/option>\n            <option value=\"Niece\">Niece<\/option>\n            <option value=\"Cousin\">Cousin<\/option>\n\n            <option value=\"Godson\">Godson<\/option>\n            <option value=\"Goddaughter\">Goddaughter<\/option>\n\n            <option value=\"Stepfather\">Stepfather<\/option>\n            <option value=\"Stepmother\">Stepmother<\/option>\n\n            <option value=\"Sister-in-law\">Sister-in-law<\/option>\n            <option value=\"Brother-in-law\">Brother-in-law<\/option>\n            <option value=\"Mother-in-law\">Mother-in-law<\/option>\n            <option value=\"Father-in-law\">Father-in-law<\/option>\n            <option value=\"Daughter-in-law\">Daughter-in-law<\/option>\n            <option value=\"Son-in-law\">Son-in-law<\/option>\n            <option value=\"Ex-wife\">Ex-wife<\/option>\n            <option value=\"Ex-husband\">Ex-husband<\/option>\n\n            <option value=\"Other\">Other<\/option>\n          <\/select>\n\n          <button class=\"btn\" id=\"addBtn\">Add<\/button>\n          <button class=\"btn ghost\" id=\"connectBtn\" title=\"Connect two cards with a line\">Connect<\/button>\n          <button class=\"btn ghost\" id=\"deleteLinesBtn\" title=\"Click a line to remove it\">Delete lines<\/button>\n          <button class=\"btn ghost\" id=\"resetBtn\" title=\"Clear local data &#038; reset layout\">Reset<\/button>\n\n          <label for=\"compactToggle\">Compact<\/label>\n          <input type=\"checkbox\" id=\"compactToggle\" \/>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <!-- Escenario -->\n    <div class=\"stage\" id=\"stage\">\n      <svg id=\"links\">\n        <defs>\n          <filter id=\"glow\" x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n            <feGaussianBlur stdDeviation=\"1.2\" result=\"blur\" \/>\n            <feMerge>\n              <feMergeNode in=\"blur\" \/>\n              <feMergeNode in=\"SourceGraphic\" \/>\n            <\/feMerge>\n          <\/filter>\n        <\/defs>\n      <\/svg>\n    <\/div>\n\n    <div class=\"helper\">\n      Drag the cards to arrange your family tree.  \n      Buttons: Edit \u00b7 Dates \u00b7 Photo \u00b7 Files \u00b7 Audio \u00b7 Video \u00b7 Size \u00b7 Duplicate \u00b7 Delete.  \n      Photos and videos are not stored on a server, only locally or as links you add.  \n      Your tree is saved locally in this browser.\n    <\/div>\n\n    <!-- inputs ocultos -->\n    <input type=\"file\" id=\"photoInput\" accept=\"image\/*\" style=\"display:none\" \/>\n    <input type=\"file\" id=\"audioInput\" accept=\"audio\/*\" style=\"display:none\" \/>\n    <input type=\"file\" id=\"fileInput\" style=\"display:none\" \/>\n\n    <!-- overlay video (reproductor para data URLs antiguos) -->\n    <div id=\"videoOverlay\" class=\"overlay hidden\">\n      <div class=\"overlay-inner\">\n        <div class=\"overlay-header\">\n          <span>Video<\/span>\n          <button id=\"closeVideo\" aria-label=\"Close\">\u00d7<\/button>\n        <\/div>\n        <video id=\"videoPlayer\" controls><\/video>\n      <\/div>\n    <\/div>\n\n    <!-- overlay fotos (galer\u00eda) -->\n    <div id=\"photoOverlay\" class=\"overlay hidden\">\n      <div class=\"overlay-inner\">\n        <div class=\"overlay-header\">\n          <span id=\"photoTitle\">Photos<\/span>\n          <button id=\"closePhoto\" aria-label=\"Close\">\u00d7<\/button>\n        <\/div>\n\n        <img id=\"photoView\" alt=\"Photo\" \/>\n\n        <div class=\"photo-nav\">\n          <button id=\"prevPhoto\" type=\"button\">\u25c0 Prev<\/button>\n          <div class=\"photo-counter\" id=\"photoCounter\">0 \/ 0<\/div>\n          <button id=\"nextPhoto\" type=\"button\">Next \u25b6<\/button>\n        <\/div>\n\n        <div class=\"photo-actions\">\n          <button id=\"addPhotoBtn\" type=\"button\">+ Add photo<\/button>\n          <button id=\"deletePhotoBtn\" type=\"button\">Delete photo<\/button>\n        <\/div>\n\n        <div class=\"photo-thumbs\" id=\"photoThumbs\"><\/div>\n      <\/div>\n    <\/div>\n\n    <!-- overlay archivos -->\n    <div id=\"fileOverlay\" class=\"overlay hidden\">\n      <div class=\"overlay-inner\">\n        <div class=\"overlay-header\">\n          <span id=\"fileTitle\">Files<\/span>\n          <button id=\"closeFile\" aria-label=\"Close\">\u00d7<\/button>\n        <\/div>\n\n        <div class=\"file-actions\">\n          <button id=\"addFileBtn\" type=\"button\">+ Add file<\/button>\n        <\/div>\n\n        <ul id=\"fileList\" class=\"file-list\"><\/ul>\n      <\/div>\n    <\/div>\n\n    <!-- overlay audio -->\n    <div id=\"audioOverlay\" class=\"overlay hidden\">\n      <div class=\"overlay-inner\">\n        <div class=\"overlay-header\">\n          <span id=\"audioTitle\">Audio<\/span>\n          <button id=\"closeAudio\" aria-label=\"Close\">\u00d7<\/button>\n        <\/div>\n\n        <div class=\"file-actions\">\n          <button id=\"addAudioBtn\" type=\"button\">+ Add audio<\/button>\n        <\/div>\n\n        <ul id=\"audioList\" class=\"file-list\"><\/ul>\n      <\/div>\n    <\/div>\n\n    <!-- overlay lista de videos (por enlaces) -->\n    <div id=\"videoListOverlay\" class=\"overlay hidden\">\n      <div class=\"overlay-inner\">\n        <div class=\"overlay-header\">\n          <span id=\"videoListTitle\">Videos<\/span>\n          <button id=\"closeVideoList\" aria-label=\"Close\">\u00d7<\/button>\n        <\/div>\n\n        <div class=\"file-actions\">\n          <button id=\"addVideoBtn\" type=\"button\">+ Add video link<\/button>\n        <\/div>\n\n        <ul id=\"videoList\" class=\"file-list\"><\/ul>\n      <\/div>\n    <\/div>\n  <\/main>\n\n  <script>\n    const stage = document.getElementById('stage');\n    const svg   = document.getElementById('links');\n    const nameInput = document.getElementById('nameInput');\n    const relInput  = document.getElementById('relInput');\n    const addBtn    = document.getElementById('addBtn');\n    const resetBtn  = document.getElementById('resetBtn');\n    const photoInput = document.getElementById('photoInput');\n    const audioInput = document.getElementById('audioInput');\n    const fileInput  = document.getElementById('fileInput');\n    const compactToggle = document.getElementById('compactToggle');\n    const connectBtn = document.getElementById('connectBtn');\n    const deleteLinesBtn = document.getElementById('deleteLinesBtn');\n\n    const videoOverlay = document.getElementById('videoOverlay');\n    const videoPlayer  = document.getElementById('videoPlayer');\n    const closeVideo   = document.getElementById('closeVideo');\n\n    const photoOverlay = document.getElementById('photoOverlay');\n    const photoView    = document.getElementById('photoView');\n    const closePhoto   = document.getElementById('closePhoto');\n    const photoTitle   = document.getElementById('photoTitle');\n    const prevPhotoBtn = document.getElementById('prevPhoto');\n    const nextPhotoBtn = document.getElementById('nextPhoto');\n    const photoCounter = document.getElementById('photoCounter');\n    const addPhotoBtn  = document.getElementById('addPhotoBtn');\n    const deletePhotoBtn = document.getElementById('deletePhotoBtn');\n    const photoThumbs  = document.getElementById('photoThumbs');\n\n    const fileOverlay  = document.getElementById('fileOverlay');\n    const fileTitle    = document.getElementById('fileTitle');\n    const closeFile    = document.getElementById('closeFile');\n    const addFileBtn   = document.getElementById('addFileBtn');\n    const fileList     = document.getElementById('fileList');\n\n    const audioOverlay = document.getElementById('audioOverlay');\n    const audioTitle   = document.getElementById('audioTitle');\n    const closeAudio   = document.getElementById('closeAudio');\n    const addAudioBtn  = document.getElementById('addAudioBtn');\n    const audioList    = document.getElementById('audioList');\n\n    const videoListOverlay = document.getElementById('videoListOverlay');\n    const videoListTitle   = document.getElementById('videoListTitle');\n    const closeVideoList   = document.getElementById('closeVideoList');\n    const addVideoBtn      = document.getElementById('addVideoBtn');\n    const videoListEl      = document.getElementById('videoList');\n\n    const STORAGE_KEY = 'family_tree_v1';\n\n    const DEFAULT_NODES = [\n      { id:'you',  name:'Your Name', rel:'You',       x:50, y:55, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' },\n      { id:'mom',  name:'Mother',    rel:'Mother',    x:32, y:35, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' },\n      { id:'dad',  name:'Father',    rel:'Father',    x:68, y:35, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' },\n      { id:'m_gm', name:'Grandmother', rel:'Grandmother (Maternal)', x:22, y:20, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' },\n      { id:'m_gf', name:'Grandfather', rel:'Grandfather (Maternal)', x:42, y:20, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' },\n      { id:'p_gm', name:'Grandmother', rel:'Grandmother (Paternal)', x:58, y:20, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' },\n      { id:'p_gf', name:'Grandfather', rel:'Grandfather (Paternal)', x:78, y:20, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' },\n      { id:'kid1', name:'Son',      rel:'Son',       x:43, y:75, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' },\n      { id:'kid2', name:'Daughter', rel:'Daughter',  x:57, y:75, photo:null, photos:[], files:[], audioClips:[], videos:[], size:'normal' }\n    ];\n\n    const DEFAULT_EDGES = [\n      ['mom','you'],\n      ['dad','you'],\n      ['m_gm','mom'],\n      ['m_gf','mom'],\n      ['p_gm','dad'],\n      ['p_gf','dad'],\n      ['you','kid1'],\n      ['you','kid2']\n    ];\n\n    let state = loadState();\n    let draggingNode = null;\n    let dragOffsetX = 0;\n    let dragOffsetY = 0;\n\n    let photoTargetId = null;\n    let audioTargetId = null;\n    let fileTargetId  = null;\n\n    let connectMode = false;\n    let pendingFrom = null;\n    let deleteLineMode = false;\n\n    const audioPlayer = new Audio();\n\n    \/* fotos (galer\u00eda) *\/\n    let currentPhotoNodeId = null;\n    let currentPhotoIndex = 0;\n\n    \/* archivos *\/\n    let currentFileNodeId = null;\n\n    \/* audio \/ video (listas) *\/\n    let currentAudioNodeId = null;\n    let currentVideoNodeId = null;\n\n    function loadState(){\n      let st;\n      try{\n        const raw = localStorage.getItem(STORAGE_KEY);\n        if(raw){\n          const parsed = JSON.parse(raw);\n          if(parsed && Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)){\n            st = parsed;\n          }\n        }\n      }catch(e){}\n\n      if(!st){\n        st = { nodes: DEFAULT_NODES, edges: DEFAULT_EDGES };\n      }\n\n      const rect = stage.getBoundingClientRect();\n      if(rect.width && rect.height && Array.isArray(st.nodes)){\n        st.nodes.forEach(n => {\n          if(typeof n.x === 'number' && typeof n.y === 'number'){\n            if(n.x > 100 || n.y > 100){\n              n.x = (n.x \/ rect.width) * 100;\n              n.y = (n.y \/ rect.height) * 100;\n            }\n          }\n\n          if(!('photo' in n)) n.photo = null;\n          if(!('photos' in n)) n.photos = n.photo ? [n.photo] : [];\n          if(!('files' in n))  n.files = [];\n\n          if(!('audioClips' in n)) n.audioClips = [];\n          if(n.audio && n.audioClips.length === 0){\n            n.audioClips.push({ name:'Audio', type:'audio', data:n.audio });\n          }\n          delete n.audio;\n\n          if(!('videos' in n)) n.videos = [];\n          if(n.video && n.videos.length === 0){\n            n.videos.push({ name:'Video', url:n.video });\n          }\n          delete n.video;\n\n          if(Array.isArray(n.videos)){\n            n.videos = n.videos.map(v => {\n              if(v.data && !v.url){\n                v.url = v.data;\n                delete v.data;\n              }\n              return v;\n            });\n          }\n\n          if(!('birth' in n)) n.birth = '';\n          if(!('death' in n)) n.death = '';\n          if(!('marriage' in n)) n.marriage = '';\n          if(!('size' in n)){\n            if(n.small) n.size = 'small';\n            else n.size = 'normal';\n          }\n        });\n        try{\n          localStorage.setItem(STORAGE_KEY, JSON.stringify(st));\n        }catch(e){}\n      }\n\n      return st;\n    }\n\n    function saveState(){\n      localStorage.setItem(STORAGE_KEY, JSON.stringify(state));\n    }\n\n    function escapeHTML(str){\n      return String(str).replace(\/[&<>\"']\/g, function(c){\n        return ({\n          '&':'&amp;',\n          '<':'&lt;',\n          '>':'&gt;',\n          '\"':'&quot;',\n          \"'\":'&#39;'\n        })[c];\n      });\n    }\n\n    function pctToPx(xPct,yPct){\n      const rect = stage.getBoundingClientRect();\n      return {\n        x: rect.width * xPct \/ 100,\n        y: rect.height * yPct \/ 100\n      };\n    }\n\n    function pxToPct(x,y){\n      const rect = stage.getBoundingClientRect();\n      return {\n        x: (x \/ rect.width) * 100,\n        y: (y \/ rect.height) * 100\n      };\n    }\n\n    function findNode(id){\n      return state.nodes.find(n => n.id === id) || null;\n    }\n\n    \/* COMPRESI\u00d3N AUTOM\u00c1TICA DE FOTOS *\/\n    function compressImage(file, maxSizePx = 900, quality = 0.75){\n      return new Promise((resolve, reject) => {\n        const reader = new FileReader();\n        const img = new Image();\n\n        reader.onload = e => {\n          img.onload = () => {\n            let width = img.width;\n            let height = img.height;\n\n            if(width > height){\n              if(width > maxSizePx){\n                height = Math.round(height * (maxSizePx \/ width));\n                width = maxSizePx;\n              }\n            }else{\n              if(height > maxSizePx){\n                width = Math.round(width * (maxSizePx \/ height));\n                height = maxSizePx;\n              }\n            }\n\n            const canvas = document.createElement('canvas');\n            canvas.width = width;\n            canvas.height = height;\n            const ctx = canvas.getContext('2d');\n\n            ctx.drawImage(img, 0, 0, width, height);\n\n            const dataUrl = canvas.toDataURL('image\/jpeg', quality);\n            resolve(dataUrl);\n          };\n\n          img.onerror = err => reject(err);\n          img.src = e.target.result;\n        };\n\n        reader.onerror = err => reject(err);\n        reader.readAsDataURL(file);\n      });\n    }\n\n    function render(){\n      Array.from(stage.querySelectorAll('.node')).forEach(el => el.remove());\n\n      state.nodes.forEach(node => {\n        const el = document.createElement('div');\n        el.className = 'node';\n        el.dataset.id = node.id;\n\n        if(node.size === 'small'){\n          el.classList.add('node-small');\n        }else if(node.size === 'tiny'){\n          el.classList.add('node-tiny');\n        }else if(node.size === 'supermini'){\n          el.classList.add('node-supermini');\n        }\n\n        const avatarPhoto = (node.photos && node.photos.length > 0)\n          ? node.photos[0]\n          : node.photo;\n\n        const photoStyle = avatarPhoto\n          ? `background-image:url('${avatarPhoto}')`\n          : '';\n\n        const audioCount = (node.audioClips && node.audioClips.length) || 0;\n        const videoCount = (node.videos && node.videos.length) || 0;\n\n        const audioLabel = audioCount > 1\n          ? `Audio (${audioCount})`\n          : (audioCount === 1 ? 'Audio \u2713' : 'Audio');\n\n        const videoLabel = videoCount > 1\n          ? `Video (${videoCount})`\n          : (videoCount === 1 ? 'Video \u2713' : 'Video');\n\n        let sizeLabel = 'Size: normal';\n        if(node.size === 'small') sizeLabel = 'Size: small';\n        else if(node.size === 'tiny') sizeLabel = 'Size: tiny';\n        else if(node.size === 'supermini') sizeLabel = 'Size: supermini';\n\n        const hideDates = (node.size === 'tiny' || node.size === 'supermini');\n        const datesText = (!hideDates) ? [\n          node.birth ? `Born: ${escapeHTML(node.birth)}` : '',\n          node.death ? `Died: ${escapeHTML(node.death)}` : '',\n          node.marriage ? `Married: ${escapeHTML(node.marriage)}` : ''\n        ].filter(Boolean).join(' \u00b7 ') : '';\n\n        const photosCount = (node.photos && node.photos.length) || 0;\n        const photoLabel = photosCount > 1\n          ? `Photos (${photosCount})`\n          : (photosCount === 1 ? 'Photo \u2713' : 'Photo');\n\n        const filesCount = (node.files && node.files.length) || 0;\n        const filesLabel = filesCount > 0 ? `Files (${filesCount})` : 'Files';\n\n        el.innerHTML = `\n          <div class=\"node-row\">\n            <div class=\"avatar\" style=\"${photoStyle}\"><\/div>\n            <div>\n              <h3>${escapeHTML(node.name)}<\/h3>\n              <div class=\"small\">${escapeHTML(node.rel)}<\/div>\n              ${datesText ? `<div class=\"small\">${datesText}<\/div>` : ''}\n            <\/div>\n          <\/div>\n          <div class=\"node-actions\">\n            <button class=\"btn\" data-action=\"edit\">Edit<\/button>\n            <button class=\"btn ghost\" data-action=\"dates\">Dates<\/button>\n            <button class=\"btn ghost\" data-action=\"photo\">${photoLabel}<\/button>\n            <button class=\"btn ghost\" data-action=\"files\">${filesLabel}<\/button>\n            <button class=\"btn ghost\" data-action=\"audio\">${audioLabel}<\/button>\n            <button class=\"btn ghost\" data-action=\"video\">${videoLabel}<\/button>\n            <button class=\"btn ghost\" data-action=\"size\">${sizeLabel}<\/button>\n            <button class=\"btn ghost\" data-action=\"dup\">Duplicate<\/button>\n            <button class=\"btn ghost\" data-action=\"del\">Delete<\/button>\n          <\/div>\n        `;\n\n        const pos = pctToPx(node.x, node.y);\n        el.style.left = pos.x + 'px';\n        el.style.top  = pos.y + 'px';\n\n        stage.appendChild(el);\n        makeDraggable(el);\n        attachNodeButtons(el);\n\n        const avatarEl = el.querySelector('.avatar');\n        avatarEl.addEventListener('click', () => {\n          openPhotoGallery(node.id, 0);\n        });\n      });\n\n      drawLinks();\n    }\n\n    function drawLinks(){\n      const stageRect = stage.getBoundingClientRect();\n      const width = stageRect.width;\n      const height = stageRect.height;\n\n      svg.setAttribute('viewBox', `0 0 ${width} ${height}`);\n\n      Array.from(svg.querySelectorAll('path')).forEach(p => p.remove());\n\n      const nodeEls = {};\n      Array.from(stage.querySelectorAll('.node')).forEach(el => {\n        nodeEls[el.dataset.id] = el;\n      });\n\n      state.edges.forEach(([fromId,toId]) => {\n        const fromEl = nodeEls[fromId];\n        const toEl   = nodeEls[toId];\n        if(!fromEl || !toEl) return;\n\n        const fr = fromEl.getBoundingClientRect();\n        const tr = toEl.getBoundingClientRect();\n        const sr = stageRect;\n\n        const ax = fr.left + fr.width \/ 2 - sr.left;\n        const ay = fr.top  + fr.height - sr.top;\n        const bx = tr.left + tr.width \/ 2 - sr.left;\n        const by = tr.top  - sr.top;\n\n        const mx = (ax + bx) \/ 2;\n        const my = (ay + by) \/ 2 - 60;\n\n        const d = `M ${ax} ${ay} Q ${mx} ${my} ${bx} ${by}`;\n        const path = document.createElementNS('http:\/\/www.w3.org\/2000\/svg','path');\n        path.setAttribute('d', d);\n        path.setAttribute('class', 'link');\n        path.dataset.from = fromId;\n        path.dataset.to   = toId;\n        path.addEventListener('click', onLinkClick);\n        svg.appendChild(path);\n      });\n    }\n\n    function makeDraggable(el){\n      el.addEventListener('mousedown', onMouseDownNode);\n    }\n\n    function onMouseDownNode(e){\n      const el = e.currentTarget;\n\n      if(connectMode){\n        handleConnectClick(el);\n        return;\n      }\n\n      if(e.target.closest('button') || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT'){\n        return;\n      }\n\n      draggingNode = el;\n      const rect = el.getBoundingClientRect();\n      const stageRect = stage.getBoundingClientRect();\n\n      dragOffsetX = e.clientX - rect.left;\n      dragOffsetY = e.clientY - rect.top;\n      el.classList.add('dragging');\n\n      document.addEventListener('mousemove', onMouseMove);\n      document.addEventListener('mouseup', onMouseUp);\n    }\n\n    function onMouseMove(e){\n      if(!draggingNode) return;\n      const stageRect = stage.getBoundingClientRect();\n\n      let newLeft = e.clientX - stageRect.left - dragOffsetX;\n      let newTop  = e.clientY - stageRect.top  - dragOffsetY;\n\n      if(newLeft < 0) newLeft = 0;\n      if(newTop < 0)  newTop = 0;\n      if(newLeft > stageRect.width - 160) newLeft = stageRect.width - 160;\n      if(newTop > stageRect.height - 120) newTop = stageRect.height - 120;\n\n      draggingNode.style.left = newLeft + 'px';\n      draggingNode.style.top  = newTop  + 'px';\n\n      drawLinks();\n    }\n\n    function onMouseUp(e){\n      if(!draggingNode) return;\n      const el = draggingNode;\n      draggingNode = null;\n      el.classList.remove('dragging');\n\n      const rect = el.getBoundingClientRect();\n      const stageRect = stage.getBoundingClientRect();\n      const centerX = rect.left - stageRect.left + rect.width\/2;\n      const centerY = rect.top  - stageRect.top  + rect.height\/2;\n      const pct = pxToPct(centerX, centerY);\n\n      const node = findNode(el.dataset.id);\n      if(node){\n        node.x = Math.min(98, Math.max(2, pct.x));\n        node.y = Math.min(98, Math.max(2, pct.y));\n        saveState();\n      }\n\n      drawLinks();\n\n      document.removeEventListener('mousemove', onMouseMove);\n      document.removeEventListener('mouseup', onMouseUp);\n    }\n\n    function handleConnectClick(el){\n      const id = el.dataset.id;\n      if(!pendingFrom){\n        pendingFrom = id;\n        Array.from(stage.querySelectorAll('.node.pending-connect'))\n          .forEach(n => n.classList.remove('pending-connect'));\n        el.classList.add('pending-connect');\n      }else{\n        const from = pendingFrom;\n        const to = id;\n        if(from !== to){\n          state.edges.push([from,to]);\n          saveState();\n          drawLinks();\n        }\n        Array.from(stage.querySelectorAll('.node.pending-connect'))\n          .forEach(n => n.classList.remove('pending-connect'));\n        pendingFrom = null;\n      }\n    }\n\n    connectBtn.addEventListener('click', function(){\n      connectMode = !connectMode;\n      if(connectMode){\n        deleteLineMode = false;\n        document.body.classList.remove('delete-lines');\n        deleteLinesBtn.textContent = 'Delete lines';\n      }\n      pendingFrom = null;\n      Array.from(stage.querySelectorAll('.node.pending-connect'))\n        .forEach(n => n.classList.remove('pending-connect'));\n      connectBtn.textContent = connectMode ? 'Connecting\u2026' : 'Connect';\n    });\n\n    function onLinkClick(e){\n      if(!deleteLineMode) return;\n      const from = e.target.dataset.from;\n      const to   = e.target.dataset.to;\n      state.edges = state.edges.filter(([a,b]) => !(a === from && b === to));\n      saveState();\n      drawLinks();\n    }\n\n    deleteLinesBtn.addEventListener('click', function(){\n      deleteLineMode = !deleteLineMode;\n      if(deleteLineMode){\n        connectMode = false;\n        pendingFrom = null;\n        connectBtn.textContent = 'Connect';\n      }\n      document.body.classList.toggle('delete-lines', deleteLineMode);\n      deleteLinesBtn.textContent = deleteLineMode ? 'Deleting lines\u2026' : 'Delete lines';\n    });\n\n    function attachNodeButtons(el){\n      el.querySelectorAll('[data-action]').forEach(btn => {\n        btn.addEventListener('click', function(ev){\n          ev.stopPropagation();\n          const action = btn.dataset.action;\n          const id = el.dataset.id;\n          const node = findNode(id);\n          if(!node) return;\n\n          if(action === 'edit'){\n            const newName = prompt('Full name:', node.name || '');\n            if(newName === null) return;\n            const newRel  = prompt('Relationship:', node.rel || '');\n            if(newRel === null) return;\n            node.name = newName.trim() || node.name;\n            node.rel  = newRel.trim()  || node.rel;\n            saveState();\n            render();\n          }else if(action === 'dates'){\n            const birth = prompt('Birth (e.g. 1947-03-15 or text):', node.birth || '');\n            if(birth === null) return;\n            const death = prompt('Death (optional):', node.death || '');\n            if(death === null) return;\n            const marriage = prompt('Marriage (optional):', node.marriage || '');\n            if(marriage === null) return;\n            node.birth = birth.trim();\n            node.death = death.trim();\n            node.marriage = marriage.trim();\n            saveState();\n            render();\n          }else if(action === 'del'){\n            if(confirm('Delete this card?')){\n              state.nodes = state.nodes.filter(n => n.id !== id);\n              state.edges = state.edges.filter(([a,b]) => a !== id && b !== id);\n              saveState();\n              render();\n            }\n          }else if(action === 'dup'){\n            const newId = id + '_' + Math.random().toString(36).slice(2,6);\n            state.nodes.push({\n              id:newId,\n              name:node.name,\n              rel:node.rel,\n              x:node.x + 2,\n              y:node.y + 4,\n              photo:node.photo || null,\n              photos:(node.photos && node.photos.length) ? [...node.photos] : [],\n              files:(node.files && node.files.length) ? [...node.files] : [],\n              audioClips:(node.audioClips && node.audioClips.length) ? [...node.audioClips] : [],\n              videos:(node.videos && node.videos.length) ? [...node.videos] : [],\n              birth:node.birth || '',\n              death:node.death || '',\n              marriage:node.marriage || '',\n              size:node.size || 'normal'\n            });\n            state.edges.push([id, newId]);\n            saveState();\n            render();\n          }else if(action === 'photo'){\n            openPhotoGallery(id, 0);\n          }else if(action === 'files'){\n            openFileManager(id);\n          }else if(action === 'audio'){\n            openAudioManager(id);\n          }else if(action === 'video'){\n            openVideoManager(id);\n          }else if(action === 'size'){\n            if(node.size === 'normal'){\n              node.size = 'small';\n            }else if(node.size === 'small'){\n              node.size = 'tiny';\n            }else if(node.size === 'tiny'){\n              node.size = 'supermini';\n            }else{\n              node.size = 'normal';\n            }\n            saveState();\n            render();\n          }\n        });\n      });\n    }\n\n    \/* === FOTO: SUBIR Y GUARDAR EN GALER\u00cdA === *\/\n    photoInput.addEventListener('change', async function(){\n      const file = photoInput.files && photoInput.files[0];\n      if(!file || !photoTargetId) return;\n\n      const node = findNode(photoTargetId);\n      if(!node) return;\n\n      try{\n        let dataUrl;\n        if(file.size > 800 * 1024){\n          dataUrl = await compressImage(file, 900, 0.75);\n        }else{\n          const reader = new FileReader();\n          dataUrl = await new Promise((resolve, reject) => {\n            reader.onload = ev => resolve(ev.target.result);\n            reader.onerror = err => reject(err);\n            reader.readAsDataURL(file);\n          });\n        }\n        if(!node.photos) node.photos = [];\n        node.photos.push(dataUrl);\n        node.photo = node.photos[0] || null;\n        saveState();\n        render();\n        if(currentPhotoNodeId === node.id){\n          updatePhotoOverlay();\n        }\n      }catch(err){\n        console.error('Error processing photo', err);\n        alert('The photo could not be processed. Please try another image.');\n      }\n    });\n\n    audioInput.addEventListener('change', function(){\n      const file = audioInput.files && audioInput.files[0];\n      if(!file || !audioTargetId) return;\n      const reader = new FileReader();\n      reader.onload = function(ev){\n        const node = findNode(audioTargetId);\n        if(node){\n          if(!node.audioClips) node.audioClips = [];\n          node.audioClips.push({\n            name:file.name || 'Audio',\n            type:file.type || 'audio',\n            data:ev.target.result\n          });\n          saveState();\n          render();\n          if(currentAudioNodeId === node.id){\n            updateAudioOverlay();\n          }\n        }\n      };\n      reader.readAsDataURL(file);\n    });\n\n    \/* === ARCHIVOS === *\/\n    fileInput.addEventListener('change', function(){\n      const file = fileInput.files && fileInput.files[0];\n      if(!file || !fileTargetId) return;\n\n      const reader = new FileReader();\n      reader.onload = function(ev){\n        const node = findNode(fileTargetId);\n        if(!node) return;\n        if(!node.files) node.files = [];\n        node.files.push({\n          name: file.name,\n          type: file.type || 'Unknown',\n          data: ev.target.result\n        });\n        saveState();\n        render();\n        if(currentFileNodeId === node.id){\n          updateFileOverlay();\n        }\n      };\n      reader.readAsDataURL(file);\n    });\n\n    function openVideo(src){\n      videoPlayer.src = src;\n      videoOverlay.classList.remove('hidden');\n      videoPlayer.play().catch(()=>{});\n    }\n\n    closeVideo.addEventListener('click', function(){\n      videoOverlay.classList.add('hidden');\n      videoPlayer.pause();\n      videoPlayer.src = \"\";\n    });\n\n    videoOverlay.addEventListener('click', function(e){\n      if(e.target === videoOverlay){\n        videoOverlay.classList.add('hidden');\n        videoPlayer.pause();\n        videoPlayer.src = \"\";\n      }\n    });\n\n    \/* === GALER\u00cdA DE FOTOS === *\/\n    function openPhotoGallery(nodeId, startIndex){\n      const node = findNode(nodeId);\n      if(!node) return;\n      if(!node.photos) node.photos = node.photo ? [node.photo] : [];\n\n      currentPhotoNodeId = nodeId;\n      currentPhotoIndex = startIndex || 0;\n      photoTargetId = nodeId;\n\n      updatePhotoOverlay();\n      photoOverlay.classList.remove('hidden');\n    }\n\n    function updatePhotoOverlay(){\n      const node = findNode(currentPhotoNodeId);\n      if(!node) return;\n      const photos = node.photos || [];\n\n      photoTitle.textContent = node.name + ' \u2013 Photos';\n\n      if(photos.length === 0){\n        photoView.style.display = 'none';\n        photoView.src = '';\n        photoCounter.textContent = '0 \/ 0';\n        prevPhotoBtn.disabled = true;\n        nextPhotoBtn.disabled = true;\n        deletePhotoBtn.disabled = true;\n      }else{\n        photoView.style.display = 'block';\n        if(currentPhotoIndex < 0) currentPhotoIndex = 0;\n        if(currentPhotoIndex >= photos.length) currentPhotoIndex = photos.length - 1;\n        photoView.src = photos[currentPhotoIndex];\n        photoCounter.textContent = (currentPhotoIndex + 1) + ' \/ ' + photos.length;\n        prevPhotoBtn.disabled = photos.length <= 1;\n        nextPhotoBtn.disabled = photos.length <= 1;\n        deletePhotoBtn.disabled = false;\n      }\n\n      photoThumbs.innerHTML = '';\n      photos.forEach((src, idx) => {\n        const img = document.createElement('img');\n        img.src = src;\n        if(idx === currentPhotoIndex) img.classList.add('active');\n        img.addEventListener('click', () => {\n          currentPhotoIndex = idx;\n          updatePhotoOverlay();\n        });\n        photoThumbs.appendChild(img);\n      });\n    }\n\n    prevPhotoBtn.addEventListener('click', () => {\n      const node = findNode(currentPhotoNodeId);\n      if(!node || !node.photos || node.photos.length <= 1) return;\n      currentPhotoIndex = (currentPhotoIndex - 1 + node.photos.length) % node.photos.length;\n      updatePhotoOverlay();\n    });\n\n    nextPhotoBtn.addEventListener('click', () => {\n      const node = findNode(currentPhotoNodeId);\n      if(!node || !node.photos || node.photos.length <= 1) return;\n      currentPhotoIndex = (currentPhotoIndex + 1) % node.photos.length;\n      updatePhotoOverlay();\n    });\n\n    addPhotoBtn.addEventListener('click', () => {\n      if(!currentPhotoNodeId) return;\n      photoTargetId = currentPhotoNodeId;\n      photoInput.value = \"\";\n      photoInput.click();\n    });\n\n    deletePhotoBtn.addEventListener('click', () => {\n      const node = findNode(currentPhotoNodeId);\n      if(!node || !node.photos || node.photos.length === 0) return;\n      if(!confirm('Delete this photo?')) return;\n\n      node.photos.splice(currentPhotoIndex, 1);\n      node.photo = node.photos[0] || null;\n      saveState();\n      render();\n      updatePhotoOverlay();\n    });\n\n    closePhoto.addEventListener('click', function(){\n      photoOverlay.classList.add('hidden');\n      photoView.src = \"\";\n      currentPhotoNodeId = null;\n      currentPhotoIndex = 0;\n    });\n\n    photoOverlay.addEventListener('click', function(e){\n      if(e.target === photoOverlay){\n        photoOverlay.classList.add('hidden');\n        photoView.src = \"\";\n        currentPhotoNodeId = null;\n        currentPhotoIndex = 0;\n      }\n    });\n\n    \/* === FILE MANAGER === *\/\n    function openFileManager(nodeId){\n      const node = findNode(nodeId);\n      if(!node) return;\n      if(!node.files) node.files = [];\n\n      currentFileNodeId = nodeId;\n      fileTargetId = nodeId;\n      fileTitle.textContent = node.name + ' \u2013 Files';\n\n      updateFileOverlay();\n      fileOverlay.classList.remove('hidden');\n    }\n\n    function updateFileOverlay(){\n      const node = findNode(currentFileNodeId);\n      if(!node) return;\n      const files = node.files || [];\n\n      fileList.innerHTML = '';\n      if(files.length === 0){\n        const li = document.createElement('li');\n        li.textContent = 'No files added yet.';\n        li.style.opacity = '0.8';\n        fileList.appendChild(li);\n        return;\n      }\n\n      files.forEach((f, idx) => {\n        const li = document.createElement('li');\n        li.className = 'file-item';\n\n        const main = document.createElement('div');\n        main.className = 'file-main';\n\n        const nameEl = document.createElement('div');\n        nameEl.className = 'file-name';\n        nameEl.textContent = f.name || 'Unnamed file';\n\n        const metaEl = document.createElement('div');\n        metaEl.className = 'file-meta';\n        metaEl.textContent = f.type || 'Unknown type';\n\n        main.appendChild(nameEl);\n        main.appendChild(metaEl);\n\n        const buttons = document.createElement('div');\n        buttons.className = 'file-buttons';\n\n        const openLink = document.createElement('a');\n        openLink.textContent = 'Open';\n        openLink.href = f.data;\n        openLink.target = '_blank';\n        openLink.rel = 'noopener noreferrer';\n\n        const downloadLink = document.createElement('a');\n        downloadLink.textContent = 'Download';\n        downloadLink.href = f.data;\n        downloadLink.download = f.name || 'file';\n\n        const delBtn = document.createElement('button');\n        delBtn.textContent = 'Delete';\n        delBtn.addEventListener('click', () => {\n          if(!confirm('Delete this file?')) return;\n          node.files.splice(idx, 1);\n          saveState();\n          render();\n          updateFileOverlay();\n        });\n\n        buttons.appendChild(openLink);\n        buttons.appendChild(downloadLink);\n        buttons.appendChild(delBtn);\n\n        li.appendChild(main);\n        li.appendChild(buttons);\n        fileList.appendChild(li);\n      });\n    }\n\n    addFileBtn.addEventListener('click', () => {\n      if(!currentFileNodeId) return;\n      fileTargetId = currentFileNodeId;\n      fileInput.value = \"\";\n      fileInput.click();\n    });\n\n    closeFile.addEventListener('click', () => {\n      fileOverlay.classList.add('hidden');\n      currentFileNodeId = null;\n      fileTargetId = null;\n    });\n\n    fileOverlay.addEventListener('click', (e) => {\n      if(e.target === fileOverlay){\n        fileOverlay.classList.add('hidden');\n        currentFileNodeId = null;\n        fileTargetId = null;\n      }\n    });\n\n    \/* === AUDIO MANAGER === *\/\n    function openAudioManager(nodeId){\n      const node = findNode(nodeId);\n      if(!node) return;\n      if(!node.audioClips) node.audioClips = [];\n\n      currentAudioNodeId = nodeId;\n      audioTargetId = nodeId;\n      audioTitle.textContent = node.name + ' \u2013 Audio';\n\n      updateAudioOverlay();\n      audioOverlay.classList.remove('hidden');\n    }\n\n    function updateAudioOverlay(){\n      const node = findNode(currentAudioNodeId);\n      if(!node) return;\n      const clips = node.audioClips || [];\n\n      audioList.innerHTML = '';\n      if(clips.length === 0){\n        const li = document.createElement('li');\n        li.textContent = 'No audio clips yet.';\n        li.style.opacity = '0.8';\n        audioList.appendChild(li);\n        return;\n      }\n\n      clips.forEach((c, idx) => {\n        const li = document.createElement('li');\n        li.className = 'file-item';\n\n        const main = document.createElement('div');\n        main.className = 'file-main';\n\n        const nameEl = document.createElement('div');\n        nameEl.className = 'file-name';\n        nameEl.textContent = c.name || 'Audio ' + (idx+1);\n\n        const metaEl = document.createElement('div');\n        metaEl.className = 'file-meta';\n        metaEl.textContent = c.type || 'audio';\n\n        main.appendChild(nameEl);\n        main.appendChild(metaEl);\n\n        const buttons = document.createElement('div');\n        buttons.className = 'file-buttons';\n\n        const playBtn = document.createElement('button');\n        playBtn.textContent = 'Play';\n        playBtn.addEventListener('click', () => {\n          audioPlayer.src = c.data;\n          audioPlayer.play().catch(()=>{});\n        });\n\n        const delBtn = document.createElement('button');\n        delBtn.textContent = 'Delete';\n        delBtn.addEventListener('click', () => {\n          if(!confirm('Delete this audio?')) return;\n          node.audioClips.splice(idx, 1);\n          saveState();\n          render();\n          updateAudioOverlay();\n        });\n\n        buttons.appendChild(playBtn);\n        buttons.appendChild(delBtn);\n\n        li.appendChild(main);\n        li.appendChild(buttons);\n        audioList.appendChild(li);\n      });\n    }\n\n    addAudioBtn.addEventListener('click', () => {\n      if(!currentAudioNodeId) return;\n      audioTargetId = currentAudioNodeId;\n      audioInput.value = \"\";\n      audioInput.click();\n    });\n\n    closeAudio.addEventListener('click', () => {\n      audioOverlay.classList.add('hidden');\n      currentAudioNodeId = null;\n      audioTargetId = null;\n    });\n\n    audioOverlay.addEventListener('click', (e) => {\n      if(e.target === audioOverlay){\n        audioOverlay.classList.add('hidden');\n        currentAudioNodeId = null;\n        audioTargetId = null;\n      }\n    });\n\n    \/* === VIDEO LIST MANAGER (por enlaces) === *\/\n    function openVideoManager(nodeId){\n      const node = findNode(nodeId);\n      if(!node) return;\n      if(!node.videos) node.videos = [];\n\n      currentVideoNodeId = nodeId;\n      videoListTitle.textContent = node.name + ' \u2013 Videos';\n\n      updateVideoListOverlay();\n      videoListOverlay.classList.remove('hidden');\n    }\n\n    function updateVideoListOverlay(){\n      const node = findNode(currentVideoNodeId);\n      if(!node) return;\n      const vids = node.videos || [];\n\n      videoListEl.innerHTML = '';\n      if(vids.length === 0){\n        const li = document.createElement('li');\n        li.textContent = 'No videos yet. Use \"Add video link\" to paste a URL (YouTube, Drive, etc.).';\n        li.style.opacity = '0.8';\n        videoListEl.appendChild(li);\n        return;\n      }\n\n      vids.forEach((v, idx) => {\n        const li = document.createElement('li');\n        li.className = 'file-item';\n\n        const main = document.createElement('div');\n        main.className = 'file-main';\n\n        const nameEl = document.createElement('div');\n        nameEl.className = 'file-name';\n        nameEl.textContent = v.name || 'Video ' + (idx+1);\n\n        const metaEl = document.createElement('div');\n        metaEl.className = 'file-meta';\n        metaEl.textContent = v.url || '';\n\n        main.appendChild(nameEl);\n        main.appendChild(metaEl);\n\n        const buttons = document.createElement('div');\n        buttons.className = 'file-buttons';\n\n        const openBtn = document.createElement('button');\n        openBtn.textContent = 'Open';\n        openBtn.addEventListener('click', () => {\n          if(!v.url) return;\n          if(v.url.startsWith('data:video')){\n            openVideo(v.url);\n          }else{\n            window.open(v.url, '_blank');\n          }\n        });\n\n        const delBtn = document.createElement('button');\n        delBtn.textContent = 'Delete';\n        delBtn.addEventListener('click', () => {\n          if(!confirm('Delete this video link?')) return;\n          node.videos.splice(idx, 1);\n          saveState();\n          render();\n          updateVideoListOverlay();\n        });\n\n        buttons.appendChild(openBtn);\n        buttons.appendChild(delBtn);\n\n        li.appendChild(main);\n        li.appendChild(buttons);\n        videoListEl.appendChild(li);\n      });\n    }\n\n    addVideoBtn.addEventListener('click', () => {\n      if(!currentVideoNodeId) return;\n      const node = findNode(currentVideoNodeId);\n      if(!node) return;\n      if(!node.videos) node.videos = [];\n\n      const title = prompt('Video title (e.g. \"Message for my family 2025\"):', '');\n      if(title === null) return;\n\n      const url = prompt('Paste the video link (YouTube, Google Drive, iCloud, etc.):', '');\n      if(url === null) return;\n\n      const cleanUrl = (url || '').trim();\n      if(!cleanUrl){\n        alert('You must paste a link.');\n        return;\n      }\n\n      node.videos.push({\n        name: (title || '').trim() || 'Video',\n        url: cleanUrl\n      });\n\n      saveState();\n      render();\n      updateVideoListOverlay();\n    });\n\n    closeVideoList.addEventListener('click', () => {\n      videoListOverlay.classList.add('hidden');\n      currentVideoNodeId = null;\n    });\n\n    videoListOverlay.addEventListener('click', (e) => {\n      if(e.target === videoListOverlay){\n        videoListOverlay.classList.add('hidden');\n        currentVideoNodeId = null;\n      }\n    });\n\n    \/* === RESTO === *\/\n\n    addBtn.addEventListener('click', function(){\n      const name = (nameInput.value || '').trim() || 'New Member';\n      const rel  = relInput.value || 'Relative';\n\n      const newId = (name.toLowerCase().replace(\/[^a-z0-9]+\/g,'-') || 'member')\n                  + '-' + Math.random().toString(36).slice(2,5);\n\n      const newNode = {\n        id:newId,\n        name:name,\n        rel:rel,\n        x:50,\n        y:50,\n        photo:null,\n        photos:[],\n        files:[],\n        audioClips:[],\n        videos:[],\n        birth:'',\n        death:'',\n        marriage:'',\n        size:'normal'\n      };\n\n      state.nodes.push(newNode);\n      autoConnect(newNode);\n      saveState();\n      nameInput.value = '';\n      render();\n    });\n\n    resetBtn.addEventListener('click', function(){\n      if(confirm('Clear this tree and reset to default layout?')){\n        localStorage.removeItem(STORAGE_KEY);\n        state = { nodes: DEFAULT_NODES, edges: DEFAULT_EDGES };\n        render();\n      }\n    });\n\n    function autoConnect(node){\n      const you = state.nodes.find(n => n.rel === 'You' || n.id === 'you');\n      if(!you) return;\n      const r = node.rel.toLowerCase();\n\n      if(r.includes('mother') || r.includes('father') || r.includes('stepfather') || r.includes('stepmother')){\n        state.edges.push([node.id, you.id]);\n      }else if(r.includes('son') || r.includes('daughter') || r.includes('stepdaughter') || r.includes('stepson')){\n        state.edges.push([you.id, node.id]);\n      }else if(r.includes('spouse') || r.includes('partner') || r.includes('husband') || r.includes('wife')){\n        state.edges.push([you.id, node.id]);\n      }\n    }\n\n    compactToggle.addEventListener('change', function(){\n      document.body.classList.toggle('compact', compactToggle.checked);\n    });\n\n    window.addEventListener('resize', drawLinks);\n    window.addEventListener('scroll', drawLinks);\n\n    render();\n  <\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>Private \u00b7 Family Tree Add member: YouMotherFatherSpousePartnerSonDaughterStepsonStepdaughterBrotherSisterHalf-brotherHalf-sisterStepbrotherStepsisterGrandmother (Maternal)Grandfather (Maternal)Grandmother (Paternal)Grandfather (Paternal)Great-grandfatherGreat-grandmotherUncleAuntNephewNieceCousinGodsonGoddaughterStepfatherStepmotherSister-in-lawBrother-in-lawMother-in-lawFather-in-lawDaughter-in-lawSon-in-lawEx-wifeEx-husbandOther Add Connect Delete lines Reset Compact Drag the cards to arrange your family tree. Buttons: Edit \u00b7 Dates \u00b7 Photo \u00b7 Files \u00b7 Audio \u00b7 Video \u00b7 Size \u00b7 Duplicate \u00b7 Delete. Photos and videos are not stored on a server, only locally or [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_coblocks_attr":"","_coblocks_dimensions":"","_coblocks_responsive_height":"","_coblocks_accordion_ie_support":"","pmpro_default_level":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"class_list":["post-1103","page","type-page","status-publish","hentry","pmpro-has-access"],"_links":{"self":[{"href":"https:\/\/tree-of-mylife.com\/es\/wp-json\/wp\/v2\/pages\/1103","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tree-of-mylife.com\/es\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/tree-of-mylife.com\/es\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/tree-of-mylife.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tree-of-mylife.com\/es\/wp-json\/wp\/v2\/comments?post=1103"}],"version-history":[{"count":43,"href":"https:\/\/tree-of-mylife.com\/es\/wp-json\/wp\/v2\/pages\/1103\/revisions"}],"predecessor-version":[{"id":1171,"href":"https:\/\/tree-of-mylife.com\/es\/wp-json\/wp\/v2\/pages\/1103\/revisions\/1171"}],"wp:attachment":[{"href":"https:\/\/tree-of-mylife.com\/es\/wp-json\/wp\/v2\/media?parent=1103"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}