<!-- ================================================================
     DESPEDIDA DE ALBERTO · app en tiempo real para WordPress
     Pega TODO esto en un bloque "HTML personalizado" de la página.
     Necesita el snippet PHP (endpoint REST) activo en el mismo sitio.
     ================================================================ -->
<div id="despApp">
<style>
  #despApp {
    --ink:#14121c; --surface:#1e1b29; --surface-2:#262233; --line:#332e44;
    --text:#ede8f2; --muted:#a29cb4; --faint:#6f6a82;
    --rose:#ff476f; --rose-soft:#ff9bb2; --gold:#e8b54d; --green:#43dd9c; --amber:#f5b851;
    --shadow:0 18px 50px -20px rgba(0,0,0,.7); --radius:16px; --maxw:640px;
    background:
      radial-gradient(120% 60% at 80% -10%, rgba(255,71,111,.18), transparent 60%),
      radial-gradient(90% 50% at 0% 0%, rgba(232,181,77,.12), transparent 55%),
      var(--ink);
    color:var(--text); font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
    -webkit-font-smoothing:antialiased; border-radius:20px; overflow:hidden; padding-bottom:34px;
  }
  #despApp *{box-sizing:border-box;}
  #despApp .wrap,#despApp .hero,#despApp .tabs-inner,#despApp main,#despApp .share{max-width:var(--maxw);margin-left:auto;margin-right:auto;}

  #despApp .hero{padding:28px 18px 16px;}
  #despApp .toprow{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:12px;}
  #despApp .eyebrow{font-size:12px;letter-spacing:.22em;text-transform:uppercase;color:var(--gold);font-weight:700;margin:0;display:flex;align-items:center;gap:8px;}
  #despApp .eyebrow::before{content:"";width:24px;height:2px;background:var(--gold);border-radius:2px;}
  #despApp .sync{display:flex;align-items:center;gap:7px;font-size:12px;font-weight:600;color:var(--muted);background:var(--surface);border:1px solid var(--line);padding:5px 11px;border-radius:20px;white-space:nowrap;}
  #despApp .dot{width:8px;height:8px;border-radius:50%;background:var(--faint);flex:0 0 auto;}
  #despApp .sync.live .dot{background:var(--green);box-shadow:0 0 0 0 rgba(67,221,156,.6);animation:pulse 2s infinite;}
  #despApp .sync.local .dot{background:var(--amber);}
  @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(67,221,156,.55);}70%{box-shadow:0 0 0 7px rgba(67,221,156,0);}100%{box-shadow:0 0 0 0 rgba(67,221,156,0);}}
  @media (prefers-reduced-motion:reduce){#despApp .sync.live .dot{animation:none;}}

  #despApp .title{font-size:clamp(34px,9vw,52px);line-height:.98;margin:0;font-weight:800;letter-spacing:-.02em;}
  #despApp .title em{font-style:normal;color:var(--rose);}
  #despApp .subtitle{color:var(--muted);margin:10px 0 0;font-size:15px;max-width:46ch;}
  #despApp .countdown{display:flex;gap:8px;margin-top:20px;flex-wrap:wrap;}
  #despApp .cd-box{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:10px 14px;min-width:66px;text-align:center;box-shadow:var(--shadow);}
  #despApp .cd-num{font-size:26px;font-weight:800;font-variant-numeric:tabular-nums;letter-spacing:-.02em;line-height:1;}
  #despApp .cd-lab{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-top:5px;}
  #despApp .cd-num.live{color:var(--rose);}

  #despApp .tabs{position:sticky;top:0;z-index:20;background:color-mix(in srgb,var(--ink) 88%,transparent);backdrop-filter:blur(12px);border-bottom:1px solid var(--line);margin-top:16px;}
  #despApp .tabs-inner{display:flex;gap:4px;padding:6px;}
  #despApp .tab{flex:1;border:0;background:transparent;color:var(--muted);font:inherit;font-weight:600;font-size:14px;padding:10px 6px;border-radius:10px;cursor:pointer;transition:background .18s,color .18s;display:flex;align-items:center;justify-content:center;gap:6px;}
  #despApp .tab:hover{color:var(--text);}
  #despApp .tab[aria-selected="true"]{background:var(--rose);color:#fff;}
  #despApp .tab .badge{background:color-mix(in srgb,currentColor 22%,transparent);border-radius:20px;font-size:11px;padding:1px 7px;font-variant-numeric:tabular-nums;}

  #despApp main{padding:20px 18px 0;}
  #despApp .panel{display:none;}
  #despApp .panel.active{display:block;animation:fade .25s ease;}
  @keyframes fade{from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:none;}}
  @media (prefers-reduced-motion:reduce){#despApp .panel.active{animation:none;}}
  #despApp .section-head{display:flex;align-items:baseline;justify-content:space-between;margin:4px 0 14px;gap:10px;}
  #despApp .section-head h2{font-size:20px;margin:0;letter-spacing:-.01em;}
  #despApp .section-head .hint{font-size:13px;color:var(--faint);text-align:right;}

  #despApp .card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:14px;margin-bottom:10px;box-shadow:var(--shadow);}
  #despApp .row{display:flex;gap:12px;align-items:center;}

  #despApp .ag-item{display:flex;gap:14px;align-items:flex-start;}
  #despApp .ag-time{font-weight:800;font-variant-numeric:tabular-nums;color:var(--gold);background:var(--surface-2);border:1px solid var(--line);border-radius:10px;padding:6px 10px;font-size:15px;min-width:62px;text-align:center;flex-shrink:0;}
  #despApp .ag-body{flex:1;min-width:0;}
  #despApp .ag-plan{font-weight:600;font-size:15px;}
  #despApp .ag-place{color:var(--muted);font-size:13px;margin-top:3px;}
  #despApp .ag-place:not(:empty)::before{content:"📍 ";}
  #despApp .ag-place:empty::before{content:"➕ añadir lugar";color:var(--faint);}

  #despApp .avatar{width:38px;height:38px;border-radius:50%;flex-shrink:0;display:grid;place-items:center;font-weight:700;color:#fff;font-size:15px;}
  #despApp .person{display:flex;align-items:center;gap:10px;}
  #despApp .person-main{flex:1;min-width:0;}
  #despApp .person-name{font-weight:600;font-size:15px;}
  #despApp .person-sub{font-size:12px;color:var(--faint);}
  #despApp .status-pills{display:flex;gap:4px;}
  #despApp .sp{border:1px solid var(--line);background:var(--surface-2);color:var(--muted);border-radius:20px;font:inherit;font-size:12px;font-weight:600;padding:5px 9px;cursor:pointer;transition:all .15s;}
  #despApp .sp[data-on="si"]{background:var(--green);border-color:var(--green);color:#06231a;}
  #despApp .sp[data-on="pend"]{background:var(--amber);border-color:var(--amber);color:#2c1c00;}
  #despApp .sp[data-on="no"]{background:var(--surface-2);border-color:var(--rose);color:var(--rose);}
  #despApp .plus-ctrl{display:flex;align-items:center;gap:6px;}
  #despApp .plus-ctrl button{width:26px;height:26px;border-radius:8px;border:1px solid var(--line);background:var(--surface-2);color:var(--text);font:inherit;font-size:16px;cursor:pointer;line-height:1;}
  #despApp .plus-ctrl span{font-variant-numeric:tabular-nums;min-width:16px;text-align:center;font-size:14px;}
  #despApp .tally{display:flex;gap:8px;margin-bottom:14px;}
  #despApp .tally-box{flex:1;background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:10px 12px;text-align:center;}
  #despApp .tally-num{font-size:24px;font-weight:800;font-variant-numeric:tabular-nums;}
  #despApp .tally-lab{font-size:11px;color:var(--faint);text-transform:uppercase;letter-spacing:.1em;margin-top:2px;}
  #despApp .tally-box.c-si .tally-num{color:var(--green);}
  #despApp .tally-box.c-pend .tally-num{color:var(--amber);}

  #despApp .bote-hero{background:linear-gradient(135deg,color-mix(in srgb,var(--rose) 24%,var(--surface)),var(--surface));border:1px solid var(--line);border-radius:var(--radius);padding:18px;margin-bottom:14px;box-shadow:var(--shadow);}
  #despApp .bote-total{font-size:40px;font-weight:800;letter-spacing:-.02em;font-variant-numeric:tabular-nums;line-height:1;}
  #despApp .bote-total small{font-size:20px;font-weight:700;opacity:.7;}
  #despApp .bote-sub{color:var(--rose-soft);font-size:13px;margin-top:6px;font-weight:600;}
  #despApp .bote-split{display:flex;gap:16px;margin-top:14px;flex-wrap:wrap;}
  #despApp .bote-split div{font-size:13px;color:var(--muted);}
  #despApp .bote-split b{display:block;font-size:18px;color:var(--text);font-variant-numeric:tabular-nums;margin-top:2px;}
  #despApp .exp{display:flex;align-items:center;gap:12px;}
  #despApp .exp-main{flex:1;min-width:0;}
  #despApp .exp-concept{font-weight:600;font-size:14px;}
  #despApp .exp-payer{font-size:12px;color:var(--faint);}
  #despApp .exp-amt{font-weight:700;font-variant-numeric:tabular-nums;font-size:15px;}
  #despApp .balance{display:flex;align-items:center;justify-content:space-between;padding:9px 0;border-bottom:1px solid var(--line);}
  #despApp .balance:last-child{border-bottom:0;}
  #despApp .bv{font-variant-numeric:tabular-nums;font-weight:700;font-size:14px;}
  #despApp .bv.pos{color:var(--green);}
  #despApp .bv.neg{color:var(--rose);}
  #despApp .bv.zero{color:var(--faint);}

  #despApp .addform{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px;}
  #despApp input,#despApp select{font:inherit;color:var(--text);background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:10px 12px;outline:none;min-width:0;}
  #despApp input:focus,#despApp select:focus{border-color:var(--rose);box-shadow:0 0 0 3px color-mix(in srgb,var(--rose) 25%,transparent);}
  #despApp input::placeholder{color:var(--faint);}
  #despApp .btn{font:inherit;font-weight:700;border:0;border-radius:10px;padding:10px 16px;cursor:pointer;background:var(--rose);color:#fff;transition:filter .15s,transform .05s;}
  #despApp .btn:hover{filter:brightness(1.08);}
  #despApp .btn:active{transform:translateY(1px);}
  #despApp .btn.ghost{background:var(--surface-2);color:var(--text);border:1px solid var(--line);}
  #despApp .icon-btn{border:0;background:transparent;color:var(--faint);cursor:pointer;font-size:17px;padding:4px 6px;border-radius:8px;line-height:1;}
  #despApp .icon-btn:hover{color:var(--rose);background:var(--surface-2);}
  #despApp .empty{text-align:center;color:var(--faint);padding:24px 10px;font-size:14px;}
  #despApp [contenteditable]{outline:none;border-radius:6px;}
  #despApp [contenteditable]:focus{box-shadow:0 0 0 2px color-mix(in srgb,var(--rose) 40%,transparent);}

  #despApp .share{margin-top:22px;padding:16px;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);}
  #despApp .share h3{margin:0 0 4px;font-size:15px;}
  #despApp .share p{margin:0 0 12px;font-size:13px;color:var(--muted);}
  #despApp .share-btns{display:flex;gap:8px;flex-wrap:wrap;}
  #despApp .toast{position:fixed;left:50%;bottom:22px;transform:translateX(-50%) translateY(20px);background:var(--text);color:var(--ink);padding:11px 18px;border-radius:999px;font-weight:600;font-size:14px;opacity:0;pointer-events:none;transition:all .28s;z-index:9999;box-shadow:var(--shadow);}
  #despApp .toast.show{opacity:1;transform:translateX(-50%) translateY(0);}
  #despApp .footnote{margin:16px auto 0;max-width:var(--maxw);padding:0 18px;color:var(--faint);font-size:12px;text-align:center;line-height:1.5;}
</style>

  <header class="hero">
    <div class="toprow">
      <p class="eyebrow">Última noche de libertad</p>
      <div class="sync local" id="syncPill"><span class="dot"></span><span id="syncTxt">Conectando…</span></div>
    </div>
    <h1 class="title">Despedida de <em contenteditable="true" id="heName" spellcheck="false">Alberto</em></h1>
    <p class="subtitle" contenteditable="true" id="heSub" spellcheck="false">Sábado 8 de agosto · lo damos todo por el que se nos casa 🥂</p>
    <div class="countdown" id="countdown" aria-live="polite"></div>
  </header>

  <nav class="tabs" role="tablist" aria-label="Secciones">
    <div class="tabs-inner">
      <button class="tab" role="tab" id="tab-agenda" aria-controls="p-agenda" aria-selected="true">🗓️ Plan <span class="badge" id="b-agenda">0</span></button>
      <button class="tab" role="tab" id="tab-gente" aria-controls="p-gente" aria-selected="false">👥 Gente <span class="badge" id="b-gente">0</span></button>
      <button class="tab" role="tab" id="tab-bote" aria-controls="p-bote" aria-selected="false">💶 Bote <span class="badge" id="b-bote">0€</span></button>
    </div>
  </nav>

  <main>
    <section class="panel active" id="p-agenda" role="tabpanel" aria-labelledby="tab-agenda">
      <div class="section-head"><h2>Plan del día</h2><span class="hint">toca la hora o el texto para editar</span></div>
      <div id="agendaList"></div>
      <div class="card"><div class="addform">
        <input id="agTime" type="text" inputmode="numeric" placeholder="13:00" style="max-width:90px" aria-label="Hora">
        <input id="agPlan" type="text" placeholder="Qué hacemos" style="flex:1 1 120px" aria-label="Plan">
        <input id="agPlace" type="text" placeholder="Dónde (opcional)" style="flex:1 1 120px" aria-label="Lugar">
        <button class="btn" id="agAdd">Añadir</button>
      </div></div>
    </section>

    <section class="panel" id="p-gente" role="tabpanel" aria-labelledby="tab-gente">
      <div class="tally">
        <div class="tally-box c-si"><div class="tally-num" id="tSi">0</div><div class="tally-lab">Van seguro</div></div>
        <div class="tally-box c-pend"><div class="tally-num" id="tPend">0</div><div class="tally-lab">Pendientes</div></div>
        <div class="tally-box"><div class="tally-num" id="tHeads">0</div><div class="tally-lab">Cabezas</div></div>
      </div>
      <div id="peopleList"></div>
      <div class="card"><div class="addform">
        <input id="pName" type="text" placeholder="Nombre" style="flex:1 1 140px" aria-label="Nombre">
        <button class="btn" id="pAdd">Añadir</button>
      </div></div>
    </section>

    <section class="panel" id="p-bote" role="tabpanel" aria-labelledby="tab-bote">
      <div class="bote-hero">
        <div class="bote-total" id="boteTotal">0<small>€</small></div>
        <div class="bote-sub" id="boteSub">Aún no hay gastos apuntados</div>
        <div class="bote-split">
          <div>Por cabeza<b id="botePer">0€</b></div>
          <div>Cabezas<b id="boteHeads">0</b></div>
          <div>Gastos<b id="boteCount">0</b></div>
        </div>
      </div>
      <div class="section-head"><h2>Gastos</h2><span class="hint">quién ha puesto qué</span></div>
      <div id="expList"></div>
      <div class="card"><div class="addform">
        <input id="exConcept" type="text" placeholder="Concepto (cena, karting…)" style="flex:1 1 120px" aria-label="Concepto">
        <select id="exPayer" aria-label="Quién pagó"></select>
        <input id="exAmt" type="number" inputmode="decimal" min="0" step="0.01" placeholder="€" style="max-width:90px" aria-label="Importe">
        <button class="btn" id="exAdd">Añadir</button>
      </div></div>
      <div class="section-head" style="margin-top:22px"><h2>Cuentas claras</h2><span class="hint">reparto a partes iguales</span></div>
      <div class="card" id="balanceCard"></div>
    </section>
  </main>

  <div class="share">
    <h3>Compartir con el grupo</h3>
    <p>Pasa el enlace de esta página por WhatsApp. Todos editan aquí y se sincroniza en el móvil de cada uno.</p>
    <div class="share-btns">
      <button class="btn" id="shareLink">🔗 Copiar enlace</button>
      <button class="btn ghost" id="shareWa">💬 WhatsApp</button>
      <button class="btn ghost" id="shareDl">⬇️ Descargar resumen</button>
    </div>
  </div>
  <p class="footnote" id="footnote">Los cambios se guardan en el sitio y se sincronizan cada pocos segundos.</p>
</div>

<script>
(function () {
  "use strict";

  /* ====== CONFIG ====== */
  var API = "/wp-json/despedida/v1/state";   // endpoint REST (mismo sitio)
  var TOKEN = "ALBERTO-8AGO";                 // mismo token que en el PHP
  var EVENT_ISO = "2026-08-08T13:00:00";
  var POLL_MS = 3000;
  /* ==================== */

  var COLORS = ["#ff476f","#e8b54d","#43dd9c","#5b8cff","#c07bff","#ff8a4c","#3ec8d8","#f5648a"];
  var root = document.getElementById("despApp");
  var $ = function (id) { return document.getElementById(id); };
  var clientId = "c" + Math.floor(Math.random() * 1e9).toString(36);
  var now = function () { return Date.now(); };

  function baseState() {
    var t = 1;
    return {
      room: "despedida-alberto",
      name: "Alberto", nameM: t, sub: "Sábado 8 de agosto · lo damos todo por el que se nos casa 🥂", subM: t,
      tomb: {}, updatedAt: t,
      agenda: [
        { id:"a1", m:t, time:"13:00", plan:"Vermú de bienvenida", place:"" },
        { id:"a2", m:t, time:"14:30", plan:"Comida (mesa reservada)", place:"" },
        { id:"a3", m:t, time:"18:00", plan:"Actividad sorpresa", place:"" },
        { id:"a4", m:t, time:"21:30", plan:"Cena", place:"" },
        { id:"a5", m:t, time:"00:00", plan:"Fiesta 🎉", place:"" }
      ],
      people: [ { id:"p1", m:t, name:"Alberto", status:"si", plus:0 }, { id:"p2", m:t, name:"Antonio", status:"si", plus:0 } ],
      expenses: []
    };
  }

  var state = loadLocal() || baseState();

  /* ---------- local cache ---------- */
  function loadLocal() { try { var r = localStorage.getItem("despedida-alberto"); return r ? JSON.parse(r) : null; } catch (e) { return null; } }
  function saveLocal() { try { localStorage.setItem("despedida-alberto", JSON.stringify(state)); } catch (e) {} }

  /* ---------- merge (CRDT-lite) ---------- */
  function mergeLists(a, b, tomb) {
    var map = {};
    (a || []).forEach(function (it) { map[it.id] = it; });
    (b || []).forEach(function (it) { var e = map[it.id]; if (!e || (it.m||0) > (e.m||0)) map[it.id] = it; });
    var out = [];
    Object.keys(map).forEach(function (id) { var it = map[id]; if ((tomb[id]||0) >= (it.m||0)) return; out.push(it); });
    return out;
  }
  function merge(local, remote) {
    if (!remote) return local;
    var tomb = {};
    [local.tomb, remote.tomb].forEach(function (t) { if (t) Object.keys(t).forEach(function (id) { tomb[id] = Math.max(tomb[id]||0, t[id]||0); }); });
    var m = {
      room: local.room || "despedida-alberto", tomb: tomb,
      name:  (remote.nameM||0) > (local.nameM||0) ? remote.name : local.name,  nameM: Math.max(local.nameM||0, remote.nameM||0),
      sub:   (remote.subM||0)  > (local.subM||0)  ? remote.sub  : local.sub,   subM:  Math.max(local.subM||0,  remote.subM||0),
      agenda:   mergeLists(local.agenda,   remote.agenda,   tomb),
      people:   mergeLists(local.people,   remote.people,   tomb),
      expenses: mergeLists(local.expenses, remote.expenses, tomb),
      updatedAt: Math.max(local.updatedAt||0, remote.updatedAt||0)
    };
    m.agenda.sort(function (a, b) { return (a.time||"99").localeCompare(b.time||"99"); });
    return m;
  }
  function canon(s) {
    if (!s) return "null";
    var sl = function (l) { return (l||[]).slice().sort(function (a, b) { return a.id < b.id ? -1 : 1; }); };
    var tk = {}; Object.keys(s.tomb||{}).sort().forEach(function (k) { tk[k] = s.tomb[k]; });
    return JSON.stringify({ name:s.name, nameM:s.nameM, sub:s.sub, subM:s.subM, tomb:tk, agenda:sl(s.agenda), people:sl(s.people), expenses:sl(s.expenses) });
  }

  /* ---------- network ---------- */
  var syncing = false, lastOk = 0;
  function apiGet() {
    var ctrl = new AbortController(); var to = setTimeout(function () { ctrl.abort(); }, 6000);
    return fetch(API, { method:"GET", headers:{ "Accept":"application/json" }, signal:ctrl.signal, cache:"no-store" })
      .then(function (r) { clearTimeout(to); if (!r.ok) throw new Error("http " + r.status); return r.text(); })
      .then(function (t) { return t && t !== "null" ? JSON.parse(t) : null; });
  }
  function apiPost(s) {
    var ctrl = new AbortController(); var to = setTimeout(function () { ctrl.abort(); }, 6000);
    return fetch(API, { method:"POST", headers:{ "Content-Type":"application/json", "X-Room-Token":TOKEN }, body:JSON.stringify(s), signal:ctrl.signal })
      .then(function (r) { clearTimeout(to); if (!r.ok) throw new Error("http " + r.status); return true; });
  }
  function setStatus(ok) {
    var pill = $("syncPill"), txt = $("syncTxt");
    if (ok) { pill.className = "sync live"; txt.textContent = "En vivo"; }
    else { pill.className = "sync local"; txt.textContent = "Modo local"; }
  }

  function sync() {
    if (syncing) return Promise.resolve();
    syncing = true;
    return apiGet().then(function (remote) {
      lastOk = now(); setStatus(true);
      var merged = merge(state, remote);
      if (canon(merged) !== canon(state)) { state = merged; saveLocal(); render(); }
      if (canon(merged) !== canon(remote)) { return apiPost(state); }
    }).catch(function () {
      if (now() - lastOk > 7000) setStatus(false);
    }).then(function () { syncing = false; });
  }
  var pushT = null;
  function scheduleSync() { clearTimeout(pushT); pushT = setTimeout(sync, 450); }

  /* ---------- mutations ---------- */
  function changed() { state.updatedAt = now(); saveLocal(); render(); scheduleSync(); }
  function uid(p) { return (p||"x") + now().toString(36) + Math.floor(Math.random()*1e4).toString(36); }
  function killItem(list, it) { state.tomb[it.id] = now(); state[list] = state[list].filter(function (x) { return x.id !== it.id; }); changed(); }

  /* ---------- helpers ---------- */
  function el(tag, cls, html) { var e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; }
  function esc(s) { return String(s).replace(/[&<>"]/g, function (c) { return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]; }); }
  function money(n) { var r = Math.round(n*100)/100; return (Number.isInteger(r) ? r.toString() : r.toFixed(2)).replace(".", ","); }
  function heads() { return state.people.reduce(function (a, p) { return a + (p.status === "si" ? 1 + (p.plus||0) : 0); }, 0); }
  function totalSpent() { return state.expenses.reduce(function (a, e) { return a + (Number(e.amt)||0); }, 0); }
  function toast(msg) { var t = $("despToast"); t.textContent = msg; t.classList.add("show"); clearTimeout(toast._t); toast._t = setTimeout(function () { t.classList.remove("show"); }, 1900); }

  /* ---------- countdown ---------- */
  function tickCountdown() {
    var box = $("countdown"), diff = new Date(EVENT_ISO) - new Date();
    if (diff <= 0) { box.innerHTML = '<div class="cd-box"><div class="cd-num live">¡HOY!</div><div class="cd-lab">Es la noche 🔥</div></div>'; return; }
    var d = Math.floor(diff/86400000), h = Math.floor(diff%86400000/3600000), m = Math.floor(diff%3600000/60000);
    box.innerHTML = [[d,"Días"],[h,"Horas"],[m,"Min"]].map(function (p) {
      return '<div class="cd-box"><div class="cd-num">'+p[0]+'</div><div class="cd-lab">'+p[1]+'</div></div>';
    }).join("");
  }

  /* ---------- render ---------- */
  function render() {
    if (document.activeElement && document.activeElement.isContentEditable) { /* no repintar mientras se edita texto */ }
    else { renderHeader(); }
    renderAgenda(); renderPeople(); renderBote(); renderBadges();
  }
  function renderHeader() {
    if ($("heName").textContent !== state.name) $("heName").textContent = state.name;
    if ($("heSub").textContent !== state.sub) $("heSub").textContent = state.sub;
  }
  function renderBadges() {
    $("b-agenda").textContent = state.agenda.length;
    $("b-gente").textContent = state.people.filter(function (p) { return p.status === "si"; }).length;
    $("b-bote").textContent = money(totalSpent()) + "€";
  }
  function renderAgenda() {
    var list = $("agendaList"); list.innerHTML = "";
    if (!state.agenda.length) { list.appendChild(el("div","empty","Sin planes todavía. Añade el primero abajo 👇")); return; }
    state.agenda.forEach(function (it) {
      var card = el("div","card"), wrap = el("div","ag-item");
      var time = el("div","ag-time"); time.contentEditable = "true"; time.spellcheck = false; time.textContent = it.time || "—";
      time.addEventListener("blur", function () { var v = time.textContent.trim(); if (v !== it.time) { it.time = v; it.m = now(); changed(); } });
      var body = el("div","ag-body");
      var plan = el("div","ag-plan"); plan.contentEditable = "true"; plan.spellcheck = false; plan.textContent = it.plan || "";
      plan.addEventListener("blur", function () { var v = plan.textContent.trim(); if (v !== it.plan) { it.plan = v; it.m = now(); changed(); } });
      var place = el("div","ag-place"); place.contentEditable = "true"; place.spellcheck = false; place.textContent = it.place || "";
      place.addEventListener("blur", function () { var v = place.textContent.trim(); if (v !== it.place) { it.place = v; it.m = now(); changed(); } });
      body.appendChild(plan); body.appendChild(place);
      var del = el("button","icon-btn","✕"); del.title = "Eliminar"; del.addEventListener("click", function () { killItem("agenda", it); });
      wrap.appendChild(time); wrap.appendChild(body); wrap.appendChild(del); card.appendChild(wrap); list.appendChild(card);
    });
  }
  function renderPeople() {
    var list = $("peopleList"); list.innerHTML = "";
    var si = 0, pend = 0;
    state.people.forEach(function (p) { if (p.status === "si") si++; else if (p.status === "pend") pend++; });
    $("tSi").textContent = si; $("tPend").textContent = pend; $("tHeads").textContent = heads();
    if (!state.people.length) list.appendChild(el("div","empty","Aún no hay nadie apuntado."));
    state.people.forEach(function (p, i) {
      var card = el("div","card"), row = el("div","person");
      var av = el("div","avatar"); av.style.background = COLORS[i % COLORS.length]; av.textContent = (p.name||"?").trim().charAt(0).toUpperCase() || "?";
      var main = el("div","person-main");
      var nm = el("div","person-name"); nm.contentEditable = "true"; nm.spellcheck = false; nm.textContent = p.name;
      nm.addEventListener("blur", function () { var v = nm.textContent.trim() || "¿?"; if (v !== p.name) { p.name = v; p.m = now(); changed(); refreshPayers(); } });
      var sub = el("div","person-sub"); sub.textContent = p.plus ? ("+"+p.plus+" acompañante"+(p.plus>1?"s":"")) : "solo";
      main.appendChild(nm); main.appendChild(sub);
      var pills = el("div","status-pills");
      [["si","Va"],["pend","?"],["no","No"]].forEach(function (opt) {
        var b = el("button","sp",opt[1]); if (p.status === opt[0]) b.setAttribute("data-on", opt[0]);
        b.addEventListener("click", function () { p.status = opt[0]; p.m = now(); changed(); }); pills.appendChild(b);
      });
      var plus = el("div","plus-ctrl");
      var minus = el("button",null,"−"); minus.setAttribute("aria-label","Quitar acompañante");
      var cnt = el("span",null,String(p.plus||0));
      var addb = el("button",null,"+"); addb.setAttribute("aria-label","Añadir acompañante");
      minus.addEventListener("click", function () { p.plus = Math.max(0,(p.plus||0)-1); p.m = now(); changed(); });
      addb.addEventListener("click", function () { p.plus = (p.plus||0)+1; p.m = now(); changed(); });
      plus.appendChild(minus); plus.appendChild(cnt); plus.appendChild(addb);
      var del = el("button","icon-btn","✕"); del.title = "Eliminar"; del.addEventListener("click", function () { killItem("people", p); refreshPayers(); });
      row.appendChild(av); row.appendChild(main); card.appendChild(row);
      var row2 = el("div","row"); row2.style.marginTop = "12px"; row2.style.justifyContent = "space-between";
      row2.appendChild(pills); row2.appendChild(plus); row2.appendChild(del); card.appendChild(row2); list.appendChild(card);
    });
  }
  function refreshPayers() {
    var sel = $("exPayer"), cur = sel.value; sel.innerHTML = "";
    if (!state.people.length) { var o = el("option"); o.value = ""; o.textContent = "(añade gente)"; sel.appendChild(o); return; }
    state.people.forEach(function (p) { var o = el("option"); o.value = p.id; o.textContent = p.name; sel.appendChild(o); });
    if (cur && state.people.some(function (p) { return p.id === cur; })) sel.value = cur;
  }
  function renderBote() {
    var total = totalSpent(), h = heads(), per = h > 0 ? total/h : 0;
    $("boteTotal").innerHTML = money(total) + '<small>€</small>';
    $("botePer").textContent = money(per) + "€"; $("boteHeads").textContent = h; $("boteCount").textContent = state.expenses.length;
    $("boteSub").textContent = total === 0 ? "Aún no hay gastos apuntados" : (h > 0 ? "Cada cabeza pone " + money(per) + "€" : "Marca quién va para repartir");
    var list = $("expList"); list.innerHTML = "";
    if (!state.expenses.length) list.appendChild(el("div","empty","Sin gastos. Apunta lo que se va pagando."));
    state.expenses.forEach(function (e) {
      var payer = state.people.filter(function (p) { return p.id === e.payer; })[0];
      var card = el("div","card"), r = el("div","exp"), main = el("div","exp-main");
      main.appendChild(el("div","exp-concept", esc(e.concept)));
      main.appendChild(el("div","exp-payer","Pagó " + esc(payer ? payer.name : "¿?")));
      var del = el("button","icon-btn","✕"); del.title = "Eliminar"; del.addEventListener("click", function () { killItem("expenses", e); });
      r.appendChild(main); r.appendChild(el("div","exp-amt", money(e.amt) + "€")); r.appendChild(del); card.appendChild(r); list.appendChild(card);
    });
    renderBalances(per);
  }
  function renderBalances(per) {
    var card = $("balanceCard"); card.innerHTML = "";
    var going = state.people.filter(function (p) { return p.status === "si"; });
    if (!going.length || totalSpent() === 0) { card.appendChild(el("div","empty","Marca quién va y apunta gastos para ver el reparto.")); return; }
    going.forEach(function (p) {
      var share = per * (1 + (p.plus||0));
      var paid = state.expenses.reduce(function (a, e) { return a + (e.payer === p.id ? (Number(e.amt)||0) : 0); }, 0);
      var net = paid - share, row = el("div","balance");
      row.appendChild(el("div","bn", esc(p.name) + (p.plus ? " (+"+p.plus+")" : "")));
      var cls = net > 0.005 ? "pos" : net < -0.005 ? "neg" : "zero";
      var txt = net > 0.005 ? "le deben " + money(net) + "€" : net < -0.005 ? "pone " + money(-net) + "€" : "en paz ✓";
      row.appendChild(el("div","bv " + cls, txt)); card.appendChild(row);
    });
  }

  /* ---------- summary / share ---------- */
  function summaryText() {
    var L = [];
    L.push("🥳 DESPEDIDA DE " + (state.name||"ALBERTO").toUpperCase());
    L.push(state.sub || ""); L.push("");
    L.push("🗓️ PLAN");
    state.agenda.forEach(function (a) { L.push("  " + (a.time||"—") + "  " + a.plan + (a.place ? " (" + a.place + ")" : "")); });
    L.push("");
    L.push("👥 VAN (" + heads() + " en total)");
    state.people.filter(function (p) { return p.status === "si"; }).forEach(function (p) { L.push("  ✓ " + p.name + (p.plus ? " +" + p.plus : "")); });
    var pend = state.people.filter(function (p) { return p.status === "pend"; });
    if (pend.length) L.push("  ⏳ Pendientes: " + pend.map(function (p) { return p.name; }).join(", "));
    var total = totalSpent(), h = heads();
    if (total > 0) { L.push(""); L.push("💶 BOTE: " + money(total) + "€" + (h > 0 ? "  ·  " + money(total/h) + "€ por cabeza" : "")); }
    L.push(""); L.push(location.origin + location.pathname);
    return L.join("\n");
  }
  function copy(text, ok) {
    if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(function () { toast(ok); }, function () { fb(text, ok); });
    else fb(text, ok);
    function fb(t, ok) { var ta = document.createElement("textarea"); ta.value = t; ta.style.position = "fixed"; ta.style.opacity = "0"; root.appendChild(ta); ta.select(); try { document.execCommand("copy"); toast(ok); } catch (e) { toast("Copia manual"); } root.removeChild(ta); }
  }
  function download() {
    var blob = new Blob([summaryText()], { type:"text/plain;charset=utf-8" });
    var a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = "despedida-alberto.txt";
    root.appendChild(a); a.click(); a.remove(); setTimeout(function () { URL.revokeObjectURL(a.href); }, 1000);
  }

  /* ---------- header edit ---------- */
  $("heName").addEventListener("blur", function () { var v = $("heName").textContent.trim() || "Alberto"; if (v !== state.name) { state.name = v; state.nameM = now(); changed(); } });
  $("heSub").addEventListener("blur", function () { var v = $("heSub").textContent.trim(); if (v !== state.sub) { state.sub = v; state.subM = now(); changed(); } });

  /* ---------- adds ---------- */
  function addAgenda() {
    var time = $("agTime").value.trim(), plan = $("agPlan").value.trim(), place = $("agPlace").value.trim();
    if (!plan) { $("agPlan").focus(); return; }
    state.agenda.push({ id:uid("a"), m:now(), time:time, plan:plan, place:place });
    state.agenda.sort(function (a, b) { return (a.time||"99").localeCompare(b.time||"99"); });
    $("agTime").value = ""; $("agPlan").value = ""; $("agPlace").value = ""; changed(); $("agTime").focus();
  }
  function addPerson() { var n = $("pName").value.trim(); if (!n) return; state.people.push({ id:uid("p"), m:now(), name:n, status:"pend", plus:0 }); $("pName").value = ""; changed(); refreshPayers(); $("pName").focus(); }
  function addExpense() {
    var c = $("exConcept").value.trim(), payer = $("exPayer").value, amt = parseFloat($("exAmt").value);
    if (!c || !payer || !(amt > 0)) { if (!payer) toast("Añade gente primero"); return; }
    state.expenses.push({ id:uid("e"), m:now(), concept:c, payer:payer, amt:amt });
    $("exConcept").value = ""; $("exAmt").value = ""; changed(); $("exConcept").focus();
  }

  /* ---------- tabs ---------- */
  var tabs = root.querySelectorAll(".tab");
  tabs.forEach(function (t) {
    t.addEventListener("click", function () {
      tabs.forEach(function (x) { x.setAttribute("aria-selected","false"); }); t.setAttribute("aria-selected","true");
      root.querySelectorAll(".panel").forEach(function (p) { p.classList.remove("active"); });
      $(t.getAttribute("aria-controls")).classList.add("active");
    });
  });

  /* ---------- toast node ---------- */
  var toastEl = el("div","toast"); toastEl.id = "despToast"; document.body.appendChild(toastEl);

  /* ---------- wire ---------- */
  $("agAdd").addEventListener("click", addAgenda);
  $("agPlan").addEventListener("keydown", function (e) { if (e.key === "Enter") addAgenda(); });
  $("agPlace").addEventListener("keydown", function (e) { if (e.key === "Enter") addAgenda(); });
  $("pAdd").addEventListener("click", addPerson);
  $("pName").addEventListener("keydown", function (e) { if (e.key === "Enter") addPerson(); });
  $("exAdd").addEventListener("click", addExpense);
  $("exAmt").addEventListener("keydown", function (e) { if (e.key === "Enter") addExpense(); });
  $("shareLink").addEventListener("click", function () { copy(location.origin + location.pathname, "Enlace copiado ✓"); });
  $("shareWa").addEventListener("click", function () { window.open("https://wa.me/?text=" + encodeURIComponent(summaryText()), "_blank"); });
  $("shareDl").addEventListener("click", download);

  /* ---------- go ---------- */
  refreshPayers(); render(); tickCountdown(); setInterval(tickCountdown, 30000);
  sync(); setInterval(sync, POLL_MS);
})();
</script>
0
0
Tu carro de la compra
Tu carro está vacíovolver a la tienda