{"id":836,"date":"2026-04-12T18:55:40","date_gmt":"2026-04-12T18:55:40","guid":{"rendered":"https:\/\/app.bodafranyjose.com\/?page_id=836"},"modified":"2026-04-13T17:57:14","modified_gmt":"2026-04-13T17:57:14","slug":"mesas","status":"publish","type":"page","link":"https:\/\/app.bodafranyjose.com\/index.php\/mesas\/","title":{"rendered":"mesas"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"836\" class=\"elementor elementor-836\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-c48fa9f e-flex e-con-boxed e-con e-parent\" data-id=\"c48fa9f\" data-element_type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f1cac3f elementor-widget elementor-widget-html\" data-id=\"f1cac3f\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<style>\n  .seating-container {\n    background: #fff;\n    border-radius: 22px;\n    box-shadow: 0 6px 24px rgba(0,0,0,0.07);\n    padding: 22px 18px;\n    max-width: 1050px;\n    margin: 0 auto;\n    font-family: 'Poppins', sans-serif;\n    color: #333;\n  }\n\n  .seating-head {\n    text-align: center;\n    margin-bottom: 14px;\n  }\n\n  .seating-title {\n    color: #b67b91;\n    margin: 0 0 5px;\n    font-size: 1.72rem;\n    font-weight: 700;\n    line-height: 1.15;\n  }\n\n  .seating-subtitle {\n    color: #666;\n    margin: 0;\n    font-size: 0.9rem;\n    font-weight: 600;\n    line-height: 1.25;\n  }\n\n  .resumen-dashboard {\n    display: grid;\n    grid-template-columns: repeat(4, minmax(0, 1fr));\n    gap: 9px;\n    margin-bottom: 12px;\n  }\n\n  .resumen-card {\n    background: #faf6f8;\n    border: 1px solid transparent;\n    border-radius: 13px;\n    text-align: center;\n    padding: 8px 7px;\n    box-shadow: 0 2px 9px rgba(0,0,0,0.04);\n    cursor: pointer;\n    transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease, background 0.2s ease;\n  }\n\n  .resumen-card:hover {\n    transform: translateY(-1px);\n    box-shadow: 0 5px 14px rgba(0,0,0,0.07);\n  }\n\n  .resumen-card.active {\n    background: #fff;\n    border-color: #b67b91;\n    box-shadow: 0 5px 16px rgba(182,123,145,0.16);\n  }\n\n  .resumen-card h3 {\n    font-size: 0.76rem;\n    color: #b67b91;\n    margin: 0 0 4px;\n    font-weight: 700;\n    line-height: 1.1;\n  }\n\n  .resumen-card span {\n    font-size: 1.15rem;\n    font-weight: 800;\n    color: #333;\n    line-height: 1.1;\n  }\n\n  .resumen-card.mesas { background: #f4f0f8; }\n  .resumen-card.plazas { background: #f8e6ec; }\n  .resumen-card.sentados { background: #eaf8f0; }\n  .resumen-card.libres { background: #fff3d8; }\n\n  .resumen-card.active.mesas,\n  .resumen-card.active.plazas,\n  .resumen-card.active.sentados,\n  .resumen-card.active.libres {\n    background: #fff;\n  }\n\n  .seating-progress {\n    margin: 0 0 12px;\n  }\n\n  .seating-progress-label {\n    display: flex;\n    justify-content: space-between;\n    gap: 10px;\n    color: #666;\n    font-size: 0.78rem;\n    font-weight: 700;\n    margin-bottom: 5px;\n  }\n\n  .seating-progress-track {\n    height: 7px;\n    background: #f0eaed;\n    border-radius: 999px;\n    overflow: hidden;\n  }\n\n  .seating-progress-track span {\n    display: block;\n    height: 100%;\n    width: 0%;\n    background: linear-gradient(90deg, #b67b91, #a56b82);\n    border-radius: 999px;\n    transition: width 0.4s ease;\n  }\n\n  .seating-tools {\n    display: grid;\n    gap: 9px;\n    margin-bottom: 14px;\n  }\n\n  .seating-search-wrap {\n    position: relative;\n  }\n\n  .seating-search-wrap::before,\n  .seating-search-wrap::after {\n    content: none;\n    display: none;\n  }\n\n  .seating-search {\n    width: 100%;\n    min-height: 43px;\n    padding: 10px 12px;\n    border-radius: 12px;\n    border: 1px solid #eadde3;\n    font-size: 16px;\n    font-weight: 400;\n    box-sizing: border-box;\n    background: #fff;\n    box-shadow: 0 2px 10px rgba(0,0,0,0.035);\n  }\n\n  .seating-search::placeholder {\n    color: #888;\n    font-weight: 300;\n  }\n\n  .seating-actions-primary,\n  .seating-actions-secondary {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 9px;\n  }\n\n  .seating-search:focus,\n  .add-field input:focus,\n  .add-field select:focus,\n  .mesa-controls input:focus,\n  .mesa-controls select:focus,\n  .asiento-search:focus {\n    outline: 2px solid #f2e1e8;\n    border-color: #b67b91;\n  }\n\n  .btn-main,\n  .btn-small,\n  .btn-secondary,\n  .btn-danger {\n    border-radius: 10px;\n    cursor: pointer;\n    font-family: 'Poppins', sans-serif;\n    font-weight: 700;\n    transition: 0.25s ease;\n    white-space: nowrap;\n    min-height: 40px;\n  }\n\n  .btn-main,\n  .btn-small {\n    background: #b67b91;\n    color: #fff;\n    border: none;\n  }\n\n  .btn-main {\n    padding: 10px 13px;\n    font-size: 0.9rem;\n  }\n\n  .btn-small {\n    padding: 8px 12px;\n    font-size: 0.86rem;\n  }\n\n  .btn-main:hover,\n  .btn-small:hover {\n    background: #a56b82;\n  }\n\n  .btn-secondary {\n    background: #fff;\n    color: #b67b91;\n    border: 1px solid #e5c9d4;\n    padding: 8px 12px;\n    font-size: 0.84rem;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.025);\n  }\n\n  .btn-secondary:hover {\n    background: #f9f0f4;\n    border-color: #b67b91;\n  }\n\n  .btn-secondary.feature {\n    background: #faf6f8;\n    border-color: #d8adbf;\n    color: #a5447d;\n  }\n\n  .btn-danger {\n    background: #fff;\n    color: #a5447d;\n    border: 1px solid #f0c8d6;\n    padding: 8px 12px;\n    font-size: 0.86rem;\n  }\n\n  .btn-danger:hover {\n    background: #fcefee;\n    color: #8f3568;\n  }\n\n  .add-table {\n    display: none;\n    grid-template-columns: 1fr 0.7fr 0.55fr auto auto;\n    gap: 10px;\n    align-items: end;\n    margin-bottom: 14px;\n    background: #faf6f8;\n    padding: 12px;\n    border: 1px solid #f0e6ea;\n    border-radius: 16px;\n  }\n\n  .add-table.active {\n    display: grid;\n  }\n\n  .add-field label {\n    display: block;\n    color: #b67b91;\n    font-weight: 700;\n    font-size: 0.82rem;\n    margin-bottom: 5px;\n  }\n\n  .add-field input,\n  .add-field select {\n    width: 100%;\n    min-height: 41px;\n    padding: 9px 10px;\n    border: 1px solid #e2d4da;\n    border-radius: 10px;\n    font-size: 16px;\n    box-sizing: border-box;\n    background: #fff;\n  }\n\n  .sin-sentar-panel {\n    display: none;\n    margin-bottom: 14px;\n    background: #faf6f8;\n    border: 1px solid #f0dce5;\n    border-radius: 16px;\n    padding: 12px;\n    box-shadow: 0 3px 12px rgba(0,0,0,0.035);\n  }\n\n  .sin-sentar-panel.active {\n    display: block;\n  }\n\n  .sin-sentar-head {\n    display: flex;\n    justify-content: space-between;\n    gap: 10px;\n    align-items: flex-start;\n    margin-bottom: 10px;\n  }\n\n  .sin-sentar-head h3 {\n    margin: 0;\n    color: #b67b91;\n    font-size: 0.98rem;\n    font-weight: 800;\n    line-height: 1.2;\n  }\n\n  .sin-sentar-head p {\n    margin: 3px 0 0;\n    color: #666;\n    font-size: 0.8rem;\n    line-height: 1.25;\n  }\n\n  .sin-sentar-balance {\n    background: #fff;\n    border: 1px solid #f0e6ea;\n    color: #444;\n    border-radius: 12px;\n    padding: 9px 10px;\n    margin-bottom: 10px;\n    font-size: 0.84rem;\n    font-weight: 700;\n    line-height: 1.25;\n  }\n\n  .sin-sentar-balance.ok { color: #3c8d56; }\n  .sin-sentar-balance.warn { color: #a5447d; }\n\n  .sin-sentar-grid {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 7px;\n  }\n\n  .sin-sentar-pill {\n    background: #fff;\n    color: #444;\n    border: 1px solid #f0e6ea;\n    border-radius: 999px;\n    padding: 8px 10px;\n    cursor: pointer;\n    font-family: 'Poppins', sans-serif;\n    font-size: 0.82rem;\n    font-weight: 700;\n    transition: 0.2s;\n  }\n\n  .sin-sentar-pill:hover {\n    background: #f2e1e8;\n    color: #b67b91;\n  }\n\n  .sin-sentar-empty {\n    background: #fff;\n    color: #3c8d56;\n    border: 1px solid #e3f7e6;\n    border-radius: 12px;\n    padding: 11px;\n    font-size: 0.86rem;\n    font-weight: 700;\n    text-align: center;\n    width: 100%;\n  }\n\n  .mesas-grid {\n    display: grid;\n    grid-template-columns: repeat(2, minmax(0, 1fr));\n    gap: 12px;\n  }\n\n  .mesa-card {\n    background: #fff;\n    border: 1px solid #f0e6ea;\n    border-radius: 16px;\n    overflow: visible;\n    box-shadow: 0 3px 12px rgba(0,0,0,0.045);\n  }\n\n  .mesa-card.completa {\n    border-color: #d9efd9;\n  }\n\n  .mesa-header {\n    display: grid;\n    grid-template-columns: 1fr 34px;\n    gap: 10px;\n    align-items: center;\n    background: #faf6f8;\n    padding: 11px 12px;\n    cursor: pointer;\n    border-radius: 16px 16px 0 0;\n    min-height: 56px;\n  }\n\n  .mesa-header:hover {\n    background: #f8f0f4;\n  }\n\n  .mesa-header h3 {\n    margin: 0;\n    color: #b67b91;\n    font-size: 1rem;\n    line-height: 1.12;\n    font-weight: 800;\n  }\n\n  .mesa-meta {\n    color: #666;\n    font-size: 0.78rem;\n    margin-top: 3px;\n    font-weight: 600;\n    line-height: 1.18;\n  }\n\n  .mesa-badges {\n    display: flex;\n    gap: 5px;\n    flex-wrap: wrap;\n    margin-top: 6px;\n  }\n\n  .mesa-badge {\n    display: inline-flex;\n    align-items: center;\n    background: #fff;\n    color: #b67b91;\n    border: 1px solid #f0e6ea;\n    border-radius: 999px;\n    padding: 2px 7px;\n    font-size: 0.68rem;\n    font-weight: 800;\n    line-height: 1.2;\n  }\n\n  .mesa-badge.ok { color: #3c8d56; }\n  .mesa-badge.warn { color: #a5447d; }\n\n  .mesa-header-actions {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n  }\n\n  .toggle-mesa {\n    color: #b67b91;\n    font-size: 1.05rem;\n    transition: transform 0.25s ease, background 0.2s ease;\n    width: 32px;\n    height: 32px;\n    border-radius: 10px;\n    background: #fff;\n    border: 1px solid #f0e6ea;\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    box-shadow: 0 2px 7px rgba(0,0,0,0.035);\n  }\n\n  .toggle-mesa.open {\n    transform: rotate(180deg);\n    background: #f2e1e8;\n  }\n\n  .mesa-content {\n    display: none;\n    padding: 12px;\n    background: #fff;\n    border-top: 1px solid #f0e6ea;\n    border-radius: 0 0 16px 16px;\n  }\n\n  .mesa-content.active {\n    display: block;\n  }\n\n  .mesa-controls {\n    display: grid;\n    grid-template-columns: 1fr 0.7fr 0.7fr;\n    gap: 9px;\n    margin-bottom: 11px;\n  }\n\n  .mesa-controls input,\n  .mesa-controls select {\n    width: 100%;\n    min-height: 40px;\n    padding: 8px 10px;\n    border: 1px solid #e2d4da;\n    border-radius: 10px;\n    font-size: 16px;\n    box-sizing: border-box;\n    background: #fff;\n  }\n\n  .mesa-visual-wrap {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    margin: 4px 0 12px;\n    background: linear-gradient(180deg, #fff, #fbf7f9);\n    border: 1px solid #f0e6ea;\n    border-radius: 16px;\n    padding: 10px;\n    min-height: 220px;\n    box-sizing: border-box;\n  }\n\n  .mesa-visual {\n    --seat-size: 28px;\n    --seat-radius: 78px;\n    position: relative;\n    width: clamp(184px, 28vw, 200px);\n    aspect-ratio: 1 \/ 1;\n    height: auto;\n    margin: 0 auto;\n  }\n\n  .mesa-table {\n    position: absolute;\n    inset: 24%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: #f8e6ec;\n    border: 2px solid #b67b91;\n    color: #b67b91;\n    font-weight: 800;\n    text-align: center;\n    padding: 10px;\n    box-sizing: border-box;\n    font-size: 0.84rem;\n    line-height: 1.12;\n    word-break: break-word;\n    box-shadow: 0 5px 12px rgba(182,123,145,0.12);\n  }\n\n  .mesa-table.circular {\n    border-radius: 50%;\n  }\n\n  .mesa-table.rectangular {\n    inset: 33% 16%;\n    border-radius: 13px;\n  }\n\n  .seat-dot {\n    position: absolute;\n    width: var(--seat-size);\n    height: var(--seat-size);\n    border-radius: 50%;\n    background: #fff;\n    border: 2px solid #d9c3cd;\n    box-shadow: 0 3px 8px rgba(0,0,0,0.08);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 0.66rem;\n    font-weight: 800;\n    color: #b67b91;\n    line-height: 1;\n  }\n\n  .seat-dot.ocupado {\n    background: #b67b91;\n    border-color: #b67b91;\n    color: #fff;\n  }\n\n  .mesa-visual.circular .seat-dot {\n    left: 50%;\n    top: 50%;\n    transform: translate(-50%, -50%) rotate(var(--angle)) translate(var(--seat-radius)) rotate(calc(var(--angle) * -1));\n    transform-origin: center;\n  }\n\n  .mesa-visual.rectangular .seat-dot {\n    left: var(--x);\n    top: var(--y);\n    transform: translate(-50%, -50%);\n  }\n\n  .asientos-lista {\n    display: grid;\n    gap: 7px;\n  }\n\n  .asiento-row {\n    display: grid;\n    grid-template-columns: 42px 1fr;\n    gap: 8px;\n    align-items: center;\n  }\n\n  .asiento-num {\n    color: #b67b91;\n    font-weight: 800;\n    font-size: 0.78rem;\n    line-height: 1;\n    text-align: right;\n  }\n\n  .asiento-picker {\n    position: relative;\n  }\n\n  .asiento-search {\n    width: 100%;\n    min-height: 40px;\n    padding: 8px 9px;\n    border: 1px solid #e2d4da;\n    border-radius: 10px;\n    font-size: 16px;\n    box-sizing: border-box;\n    background: #fff;\n  }\n\n  .asiento-options {\n    display: none;\n    position: absolute;\n    z-index: 50;\n    left: 0;\n    right: 0;\n    top: calc(100% + 5px);\n    max-height: 220px;\n    overflow-y: auto;\n    background: #fff;\n    border: 1px solid #f0e6ea;\n    border-radius: 12px;\n    box-shadow: 0 9px 24px rgba(0,0,0,0.13);\n    padding: 6px;\n  }\n\n  .asiento-picker.open .asiento-options {\n    display: block;\n  }\n\n  .asiento-option {\n    width: 100%;\n    text-align: left;\n    background: none;\n    border: none;\n    color: #444;\n    padding: 9px 10px;\n    border-radius: 9px;\n    cursor: pointer;\n    font-family: 'Poppins', sans-serif;\n    font-size: 0.88rem;\n    font-weight: 600;\n  }\n\n  .asiento-option:hover {\n    background: #faf6f8;\n    color: #b67b91;\n  }\n\n  .asiento-option.clear {\n    color: #a5447d;\n    font-weight: 800;\n  }\n\n  .asiento-option:disabled {\n    color: #999;\n    cursor: default;\n  }\n\n  .mesa-actions {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 9px;\n    margin-top: 12px;\n  }\n\n  .empty-state {\n    text-align: center;\n    color: #b67b91;\n    padding: 20px 12px;\n    background: #faf6f8;\n    border-radius: 16px;\n    grid-column: 1 \/ -1;\n    font-weight: 700;\n  }\n\n  @media (max-width: 900px) {\n    .add-table,\n    .mesa-controls {\n      grid-template-columns: 1fr 1fr;\n    }\n\n    .mesas-grid {\n      grid-template-columns: 1fr;\n    }\n  }\n\n  @media (max-width: 768px) {\n    .seating-container {\n      padding: 16px 12px 112px;\n      border-radius: 18px;\n    }\n\n    .seating-head {\n      margin-bottom: 10px;\n    }\n\n    .seating-title {\n      font-size: 1.36rem;\n      margin-bottom: 4px;\n    }\n\n    .seating-subtitle {\n      font-size: 0.78rem;\n      line-height: 1.25;\n    }\n\n    .resumen-dashboard {\n      grid-template-columns: repeat(4, minmax(0, 1fr));\n      gap: 6px;\n      margin-bottom: 10px;\n    }\n\n    .resumen-card {\n      padding: 7px 4px;\n      border-radius: 11px;\n      min-height: 48px;\n    }\n\n    .resumen-card h3 {\n      font-size: 0.59rem;\n      margin-bottom: 3px;\n    }\n\n    .resumen-card span {\n      font-size: 0.9rem;\n    }\n\n    .seating-progress {\n      margin-bottom: 10px;\n    }\n\n    .seating-progress-label {\n      font-size: 0.7rem;\n      margin-bottom: 4px;\n    }\n\n    .seating-progress-track {\n      height: 6px;\n    }\n\n    .seating-tools {\n      gap: 8px;\n      margin-bottom: 12px;\n    }\n\n    .seating-search {\n      min-height: 42px;\n      border-radius: 12px;\n      padding: 9px 12px;\n    }\n\n    .btn-main,\n    .btn-secondary,\n    .btn-small,\n    .btn-danger {\n      width: 100%;\n      min-height: 39px;\n    }\n\n    .btn-main {\n      font-size: 0.86rem;\n      padding: 9px 10px;\n    }\n\n    .btn-secondary {\n      font-size: 0.78rem;\n      padding: 8px 9px;\n    }\n\n    .add-table {\n      grid-template-columns: 1fr;\n      gap: 8px;\n      padding: 11px;\n    }\n\n    .mesa-controls {\n      grid-template-columns: minmax(0, 1.25fr) minmax(82px, 0.75fr) minmax(68px, 0.65fr);\n      gap: 6px;\n      padding: 0;\n      margin-bottom: 8px;\n    }\n\n    .mesa-controls input,\n    .mesa-controls select {\n      min-height: 36px;\n      padding: 7px 8px;\n      border-radius: 9px;\n      font-size: 16px;\n    }\n\n    .sin-sentar-panel {\n      padding: 11px;\n      border-radius: 15px;\n      margin-bottom: 12px;\n    }\n\n    .sin-sentar-head {\n      align-items: stretch;\n      gap: 8px;\n    }\n\n    .sin-sentar-head .btn-secondary {\n      width: auto;\n      align-self: flex-start;\n      min-height: 34px;\n      padding: 7px 10px;\n    }\n\n    .sin-sentar-head h3 {\n      font-size: 0.92rem;\n    }\n\n    .sin-sentar-head p {\n      font-size: 0.76rem;\n    }\n\n    .sin-sentar-balance {\n      padding: 8px 9px;\n      font-size: 0.8rem;\n    }\n\n    .sin-sentar-grid {\n      display: grid;\n      grid-template-columns: 1fr;\n      gap: 7px;\n    }\n\n    .sin-sentar-pill {\n      width: 100%;\n      border-radius: 11px;\n      text-align: left;\n      padding: 9px 10px;\n      font-size: 0.82rem;\n      min-height: 39px;\n    }\n\n    .mesas-grid {\n      gap: 10px;\n    }\n\n    .mesa-card {\n      border-radius: 15px;\n      box-shadow: 0 2px 10px rgba(0,0,0,0.04);\n    }\n\n    .mesa-card:last-child {\n      margin-bottom: 22px;\n    }\n\n    .mesa-header {\n      grid-template-columns: 1fr 32px;\n      align-items: center;\n      gap: 8px;\n      padding: 10px 11px;\n      min-height: 52px;\n      border-radius: 15px 15px 0 0;\n    }\n\n    .mesa-header h3 {\n      font-size: 0.94rem;\n    }\n\n    .mesa-meta {\n      font-size: 0.72rem;\n      margin-top: 2px;\n    }\n\n    .mesa-badges {\n      margin-top: 5px;\n      gap: 4px;\n    }\n\n    .mesa-badge {\n      font-size: 0.62rem;\n      padding: 2px 6px;\n    }\n\n    .toggle-mesa {\n      width: 30px;\n      height: 30px;\n      font-size: 0.96rem;\n      border-radius: 9px;\n    }\n\n    .mesa-content {\n      padding: 9px;\n      border-radius: 0 0 15px 15px;\n    }\n\n    .mesa-visual-wrap {\n      padding: 8px;\n      margin: 2px 0 9px;\n      border-radius: 14px;\n      min-height: 186px;\n    }\n\n    .mesa-visual {\n      --seat-size: 24px;\n      --seat-radius: clamp(58px, 18vw, 64px);\n      width: clamp(156px, 48vw, 172px);\n    }\n\n    .mesa-table {\n      inset: 25%;\n      font-size: 0.72rem;\n      padding: 8px;\n    }\n\n    .mesa-table.rectangular {\n      inset: 34% 17%;\n    }\n\n    .seat-dot {\n      font-size: 0.58rem;\n      border-width: 2px;\n      box-shadow: 0 2px 6px rgba(0,0,0,0.075);\n    }\n\n    .asientos-lista {\n      gap: 6px;\n    }\n\n    .asiento-row {\n      grid-template-columns: 30px 1fr;\n      gap: 6px;\n      align-items: center;\n    }\n\n    .asiento-num {\n      font-size: 0.72rem;\n      text-align: right;\n    }\n\n    .asiento-search {\n      min-height: 38px;\n      padding: 7px 9px;\n      border-radius: 9px;\n    }\n\n    .mesa-actions {\n      grid-template-columns: 1fr 1fr;\n      gap: 8px;\n      margin-top: 10px;\n      padding-bottom: 4px;\n    }\n  }\n\n  @media (max-width: 420px) {\n    .mesa-controls {\n      grid-template-columns: 1fr 86px 72px;\n    }\n\n    .mesa-visual-wrap {\n      min-height: 178px;\n    }\n\n    .mesa-visual {\n      --seat-radius: 58px;\n      width: 160px;\n    }\n  }\n\n  @media (max-width: 380px) {\n    .resumen-dashboard {\n      gap: 5px;\n    }\n\n    .resumen-card h3 {\n      font-size: 0.56rem;\n    }\n\n    .resumen-card span {\n      font-size: 0.86rem;\n    }\n\n    .btn-main,\n    .btn-secondary {\n      font-size: 0.75rem;\n    }\n\n    .mesa-controls {\n      grid-template-columns: 1fr;\n      gap: 6px;\n    }\n\n    .mesa-visual-wrap {\n      min-height: 170px;\n      padding: 7px;\n    }\n\n    .mesa-visual {\n      --seat-size: 23px;\n      --seat-radius: 54px;\n      width: 150px;\n    }\n\n    .mesa-table {\n      inset: 26%;\n      font-size: 0.68rem;\n      padding: 7px;\n    }\n\n    .mesa-table.rectangular {\n      inset: 35% 17%;\n    }\n\n    .seat-dot {\n      font-size: 0.54rem;\n    }\n  }\n<\/style>\n\n<div class=\"seating-container\">\n  <div class=\"seating-head\">\n    <h2 class=\"seating-title\">\ud83e\ude91 Seating Plan<\/h2>\n    <p class=\"seating-subtitle\">Coloca invitados, acompa\u00f1a mesas y revisa plazas libres de un vistazo.<\/p>\n  <\/div>\n\n  <div class=\"resumen-dashboard\">\n    <div class=\"resumen-card mesas active\" onclick=\"filtrarMesas('todas')\">\n      <h3>Mesas<\/h3><span id=\"totalMesas\">0<\/span>\n    <\/div>\n    <div class=\"resumen-card plazas\" onclick=\"filtrarMesas('conPlazas')\">\n      <h3>Plazas<\/h3><span id=\"totalPlazas\">0<\/span>\n    <\/div>\n    <div class=\"resumen-card sentados\" onclick=\"filtrarMesas('conSentados')\">\n      <h3>Sentados<\/h3><span id=\"totalSentados\">0<\/span>\n    <\/div>\n    <div class=\"resumen-card libres\" onclick=\"filtrarMesas('conLibres')\">\n      <h3>Libres<\/h3><span id=\"totalLibres\">0<\/span>\n    <\/div>\n  <\/div>\n\n  <div class=\"seating-progress\">\n    <div class=\"seating-progress-label\">\n      <span>Ocupaci\u00f3n total<\/span>\n      <span id=\"seatingProgressText\">0%<\/span>\n    <\/div>\n    <div class=\"seating-progress-track\">\n      <span id=\"seatingProgress\"><\/span>\n    <\/div>\n  <\/div>\n\n  <div class=\"seating-tools\">\n    <div class=\"seating-search-wrap\">\n      <input type=\"text\" id=\"buscarMesa\" class=\"seating-search\" placeholder=\"Buscar mesa o invitado...\" oninput=\"actualizarBusquedaMesas(this.value)\">\n    <\/div>\n\n    <div class=\"seating-actions-primary\">\n      <button class=\"btn-main\" onclick=\"toggleFormularioMesa()\">\u2795 A\u00f1adir mesa<\/button>\n      <button class=\"btn-secondary feature\" id=\"btnSinSentar\" onclick=\"toggleSinSentar()\">\ud83d\udc65 Sin sentar<\/button>\n    <\/div>\n\n    <div class=\"seating-actions-secondary\">\n      <button class=\"btn-secondary\" onclick=\"actualizarInvitados()\">\ud83d\udd04 Invitados<\/button>\n      <button class=\"btn-secondary\" onclick=\"exportarMesas()\">\ud83d\udce4 Exportar<\/button>\n    <\/div>\n  <\/div>\n\n  <div class=\"add-table\" id=\"formNuevaMesa\">\n    <div class=\"add-field\">\n      <label>Nombre<\/label>\n      <input type=\"text\" id=\"nuevaMesaNombre\" placeholder=\"Mesa 1, Familia, Amigos...\">\n    <\/div>\n\n    <div class=\"add-field\">\n      <label>Forma<\/label>\n      <select id=\"nuevaMesaTipo\">\n        <option value=\"circular\">Circular<\/option>\n        <option value=\"rectangular\">Rectangular<\/option>\n      <\/select>\n    <\/div>\n\n    <div class=\"add-field\">\n      <label>Personas<\/label>\n      <input type=\"number\" id=\"nuevaMesaCapacidad\" value=\"8\" min=\"1\" max=\"24\">\n    <\/div>\n\n    <button class=\"btn-main\" onclick=\"agregarMesa()\">Guardar mesa<\/button>\n    <button class=\"btn-secondary\" onclick=\"toggleFormularioMesa()\">Cerrar<\/button>\n  <\/div>\n\n  <div class=\"sin-sentar-panel\" id=\"sinSentarPanel\">\n    <div class=\"sin-sentar-head\">\n      <div>\n        <h3>\ud83d\udc65 Invitados sin sentar<\/h3>\n        <p>Toca un nombre para copiarlo y pegarlo r\u00e1pido en un asiento.<\/p>\n      <\/div>\n      <button class=\"btn-secondary\" onclick=\"toggleSinSentar()\">Cerrar<\/button>\n    <\/div>\n\n    <div class=\"sin-sentar-balance\" id=\"sinSentarBalance\"><\/div>\n    <div class=\"sin-sentar-grid\" id=\"listaSinSentar\"><\/div>\n  <\/div>\n\n  <div id=\"listaMesas\" class=\"mesas-grid\"><\/div>\n<\/div>\n\n<script>\nconst MESAS_JSON_URL = 'https:\/\/app.bodafranyjose.com\/wp-content\/uploads\/mesas\/mesas.json';\nconst GUARDAR_MESAS_URL = 'https:\/\/app.bodafranyjose.com\/wp-content\/uploads\/mesas\/guardar_mesas.php?key=bodafest2025KEY';\nconst INVITADOS_JSON_URL = 'https:\/\/app.bodafranyjose.com\/wp-content\/uploads\/invitados\/invitados.json';\n\nlet mesas = [];\nlet invitados = [];\nlet mesasAbiertas = new Set();\nlet busquedaMesas = '';\nlet filtroMesas = 'todas';\n\nfunction escaparHTML(valor) {\n  return String(valor || \"\")\n    .replace(\/&\/g, \"&amp;\")\n    .replace(\/<\/g, \"&lt;\")\n    .replace(\/>\/g, \"&gt;\")\n    .replace(\/\"\/g, \"&quot;\")\n    .replace(\/'\/g, \"&#039;\");\n}\n\nfunction escaparAtributoJS(valor) {\n  return String(valor || \"\")\n    .replace(\/\\\\\/g, \"\\\\\\\\\")\n    .replace(\/'\/g, \"\\\\'\")\n    .replace(\/\"\/g, \"&quot;\")\n    .replace(\/<\/g, \"&lt;\")\n    .replace(\/>\/g, \"&gt;\")\n    .replace(\/\\n\/g, \" \")\n    .replace(\/\\r\/g, \" \");\n}\n\nfunction normalizarTexto(valor) {\n  return String(valor || \"\")\n    .normalize(\"NFD\")\n    .replace(\/[\\u0300-\\u036f]\/g, \"\")\n    .trim()\n    .toLowerCase();\n}\n\nfunction limpiarCSV(valor) {\n  return String(valor || \"\")\n    .replace(\/\"\/g, '\"\"')\n    .replace(\/\\n\/g, \" \")\n    .replace(\/\\r\/g, \" \");\n}\n\nfunction obtenerInicial(nombre) {\n  const limpio = String(nombre || \"\").trim();\n\n  if (!limpio) {\n    return \"\";\n  }\n\n  return limpio\n    .split(\/\\s+\/)\n    .slice(0, 2)\n    .map(parte => parte.charAt(0).toUpperCase())\n    .join(\"\");\n}\n\nfunction nombreAcompanante(inv) {\n  if (!inv || !inv.acompanante) {\n    return \"\";\n  }\n\n  return inv.acompananteNombre && inv.acompananteNombre.trim()\n    ? inv.acompananteNombre.trim()\n    : \"Acompa\u00f1ante de \" + (inv.nombre || \"\");\n}\n\nfunction crearAsientos(capacidad, actuales) {\n  const total = Math.max(1, Math.min(parseInt(capacidad, 10) || 1, 24));\n  const asientos = Array.isArray(actuales) ? actuales.slice(0, total) : [];\n\n  while (asientos.length < total) {\n    asientos.push(\"\");\n  }\n\n  return asientos;\n}\n\nfunction normalizarMesa(mesa, index) {\n  const capacidad = Math.max(1, Math.min(parseInt(mesa.capacidad, 10) || 8, 24));\n\n  return {\n    id: mesa.id || (\"mesa-\" + Date.now() + \"-\" + index),\n    nombre: mesa.nombre || (\"Mesa \" + (index + 1)),\n    tipo: mesa.tipo === \"rectangular\" ? \"rectangular\" : \"circular\",\n    capacidad: capacidad,\n    asientos: crearAsientos(capacidad, mesa.asientos)\n  };\n}\n\nfunction normalizarMesas() {\n  mesas = mesas.map(function (mesa, index) {\n    return normalizarMesa(mesa, index);\n  });\n}\n\nfunction obtenerOpcionesPersonas() {\n  const personas = [];\n\n  invitados.forEach(function (inv) {\n    const nombre = inv.nombre || \"\";\n\n    if (!nombre) {\n      return;\n    }\n\n    personas.push(nombre);\n\n    if (inv.acompanante) {\n      personas.push(nombreAcompanante(inv));\n    }\n  });\n\n  return personas;\n}\n\nfunction obtenerPersonasSentadas() {\n  const sentadas = [];\n\n  mesas.forEach(function (mesa) {\n    const asientos = Array.isArray(mesa.asientos) ? mesa.asientos : [];\n\n    asientos.forEach(function (nombre) {\n      if (nombre) {\n        sentadas.push(nombre);\n      }\n    });\n  });\n\n  return sentadas;\n}\n\nfunction obtenerPersonasSinSentar() {\n  const sentadas = new Set(obtenerPersonasSentadas().map(normalizarTexto));\n\n  return obtenerOpcionesPersonas()\n    .filter(function (nombre) {\n      return !sentadas.has(normalizarTexto(nombre));\n    })\n    .sort(function (a, b) {\n      return a.localeCompare(b, \"es\");\n    });\n}\n\nasync function cargarDatos() {\n  try {\n    const resMesas = await fetch(MESAS_JSON_URL + '?v=' + Date.now());\n    mesas = await resMesas.json();\n\n    if (!Array.isArray(mesas)) {\n      mesas = [];\n    }\n  } catch (e) {\n    console.error(\"Error cargando mesas:\", e);\n    mesas = [];\n  }\n\n  await cargarInvitados();\n  normalizarMesas();\n  renderMesas();\n}\n\nasync function cargarInvitados() {\n  try {\n    const resInvitados = await fetch(INVITADOS_JSON_URL + '?v=' + Date.now());\n    invitados = await resInvitados.json();\n\n    if (!Array.isArray(invitados)) {\n      invitados = [];\n    }\n  } catch (e) {\n    console.warn(\"No se pudo cargar la lista de invitados:\", e);\n    invitados = [];\n  }\n}\n\nasync function actualizarInvitados() {\n  await cargarInvitados();\n  renderMesas();\n  alert(\"\u2705 Invitados actualizados\");\n}\n\nfunction actualizarBusquedaMesas(valor) {\n  busquedaMesas = valor;\n  renderMesas();\n}\n\nfunction filtrarMesas(tipo) {\n  filtroMesas = tipo;\n\n  document.querySelectorAll(\".resumen-card\").forEach(function (card) {\n    card.classList.remove(\"active\");\n  });\n\n  let selector = \".resumen-card.mesas\";\n\n  if (tipo === \"conPlazas\") {\n    selector = \".resumen-card.plazas\";\n  } else if (tipo === \"conSentados\") {\n    selector = \".resumen-card.sentados\";\n  } else if (tipo === \"conLibres\") {\n    selector = \".resumen-card.libres\";\n  }\n\n  const card = document.querySelector(selector);\n\n  if (card) {\n    card.classList.add(\"active\");\n  }\n\n  renderMesas();\n}\n\nfunction pasaFiltroMesa(mesa) {\n  const ocupados = Array.isArray(mesa.asientos) ? mesa.asientos.filter(Boolean).length : 0;\n  const libres = (Number(mesa.capacidad) || 0) - ocupados;\n\n  if (filtroMesas === \"conPlazas\") {\n    return (Number(mesa.capacidad) || 0) > 0;\n  }\n\n  if (filtroMesas === \"conSentados\") {\n    return ocupados > 0;\n  }\n\n  if (filtroMesas === \"conLibres\") {\n    return libres > 0;\n  }\n\n  return true;\n}\n\nfunction toggleFormularioMesa() {\n  const form = document.getElementById(\"formNuevaMesa\");\n\n  if (form) {\n    form.classList.toggle(\"active\");\n  }\n}\n\nfunction toggleSinSentar() {\n  const panel = document.getElementById(\"sinSentarPanel\");\n\n  if (!panel) {\n    return;\n  }\n\n  panel.classList.toggle(\"active\");\n  renderSinSentar();\n}\n\nfunction renderSinSentar() {\n  const contenedor = document.getElementById(\"listaSinSentar\");\n  const balance = document.getElementById(\"sinSentarBalance\");\n  const boton = document.getElementById(\"btnSinSentar\");\n\n  if (!contenedor || !balance || !boton) {\n    return;\n  }\n\n  const sinSentar = obtenerPersonasSinSentar();\n  const totalPlazas = mesas.reduce(function (acc, mesa) {\n    return acc + (Number(mesa.capacidad) || 0);\n  }, 0);\n  const totalSentados = mesas.reduce(function (acc, mesa) {\n    return acc + (Array.isArray(mesa.asientos) ? mesa.asientos.filter(Boolean).length : 0);\n  }, 0);\n  const libres = totalPlazas - totalSentados;\n\n  boton.textContent = \"\ud83d\udc65 Sin sentar (\" + sinSentar.length + \")\";\n  contenedor.innerHTML = \"\";\n\n  balance.classList.remove(\"ok\", \"warn\");\n\n  if (sinSentar.length === 0) {\n    balance.textContent = \"Todos los invitados y acompa\u00f1antes est\u00e1n sentados.\";\n    balance.classList.add(\"ok\");\n    contenedor.innerHTML = '<div class=\"sin-sentar-empty\">Todo el mundo tiene sitio asignado.<\/div>';\n    return;\n  }\n\n  if (libres < sinSentar.length) {\n    balance.textContent = \"Faltan \" + (sinSentar.length - libres) + \" plazas para sentar a todos.\";\n    balance.classList.add(\"warn\");\n  } else {\n    balance.textContent = libres + \" plazas libres para \" + sinSentar.length + \" personas sin sentar.\";\n    balance.classList.add(\"ok\");\n  }\n\n  sinSentar.forEach(function (nombre) {\n    const btn = document.createElement(\"button\");\n    btn.type = \"button\";\n    btn.className = \"sin-sentar-pill\";\n    btn.textContent = nombre;\n    btn.onclick = function () {\n      copiarSinSentar(nombre);\n    };\n    contenedor.appendChild(btn);\n  });\n}\n\nfunction copiarSinSentar(nombre) {\n  if (navigator.clipboard && navigator.clipboard.writeText) {\n    navigator.clipboard.writeText(nombre).then(function () {\n      if (typeof window.mostrarEstadoGuardado === \"function\") {\n        window.mostrarEstadoGuardado(\"saved\", \"Nombre copiado\");\n      } else {\n        alert(\"Nombre copiado: \" + nombre);\n      }\n    }).catch(function () {\n      prompt(\"Copia este nombre:\", nombre);\n    });\n  } else {\n    prompt(\"Copia este nombre:\", nombre);\n  }\n}\n\nfunction renderMesas() {\n  const contenedor = document.getElementById(\"listaMesas\");\n\n  if (!contenedor) {\n    return;\n  }\n\n  contenedor.innerHTML = \"\";\n\n  normalizarMesas();\n  actualizarResumen();\n  renderSinSentar();\n\n  const termino = normalizarTexto(busquedaMesas);\n\n  const lista = mesas\n    .map(function (mesa, index) {\n      return Object.assign({}, mesa, { index: index });\n    })\n    .filter(function (mesa) {\n      if (!pasaFiltroMesa(mesa)) {\n        return false;\n      }\n\n      if (!termino) {\n        return true;\n      }\n\n      return normalizarTexto(mesa.nombre).includes(termino) ||\n        mesa.asientos.some(function (nombre) {\n          return normalizarTexto(nombre).includes(termino);\n        });\n    });\n\n  if (!lista.length) {\n    contenedor.innerHTML = '<div class=\"empty-state\">No hay mesas que coincidan con este filtro.<\/div>';\n    return;\n  }\n\n  lista.forEach(function (mesa) {\n    const i = mesa.index;\n    const card = document.createElement(\"div\");\n    const ocupados = mesa.asientos.filter(Boolean).length;\n    const libres = mesa.capacidad - ocupados;\n    const estaAbierta = mesasAbiertas.has(mesa.id);\n    const completa = ocupados >= mesa.capacidad;\n\n    card.className = \"mesa-card \" + (completa ? \"completa\" : \"\");\n\n    card.innerHTML = `\n      <div class=\"mesa-header\" onclick=\"toggleMesa('${escaparAtributoJS(mesa.id)}')\">\n        <div>\n          <h3>${escaparHTML(mesa.nombre)}<\/h3>\n          <div class=\"mesa-meta\">${mesa.tipo === \"circular\" ? \"Circular\" : \"Rectangular\"} \u00b7 ${ocupados}\/${mesa.capacidad} sentados<\/div>\n          <div class=\"mesa-badges\">\n            <span class=\"mesa-badge ${completa ? \"ok\" : \"warn\"}\">${completa ? \"Completa\" : libres + \" libres\"}<\/span>\n            <span class=\"mesa-badge\">${mesa.capacidad} plazas<\/span>\n          <\/div>\n        <\/div>\n\n        <div class=\"mesa-header-actions\">\n          <span class=\"toggle-mesa ${estaAbierta ? \"open\" : \"\"}\">\u25be<\/span>\n        <\/div>\n      <\/div>\n\n      <div class=\"mesa-content ${estaAbierta ? \"active\" : \"\"}\">\n        <div class=\"mesa-controls\">\n          <input type=\"text\" value=\"${escaparHTML(mesa.nombre)}\" onchange=\"actualizarMesa(${i}, 'nombre', this.value)\">\n\n          <select onchange=\"actualizarMesa(${i}, 'tipo', this.value)\">\n            <option value=\"circular\" ${mesa.tipo === \"circular\" ? \"selected\" : \"\"}>Circular<\/option>\n            <option value=\"rectangular\" ${mesa.tipo === \"rectangular\" ? \"selected\" : \"\"}>Rectangular<\/option>\n          <\/select>\n\n          <input type=\"number\" value=\"${mesa.capacidad}\" min=\"1\" max=\"24\" onchange=\"actualizarMesa(${i}, 'capacidad', this.value)\">\n        <\/div>\n\n        <div class=\"mesa-visual-wrap\">\n          <div class=\"mesa-visual ${mesa.tipo}\">\n            <div class=\"mesa-table ${mesa.tipo}\">${escaparHTML(mesa.nombre)}<\/div>\n            ${generarPuntosMesa(mesa)}\n          <\/div>\n        <\/div>\n\n        <div class=\"asientos-lista\">\n          ${generarEditorAsientos(mesa, i)}\n        <\/div>\n\n        <div class=\"mesa-actions\">\n          <button class=\"btn-small\" onclick=\"cerrarMesa('${escaparAtributoJS(mesa.id)}')\">Cerrar<\/button>\n          <button class=\"btn-danger\" onclick=\"eliminarMesa(${i})\">Eliminar<\/button>\n        <\/div>\n      <\/div>\n    `;\n\n    contenedor.appendChild(card);\n  });\n}\n\nfunction toggleMesa(id) {\n  if (mesasAbiertas.has(id)) {\n    mesasAbiertas.delete(id);\n  } else {\n    mesasAbiertas.add(id);\n  }\n\n  renderMesas();\n}\n\nfunction cerrarMesa(id) {\n  mesasAbiertas.delete(id);\n  renderMesas();\n}\n\nfunction generarPuntosMesa(mesa) {\n  if (mesa.tipo === \"rectangular\") {\n    return generarPuntosRectangulares(mesa);\n  }\n\n  return generarPuntosCirculares(mesa);\n}\n\nfunction generarPuntosCirculares(mesa) {\n  return mesa.asientos.map(function (nombre, index) {\n    const angle = (360 \/ mesa.capacidad) * index - 90;\n    const ocupado = nombre ? \"ocupado\" : \"\";\n    const etiqueta = nombre ? nombre : \"Libre\";\n    const textoPunto = nombre ? obtenerInicial(nombre) : \"\";\n\n    return `\n      <div class=\"seat-dot ${ocupado}\" style=\"--angle:${angle}deg;\" title=\"${escaparHTML(etiqueta)}\">\n        ${escaparHTML(textoPunto)}\n      <\/div>\n    `;\n  }).join(\"\");\n}\n\nfunction generarPuntosRectangulares(mesa) {\n  const total = mesa.capacidad;\n  const arriba = Math.ceil(total \/ 2);\n\n  return mesa.asientos.map(function (nombre, index) {\n    const ocupado = nombre ? \"ocupado\" : \"\";\n    const etiqueta = nombre ? nombre : \"Libre\";\n    const textoPunto = nombre ? obtenerInicial(nombre) : \"\";\n    const esArriba = index < arriba;\n    const posicionEnFila = esArriba ? index : index - arriba;\n    const totalFila = esArriba ? arriba : total - arriba;\n    const x = totalFila <= 1 ? 50 : 12 + (76 \/ (totalFila - 1)) * posicionEnFila;\n    const y = esArriba ? 28 : 72;\n\n    return `\n      <div class=\"seat-dot ${ocupado}\" style=\"--x:${x}%;--y:${y}%;\" title=\"${escaparHTML(etiqueta)}\">\n        ${escaparHTML(textoPunto)}\n      <\/div>\n    `;\n  }).join(\"\");\n}\n\nfunction generarEditorAsientos(mesa, mesaIndex) {\n  return mesa.asientos.map(function (nombre, asientoIndex) {\n    const pickerId = \"picker-\" + mesaIndex + \"-\" + asientoIndex;\n    const opciones = generarOpcionesPicker(nombre, mesaIndex, asientoIndex, \"\");\n\n    return `\n      <div class=\"asiento-row\">\n        <div class=\"asiento-num\">S${asientoIndex + 1}<\/div>\n\n        <div class=\"asiento-picker\" id=\"${pickerId}\">\n          <input\n            type=\"text\"\n            class=\"asiento-search\"\n            value=\"${escaparHTML(nombre)}\"\n            placeholder=\"Buscar invitado...\"\n            onfocus=\"abrirPicker('${pickerId}', ${mesaIndex}, ${asientoIndex}, this.value)\"\n            onclick=\"abrirPicker('${pickerId}', ${mesaIndex}, ${asientoIndex}, this.value)\"\n            oninput=\"filtrarPicker('${pickerId}', ${mesaIndex}, ${asientoIndex}, this.value)\"\n            onblur=\"cerrarPickerConRetraso('${pickerId}', ${mesaIndex}, ${asientoIndex})\"\n          >\n\n          <div class=\"asiento-options\">\n            ${opciones}\n          <\/div>\n        <\/div>\n      <\/div>\n    `;\n  }).join(\"\");\n}\n\nfunction obtenerPersonasDisponibles(actual) {\n  const actualNormalizado = normalizarTexto(actual);\n  const nombresUsados = new Set();\n\n  mesas.forEach(function (mesa) {\n    mesa.asientos.forEach(function (nombre) {\n      const nombreNormalizado = normalizarTexto(nombre);\n\n      if (nombreNormalizado && nombreNormalizado !== actualNormalizado) {\n        nombresUsados.add(nombreNormalizado);\n      }\n    });\n  });\n\n  return obtenerOpcionesPersonas()\n    .filter(function (nombre) {\n      const nombreNormalizado = normalizarTexto(nombre);\n      return !nombresUsados.has(nombreNormalizado) || nombreNormalizado === actualNormalizado;\n    })\n    .sort(function (a, b) {\n      return a.localeCompare(b, \"es\");\n    });\n}\n\nfunction generarOpcionesPicker(actual, mesaIndex, asientoIndex, busqueda) {\n  const termino = normalizarTexto(busqueda);\n  const actualNormalizado = normalizarTexto(actual);\n\n  const opciones = obtenerPersonasDisponibles(actual)\n    .filter(function (nombre) {\n      return normalizarTexto(nombre).includes(termino);\n    });\n\n  let html = `\n    <button type=\"button\" class=\"asiento-option clear\" onmousedown=\"event.preventDefault(); seleccionarAsiento(${mesaIndex}, ${asientoIndex}, '')\">\n      Dejar libre\n    <\/button>\n  `;\n\n  opciones.forEach(function (nombre) {\n    const seleccionado = normalizarTexto(nombre) === actualNormalizado ? \" \u00b7 sentado aqu\u00ed\" : \"\";\n\n    html += `\n      <button type=\"button\" class=\"asiento-option\" onmousedown=\"event.preventDefault(); seleccionarAsiento(${mesaIndex}, ${asientoIndex}, '${escaparAtributoJS(nombre)}')\">\n        ${escaparHTML(nombre)}${seleccionado}\n      <\/button>\n    `;\n  });\n\n  if (busqueda.trim() && !opciones.some(function (nombre) {\n    return normalizarTexto(nombre) === normalizarTexto(busqueda);\n  })) {\n    html += `\n      <button type=\"button\" class=\"asiento-option\" onmousedown=\"event.preventDefault(); seleccionarAsiento(${mesaIndex}, ${asientoIndex}, '${escaparAtributoJS(busqueda)}')\">\n        Usar \"${escaparHTML(busqueda)}\"\n      <\/button>\n    `;\n  }\n\n  if (!opciones.length && !busqueda.trim()) {\n    html += `\n      <button type=\"button\" class=\"asiento-option\" disabled>\n        No hay invitados disponibles\n      <\/button>\n    `;\n  }\n\n  return html;\n}\n\nfunction abrirPicker(pickerId, mesaIndex, asientoIndex, busqueda) {\n  document.querySelectorAll(\".asiento-picker.open\").forEach(function (picker) {\n    if (picker.id !== pickerId) {\n      picker.classList.remove(\"open\");\n    }\n  });\n\n  const picker = document.getElementById(pickerId);\n\n  if (!picker) {\n    return;\n  }\n\n  const options = picker.querySelector(\".asiento-options\");\n  const actual = mesas[mesaIndex] && mesas[mesaIndex].asientos\n    ? mesas[mesaIndex].asientos[asientoIndex] || \"\"\n    : \"\";\n\n  options.innerHTML = generarOpcionesPicker(actual, mesaIndex, asientoIndex, busqueda);\n  picker.classList.add(\"open\");\n}\n\nfunction filtrarPicker(pickerId, mesaIndex, asientoIndex, busqueda) {\n  const picker = document.getElementById(pickerId);\n\n  if (!picker) {\n    return;\n  }\n\n  const options = picker.querySelector(\".asiento-options\");\n  const actual = mesas[mesaIndex] && mesas[mesaIndex].asientos\n    ? mesas[mesaIndex].asientos[asientoIndex] || \"\"\n    : \"\";\n\n  options.innerHTML = generarOpcionesPicker(actual, mesaIndex, asientoIndex, busqueda);\n  picker.classList.add(\"open\");\n}\n\nfunction cerrarPickerConRetraso(pickerId, mesaIndex, asientoIndex) {\n  setTimeout(function () {\n    const picker = document.getElementById(pickerId);\n\n    if (!picker) {\n      return;\n    }\n\n    picker.classList.remove(\"open\");\n\n    const input = picker.querySelector(\".asiento-search\");\n    const actual = mesas[mesaIndex] && mesas[mesaIndex].asientos\n      ? mesas[mesaIndex].asientos[asientoIndex] || \"\"\n      : \"\";\n\n    if (input) {\n      input.value = actual;\n    }\n  }, 180);\n}\n\nasync function seleccionarAsiento(mesaIndex, asientoIndex, valor) {\n  await cambiarAsiento(mesaIndex, asientoIndex, valor);\n}\n\nasync function agregarMesa() {\n  const nombreInput = document.getElementById(\"nuevaMesaNombre\");\n  const tipoInput = document.getElementById(\"nuevaMesaTipo\");\n  const capacidadInput = document.getElementById(\"nuevaMesaCapacidad\");\n\n  if (!nombreInput || !tipoInput || !capacidadInput) {\n    alert(\"No encuentro el formulario de a\u00f1adir mesa.\");\n    return;\n  }\n\n  const nombre = nombreInput.value.trim() || \"Mesa \" + (mesas.length + 1);\n  const tipo = tipoInput.value;\n  const capacidad = Math.max(1, Math.min(parseInt(capacidadInput.value, 10) || 8, 24));\n  const id = \"mesa-\" + Date.now();\n\n  mesas.push({\n    id: id,\n    nombre: nombre,\n    tipo: tipo,\n    capacidad: capacidad,\n    asientos: crearAsientos(capacidad, [])\n  });\n\n  mesasAbiertas.add(id);\n\n  nombreInput.value = \"\";\n  tipoInput.value = \"circular\";\n  capacidadInput.value = \"8\";\n\n  const form = document.getElementById(\"formNuevaMesa\");\n  if (form) {\n    form.classList.remove(\"active\");\n  }\n\n  renderMesas();\n  await guardarCambios(true);\n}\n\nasync function actualizarMesa(i, campo, valor) {\n  if (!mesas[i]) {\n    return;\n  }\n\n  if (campo === \"capacidad\") {\n    const capacidad = Math.max(1, Math.min(parseInt(valor, 10) || 1, 24));\n    mesas[i].capacidad = capacidad;\n    mesas[i].asientos = crearAsientos(capacidad, mesas[i].asientos);\n  } else if (campo === \"tipo\") {\n    mesas[i].tipo = valor === \"rectangular\" ? \"rectangular\" : \"circular\";\n  } else {\n    mesas[i][campo] = valor;\n  }\n\n  mesasAbiertas.add(mesas[i].id);\n  renderMesas();\n  await guardarCambios(true);\n}\n\nasync function cambiarAsiento(mesaIndex, asientoIndex, valor) {\n  if (!mesas[mesaIndex]) {\n    return;\n  }\n\n  const nombre = String(valor || \"\").trim();\n\n  if (mesas[mesaIndex].asientos[asientoIndex] === nombre) {\n    return;\n  }\n\n  mesas[mesaIndex].asientos[asientoIndex] = nombre;\n  mesasAbiertas.add(mesas[mesaIndex].id);\n\n  renderMesas();\n  await guardarCambios(true);\n}\n\nasync function eliminarMesa(i) {\n  if (!mesas[i]) {\n    return;\n  }\n\n  if (confirm(`\u00bfEliminar \"${mesas[i].nombre}\"?`)) {\n    mesasAbiertas.delete(mesas[i].id);\n    mesas.splice(i, 1);\n    renderMesas();\n    await guardarCambios(true);\n  }\n}\n\nfunction actualizarResumen() {\n  const totalMesas = mesas.length;\n  const totalPlazas = mesas.reduce(function (acc, mesa) {\n    return acc + (Number(mesa.capacidad) || 0);\n  }, 0);\n  const totalSentados = mesas.reduce(function (acc, mesa) {\n    return acc + (Array.isArray(mesa.asientos) ? mesa.asientos.filter(Boolean).length : 0);\n  }, 0);\n  const totalLibres = totalPlazas - totalSentados;\n  const porcentaje = totalPlazas > 0 ? Math.min((totalSentados \/ totalPlazas) * 100, 100) : 0;\n\n  const totalMesasEl = document.getElementById(\"totalMesas\");\n  const totalPlazasEl = document.getElementById(\"totalPlazas\");\n  const totalSentadosEl = document.getElementById(\"totalSentados\");\n  const totalLibresEl = document.getElementById(\"totalLibres\");\n  const progressEl = document.getElementById(\"seatingProgress\");\n  const progressTextEl = document.getElementById(\"seatingProgressText\");\n\n  if (totalMesasEl) totalMesasEl.textContent = totalMesas;\n  if (totalPlazasEl) totalPlazasEl.textContent = totalPlazas;\n  if (totalSentadosEl) totalSentadosEl.textContent = totalSentados;\n  if (totalLibresEl) totalLibresEl.textContent = totalLibres;\n  if (progressEl) progressEl.style.width = porcentaje + \"%\";\n  if (progressTextEl) progressTextEl.textContent = Math.round(porcentaje) + \"%\";\n}\n\nasync function guardarCambios(silencioso = false) {\n  try {\n    normalizarMesas();\n\n    const res = await fetch(GUARDAR_MESAS_URL, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application\/json' },\n      body: JSON.stringify(mesas)\n    });\n\n    if (!res.ok) {\n      throw new Error(\"Error HTTP \" + res.status);\n    }\n\n    if (!silencioso) {\n      alert(\"\u2705 Seating plan guardado correctamente\");\n    }\n  } catch (e) {\n    console.error(\"\u274c Error al guardar seating plan:\", e);\n\n    if (!silencioso) {\n      alert(\"\u274c Error al guardar el seating plan\");\n    }\n  }\n}\n\nfunction exportarMesas() {\n  const filas = [];\n\n  filas.push([\n    \"Mesa\",\n    \"Forma\",\n    \"Capacidad\",\n    \"Asiento\",\n    \"Invitado\"\n  ]);\n\n  mesas.forEach(function (mesa) {\n    mesa.asientos.forEach(function (nombre, index) {\n      filas.push([\n        mesa.nombre || \"\",\n        mesa.tipo || \"\",\n        mesa.capacidad || \"\",\n        \"S\" + (index + 1),\n        nombre || \"\"\n      ]);\n    });\n  });\n\n  const csv = filas\n    .map(function (fila) {\n      return fila.map(function (celda) {\n        return `\"${limpiarCSV(celda)}\"`;\n      }).join(\";\");\n    })\n    .join(\"\\n\");\n\n  const blob = new Blob([\"\\uFEFF\" + csv], {\n    type: \"text\/csv;charset=utf-8;\"\n  });\n\n  const url = URL.createObjectURL(blob);\n  const enlace = document.createElement(\"a\");\n\n  enlace.href = url;\n  enlace.download = \"seating-plan-boda-fran-y-jose.csv\";\n  enlace.click();\n\n  URL.revokeObjectURL(url);\n}\n\ncargarDatos();\n\nwindow.guardarCambios = guardarCambios;\n<\/script>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>\ud83e\ude91 Seating Plan Coloca invitados, acompa\u00f1a mesas y revisa plazas libres de un vistazo. Mesas 0 Plazas 0 Sentados 0 Libres 0 Ocupaci\u00f3n total 0% \u2795 A\u00f1adir mesa \ud83d\udc65 Sin sentar \ud83d\udd04 Invitados \ud83d\udce4 Exportar Nombre Forma CircularRectangular Personas Guardar mesa Cerrar \ud83d\udc65 Invitados sin sentar Toca un nombre para copiarlo y pegarlo r\u00e1pido en [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"footnotes":""},"class_list":["post-836","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>mesas -<\/title>\n<meta name=\"robots\" content=\"noindex, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<meta property=\"og:locale\" content=\"es_ES\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"mesas -\" \/>\n<meta property=\"og:description\" content=\"\ud83e\ude91 Seating Plan Coloca invitados, acompa\u00f1a mesas y revisa plazas libres de un vistazo. Mesas 0 Plazas 0 Sentados 0 Libres 0 Ocupaci\u00f3n total 0% \u2795 A\u00f1adir mesa \ud83d\udc65 Sin sentar \ud83d\udd04 Invitados \ud83d\udce4 Exportar Nombre Forma CircularRectangular Personas Guardar mesa Cerrar \ud83d\udc65 Invitados sin sentar Toca un nombre para copiarlo y pegarlo r\u00e1pido en [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/app.bodafranyjose.com\/index.php\/mesas\/\" \/>\n<meta property=\"article:modified_time\" content=\"2026-04-13T17:57:14+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data1\" content=\"17 minutos\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/app.bodafranyjose.com\\\/index.php\\\/mesas\\\/\",\"url\":\"https:\\\/\\\/app.bodafranyjose.com\\\/index.php\\\/mesas\\\/\",\"name\":\"mesas -\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/app.bodafranyjose.com\\\/#website\"},\"datePublished\":\"2026-04-12T18:55:40+00:00\",\"dateModified\":\"2026-04-13T17:57:14+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/app.bodafranyjose.com\\\/index.php\\\/mesas\\\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/app.bodafranyjose.com\\\/index.php\\\/mesas\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/app.bodafranyjose.com\\\/index.php\\\/mesas\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Portada\",\"item\":\"https:\\\/\\\/app.bodafranyjose.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"mesas\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/app.bodafranyjose.com\\\/#website\",\"url\":\"https:\\\/\\\/app.bodafranyjose.com\\\/\",\"name\":\"\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/app.bodafranyjose.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"mesas -","robots":{"index":"noindex","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"og_locale":"es_ES","og_type":"article","og_title":"mesas -","og_description":"\ud83e\ude91 Seating Plan Coloca invitados, acompa\u00f1a mesas y revisa plazas libres de un vistazo. Mesas 0 Plazas 0 Sentados 0 Libres 0 Ocupaci\u00f3n total 0% \u2795 A\u00f1adir mesa \ud83d\udc65 Sin sentar \ud83d\udd04 Invitados \ud83d\udce4 Exportar Nombre Forma CircularRectangular Personas Guardar mesa Cerrar \ud83d\udc65 Invitados sin sentar Toca un nombre para copiarlo y pegarlo r\u00e1pido en [&hellip;]","og_url":"https:\/\/app.bodafranyjose.com\/index.php\/mesas\/","article_modified_time":"2026-04-13T17:57:14+00:00","twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"17 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/app.bodafranyjose.com\/index.php\/mesas\/","url":"https:\/\/app.bodafranyjose.com\/index.php\/mesas\/","name":"mesas -","isPartOf":{"@id":"https:\/\/app.bodafranyjose.com\/#website"},"datePublished":"2026-04-12T18:55:40+00:00","dateModified":"2026-04-13T17:57:14+00:00","breadcrumb":{"@id":"https:\/\/app.bodafranyjose.com\/index.php\/mesas\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/app.bodafranyjose.com\/index.php\/mesas\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/app.bodafranyjose.com\/index.php\/mesas\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Portada","item":"https:\/\/app.bodafranyjose.com\/"},{"@type":"ListItem","position":2,"name":"mesas"}]},{"@type":"WebSite","@id":"https:\/\/app.bodafranyjose.com\/#website","url":"https:\/\/app.bodafranyjose.com\/","name":"","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/app.bodafranyjose.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"}]}},"_links":{"self":[{"href":"https:\/\/app.bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/836","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/app.bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/app.bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/app.bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/app.bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/comments?post=836"}],"version-history":[{"count":63,"href":"https:\/\/app.bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/836\/revisions"}],"predecessor-version":[{"id":1019,"href":"https:\/\/app.bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/836\/revisions\/1019"}],"wp:attachment":[{"href":"https:\/\/app.bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/media?parent=836"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}