{"id":836,"date":"2026-04-12T18:55:40","date_gmt":"2026-04-12T18:55:40","guid":{"rendered":"https:\/\/bodafranyjose.com\/?page_id=836"},"modified":"2026-04-14T14:46:43","modified_gmt":"2026-04-14T14:46:43","slug":"mesas","status":"publish","type":"page","link":"https:\/\/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<!DOCTYPE html>\n<html lang=\"es\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>Seating Plan \u2013 Boda<\/title>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Poppins:wght@400;600;700;800&display=swap\" rel=\"stylesheet\">\n<style>\n\/* ===== PRINT ===== *\/\n@media print {\n  .seating-tools, .add-table, .sin-sentar-panel,\n  .mesa-content .mesa-controls, .mesa-content .mesa-actions,\n  .mesa-visual-wrap, .asiento-picker .asiento-options,\n  .btn-main, .btn-secondary, .btn-small, .btn-danger,\n  .toggle-mesa, .undo-bar { display: none !important; }\n  .mesa-content { display: block !important; padding: 8px 12px !important; }\n  .mesa-card { break-inside: avoid; box-shadow: none !important; border: 1px solid #ccc !important; }\n  .mesas-grid { grid-template-columns: repeat(2, 1fr) !important; }\n  body { padding: 0 !important; }\n  .seating-container { box-shadow: none !important; padding: 12px !important; }\n}\n\n\/* ===== BASE ===== *\/\n*, *::before, *::after { box-sizing: border-box; }\n\nbody {\n  margin: 0;\n  padding: 0 0 112px;\n  font-family: 'Poppins', sans-serif;\n  background: #fdf8fa;\n  color: #333;\n}\n\n\/* ===== CONTAINER ===== *\/\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}\n\n\/* ===== HEADER ===== *\/\n.seating-head { text-align: center; margin-bottom: 14px; }\n.seating-title { color: #b67b91; margin: 0 0 5px; font-size: 1.72rem; font-weight: 700; line-height: 1.15; }\n.seating-subtitle { color: #666; margin: 0; font-size: 0.9rem; font-weight: 600; line-height: 1.25; }\n\n\/* ===== DASHBOARD ===== *\/\n.resumen-dashboard {\n  display: grid;\n  grid-template-columns: repeat(4, minmax(0, 1fr));\n  gap: 9px;\n  margin-bottom: 12px;\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.resumen-card:hover { transform: translateY(-1px); box-shadow: 0 5px 14px rgba(0,0,0,0.07); }\n.resumen-card.active { background: #fff; border-color: #b67b91; box-shadow: 0 5px 16px rgba(182,123,145,0.16); }\n.resumen-card h3 { font-size: 0.76rem; color: #b67b91; margin: 0 0 4px; font-weight: 700; line-height: 1.1; }\n.resumen-card span { font-size: 1.15rem; font-weight: 800; color: #333; line-height: 1.1; }\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.resumen-card.active.mesas,\n.resumen-card.active.plazas,\n.resumen-card.active.sentados,\n.resumen-card.active.libres { background: #fff; }\n\n\/* ===== PROGRESS ===== *\/\n.seating-progress { margin: 0 0 12px; }\n.seating-progress-label { display: flex; justify-content: space-between; gap: 10px; color: #666; font-size: 0.78rem; font-weight: 700; margin-bottom: 5px; }\n.seating-progress-track { height: 7px; background: #f0eaed; border-radius: 999px; overflow: hidden; }\n.seating-progress-track span { display: block; height: 100%; width: 0%; background: linear-gradient(90deg, #b67b91, #a56b82); border-radius: 999px; transition: width 0.4s ease; }\n\n\/* ===== UNDO BAR ===== *\/\n.undo-bar {\n  display: none;\n  align-items: center;\n  justify-content: space-between;\n  gap: 10px;\n  background: #fff8e8;\n  border: 1px solid #f5dda0;\n  border-radius: 12px;\n  padding: 9px 12px;\n  margin-bottom: 10px;\n  font-size: 0.84rem;\n  font-weight: 600;\n  color: #7a5c00;\n  animation: slideDown 0.25s ease;\n}\n.undo-bar.visible { display: flex; }\n.undo-bar button {\n  background: #f5c842;\n  color: #5a4000;\n  border: none;\n  border-radius: 8px;\n  padding: 5px 12px;\n  font-family: 'Poppins', sans-serif;\n  font-size: 0.82rem;\n  font-weight: 700;\n  cursor: pointer;\n  white-space: nowrap;\n}\n.undo-bar button:hover { background: #e6b830; }\n@keyframes slideDown { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } }\n\n\/* ===== TOOLS ===== *\/\n.seating-tools { display: grid; gap: 9px; margin-bottom: 14px; }\n.seating-search-wrap { position: relative; }\n.seating-search {\n  width: 100%; min-height: 43px; padding: 10px 12px;\n  border-radius: 12px; border: 1px solid #eadde3;\n  font-size: 16px; font-weight: 400; box-sizing: border-box;\n  background: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.035);\n  font-family: 'Poppins', sans-serif;\n}\n.seating-search::placeholder { color: #888; font-weight: 300; }\n.seating-actions-primary,\n.seating-actions-secondary { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }\n.seating-actions-secondary { grid-template-columns: 1fr 1fr 1fr; }\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.mesa-nota:focus { outline: 2px solid #f2e1e8; border-color: #b67b91; }\n\n\/* ===== BUTTONS ===== *\/\n.btn-main, .btn-small, .btn-secondary, .btn-danger {\n  border-radius: 10px; cursor: pointer; font-family: 'Poppins', sans-serif;\n  font-weight: 700; transition: 0.25s ease; white-space: nowrap; min-height: 40px;\n}\n.btn-main, .btn-small { background: #b67b91; color: #fff; border: none; }\n.btn-main { padding: 10px 13px; font-size: 0.9rem; }\n.btn-small { padding: 8px 12px; font-size: 0.86rem; }\n.btn-main:hover, .btn-small:hover { background: #a56b82; }\n.btn-secondary {\n  background: #fff; color: #b67b91; border: 1px solid #e5c9d4;\n  padding: 8px 12px; font-size: 0.84rem; box-shadow: 0 2px 8px rgba(0,0,0,0.025);\n}\n.btn-secondary:hover { background: #f9f0f4; border-color: #b67b91; }\n.btn-secondary.feature { background: #faf6f8; border-color: #d8adbf; color: #a5447d; }\n.btn-danger { background: #fff; color: #a5447d; border: 1px solid #f0c8d6; padding: 8px 12px; font-size: 0.86rem; }\n.btn-danger:hover { background: #fcefee; color: #8f3568; }\n\n\/* ===== ADD TABLE FORM ===== *\/\n.add-table {\n  display: none;\n  grid-template-columns: 1fr 0.6fr 0.5fr 0.6fr 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.add-table.active { display: grid; }\n.add-field label { display: block; color: #b67b91; font-weight: 700; font-size: 0.82rem; margin-bottom: 5px; }\n.add-field input,\n.add-field select {\n  width: 100%; min-height: 41px; padding: 9px 10px;\n  border: 1px solid #e2d4da; border-radius: 10px;\n  font-size: 16px; box-sizing: border-box; background: #fff;\n  font-family: 'Poppins', sans-serif;\n}\n\n\/* ===== GRUPO COLORES ===== *\/\n.grupo-color-wrap { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }\n.grupo-color-btn {\n  width: 22px; height: 22px; border-radius: 50%; border: 2px solid transparent;\n  cursor: pointer; transition: transform 0.15s ease, border-color 0.15s ease;\n  flex-shrink: 0;\n}\n.grupo-color-btn:hover { transform: scale(1.2); }\n.grupo-color-btn.sel { border-color: #333; transform: scale(1.15); }\n\n\/* ===== SIN SENTAR ===== *\/\n.sin-sentar-panel {\n  display: none; margin-bottom: 14px; background: #faf6f8;\n  border: 1px solid #f0dce5; border-radius: 16px; padding: 12px;\n  box-shadow: 0 3px 12px rgba(0,0,0,0.035);\n}\n.sin-sentar-panel.active { display: block; }\n.sin-sentar-head { display: flex; justify-content: space-between; gap: 10px; align-items: flex-start; margin-bottom: 10px; }\n.sin-sentar-head h3 { margin: 0; color: #b67b91; font-size: 0.98rem; font-weight: 800; line-height: 1.2; }\n.sin-sentar-head p { margin: 3px 0 0; color: #666; font-size: 0.8rem; line-height: 1.25; }\n.sin-sentar-balance { background: #fff; border: 1px solid #f0e6ea; color: #444; border-radius: 12px; padding: 9px 10px; margin-bottom: 10px; font-size: 0.84rem; font-weight: 700; line-height: 1.25; }\n.sin-sentar-balance.ok { color: #3c8d56; }\n.sin-sentar-balance.warn { color: #a5447d; }\n.sin-sentar-grid { display: flex; flex-wrap: wrap; gap: 7px; }\n.sin-sentar-pill {\n  background: #fff; color: #444; border: 1px solid #f0e6ea; border-radius: 999px;\n  padding: 8px 10px; cursor: pointer; font-family: 'Poppins', sans-serif;\n  font-size: 0.82rem; font-weight: 700; transition: 0.2s;\n}\n.sin-sentar-pill:hover { background: #f2e1e8; color: #b67b91; }\n.sin-sentar-empty { background: #fff; color: #3c8d56; border: 1px solid #e3f7e6; border-radius: 12px; padding: 11px; font-size: 0.86rem; font-weight: 700; text-align: center; width: 100%; }\n\n\/* ===== MESAS GRID ===== *\/\n.mesas-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }\n.mesa-card { background: #fff; border: 1px solid #f0e6ea; border-radius: 16px; overflow: visible; box-shadow: 0 3px 12px rgba(0,0,0,0.045); }\n.mesa-card.completa { border-color: #d9efd9; }\n\n\/* Color de grupo en borde izquierdo *\/\n.mesa-card[data-color] { border-left: 4px solid var(--gc, #f0e6ea); }\n\n.mesa-header {\n  display: grid; grid-template-columns: 1fr 34px; gap: 10px; align-items: center;\n  background: #faf6f8; padding: 11px 12px; cursor: pointer;\n  border-radius: 16px 16px 0 0; min-height: 56px;\n}\n.mesa-header:hover { background: #f8f0f4; }\n.mesa-header h3 { margin: 0; color: #b67b91; font-size: 1rem; line-height: 1.12; font-weight: 800; }\n.mesa-meta { color: #666; font-size: 0.78rem; margin-top: 3px; font-weight: 600; line-height: 1.18; }\n.mesa-badges { display: flex; gap: 5px; flex-wrap: wrap; margin-top: 6px; }\n.mesa-badge { display: inline-flex; align-items: center; background: #fff; color: #b67b91; border: 1px solid #f0e6ea; border-radius: 999px; padding: 2px 7px; font-size: 0.68rem; font-weight: 800; line-height: 1.2; }\n.mesa-badge.ok { color: #3c8d56; }\n.mesa-badge.warn { color: #a5447d; }\n.mesa-badge.grupo { font-size: 0.66rem; }\n.mesa-header-actions { display: flex; align-items: center; justify-content: flex-end; }\n.toggle-mesa {\n  color: #b67b91; font-size: 1.05rem; transition: transform 0.25s ease, background 0.2s ease;\n  width: 32px; height: 32px; border-radius: 10px; background: #fff;\n  border: 1px solid #f0e6ea; display: inline-flex; align-items: center; justify-content: center;\n  box-shadow: 0 2px 7px rgba(0,0,0,0.035);\n}\n.toggle-mesa.open { transform: rotate(180deg); background: #f2e1e8; }\n\n.mesa-content { display: none; padding: 12px; background: #fff; border-top: 1px solid #f0e6ea; border-radius: 0 0 16px 16px; }\n.mesa-content.active { display: block; }\n\n\/* Controls row *\/\n.mesa-controls { display: grid; grid-template-columns: 1fr 0.7fr 0.7fr; gap: 9px; margin-bottom: 8px; }\n.mesa-controls input,\n.mesa-controls select {\n  width: 100%; min-height: 40px; padding: 8px 10px; border: 1px solid #e2d4da;\n  border-radius: 10px; font-size: 16px; box-sizing: border-box; background: #fff;\n  font-family: 'Poppins', sans-serif;\n}\n\n\/* Grupo row *\/\n.mesa-grupo-row { display: grid; grid-template-columns: 1fr auto; gap: 9px; align-items: center; margin-bottom: 8px; }\n.mesa-grupo-row input {\n  width: 100%; min-height: 36px; padding: 7px 10px; border: 1px solid #e2d4da;\n  border-radius: 10px; font-size: 15px; box-sizing: border-box; background: #fff;\n  font-family: 'Poppins', sans-serif;\n}\n\n\/* Nota *\/\n.mesa-nota-wrap { margin-bottom: 10px; }\n.mesa-nota-label { font-size: 0.78rem; color: #b67b91; font-weight: 700; margin-bottom: 4px; }\n.mesa-nota {\n  width: 100%; min-height: 56px; padding: 8px 10px; border: 1px solid #e2d4da;\n  border-radius: 10px; font-size: 14px; resize: vertical; background: #fff;\n  font-family: 'Poppins', sans-serif; box-sizing: border-box; line-height: 1.4;\n}\n.mesa-nota::placeholder { color: #bbb; }\n\n\/* Nota badge en header *\/\n.mesa-nota-badge { font-size: 0.72rem; color: #888; margin-top: 3px; font-style: italic; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; }\n\n\/* Visual *\/\n.mesa-visual-wrap {\n  display: flex; justify-content: center; align-items: center;\n  margin: 4px 0 12px; background: linear-gradient(180deg, #fff, #fbf7f9);\n  border: 1px solid #f0e6ea; border-radius: 16px; padding: 10px;\n  min-height: 220px; box-sizing: border-box;\n}\n.mesa-visual {\n  --seat-size: 28px; --seat-radius: 78px;\n  position: relative; width: clamp(184px, 28vw, 200px);\n  aspect-ratio: 1 \/ 1; height: auto; margin: 0 auto;\n}\n.mesa-table {\n  position: absolute; inset: 24%;\n  display: flex; align-items: center; justify-content: center;\n  background: #f8e6ec; border: 2px solid #b67b91; color: #b67b91;\n  font-weight: 800; text-align: center; padding: 10px; box-sizing: border-box;\n  font-size: 0.84rem; line-height: 1.12; word-break: break-word;\n  box-shadow: 0 5px 12px rgba(182,123,145,0.12);\n}\n.mesa-table.circular { border-radius: 50%; }\n.mesa-table.rectangular { inset: 33% 16%; border-radius: 13px; }\n\n.seat-dot {\n  position: absolute; width: var(--seat-size); height: var(--seat-size);\n  border-radius: 50%; background: #fff; border: 2px solid #d9c3cd;\n  box-shadow: 0 3px 8px rgba(0,0,0,0.08);\n  display: flex; align-items: center; justify-content: center;\n  font-size: 0.66rem; font-weight: 800; color: #b67b91; line-height: 1;\n}\n.seat-dot.ocupado { background: #b67b91; border-color: #b67b91; color: #fff; }\n.mesa-visual.circular .seat-dot {\n  left: 50%; 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.mesa-visual.rectangular .seat-dot { left: var(--x); top: var(--y); transform: translate(-50%, -50%); }\n\n\/* Asientos *\/\n.asientos-lista { display: grid; gap: 7px; }\n.asiento-row { display: grid; grid-template-columns: 42px 1fr; gap: 8px; align-items: center; }\n.asiento-num { color: #b67b91; font-weight: 800; font-size: 0.78rem; line-height: 1; text-align: right; }\n.asiento-picker { position: relative; }\n.asiento-search {\n  width: 100%; min-height: 40px; padding: 8px 9px; border: 1px solid #e2d4da;\n  border-radius: 10px; font-size: 16px; box-sizing: border-box; background: #fff;\n  font-family: 'Poppins', sans-serif;\n}\n.asiento-options {\n  display: none; position: absolute; z-index: 50; left: 0; right: 0;\n  top: calc(100% + 5px); max-height: 220px; overflow-y: auto;\n  background: #fff; border: 1px solid #f0e6ea; border-radius: 12px;\n  box-shadow: 0 9px 24px rgba(0,0,0,0.13); padding: 6px;\n}\n.asiento-picker.open .asiento-options { display: block; }\n.asiento-option {\n  width: 100%; text-align: left; background: none; border: none; color: #444;\n  padding: 9px 10px; border-radius: 9px; cursor: pointer;\n  font-family: 'Poppins', sans-serif; font-size: 0.88rem; font-weight: 600;\n}\n.asiento-option:hover { background: #faf6f8; color: #b67b91; }\n.asiento-option.clear { color: #a5447d; font-weight: 800; }\n.asiento-option:disabled { color: #999; cursor: default; }\n\n\/* Mesa actions *\/\n.mesa-actions { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 9px; margin-top: 12px; }\n\n.empty-state { text-align: center; color: #b67b91; padding: 20px 12px; background: #faf6f8; border-radius: 16px; grid-column: 1 \/ -1; font-weight: 700; }\n\n\/* ===== GRUPOS PALETA ===== *\/\n.grupos-paleta { display: flex; gap: 5px; flex-wrap: wrap; }\n.gp-btn {\n  width: 20px; height: 20px; border-radius: 50%; border: 2px solid transparent;\n  cursor: pointer; transition: transform 0.15s, border-color 0.15s; flex-shrink: 0;\n}\n.gp-btn:hover { transform: scale(1.2); }\n.gp-btn.sel { border-color: #333; }\n\n\/* ===== RESPONSIVE ===== *\/\n@media (max-width: 900px) {\n  .add-table { grid-template-columns: 1fr 1fr; }\n  .mesa-controls { grid-template-columns: 1fr 1fr; }\n  .mesas-grid { grid-template-columns: 1fr; }\n}\n\n@media (max-width: 768px) {\n  .seating-container { padding: 16px 12px 112px; border-radius: 18px; }\n  .seating-title { font-size: 1.36rem; margin-bottom: 4px; }\n  .seating-subtitle { font-size: 0.78rem; }\n  .resumen-dashboard { gap: 6px; margin-bottom: 10px; }\n  .resumen-card { padding: 7px 4px; border-radius: 11px; min-height: 48px; }\n  .resumen-card h3 { font-size: 0.59rem; margin-bottom: 3px; }\n  .resumen-card span { font-size: 0.9rem; }\n  .seating-progress { margin-bottom: 10px; }\n  .seating-progress-label { font-size: 0.7rem; margin-bottom: 4px; }\n  .seating-progress-track { height: 6px; }\n  .seating-tools { gap: 8px; margin-bottom: 12px; }\n  .seating-search { min-height: 42px; border-radius: 12px; padding: 9px 12px; }\n  .btn-main, .btn-secondary, .btn-small, .btn-danger { width: 100%; min-height: 39px; }\n  .btn-main { font-size: 0.86rem; padding: 9px 10px; }\n  .btn-secondary { font-size: 0.78rem; padding: 8px 9px; }\n  .seating-actions-secondary { grid-template-columns: 1fr 1fr 1fr; }\n  .add-table { grid-template-columns: 1fr; gap: 8px; padding: 11px; }\n  .mesa-controls { grid-template-columns: minmax(0, 1.25fr) minmax(82px, 0.75fr) minmax(68px, 0.65fr); gap: 6px; margin-bottom: 8px; }\n  .mesa-controls input, .mesa-controls select { min-height: 36px; padding: 7px 8px; border-radius: 9px; font-size: 16px; }\n  .sin-sentar-panel { padding: 11px; border-radius: 15px; margin-bottom: 12px; }\n  .sin-sentar-head .btn-secondary { width: auto; align-self: flex-start; min-height: 34px; padding: 7px 10px; }\n  .sin-sentar-head h3 { font-size: 0.92rem; }\n  .sin-sentar-head p { font-size: 0.76rem; }\n  .sin-sentar-balance { padding: 8px 9px; font-size: 0.8rem; }\n  .sin-sentar-grid { display: grid; grid-template-columns: 1fr; gap: 7px; }\n  .sin-sentar-pill { width: 100%; border-radius: 11px; text-align: left; padding: 9px 10px; font-size: 0.82rem; min-height: 39px; }\n  .mesas-grid { gap: 10px; }\n  .mesa-card { border-radius: 15px; box-shadow: 0 2px 10px rgba(0,0,0,0.04); }\n  .mesa-card:last-child { margin-bottom: 22px; }\n  .mesa-header { grid-template-columns: 1fr 32px; gap: 8px; padding: 10px 11px; min-height: 52px; border-radius: 15px 15px 0 0; }\n  .mesa-header h3 { font-size: 0.94rem; }\n  .mesa-meta { font-size: 0.72rem; margin-top: 2px; }\n  .mesa-badges { margin-top: 5px; gap: 4px; }\n  .mesa-badge { font-size: 0.62rem; padding: 2px 6px; }\n  .toggle-mesa { width: 30px; height: 30px; font-size: 0.96rem; border-radius: 9px; }\n  .mesa-content { padding: 9px; border-radius: 0 0 15px 15px; }\n  .mesa-visual-wrap { padding: 8px; margin: 2px 0 9px; border-radius: 14px; min-height: 186px; }\n  .mesa-visual { --seat-size: 24px; --seat-radius: clamp(58px, 18vw, 64px); width: clamp(156px, 48vw, 172px); }\n  .mesa-table { inset: 25%; font-size: 0.72rem; padding: 8px; }\n  .mesa-table.rectangular { inset: 34% 17%; }\n  .seat-dot { font-size: 0.58rem; border-width: 2px; box-shadow: 0 2px 6px rgba(0,0,0,0.075); }\n  .asientos-lista { gap: 6px; }\n  .asiento-row { grid-template-columns: 30px 1fr; gap: 6px; }\n  .asiento-num { font-size: 0.72rem; }\n  .asiento-search { min-height: 38px; padding: 7px 9px; border-radius: 9px; }\n  .mesa-actions { grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }\n  .mesa-grupo-row { grid-template-columns: 1fr; }\n  .undo-bar { font-size: 0.8rem; }\n}\n\n@media (max-width: 420px) {\n  .mesa-controls { grid-template-columns: 1fr 86px 72px; }\n  .mesa-visual-wrap { min-height: 178px; }\n  .mesa-visual { --seat-radius: 58px; width: 160px; }\n}\n\n@media (max-width: 380px) {\n  .resumen-dashboard { gap: 5px; }\n  .resumen-card h3 { font-size: 0.56rem; }\n  .resumen-card span { font-size: 0.86rem; }\n  .btn-main, .btn-secondary { font-size: 0.75rem; }\n  .mesa-controls { grid-template-columns: 1fr; gap: 6px; }\n  .mesa-visual-wrap { min-height: 170px; padding: 7px; }\n  .mesa-visual { --seat-size: 23px; --seat-radius: 54px; width: 150px; }\n  .mesa-table { inset: 26%; font-size: 0.68rem; padding: 7px; }\n  .mesa-table.rectangular { inset: 35% 17%; }\n  .seat-dot { font-size: 0.54rem; }\n}\n<\/style>\n<\/head>\n<body>\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  <!-- UNDO BAR -->\n  <div class=\"undo-bar\" id=\"undoBar\">\n    <span id=\"undoMsg\">\u21a9\ufe0f Cambio realizado<\/span>\n    <button onclick=\"deshacerCambio()\">Deshacer<\/button>\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, grupo 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      <button class=\"btn-secondary\" onclick=\"imprimirMesas()\">\ud83d\udda8\ufe0f Imprimir<\/button>\n    <\/div>\n  <\/div>\n\n  <!-- ADD TABLE FORM -->\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    <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    <div class=\"add-field\">\n      <label>Personas<\/label>\n      <input type=\"number\" id=\"nuevaMesaCapacidad\" value=\"8\" min=\"1\" max=\"24\">\n    <\/div>\n    <div class=\"add-field\">\n      <label>Grupo (color)<\/label>\n      <div class=\"grupo-color-wrap\" id=\"nuevaMesaColorWrap\">\n        <!-- colores generados por JS -->\n      <\/div>\n    <\/div>\n    <button class=\"btn-main\" onclick=\"agregarMesa()\">Guardar<\/button>\n    <button class=\"btn-secondary\" onclick=\"toggleFormularioMesa()\">Cerrar<\/button>\n  <\/div>\n\n  <!-- SIN SENTAR -->\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    <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>\n\/\/ ============================================================\n\/\/ CONFIGURACI\u00d3N\n\/\/ ============================================================\nconst MESAS_JSON_URL    = 'https:\/\/bodafranyjose.com\/wp-content\/uploads\/mesas\/mesas.json';\nconst GUARDAR_MESAS_URL = 'https:\/\/bodafranyjose.com\/wp-content\/uploads\/mesas\/guardar_mesas.php?key=bodafest2025KEY';\nconst INVITADOS_JSON_URL= 'https:\/\/bodafranyjose.com\/wp-content\/uploads\/invitados\/invitados.json';\n\n\/\/ Paleta de grupos\nconst GRUPO_COLORES = [\n  { id: 'none',   hex: '#e0d6db', label: 'Sin grupo' },\n  { id: 'rosa',   hex: '#e87da0', label: 'Rosa' },\n  { id: 'lila',   hex: '#9b7de8', label: 'Lila' },\n  { id: 'azul',   hex: '#5ba3e8', label: 'Azul' },\n  { id: 'verde',  hex: '#5bc47a', label: 'Verde' },\n  { id: 'naranja',hex: '#e8965b', label: 'Naranja' },\n  { id: 'amarillo',hex:'#e8d45b', label: 'Amarillo' },\n  { id: 'gris',   hex: '#8fa3b0', label: 'Gris' },\n];\n\n\/\/ ============================================================\n\/\/ ESTADO\n\/\/ ============================================================\nlet mesas          = [];\nlet invitados      = [];\nlet mesasAbiertas  = new Set();\nlet busquedaMesas  = '';\nlet filtroMesas    = 'todas';\nlet undoStack      = [];   \/\/ historial de deshacer\nlet undoTimer      = null;\nlet nuevaMesaColor = 'none';\n\n\/\/ ============================================================\n\/\/ UTILIDADES\n\/\/ ============================================================\nfunction escaparHTML(v) {\n  return String(v||'').replace(\/&\/g,'&amp;').replace(\/<\/g,'&lt;').replace(\/>\/g,'&gt;').replace(\/\"\/g,'&quot;').replace(\/'\/g,'&#039;');\n}\nfunction escaparJS(v) {\n  return String(v||'').replace(\/\\\\\/g,'\\\\\\\\').replace(\/'\/g,\"\\\\'\").replace(\/\"\/g,'&quot;').replace(\/<\/g,'&lt;').replace(\/>\/g,'&gt;').replace(\/\\n\/g,' ').replace(\/\\r\/g,' ');\n}\nfunction normalizar(v) {\n  return String(v||'').normalize('NFD').replace(\/[\\u0300-\\u036f]\/g,'').trim().toLowerCase();\n}\nfunction limpiarCSV(v) {\n  return String(v||'').replace(\/\"\/g,'\"\"').replace(\/\\n\/g,' ').replace(\/\\r\/g,' ');\n}\nfunction iniciales(nombre) {\n  return String(nombre||'').trim().split(\/\\s+\/).slice(0,2).map(p=>p.charAt(0).toUpperCase()).join('');\n}\nfunction colorDeGrupo(id) {\n  const c = GRUPO_COLORES.find(c=>c.id===id);\n  return c ? c.hex : null;\n}\nfunction labelDeGrupo(id) {\n  const c = GRUPO_COLORES.find(c=>c.id===id);\n  return (c && c.id !== 'none') ? c.label : '';\n}\n\n\/\/ ============================================================\n\/\/ DESHACER\n\/\/ ============================================================\nfunction guardarSnapshot(descripcion) {\n  \/\/ Guarda copia profunda del estado actual\n  undoStack.push({ descripcion, mesas: JSON.parse(JSON.stringify(mesas)) });\n  if (undoStack.length > 20) undoStack.shift(); \/\/ m\u00e1ximo 20 niveles\n}\nfunction mostrarBarraDeshacer(msg) {\n  const bar = document.getElementById('undoBar');\n  const msgEl = document.getElementById('undoMsg');\n  if (!bar || !msgEl) return;\n  msgEl.textContent = '\u21a9\ufe0f ' + msg;\n  bar.classList.add('visible');\n  if (undoTimer) clearTimeout(undoTimer);\n  undoTimer = setTimeout(() => bar.classList.remove('visible'), 6000);\n}\nfunction deshacerCambio() {\n  if (!undoStack.length) return;\n  const snap = undoStack.pop();\n  mesas = snap.mesas;\n  renderMesas();\n  guardarCambios(true);\n  const bar = document.getElementById('undoBar');\n  if (bar) bar.classList.remove('visible');\n}\n\n\/\/ ============================================================\n\/\/ ACOMPA\u00d1ANTES \/ PERSONAS\n\/\/ ============================================================\nfunction nombreAcompanante(inv) {\n  if (!inv || !inv.acompanante) return '';\n  return inv.acompananteNombre && inv.acompananteNombre.trim()\n    ? inv.acompananteNombre.trim()\n    : 'Acompa\u00f1ante de ' + (inv.nombre||'');\n}\nfunction crearAsientos(cap, actuales) {\n  const total = Math.max(1, Math.min(parseInt(cap,10)||1, 24));\n  const a = Array.isArray(actuales) ? actuales.slice(0,total) : [];\n  while (a.length < total) a.push('');\n  return a;\n}\nfunction normalizarMesa(m, i) {\n  const cap = Math.max(1, Math.min(parseInt(m.capacidad,10)||8, 24));\n  return {\n    id:       m.id || ('mesa-'+Date.now()+'-'+i),\n    nombre:   m.nombre || ('Mesa '+(i+1)),\n    tipo:     m.tipo==='rectangular' ? 'rectangular' : 'circular',\n    capacidad:cap,\n    asientos: crearAsientos(cap, m.asientos),\n    nota:     m.nota || '',\n    grupo:    m.grupo || 'none',\n  };\n}\nfunction normalizarMesas() {\n  mesas = mesas.map(normalizarMesa);\n}\nfunction obtenerOpcionesPersonas() {\n  const p = [];\n  invitados.forEach(inv => {\n    if (inv.nombre) {\n      p.push(inv.nombre);\n      if (inv.acompanante) p.push(nombreAcompanante(inv));\n    }\n  });\n  return p;\n}\nfunction obtenerPersonasSentadas() {\n  return mesas.flatMap(m => (m.asientos||[]).filter(Boolean));\n}\nfunction obtenerPersonasSinSentar() {\n  const sentadas = new Set(obtenerPersonasSentadas().map(normalizar));\n  return obtenerOpcionesPersonas()\n    .filter(n => !sentadas.has(normalizar(n)))\n    .sort((a,b) => a.localeCompare(b,'es'));\n}\n\n\/\/ ============================================================\n\/\/ CARGA DE DATOS\n\/\/ ============================================================\nasync function cargarDatos() {\n  try {\n    const r = await fetch(MESAS_JSON_URL+'?v='+Date.now());\n    mesas = await r.json();\n    if (!Array.isArray(mesas)) mesas = [];\n  } catch(e) { console.error('Error cargando mesas:',e); mesas = []; }\n  await cargarInvitados();\n  normalizarMesas();\n  renderMesas();\n}\nasync function cargarInvitados() {\n  try {\n    const r = await fetch(INVITADOS_JSON_URL+'?v='+Date.now());\n    invitados = await r.json();\n    if (!Array.isArray(invitados)) invitados = [];\n  } catch(e) { console.warn('No se pudo cargar invitados:',e); invitados = []; }\n}\nasync function actualizarInvitados() {\n  await cargarInvitados();\n  renderMesas();\n  alert('\u2705 Invitados actualizados');\n}\n\n\/\/ ============================================================\n\/\/ B\u00daSQUEDA Y FILTROS\n\/\/ ============================================================\nfunction actualizarBusquedaMesas(v) { busquedaMesas = v; renderMesas(); }\nfunction filtrarMesas(tipo) {\n  filtroMesas = tipo;\n  document.querySelectorAll('.resumen-card').forEach(c => c.classList.remove('active'));\n  const sel = tipo==='conPlazas' ? '.resumen-card.plazas'\n            : tipo==='conSentados' ? '.resumen-card.sentados'\n            : tipo==='conLibres'   ? '.resumen-card.libres'\n            : '.resumen-card.mesas';\n  document.querySelector(sel)?.classList.add('active');\n  renderMesas();\n}\nfunction pasaFiltroMesa(m) {\n  const ocu = (m.asientos||[]).filter(Boolean).length;\n  const lib = (m.capacidad||0) - ocu;\n  if (filtroMesas==='conPlazas')   return (m.capacidad||0) > 0;\n  if (filtroMesas==='conSentados') return ocu > 0;\n  if (filtroMesas==='conLibres')   return lib > 0;\n  return true;\n}\n\n\/\/ ============================================================\n\/\/ FORMULARIO NUEVA MESA\n\/\/ ============================================================\nfunction toggleFormularioMesa() {\n  const f = document.getElementById('formNuevaMesa');\n  if (f) f.classList.toggle('active');\n}\nfunction renderColorPickerNuevaMesa() {\n  const wrap = document.getElementById('nuevaMesaColorWrap');\n  if (!wrap) return;\n  wrap.innerHTML = GRUPO_COLORES.map(c => `\n    <button type=\"button\" class=\"grupo-color-btn ${nuevaMesaColor===c.id?'sel':''}\"\n      style=\"background:${c.hex}\" title=\"${c.label}\"\n      onclick=\"seleccionarColorNuevaMesa('${c.id}')\"><\/button>\n  `).join('');\n}\nfunction seleccionarColorNuevaMesa(id) {\n  nuevaMesaColor = id;\n  renderColorPickerNuevaMesa();\n}\n\n\/\/ ============================================================\n\/\/ SIN SENTAR\n\/\/ ============================================================\nfunction toggleSinSentar() {\n  document.getElementById('sinSentarPanel')?.classList.toggle('active');\n  renderSinSentar();\n}\nfunction renderSinSentar() {\n  const cont  = document.getElementById('listaSinSentar');\n  const bal   = document.getElementById('sinSentarBalance');\n  const btn   = document.getElementById('btnSinSentar');\n  if (!cont||!bal||!btn) return;\n  const sinSentar = obtenerPersonasSinSentar();\n  const totalPlazas   = mesas.reduce((a,m) => a+(m.capacidad||0), 0);\n  const totalSentados = mesas.reduce((a,m) => a+(m.asientos||[]).filter(Boolean).length, 0);\n  const libres = totalPlazas - totalSentados;\n  btn.textContent = '\ud83d\udc65 Sin sentar ('+sinSentar.length+')';\n  cont.innerHTML = '';\n  bal.classList.remove('ok','warn');\n  if (sinSentar.length===0) {\n    bal.textContent = 'Todos los invitados y acompa\u00f1antes est\u00e1n sentados.';\n    bal.classList.add('ok');\n    cont.innerHTML = '<div class=\"sin-sentar-empty\">Todo el mundo tiene sitio asignado. \ud83c\udf89<\/div>';\n    return;\n  }\n  if (libres < sinSentar.length) {\n    bal.textContent = 'Faltan '+(sinSentar.length-libres)+' plazas para sentar a todos.';\n    bal.classList.add('warn');\n  } else {\n    bal.textContent = libres+' plazas libres para '+sinSentar.length+' personas sin sentar.';\n    bal.classList.add('ok');\n  }\n  sinSentar.forEach(nombre => {\n    const btn2 = document.createElement('button');\n    btn2.type = 'button';\n    btn2.className = 'sin-sentar-pill';\n    btn2.textContent = nombre;\n    btn2.onclick = () => copiarSinSentar(nombre);\n    cont.appendChild(btn2);\n  });\n}\nfunction copiarSinSentar(nombre) {\n  if (navigator.clipboard?.writeText) {\n    navigator.clipboard.writeText(nombre)\n      .then(() => window.mostrarEstadoGuardado?.('saved','Nombre copiado') || alert('Nombre copiado: '+nombre))\n      .catch(() => prompt('Copia este nombre:', nombre));\n  } else {\n    prompt('Copia este nombre:', nombre);\n  }\n}\n\n\/\/ ============================================================\n\/\/ RENDER PRINCIPAL\n\/\/ ============================================================\nfunction renderMesas() {\n  const cont = document.getElementById('listaMesas');\n  if (!cont) return;\n  cont.innerHTML = '';\n  normalizarMesas();\n  actualizarResumen();\n  renderSinSentar();\n  renderColorPickerNuevaMesa();\n\n  const termino = normalizar(busquedaMesas);\n  const lista = mesas\n    .map((m,i) => ({...m, index:i}))\n    .filter(m => {\n      if (!pasaFiltroMesa(m)) return false;\n      if (!termino) return true;\n      return normalizar(m.nombre).includes(termino)\n        || normalizar(m.nota||'').includes(termino)\n        || labelDeGrupo(m.grupo).toLowerCase().includes(termino)\n        || (m.asientos||[]).some(n => normalizar(n).includes(termino));\n    });\n\n  if (!lista.length) {\n    cont.innerHTML = '<div class=\"empty-state\">No hay mesas que coincidan con este filtro.<\/div>';\n    return;\n  }\n\n  lista.forEach(mesa => {\n    const i       = mesa.index;\n    const ocu     = (mesa.asientos||[]).filter(Boolean).length;\n    const lib     = mesa.capacidad - ocu;\n    const abierta = mesasAbiertas.has(mesa.id);\n    const completa= ocu >= mesa.capacidad;\n    const color   = colorDeGrupo(mesa.grupo);\n    const grupoLabel = labelDeGrupo(mesa.grupo);\n    const notaPreview = mesa.nota ? mesa.nota.substring(0,40)+(mesa.nota.length>40?'\u2026':'') : '';\n\n    const card = document.createElement('div');\n    card.className = 'mesa-card'+(completa?' completa':'');\n    if (color && mesa.grupo !== 'none') {\n      card.setAttribute('data-color','');\n      card.style.setProperty('--gc', color);\n    }\n\n    card.innerHTML = `\n      <div class=\"mesa-header\" onclick=\"toggleMesa('${escaparJS(mesa.id)}')\">\n        <div>\n          <h3>${escaparHTML(mesa.nombre)}<\/h3>\n          <div class=\"mesa-meta\">${mesa.tipo==='circular'?'Circular':'Rectangular'} \u00b7 ${ocu}\/${mesa.capacidad} sentados<\/div>\n          <div class=\"mesa-badges\">\n            <span class=\"mesa-badge ${completa?'ok':'warn'}\">${completa?'Completa':lib+' libres'}<\/span>\n            <span class=\"mesa-badge\">${mesa.capacidad} plazas<\/span>\n            ${grupoLabel ? `<span class=\"mesa-badge grupo\" style=\"background:${color}22;color:${color};border-color:${color}44\">${grupoLabel}<\/span>` : ''}\n          <\/div>\n          ${notaPreview ? `<div class=\"mesa-nota-badge\">\ud83d\udcdd ${escaparHTML(notaPreview)}<\/div>` : ''}\n        <\/div>\n        <div class=\"mesa-header-actions\">\n          <span class=\"toggle-mesa ${abierta?'open':''}\">\u25be<\/span>\n        <\/div>\n      <\/div>\n\n      <div class=\"mesa-content ${abierta?'active':''}\">\n        <div class=\"mesa-controls\">\n          <input type=\"text\" value=\"${escaparHTML(mesa.nombre)}\" placeholder=\"Nombre\" onchange=\"actualizarMesa(${i},'nombre',this.value)\">\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          <input type=\"number\" value=\"${mesa.capacidad}\" min=\"1\" max=\"24\" onchange=\"actualizarMesa(${i},'capacidad',this.value)\">\n        <\/div>\n\n        <div class=\"mesa-grupo-row\">\n          <div>\n            <div style=\"font-size:0.78rem;color:#b67b91;font-weight:700;margin-bottom:5px;\">Grupo \/ etiqueta<\/div>\n            <div class=\"grupos-paleta\" id=\"paleta-${i}\">\n              ${GRUPO_COLORES.map(c=>`\n                <button type=\"button\" class=\"gp-btn ${mesa.grupo===c.id?'sel':''}\"\n                  style=\"background:${c.hex}\" title=\"${c.label}\"\n                  onclick=\"actualizarMesa(${i},'grupo','${c.id}')\"><\/button>\n              `).join('')}\n            <\/div>\n          <\/div>\n        <\/div>\n\n        <div class=\"mesa-nota-wrap\">\n          <div class=\"mesa-nota-label\">\ud83d\udcdd Nota de mesa<\/div>\n          <textarea class=\"mesa-nota\" placeholder=\"Sin gluten, familia novia, mesa VIP\u2026\" onchange=\"actualizarMesa(${i},'nota',this.value)\">${escaparHTML(mesa.nota||'')}<\/textarea>\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('${escaparJS(mesa.id)}')\">Cerrar<\/button>\n          <button class=\"btn-secondary\" onclick=\"imprimirMesa(${i})\">\ud83d\udda8\ufe0f<\/button>\n          <button class=\"btn-danger\" onclick=\"eliminarMesa(${i})\">Eliminar<\/button>\n        <\/div>\n      <\/div>\n    `;\n    cont.appendChild(card);\n  });\n}\n\nfunction toggleMesa(id) {\n  mesasAbiertas.has(id) ? mesasAbiertas.delete(id) : mesasAbiertas.add(id);\n  renderMesas();\n}\nfunction cerrarMesa(id) { mesasAbiertas.delete(id); renderMesas(); }\n\n\/\/ ============================================================\n\/\/ PUNTOS VISUALES\n\/\/ ============================================================\nfunction generarPuntosMesa(m) {\n  return m.tipo==='rectangular' ? generarPuntosRect(m) : generarPuntosCirc(m);\n}\nfunction generarPuntosCirc(m) {\n  return m.asientos.map((nombre,i) => {\n    const angle = (360\/m.capacidad)*i - 90;\n    return `<div class=\"seat-dot ${nombre?'ocupado':''}\" style=\"--angle:${angle}deg;\" title=\"${escaparHTML(nombre||'Libre')}\">${escaparHTML(iniciales(nombre))}<\/div>`;\n  }).join('');\n}\nfunction generarPuntosRect(m) {\n  const arriba = Math.ceil(m.capacidad\/2);\n  return m.asientos.map((nombre,i) => {\n    const esArr = i < arriba;\n    const pos   = esArr ? i : i - arriba;\n    const total = esArr ? arriba : m.capacidad - arriba;\n    const x     = total<=1 ? 50 : 12 + (76\/(total-1))*pos;\n    const y     = esArr ? 28 : 72;\n    return `<div class=\"seat-dot ${nombre?'ocupado':''}\" style=\"--x:${x}%;--y:${y}%;\" title=\"${escaparHTML(nombre||'Libre')}\">${escaparHTML(iniciales(nombre))}<\/div>`;\n  }).join('');\n}\n\n\/\/ ============================================================\n\/\/ PICKER DE ASIENTOS \u2014 fix iOS con pointerdown\n\/\/ ============================================================\nfunction generarEditorAsientos(mesa, mi) {\n  return mesa.asientos.map((nombre, ai) => {\n    const pid = 'picker-'+mi+'-'+ai;\n    return `\n      <div class=\"asiento-row\">\n        <div class=\"asiento-num\">S${ai+1}<\/div>\n        <div class=\"asiento-picker\" id=\"${pid}\">\n          <input type=\"text\" class=\"asiento-search\" value=\"${escaparHTML(nombre)}\"\n            placeholder=\"Buscar invitado...\"\n            onfocus=\"abrirPicker('${pid}',${mi},${ai},this.value)\"\n            onclick=\"abrirPicker('${pid}',${mi},${ai},this.value)\"\n            oninput=\"filtrarPicker('${pid}',${mi},${ai},this.value)\"\n            onblur=\"cerrarPickerDelay('${pid}',${mi},${ai})\">\n          <div class=\"asiento-options\">${generarOpciones(nombre,mi,ai,'')}<\/div>\n        <\/div>\n      <\/div>`;\n  }).join('');\n}\n\nfunction obtenerDisponibles(actual) {\n  const actN = normalizar(actual);\n  const usados = new Set();\n  mesas.forEach(m => m.asientos.forEach(n => { const nn=normalizar(n); if(nn && nn!==actN) usados.add(nn); }));\n  return obtenerOpcionesPersonas()\n    .filter(n => !usados.has(normalizar(n)) || normalizar(n)===actN)\n    .sort((a,b) => a.localeCompare(b,'es'));\n}\n\nfunction generarOpciones(actual, mi, ai, busq) {\n  const term = normalizar(busq);\n  const actN = normalizar(actual);\n  const opts = obtenerDisponibles(actual).filter(n => normalizar(n).includes(term));\n  let html = `<button type=\"button\" class=\"asiento-option clear\" onpointerdown=\"event.preventDefault();seleccionarAsiento(${mi},${ai},'')\">Dejar libre<\/button>`;\n  opts.forEach(n => {\n    const tag = normalizar(n)===actN ? ' \u00b7 aqu\u00ed' : '';\n    html += `<button type=\"button\" class=\"asiento-option\" onpointerdown=\"event.preventDefault();seleccionarAsiento(${mi},${ai},'${escaparJS(n)}')\">${escaparHTML(n)}${tag}<\/button>`;\n  });\n  if (busq.trim() && !opts.some(n => normalizar(n)===normalizar(busq))) {\n    html += `<button type=\"button\" class=\"asiento-option\" onpointerdown=\"event.preventDefault();seleccionarAsiento(${mi},${ai},'${escaparJS(busq)}')\">Usar \"${escaparHTML(busq)}\"<\/button>`;\n  }\n  if (!opts.length && !busq.trim()) {\n    html += `<button type=\"button\" class=\"asiento-option\" disabled>No hay invitados disponibles<\/button>`;\n  }\n  return html;\n}\n\nfunction abrirPicker(pid, mi, ai, busq) {\n  document.querySelectorAll('.asiento-picker.open').forEach(p => { if(p.id!==pid) p.classList.remove('open'); });\n  const picker = document.getElementById(pid);\n  if (!picker) return;\n  const actual = mesas[mi]?.asientos[ai] || '';\n  picker.querySelector('.asiento-options').innerHTML = generarOpciones(actual, mi, ai, busq);\n  picker.classList.add('open');\n}\nfunction filtrarPicker(pid, mi, ai, busq) {\n  const picker = document.getElementById(pid);\n  if (!picker) return;\n  const actual = mesas[mi]?.asientos[ai] || '';\n  picker.querySelector('.asiento-options').innerHTML = generarOpciones(actual, mi, ai, busq);\n  picker.classList.add('open');\n}\nfunction cerrarPickerDelay(pid, mi, ai) {\n  \/\/ Espera m\u00e1s en iOS para que el pointerdown se registre antes del blur\n  setTimeout(() => {\n    const picker = document.getElementById(pid);\n    if (!picker) return;\n    picker.classList.remove('open');\n    const input = picker.querySelector('.asiento-search');\n    if (input) input.value = mesas[mi]?.asientos[ai] || '';\n  }, 250);\n}\n\nasync function seleccionarAsiento(mi, ai, valor) {\n  await cambiarAsiento(mi, ai, valor);\n}\n\n\/\/ ============================================================\n\/\/ CRUD MESAS\n\/\/ ============================================================\nasync function agregarMesa() {\n  const nombre   = document.getElementById('nuevaMesaNombre')?.value.trim() || 'Mesa '+(mesas.length+1);\n  const tipo     = document.getElementById('nuevaMesaTipo')?.value || 'circular';\n  const capacidad= Math.max(1, Math.min(parseInt(document.getElementById('nuevaMesaCapacidad')?.value,10)||8, 24));\n  const id       = 'mesa-'+Date.now();\n  guardarSnapshot('A\u00f1adir mesa');\n  mesas.push({ id, nombre, tipo, capacidad, asientos: crearAsientos(capacidad,[]), nota:'', grupo: nuevaMesaColor });\n  mesasAbiertas.add(id);\n  document.getElementById('nuevaMesaNombre').value = '';\n  document.getElementById('nuevaMesaTipo').value = 'circular';\n  document.getElementById('nuevaMesaCapacidad').value = '8';\n  nuevaMesaColor = 'none';\n  document.getElementById('formNuevaMesa')?.classList.remove('active');\n  renderMesas();\n  await guardarCambios(true);\n}\n\nasync function actualizarMesa(i, campo, valor) {\n  if (!mesas[i]) return;\n  guardarSnapshot('Editar '+campo);\n  if (campo==='capacidad') {\n    const cap = Math.max(1, Math.min(parseInt(valor,10)||1, 24));\n    mesas[i].capacidad = cap;\n    mesas[i].asientos  = crearAsientos(cap, 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  mesasAbiertas.add(mesas[i].id);\n  renderMesas();\n  await guardarCambios(true);\n  if (['nombre','capacidad','tipo','grupo','nota'].includes(campo)) {\n    mostrarBarraDeshacer('Cambio en \"'+mesas[i]?.nombre+'\"');\n  }\n}\n\nasync function cambiarAsiento(mi, ai, valor) {\n  if (!mesas[mi]) return;\n  const nombre = String(valor||'').trim();\n  if (mesas[mi].asientos[ai]===nombre) return;\n  guardarSnapshot('Cambiar asiento');\n  const prev = mesas[mi].asientos[ai];\n  mesas[mi].asientos[ai] = nombre;\n  mesasAbiertas.add(mesas[mi].id);\n  renderMesas();\n  await guardarCambios(true);\n  const msg = nombre\n    ? `${nombre} \u2192 ${mesas[mi].nombre} S${ai+1}`\n    : `Liberado S${ai+1} (era ${prev||'vac\u00edo'})`;\n  mostrarBarraDeshacer(msg);\n}\n\nasync function eliminarMesa(i) {\n  if (!mesas[i]) return;\n  if (confirm(`\u00bfEliminar \"${mesas[i].nombre}\"?`)) {\n    guardarSnapshot('Eliminar mesa');\n    mesasAbiertas.delete(mesas[i].id);\n    const nombre = mesas[i].nombre;\n    mesas.splice(i,1);\n    renderMesas();\n    await guardarCambios(true);\n    mostrarBarraDeshacer(`Mesa \"${nombre}\" eliminada`);\n  }\n}\n\n\/\/ ============================================================\n\/\/ RESUMEN\n\/\/ ============================================================\nfunction actualizarResumen() {\n  const totalPlazas   = mesas.reduce((a,m)=>a+(m.capacidad||0),0);\n  const totalSentados = mesas.reduce((a,m)=>a+(m.asientos||[]).filter(Boolean).length,0);\n  const totalLibres   = totalPlazas - totalSentados;\n  const pct = totalPlazas>0 ? Math.min((totalSentados\/totalPlazas)*100,100) : 0;\n  document.getElementById('totalMesas').textContent    = mesas.length;\n  document.getElementById('totalPlazas').textContent   = totalPlazas;\n  document.getElementById('totalSentados').textContent = totalSentados;\n  document.getElementById('totalLibres').textContent   = totalLibres;\n  const prog = document.getElementById('seatingProgress');\n  if (prog) prog.style.width = pct+'%';\n  const progTxt = document.getElementById('seatingProgressText');\n  if (progTxt) progTxt.textContent = Math.round(pct)+'%';\n}\n\n\/\/ ============================================================\n\/\/ GUARDAR\n\/\/ ============================================================\nasync function guardarCambios(silencioso=false) {\n  try {\n    normalizarMesas();\n    const res = await fetch(GUARDAR_MESAS_URL, {\n      method:'POST',\n      headers:{'Content-Type':'application\/json'},\n      body: JSON.stringify(mesas)\n    });\n    if (!res.ok) throw new Error('HTTP '+res.status);\n    if (!silencioso) alert('\u2705 Seating plan guardado correctamente');\n  } catch(e) {\n    console.error('\u274c Error al guardar:',e);\n    if (!silencioso) alert('\u274c Error al guardar el seating plan');\n  }\n}\n\n\/\/ ============================================================\n\/\/ EXPORTAR\n\/\/ ============================================================\nfunction exportarMesas() {\n  const filas = [['Mesa','Forma','Capacidad','Grupo','Nota','Asiento','Invitado']];\n  mesas.forEach(m => {\n    m.asientos.forEach((n,i) => {\n      filas.push([m.nombre||'', m.tipo||'', m.capacidad||'', labelDeGrupo(m.grupo)||'', m.nota||'', 'S'+(i+1), n||'']);\n    });\n  });\n  const csv = filas.map(f => f.map(c=>`\"${limpiarCSV(c)}\"`).join(';')).join('\\n');\n  const blob = new Blob(['\\uFEFF'+csv], {type:'text\/csv;charset=utf-8;'});\n  const url  = URL.createObjectURL(blob);\n  const a    = document.createElement('a');\n  a.href=url; a.download='seating-plan-boda.csv'; a.click();\n  URL.revokeObjectURL(url);\n}\n\n\/\/ ============================================================\n\/\/ IMPRIMIR\n\/\/ ============================================================\nfunction imprimirMesas() {\n  \/\/ Abre todas las mesas antes de imprimir\n  mesas.forEach(m => mesasAbiertas.add(m.id));\n  renderMesas();\n  setTimeout(() => window.print(), 300);\n}\nfunction imprimirMesa(i) {\n  \/\/ Solo imprime una mesa mostr\u00e1ndola en nueva ventana\n  const m = mesas[i];\n  if (!m) return;\n  const asientosList = m.asientos.map((n,idx)=>`<tr><td>S${idx+1}<\/td><td>${n||'\u2014'}<\/td><\/tr>`).join('');\n  const win = window.open('','_blank','width=600,height=700');\n  win.document.write(`\n    <html><head><meta charset=\"UTF-8\"><title>${m.nombre}<\/title>\n    <style>body{font-family:sans-serif;padding:20px}h2{color:#b67b91}table{border-collapse:collapse;width:100%}td{border:1px solid #ddd;padding:8px}tr:nth-child(even){background:#f9f0f4}<\/style>\n    <\/head><body>\n    <h2>\ud83e\ude91 ${m.nombre}<\/h2>\n    <p><b>Forma:<\/b> ${m.tipo} \u00b7 <b>Plazas:<\/b> ${m.capacidad} \u00b7 <b>Grupo:<\/b> ${labelDeGrupo(m.grupo)||'\u2014'}<\/p>\n    ${m.nota ? `<p><b>Nota:<\/b> ${m.nota}<\/p>` : ''}\n    <table><tr><th>Asiento<\/th><th>Invitado<\/th><\/tr>${asientosList}<\/table>\n    <script>window.onload=()=>window.print()<\\\/script>\n    <\/body><\/html>`);\n  win.document.close();\n}\n\n\/\/ ============================================================\n\/\/ INIT\n\/\/ ============================================================\ncargarDatos();\nwindow.guardarCambios = guardarCambios;\n<\/script>\n<\/body>\n<\/html>\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>Seating Plan \u2013 Boda \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% \u21a9\ufe0f Cambio realizado Deshacer \u2795 A\u00f1adir mesa \ud83d\udc65 Sin sentar \ud83d\udd04 Invitados \ud83d\udce4 Exportar \ud83d\udda8\ufe0f Imprimir Nombre Forma CircularRectangular Personas Grupo (color) Guardar Cerrar \ud83d\udc65 Invitados [&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.7 - 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=\"Seating Plan \u2013 Boda \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% \u21a9\ufe0f Cambio realizado Deshacer \u2795 A\u00f1adir mesa \ud83d\udc65 Sin sentar \ud83d\udd04 Invitados \ud83d\udce4 Exportar \ud83d\udda8\ufe0f Imprimir Nombre Forma CircularRectangular Personas Grupo (color) Guardar Cerrar \ud83d\udc65 Invitados [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/bodafranyjose.com\/index.php\/mesas\/\" \/>\n<meta property=\"article:modified_time\" content=\"2026-04-14T14:46:43+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=\"21 minutos\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/mesas\\\/\",\"url\":\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/mesas\\\/\",\"name\":\"mesas -\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/#website\"},\"datePublished\":\"2026-04-12T18:55:40+00:00\",\"dateModified\":\"2026-04-14T14:46:43+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/mesas\\\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/mesas\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/index.php\\\/mesas\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Portada\",\"item\":\"https:\\\/\\\/bodafranyjose.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"mesas\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/bodafranyjose.com\\\/#website\",\"url\":\"https:\\\/\\\/bodafranyjose.com\\\/\",\"name\":\"\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/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":"Seating Plan \u2013 Boda \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% \u21a9\ufe0f Cambio realizado Deshacer \u2795 A\u00f1adir mesa \ud83d\udc65 Sin sentar \ud83d\udd04 Invitados \ud83d\udce4 Exportar \ud83d\udda8\ufe0f Imprimir Nombre Forma CircularRectangular Personas Grupo (color) Guardar Cerrar \ud83d\udc65 Invitados [&hellip;]","og_url":"https:\/\/bodafranyjose.com\/index.php\/mesas\/","article_modified_time":"2026-04-14T14:46:43+00:00","twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"21 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/bodafranyjose.com\/index.php\/mesas\/","url":"https:\/\/bodafranyjose.com\/index.php\/mesas\/","name":"mesas -","isPartOf":{"@id":"https:\/\/bodafranyjose.com\/#website"},"datePublished":"2026-04-12T18:55:40+00:00","dateModified":"2026-04-14T14:46:43+00:00","breadcrumb":{"@id":"https:\/\/bodafranyjose.com\/index.php\/mesas\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/bodafranyjose.com\/index.php\/mesas\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/bodafranyjose.com\/index.php\/mesas\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Portada","item":"https:\/\/bodafranyjose.com\/"},{"@type":"ListItem","position":2,"name":"mesas"}]},{"@type":"WebSite","@id":"https:\/\/bodafranyjose.com\/#website","url":"https:\/\/bodafranyjose.com\/","name":"","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/bodafranyjose.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"}]}},"_links":{"self":[{"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/836","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/comments?post=836"}],"version-history":[{"count":60,"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/836\/revisions"}],"predecessor-version":[{"id":1015,"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/pages\/836\/revisions\/1015"}],"wp:attachment":[{"href":"https:\/\/bodafranyjose.com\/index.php\/wp-json\/wp\/v2\/media?parent=836"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}