{"id":2965,"date":"2025-11-01T07:23:29","date_gmt":"2025-11-01T06:23:29","guid":{"rendered":"https:\/\/mvips.net\/?page_id=2965"},"modified":"2025-11-22T19:47:07","modified_gmt":"2025-11-22T18:47:07","slug":"bukas_1","status":"publish","type":"page","link":"https:\/\/mvips.net\/en\/bukas_1\/","title":{"rendered":"Bukas El que Vale"},"content":{"rendered":"<!doctype html>\n<html lang=\"es\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>MVIPS \u2014 Bukas (todo en uno, con chips+auto)<\/title>\n<!-- PWA opcional (no imprescindible para el formulario) -->\n<link rel=\"manifest\" href=\"\/wp-content\/uploads\/bukas\/manifest.json\">\n<meta name=\"theme-color\" content=\"#6b5f3b\">\n<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n<!-- TODO: replace YOUR_GOOGLE_MAPS_API_KEY with actual key -->\n<script\n  id=\"bukas-maps\"\n  src=\"https:\/\/maps.googleapis.com\/maps\/api\/js?key=AIzaSyB0_HWfymWPM2xSOLm7hSL0v2jw_mFXhLk&#038;libraries=places&#038;language=es&#038;region=ES\"\n  async\n  defer><\/script>\n<style>\n\n\/* Comentarios: textarea un poco m\u00e1s bajo *\/\n#comentarios textarea{\n  min-height: 80px;   \/* prueba 80; si lo quieres a\u00fan m\u00e1s bajo, 60 *\/\n}\n\n\n\/* Oculta solo cuando el elemento tenga el atributo [hidden] *\/\n#s1Price[hidden],\n#s2Price[hidden],\n#s3Price[hidden],\n#pagoTotal[hidden] { display:none !important; }\n\n\n:root{\n  --brand:#6b5f3b; --brand-2:#b19b61; --ink:#222; --muted:#555; --bg:#fcfcfa; --card:#fefdf9;\n  --border:#dcd7c9; --border-2:#e6e0d2; --focus-ring:rgba(177,155,97,.22);\n  --warn-bg:#fff9e6; --warn-border:#f2d174; --radius:12px; --radius-sm:8px;\n  --gap-x:12px; --gap-y:10px; --control-h:42px; --font:\"Segoe UI\",system-ui,Arial,sans-serif;\n  --chip-bg: #f9fafb;\n  --chip-border: #e5e7eb;\n  --chip-shadow: 0 1px 2px rgba(0,0,0,.06);\n  --chip-name: #111827;\n  --chip-addr: #6b7280;\n  --chip-hover: #f3f4f6;\n  --chip-accent: #2563eb;\n}\n*{box-sizing:border-box}\nbody{background:#fff;color:var(--ink);font-family:var(--font);}\n#bukasForm{ max-width:900px; margin:30px auto; color:var(--ink); }\n.form-head{\n  display:flex;\n  align-items:center;\n  justify-content:space-between;\n  gap:12px;\n  margin:20px auto;\n  max-width:900px;\n  flex-wrap:wrap;              \/* permite que baje a la l\u00ednea de abajo *\/\n}\n.form-head h2{ margin:0; color:var(--brand); font-weight:800; letter-spacing:.5px; }\n.btn-gold{ appearance:none; border:0; border-radius:12px; cursor:pointer; padding:10px 16px; font:700 14px\/1 var(--font); color:#fff; background: linear-gradient(180deg, #c8b782 0%, var(--brand-2) 100%); box-shadow:0 6px 18px rgba(0,0,0,.16); transition:transform .06s ease, filter .18s ease, box-shadow .18s ease; white-space:nowrap; }\n.btn-gold:hover{ filter:brightness(1.03); box-shadow:0 8px 22px rgba(0,0,0,.18); }\n.btn-gold:active{ transform:translateY(1px); box-shadow:0 4px 14px rgba(0,0,0,.15); }\n#bukasForm fieldset{ border:1px solid var(--border); padding:6px 16px 22px; border-radius:var(--radius); margin:18px 0; background:var(--bg); box-shadow:0 1px 3px rgba(0,0,0,.04); }\n#bukasForm legend{ font-weight:700; color:var(--brand); padding:0 6px; font-size:1rem; }\n#bukasForm label br{ display:none; }\n#bukasForm label{\n  display:grid;\n  grid-template-rows:auto auto;\n  row-gap:2px;        \/* separaci\u00f3n muy peque\u00f1a entre texto e input *\/\n  align-content:start;\n  margin:.1rem 0;\n  font-size:.95rem;\n}\n#bukasForm small.hint{color:var(--muted);font-size:.85rem;margin-top:.25rem}\n#bukasForm input[type=\"text\"],\n#bukasForm input[type=\"email\"],\n#bukasForm input[type=\"number\"],\n#bukasForm input[type=\"date\"],\n#bukasForm input[type=\"time\"],\n#bukasForm select,\n#bukasForm textarea{ width:100%; height:var(--control-h); padding:8px 10px; font-size:15px; border:1px solid #cfd3d7; border-radius:6px; background:#fff; transition:border-color .18s, box-shadow .18s, background-color .18s; }\n#bukasForm textarea{min-height:110px;height:auto;resize:vertical;}\n#bukasForm input:focus,#bukasForm select:focus,#bukasForm textarea:focus{ outline:none;border-color:var(--brand-2);box-shadow:0 0 0 3px var(--focus-ring); }\n#bukasForm input[readonly]{background:#fafafa;color:#555}\n.fs-grid{ display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:var(--gap-y) var(--gap-x); }\n.span-12{grid-column:span 12}\n@media (min-width:900px){ .span-2{grid-column:span 2} .span-3{grid-column:span 3} .span-4{grid-column:span 4} .span-5{grid-column:span 5} .span-6{grid-column:span 6} .span-7{grid-column:span 7} .span-8{grid-column:span 8} .span-9{grid-column:span 9} .span-10{grid-column:span 10} .span-11{grid-column:span 11} .span-12{grid-column:span 12} }\n@media (max-width:899.98px){ .fs-grid>*{grid-column:1 \/ -1 !important} }\n.extras{ display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:var(--gap-y) var(--gap-x); padding:4px 0; }\n.extras label{ display:flex;align-items:center;gap:.65rem; min-height:56px;padding:10px 14px;border-radius:var(--radius); background:#fbfaf7;border:1px solid var(--border-2); transition:background .18s, box-shadow .18s, border-color .18s; }\n.extras input[type=\"checkbox\"]{width:22px;height:22px;flex:0 0 auto;accent-color:var(--brand);}\n.extras label:hover{background:#f7f4eb}\n.aviso-otros{ margin:.35rem 0 0;padding:.5rem .6rem;border:1px solid var(--warn-border); background:var(--warn-bg);border-radius:var(--radius-sm);font-size:.92rem; color:var(--brand); }\n.stepper {display:flex; gap:8px; align-items:center; margin:14px 0 18px; flex-wrap:wrap;}\n@media (min-width:860px){ .stepper{flex-wrap:nowrap;} }\n.stepper__item{ flex:1; display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius:12px; border:1px solid var(--border); background:#fff; color:#6b6b6b; font-weight:600; font-size:.9rem; }\n.stepper__item[data-state=\"done\"]{ background:#e8f7e8; border-color:#b4e2b4; color:#116611; }\n.stepper__item[data-state=\"active\"]{ background:linear-gradient(180deg, #c8b782 0%, var(--brand-2) 100%); color:#fff; border-color:transparent; box-shadow:0 3px 12px rgba(0,0,0,.12); }\n.stepper__num{ width:24px; height:24px; border-radius:999px; display:inline-grid; place-items:center; font-weight:800; border:1px solid #d8d8d8; background:#fff; color:#333; font-size:.9rem; }\n.stepper__item[data-state=\"active\"] .stepper__num{ background:#fff; color:#6b5f3b; border-color:transparent; }\n.form-block[hidden]{ display:none !important; }\n.form-block{\n  position:relative;\n  padding-bottom:90px;   \/* deja espacio para los botones pegados abajo *\/\n}\n\n.form-block[data-active=\"1\"]{ display:block; }\n.block-actions{\n  display:flex;\n  justify-content:flex-end;          \/* Siguiente\/Enviar siempre a la derecha *\/\n  gap:12px;\n  margin-top:10px;\n\n  position:sticky;                   \/* se pega al borde inferior de la ventana *\/\n  bottom:0;\n  z-index:10;\n\n  padding-top:12px;\n  padding-bottom:8px;\n  background:linear-gradient(to top, #f8fafc 70%, transparent);\n}\n\n\/* REGLA COM\u00daN: mismo ancho para los dos botones *\/\n.block-actions .btn-prev,\n.block-actions .btn-next{\n  display:inline-flex;\n  align-items:center;\n  justify-content:center;\n  text-align:center;\n\n  \/* ancho id\u00e9ntico en todos los idiomas *\/\n  width: 140px;         \/* ajusta a tu gusto: 130\u2013160px suele ir bien *\/\n  padding:10px 16px;    \/* mismo padding para los dos *\/\n  border-radius:12px;\n  font-weight:700;\n  box-sizing:border-box;\n}\n\n\/* Estilo espec\u00edfico de \u201cAnterior\u201d *\/\n.block-actions .btn-prev{\n  appearance:none;\n  border:1px solid var(--border);\n  background:#fff;\n  color:#333;\n  cursor:pointer;\n}\n\n\/* Estilo espec\u00edfico de \u201cSiguiente\u201d *\/\n.block-actions .btn-next{\n  appearance:none;\n  border:0;\n  cursor:pointer;\n  background: linear-gradient(180deg, #c8b782 0%, var(--brand-2) 100%);\n  color:#fff;\n  box-shadow:0 6px 18px rgba(0,0,0,.16);\n}\n.block-actions .btn-next[disabled]{\n  opacity:.6;\n  cursor:not-allowed;\n  box-shadow:none;\n  filter:grayscale(.2);\n}\n\n\n\n.block-actions .btn-next[disabled]{ opacity:.6; cursor:not-allowed; box-shadow:none; filter:grayscale(.2); }\n.precio-chip{ display:inline-flex; align-items:center; gap:.5rem; padding:.5rem .75rem; border:1px solid #e5e7eb; border-radius:999px; font-weight:600; font-size:14px; background:#f8fafc; color:#111827; }\n.precio-chip small{ font-weight:400; color:#6b7280 }\n.pay-info[hidden]{ display:none !important; }\n.bank-box{ background:#fbfaf7; border:1px solid var(--border-2); border-radius:var(--radius); padding:12px 14px; line-height:1.35; font-size:.95rem; }\n.toast-card{background:#fff;border:1px solid var(--border);border-radius:16px;padding:18px 20px;box-shadow:0 10px 40px rgba(0,0,0,.18);max-width:520px;width:calc(100% - 40px);text-align:center}\n.toast-card h3{margin:0 0 .35rem;color:var(--brand);font-weight:800}\n.toast-card p{margin:.25rem 0 .9rem;color:var(--muted)}\n.toast-actions{display:flex;gap:10px;justify-content:center}\n\n\/* P\u00e1gina de \u201cPedido enviado\u201d como overlay a pantalla completa *\/\n#toast{\n  position: fixed;\n  inset: 0;                        \/* top\/right\/bottom\/left: 0 *\/\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background: rgba(0,0,0,.45);     \/* oscurece el fondo *\/\n  z-index: 9999;\n}\n\n\/* Cuando tiene [hidden] no se ve *\/\n#toast[hidden]{\n  display: none !important;\n}\n\n\n\n\/* Chips\/segmented con icono *\/\n.chips { display:flex; flex-wrap:wrap; gap:8px; margin-top:6px; }\n.chips .chip{ appearance:none; border:1px solid var(--border); border-radius:9999px; padding:10px 14px; font:600 14px\/1.2 var(--font); color:#2b2b2b; background:#fff; cursor:pointer; display:inline-flex; gap:8px; align-items:center; transition:transform .05s, box-shadow .18s, background .18s, color .18s, border-color .18s; }\n.chips .chip:hover{ background:#f7f4eb; }\n.chips .chip:active{ transform:translateY(1px); }\n.chips .chip.is-active{ background: linear-gradient(180deg, #c8b782 0%, var(--brand-2) 100%); color:#fff; border-color: transparent; box-shadow:0 3px 12px rgba(0,0,0,.10); }\n.chip .ico{ width:20px; height:20px; display:inline-grid; place-items:center; filter:grayscale(1) brightness(0); opacity:.9; }\n.chips .chip.is-active .ico{ filter:none; opacity:1; }\n\/* === Override: chips de ancho uniforme === *\/\n.chips{\n  display:grid; \/* sustituye el flex anterior sin borrarlo *\/\n  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n  gap:8px;      \/* mantenemos el mismo espaciado *\/\n}\n.chips .chip{\n  width:100%;            \/* cada chip ocupa toda su columna *\/\n  justify-content:center;\/* centra icono + texto *\/\n}\n.hint-inline{ font-size:.86rem; color:#6b6b6b; margin-top:4px; }\n.badge{display:inline-block;padding:4px 8px;border-radius:9999px;font-weight:600;font-size:.8rem}\n.badge-ok{background:#e8f7e8;color:#116611;border:1px solid #b4e2b4}\n.badge-warn{background:#fff3cd;color:#7a5d00;border:1px solid #ffeeba}\n.badge-err{background:#ffe4e6;color:#7f1d1d;border:1px solid #fecdd3}\n.visually-hidden-select{ position:absolute !important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden; clip:rect(0 0 0 0);white-space:nowrap;border:0; }\n.yn-toggle{ display:inline-flex;align-items:center;gap:6px;padding:3px;border-radius:9999px; border:1px solid #e0dccb;background:#fff;width:fit-content;max-width:100%;justify-self:start; box-shadow:0 1px 0 rgba(0,0,0,.03); }\n.yn-toggle button{ appearance:none;border:0;border-radius:9999px;padding:8px 14px;min-width:72px;line-height:1; font:600 14px\/1 var(--font);background:transparent;color:#3b3b3b;cursor:pointer; transition:background .18s, color .18s, box-shadow .18s, transform .02s; }\n.yn-toggle button:hover{background:#f7f4eb}\n.yn-toggle button.is-active{ background:var(--brand);color:#fff;box-shadow:0 1px 0 rgba(0,0,0,.12),0 0 0 2px rgba(177,155,97,.25); }\n<\/style>\n<style>\n\/* Ensure PAC dropdown is clickable above UI overlays *\/\n.pac-container { z-index: 9999 !important; }\n\n\/* Place chip styling *\/\n.place-chip{\n  display: inline-flex;\n  align-items: center;\n  gap: .75rem;\n  max-width: min(560px, 100%);\n  padding: .625rem .75rem;\n  margin-block: .375rem;\n  background: var(--chip-bg);\n  border: 1px solid var(--chip-border);\n  border-radius: .75rem;\n  box-shadow: var(--chip-shadow);\n  transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease;\n}\n\n.place-chip:hover{\n  background: var(--chip-hover);\n  border-color: #d1d5db;\n  box-shadow: 0 2px 6px rgba(0,0,0,.06);\n}\n\n.place-chip__icon{\n  flex: 0 0 28px;\n  height: 28px; width: 28px;\n  border-radius: 50%;\n  display: grid; place-items: center;\n  background: #e8eefc;\n  color: var(--chip-accent);\n  font-size: 16px; line-height: 1;\n}\n\n.place-chip__body{\n  display: grid;\n  min-width: 0;\n}\n\n.place-chip__name{\n  color: var(--chip-name);\n  font-weight: 700;\n  font-size: .95rem;\n  line-height: 1.2;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.place-chip__addr{\n  color: var(--chip-addr);\n  font-size: .87rem;\n  line-height: 1.25;\n  display: -webkit-box;\n  -webkit-line-clamp: 2;\n  line-clamp: 2;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n}\n\n.place-chip__clear{\n  margin-left: .25rem;\n  margin-right: .125rem;\n  border: none;\n  background: transparent;\n  color: #9ca3af;\n  font-size: 18px;\n  line-height: 1;\n  border-radius: .5rem;\n  cursor: pointer;\n  padding: .25rem;\n}\n.place-chip__clear:hover{ color: #4b5563; background: rgba(0,0,0,.04); }\n\n@media (max-width: 480px){\n  .place-chip{ padding: .55rem .65rem; gap: .6rem; }\n  .place-chip__name{ font-size: .9rem; }\n  .place-chip__addr{ font-size: .82rem; -webkit-line-clamp: 2; line-clamp: 2; }\n}\n<\/style>\n<style>\n  \/* Oculta el bot\u00f3n y su modal SOLO en esta p\u00e1gina *\/\n  #floating-button,\n  #custom-modal { display: none !important; }\n<\/style>\n\n<style id=\"pago-css\">\n\/* M\u00e1s espacio entre Comentarios y Forma de pago *\/\n#comentarios {\n  margin-bottom: 36px !important;\n}\n#comentarios + #pago {\n  margin-top: 32px !important;\n}\n\n\/* Cabecera de pago: cada l\u00ednea en su sitio, alineada a la izquierda *\/\n#pago .pago-top{\n  margin:4px 0;\n  grid-column: 1 \/ -1;\n}\n\n#pago .pago-top label{\n  display:block !important; \/* cada label ocupa toda la fila *\/\n  margin:0;\n  padding:0;\n}\n\n#pago .pago-top label > span{\n  display:inline-block;\n  margin-right:8px;\n  white-space:normal;       \/* \u201cServicios contratados:\u201d puede partirse en 2 l\u00edneas si hace falta *\/\n}\n\n#pago .precio-chip{\n  display:inline-flex;\n  align-items:center;\n  justify-content:center;\n  white-space:nowrap;\n}\n\n\/* Botones de forma de pago *\/\n.pago-buttons{\n  display:flex;\n  flex-wrap:wrap;\n  gap:.5rem;\n  align-items:center;\n  margin:.5rem 0 0;\n}\n\n.pago-btn{\n  display:inline-flex !important;\n  align-items:center;\n  justify-content:center;\n  flex:1 1 0;          \/* \ud83d\udc49 todos crecen igual *\/\n  min-width: 190px;    \/* ajusta 180\u2013210px seg\u00fan veas *\/\n  max-width: 100%;\n  padding:.45rem .9rem;\n  line-height:1.2;\n  font:inherit;\n  text-align:center;   \/* centra el texto si salta a dos l\u00edneas *\/\n  white-space:normal;  \/* permite que el texto se parta *\/\n  cursor:pointer;\n  border:1px solid #c9c6b8;\n  border-radius:999px;\n  background:#f6f3e8;\n  box-shadow:none;\n}\n\n.pago-btn:focus{\n  outline:2px solid #b7ae8c;\n  outline-offset:2px;\n}\n.pago-btn.active,\n.pago-btn[aria-checked=\"true\"]{\n  background:#e0d9bf;\n  border-color:#b7ae8c;\n}\n\n\/* Caja bancaria *\/\n.bank-box{\n  margin-top:.6rem;\n  border:1px solid #e2e2e2;\n  padding:.6rem .8rem;\n  border-radius:.5rem;\n  background:#faf9f6;\n}\n.muted{\n  color:#6b6b6b;\n  font-size:.85em;\n  margin-top:.25rem;\n}\n\/* Comentarios: textarea m\u00e1s bajo *\/\n#comentarios textarea {\n  min-height: 80px !important; \/* antes era m\u00e1s alto, prueba con 80px o 70px *\/\n  height: auto;\n}\n\n<\/style>\n\n\n\n\n\n<style id=\"ux-css\">\n\/* Foco visible coherente *\/\n:where(button, [type=\"button\"], [type=\"submit\"], select, input, textarea):focus-visible{\noutline: 2px solid var(--brand-2);\noutline-offset: 2px;\nbox-shadow: 0 0 0 3px var(--focus-ring);\n}\n\n\n\/* Errores inline *\/\n.field-error{ color:#7f1d1d; font-size:.85rem; margin-top:.35rem }\n.is-invalid{ border-color:#ef4444 !important; box-shadow:0 0 0 3px rgba(239,68,68,.15) !important; }\n.address-candidates{\n  margin-top:6px;\n  padding:8px 10px;\n  border:1px solid var(--border-2);\n  border-radius:var(--radius-sm);\n  background:#fbfaf7;\n  display:grid;\n  gap:6px;\n}\n.address-candidates__title{\n  margin:0;\n  font-size:.85rem;\n  font-weight:600;\n  color:var(--brand);\n}\n.address-candidates__option{\n  appearance:none;\n  border:1px solid var(--border-2);\n  background:#fff;\n  border-radius:8px;\n  padding:8px 10px;\n  text-align:left;\n  font:500 .9rem\/1.3 var(--font);\n  cursor:pointer;\n  transition:border-color .18s, box-shadow .18s, background .18s;\n}\n.address-candidates__option:hover,\n.address-candidates__option:focus-visible{\n  border-color:var(--brand-2);\n  box-shadow:0 0 0 2px rgba(177,155,97,.18);\n  background:#fffdf5;\n}\n.address-candidates__message{\n  font-size:.85rem;\n  color:var(--brand);\n  margin:0;\n}\n\n\n\/* Stepper pegajoso en pantallas altas *\/\n@media (min-width: 900px){\n.stepper{ position: sticky; top: 0; z-index: 5; background: #fff; padding-top: 8px; }\n}\n\n\n\/* Bot\u00f3n enviar consistente *\/\nbutton[type=\"submit\"]{\nappearance:none; border:0; border-radius:12px; padding:12px 24px; font-weight:800; cursor:pointer;\nbackground: linear-gradient(180deg, #c8b782 0%, var(--brand-2) 100%); color:#fff; box-shadow:0 6px 18px rgba(0,0,0,.16);\n}\nbutton[type=\"submit\"][disabled]{ opacity:.6; cursor:not-allowed; box-shadow:none; filter:grayscale(.2) }\n<\/style>\n\n<style id=\"stepper-compact\">\n  \/* Botones del stepper m\u00e1s angostos y con texto centrado *\/\n  .stepper { gap: 6px; } \/* menos separaci\u00f3n entre botones *\/\n\n  .stepper__item{\n    flex: 1 1 0 !important;  \/* todos crecen por igual *\/\n    min-width: 0;            \/* evita desbordes *\/\n    justify-content: center; \/* centra el texto *\/\n    text-align: center;\n    white-space: nowrap;   \/* que no parta el texto en 2 l\u00edneas *\/\n  }\n<\/style>\n\n<style>\n  \/* Oculta cualquier contenedor de chips *\/\n  .chips{ display:none !important; }\n<\/style>\n\n<style>\n  \/* Que el output se vea como un input normal *\/\n  #s1Price,\n  #s2Price,\n  #s3Price{\n    display:flex;\n    align-items:center;\n    justify-content:flex-end; \/* valor a la derecha como en un input *\/\n    height: var(--control-h);\n    padding: 8px 10px;\n    border: 1px solid #cfd3d7;\n    border-radius: 6px;\n    background:#fff;\n    font-size: 15px;\n    font-weight: 600;\n    color: var(--ink);\n    box-shadow: none;\n  }\n\n  \/* Texto \u201c\u2014\u201d o importe dentro *\/\n  #s1Price [data-c=\"EUR\"],\n  #s2Price [data-c=\"EUR\"],\n  #s3Price [data-c=\"EUR\"]{\n    margin-left:auto; \/* empuja a la derecha *\/\n  }\n\n  \/* Mant\u00e9n el ocultado cuando [hidden] *\/\n  #s1Price[hidden],\n  #s2Price[hidden],\n  #s3Price[hidden]{ display:none !important; }\n\n  \/* Evita que la apariencia \"pill\" de .precio-chip del resto afecte a los de servicio *\/\n  #s1Price, #s2Price, #s3Price { border-radius:6px !important; background:#fff !important; }\n<\/style>\n\n<style id=\"toggles-align\">\n\/* Hacer que estas filas ocupen todo el ancho y que el S\u00ed\/No quede pegado al margen derecho,\n   igual que el bot\u00f3n Siguiente *\/\n\n#bukasForm .fs-grid label:has(#s1AddContact),\n#bukasForm .fs-grid label:has(#s2AddContact),\n#bukasForm .fs-grid label:has(#s3AddContact),\n#bukasForm .fs-grid label:has(#t2),\n#bukasForm .fs-grid label:has(#t3){\n  grid-column: 1 \/ -1 !important; \/* de la columna 1 a la 12 = ancho completo *\/\n  display:flex;\n  align-items:center;\n  justify-content:space-between;\n  gap:8px;\n}\n\n\/* Texto de la pregunta a la izquierda *\/\n#bukasForm .fs-grid label:has(#s1AddContact) span,\n#bukasForm .fs-grid label:has(#s2AddContact) span,\n#bukasForm .fs-grid label:has(#s3AddContact) span,\n#bukasForm .fs-grid label:has(#t2) span,\n#bukasForm .fs-grid label:has(#t3) span{\n  flex:1;\n}\n\n\/* Quitamos el salto de l\u00ednea entre texto y control *\/\n#bukasForm .fs-grid label:has(#s1AddContact) br,\n#bukasForm .fs-grid label:has(#s2AddContact) br,\n#bukasForm .fs-grid label:has(#s3AddContact) br,\n#bukasForm .fs-grid label:has(#t2) br,\n#bukasForm .fs-grid label:has(#t3) br{\n  display:none;\n}\n\n\/* El select \/ toggle se empuja a la derecha dentro de la fila de 12 columnas *\/\n#bukasForm .fs-grid label:has(#s1AddContact) select,\n#bukasForm .fs-grid label:has(#s2AddContact) select,\n#bukasForm .fs-grid label:has(#s3AddContact) select,\n#bukasForm .fs-grid label:has(#t2) select,\n#bukasForm .fs-grid label:has(#t3) select,\n#bukasForm .fs-grid label:has(#s1AddContact) .yn-toggle,\n#bukasForm .fs-grid label:has(#s2AddContact) .yn-toggle,\n#bukasForm .fs-grid label:has(#s3AddContact) .yn-toggle,\n#bukasForm .fs-grid label:has(#t2) .yn-toggle,\n#bukasForm .fs-grid label:has(#t3) .yn-toggle{\n  margin-left:auto;\n}\n<\/style>\n\n\n\n<style id=\"mobile-stepper-fix\">\n\/* ===== Mejoras m\u00f3vil: stepper + entradas ===== *\/\n@media (max-width: 480px){\n\n  \/* Stepper en una sola fila con scroll suave *\/\n  .stepper{\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n    gap: 6px;                 \/* m\u00e1s compacto *\/\n    padding-bottom: 6px;\n    scroll-snap-type: x proximity;\n  }\n  .stepper__item{\n    flex: 0 0 auto;           \/* ya no ocupa todo el ancho *\/\n    min-width: 118px;         \/* cada \u201cp\u00edldora\u201d cabe y scrollea *\/\n    padding: 8px 10px;\n    font-size: .9rem;\n    white-space: nowrap;\n    scroll-snap-align: start;\n  }\n  .stepper__item[data-state=\"active\"]{\n    box-shadow: 0 2px 10px rgba(0,0,0,.12);\n  }\n  \/* Oculta el c\u00edrculo num\u00e9rico para ganar espacio *\/\n  .stepper__num{ display:none; }\n\n  \/* Botones S\u00ed\/No: c\u00f3modos al toque, pero m\u00e1s compactos *\/\n  .yn-toggle{ gap:4px; }\n  .yn-toggle button{\n    min-width: 64px;\n    padding: 10px 12px;\n    font-size: 14px;\n  }\n\n\n\n  \/* Inputs a 16px para evitar zoom en iOS\/Android *\/\n  #bukasForm input[type=\"text\"],\n  #bukasForm input[type=\"email\"],\n  #bukasForm input[type=\"number\"],\n  #bukasForm input[type=\"date\"],\n  #bukasForm input[type=\"time\"],\n  #bukasForm select,\n  #bukasForm textarea{\n    font-size: 16px;\n  }\n\n  \/* Ajuste ligero de paddings generales *\/\n  #bukasForm fieldset{ padding:14px 12px 18px; }\n  .block-actions{ gap:8px; }\n}\n\n\/* iOS: refuerzo del font-size 16px para inputs (evita zoom al foco) *\/\n@supports (-webkit-touch-callout: none){\n  #bukasForm input,\n  #bukasForm select,\n  #bukasForm textarea{ font-size: 16px; }\n}\n\n.lang-switcher{\n  display:grid;\n  grid-template-columns: repeat(2, 1fr);\n  gap:6px;\n}\n\n.lang-switcher button{\n  border:1px solid var(--border-2);\n  background:#fff;\n  border-radius:999px;\n  padding:4px 10px;\n  font-size:13px;\n  cursor:pointer;\n  width:100%;\n\n  display:flex;\n  align-items:center;\n  justify-content:center;\n  gap:6px;\n}\n\n.lang-switcher button:hover{\n  filter:brightness(1.03);\n}\n\n.lang-switcher button.active{\n  background: var(--brand-2);\n  color: #fff;\n  border-color: var(--brand-2);\n  font-weight: 700;\n}\n\n\/* base bandera *\/\n.flag{\n  width:18px;\n  height:12px;\n  border-radius:2px;\n  box-shadow:0 0 0 1px rgba(0,0,0,.12);\n  flex-shrink:0;\n}\n\n\/* Espa\u00f1a *\/\n.flag-es{\n  background:\n    linear-gradient(to bottom,\n      #aa151b 0 33.33%,\n      #f1bf00 33.33% 66.66%,\n      #aa151b 66.66% 100%);\n}\n\n\/* Francia *\/\n.flag-fr{\n  background:\n    linear-gradient(to right,\n      #0055a4 0 33.33%,\n      #ffffff 33.33% 66.66%,\n      #ef4135 66.66% 100%);\n}\n\n\/* Catalu\u00f1a (senyera simple) *\/\n.flag-cat{\n  background:\n    linear-gradient(to bottom,\n      #ffcc00 0 20%,\n      #c8102e 20% 40%,\n      #ffcc00 40% 60%,\n      #c8102e 60% 80%,\n      #ffcc00 80% 100%);\n}\n\n\/* UK simplificada para EN *\/\n.flag-gb{\n  background:\n    \/* barra horizontal roja *\/\n    linear-gradient(to bottom,\n      transparent 0 40%,\n      #cf142b 40% 60%,\n      transparent 60% 100%),\n    \/* barra vertical roja *\/\n    linear-gradient(to right,\n      transparent 0 40%,\n      #cf142b 40% 60%,\n      transparent 60% 100%),\n    \/* fondo blanco *\/\n    #ffffff;\n}\n\n.lang-switcher .lang-code{\n  text-transform:uppercase;\n}\n\n\n\n\n\n\n.field-error{ \n  color:#7f1d1d; \n  font-size:.82rem; \n  line-height:1.2;\n  margin-top:.35rem; \n}\n.is-invalid{ border-color:#ef4444 !important; box-shadow:0 0 0 3px rgba(239,68,68,.15) !important; }\n\n\/* Soft highlight for empty required fields *\/\n.req-empty{\n  background-color:#e0f2fe !important; \/* soft blue *\/\n  transition:background-color .15s ease;\n}\n.req-empty:focus{\n  outline:0;\n  box-shadow:0 0 0 2px rgba(59,130,246,.2);\n}\n\n\/* Keep layout tidy if the input uses full-width styles *\/\n.has-field-error { display:flex; flex-direction:column; }\n\n\n<\/style>\n\n<style>\n\/* ============================================\n   M\u00d3VIL APP-LIKE: Experiencia tipo app (PASOS 1-3)\n   ============================================ *\/\n@media (max-width: 768px) {\n  \/* PASO 1: Pantalla completa *\/\n  body {\n    margin: 0 !important;\n    padding: 0 !important;\n    padding-top: 100px !important; \/* espacio extra para app bar fija + stepper *\/\n    background: #fcfcfa;\n    min-height: 100vh;\n  }\n\n  #bukasForm {\n    max-width: 100% !important;\n    margin: 0 !important;\n    padding: 0 !important;\n    width: 100%;\n  }\n\n  .form-head {\n    max-width: 100% !important;\n    margin: 0 !important;\n    padding: 0 !important;\n    width: 100%;\n    box-sizing: border-box;\n  }\n\n  #bukasForm fieldset {\n    margin-left: 0 !important;\n    margin-right: 0 !important;\n    margin-top: 10px;\n    margin-bottom: 10px;\n    border-left: none !important;\n    border-right: none !important;\n    border-radius: 12px !important;\n    padding: 14px 14px 18px !important;\n    background: #f9fafb !important;\n    border: 1px solid #e5e7eb !important;\n    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06) !important;\n  }\n\n  .form-block {\n    padding-left: 0 !important;\n    padding-right: 0 !important;\n    padding-bottom: 80px !important; \/* espacio para barra inferior *\/\n    padding-top: 4px;\n  }\n\n  \/* PASO 2: App bar fija *\/\n  .form-head {\n    position: fixed !important;\n    top: 0 !important;\n    left: 0 !important;\n    right: 0 !important;\n    z-index: 100 !important;\n    background: #fff !important;\n    backdrop-filter: blur(10px);\n    -webkit-backdrop-filter: blur(10px);\n    border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important;\n    padding: 12px 16px !important;\n    display: flex !important;\n    align-items: center !important;\n    justify-content: space-between !important;\n    gap: 12px;\n  }\n\n  .stepper {\n    margin-top: 4px !important; \/* asegura separaci\u00f3n respecto al cabezal fijo *\/\n  }\n\n  .form-head h2 {\n    font-size: 1.2rem !important;\n    margin: 0 !important;\n    flex: 1;\n  }\n\n  .lang-switcher {\n    gap: 4px !important;\n  }\n\n  .lang-switcher button {\n    padding: 6px 10px !important;\n    font-size: 12px !important;\n  }\n\n  \/* PASO 5: Inputs y textos con look de app *\/\n  #bukasForm legend {\n    font-size: 0.95rem !important;\n    font-weight: 700 !important;\n    color: #111827 !important;\n    padding: 0 4px !important;\n    margin-bottom: 4px !important;\n  }\n\n  #bukasForm label {\n    font-size: 0.9rem !important;\n    margin: 0.15rem 0 !important;\n    row-gap: 1px !important;\n    grid-template-rows: auto auto !important;\n  }\n\n  \/* Reducir espacio vertical entre filas de la grid en m\u00f3vil *\/\n  .fs-grid {\n    row-gap: 6px !important;\n  }\n\n  #bukasForm input[type=\"text\"],\n  #bukasForm input[type=\"email\"],\n  #bukasForm input[type=\"number\"],\n  #bukasForm input[type=\"date\"],\n  #bukasForm input[type=\"time\"],\n  #bukasForm select,\n  #bukasForm textarea {\n    height: 44px;\n    min-height: 44px;\n    padding: 10px 12px;\n    border-radius: 10px;\n    border: 1px solid #d1d5db;\n    background: #ffffff;\n    font-size: 15px;\n    line-height: 1.2;\n    box-shadow: 0 1px 1px rgba(15,23,42,0.03);\n    transition:\n      border-color 0.15s ease,\n      box-shadow 0.15s ease,\n      background-color 0.15s ease,\n      transform 0.08s ease;\n  }\n\n  #bukasForm textarea {\n    min-height: 90px;\n    height: auto;\n  }\n\n  #bukasForm input:focus,\n  #bukasForm select:focus,\n  #bukasForm textarea:focus {\n    outline: none;\n    border-color: #c8b782;\n    box-shadow: 0 0 0 1px rgba(200,183,130,0.9), 0 0 0 4px rgba(200,183,130,0.25);\n    background: #ffffff;\n    transform: translateY(-1px);\n  }\n\n  #bukasForm small.hint,\n  .hint,\n  .muted {\n    font-size: 0.8rem !important;\n    color: #6b7280 !important;\n  }\n\n  \/* Errores: menos espacio vertical debajo del campo *\/\n  .field-error {\n    margin-top: 2px !important;\n    font-size: 0.78rem !important;\n    line-height: 1.2 !important;\n  }\n\n  \/* PASO 3: Stepper tipo tabs *\/\n  .stepper {\n    display: flex !important;\n    flex-wrap: nowrap !important;\n    overflow-x: auto !important;\n    overflow-y: hidden !important;\n    -webkit-overflow-scrolling: touch;\n    scroll-snap-type: x proximity;\n    gap: 6px !important;\n    margin: 8px 0 12px !important;\n    padding: 0 16px 8px !important;\n    scrollbar-width: none; \/* Firefox *\/\n    -ms-overflow-style: none; \/* IE\/Edge *\/\n  }\n\n  .stepper::-webkit-scrollbar {\n    display: none; \/* Chrome\/Safari *\/\n  }\n\n  .stepper__item {\n    flex: 0 0 auto !important;\n    min-width: 100px !important;\n    max-width: 140px !important;\n    padding: 10px 14px !important;\n    border-radius: 20px !important;\n    font-size: 0.875rem !important;\n    font-weight: 600 !important;\n    white-space: nowrap !important;\n    scroll-snap-align: start;\n    transition: all 0.2s ease;\n    border: 1.5px solid #e5e7eb !important;\n    background: #fff !important;\n    color: #6b7280 !important;\n  }\n\n  .stepper__item[data-state=\"active\"] {\n    background: linear-gradient(180deg, #c8b782 0%, var(--brand-2) 100%) !important;\n    color: #fff !important;\n    border-color: transparent !important;\n    box-shadow: 0 2px 8px rgba(107, 95, 59, 0.25) !important;\n    font-weight: 700 !important;\n  }\n\n  .stepper__item[data-state=\"done\"] {\n    background: #f0f9ff !important;\n    border-color: #bae6fd !important;\n    color: #0369a1 !important;\n  }\n\n  .stepper__num {\n    display: none !important;\n  }\n\n  \/* PASO 4: Barra inferior fija tipo app *\/\n  .block-actions {\n    position: fixed !important;\n    bottom: 0 !important;\n    left: 0 !important;\n    right: 0 !important;\n    z-index: 100 !important;\n    background: #fff !important;\n    backdrop-filter: blur(10px);\n    -webkit-backdrop-filter: blur(10px);\n    border-top: 1px solid rgba(0, 0, 0, 0.08) !important;\n    box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05) !important;\n    padding: 12px 16px !important;\n    margin: 0 !important;\n    display: flex !important;\n    justify-content: space-between !important;\n    gap: 12px !important;\n    box-sizing: border-box;\n    \/* Safe area para iPhone con notch *\/\n    padding-bottom: max(12px, env(safe-area-inset-bottom)) !important;\n  }\n\n  \/* Botones m\u00e1s grandes y t\u00e1ctiles *\/\n  .block-actions .btn-prev,\n  .block-actions .btn-next {\n    flex: 1 !important;\n    min-height: 48px !important;\n    padding: 14px 20px !important;\n    font-size: 16px !important;\n    font-weight: 700 !important;\n    border-radius: 12px !important;\n    width: auto !important;\n    max-width: none !important;\n    box-sizing: border-box;\n    transition: all 0.2s ease;\n  }\n\n  \/* Bot\u00f3n \"Anterior\" m\u00e1s visible *\/\n  .block-actions .btn-prev {\n    background: #f3f4f6 !important;\n    border: 1.5px solid #d1d5db !important;\n    color: #374151 !important;\n  }\n\n  .block-actions .btn-prev:active {\n    background: #e5e7eb !important;\n    transform: scale(0.98);\n  }\n\n  \/* Bot\u00f3n \"Siguiente\/Enviar\" destacado *\/\n  .block-actions .btn-next {\n    background: linear-gradient(180deg, #c8b782 0%, var(--brand-2) 100%) !important;\n    color: #fff !important;\n    border: none !important;\n    box-shadow: 0 4px 12px rgba(107, 95, 59, 0.3) !important;\n  }\n\n  .block-actions .btn-next:active {\n    transform: scale(0.98);\n    box-shadow: 0 2px 8px rgba(107, 95, 59, 0.25) !important;\n  }\n\n  .block-actions .btn-next[disabled] {\n    opacity: 0.5 !important;\n    box-shadow: none !important;\n  }\n\n  \/* Asegurar espacio para la barra inferior en el contenido *\/\n  .form-block {\n    padding-bottom: 80px !important;\n  }\n\n  \/* Ocultar footer en m\u00f3vil *\/\n  .footer-desktop-only {\n    display: none !important;\n  }\n}\n<\/style>\n\n<\/head>\n<body>\n<div class=\"form-head\">\n  <h2 data-i18n=\"app.title\">Reservas aaa<\/h2>\n\n<div class=\"lang-switcher\">\n  <button type=\"button\" data-lang=\"es\">\n    <span class=\"flag flag-es\" aria-hidden=\"true\"><\/span>\n    <span class=\"lang-code\">ES<\/span>\n  <\/button>\n\n  <button type=\"button\" data-lang=\"en\">\n    <span class=\"flag flag-gb\" aria-hidden=\"true\"><\/span>\n    <span class=\"lang-code\">EN<\/span>\n  <\/button>\n\n  <button type=\"button\" data-lang=\"ca\">\n    <span class=\"flag flag-cat\" aria-hidden=\"true\"><\/span>\n    <span class=\"lang-code\">CA<\/span>\n  <\/button>\n\n  <button type=\"button\" data-lang=\"fr\">\n    <span class=\"flag flag-fr\" aria-hidden=\"true\"><\/span>\n    <span class=\"lang-code\">FR<\/span>\n  <\/button>\n<\/div>\n\n\n  <button id=\"btnInstall\" class=\"btn-gold\" hidden data-i18n=\"btn.install\">\n    Descarga nuestra App\n  <\/button>\n<\/div>\n<form id=\"bukasForm\" method=\"post\" novalidate action=\"\">\n  <!-- Stepper -->\n <nav class=\"stepper\" data-i18n-aria-label=\"step.nav.label\" aria-label=\"\">\n  <div class=\"stepper__item\" data-step=\"1\" data-state=\"active\" data-i18n=\"step.1\">Cliente<\/div>\n  <div class=\"stepper__item\" data-step=\"2\" data-state=\"idle\"   data-i18n=\"step.2\">Servicio 1<\/div>\n  <div class=\"stepper__item\" data-step=\"3\" data-state=\"idle\"   data-i18n=\"step.3\">Servicio 2<\/div>\n  <div class=\"stepper__item\" data-step=\"4\" data-state=\"idle\"   data-i18n=\"step.4\">Servicio 3<\/div>\n  <div class=\"stepper__item\" data-step=\"5\" data-state=\"idle\"   data-i18n=\"step.5\">Final<\/div>\n<\/nav>\n\n  <!-- BLOQUE 1: Cliente -->\n  <section class=\"form-block\" data-block=\"1\" data-active=\"1\">\n<fieldset class=\"fs-grid\" id=\"bloqueCliente\">\n  <legend data-i18n=\"legend.cliente\">Datos del cliente<\/legend>\n\n<label class=\"span-4\">\n  <span data-i18n=\"label.cliNum\">N\u00ba de cliente (opcional)<\/span><br>\n  <input\n    type=\"text\"\n    name=\"N\u00famero de cliente\"\n    id=\"cliNum\"\n    autocomplete=\"off\"\n    data-i18n-placeholder=\"ph.cliNum\"\n    placeholder=\"Ej: 120045\">\n<\/label>\n\n\n<label class=\"span-8\">\n  <span data-i18n=\"label.taxId\">CIF, VAT o Pasaporte*<\/span><br>\n  <input\n    required\n type=\"text\"\n    name=\"N\u00famero de Identificaci\u00f3n Fiscal (CIF, VAT o Pasaporte)\"\n    id=\"cliTax\"\n    autocomplete=\"off\"\n    data-i18n-placeholder=\"ph.taxId\"\n    placeholder=\"ESB12345678 \/ GB123456789 \/ Pasaporte\">\n<\/label>\n\n\n  <div class=\"span-12\" id=\"clienteStatus\" aria-live=\"polite\"><\/div>\n\n  <label class=\"span-6\">\n    <span data-i18n=\"label.cliName\">Nombre del cliente*<\/span><br>\n    <input\n     required\n type=\"text\"\n     name=\"Nombre del cliente\"\n     id=\"cliName\"\n     autocomplete=\"name\">\n  <\/label>\n\n\n<label class=\"span-6\">\n  <span data-i18n=\"label.cliPhone\">Tel\u00e9fono*<\/span><br>\n  <input\n    required\n type=\"text\"\n    name=\"Tel\u00e9fono\"\n    id=\"cliPhone\"\n    inputmode=\"tel\"\n    autocomplete=\"tel\">\n<\/label>\n\n\n<label class=\"span-12\">\n  <span data-i18n=\"label.cliBilling\">Domicilio de facturaci\u00f3n*<\/span><br>\n  <input\n    required\n type=\"text\"\n    name=\"Domicilio de Facturaci\u00f3n\"\n    id=\"cliBilling\"\n    autocomplete=\"street-address\">\n<\/label>\n\n\n <label class=\"span-12\">\n  <span data-i18n=\"label.cliEmail\">Correo electr\u00f3nico*<\/span><br>\n  <input\n    required\n type=\"email\"\n    name=\"Correo electr\u00f3nico\"\n    id=\"cliEmail\"\n    placeholder=\"email@dominio.com\"\n    autocomplete=\"email\">\n  <small data-i18n=\"cliente.email_hint\"><\/small>\n<\/label>\n\n<\/fieldset>\n\n    <div class=\"block-actions\">\n      <button type=\"button\" class=\"btn-prev\" data-prev disabled data-i18n=\"btn.prev\">Anterior<\/button>\n      <button type=\"button\" class=\"btn-next\" data-next data-i18n=\"btn.next\">Siguiente<\/button>\n    <\/div>\n  <\/section>\n <!-- BLOQUE 2: Servicio 1 (separado en 2 fieldsets) -->\n<!-- BLOQUE 2: Servicio 1 (separado en 2 fieldsets) -->\n<section class=\"form-block\" data-block=\"2\" hidden>\n  <!-- Bloque A: campos principales -->\n  <fieldset class=\"fs-grid\" id=\"s1-core\">\n  <legend data-i18n=\"legend.serv1\">Servicio 1<\/legend>\n\n  <label class=\"span-4\">\n    <span data-i18n=\"label.lugar\">Lugar*<\/span><br>\n    <select id=\"s1Lugar\" name=\"Lugar\" required>\n      <option value=\"Barcelona\">Barcelona<\/option>\n      <option value=\"Madrid\">Madrid<\/option>\n    <\/select>\n  <\/label>\n\n  <label class=\"span-4\">\n    <span data-i18n=\"label.vehiculo\">Veh\u00edculo*<\/span><br>\n    <select id=\"s1Vehiculo\" name=\"Veh\u00edculo\" required>\n  <option value=\"Coche\"      data-i18n=\"vehicle.turismo\"><\/option>\n  <option value=\"Van\"        data-i18n=\"vehicle.van\"><\/option>\n  <option value=\"Minibus 15\" data-i18n=\"vehicle.minibus15\"><\/option>\n  <option value=\"Minibus 25\" data-i18n=\"vehicle.minibus25\"><\/option>\n  <option value=\"Bus 50\"     data-i18n=\"vehicle.bus\"><\/option>\n<\/select>\n    <div class=\"muted\" id=\"s1PaxHint\" aria-live=\"polite\"><\/div>\n  <\/label>\n\n  <label class=\"span-4\">\n    <span data-i18n=\"label.servicio\">Servicio*<\/span><br>\n    <select id=\"s1Servicio\" name=\"Servicio\" required>\n      <option value=\"\">\u2014<\/option>\n      <!-- Se rellena por JS seg\u00fan lugar\/veh\u00edculo -->\n    <\/select>\n  <\/label>\n<\/fieldset>\n\n\n  <!-- Bloque B: resto de campos -->\n  <!-- Bloque B: resto de campos -->\n<fieldset class=\"fs-grid\" id=\"s1\">\n  <legend data-i18n=\"legend.serv1det\">Detalles del servicio 1<\/legend>\n\n  <!-- Fila 1 -->\n  <label class=\"span-6\">\n    <span data-i18n=\"label.pickupDate\">Fecha de recogida*<\/span><br>\n    <input required type=\"date\" id=\"s1Date\" name=\"Fecha de recogida\">\n  <\/label>\n  <label class=\"span-6\">\n    <span data-i18n=\"label.pickupTime\">Hora de recogida*<\/span><br>\n    <input required type=\"time\" id=\"s1Time\" name=\"Hora de recogida\">\n  <\/label>\n\n  <!-- Fila 2 -->\n  <label class=\"span-6\">\n    <span data-i18n=\"label.paxCount\">Cantidad de pasajeros*<\/span><br>\n    <input required\n type=\"number\" min=\"1\" step=\"1\"\n           id=\"s1Pax\"\n           name=\"Cantidad de pasajeros?\"\n           inputmode=\"numeric\"\n           placeholder=\"M\u00e1x. \u2014\">\n  <\/label>\n  <label class=\"span-6\">\n    <span data-i18n=\"label.flightNum\">Nro.Vuelo\/Tren<\/span><br>\n    <input type=\"text\"\n           id=\"s1Flight\"\n           name=\"Introduzca el n\u00famero de vuelo (si corresponde)\"\n           data-i18n-placeholder=\"ph.flight\"\n           placeholder=\"Ej.: IB1234 \/ AVE 03192\">\n  <\/label>\n\n  <!-- Fila 3 -->\n  <label class=\"span-6\">\n    <span data-i18n=\"label.pickupPoint\">Punto de recogida*<\/span><br>\n    <input required\n type=\"text\"\n           id=\"s1Pickup\"\n           name=\"Punto de recogida\"\n           autocomplete=\"off\"\n           data-i18n-placeholder=\"ph.pickup\"\n           placeholder=\"Hotel, calle, aeropuerto\u2026\">\n    <input type=\"hidden\"\n           id=\"s1PickupExact\"\n           name=\"Direcci\u00f3n exacta de recogida\">\n    <input type=\"hidden\" id=\"s1PickupName\" name=\"Nombre recogida\">\n    <input type=\"hidden\" id=\"s1PickupLat\" name=\"Latitud recogida\">\n    <input type=\"hidden\" id=\"s1PickupLng\" name=\"Longitud recogida\">\n    <input type=\"hidden\" id=\"s1PickupPlaceId\" name=\"PlaceId recogida\">\n  <\/label>\n  <label class=\"span-6\" style=\"display:none;\">\n    <!-- Hidden label for layout compatibility -->\n  <\/label>\n\n  <!-- Fila 4 -->\n  <label class=\"span-6\">\n    <span data-i18n=\"label.dropoffPoint\">Punto de destino*<\/span><br>\n    <input required\n type=\"text\"\n           id=\"s1Dropoff\"\n           name=\"Punto de destino\"\n           autocomplete=\"off\"\n           data-i18n-placeholder=\"ph.dropoff\"\n           placeholder=\"Hotel, calle, aeropuerto\u2026\">\n    <input type=\"hidden\"\n           id=\"s1DropoffExact\"\n           name=\"Direcci\u00f3n exacta de destino\">\n    <input type=\"hidden\" id=\"s1DropoffName\" name=\"Nombre destino\">\n    <input type=\"hidden\" id=\"s1DropoffLat\" name=\"Latitud destino\">\n    <input type=\"hidden\" id=\"s1DropoffLng\" name=\"Longitud destino\">\n    <input type=\"hidden\" id=\"s1DropoffPlaceId\" name=\"PlaceId destino\">\n  <\/label>\n  <label class=\"span-6\" style=\"display:none;\">\n    <!-- Hidden label for layout compatibility -->\n  <\/label>\n\n  <!-- Fila 5 -->\n  <label class=\"span-6\">\n    <span data-i18n=\"label.distance\">Distancia (km)<\/span><br>\n    <input type=\"text\"\n           id=\"s1Distance\"\n           name=\"Distancia (km)\"\n           readonly\n placeholder=\"\u2014\">\n  <\/label>\n  <label class=\"span-6\">\n    <span data-i18n=\"label.price\">Precio (Iva Incluido)<\/span><br>\n    <output id=\"s1Price\" hidden aria-live=\"polite\">\n      <span data-c=\"EUR\">\u2014<\/span>\n    <\/output>\n  <\/label>\n\n  <!-- Extras + otros + contacto adicional -->\n<div class=\"span-12\" id=\"s1ExtrasWrap\">\n  <div class=\"extras\"\n       data-for=\"1\"\n       role=\"group\"\n       data-i18n-aria-label=\"label.extras\"\n       aria-label=\"Adicionales Servicio 1\">\n    <label>\n      <input type=\"checkbox\" id=\"s1CartelNombre\" name=\"cartel_nombre\">\n      <span data-i18n=\"extras.cartel\">Cartel con Nombre<\/span>\n      <div class=\"extra-comment\" data-i18n=\"extras.cartel.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique nombre en el siguiente recuadro<\/div>\n      <input type=\"text\" id=\"s1CartelNombreDetalle\" name=\"cartel_nombre_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n    <\/label>\n    <label>\n      <input type=\"checkbox\" id=\"s1AsientoInfantil\" name=\"asiento_infantil\">\n      <span data-i18n=\"extras.asiento\">Asiento Infantil<\/span>\n      <div class=\"extra-comment\" data-i18n=\"extras.asiento.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique edad del ni\u00f1o en el siguiente recuadro<\/div>\n      <input type=\"text\" id=\"s1AsientoInfantilDetalle\" name=\"asiento_infantil_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n    <\/label>\n    <label>\n      <input type=\"checkbox\" id=\"s1ChoferIdioma\" name=\"chofer_idioma\">\n      <span data-i18n=\"extras.chofer\">Chofer con idioma<\/span>\n      <div class=\"extra-comment\" data-i18n=\"extras.chofer.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique idioma solicitado en el siguiente recuadro<\/div>\n      <input type=\"text\" id=\"s1ChoferIdiomaDetalle\" name=\"chofer_idioma_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n    <\/label>\n  <\/div>\n<\/div>\n\n\n  <label class=\"span-12\">\n  <span data-i18n=\"label.otro\">Otro (especificar)<\/span><br>\n  <input type=\"text\"\n         id=\"s1Otro\"\n         name=\"Otro (especificar)\"\n         data-i18n-placeholder=\"ph.otro\"\n         placeholder=\"Describe cualquier necesidad adicional\">\n<\/label>\n\n  <label class=\"span-6\">\n  <span data-i18n=\"label.addContact\">\u00bfAgregar persona de contacto adem\u00e1s del cliente?<\/span><br>\n  <select id=\"s1AddContact\"\n          name=\"Quiere agregar una persona de contacto adem\u00e1s del cliente?\">\n    <option>No<\/option><option>S\u00ed<\/option>\n  <\/select>\n<\/label>\n\n<div id=\"c1\" class=\"span-12\" hidden>\n  <div class=\"fs-grid\">\n    <label class=\"span-4\">\n      <span data-i18n=\"label.contactName\">Nombre del contacto*<\/span><br>\n      <input type=\"text\" id=\"s1ContactName\" name=\"Nombre del contacto\">\n    <\/label>\n    <label class=\"span-4\">\n      <span data-i18n=\"label.contactPhone\">Tel\u00e9fono de contacto*<\/span><br>\n      <input type=\"text\" id=\"s1ContactPhone\" name=\"Tel\u00e9fono de contacto\" inputmode=\"tel\">\n    <\/label>\n    <label class=\"span-4\">\n      <span data-i18n=\"label.contactEmail\">Email de contacto<\/span><br>\n      <input type=\"email\"\n             id=\"s1ContactEmail\"\n             name=\"Email de contacto\"\n             data-i18n-placeholder=\"ph.contactEmail\"\n             placeholder=\"email@dominio.com\">\n    <\/label>\n  <\/div>\n<\/div>\n\n<\/fieldset>\n\n\n\n  <!-- Activaci\u00f3n de Servicio 2 -->\n  <fieldset class=\"fs-grid\">\n    <label class=\"span-6\">\n      <span data-i18n=\"label.addService\">\u00bfAgregar Servicio?<\/span><br>\n      <select id=\"t2\" name=\"Desea agregar otro servicio al servicio Nro1?\" aria-controls=\"serv2\" aria-expanded=\"false\">\n        <option selected>No<\/option><option>S\u00ed<\/option>\n      <\/select>\n    <\/label>\n    <small class=\"hint span-12\" data-i18n=\"hint.addService2\">Si eliges \u201cS\u00ed\u201d, en el siguiente paso se abrir\u00e1 el formulario de Servicio 2.<\/small>\n  <\/fieldset>\n\n  <div class=\"block-actions\">\n    <button type=\"button\" class=\"btn-prev\" data-prev data-i18n=\"btn.prev\">Anterior<\/button>\n    <button type=\"button\" class=\"btn-next\" data-next data-i18n=\"btn.next\">Siguiente<\/button>\n  <\/div>\n<\/section>\n\n  <!-- BLOQUE 3\/4 se mantienen (omito por espacio). Puedes duplicar la misma l\u00f3gica de S1 para S2\/S3. -->\n  <section class=\"form-block\" data-block=\"3\" hidden>\n  <fieldset class=\"fs-grid\" id=\"serv2\">\n    <legend data-i18n=\"legend.serv2\">Servicio 2<\/legend>\n\n    <label class=\"span-4\">\n      <span data-i18n=\"label.lugar\">Lugar*<\/span><br>\n      <select id=\"s2Lugar\" name=\"Lugardos\" required>\n        <option>Barcelona<\/option><option>Madrid<\/option>\n      <\/select>\n      <div class=\"chips\" data-chips-for=\"s2Lugar\"><\/div>\n    <\/label>\n\n    <label class=\"span-4\">\n      <span data-i18n=\"label.vehiculo\">Veh\u00edculo*<\/span><br>\n      <select id=\"s2Vehiculo\" name=\"2_Veh\u00edculo\" required>\n  <option value=\"Coche\"      data-i18n=\"vehicle.turismo\"><\/option>\n  <option value=\"Van\"        data-i18n=\"vehicle.van\"><\/option>\n  <option value=\"Minibus 15\" data-i18n=\"vehicle.minibus15\"><\/option>\n  <option value=\"Minibus 25\" data-i18n=\"vehicle.minibus25\"><\/option>\n  <option value=\"Bus 50\"     data-i18n=\"vehicle.bus\"><\/option>\n<\/select>\n      <div class=\"chips\" data-chips-for=\"s2Vehiculo\"><\/div>\n      <div class=\"hint-inline\" id=\"s2PaxHint\" aria-live=\"polite\"><\/div>\n    <\/label>\n\n    <label class=\"span-4\">\n      <span data-i18n=\"label.servicio\">Servicio*<\/span><br>\n      <select id=\"s2Servicio\" name=\"2_Servicio\" required>\n        <option value=\"\"><\/option>\n      <\/select>\n      <div class=\"chips\" data-chips-for=\"s2Servicio\"><\/div>\n    <\/label>\n  <\/fieldset>\n\n  <fieldset class=\"fs-grid\" id=\"s2\">\n    <legend data-i18n=\"legend.serv2.detalles\">Detalles del servicio 2<\/legend>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.fecha\">Fecha de recogida*<\/span><br>\n      <input required type=\"date\" id=\"s2Date\" name=\"2_Fecha de recogida\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.hora\">Hora de recogida*<\/span><br>\n      <input required type=\"time\" id=\"s2Time\" name=\"2_Hora de recogida\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.pax\">Cantidad de pasajeros*<\/span><br>\n      <input required type=\"number\" min=\"1\" step=\"1\" id=\"s2Pax\" name=\"2_Cantidad de pasajeros?\" inputmode=\"numeric\" placeholder=\"M\u00e1x. \u2014\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.flight\">Nro.Vuelo\/Tren<\/span><br>\n      <input type=\"text\"\n             id=\"s2Flight\"\n             name=\"2_Introduzca el n\u00famero de vuelo (si corresponde)\"\n             data-i18n-placeholder=\"ph.flight\"\n             placeholder=\"Ej.: IB1234 \/ AVE 03192\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.pickup\">Punto de recogida*<\/span><br>\n      <input required type=\"text\"\n             id=\"s2Pickup\"\n             name=\"2_Punto de recogida\"\n             autocomplete=\"off\"\n             data-i18n-placeholder=\"ph.pickup\"\n             placeholder=\"Hotel, calle, aeropuerto\u2026\">\n      <input type=\"hidden\"\n             id=\"s2PickupExact\"\n             name=\"2_Direcci\u00f3n exacta de recogida\">\n      <input type=\"hidden\" id=\"s2PickupName\" name=\"2_Nombre recogida\">\n      <input type=\"hidden\" id=\"s2PickupLat\" name=\"2_Latitud recogida\">\n      <input type=\"hidden\" id=\"s2PickupLng\" name=\"2_Longitud recogida\">\n      <input type=\"hidden\" id=\"s2PickupPlaceId\" name=\"2_PlaceId recogida\">\n    <\/label>\n\n    <label class=\"span-6\" style=\"display:none;\">\n      <!-- Hidden label for layout compatibility -->\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.dropoff\">Punto de destino*<\/span><br>\n      <input required type=\"text\"\n             id=\"s2Dropoff\"\n             name=\"2_Punto de destino\"\n             autocomplete=\"off\"\n             data-i18n-placeholder=\"ph.dropoff\"\n             placeholder=\"Hotel, calle, evento\u2026\">\n      <input type=\"hidden\"\n             id=\"s2DropoffExact\"\n             name=\"2_Direcci\u00f3n exacta de destino\">\n      <input type=\"hidden\" id=\"s2DropoffName\" name=\"2_Nombre destino\">\n      <input type=\"hidden\" id=\"s2DropoffLat\" name=\"2_Latitud destino\">\n      <input type=\"hidden\" id=\"s2DropoffLng\" name=\"2_Longitud destino\">\n      <input type=\"hidden\" id=\"s2DropoffPlaceId\" name=\"2_PlaceId destino\">\n    <\/label>\n\n    <label class=\"span-6\" style=\"display:none;\">\n      <!-- Hidden label for layout compatibility -->\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.distancia\">Distancia (km)<\/span><br>\n      <input type=\"text\"\n             id=\"s2Distance\"\n             name=\"2_Distancia (km)\"\n             readonly\n data-i18n-placeholder=\"ph.distancia\"\n             placeholder=\"\u2014\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.precio\">Precio (Iva Incluido)<\/span><br>\n      <output id=\"s2Price\" hidden aria-live=\"polite\">\n        <span data-c=\"EUR\">\u2014<\/span>\n      <\/output>\n    <\/label>\n\n    <div class=\"span-12\" id=\"s2ExtrasWrap\">\n      <div class=\"extras\" data-for=\"2\" role=\"group\" data-i18n-aria-label=\"label.extras\" aria-label=\"Adicionales Servicio 2\">\n        <label>\n          <input type=\"checkbox\" id=\"s2CartelNombre\" name=\"cartel_nombre\">\n          <span data-i18n=\"extras.cartel\">Cartel con Nombre<\/span>\n          <div class=\"extra-comment\" data-i18n=\"extras.cartel.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique nombre en el siguiente recuadro<\/div>\n          <input type=\"text\" id=\"s2CartelNombreDetalle\" name=\"cartel_nombre_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n        <\/label>\n        <label>\n          <input type=\"checkbox\" id=\"s2AsientoInfantil\" name=\"asiento_infantil\">\n          <span data-i18n=\"extras.asiento\">Asiento Infantil<\/span>\n          <div class=\"extra-comment\" data-i18n=\"extras.asiento.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique edad del ni\u00f1o en el siguiente recuadro<\/div>\n          <input type=\"text\" id=\"s2AsientoInfantilDetalle\" name=\"asiento_infantil_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n        <\/label>\n        <label>\n          <input type=\"checkbox\" id=\"s2ChoferIdioma\" name=\"chofer_idioma\">\n          <span data-i18n=\"extras.chofer\">Chofer con idioma<\/span>\n          <div class=\"extra-comment\" data-i18n=\"extras.chofer.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique idioma solicitado en el siguiente recuadro<\/div>\n          <input type=\"text\" id=\"s2ChoferIdiomaDetalle\" name=\"chofer_idioma_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n        <\/label>\n      <\/div>\n    <\/div>\n\n    <label class=\"span-12\">\n      <span data-i18n=\"label.otro\">Otro (especificar)<\/span><br>\n      <input type=\"text\"\n             id=\"s2Otro\"\n             name=\"2_Otro (especificar)\"\n             data-i18n-placeholder=\"ph.otro\"\n             placeholder=\"Describe cualquier necesidad adicional\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.addContact\">\u00bfAgregar persona de contacto adem\u00e1s del cliente?<\/span><br>\n      <select id=\"s2AddContact\" name=\"2_Quiere agregar una persona de contacto adem\u00e1s del cliente?\">\n        <option>No<\/option><option>S\u00ed<\/option>\n      <\/select>\n    <\/label>\n\n    <div id=\"c2\" class=\"span-12\" hidden>\n      <div class=\"fs-grid\">\n        <label class=\"span-4\">\n          <span data-i18n=\"label.contactName\">Nombre del contacto*<\/span><br>\n          <input type=\"text\" id=\"s2ContactName\" name=\"2_Nombre\">\n        <\/label>\n        <label class=\"span-4\">\n          <span data-i18n=\"label.contactPhone\">Tel\u00e9fono de contacto*<\/span><br>\n          <input type=\"text\" id=\"s2ContactPhone\" name=\"2_Tel\u00e9fono\" inputmode=\"tel\">\n        <\/label>\n        <label class=\"span-4\">\n          <span data-i18n=\"label.contactEmail\">Email de contacto<\/span><br>\n          <input type=\"email\"\n                 id=\"s2ContactEmail\"\n                 name=\"2_Email\"\n                 data-i18n-placeholder=\"ph.contactEmail\"\n                 placeholder=\"email@dominio.com\">\n        <\/label>\n      <\/div>\n    <\/div>\n  <\/fieldset>\n\n  <fieldset class=\"fs-grid\">\n    <label class=\"span-6\">\n      <span data-i18n=\"label.addService\">\u00bfAgregar Servicio?<\/span><br>\n      <select id=\"t3\" name=\"Desea agregar otro servicio al servicio Nro2?\" aria-controls=\"serv3\" aria-expanded=\"false\">\n        <option selected>No<\/option><option>S\u00ed<\/option>\n      <\/select>\n    <\/label>\n    <small class=\"hint span-12\" data-i18n=\"hint.addService3\">Si eliges \u201cS\u00ed\u201d, en el siguiente paso se abrir\u00e1 el formulario de Servicio 3.<\/small>\n  <\/fieldset>\n\n  <div class=\"block-actions\">\n    <button type=\"button\" class=\"btn-prev\" data-prev data-i18n=\"btn.prev\">Anterior<\/button>\n    <button type=\"button\" class=\"btn-next\" data-next data-i18n=\"btn.next\">Siguiente<\/button>\n  <\/div>\n<\/section>\n\n\n  <section class=\"form-block\" data-block=\"4\" hidden>\n  <fieldset class=\"fs-grid\" id=\"serv3\">\n    <legend data-i18n=\"legend.serv3\">Servicio 3<\/legend>\n\n    <label class=\"span-4\">\n      <span data-i18n=\"label.lugar\">Lugar*<\/span><br>\n      <select id=\"s3Lugar\" name=\"Lugartres\" required>\n        <option>Barcelona<\/option><option>Madrid<\/option>\n      <\/select>\n      <div class=\"chips\" data-chips-for=\"s3Lugar\"><\/div>\n    <\/label>\n\n    <label class=\"span-4\">\n      <span data-i18n=\"label.vehiculo\">Veh\u00edculo*<\/span><br>\n      <select id=\"s3Vehiculo\" name=\"3_Veh\u00edculo\" required>\n  <option value=\"Coche\"      data-i18n=\"vehicle.turismo\"><\/option>\n  <option value=\"Van\"        data-i18n=\"vehicle.van\"><\/option>\n  <option value=\"Minibus 15\" data-i18n=\"vehicle.minibus15\"><\/option>\n  <option value=\"Minibus 25\" data-i18n=\"vehicle.minibus25\"><\/option>\n  <option value=\"Bus 50\"     data-i18n=\"vehicle.bus\"><\/option>\n<\/select>\n      <div class=\"chips\" data-chips-for=\"s3Vehiculo\"><\/div>\n      <div class=\"hint-inline\" id=\"s3PaxHint\" aria-live=\"polite\"><\/div>\n    <\/label>\n\n    <label class=\"span-4\">\n      <span data-i18n=\"label.servicio\">Servicio*<\/span><br>\n      <select id=\"s3Servicio\" name=\"3_Servicio\" required>\n        <option value=\"\"><\/option>\n      <\/select>\n      <div class=\"chips\" data-chips-for=\"s3Servicio\"><\/div>\n    <\/label>\n  <\/fieldset>\n\n  <fieldset class=\"fs-grid\" id=\"s3\">\n    <legend data-i18n=\"legend.serv3.detalles\">Detalles del servicio 3<\/legend>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.fecha\">Fecha de recogida*<\/span><br>\n      <input required type=\"date\" id=\"s3Date\" name=\"3_Fecha de recogida\">\n    <\/label>\n    <label class=\"span-6\">\n      <span data-i18n=\"label.hora\">Hora de recogida*<\/span><br>\n      <input required type=\"time\" id=\"s3Time\" name=\"3_Hora de recogida\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.pax\">Cantidad de pasajeros*<\/span><br>\n      <input required type=\"number\" min=\"1\" step=\"1\" id=\"s3Pax\" name=\"3_Cantidad de pasajeros?\" inputmode=\"numeric\" placeholder=\"M\u00e1x. \u2014\">\n    <\/label>\n    <label class=\"span-6\">\n      <span data-i18n=\"label.flight\">Nro.Vuelo\/Tren<\/span><br>\n      <input type=\"text\"\n             id=\"s3Flight\"\n             name=\"3_Introduzca el n\u00famero de vuelo (si corresponde)\"\n             data-i18n-placeholder=\"ph.flight\"\n             placeholder=\"Ej.: IB1234 \/ AVE 03192\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.pickup\">Punto de recogida*<\/span><br>\n      <input required type=\"text\"\n             id=\"s3Pickup\"\n             name=\"3_Punto de recogida\"\n             autocomplete=\"off\"\n             data-i18n-placeholder=\"ph.pickup\"\n             placeholder=\"Hotel, calle, aeropuerto\u2026\">\n      <input type=\"hidden\"\n             id=\"s3PickupExact\"\n             name=\"3_Direcci\u00f3n exacta de recogida\">\n      <input type=\"hidden\" id=\"s3PickupName\" name=\"3_Nombre recogida\">\n      <input type=\"hidden\" id=\"s3PickupLat\" name=\"3_Latitud recogida\">\n      <input type=\"hidden\" id=\"s3PickupLng\" name=\"3_Longitud recogida\">\n      <input type=\"hidden\" id=\"s3PickupPlaceId\" name=\"3_PlaceId recogida\">\n    <\/label>\n\n    <label class=\"span-6\" style=\"display:none;\">\n      <!-- Hidden label for layout compatibility -->\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.dropoff\">Punto de destino*<\/span><br>\n      <input required type=\"text\"\n             id=\"s3Dropoff\"\n             name=\"3_Punto de destino\"\n             autocomplete=\"off\"\n             data-i18n-placeholder=\"ph.dropoff\"\n             placeholder=\"Hotel, calle, evento\u2026\">\n      <input type=\"hidden\"\n             id=\"s3DropoffExact\"\n             name=\"3_Direcci\u00f3n exacta de destino\">\n      <input type=\"hidden\" id=\"s3DropoffName\" name=\"3_Nombre destino\">\n      <input type=\"hidden\" id=\"s3DropoffLat\" name=\"3_Latitud destino\">\n      <input type=\"hidden\" id=\"s3DropoffLng\" name=\"3_Longitud destino\">\n      <input type=\"hidden\" id=\"s3DropoffPlaceId\" name=\"3_PlaceId destino\">\n    <\/label>\n\n    <label class=\"span-6\" style=\"display:none;\">\n      <!-- Hidden label for layout compatibility -->\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.distancia\">Distancia (km)<\/span><br>\n      <input type=\"text\"\n             id=\"s3Distance\"\n             name=\"3_Distancia (km)\"\n             readonly\n data-i18n-placeholder=\"ph.distancia\"\n             placeholder=\"\u2014\">\n    <\/label>\n    <label class=\"span-6\">\n      <span data-i18n=\"label.precio\">Precio (Iva Incluido)<\/span><br>\n      <output id=\"s3Price\" hidden aria-live=\"polite\">\n        <span data-c=\"EUR\">\u2014<\/span>\n      <\/output>\n    <\/label>\n\n    <div class=\"span-12\" id=\"s3ExtrasWrap\">\n      <div class=\"extras\" data-for=\"3\" role=\"group\" data-i18n-aria-label=\"label.extras\" aria-label=\"Adicionales Servicio 3\">\n        <label>\n          <input type=\"checkbox\" id=\"s3CartelNombre\" name=\"cartel_nombre\">\n          <span data-i18n=\"extras.cartel\">Cartel con Nombre<\/span>\n          <div class=\"extra-comment\" data-i18n=\"extras.cartel.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique nombre en el siguiente recuadro<\/div>\n          <input type=\"text\" id=\"s3CartelNombreDetalle\" name=\"cartel_nombre_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n        <\/label>\n        <label>\n          <input type=\"checkbox\" id=\"s3AsientoInfantil\" name=\"asiento_infantil\">\n          <span data-i18n=\"extras.asiento\">Asiento Infantil<\/span>\n          <div class=\"extra-comment\" data-i18n=\"extras.asiento.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique edad del ni\u00f1o en el siguiente recuadro<\/div>\n          <input type=\"text\" id=\"s3AsientoInfantilDetalle\" name=\"asiento_infantil_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n        <\/label>\n        <label>\n          <input type=\"checkbox\" id=\"s3ChoferIdioma\" name=\"chofer_idioma\">\n          <span data-i18n=\"extras.chofer\">Chofer con idioma<\/span>\n          <div class=\"extra-comment\" data-i18n=\"extras.chofer.comment\" style=\"font-size:0.85rem;color:#6b7280;margin-top:4px;\">Indique idioma solicitado en el siguiente recuadro<\/div>\n          <input type=\"text\" id=\"s3ChoferIdiomaDetalle\" name=\"chofer_idioma_detalle\" placeholder=\"\" style=\"display:none;margin-top:6px;width:100%;padding:6px 10px;border:1px solid var(--border-2);border-radius:var(--radius-sm);\">\n        <\/label>\n      <\/div>\n    <\/div>\n\n    <label class=\"span-12\">\n      <span data-i18n=\"label.otro\">Otro (especificar)<\/span><br>\n      <input type=\"text\"\n             id=\"s3Otro\"\n             name=\"3_Otro (especificar)\"\n             data-i18n-placeholder=\"ph.otro\"\n             placeholder=\"Describe cualquier necesidad adicional\">\n    <\/label>\n\n    <label class=\"span-6\">\n      <span data-i18n=\"label.addContact\">\u00bfAgregar persona de contacto adem\u00e1s del cliente?<\/span><br>\n      <select id=\"s3AddContact\" name=\"3_Quiere agregar una persona de contacto adem\u00e1s del cliente?\">\n        <option>No<\/option><option>S\u00ed<\/option>\n      <\/select>\n    <\/label>\n    <div id=\"c3\" class=\"span-12\" hidden>\n      <div class=\"fs-grid\">\n        <label class=\"span-4\">\n          <span data-i18n=\"label.contactName\">Nombre del contacto*<\/span><br>\n          <input type=\"text\" id=\"s3ContactName\" name=\"3_Nombre\">\n        <\/label>\n        <label class=\"span-4\">\n          <span data-i18n=\"label.contactPhone\">Tel\u00e9fono de contacto*<\/span><br>\n          <input type=\"text\" id=\"s3ContactPhone\" name=\"3_Tel\u00e9fono\" inputmode=\"tel\">\n        <\/label>\n        <label class=\"span-4\">\n          <span data-i18n=\"label.contactEmail\">Email de contacto<\/span><br>\n          <input type=\"email\"\n                 id=\"s3ContactEmail\"\n                 name=\"3_Email\"\n                 data-i18n-placeholder=\"ph.contactEmail\"\n                 placeholder=\"email@dominio.com\">\n        <\/label>\n      <\/div>\n    <\/div>\n  <\/fieldset>\n\n  <div class=\"block-actions\">\n    <button type=\"button\" class=\"btn-prev\" data-prev data-i18n=\"btn.prev\">Anterior<\/button>\n    <button type=\"button\" class=\"btn-next\" data-next data-i18n=\"btn.next\">Siguiente<\/button>\n  <\/div>\n<\/section>\n\n\n\n  <!-- BLOQUE 5: Comentarios + Pago + Env\u00edo -->\n  <section class=\"form-block\" data-block=\"5\" hidden>\n  <!-- Comentarios -->\n  <fieldset class=\"fs-grid\" id=\"comentarios\">\n    <legend data-i18n=\"legend.comentarios\">Comentarios<\/legend>\n    <label class=\"span-12\">\n      <span data-i18n=\"label.comentarios\">Comentarios u observaciones<\/span><br>\n      <textarea name=\"Comentarios u observaciones\" rows=\"4\"><\/textarea>\n    <\/label>\n  <\/fieldset>\n\n  <!-- Pago -->\n  <fieldset class=\"fs-grid\" id=\"pago\">\n    <legend id=\"pagoLegend\" data-i18n=\"legend.pago\">Forma de pago<\/legend>\n\n    <div class=\"pago-top span-12\">\n      <label>\n        <span data-i18n=\"label.pago.total\">Precio total:<\/span>\n        <output id=\"pagoTotal\" class=\"precio-chip\" aria-live=\"polite\">0,00&nbsp;\u20ac<\/output>\n      <\/label>\n    <\/div>\n\n    <div class=\"pago-top span-12\">\n      <label>\n        <span data-i18n=\"label.pago.servicios\">Servicios contratados:<\/span>\n        <output id=\"pagoServiciosCount\" class=\"precio-chip\"><span>0<\/span><\/output>\n      <\/label>\n    <\/div>\n\n    <div class=\"form-group span-12\">\n      <label for=\"pagoMetodo\" id=\"lblPago\" data-i18n=\"label.pago.metodo\">Forma de pago*<\/label>\n\n      <!-- select real, oculto -->\n      <select id=\"pagoMetodo\"\n              name=\"Indique la forma de pago\"\n              class=\"visually-hidden-select\"\n              aria-hidden=\"true\" tabindex=\"-1\">\n        <option value=\"Transferencia bancaria\" data-i18n=\"pago.metodo.transfer\">Transferencia bancaria<\/option>\n        <option value=\"Cargo a cuenta del cliente\" data-i18n=\"pago.metodo.cargo\">Cargo a cuenta del cliente<\/option>\n        <option value=\"Tarjeta de cr\u00e9dito\" data-i18n=\"pago.metodo.tarjeta\">Tarjeta de cr\u00e9dito<\/option>\n      <\/select>\n\n      <!-- botones bonitos -->\n      <div class=\"pago-buttons\" role=\"radiogroup\" aria-labelledby=\"lblPago\">\n        <button type=\"button\" class=\"pago-btn\" role=\"radio\" data-value=\"Transferencia bancaria\" data-i18n=\"pago.metodo.transfer\">Transferencia bancaria<\/button>\n        <button type=\"button\" class=\"pago-btn\" role=\"radio\" data-value=\"Cargo a cuenta del cliente\" data-i18n=\"pago.metodo.cargo\">Cargo a cuenta del cliente<\/button>\n        <button type=\"button\" class=\"pago-btn\" role=\"radio\" data-value=\"Tarjeta de cr\u00e9dito\" data-i18n=\"pago.metodo.tarjeta\">Tarjeta de cr\u00e9dito<\/button>\n      <\/div>\n\n      <!-- Caja bancaria -->\n      <div id=\"bankBox\" class=\"bank-box\">\n        <strong data-i18n=\"pago.bank.title\">Datos bancarios de la empresa<\/strong><br>\n        Titular: MINI VIP&#8217;S CAR DRIVERS, S.L.<br>\n        CIF: B62551551<br>\n        Banco: Banco Sabadell<br>\n        IBAN: ES74 0081 7011 1500 0172 4681<br>\n        Concepto: N\u00ba cliente <span id=\"bankCliNum\">___<\/span> \u2014 N\u00ba reserva <span id=\"bankResNum\">\u2014<\/span>\n        <div class=\"muted\" data-i18n=\"pago.bank.help\">\n          Si eliges transferencia o cargo a cuenta, usa estos datos. Puedes enviarnos el comprobante respondiendo al email de confirmaci\u00f3n.\n        <\/div>\n      <\/div>\n\n      <!-- Aviso pago con tarjeta -->\n      <div id=\"cardInfo\" class=\"muted\" hidden data-i18n=\"pago.card.info\">\n        Si eliges pagar con tarjeta, en el email de confirmaci\u00f3n incluiremos un bot\u00f3n con el enlace a nuestra pasarela de pago segura.\n      <\/div>\n    <\/div>\n  <\/fieldset>\n\n  <div class=\"block-actions\">\n    <button type=\"button\" class=\"btn-prev\" data-prev data-i18n=\"btn.prev\">Anterior<\/button>\n    <div>\n      <button type=\"submit\" class=\"btn-next\" data-i18n=\"btn.submit\">Enviar<\/button>\n      <span id=\"estado\" aria-live=\"polite\"><\/span>\n    <\/div>\n  <\/div>\n\n  <!-- Toast -->\n  <div id=\"toast\" role=\"dialog\" aria-modal=\"true\" aria-live=\"assertive\" data-open=\"0\" hidden>\n    <div class=\"toast-card\" role=\"document\">\n      <h3 data-i18n=\"toast.title\">Pedido enviado<\/h3>\n      <p data-i18n=\"toast.body\">Tu pedido ha sido enviado correctamente.<\/p>\n      <div class=\"toast-actions\">\n        <button id=\"toastClose\" type=\"button\" class=\"btn-primary\" data-i18n=\"btn.ok\">Aceptar<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/section>\n\n<input type=\"hidden\" name=\"trp-form-language\" value=\"en\"\/><\/form>\n<!-- iOS helper modal -->\n<div id=\"bukas-ios-guide\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"iosGuideTitle\" hidden>\n  <div class=\"card\" role=\"document\">\n    <h3 id=\"iosGuideTitle\" data-i18n=\"ios.title\">A\u00f1adir a pantalla de inicio<\/h3>\n    <p data-i18n=\"ios.desc\">En iPhone\/iPad no se puede instalar con un clic. Haz esto:<\/p>\n    <ol style=\"margin:.5rem 0 1rem; padding-left:1.2rem;\">\n      <li data-i18n=\"ios.step1\">Abre esta p\u00e1gina en <strong>Safari<\/strong>.<\/li>\n      <li data-i18n=\"ios.step2\">Toca el bot\u00f3n <strong>Compartir<\/strong> (cuadro con flecha).<\/li>\n      <li data-i18n=\"ios.step3\">Elige <strong>A\u00f1adir a pantalla de inicio<\/strong>.<\/li>\n    <\/ol>\n    <div class=\"row\">\n      <button id=\"iosGuideClose\" class=\"ghost\" type=\"button\" data-i18n=\"btn.close\">Cerrar<\/button>\n      <a class=\"cta\" href=\"\/en\/bukas\/\" role=\"button\" data-i18n=\"btn.openSafari\">Abrir en Safari<\/a>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script type=\"module\">\n\n\/\/ Diccionario\n\nconst I18N = {\n  es: {\n    \/\/ Cabecera y botones principales\n    'app.title': 'Reservas',\n    'btn.install': 'Descarga nuestra App',\n    'btn.next': 'Siguiente',\n    'btn.prev': 'Anterior',\n    'btn.submit': 'Enviar',\n    'btn.accept': 'Aceptar',\n\n    \/\/ Stepper\n    'step.1': 'Cliente',\n    'step.2': 'Servicio 1',\n    'step.3': 'Servicio 2',\n    'step.4': 'Servicio 3',\n    'step.5': 'Final',\n\n    \/\/ Bloque 1 - Cliente\n    'legend.cliente': 'Datos del cliente',\n    'label.cliNum': 'N\u00ba de cliente (opcional)',\n    'label.cliTax': 'CIF, VAT o Pasaporte*',\n    'label.cliName': 'Nombre del cliente*',\n    'label.cliPhone': 'Tel\u00e9fono*',\n    'label.cliBilling': 'Domicilio de facturaci\u00f3n*',\n    'label.cliEmail': 'Correo electr\u00f3nico*',\n    'hint.cliEmail': 'Para esta reserva puedes indicar un email distinto al de la ficha.',\n    'hint.maxPax': 'Capacidad m\u00e1xima para {tipo}: {max} pax',\n\n\n    \/\/ Bloque 2 - Servicio\n    'legend.serv1': 'Servicio 1',\n    'legend.serv1.detalles': 'Detalles del servicio 1',\n    'label.lugar': 'Lugar*',\n    'label.vehiculo': 'Veh\u00edculo*',\n    'label.servicio': 'Servicio*',\n    'label.fecha': 'Fecha de recogida*',\n    'label.hora': 'Hora de recogida*',\n    'label.pax': 'Cantidad de pasajeros*',\n    'label.flight': 'Nro.Vuelo\/Tren',\n    'label.pickup': 'Punto de recogida*',\n    'label.pickupExact': 'Direcci\u00f3n exacta de recogida',\n    'label.dropoff': 'Punto de destino*',\n    'label.dropoffExact': 'Direcci\u00f3n exacta de destino',\n    'label.distancia': 'Distancia (km)',\n    'label.precio': 'Precio (Iva Incluido)',\n    'label.otro': 'Otro (especificar)',\n    'label.addContact': '\u00bfAgregar persona de contacto adem\u00e1s del cliente?',\n    'label.contactName': 'Nombre del contacto*',\n    'label.contactPhone': 'Tel\u00e9fono de contacto*',\n    'label.contactEmail': 'Email de contacto*',\n    'label.addService': '\u00bfAgregar Servicio?',\n    'hint.addService': 'Si eliges \u201cS\u00ed\u201d, en el siguiente paso se abrir\u00e1 el formulario del servicio siguiente.',\n\n    \/\/ Extras\n    'extras.cartel': 'Cartel con Nombre',\n    'extras.cartel.comment': 'Indique nombre en el siguiente recuadro',\n    'extras.asiento': 'Asiento Infantil',\n    'extras.asiento.comment': 'Indique edad del ni\u00f1o en el siguiente recuadro',\n    'extras.chofer': 'Chofer con idioma',\n    'extras.chofer.comment': 'Indique idioma solicitado en el siguiente recuadro',\n\n    \/\/ Comentarios\n    'legend.comentarios': 'Comentarios',\n    'label.comentarios': 'Comentarios u observaciones',\n\n    \/\/ Pago\n    'legend.pago': 'Forma de pago',\n    'label.pago.total': 'Precio total:',\n    'label.pago.servicios': 'Servicios contratados:',\n    'label.pago.metodo': 'Forma de pago*',\n    'pago.metodo.transfer': 'Transferencia bancaria',\n    'pago.metodo.cargo': 'Cargo a cuenta del cliente',\n    'pago.metodo.tarjeta': 'Tarjeta de cr\u00e9dito',\n    'pago.bank.title': 'Datos bancarios de la empresa',\n    'pago.bank.text': 'Titular: MINI VIP\\'S CAR DRIVERS, S.L.\\nCIF: B62551551\\nBanco: Banco Sabadell\\nIBAN: ES74 0081 7011 1500 0172 4681\\nConcepto: Reserva \u2014 N\u00ba cliente ___ \u2014 N\u00ba reserva \u2014',\n    'pago.bank.help': 'Si eliges transferencia o cargo a cuenta, usa estos datos. Puedes enviarnos el comprobante respondiendo al email de confirmaci\u00f3n.',\n    'pago.card.info': 'Si eliges pagar con tarjeta, en el email de confirmaci\u00f3n incluiremos un bot\u00f3n con el enlace a nuestra pasarela de pago segura.',\n\n    \/\/ Toast final\n    'toast.title': 'Pedido enviado',\n    'toast.text': 'Tu pedido ha sido enviado correctamente.',\n\n    \/\/ Placeholders\n    'ph.cliemail': 'email@dominio.com',\n    'ph.clinum': 'Ej: 120045',\n    'ph.clitax': 'ESB12345678 \/ GB123456789 \/ Pasaporte',\n    'ph.pickup': 'Hotel, calle, aeropuerto\u2026',\n    'ph.dropoff': 'Hotel, calle, evento\u2026',\n    'ph.pickupExact': '(auto si corresponde)',\n    'ph.dropoffExact': '(auto si corresponde)',\n    'ph.flight': 'Ej.: IB1234 \/ AVE 03192',\n    'ph.otro': 'Describe cualquier necesidad adicional',\n    'ph.distancia': '\u2014',\n\n    \/\/ Etiquetas que faltaban (bloque servicio)\n    'label.paxCount': 'Cantidad de pasajeros*',\n    'label.flightNum': 'Nro.Vuelo\/Tren',\n    'label.distance': 'Distancia (km)',\n    'label.pickupPoint': 'Punto de recogida*',\n    'label.dropoffPoint': 'Punto de destino*',\n    'label.price': 'Precio (Iva Incluido)',\n\n    'label.taxId': 'CIF, VAT o Pasaporte*',\n    'ph.cliNum': 'Ej: 120045',\n    'ph.taxId': 'ESB12345678 \/ GB123456789 \/ Pasaporte',\n    'cliente.badge.intro': 'Introduce N\u00ba de cliente y CIF\/VAT para autocompletar',\n\n     \/\/ S\u00ed \/ No visual\n    'yn.si': 'S\u00ed',\n    'yn.no': 'No',\n\n        \/\/ Avisos de precio \/ cotizaci\u00f3n\n    'hint.customQuote': 'Por la ubicaci\u00f3n de los puntos de origen o destino, este servicio podr\u00eda necesitar una cotizaci\u00f3n personalizada.',\n    'hint.extraCost': 'Ciertos servicios podr\u00edan suponer un coste adicional.',\n\n    'cliente.badge.search': 'Buscando\u2026',\n    \/\/ ES\n    'cliente.badge.ok': 'Cliente verificado: datos bloqueados (email editable)',\n    'cliente.badge.editable': 'Cliente encontrado \u2014 puedes editar.',\n    'cliente.badge.notfound': 'Cliente no encontrado. Completa manualmente.',\n\n    \/\/ ES\n    'ph.pickup': 'Hotel, calle, aeropuerto\u2026',\n    'ph.dropoff': 'Hotel, calle, evento\u2026',\n    'ph.pickupExact': '(auto si corresponde)',\n    'ph.dropoffExact': '(auto si corresponde)',\n\n    \/\/ Tipos de servicio\n    'service.traslado': 'Traslado',\n    'service.disp4h': 'Disposici\u00f3n 4 horas en la Ciudad',\n    'service.disp8h': 'Disposici\u00f3n 8 horas en la Ciudad',\n\n\n    'estado.enviando': 'Enviando\u2026',\n    'estado.error': 'No se pudo enviar. Revisa tu conexi\u00f3n e int\u00e9ntalo de nuevo.',\n    'hint.stepRequired': 'Completa los campos requeridos para continuar.',\n    'required.msg': 'Campo obligatorio',\n    'error.maxPax': 'No podemos procesar reservas con m\u00e1s de {max} pasajeros para este veh\u00edculo. Reduce el n\u00famero de pasajeros o elige un veh\u00edculo con mayor capacidad.',\n    'address.choose': 'Selecciona la direcci\u00f3n correcta:',\n    'address.noResults': 'No encontramos direcciones coincidentes. Ajusta la b\u00fasqueda o a\u00f1ade m\u00e1s detalles.',\n    'addressCandidates.pickupTitle': 'Selecciona la direcci\u00f3n exacta de recogida',\n    'addressCandidates.dropoffTitle': 'Selecciona la direcci\u00f3n exacta de destino',\n\n    \/\/ Mensajes cliente\n    'cliente.badge.intro': 'Introduce N\u00ba de cliente y CIF\/VAT para autocompletar',\n    'cliente.email_hint': 'Para esta reserva puedes indicar un email distinto al de la ficha.',\n\n  \/\/ Hint \u201cAdd another service?\u201d\n    'hint.addService2': 'Si eliges \u201cS\u00ed\u201d, en el siguiente paso se abrir\u00e1 el formulario de Servicio 2.',\n    'hint.addService3': 'Si eliges \u201cS\u00ed\u201d, en el siguiente paso se abrir\u00e1 el formulario de Servicio 3.',\n\n    'vehicle.turismo':   'Turismo',\n    'vehicle.van':       'Monovolumen \/ Van',\n    'vehicle.minibus15': 'Minib\u00fas 15',\n    'vehicle.minibus25': 'Minib\u00fas 25',\n    'vehicle.bus':       'Bus',\n\n        \/\/ ---- Missing Service 1 labels ----\n    'legend.serv1det': 'Servicio 1 detalles',\n    'label.pickupDate': 'Fecha de recogida*',\n    'label.pickupTime': 'Hora de recogida*',\n\n    'legend.serv2det': 'Servicio 2 detalles',\n\n    'legend.serv3det': 'Servicio 3 detalles',\n\n    \/\/ Bloque 2 - Servicio\n    'legend.serv1': 'Servicio 1',\n    'legend.serv1.detalles': 'Detalles del servicio 1',\n    'legend.serv2': 'Servicio 2',\n    'legend.serv2.detalles': 'Detalles del servicio 2',\n    'legend.serv3': 'Servicio 3',\n    'legend.serv3.detalles': 'Detalles del servicio 3',\n\n\n\n    \/\/ ...\n    \/\/ ---- Missing Service 1 labels ----\n    'legend.serv1det': 'Servicio 1 detalles',\n    'legend.serv2det': 'Servicio 2 detalles',\n    'legend.serv3det': 'Servicio 3 detalles',\n\n\n    'toast.title': 'Pedido enviado',\n    'toast.body': 'Tu pedido ha sido enviado correctamente.',\n    'btn.ok': 'Aceptar',\n\n  },\n\n  en: {\n    'app.title': 'Booking',\n    'btn.install': 'Download our App',\n    'btn.next': 'Next',\n    'btn.prev': 'Previous',\n    'btn.submit': 'Send',\n    'btn.accept': 'OK',\n\n    \/\/ Stepper\n    'step.1': 'Client',\n    'step.2': 'Service 1',\n    'step.3': 'Service 2',\n    'step.4': 'Service 3',\n    'step.5': 'Finish',\n\n    \/\/ Client\n    'legend.cliente': 'Client details',\n    'label.cliNum': 'Client No. (optional)',\n    'label.clitax': 'Tax ID \/ VAT \/ Passport*',\n    'label.cliName': 'Client name*',\n    'label.cliphone': 'Phone*',\n    'label.clibilling': 'Billing address*',\n    'label.cliemail': 'Email address*',\n    'hint.cliEmail': 'You may use a different email than the one registered.',\n    'hint.maxPax': 'Maximum capacity for {tipo}: {max} pax',\n\n\n    \/\/ Service\n    'legend.serv1': 'Service 1',\n    'legend.serv1.detalles': 'Service 1 details',\n    'label.lugar': 'Location*',\n    'label.vehiculo': 'Vehicle*',\n    'label.servicio': 'Service*',\n    'label.fecha': 'Pickup date*',\n    'label.hora': 'Pickup time*',\n    'label.pax': 'Number of passengers*',\n    'label.flight': 'Flight\/Train No.',\n    'label.pickup': 'Pickup point*',\n    'label.pickupExact': 'Exact pickup address',\n    'label.dropoff': 'Drop-off point*',\n    'label.dropoffExact': 'Exact drop-off address',\n    'label.distancia': 'Distance (km)',\n    'label.precio': 'Price (VAT included)',\n    'label.otro': 'Other (specify)',\n    'label.addContact': 'Add a contact person besides the client?',\n    'label.contactName': 'Contact name*',\n    'label.contactPhone': 'Contact phone*',\n    'label.contactEmail': 'Contact email*',\n    'label.addService': 'Add another service?',\n    'hint.addService': 'If you choose \u201cYes\u201d, the next service form will open in the following step.',\n\n    'legend.serv2': 'Service 2',\n    'legend.serv2.detalles': 'Service 2 details',\n    'legend.serv3': 'Service 3',\n    'legend.serv3.detalles': 'Service 3 details',\n\n    \/\/ opcional pero recomendable para S1:\n    'legend.serv1det': 'Service 1 details',\n    'legend.serv2det': 'Service 2 details',\n    'legend.serv3det': 'Service 3 details',\n\n\n    \/\/ Extras\n    'extras.cartel': 'Name sign',\n    'extras.cartel.comment': 'Please indicate the name in the box below',\n    'extras.asiento': 'Child seat',\n    'extras.asiento.comment': 'Please indicate the child\\'s age in the box below',\n    'extras.chofer': 'Driver with specific language',\n    'extras.chofer.comment': 'Please indicate the requested language in the box below',\n\n    \/\/ Comments\n    'legend.comentarios': 'Comments',\n    'label.comentarios': 'Comments or observations',\n\n    \/\/ Payment\n    'legend.pago': 'Payment method',\n    'label.pago.total': 'Total price:',\n    'label.pago.servicios': 'Services booked:',\n    'label.pago.metodo': 'Payment method*',\n    'pago.metodo.transfer': 'Bank transfer',\n    'pago.metodo.cargo': 'Charge to client account',\n    'pago.metodo.tarjeta': 'Credit card',\n    'pago.bank.title': 'Company bank details',\n    'pago.bank.text': 'Holder: MINI VIP\\'S CAR DRIVERS, S.L.\\nTax ID: B62551551\\nBank: Banco Sabadell\\nIBAN: ES74 0081 7011 1500 0172 4681\\nReference: Booking \u2014 Client No. ___ \u2014 Reservation No. \u2014',\n    'pago.bank.help': 'If you choose bank transfer or account charge, use these details. You can send the payment receipt by replying to the confirmation email.',\n    'pago.card.info': 'If you choose to pay by card, the confirmation email will include a button with a link to our secure payment gateway.',\n\n    \/\/ Toast\n    'toast.title': 'Order sent',\n    'toast.text': 'Your order has been successfully submitted.',\n    'toast.body': 'Your order has been sent successfully.',\n    'btn.ok': 'OK',\n\n\n    \/\/ Placeholders\n    'ph.cliemail': 'email@example.com',\n    'ph.clinum': 'Ex: 120045',\n    'ph.clitax': 'ESB12345678 \/ GB123456789 \/ Passport',\n    'ph.pickup': 'Hotel, street, airport\u2026',\n    'ph.dropoff': 'Hotel, street, event\u2026',\n    'ph.pickupExact': '(auto if applicable)',\n    'ph.dropoffExact': '(auto if applicable)',\n    'ph.flight': 'e.g. IB1234 \/ AVE 03192',\n    'ph.otro': 'Describe any additional needs',\n    'ph.distancia': '\u2014',\n\n        \/\/ Missing service labels\n    'label.paxCount': 'Number of passengers*',\n    'label.flightNum': 'Flight\/Train No.',\n    'label.distance': 'Distance (km)',\n    'label.pickupPoint': 'Pickup point*',\n    'label.dropoffPoint': 'Drop-off point*',\n    'label.price': 'Price (VAT included)',\n\n    'label.taxId': 'Tax ID \/ VAT \/ Passport*',\n    'ph.cliNum': 'e.g. 120045',\n    'ph.taxId': 'ESB12345678 \/ GB123456789 \/ Passport',\n    'cliente.badge.intro': 'Enter client number and CIF\/VAT to autocomplete',\n\n    'label.cliPhone': 'Phone*',\n    'label.cliBilling': 'Billing address*',\n    'label.cliEmail': 'Email address*',\n    'hint.cliEmail': 'For this booking you may use a different email than the one registered.',\n\n    \/\/ ---- Missing Service 1 labels ----\n    'legend.serv1det': 'Service 1 details',\n    'label.pickupDate': 'Pickup date*',\n    'label.pickupTime': 'Pickup time*',\n\n    \/\/ Capacity hint\n    'hint.maxPax': 'Maximum capacity for {tipo}: {max} pax',\n\n    \/\/ Service types\n    'service.traslado': 'Transfer',\n    'service.disp4h': '4h disposal',\n    'service.disp8h': '8h disposal',\n\n    \/\/ Hint \u201cAdd another service?\u201d\n    'hint.addService2': 'If you choose \u201cYes\u201d, the Service 2 form will open in the next step.',\n    'hint.addService3': 'If you choose \u201cYes\u201d, the Service 3 form will open in the next step.',\n\n        \/\/ Yes \/ No visual\n    'yn.si': 'Yes',\n    'yn.no': 'No',\n\n    \/\/ Price \/ quotation warnings\n    'hint.customQuote': 'Due to the location of the origin or destination points, this service may require a custom quote.',\n    'hint.extraCost': 'Some services may entail an additional cost.',\n\n    'cliente.badge.search': 'Searching\u2026',\n    'cliente.badge.ok': 'Client verified: data locked (email editable)',\n    'cliente.badge.editable': 'Client found \u2014 you can edit.',\n    'cliente.badge.notfound': 'Client not found. Fill in manually.',\n\n    'estado.enviando': 'Sending\u2026',\n    'estado.error': 'Could not send. Check your connection and try again.',\n    'hint.stepRequired': 'Please complete the required fields to continue.',\n    'required.msg': 'Required field',\n    'error.maxPax': 'We can\\'t process bookings with more than {max} passengers for this vehicle. Please reduce the passenger count or choose a larger vehicle.',\n    'address.choose': 'Please select the correct address:',\n    'address.noResults': 'We couldn\u2019t find matching addresses. Adjust your search or add more details.',\n    'addressCandidates.pickupTitle': 'Select the exact pickup address',\n    'addressCandidates.dropoffTitle': 'Select the exact dropoff address',\n\n    \/\/ Client messages\n    'cliente.badge.intro': 'Enter client number and Tax ID to autocomplete',\n    'cliente.email_hint': 'For this reservation, you can use an email address different from the one in the client record.',\n\n    'vehicle.turismo':   'Sedan',\n    'vehicle.van':       'Minivan \/ Van',\n    'vehicle.minibus15': 'Minibus 15 seats',\n    'vehicle.minibus25': 'Minibus 25 seats',\n    'vehicle.bus':       'Bus',\n\n  },\n\n  ca: {\n    \/\/ Cap\u00e7alera i botons principals\n    'app.title': 'Reserves',\n    'btn.install': 'Descarrega la nostra app',\n    'btn.next': 'Seg\u00fcent',\n    'btn.prev': 'Anterior',\n    'btn.submit': 'Enviar',\n    'btn.accept': 'Acceptar',\n\n    \/\/ Stepper\n    'step.1': 'Client',\n    'step.2': 'Servei 1',\n    'step.3': 'Servei 2',\n    'step.4': 'Servei 3',\n    'step.5': 'Final',\n\n    \/\/ Bloc 1 - Client\n    'legend.cliente': 'Dades del client',\n    'label.cliNum': 'N\u00fam. de client (opcional)',\n    'label.cliTax': 'CIF, IVA o passaport*',\n    'label.cliName': 'Nom del client*',\n    'label.cliPhone': 'Tel\u00e8fon*',\n    'label.cliBilling': 'Adre\u00e7a de facturaci\u00f3*',\n    'label.cliEmail': 'Correu electr\u00f2nic*',\n    'hint.cliEmail': 'Per a aquesta reserva pots indicar un correu diferent del de la fitxa.',\n    'hint.maxPax': 'Capacitat m\u00e0xima per a {tipo}: {max} passatgers',\n\n    \/\/ Bloc 2 - Servei\n    'legend.serv1': 'Servei 1',\n    'legend.serv1.detalles': 'Detalls del servei 1',\n    'label.lugar': 'Lloc*',\n    'label.vehiculo': 'Vehicle*',\n    'label.servicio': 'Servei*',\n    'label.fecha': 'Data de recollida*',\n    'label.hora': 'Hora de recollida*',\n    'label.pax': 'Nombre de passatgers*',\n    'label.flight': 'N\u00fam. vol\/tren',\n    'label.pickup': 'Punt de recollida*',\n    'label.pickupExact': 'Adre\u00e7a exacta de recollida',\n    'label.dropoff': 'Punt de destinaci\u00f3*',\n    'label.dropoffExact': 'Adre\u00e7a exacta de destinaci\u00f3',\n    'label.distancia': 'Dist\u00e0ncia (km)',\n    'label.precio': 'Preu (IVA incl\u00f2s)',\n    'label.otro': 'Altres (especifica)',\n    'label.addContact': 'Afegir una persona de contacte a m\u00e9s del client?',\n    'label.contactName': 'Nom del contacte*',\n    'label.contactPhone': 'Tel\u00e8fon de contacte*',\n    'label.contactEmail': 'Correu de contacte*',\n    'label.addService': 'Afegir servei?',\n    'hint.addService': 'Si tries \u201cS\u00ed\u201d, al pas seg\u00fcent s\u2019obrir\u00e0 el formulari del servei seg\u00fcent.',\n\n    \/\/ Extres\n    'extras.cartel': 'Cartell amb nom',\n    'extras.cartel.comment': 'Indiqueu el nom al quadre seg\u00fcent',\n    'extras.asiento': 'Seient infantil',\n    'extras.asiento.comment': 'Indiqueu l\\'edat del nen al quadre seg\u00fcent',\n    'extras.chofer': 'Xofer amb idioma',\n    'extras.chofer.comment': 'Indiqueu l\\'idioma sol\u00b7licitat al quadre seg\u00fcent',\n\n    \/\/ Comentaris\n    'legend.comentarios': 'Comentaris',\n    'label.comentarios': 'Comentaris o observacions',\n\n    \/\/ Pagament\n    'legend.pago': 'Forma de pagament',\n    'label.pago.total': 'Preu total:',\n    'label.pago.servicios': 'Serveis contractats:',\n    'label.pago.metodo': 'Forma de pagament*',\n    'pago.metodo.transfer': 'Transfer\u00e8ncia banc\u00e0ria',\n    'pago.metodo.cargo': 'C\u00e0rrec al compte del client',\n    'pago.metodo.tarjeta': 'Targeta de cr\u00e8dit',\n    'pago.bank.title': 'Dades banc\u00e0ries de l\u2019empresa',\n    'pago.bank.text':\n      'Titular: MINI VIP\\'S CAR DRIVERS, S.L.\\n' +\n      'CIF: B62551551\\n' +\n      'Banc: Banco Sabadell\\n' +\n      'IBAN: ES74 0081 7011 1500 0172 4681\\n' +\n      'Concepte: Reserva \u2014 N\u00fam. de client ___ \u2014 N\u00fam. de reserva \u2014',\n    'pago.bank.help':\n      'Si tries transfer\u00e8ncia o c\u00e0rrec a compte, fes servir aquestes dades. Pots enviar-nos el justificant responent al correu de confirmaci\u00f3.',\n    'pago.card.info':\n      'Si decideu pagar amb targeta, al correu de confirmaci\u00f3 hi inclourem un bot\u00f3 amb l\\'enlla\u00e7 a la nostra passarel\u00b7la de pagament segura.',\n\n    \/\/ Toast final\n    'toast.title': 'Comanda enviada',\n    'toast.text': 'La teva comanda s\u2019ha enviat correctament.',\n\n    \/\/ Placeholders\n    'ph.cliemail': 'email@domini.com',\n    'ph.clinum': 'Ex: 120045',\n    'ph.clitax': 'ESB12345678 \/ GB123456789 \/ Passaport',\n    'ph.pickup': 'Hotel, carrer, aeroport\u2026',\n    'ph.dropoff': 'Hotel, carrer, esdeveniment\u2026',\n    'ph.pickupExact': '(auto si escau)',\n    'ph.dropoffExact': '(auto si escau)',\n    'ph.flight': 'Ex.: IB1234 \/ AVE 03192',\n    'ph.otro': 'Descriu qualsevol necessitat addicional',\n    'ph.distancia': '\u2014',\n\n    \/\/ Etiquetes extra (bloc servei)\n    'label.paxCount': 'Nombre de passatgers*',\n    'label.flightNum': 'N\u00fam. vol\/tren',\n    'label.distance': 'Dist\u00e0ncia (km)',\n    'label.pickupPoint': 'Punt de recollida*',\n    'label.dropoffPoint': 'Punt de destinaci\u00f3*',\n    'label.price': 'Preu (IVA incl\u00f2s)',\n\n    'label.taxId': 'CIF, IVA o passaport*',\n    'ph.cliNum': 'Ex: 120045',\n    'ph.taxId': 'ESB12345678 \/ GB123456789 \/ Passaport',\n    'cliente.badge.intro':\n      'Introdueix n\u00fam. de client i CIF\/VAT per autocompletar',\n\n    \/\/ S\u00ed \/ No visual\n    'yn.si': 'S\u00ed',\n    'yn.no': 'No',\n\n    \/\/ Avisos de preu \/ pressupost\n    'hint.customQuote':\n      'Per la ubicaci\u00f3 dels punts d\u2019origen o destinaci\u00f3, aquest servei pot necessitar un pressupost personalitzat.',\n    'hint.extraCost':\n      'Alguns serveis poden comportar un cost addicional.',\n\n    'cliente.badge.search': 'Cercant\u2026',\n    'cliente.badge.ok':\n      'Client verificat: dades bloquejades (correu editable)',\n    'cliente.badge.editable': 'Client trobat \u2014 pots editar.',\n    'cliente.badge.notfound':\n      'Client no trobat. Omple les dades manualment.',\n\n    \/\/ Tipus de servei\n    'service.traslado': 'Trasllat',\n    'service.disp4h': 'Disposici\u00f3 4h',\n    'service.disp8h': 'Disposici\u00f3 8h',\n\n    'estado.enviando': 'Enviant\u2026',\n    'estado.error':\n      'No s\\'ha pogut enviar. Comprova la connexi\u00f3 i torna-ho a provar.',\n    'hint.stepRequired':\n      'Completa els camps obligatoris per continuar.',\n    'required.msg': 'Camp obligatori',\n    'error.maxPax': 'No podem processar reserves amb m\u00e9s de {max} passatgers per a aquest vehicle. Redueix el nombre de passatgers o tria un vehicle amb m\u00e9s capacitat.',\n    'address.choose': 'Selecciona l\u2019adre\u00e7a correcta:',\n    'address.noResults': 'No hem trobat adreces coincidents. Ajusta la cerca o afegeix m\u00e9s detalls.',\n    'addressCandidates.pickupTitle': 'Selecciona l\\'adre\u00e7a exacta de recollida',\n    'addressCandidates.dropoffTitle': 'Selecciona l\\'adre\u00e7a exacta de destinaci\u00f3',\n\n    \/\/ Missatges client\n    'cliente.email_hint':\n      'Per a aquesta reserva pots indicar un correu diferent del de la fitxa.',\n\n    \/\/ Hint \u201cAfegir un altre servei?\u201d\n    'hint.addService2':\n      'Si tries \u201cS\u00ed\u201d, al pas seg\u00fcent s\u2019obrir\u00e0 el formulari del Servei 2.',\n    'hint.addService3':\n      'Si tries \u201cS\u00ed\u201d, al pas seg\u00fcent s\u2019obrir\u00e0 el formulari del Servei 3.',\n\n    \/\/ Vehicles\n    'vehicle.turismo': 'Turisme',\n    'vehicle.van': 'Monovolum \/ Van',\n    'vehicle.minibus15': 'Minib\u00fas 15',\n    'vehicle.minibus25': 'Minib\u00fas 25',\n    'vehicle.bus': 'Autob\u00fas',\n\n    \/\/ Labels diferents de servei\n    'legend.serv1det': 'Detalls del servei 1',\n    'label.pickupDate': 'Data de recollida*',\n    'label.pickupTime': 'Hora de recollida*',\n\n    'legend.serv2det': 'Detalls del servei 2',\n    'legend.serv3det': 'Detalls del servei 3',\n\n    \/\/ Repeticions bloc servei\n    'legend.serv1': 'Servei 1',\n    'legend.serv1.detalles': 'Detalls del servei 1',\n    'legend.serv2': 'Servei 2',\n    'legend.serv2.detalles': 'Detalls del servei 2',\n    'legend.serv3': 'Servei 3',\n    'legend.serv3.detalles': 'Detalls del servei 3',\n\n    'legend.serv1det': 'Detalls del servei 1',\n    'legend.serv2det': 'Detalls del servei 2',\n    'legend.serv3det': 'Detalls del servei 3',\n\n    \/\/ Toast (comanda enviada)\n    'toast.body': 'La teva comanda s\u2019ha enviat correctament.',\n    'btn.ok': 'D\u2019acord'\n  },\n\n  fr: {\n    \/\/ En-t\u00eate et boutons principaux\n    'app.title': 'R\u00e9server',\n    'btn.install': 'T\u00e9l\u00e9chargez notre app',\n    'btn.next': 'Suivant',\n    'btn.prev': 'Pr\u00e9c\u00e9dent',\n    'btn.submit': 'Envoyer',\n    'btn.accept': 'Valider',\n\n    \/\/ Stepper\n    'step.1': 'Client',\n    'step.2': 'Service 1',\n    'step.3': 'Service 2',\n    'step.4': 'Service 3',\n    'step.5': 'Fin',\n\n    \/\/ Bloc 1 - Client\n    'legend.cliente': 'Donn\u00e9es du client',\n    'label.cliNum': 'N\u00b0 de client (optionnel)',\n    'label.cliTax': 'ID fiscal, TVA ou passeport*',\n    'label.cliName': 'Nom du client*',\n    'label.cliPhone': 'T\u00e9l\u00e9phone*',\n    'label.cliBilling': 'Adresse de facturation*',\n    'label.cliEmail': 'Adresse e-mail*',\n    'hint.cliEmail':\n      'Pour cette r\u00e9servation, vous pouvez indiquer un e-mail diff\u00e9rent de celui de la fiche.',\n    'hint.maxPax':\n      'Capacit\u00e9 maximale pour {tipo} : {max} passagers',\n\n    \/\/ Bloc 2 - Service\n    'legend.serv1': 'Service 1',\n    'legend.serv1.detalles': 'D\u00e9tails du service 1',\n    'label.lugar': 'Lieu*',\n    'label.vehiculo': 'V\u00e9hicule*',\n    'label.servicio': 'Service*',\n    'label.fecha': 'Date de prise en charge*',\n    'label.hora': 'Heure de prise en charge*',\n    'label.pax': 'Nombre de passagers*',\n    'label.flight': 'N\u00b0 de vol\/train',\n    'label.pickup': 'Point de prise en charge*',\n    'label.pickupExact': 'Adresse exacte de prise en charge',\n    'label.dropoff': 'Point de destination*',\n    'label.dropoffExact': 'Adresse exacte de destination',\n    'label.distancia': 'Distance (km)',\n    'label.precio': 'Prix (TVA incluse)',\n    'label.otro': 'Autre (pr\u00e9ciser)',\n    'label.addContact':\n      'Ajouter une personne de contact en plus du client ?',\n    'label.contactName': 'Nom du contact*',\n    'label.contactPhone': 'T\u00e9l\u00e9phone du contact*',\n    'label.contactEmail': 'E-mail du contact*',\n    'label.addService': 'Ajouter un service ?',\n    'hint.addService':\n      'Si vous choisissez \u00ab Oui \u00bb, le formulaire du service suivant s\u2019ouvrira \u00e0 l\u2019\u00e9tape suivante.',\n\n    \/\/ Extras\n    'extras.cartel': 'Pancarte avec nom',\n    'extras.cartel.comment': 'Veuillez indiquer le nom dans la case ci-dessous',\n    'extras.asiento': 'Si\u00e8ge enfant',\n    'extras.asiento.comment': 'Veuillez indiquer l\\'\u00e2ge de l\\'enfant dans la case ci-dessous',\n    'extras.chofer': 'Chauffeur avec langue',\n    'extras.chofer.comment': 'Veuillez indiquer la langue souhait\u00e9e dans la case ci-dessous',\n\n    \/\/ Commentaires\n    'legend.comentarios': 'Commentaires',\n    'label.comentarios': 'Commentaires ou remarques',\n\n    \/\/ Paiement\n    'legend.pago': 'Mode de paiement',\n    'label.pago.total': 'Prix total :',\n    'label.pago.servicios': 'Services r\u00e9serv\u00e9s :',\n    'label.pago.metodo': 'Mode de paiement*',\n    'pago.metodo.transfer': 'Virement bancaire',\n    'pago.metodo.cargo': 'Pr\u00e9l\u00e8vement sur le compte du client',\n    'pago.metodo.tarjeta': 'Carte de cr\u00e9dit',\n    'pago.bank.title': 'Coordonn\u00e9es bancaires de l\u2019entreprise',\n    'pago.bank.text':\n      'Titulaire : MINI VIP\\'S CAR DRIVERS, S.L.\\n' +\n      'NIF : B62551551\\n' +\n      'Banque : Banco Sabadell\\n' +\n      'IBAN : ES74 0081 7011 1500 0172 4681\\n' +\n      'Objet : R\u00e9servation \u2014 N\u00b0 client ___ \u2014 N\u00b0 de r\u00e9servation \u2014',\n    'pago.bank.help':\n      'Si vous choisissez virement ou pr\u00e9l\u00e8vement, utilisez ces coordonn\u00e9es. Vous pouvez nous envoyer le justificatif en r\u00e9pondant \u00e0 l\\'e-mail de confirmation.',\n    'pago.card.info':\n      'Si vous choisissez de payer par carte, l\\'e-mail de confirmation inclura un bouton avec le lien vers notre passerelle de paiement s\u00e9curis\u00e9e.',\n\n    \/\/ Toast final\n    'toast.title': 'Commande envoy\u00e9e',\n    'toast.text': 'Votre commande a \u00e9t\u00e9 envoy\u00e9e avec succ\u00e8s.',\n\n    \/\/ Placeholders\n    'ph.cliemail': 'email@domaine.com',\n    'ph.clinum': 'Ex : 120045',\n    'ph.clitax': 'ESB12345678 \/ GB123456789 \/ Passeport',\n    'ph.pickup': 'H\u00f4tel, rue, a\u00e9roport\u2026',\n    'ph.dropoff': 'H\u00f4tel, rue, \u00e9v\u00e9nement\u2026',\n    'ph.pickupExact': '(auto le cas \u00e9ch\u00e9ant)',\n    'ph.dropoffExact': '(auto le cas \u00e9ch\u00e9ant)',\n    'ph.flight': 'Ex. : IB1234 \/ AVE 03192',\n    'ph.otro': 'D\u00e9crivez tout besoin suppl\u00e9mentaire',\n    'ph.distancia': '\u2014',\n\n    \/\/ \u00c9tiquettes suppl\u00e9mentaires\n    'label.paxCount': 'Nombre de passagers*',\n    'label.flightNum': 'N\u00b0 de vol\/train',\n    'label.distance': 'Distance (km)',\n    'label.pickupPoint': 'Point de prise en charge*',\n    'label.dropoffPoint': 'Point de destination*',\n    'label.price': 'Prix (TVA incluse)',\n\n    'label.taxId': 'ID fiscal, TVA ou passeport*',\n    'ph.cliNum': 'Ex : 120045',\n    'ph.taxId': 'ESB12345678 \/ GB123456789 \/ Passeport',\n    'cliente.badge.intro':\n      'Saisissez le n\u00b0 de client et le CIF\/TVA pour l\u2019auto-compl\u00e9tion',\n\n    \/\/ Oui \/ Non visuel\n    'yn.si': 'Oui',\n    'yn.no': 'Non',\n\n    \/\/ Avertissements de prix \/ devis\n    'hint.customQuote':\n      'En raison de la localisation des points de d\u00e9part ou d\u2019arriv\u00e9e, ce service peut n\u00e9cessiter un devis personnalis\u00e9.',\n    'hint.extraCost':\n      'Certains services peuvent entra\u00eener un co\u00fbt suppl\u00e9mentaire.',\n\n    'cliente.badge.search': 'Recherche\u2026',\n    'cliente.badge.ok':\n      'Client v\u00e9rifi\u00e9 : donn\u00e9es verrouill\u00e9es (e-mail modifiable)',\n    'cliente.badge.editable':\n      'Client trouv\u00e9 \u2014 vous pouvez modifier.',\n    'cliente.badge.notfound':\n      'Client introuvable. Compl\u00e9tez les donn\u00e9es manuellement.',\n\n    \/\/ Types de service\n    'service.traslado': 'Transfert',\n    'service.disp4h': 'Mise \u00e0 disposition 4 h',\n    'service.disp8h': 'Mise \u00e0 disposition 8 h',\n\n    'estado.enviando': 'Envoi\u2026',\n    'estado.error':\n      '\u00c9chec de l\\'envoi. V\u00e9rifiez votre connexion et r\u00e9essayez.',\n    'hint.stepRequired':\n      'Compl\u00e9tez les champs obligatoires pour continuer.',\n    'required.msg': 'Champ obligatoire',\n    'error.maxPax': 'Nous ne pouvons pas traiter des r\u00e9servations avec plus de {max} passagers pour ce v\u00e9hicule. R\u00e9duisez le nombre de passagers ou choisissez un v\u00e9hicule avec une capacit\u00e9 sup\u00e9rieure.',\n    'address.choose': 'S\u00e9lectionnez l\u2019adresse correcte :',\n    'address.noResults': 'Aucune adresse correspondante. Affinez la recherche ou ajoutez plus de d\u00e9tails.',\n    'addressCandidates.pickupTitle': 'S\u00e9lectionnez l\\'adresse exacte de prise en charge',\n    'addressCandidates.dropoffTitle': 'S\u00e9lectionnez l\\'adresse exacte de destination',\n\n    \/\/ Messages client\n    'cliente.email_hint':\n      'Pour cette r\u00e9servation, vous pouvez utiliser une adresse e-mail diff\u00e9rente de celle enregistr\u00e9e.',\n\n    \/\/ Hint \u201cAjouter un autre service ?\u201d\n    'hint.addService2':\n      'Si vous choisissez \u00ab Oui \u00bb, le formulaire du Service 2 s\u2019ouvrira \u00e0 l\u2019\u00e9tape suivante.',\n    'hint.addService3':\n      'Si vous choisissez \u00ab Oui \u00bb, le formulaire du Service 3 s\u2019ouvrira \u00e0 l\u2019\u00e9tape suivante.',\n\n    \/\/ V\u00e9hicules\n    'vehicle.turismo': 'Berline',\n    'vehicle.van': 'Monospace \/ Van',\n    'vehicle.minibus15': 'Minibus 15 places',\n    'vehicle.minibus25': 'Minibus 25 places',\n    'vehicle.bus': 'Autocar',\n\n    \/\/ Labels service\n    'legend.serv1det': 'D\u00e9tails du service 1',\n    'label.pickupDate': 'Date de prise en charge*',\n    'label.pickupTime': 'Heure de prise en charge*',\n\n    'legend.serv2det': 'D\u00e9tails du service 2',\n    'legend.serv3det': 'D\u00e9tails du service 3',\n\n    \/\/ D\u00e9finitions r\u00e9p\u00e9t\u00e9es pour coh\u00e9rence\n    'legend.serv1': 'Service 1',\n    'legend.serv1.detalles': 'D\u00e9tails du service 1',\n    'legend.serv2': 'Service 2',\n    'legend.serv2.detalles': 'D\u00e9tails du service 2',\n    'legend.serv3': 'Service 3',\n    'legend.serv3.detalles': 'D\u00e9tails du service 3',\n\n    'legend.serv1det': 'D\u00e9tails du service 1',\n    'legend.serv2det': 'D\u00e9tails du service 2',\n    'legend.serv3det': 'D\u00e9tails du service 3',\n\n    \/\/ Toast (commande envoy\u00e9e)\n    'toast.body': 'Votre commande a \u00e9t\u00e9 envoy\u00e9e avec succ\u00e8s.',\n    'btn.ok': 'OK'\n  }\n\n};\n\n\nlet currentLang = localStorage.getItem('bukas_lang') || 'es';\n\nfunction t(key, vars = {}) {\n  const table = I18N[currentLang] || I18N.es;\n  const base = table[key] ?? I18N.es[key] ?? key;\n  return base.replace(\/\\{(\\w+)\\}\/g, (_, k) => (vars[k] ?? ''));\n}\n\n\/\/ Expose t() globally so it's accessible from IIFEs\nwindow.t = t;\n\nfunction applyTranslations(lang = currentLang) {\n  currentLang = lang;\n\n  \/\/ Textos normales\n  document.querySelectorAll('[data-i18n]').forEach(el => {\n    const key = el.getAttribute('data-i18n');\n    el.textContent = t(key);\n  });\n\n  \/\/ Placeholders\n  document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {\n    const key = el.getAttribute('data-i18n-placeholder');\n    el.placeholder = t(key);\n  });\n\n  \/\/ aria-label\n  document.querySelectorAll('[data-i18n-aria-label]').forEach(el => {\n    const key = el.getAttribute('data-i18n-aria-label');\n    el.setAttribute('aria-label', t(key));\n  });\n\n  \/\/ \ud83d\udd39 NUEVO: badges del cliente (intro, buscando, ok, etc.)\n  document.querySelectorAll('[data-i18n-badge]').forEach(el => {\n    const key = el.getAttribute('data-i18n-badge');\n    el.textContent = t(key);\n  });\n\n  \/\/ \ud83d\udd39 NUEVO: actualizar textos de los toggles S\u00ed\/No\n  document.querySelectorAll('.yn-toggle').forEach(group => {\n    const buttons = group.querySelectorAll('button');\n    if (buttons[0]) buttons[0].textContent = t('yn.si'); \/\/ S\u00ed \/ Yes\n    if (buttons[1]) buttons[1].textContent = t('yn.no'); \/\/ No\n  });\n}\n\n\n\n\/* ===========================\n   CONFIG: datos y cat\u00e1logos\n   =========================== *\/\nconst CONFIG = {\n  CLIENTS_URL: \"\", \/\/ <-- si tienes Apps Script para buscar clientes, pon la URL aqu\u00ed. Si est\u00e1 vac\u00edo: usa demo local\n  clientesDemo: [\n    { num:\"120045\", tax:\"ESB62551551\", nombre:\"MINI VIP'S CAR DRIVERS, S.L.\", tel:\"+34 900 123 123\", email:\"reservas@minivips.es\", billing:\"C\/ Ejemplo 123, Barcelona\" },\n    { num:\"110010\", tax:\"ESX1234567Z\", nombre:\"Agencia Ficticia S.A.\", tel:\"+34 931 111 222\", email:\"ops@agenciaficticia.com\", billing:\"Av. Principal 456, Madrid\" }\n  ],\n  lugares: [\n    { value:\"Barcelona\", icon:\"\ud83c\udfd6\ufe0f\" },\n    { value:\"Madrid\",    icon:\"\ud83c\udfdb\ufe0f\" }\n  ],\n  vehiculos: [\n    { value:\"Coche\",      icon:\"\ud83d\ude97\", maxPax:3 },\n    { value:\"Van\",        icon:\"\ud83d\ude90\", maxPax:7 },\n    { value:\"Minibus 15\", icon:\"\ud83d\ude8c\", maxPax:15 },\n    { value:\"Minibus 25\", icon:\"\ud83d\ude8c\", maxPax:25 },\n    { value:\"Bus 50\",     icon:\"\ud83d\ude8c\", maxPax:50 }\n  ],\n  servicios: {\n    \/\/ por lugar + veh\u00edculo (si no hay match exacto, usa \"default\")\n    \/\/ IMPORTANT: Vehicle keys MUST match canonical values: \"Coche\", \"Van\", \"Minibus 15\", \"Minibus 25\", \"Bus 50\"\n    Barcelona: {\n      \"Coche\":      [\"Traslado Aeropuerto \u21c4 Hotel\",\"Disposici\u00f3n 4 horas en la Ciudad\",\"City tour 3h\"],\n      \"Van\":        [\"Traslado Aeropuerto \u21c4 Hotel\",\"Disposici\u00f3n 4 horas en la Ciudad\",\"Event shuttle\"],\n      \"Minibus 15\": [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"],\n      \"Minibus 25\": [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"],\n      \"Bus 50\":     [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"],\n      default:      [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"]\n    },\n    Madrid: {\n      \"Coche\":      [\"Traslado Atocha \u21c4 Hotel\",\"Disposici\u00f3n 4 horas en la Ciudad\",\"City tour 3h\"],\n      \"Van\":        [\"Traslado Barajas \u21c4 Hotel\",\"Disposici\u00f3n 4 horas en la Ciudad\",\"Ifema Transfer\"],\n      \"Minibus 15\": [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"],\n      \"Minibus 25\": [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"],\n      \"Bus 50\":     [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"],\n      default:      [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"]\n    },\n    default: {\n      default: [\"Traslado punto a punto\",\"Disposici\u00f3n por horas\"]\n    }\n  },\n  \/\/ datalists por ciudad: label visible -> direcci\u00f3n exacta\/normalizada\n  puntos: {\n    Barcelona: {\n      recogida: {\n        \"Aeropuerto BCN T1\": \"Aeroport de Barcelona-El Prat, Terminal 1\",\n        \"Aeropuerto BCN T2\": \"Aeroport de Barcelona-El Prat, Terminal 2\",\n        \"Sants Estaci\u00f3\":     \"Pla\u00e7a dels Pa\u00efsos Catalans, 1, 08014 Barcelona\",\n        \"Hotel Arts\":        \"C\/ de la Marina, 19-21, 08005 Barcelona\"\n      },\n      destino: {\n        \"Fira Gran Via\":     \"Av. Joan Carles I, 64, 08908 L'Hospitalet de Llobregat\",\n        \"Camp Nou\":          \"C. d'Ar\u00edstides Maillol, 12, 08028 Barcelona\",\n        \"La Pedrera\":        \"Pg. de Gr\u00e0cia, 92, 08008 Barcelona\"\n      }\n    },\n    Madrid: {\n      recogida: {\n        \"Barajas T4\":        \"Av de la Hispanidad, s\/n, 28042 Madrid\",\n        \"Atocha\":            \"Plaza Emperador Carlos V, 28045 Madrid\",\n        \"Chamart\u00edn\":         \"Agust\u00edn de Fox\u00e1, s\/n, 28036 Madrid\",\n        \"Hotel Riu Plaza\":   \"C\/ Gran V\u00eda, 84, 28013 Madrid\"\n      },\n      destino: {\n        \"IFEMA\":             \"Av. del Parten\u00f3n, 5, 28042 Madrid\",\n        \"Santiago Bernab\u00e9u\": \"Av. de Concha Espina, 1, 28036 Madrid\",\n        \"Plaza Mayor\":       \"Plaza Mayor, 28012 Madrid\"\n      }\n    }\n  }\n};\n\/\/ Service constants: exact labels as they appear in the tariff sheet\nconst SRV_TRASLADO = 'Traslado';\nconst SRV_DISPO_4H = 'Disposici\u00f3n 4 horas en la Ciudad';\nconst SRV_DISPO_8H = 'Disposici\u00f3n 8 horas en la Ciudad';\n\nconst SERVICIOS_FIJOS = [SRV_TRASLADO, SRV_DISPO_4H, SRV_DISPO_8H];\nconst SERVICIO_LABEL_KEYS = {\n  [SRV_TRASLADO]: \"service.traslado\",\n  [SRV_DISPO_4H]: \"service.disp4h\",\n  [SRV_DISPO_8H]: \"service.disp8h\"\n};\nconst CITY_CENTER_HINTS = {\n  Barcelona: { lat: 41.3874, lng: 2.1686, radius: 25000 },\n  Madrid:    { lat: 40.4168, lng: -3.7038, radius: 30000 }\n};\n\/* =============== Utils DOM =============== *\/\nconst qs  = (sel, ctx=document) => ctx.querySelector(sel);\nconst qsa = (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel));\nfunction isVisible(el){\n  if (!el || el.disabled) return false;\n  let cur = el;\n  while (cur && cur !== document.body){\n    const cs = getComputedStyle(cur);\n    if (cs.display === 'none' || cs.visibility === 'hidden') return false;\n    cur = cur.parentElement;\n  }\n  return true;\n}\n\n\/\/ === Simple required-field validation (matches legacy behavior) ===\nfunction markInvalid(input, msg){\n  input.classList.add('is-invalid');\n  input.setAttribute('aria-invalid', 'true');\n\n  \/\/ Add\/replace a .field-error right after the input (inside the same label)\n  const label = input.closest('label') || input.parentElement;\n  if (!label) return;\n\n  let hint = label.querySelector('.field-error');\n  if (!hint){\n    hint = document.createElement('div');\n    hint.className = 'field-error';\n    label.appendChild(hint);\n  }\n  hint.textContent = msg || (t('hint.stepRequired') || 'Este campo es obligatorio.');\n}\n\nfunction clearInvalid(input){\n  input.classList.remove('is-invalid');\n  input.removeAttribute('aria-invalid');\n\n  const label = input.closest('label') || input.parentElement;\n  if (!label) return;\n\n  const hint = label.querySelector('.field-error');\n  if (hint) hint.remove();\n}\n\nfunction isEmpty(input){\n  const v = (input.value ?? '').trim();\n  if (input.type === 'select-one') return v === '' || v === '\u2014';\n  return v === '';\n}\n\n\/**\n * Validate all required controls inside a container (fieldset\/section\/form)\n * Returns { ok, firstInvalid }\n *\/\nfunction validateContainer(container){\n  const req = Array.from(container.querySelectorAll('input[required], select[required], textarea[required]'));\n  let firstInvalid = null;\n\n  for (const el of req){\n    \/\/ Skip hidden\/disabled fields\n    if (!isVisible(el) || el.disabled) continue;\n    \n    \/\/ Skip hidden exact address fields (they're not user-editable, managed by Places)\n    if (el.id && (el.id.includes('Exact') || el.id.includes('Lat') || el.id.includes('Lng') || el.id.includes('PlaceId') || el.id.includes('Name'))) continue;\n\n    \/\/ Clear previous state\n    clearInvalid(el);\n\n    \/\/ Browser-native validity first, then empty check\n    const invalid = (el.willValidate && !el.checkValidity()) || isEmpty(el);\n    if (invalid){\n      if (!firstInvalid) firstInvalid = el;\n      markInvalid(el);\n    }\n  }\n  return { ok: !firstInvalid, firstInvalid };\n}\n\n\/\/ Scroll + focus helper\nfunction focusInvalid(el){\n  try {\n    el.scrollIntoView({ behavior:'smooth', block:'center' });\n    el.focus({ preventScroll:true });\n  } catch(_){}\n}\n\n\/\/ Legacy function for compatibility (uses new helpers)\nfunction showStepErrors(blk){\n  if (!blk) return;\n  const { ok, firstInvalid } = validateContainer(blk);\n  if (!ok && firstInvalid) {\n    focusInvalid(firstInvalid);\n  }\n}\n\n\n\n\/* =============== Stepper =============== *\/\nfunction makeStepper(form){\n  const blocks  = qsa('.form-block', form);\n  const stepper = qsa('.stepper__item', form.parentElement);\n\n  function currentIndex(){\n    const i = blocks.findIndex(b => b.dataset.active === '1');\n    return i >= 0 ? i : 0;\n  }\n  function setActive(i){\n    i = Math.max(0, Math.min(i, blocks.length - 1));\n    blocks.forEach((b, idx) => { b.dataset.active = (idx===i?'1':'0'); b.hidden = idx!==i; });\n    stepper.forEach((s, idx) => s.dataset.state = idx < i ? 'done' : (idx===i ? 'active' : 'idle'));\n    const first = blocks[i].querySelector('input,select,textarea');\n    first?.focus?.();\n    refreshNext();\n  }\n  function visibleRequired(blk){\n    return qsa('input, select, textarea', blk).filter(el => isVisible(el) && el.required);\n  }\n  function canAdvance(i){\n    const blk = blocks[i];\n    if (!blk) return false;\n    if (blk.dataset.block==='3' && (qs('#t2')?.value!=='S\u00ed' || qs('#serv2')?.hidden)) return true;\n    if (blk.dataset.block==='4' && (qs('#t3')?.value!=='S\u00ed' || qs('#serv3')?.hidden)) return true;\n    return visibleRequired(blk).every(el => el.checkValidity());\n  }\n  function refreshNext(){\n    const i = currentIndex();\n    const nextBtn = blocks[i]?.querySelector('[data-next]');\n    if (!nextBtn) return;\n    nextBtn.disabled = !canAdvance(i);\n  }\n\n  \/\/ === NAV HELPERS: decidir siguiente\/anterior v\u00e1lido saltando S2\/S3 cuando est\u00e1n en \"No\" ===\nfunction isS2On(){ \n  return (qs('#t2')?.value === 'S\u00ed') && !qs('#serv2')?.hidden; \n}\nfunction isS3On(){ \n  return (qs('#t3')?.value === 'S\u00ed') && !qs('#serv3')?.hidden; \n}\n\n\/\/ siguiente bloque habilitado desde el \u00edndice actual\nfunction nextEnabledFrom(idx){\n  let t = Math.min(idx + 1, blocks.length - 1);\n  \/\/ \u00cdndices: 0=Cliente, 1=S1, 2=S2, 3=S3, 4=Final\n  if (t === 2 && !isS2On()) t++;   \/\/ salta S2 si est\u00e1 en \"No\"\n  if (t === 3 && !isS3On()) t++;   \/\/ salta S3 si est\u00e1 en \"No\"\n  return t;\n}\n\n\/\/ bloque habilitado anterior desde el \u00edndice actual\nfunction prevEnabledFrom(idx){\n  let t = Math.max(idx - 1, 0);\n  if (t === 3 && !isS3On()) t--;   \/\/ salta S3 si est\u00e1 en \"No\"\n  if (t === 2 && !isS2On()) t--;   \/\/ salta S2 si est\u00e1 en \"No\"\n  return t;\n}\n\n  \nform.addEventListener('click', e=>{\n  const btn = e.target.closest('[data-next],[data-prev]');\n  if(!btn) return;\n  e.preventDefault();\n\n  \/\/ === Estado real de S2\/S3: deben estar en \"S\u00ed\" y visibles\n  const isS2On = () => (qs('#t2')?.value === 'S\u00ed') && !qs('#serv2')?.hidden;\n  const isS3On = () => (qs('#t3')?.value === 'S\u00ed') && !qs('#serv3')?.hidden;\n\n  \/\/ === Navegaci\u00f3n habilitada (0=Cliente,1=S1,2=S2,3=S3,4=Final)\n  const nextEnabledFrom = (idx) => {\n    let t = Math.min(idx + 1, blocks.length - 1);\n    if (t === 2 && !isS2On()) t++; \/\/ salta S2 si est\u00e1 en \"No\"\n    if (t === 3 && !isS3On()) t++; \/\/ salta S3 si est\u00e1 en \"No\"\n    return t;\n  };\n  const prevEnabledFrom = (idx) => {\n    let t = Math.max(idx - 1, 0);\n    if (t === 3 && !isS3On()) t--; \/\/ salta S3 si est\u00e1 en \"No\"\n    if (t === 2 && !isS2On()) t--; \/\/ salta S2 si est\u00e1 en \"No\"\n    return t;\n  };\n\n  const i = (()=>{\n    const k = blocks.findIndex(b => b.dataset.active === '1');\n    return k >= 0 ? k : 0;\n  })();\n\n    if (btn.hasAttribute('data-next')){\n    \/\/ Validate current block before advancing\n    const { ok, firstInvalid } = validateContainer(blocks[i]);\n    if (!ok){\n      e.preventDefault();\n      focusInvalid(firstInvalid);\n      return;\n    }\n    \/\/ If valid, proceed to next step\n    setActive(nextEnabledFrom(i));\n  } else {\n    setActive(prevEnabledFrom(i));\n  }\n\n\n  \/\/ --- init perezoso de S2\/S3 si se han encendido\n  try{\n    const t2 = qs('#t2')?.value || 'No';\n    const t3 = qs('#t3')?.value || 'No';\n    const serv2 = qs('#serv2');\n    const serv3 = qs('#serv3');\n    if (t2 === 'S\u00ed' && serv2 && !serv2.__init && typeof initServicio === 'function'){\n      serv2.hidden = false; serv2.dataset.open = '1';\n      initServicio(2); serv2.__init = true;\n    }\n    if (t3 === 'S\u00ed' && serv3 && !serv3.__init && typeof initServicio === 'function'){\n      serv3.hidden = false; serv3.dataset.open = '1';\n      initServicio(3); serv3.__init = true;\n    }\n  } catch(_) {}\n\n  \/\/ --- rec\u00e1lculos no bloqueantes\n  queueMicrotask(()=>{\n    try{\n      window.cotizar?.('1'); window.cotizar?.('2'); window.cotizar?.('3');\n      typeof actualizarTotales === 'function' && actualizarTotales();\n      window.__bukasStepper?.refreshNext?.();\n    }catch(_){}\n  });\n});\n\n\n\n  ['input','change','blur'].forEach(ev=>{\n    form.addEventListener(ev,(e)=>{\n      const blk = blocks[currentIndex()];\n      if (blk && blk.contains(e.target)){\n        clearTimeout(blk.__deb);\n        blk.__deb=setTimeout(refreshNext,80);\n      }\n    });\n  });\n  return { setActive, refreshNext };\n}\n\/* =============== Chips con iconos =============== *\/\n\/\/ === renderChipsForSelect (versi\u00f3n estable) ===\nfunction renderChipsForSelect(select){\n  \/\/ Contenedor correcto: .chips[data-chips-for=\"ID\"]\n  const wrap = select.parentElement.querySelector(`.chips[data-chips-for=\"${select.id}\"]`);\n  if (!wrap) return;\n\n  \/\/ Lista seg\u00fan el tipo de select\n  let list = [];\n  if (select.id.includes('Lugar')) {\n    list = CONFIG.lugares;              \/\/ [{value, icon}]\n  } else if (select.id.includes('Vehiculo')) {\n    list = CONFIG.vehiculos;            \/\/ [{value, icon, maxPax}]\n  } else if (select.id.includes('Servicio')) {\n    \/\/ Para Servicio usamos las <option> actuales\n    list = Array.from(select.options)\n      .filter(o => o.value)\n      .map(o => ({ value: o.value, icon: \"\u2022\" }));\n  }\n\n  \/\/ Pinta chips\n  wrap.innerHTML = '';\n  list.forEach(opt => {\n    const btn = document.createElement('button');\n    btn.type = 'button';\n    btn.className = 'chip';\n    btn.dataset.value = opt.value;\n    btn.innerHTML = `<span class=\"ico\">${opt.icon || \"\u2022\"}<\/span><span>${opt.value}<\/span>`;\n    btn.addEventListener('click', () => {\n      select.value = opt.value;\n      select.dispatchEvent(new Event('change', { bubbles: true }));\n    });\n    wrap.appendChild(btn);\n  });\n\n  \/\/ Sincroniza selecci\u00f3n visual\n  const sync = () => {\n    Array.from(wrap.querySelectorAll('.chip'))\n      .forEach(ch => ch.classList.toggle('is-active', ch.dataset.value === select.value));\n  };\n  select.addEventListener('change', sync);\n  sync();\n}\n\n\n\/* =============== Servicios din\u00e1micos (desde hoja de tarifas del cliente) =============== *\/\nconst _svcSeq = {}; \/\/ Control de concurrencia si se cambian r\u00e1pido los filtros\n\/\/ >>> BEGIN PATCH: fillServicios (arregla \"[.sel.options]\" al restaurar valor)\n\/\/ Lista fija de servicios: no usa defaults ni mezcla por ciudad\/veh\u00edculo\nfunction fillServicios(n){\n  const sel = document.getElementById(`s${n}Servicio`);\n  if (!sel) return;\n\n  const lugar = document.getElementById(`s${n}Lugar`)?.value?.trim();\n  const veh   = document.getElementById(`s${n}Vehiculo`)?.value?.trim();\n\n  \/\/ Solo mostramos la lista cuando hay Lugar y Veh\u00edculo elegidos\n  const list = (lugar && veh) ? SERVICIOS_FIJOS : [];\n\n  const current = sel.value;\n  sel.innerHTML = '<option value=\"\"><\/option>';\n\n  list.forEach(s => {\n    const key   = SERVICIO_LABEL_KEYS[s] || s;\n    const label = t(key);\n    sel.insertAdjacentHTML(\n      'beforeend',\n      `<option value=\"${s}\" data-i18n=\"${key}\">${label}<\/option>`\n    );\n  });\n\n  sel.disabled = list.length === 0;\n  if (list.includes(current)) { sel.value = current; } else { sel.value = ''; }\n  \/\/ si no hab\u00eda valor y hay opciones, preselecciona la primera\n  if (!sel.value && list.length) { sel.value = list[0]; }\n\n  if (typeof renderChipsForSelect === 'function') renderChipsForSelect(sel);\n\n  sel.dispatchEvent(new Event('change', { bubbles: true }));\n  if (typeof window.cotizar === 'function') window.cotizar();\n}\n\n\n\/\/ >>> END PATCH\n\n\/* Vincular el evento tambi\u00e9n al cambiar cliente o lugar\/veh\u00edculo *\/\n['#cliNum','#cliTax','#s1Lugar','#s1Vehiculo'].forEach(id=>{\n  const el = document.querySelector(id);\n  el && el.addEventListener('change', ()=>fillServicios(1));\n});\n['#cliNum','#cliTax','#s2Lugar','#s2Vehiculo'].forEach(id=>{\n  const el = document.querySelector(id);\n  el && el.addEventListener('change', ()=>fillServicios(2));\n});\n['#cliNum','#cliTax','#s3Lugar','#s3Vehiculo'].forEach(id=>{\n  const el = document.querySelector(id);\n  el && el.addEventListener('change', ()=>fillServicios(3));\n});\n\/\/ \ud83d\udc49 Actualiza la capacidad m\u00e1xima al cambiar el veh\u00edculo en S2 y S3\ndocument.querySelector('#s2Vehiculo')?.addEventListener('change', ()=>applyMaxPax(2));\ndocument.querySelector('#s3Vehiculo')?.addEventListener('change', ()=>applyMaxPax(3));\n\n\/\/ \ud83d\udc49 Validaci\u00f3n accesible de pasajeros (sin recortar el valor del usuario)\n['#s1Pax', '#s2Pax', '#s3Pax'].forEach(sel => {\n  const inp = document.querySelector(sel);\n  if (!inp) return;\n\n  ['input', 'change', 'blur'].forEach(ev => {\n    inp.addEventListener(ev, () => {\n      validatePaxField(inp);\n      try {\n        window.__bukasStepper?.refreshNext?.();\n      } catch (_) {}\n    });\n  });\n\n  \/\/ Valida estado inicial (por si hay datos precargados)\n  validatePaxField(inp);\n});\n\nfunction setPaxError(inp, hasError, max) {\n  if (!inp) return;\n  const baseId = inp.id;\n  const errId = baseId + 'Error';\n\n  let err = document.getElementById(errId);\n  if (!err) {\n    err = document.createElement('div');\n    err.id = errId;\n    err.className = 'field-error';\n    inp.insertAdjacentElement('afterend', err);\n  }\n\n  if (hasError) {\n    err.textContent = t('error.maxPax', { max });\n    inp.classList.add('is-invalid');\n    inp.setAttribute('aria-invalid', 'true');\n    inp.setCustomValidity(' ');\n  } else {\n    err.textContent = '';\n    inp.classList.remove('is-invalid');\n    inp.removeAttribute('aria-invalid');\n    inp.setCustomValidity('');\n  }\n}\n\nfunction validatePaxField(inp) {\n  if (!inp) return true;\n\n  const max = Number(inp.max || 0);\n  const min = Number(inp.min || 0) || 1;\n  const raw = (inp.value || '').trim();\n\n  if (!raw) {\n    setPaxError(inp, false, max);\n    return false;\n  }\n\n  const val = Number(raw);\n  if (!Number.isFinite(val)) {\n    setPaxError(inp, false, max);\n    return false;\n  }\n\n  if (max && val > max) {\n    setPaxError(inp, true, max);\n    return false;\n  }\n\n  if (val < min) {\n    inp.value = String(min);\n  }\n\n  setPaxError(inp, false, max);\n  return true;\n}\n\n\/* =============== Pax m\u00e1x por veh\u00edculo =============== *\/\nfunction applyMaxPax(n = 1){\n  const veh  = qs(`#s${n}Vehiculo`)?.value || '';\n  const def  = CONFIG.vehiculos.find(v => v.value === veh);\n  const inp  = qs(`#s${n}Pax`);\n  const hint = qs(`#s${n}PaxHint`);\n  if (!inp) return;\n\n  const max = def?.maxPax || '';\n  if (max){\n    inp.max = String(max);\n    inp.placeholder = `M\u00e1x. ${max}`;\n  } else {\n    inp.removeAttribute('max');\n    inp.placeholder = `M\u00e1x. \u2014`;\n  }\n\n  if (hint){\n    hint.textContent = max ? t('hint.maxPax', { tipo: veh, max }) : '';\n  }\n\n  \/\/ Vuelve a validar con el nuevo m\u00e1ximo (sin modificar el valor del usuario)\n  validatePaxField(inp);\n  try {\n    window.__bukasStepper?.refreshNext?.();\n  } catch (_) {}\n}\n\n\/* =============== Datalists por ciudad + autocompletar exactas =============== *\/\nfunction fillDatalistsForCity(n=1){\n  const city = qs(`#s${n}Lugar`)?.value || '';\n  const pickList = qs(`#s${n}PickupOptions`);\n  const dropList = qs(`#s${n}DropoffOptions`);\n  if (!pickList || !dropList) return;\n  const cat = CONFIG.puntos[city] || { recogida:{}, destino:{} };\n  pickList.innerHTML = Object.keys(cat.recogida).map(k=>`<option value=\"${k}\">`).join('');\n  dropList.innerHTML = Object.keys(cat.destino).map(k=>`<option value=\"${k}\">`).join('');\n}\nfunction bindAddressAuto(n=1){\n  const city = qs(`#s${n}Lugar`)?.value || '';\n  const cat = CONFIG.puntos[city] || { recogida:{}, destino:{} };\n  const p = qs(`#s${n}Pickup`), pe = qs(`#s${n}PickupExact`);\n  const d = qs(`#s${n}Dropoff`), de = qs(`#s${n}DropoffExact`);\n  const copyIfEmpty = (src, dst) => {\n    if (!dst) return;\n    if (dst.dataset.addressChoice === 'pending') return;\n    if (!(dst.value || '').trim()) dst.value = (src?.value || '').trim();\n  };\n  p?.addEventListener('change', ()=>{\n    const val = p.value.trim();\n    const lookup = cat.recogida && cat.recogida[val];\n    const hasPlacesData = !!(p?.dataset?.lat || p?.dataset?.lng || p?.dataset?.placeId);\n    if (Array.isArray(lookup) && lookup.length){\n      showAddressCandidates(pe, lookup, 'pickup');\n    } else if (typeof lookup === 'string' && lookup.trim()){\n      _clearAddressCandidates_(pe);\n      _markAddressChoicePending_(pe, false);\n      pe.value = lookup;\n    } else {\n      if (hasPlacesData){\n        const latNum = Number(p.dataset.lat);\n        const lngNum = Number(p.dataset.lng);\n        const candidate = {\n          formatted: pe?.value || val,\n          lat: Number.isFinite(latNum) ? latNum : null,\n          lng: Number.isFinite(lngNum) ? lngNum : null,\n          placeId: p.dataset.placeId || '',\n          cityDetected: ''\n        };\n        _clearAddressCandidates_(pe);\n        _markAddressChoicePending_(pe, false);\n        _setHiddenAddressData_(n, 'Pickup', candidate);\n      } else {\n        _clearAddressCandidates_(pe);\n        _markAddressChoicePending_(pe, false);\n        copyIfEmpty(p, pe);\n        _setHiddenAddressData_(n, 'Pickup', null);\n      }\n    }\n  });\n  d?.addEventListener('change', ()=>{\n    const val = d.value.trim();\n    const lookup = cat.destino && cat.destino[val];\n    const hasPlacesData = !!(d?.dataset?.lat || d?.dataset?.lng || d?.dataset?.placeId);\n    if (Array.isArray(lookup) && lookup.length){\n      showAddressCandidates(de, lookup, 'dropoff');\n    } else if (typeof lookup === 'string' && lookup.trim()){\n      _clearAddressCandidates_(de);\n      _markAddressChoicePending_(de, false);\n      de.value = lookup;\n    } else {\n      if (hasPlacesData){\n        const latNum = Number(d.dataset.lat);\n        const lngNum = Number(d.dataset.lng);\n        const candidate = {\n          formatted: de?.value || val,\n          lat: Number.isFinite(latNum) ? latNum : null,\n          lng: Number.isFinite(lngNum) ? lngNum : null,\n          placeId: d.dataset.placeId || '',\n          cityDetected: ''\n        };\n        _clearAddressCandidates_(de);\n        _markAddressChoicePending_(de, false);\n        _setHiddenAddressData_(n, 'Dropoff', candidate);\n      } else {\n        _clearAddressCandidates_(de);\n        _markAddressChoicePending_(de, false);\n        copyIfEmpty(d, de);\n        _setHiddenAddressData_(n, 'Dropoff', null);\n      }\n    }\n  });\n  \/\/ tambi\u00e9n al salir del campo\n  p?.addEventListener('blur', ()=>{ copyIfEmpty(p, pe); });\n  d?.addEventListener('blur', ()=>{ copyIfEmpty(d, de); });\n}\n\/* =============== Toggle S\u00ed\/No accesible =============== *\/\nfunction enhanceYesNoSelect(select){\n  if(!select) return;\n  const opts=Array.from(select.options).map(o=>o.text.trim());\n  if(!(opts.includes('S\u00ed') && opts.includes('No'))) return;\n\n  const w=document.createElement('div');\n  w.className='yn-toggle';\n  w.setAttribute('role','radiogroup');\n\n  const y=document.createElement('button');\n  y.type='button';\n  y.textContent=t('yn.si');   \/\/ ES: S\u00ed, EN: Yes\n  y.setAttribute('role','radio');\n\n  const n=document.createElement('button');\n  n.type='button';\n  n.textContent=t('yn.no');   \/\/ ES\/EN: No\n  n.setAttribute('role','radio');\n\n  select.classList.add('visually-hidden-select');\n  select.parentNode.insertBefore(w, select.nextSibling);\n  w.appendChild(y); w.appendChild(n);\n\n  const sync=()=>{ \n    const v=(select.value||'').trim();\n    y.classList.toggle('is-active', v==='S\u00ed'); y.setAttribute('aria-checked', v==='S\u00ed'?'true':'false');\n    n.classList.toggle('is-active', v==='No'); n.setAttribute('aria-checked', v==='No'?'true':'false');\n  };\n  const set=v=>{ select.value=v; select.dispatchEvent(new Event('change',{bubbles:true})); sync(); };\n  if(!select.value){ set('No'); } else sync();\n  y.addEventListener('click',()=>set('S\u00ed'));\n  n.addEventListener('click',()=>set('No'));\n}\n\n\/* =============== Contacto extra (muestra\/required) =============== *\/\nfunction toggleContacto(selectEl, boxId){\n  const box = document.getElementById(boxId);\n  if (!selectEl || !box) return;\n\n  \/\/ Find specific fields inside the container\n  const nameInput  = box.querySelector('input[id$=\"ContactName\"]');\n  const phoneInput = box.querySelector('input[id$=\"ContactPhone\"]');\n  const emailInput = box.querySelector('input[id$=\"ContactEmail\"]');\n\n  const sync = () => {\n    const on = selectEl.value === 'S\u00ed';\n\n    \/\/ Show \/ hide the whole block\n    box.hidden = !on;\n\n    \/\/ Only name and phone are required when \"Yes\"\n    if (nameInput)  nameInput.required  = on;\n    if (phoneInput) phoneInput.required = on;\n\n    \/\/ Email is always optional\n    if (emailInput) emailInput.required = false;\n\n    \/\/ Refresh required field helpers if available\n    if (typeof window.refreshRequiredHelpers === 'function') {\n      window.refreshRequiredHelpers(document);\n    }\n  };\n\n  selectEl.addEventListener('change', sync);\n  sync(); \/\/ initial state\n}\n\/* =============== Configuraci\u00f3n =============== *\/\n\/\/ API URL - puede ser sobrescrita con window.API_URL para diferentes entornos\nconst WEBAPP_URL = window.API_URL || \"https:\/\/script.google.com\/macros\/s\/AKfycbwQhfOmGWmoYonNorV4o0g8Md2ItLsqUwAhgmrUZEYREd-Hq0dzxMalvPb23827l9GTGQ\/exec\";\nwindow.WEBAPP_URL = WEBAPP_URL;\n\n\/\/ Constantes\nconst FETCH_TIMEOUT_MS = 15000;\n\/\/ Definimos el umbral como propiedad global en window para que sea accesible desde todos los scripts\nwindow.MAX_DISTANCE_KM = 50; \/\/ Umbral para aviso de cotizaci\u00f3n personalizada\n\n\/* =============== Utilidades =============== *\/\n\/\/ Sanitiza entrada para prevenir XSS\nfunction sanitizeInput(str) {\n  if (typeof str !== 'string') return '';\n  const div = document.createElement('div');\n  div.textContent = str;\n  return div.innerHTML;\n}\n\n\/\/ Escapa HTML especial\nfunction escapeHtml(str) {\n  if (typeof str !== 'string') return '';\n  const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#039;' };\n  return str.replace(\/[&<>\"']\/g, m => map[m]);\n}\n\n\/\/ Sanitiza objeto de datos\nfunction sanitizeData(data) {\n  const sanitized = {};\n  for (const [key, value] of Object.entries(data)) {\n    sanitized[key] = typeof value === 'string' ? escapeHtml(value) : value;\n  }\n  return sanitized;\n}\n\n\/* =============== Helper: Obtener datos de servicio (elimina duplicaci\u00f3n) =============== *\/\n\/\/ Obtiene datos de un servicio (1, 2 o 3) - reemplaza c\u00f3digo duplicado\nfunction getServiceData(serviceNum, getExtrasFn) {\n  const prefix = serviceNum === 1 ? '' : serviceNum === 2 ? '2_' : '3_';\n  const lugarKey = serviceNum === 1 ? 'Lugar' : serviceNum === 2 ? 'Lugardos' : 'Lugartres';\n  const toggleId = serviceNum === 2 ? '#t2' : serviceNum === 3 ? '#t3' : null;\n  \n  const get = (sel) => {\n    const el = document.querySelector(sel);\n    return el ? (el.value || '').trim() : '';\n  };\n  \n  \/\/ Verificar si el servicio est\u00e1 activo\n  const isActive = () => {\n    if (serviceNum === 1) return true; \/\/ Servicio 1 siempre activo\n    const toggle = toggleId ? get(toggleId) === 'S\u00ed' : false;\n    const fields = [\n      `#s${serviceNum}Lugar`, `#s${serviceNum}Vehiculo`, `#s${serviceNum}Servicio`, `#s${serviceNum}Pax`,\n      `#s${serviceNum}Pickup`, `#s${serviceNum}Dropoff`, `#s${serviceNum}Date`, `#s${serviceNum}Time`,\n      `#s${serviceNum}PickupExact`, `#s${serviceNum}DropoffExact`, `#s${serviceNum}Flight`, `#s${serviceNum}Otro`\n    ];\n    const hasData = fields.some(sel => !!get(sel));\n    return toggle || hasData;\n  };\n  \n  if (!isActive()) {\n    \/\/ Retornar objeto con campos vac\u00edos\n    const emptyFields = [\n      lugarKey, `${prefix}Servicio`, `${prefix}Veh\u00edculo`, `${prefix}Cantidad de pasajeros?`,\n      `${prefix}Punto de recogida`, `${prefix}Punto de destino`,\n      `${prefix}Direcci\u00f3n exacta de recogida`, `${prefix}Direcci\u00f3n exacta de destino`,\n      serviceNum === 1 ? 'Nombre recogida' : `${prefix}Nombre recogida`,\n      serviceNum === 1 ? 'Nombre destino' : `${prefix}Nombre destino`,\n      `${prefix}Distancia (km)`, `${prefix}Fecha de recogida`, `${prefix}Hora de recogida`,\n      `${prefix}Cartel con Nombre`, `${prefix}Cartel con Nombre - Detalle`,\n      `${prefix}Asiento Infantil`, `${prefix}Asiento Infantil - Detalle`,\n      `${prefix}Chofer con idioma`, `${prefix}Chofer con idioma - Detalle`,\n      `${prefix}Otro (especificar)`,\n      serviceNum === 1 ? 'Quiere agregar una persona de contacto adem\u00e1s del cliente?' : `${prefix}Quiere agregar una persona de contacto adem\u00e1s del cliente?`,\n      serviceNum === 1 ? 'Nombre del contacto' : `${prefix}Nombre`,\n      serviceNum === 1 ? 'Tel\u00e9fono de contacto' : `${prefix}Tel\u00e9fono`,\n      serviceNum === 1 ? 'Email de contacto' : `${prefix}Email`,\n      serviceNum === 1 ? 'Introduzca el n\u00famero de vuelo (si corresponde)' : `${prefix}Introduzca el n\u00famero de vuelo (si corresponde)`\n    ];\n    const empty = {};\n    emptyFields.forEach(k => empty[k] = '');\n    return empty;\n  }\n  \n  \/\/ Servicio activo - recopilar datos\n  const data = {};\n  data[lugarKey] = get(`#s${serviceNum}Lugar`);\n  data[`${prefix}Servicio`] = get(`#s${serviceNum}Servicio`);\n  data[`${prefix}Veh\u00edculo`] = get(`#s${serviceNum}Vehiculo`);\n  data[`${prefix}Cantidad de pasajeros?`] = get(`#s${serviceNum}Pax`);\n  data[`${prefix}Punto de recogida`] = get(`#s${serviceNum}Pickup`);\n  data[`${prefix}Punto de destino`] = get(`#s${serviceNum}Dropoff`);\n  data[`${prefix}Direcci\u00f3n exacta de recogida`] = get(`#s${serviceNum}PickupExact`);\n  data[`${prefix}Direcci\u00f3n exacta de destino`] = get(`#s${serviceNum}DropoffExact`);\n  \/\/ Nombres de lugares (para mostrar en \"Trayecto\" del email)\n  if (serviceNum === 1) {\n    data['Nombre recogida'] = get(`#s${serviceNum}PickupName`);\n    data['Nombre destino'] = get(`#s${serviceNum}DropoffName`);\n  } else {\n    data[`${prefix}Nombre recogida`] = get(`#s${serviceNum}PickupName`);\n    data[`${prefix}Nombre destino`] = get(`#s${serviceNum}DropoffName`);\n  }\n  data[`${prefix}Latitud recogida`] = get(`#s${serviceNum}PickupLat`);\n  data[`${prefix}Longitud recogida`] = get(`#s${serviceNum}PickupLng`);\n  data[`${prefix}PlaceId recogida`] = get(`#s${serviceNum}PickupPlaceId`);\n  data[`${prefix}Latitud destino`] = get(`#s${serviceNum}DropoffLat`);\n  data[`${prefix}Longitud destino`] = get(`#s${serviceNum}DropoffLng`);\n  data[`${prefix}PlaceId destino`] = get(`#s${serviceNum}DropoffPlaceId`);\n  \n  const distance = get(`#s${serviceNum}Distance`);\n  if (distance) data[`${prefix}Distancia (km)`] = distance;\n  \n  data[`${prefix}Fecha de recogida`] = get(`#s${serviceNum}Date`);\n  data[`${prefix}Hora de recogida`] = get(`#s${serviceNum}Time`);\n  const extrasData = getExtrasFn(serviceNum);\n  data[`${prefix}Cartel con Nombre`] = extrasData.cartel_nombre ? 'S\u00ed' : 'No';\n  data[`${prefix}Cartel con Nombre - Detalle`] = extrasData.cartel_nombre_detalle;\n  data[`${prefix}Asiento Infantil`] = extrasData.asiento_infantil ? 'S\u00ed' : 'No';\n  data[`${prefix}Asiento Infantil - Detalle`] = extrasData.asiento_infantil_detalle;\n  data[`${prefix}Chofer con idioma`] = extrasData.chofer_idioma ? 'S\u00ed' : 'No';\n  data[`${prefix}Chofer con idioma - Detalle`] = extrasData.chofer_idioma_detalle;\n  data[`${prefix}Otro (especificar)`] = get(`#s${serviceNum}Otro`);\n  \n  const addContact = get(`#s${serviceNum}AddContact`) || 'No';\n  const contactKey = serviceNum === 1 \n    ? 'Quiere agregar una persona de contacto adem\u00e1s del cliente?'\n    : `${prefix}Quiere agregar una persona de contacto adem\u00e1s del cliente?`;\n  data[contactKey] = addContact;\n  \n  const nameKey = serviceNum === 1 ? 'Nombre del contacto' : `${prefix}Nombre`;\n  const phoneKey = serviceNum === 1 ? 'Tel\u00e9fono de contacto' : `${prefix}Tel\u00e9fono`;\n  const emailKey = serviceNum === 1 ? 'Email de contacto' : `${prefix}Email`;\n  \n  if (addContact === 'S\u00ed') {\n    data[nameKey] = get(`#s${serviceNum}ContactName`);\n    data[phoneKey] = get(`#s${serviceNum}ContactPhone`);\n    data[emailKey] = get(`#s${serviceNum}ContactEmail`);\n  } else {\n    data[nameKey] = '';\n    data[phoneKey] = '';\n    data[emailKey] = '';\n  }\n  \n  const flightKey = serviceNum === 1 \n    ? 'Introduzca el n\u00famero de vuelo (si corresponde)'\n    : `${prefix}Introduzca el n\u00famero de vuelo (si corresponde)`;\n  data[flightKey] = get(`#s${serviceNum}Flight`);\n  \n  return data;\n}\n\/\/ \u2b07\ufe0f Reemplaza el HEADERS corto por este:\nconst HEADERS = [\n  \"N\u00famero de cliente\",\"Nombre del cliente\",\"Correo electr\u00f3nico\",\"Tel\u00e9fono\",\n  \"N\u00famero de Identificaci\u00f3n Fiscal (CIF, VAT o Pasaporte)\",\"Domicilio de Facturaci\u00f3n\",\n  \/\/ Servicio 1\n  \"Lugar\",\"Servicio\",\"Veh\u00edculo\",\"Cantidad de pasajeros?\",\n  \"Punto de recogida\",\"Punto de destino\",\n  \"Direcci\u00f3n exacta de recogida\",\"Direcci\u00f3n exacta de destino\",\n  \"Latitud recogida\",\"Longitud recogida\",\"PlaceId recogida\",\n  \"Latitud destino\",\"Longitud destino\",\"PlaceId destino\",\n  \"Distancia (km)\",\n  \"Fecha de recogida\",\"Hora de recogida\",\n  \"Cartel con Nombre\",\"Cartel con Nombre - Detalle\",\n  \"Asiento Infantil\",\"Asiento Infantil - Detalle\",\n  \"Chofer con idioma\",\"Chofer con idioma - Detalle\",\n  \"Otro (especificar)\",\n  \"Quiere agregar una persona de contacto adem\u00e1s del cliente?\",\"Nombre del contacto\",\"Tel\u00e9fono de contacto\",\"Email de contacto\",\n  \"Introduzca el n\u00famero de vuelo (si corresponde)\",\n  \"Desea agregar otro servicio al servicio Nro1?\",\n  \/\/ Servicio 2 (misma fila, columnas 2_*)\n  \"Lugardos\",\"2_Servicio\",\"2_Veh\u00edculo\",\"2_Cantidad de pasajeros?\",\n  \"2_Punto de recogida\",\"2_Punto de destino\",\n  \"2_Direcci\u00f3n exacta de recogida\",\"2_Direcci\u00f3n exacta de destino\",\n  \"2_Latitud recogida\",\"2_Longitud recogida\",\"2_PlaceId recogida\",\n  \"2_Latitud destino\",\"2_Longitud destino\",\"2_PlaceId destino\",\n  \"2_Distancia (km)\",\n  \"2_Fecha de recogida\",\"2_Hora de recogida\",\n  \"2_Cartel con Nombre\",\"2_Cartel con Nombre - Detalle\",\n  \"2_Asiento Infantil\",\"2_Asiento Infantil - Detalle\",\n  \"2_Chofer con idioma\",\"2_Chofer con idioma - Detalle\",\n  \"2_Otro (especificar)\",\n  \"2_Quiere agregar una persona de contacto adem\u00e1s del cliente?\",\"2_Nombre\",\"2_Tel\u00e9fono\",\"2_Email\",\n  \"2_Introduzca el n\u00famero de vuelo (si corresponde)\",\n  \"Desea agregar otro servicio al servicio Nro2?\",\n    \/\/ Servicio 3\n  \"Lugartres\",\"3_Servicio\",\"3_Veh\u00edculo\",\"3_Cantidad de pasajeros?\",\n  \"3_Punto de recogida\",\"3_Punto de destino\",\n  \"3_Direcci\u00f3n exacta de recogida\",\"3_Direcci\u00f3n exacta de destino\",\n  \"3_Latitud recogida\",\"3_Longitud recogida\",\"3_PlaceId recogida\",\n  \"3_Latitud destino\",\"3_Longitud destino\",\"3_PlaceId destino\",\n  \"3_Distancia (km)\",\n  \"3_Fecha de recogida\",\"3_Hora de recogida\",\n  \"3_Cartel con Nombre\",\"3_Cartel con Nombre - Detalle\",\n  \"3_Asiento Infantil\",\"3_Asiento Infantil - Detalle\",\n  \"3_Chofer con idioma\",\"3_Chofer con idioma - Detalle\",\n  \"3_Otro (especificar)\",\n  \"3_Quiere agregar una persona de contacto adem\u00e1s del cliente?\",\"3_Nombre\",\"3_Tel\u00e9fono\",\"3_Email\",\n  \"3_Introduzca el n\u00famero de vuelo (si corresponde)\",\n  \/\/ Pie\n  \"Comentarios u observaciones\",\n  \"Indique la forma de pago\",\n  \"Idioma formulario\"\n];\nconst SYNONYMS = {\n  \"N\u00famero de Identificaci\u00f3n Fiscal (CIF o VAT)\":\"N\u00famero de Identificaci\u00f3n Fiscal (CIF, VAT o Pasaporte)\",\n  \"Cantidad de pasajeros\":\"Cantidad de pasajeros?\",\n  \"Introduzca el n\u00famero de vuelo (si corresponde)\":\"Introduzca el n\u00famero de vuelo (si corresponde)\"\n};\nasync function fetchWithTimeout(resource, options={}){\n  const { timeout = FETCH_TIMEOUT_MS } = options;\n  const controller = new AbortController();\n  const id = setTimeout(()=>controller.abort(), timeout);\n  try{\n    const resp = await fetch(resource, { ...options, signal: controller.signal });\n    return resp;\n  } finally { clearTimeout(id); }\n}\nwindow.fetchWithTimeout = fetchWithTimeout;\nfunction bindSubmit(form){\n  const toast = document.getElementById('toast');\n  const estado = document.getElementById('estado');\n  const toastClose = document.getElementById('toastClose');\n  const getExtras = (n) => {\n    const cartel = document.querySelector(`#s${n}CartelNombre`)?.checked || false;\n    const asiento = document.querySelector(`#s${n}AsientoInfantil`)?.checked || false;\n    const chofer = document.querySelector(`#s${n}ChoferIdioma`)?.checked || false;\n    return {\n      cartel_nombre: cartel,\n      cartel_nombre_detalle: cartel ? (document.querySelector(`#s${n}CartelNombreDetalle`)?.value || '').trim() : '',\n      asiento_infantil: asiento,\n      asiento_infantil_detalle: asiento ? (document.querySelector(`#s${n}AsientoInfantilDetalle`)?.value || '').trim() : '',\n      chofer_idioma: chofer,\n      chofer_idioma_detalle: chofer ? (document.querySelector(`#s${n}ChoferIdiomaDetalle`)?.value || '').trim() : ''\n    };\n  };\n  const val = (sel)=>document.querySelector(sel)?.value?.trim() || '';\n  \n  \/\/ We use native constraints too; we manage styles ourselves\n  form.setAttribute('novalidate', 'novalidate');\n\n  \/\/ Live cleanup: remove error state as the user fixes inputs\n  \/\/ Handle pickup\/dropoff fields separately to avoid interfering with Google Places Autocomplete\n  form.addEventListener('input', (ev)=>{\n    const t = ev.target;\n    if (!t || !t.matches('input,select,textarea')) return;\n    \n    \/\/ For pickup\/dropoff fields, only clear error if they have a value (user selected from Places or typed)\n    if (t.id && (t.id.includes('Pickup') || t.id.includes('Dropoff'))) {\n      \/\/ Only clear if field has value (not while user is typing to show suggestions)\n      if (!isEmpty(t)) {\n        clearInvalid(t);\n      }\n      return;\n    }\n    \n    \/\/ For other fields, clear error if valid\n    if (!isEmpty(t) && (!t.willValidate || t.checkValidity())){\n      clearInvalid(t);\n    }\n  });\n\n  form.addEventListener('change', (ev)=>{\n    const t = ev.target;\n    if (!t || !t.matches('input,select,textarea')) return;\n    \n    \/\/ For pickup\/dropoff fields, clear error when value changes (e.g., after selecting from Places)\n    if (t.id && (t.id.includes('Pickup') || t.id.includes('Dropoff'))) {\n      if (!isEmpty(t)) {\n        clearInvalid(t);\n      }\n      return;\n    }\n    \n    \/\/ For other fields, clear error if valid\n    if (!isEmpty(t) && (!t.willValidate || t.checkValidity())){\n      clearInvalid(t);\n    }\n  });\n\n  form.addEventListener('submit', async (e)=>{\n    \/\/ Validate all visible blocks before submitting\n    const { ok, firstInvalid } = validateContainer(form);\n    if (!ok){\n      e.preventDefault();\n      focusInvalid(firstInvalid);\n      return;\n    }\n\n    e.preventDefault();\n    estado.textContent = t('estado.enviando');\n    try{\n      \/\/ 1) Vuelca el form a objeto plano aplicando sin\u00f3nimos\n      const fd = new FormData(form);\n      const data = {};\n      for (const [k,v] of fd.entries()){\n        const key = (window.SYNONYMS && window.SYNONYMS[k]) ? window.SYNONYMS[k] : k;\n        data[key] = v;\n      }\n      \/\/ 2) Servicio 1 - siempre activo\n      const dist1 = val('#s1Distance'); \n      if(dist1) data[\"Distancia (km)\"] = dist1;\n      const extras1 = getExtras(1);\n      data[\"Cartel con Nombre\"] = extras1.cartel_nombre ? 'S\u00ed' : 'No';\n      data[\"Cartel con Nombre - Detalle\"] = extras1.cartel_nombre_detalle;\n      data[\"Asiento Infantil\"] = extras1.asiento_infantil ? 'S\u00ed' : 'No';\n      data[\"Asiento Infantil - Detalle\"] = extras1.asiento_infantil_detalle;\n      data[\"Chofer con idioma\"] = extras1.chofer_idioma ? 'S\u00ed' : 'No';\n      data[\"Chofer con idioma - Detalle\"] = extras1.chofer_idioma_detalle;\n      data[\"Desea agregar otro servicio al servicio Nro1?\"] = (document.querySelector('#t2')?.value || '').trim();\n      data[\"Desea agregar otro servicio al servicio Nro2?\"] = (document.querySelector('#t3')?.value || '').trim();\n      \n      \/\/ 3) Servicios 2 y 3 - usar funci\u00f3n helper (elimina duplicaci\u00f3n)\n      Object.assign(data, getServiceData(2, getExtras));\n      Object.assign(data, getServiceData(3, getExtras));\n      \n      \/\/ 4) Idioma con el que se ha rellenado el formulario\n      data[\"Idioma formulario\"] = currentLang;\n\n      \/\/ 4.5) Ensure client number is included with exact label AND canonical API param\n      const cliNum = (document.getElementById('cliNum')?.value || '').trim();\n      data[\"N\u00famero de cliente\"] = cliNum;  \/\/ Exact form field label\n      data.numCliente = cliNum;             \/\/ Canonical API param\n\n      \/\/ 5) Sanitizar datos antes de enviar (prevenci\u00f3n XSS)\n      const sanitizedData = sanitizeData(data);\n\n      \/\/ 6) Construye el payload EXACTO con tus HEADERS (una sola fila)\n      const payload = {};\n      HEADERS.forEach(h => { payload[h] = (h in sanitizedData) ? sanitizedData[h] : \"\"; });\n      \n      \/\/ 6.5) Ensure numCliente is also in payload for API paths\n      payload.numCliente = cliNum;\n      \n      \/\/ 7) Env\u00eda UNA solicitud con timeout\n      await fetchWithTimeout(WEBAPP_URL, {\n        method: 'POST',\n        headers: { 'Content-Type': 'application\/json' },\n        body: JSON.stringify(payload),\n        mode: 'no-cors'\n      });\n      estado.textContent = '';\n      toast.hidden = false;\n    }catch(err){\n      console.error('Submit error', err);\n      estado.textContent = t('estado.error');\n      \/\/ Mostrar error m\u00e1s detallado en desarrollo\n      if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {\n        console.error('Error details:', err);\n      }\n    }\n  });\n  \/\/ \ud83d\udd01 Dejar el formulario como nuevo y volver a Cliente\n  const resetAndBackToFirst = () => {\n    \/\/ 1) Reset est\u00e1ndar del formulario\n    form.reset();\n\n    \/\/ 1bis) Volver a poner fechas y horas por defecto\n    setDefaultDatesAndTimes();\n\n    \/\/ 2) Totales y contador de servicios\n    const tot = document.getElementById('pagoTotal');\n    if (tot) tot.textContent = '0,00 \u20ac';\n    const cnt = document.querySelector('#pagoServiciosCount span');\n    if (cnt) cnt.textContent = '0';\n\n    \/\/ 3) Concepto bancario: N\u00ba cliente en blanco\n    const cliSpan = document.getElementById('bankCliNum');\n    if (cliSpan) cliSpan.textContent = '___';\n\n    \/\/ 4) Ocultar por completo Servicio 2 y 3\n    ['serv2','serv3'].forEach(id => {\n      const fs = document.getElementById(id);\n      if (fs) {\n        fs.hidden = true;\n        fs.dataset.open = '0';\n      }\n    });\n\n    \/\/ 5) Resincro de toggles S\u00ed\/No y cajas de contacto\n    ['#t2','#t3','#s1AddContact','#s2AddContact','#s3AddContact']\n      .forEach(sel => {\n        const el = document.querySelector(sel);\n        if (el) el.dispatchEvent(new Event('change', { bubbles:true }));\n      });\n\n    \/\/ 6) Recalcular precios por si acaso\n    try {\n      window.cotizar?.('1');\n      window.cotizar?.('2');\n      window.cotizar?.('3');\n      if (typeof window.actualizarTotales === 'function') {\n        window.actualizarTotales();\n      }\n    } catch(e) {}\n\n    \/\/ 7) Limpiar aviso de cliente autocompletado\n    const status = document.getElementById('clienteStatus');\n    if (status) status.innerHTML = '';\n\n    \/\/ 8) Volver al primer bloque del stepper (Datos del cliente)\n    window.__bukasStepper?.setActive?.(0);\n\n    \/\/ 9) Subir al inicio del formulario\n    const frm = document.getElementById('bukasForm');\n    if (frm && frm.scrollIntoView) {\n      frm.scrollIntoView({ behavior:'smooth', block:'start' });\n    } else {\n      window.scrollTo(0,0);\n    }\n  };\n\n  \/\/ Bot\u00f3n \u201cAceptar\u201d del pop-up\n  toastClose?.addEventListener('click', () => {\n    toast.hidden = true;        \/\/ cierra la \u201cp\u00e1gina\u201d de Pedido enviado\n    resetAndBackToFirst();      \/\/ formulario nuevo y volvemos a Cliente\n  });\n}\n\n\/* =============== Autocomplete de cliente (REEMPLAZO COMPLETO) =============== *\/\n\/* Espera que WEBAPP_URL est\u00e9 definido arriba.\n   Respuesta esperada: { cliente:{nombre,telefono,email,direccion,numero,cif}, verified:boolean } *\/\nasync function lookupCliente({ num, tax }) {\n  const n = String(num || '').trim();\n  const t = String(tax || '').trim();\n\n  \/\/ \u26a0\ufe0f Solo buscamos si hay n\u00famero Y CIF\/VAT\n  if (!n || !t) return null;\n\n  \/\/ 1) Intento backend (Apps Script)\n  try {\n    const params = new URLSearchParams({\n      action: 'cliente',\n      numCliente: n,  \/\/ canonical param\n      taxId: t        \/\/ canonical param\n    }).toString();\n\n    const r = await fetchWithTimeout(`${WEBAPP_URL}?${params}`, {\n      cache: 'no-store',\n      timeout: 5000\n    });\n\n    if (r && r.ok) {\n      const data = await r.json().catch(() => null);\n      if (data && data.cliente) {\n        const c = data.cliente;\n        return {\n          nombre:  String(c.nombre   || '').trim(),\n          tel:     String(c.telefono || '').trim(),\n          email:   String(c.email    || '').trim(),\n          billing: String(c.direccion|| '').trim(),\n          num:     String(c.numero   || n).trim(),\n          tax:     String(c.cif      || t).trim(),\n          verified: !!data.verified\n        };\n      }\n    }\n  } catch (err) {\n    console.warn('Cliente lookup error:', err);\n  }\n\n  \/\/ 2) Fallback local (demo) \u2014 match num + tax en CONFIG.clientesDemo\n  try {\n    const demo = CONFIG?.clientesDemo || [];\n    const hit = demo.find(c => {\n      const cNum = String(c.num || '').trim().toLowerCase();\n      const cTax = String(c.tax || '').trim().toLowerCase();\n      return cNum === n.toLowerCase() && cTax === t.toLowerCase();\n    });\n\n    if (hit) {\n      return {\n        nombre:  String(hit.nombre  || '').trim(),\n        tel:     String(hit.tel     || '').trim(),\n        email:   String(hit.email   || '').trim(),\n        billing: String(hit.billing || '').trim(),\n        num:     String(hit.num     || n).trim(),\n        tax:     String(hit.tax     || t).trim(),\n        \/\/ demo \u2192 dejo verified en false para no bloquear campos\n        verified: false\n      };\n    }\n  } catch (err) {\n    console.warn('Demo cliente lookup error:', err);\n  }\n\n  return null;\n}\nfunction bindClienteAuto() {\n  const num    = document.querySelector('#cliNum');\n  const tax    = document.querySelector('#cliTax');\n  const name   = document.querySelector('#cliName');\n  const tel    = document.querySelector('#cliPhone');\n  const email  = document.querySelector('#cliEmail');\n  const bill   = document.querySelector('#cliBilling');\n  const status = document.querySelector('#clienteStatus');\n\n  const setRO = (el, ro) => el && (el.readOnly = !!ro);\n  const lock  = (on) => {\n    setRO(name,on);\n    \/\/ tel siempre editable\n    setRO(bill,on);\n    setRO(tax,on);\n    setRO(num,on);\n    \/\/ email siempre editable\n  };\n\n  \/\/ badge usando CLAVE de i18n + data-i18n-badge\n  const badge = (kind, key) => {\n    if (!status) return;\n    const cls = kind === 'ok'   ? 'badge badge-ok'\n              : kind === 'warn' ? 'badge badge-warn'\n              : 'badge badge-err';\n    status.innerHTML = `<span class=\"${cls}\" data-i18n-badge=\"${key}\">${t(key)}<\/span>`;\n  };\n\n  \/\/ Notifica cambios al stepper y revalida\n  function triggerRevalidate() {\n    ['#cliTax','#cliName','#cliPhone','#cliBilling','#cliEmail'].forEach(sel => {\n      const el = document.querySelector(sel);\n      if (el) el.dispatchEvent(new Event('input', { bubbles:true }));\n    });\n    window.__bukasStepper?.refreshNext();\n  }\n\n  let deb;\n  const debounce = (fn, t = 280) => {\n    clearTimeout(deb);\n    deb = setTimeout(fn, t);\n  };\n\n  async function query() {\n    const vNum = (num?.value || '').trim();\n    const vTax = (tax?.value || '').trim();\n\n    \/\/ Hasta que no haya N\u00daMERO Y TAX, no buscamos nada\n    if (!vNum || !vTax) {\n      badge('warn', 'cliente.badge.intro');\n      lock(false);\n      triggerRevalidate();\n      return;\n    }\n\n    badge('warn', 'cliente.badge.search');\n\n    const data = await lookupCliente({ num: vNum, tax: vTax });\n\n    \/\/ Solo consideramos match v\u00e1lido si verified === true\n    if (!data || !data.verified) {\n      \/\/ No hay combinaci\u00f3n v\u00e1lida N\u00ba + CIF\/VAT \u2192 no autocompletamos\n      badge('err', 'cliente.badge.notfound');\n      lock(false);\n      triggerRevalidate();\n      return;\n    }\n\n    \/\/ Rellenamos con datos verificados\n    \/\/ Solo rellenar si el campo est\u00e1 vac\u00edo o si el usuario no lo ha editado\n    if (data.nombre)  name.value  = data.nombre;\n    \/\/ Tel\u00e9fono: solo rellenar si est\u00e1 vac\u00edo (no sobrescribir si el usuario lo edit\u00f3)\n    if (data.tel && !tel.value) tel.value = data.tel;\n    \/\/ Email: solo rellenar si est\u00e1 vac\u00edo (no sobrescribir si el usuario lo edit\u00f3)\n    if (data.email && !email.value) email.value = data.email;\n    if (data.billing) bill.value  = data.billing;\n    if (num && data.num) num.value = data.num;\n    if (tax && data.tax) tax.value = data.tax;\n\n    badge('ok', 'cliente.badge.ok');\n    lock(true);             \/\/ bloqueamos ficha porque est\u00e1 verificada\n    triggerRevalidate();    \/\/ habilita \"Siguiente\" si lo dem\u00e1s est\u00e1 ok\n  }\n\n  ['input','change','blur'].forEach(ev => {\n    num?.addEventListener(ev, () => debounce(query));\n    tax?.addEventListener(ev, () => debounce(query));\n  });\n\n  \/\/ Primer chequeo por si el navegador ha rellenado cosas\n  const n0 = (num?.value || '').trim();\n  const t0 = (tax?.value || '').trim();\n  if (n0 && t0) {\n    query();\n  } else {\n    badge('warn', 'cliente.badge.intro');\n  }\n}\n\n\/* =============== Enhancers comunes =============== *\/\nfunction enhanceYesNo(){\n  ['#t2','#t3','#s1AddContact','#s2AddContact','#s3AddContact'].forEach(sel=>enhanceYesNoSelect(qs(sel)));\n  toggleContacto(qs('#s1AddContact'), 'c1');\n  toggleContacto(qs('#s2AddContact'), 'c2');\n  toggleContacto(qs('#s3AddContact'), 'c3'); \/\/ nuevo\n}\nfunction bindOtrosAviso(){\n  const inp = qs('#s1Otro'), avis = qs('#aviso-otros-1');\n  if (!inp || !avis) return;\n  const t=()=>{ avis.hidden = !(inp.value||'').trim(); };\n  inp.addEventListener('input', t); t();\n}\n\/\/ Despu\u00e9s de enhanceYesNo();\nconst t2   = document.querySelector('#t2');\nconst serv2= document.querySelector('#serv2');\nconst t3    = document.querySelector('#t3');\nconst serv3 = document.querySelector('#serv3');\nfunction initServicio(n){\n  \/\/ Pintar chips\n  renderChipsForSelect(document.querySelector(`#s${n}Lugar`));\n  renderChipsForSelect(document.querySelector(`#s${n}Vehiculo`));\n  renderChipsForSelect(document.querySelector(`#s${n}Servicio`));\n  \/\/ Cambios que disparan l\u00f3gica y cotizador\n  ['#s'+n+'Lugar','#s'+n+'Vehiculo'].forEach(sel=>{\n    document.querySelector(sel)?.addEventListener('change', ()=>{\n      fillServicios(n);\n      applyMaxPax(n);\n      fillDatalistsForCity(n);\n      bindAddressAuto(n);\n      window.__bukasStepper?.refreshNext?.();\n    });\n  });\n  \/\/ Inicial\n  fillServicios(n);\n  applyMaxPax(n);\n  fillDatalistsForCity(n);\n  bindAddressAuto(n);\n\/* \ud83d\udc47 A\u00d1ADE ESTO AQU\u00cd: fecha m\u00ednima (hoy) para s{n}Date *\/\n  const today = new Date().toISOString().slice(0,10);\n  const d = document.getElementById(`s${n}Date`);\n  if (d && !d.min) d.min = today;\n  \/\/ Enganches del cotizador (ya lo hace para S1; a\u00f1adimos S2)\n  [\n    `#s${n}Lugar`,`#s${n}Vehiculo`,`#s${n}Servicio`,`#s${n}Pax`,\n    `#s${n}Pickup`,`#s${n}Dropoff`,`#s${n}Date`,`#s${n}Time`,\n    `#s${n}PickupExact`,`#s${n}DropoffExact`\n  ].forEach(sel=>{\n    const el = document.querySelector(sel);\n    el?.addEventListener('change', ()=>window.cotizar?.(String(n)));\n    el?.addEventListener('blur',   ()=>window.cotizar?.(String(n)));\n    el?.addEventListener('input',  ()=>window.cotizar?.(String(n)));\n  });\n  document.querySelectorAll(`.extras[data-for=\"${n}\"] input[type=\"checkbox\"]`)\n    .forEach(ch=> ch.addEventListener('change', ()=>window.cotizar?.(String(n))));\n  setTimeout(()=>window.cotizar?.(String(n)), 0);\n}\nt2?.addEventListener('change', ()=>{\n  const on = t2.value === 'S\u00ed';\n  serv2.hidden = !on;\n  serv2.dataset.open = on ? '1' : '0';\n  if (on && !serv2.__init){ initServicio(2); serv2.__init = true; }\n});\nt3?.addEventListener('change', ()=>{\n  const on = t3.value === 'S\u00ed';\n  serv3.hidden = !on;\n  serv3.dataset.open = on ? '1' : '0';\n  if (on && !serv3.__init){ initServicio(3); serv3.__init = true; }\n});\n\/\/ Auto-init S3 si ya est\u00e1 en \"S\u00ed\" al cargar o al llegar al bloque\nif (t3 && serv3 && t3.value === 'S\u00ed') {\n  serv3.hidden = false;\n  serv3.dataset.open = '1';\n  if (!serv3.__init) { initServicio(3); serv3.__init = true; }\n}\n\/* =============== PWA (opcional) =============== *\/\nfunction setupPWA(){\n  const btn = document.getElementById('btnInstall');\n  const iosGuide = document.getElementById('bukas-ios-guide');\n  const iosClose = document.getElementById('iosGuideClose');\n  let deferredPrompt = null;\n  window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; if (btn) btn.hidden = false; });\n  window.addEventListener('appinstalled', () => { if (btn) btn.hidden = true; });\n  const isIOS = \/iphone|ipad|ipod\/i.test(navigator.userAgent);\n  const isStandalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone;\n  if (isIOS && !isStandalone && btn) btn.hidden = false;\n  btn?.addEventListener('click', async () => {\n    if (isIOS && !isStandalone) { iosGuide.hidden = false; return; }\n    if (deferredPrompt) { deferredPrompt.prompt(); try { await deferredPrompt.userChoice; } catch(_) {} deferredPrompt = null; btn.hidden = true; }\n  });\n  iosClose?.addEventListener('click', () => iosGuide.hidden = true);\n}\n\/* =============== Bootstrap (REEMPLAZO COMPLETO) =============== *\/\ndocument.addEventListener('DOMContentLoaded', () => {\n  const form = document.getElementById('bukasForm');\n  if (!form) return;\n  setupPWA?.();\n\n  \/\/ i18n: idioma actual\n  applyTranslations(currentLang);\n\n  const langButtons = document.querySelectorAll('.lang-switcher [data-lang]');\n\n  \/\/ marcar idioma guardado al cargar\n  langButtons.forEach(btn => {\n    if (btn.getAttribute('data-lang') === currentLang) {\n      btn.classList.add('active');\n    }\n  });\n\n  \/\/ cambiar idioma al hacer clic\n  langButtons.forEach(btn => {\n    btn.addEventListener('click', () => {\n      const lang = btn.getAttribute('data-lang');\n      localStorage.setItem('bukas_lang', lang);\n      applyTranslations(lang);\n\n      \/\/ actualizar color de botones\n      langButtons.forEach(b => b.classList.remove('active'));\n      btn.classList.add('active');\n      \n      \/\/ After applyTranslations, re-check helpers to update text\n      setTimeout(() => {\n        \/\/ reuse the helper's refreshAll if available; otherwise do a minimal re-run:\n        if (typeof window.refreshRequiredHelpers === 'function') {\n          window.refreshRequiredHelpers(document);\n        } else {\n          \/\/ Fallback: update any existing helper nodes' text\n          const getRequiredMsg = () => {\n            if (typeof window.t === 'function') return window.t('required.msg');\n            if (typeof t === 'function') return t('required.msg');\n            return 'Campo obligatorio';\n          };\n          document.querySelectorAll('.field-error[data-required-helper]').forEach(hint => {\n            hint.textContent = getRequiredMsg();\n          });\n        }\n      }, 50);\n\n      \/\/ \ud83d\udd39 Regenerar textos dependientes del idioma\n      \/\/ 1) Mensaje de capacidad m\u00e1xima (hint.maxPax) en S1, S2 y S3\n      if (typeof applyMaxPax === 'function') {\n        [1, 2, 3].forEach(n => {\n          try { applyMaxPax(n); } catch (e) {}\n        });\n      }\n\n      \/\/ 2) Avisos amarillos de extras\n      if (typeof extraCostMsg === 'function') {\n        [1, 2, 3].forEach(idx => {\n          const aviso = document.getElementById('extrasAviso' + idx);\n          if (aviso) aviso.textContent = extraCostMsg();\n        });\n      }\n\n      \/\/ 3) L\u00ednea autom\u00e1tica en el textarea de Comentarios\n      if (window._bukasSyncExtrasToComentarios) {\n        try { window._bukasSyncExtrasToComentarios(); } catch (e) {}\n      }\n    });\n  });\n\n\n  \n\/\/ Fechas y horas por defecto: ma\u00f1ana a esta misma hora (editable)\nfunction setDefaultDatesAndTimes() {\n  const now = new Date();\n\n  \/\/ m\u00edn: hoy\n  const minDate = now.toISOString().slice(0,10);\n\n  \/\/ fecha\/hora dentro de 24h\n  const future = new Date(now.getTime() + 24*60*60*1000);\n  const tomorrowDate = future.toISOString().slice(0,10);\n\n  \/\/ hora en formato HH:MM (24h)\n  const hh = String(future.getHours()).padStart(2,'0');\n  const mm = String(future.getMinutes()).padStart(2,'0');\n  const defaultTime = `${hh}:${mm}`;\n\n  \/\/ === FECHAS (s1Date, s2Date, s3Date) ===\n  ['s1Date','s2Date','s3Date'].forEach(id => {\n    const el = document.getElementById(id);\n    if (!el) return;\n\n    \/\/ m\u00ednimo hoy\n    el.min = minDate;\n\n    \/\/ si est\u00e1 vac\u00edo, pon ma\u00f1ana (editable)\n    if (!el.value) {\n      el.value = tomorrowDate;\n    }\n  });\n\n  \/\/ === HORAS (s1Time, s2Time, s3Time) ===\n  ['s1Time','s2Time','s3Time'].forEach(id => {\n    const el = document.getElementById(id);\n    if (!el) return;\n\n    \/\/ s\u00f3lo si el campo est\u00e1 vac\u00edo \u2192 hora por defecto ma\u00f1ana a esta hora\n    if (!el.value) {\n      el.value = defaultTime;\n    }\n  });\n}\n\n\/\/ Expose on window for reuse elsewhere\nwindow.setDefaultDatesAndTimes = setDefaultDatesAndTimes;\n\nsetDefaultDatesAndTimes();\n\n\n\n\n  \/\/ Stepper\n  const stepper = makeStepper(form);\n  window.__bukasStepper = stepper; \/\/ \u2705 expuesto para revalidaciones desde otros bloques\n\n\n  \/\/ Chips: Lugar\/Veh\u00edculo\n  renderChipsForSelect(document.querySelector('#s1Lugar'));\n  renderChipsForSelect(document.querySelector('#s1Vehiculo'));\n  renderChipsForSelect(document.querySelector('#s1Servicio')); \/\/ convierte Servicio en botones desde el inicio\n  \/\/ Al cambiar lugar\/veh\u00edculo \u2192 servicios + pax + datalists + auto exactas\n  ['#s1Lugar','#s1Vehiculo'].forEach(sel=>{\n    document.querySelector(sel)?.addEventListener('change', ()=>{\n      fillServicios(1);\n      applyMaxPax(1);\n      applyMaxPax(2);\n      applyMaxPax(3);\n      fillDatalistsForCity(1);\n      bindAddressAuto(1);\n      stepper.refreshNext();\n    });\n  });\n  \/\/ Inicializa (por si hay valores pre-cargados)\n  fillServicios(1);\n  applyMaxPax(1);\n  fillDatalistsForCity(1);\n  bindAddressAuto(1);\n  \/\/ S\u00ed\/No y contacto\n  enhanceYesNo();\n  bindOtrosAviso();\n  \/\/ Pago y env\u00edo\n  bindSubmit(form);\n  \/\/ Autocomplete cliente\n  bindClienteAuto();\n  \/\/ Tecla Enter = siguiente si procede\n  form.addEventListener('keydown',(e)=>{\n    if(e.key==='Enter' && !e.shiftKey){\n      const active = document.querySelector('.form-block[data-active=\"1\"]');\n      const nextBtn = active?.querySelector('[data-next]');\n      if(nextBtn && !nextBtn.disabled){ e.preventDefault(); nextBtn.click(); }\n    }\n  });\n  \/\/ Estado inicial del bot\u00f3n siguiente\n  setTimeout(()=> stepper.refreshNext(), 0);\n\n  \/\/ Inicializar precios al cargar la p\u00e1gina\n  setTimeout(() => {\n    if (typeof window.cotizar === 'function') {\n      window.cotizar('1');\n      window.cotizar('2');\n      window.cotizar('3');\n    }\n  }, 100);\n\n  \/\/ Google Places autocomplete (si est\u00e1 disponible)\n  \/\/ Bootstrap with Promise-based loader\n  const mapsScript = document.getElementById('bukas-maps');\n  if (!mapsScript) {\n    console.warn('[places] Maps script tag not found');\n  }\n  window.__debugPlaces = true; \/\/ enable temporary logs\n  waitForMapsPlaces().then(() => {\n    initUnifiedPlacesForAllServices();\n  }).catch(err => {\n    console.error('[places] bootstrap failed', err);\n  });\n});\n\/* =============== Pricing ===============\n   IMPORTANT: ALL prices are calculated by the backend (Apps Script).\n   The frontend displays prices returned from the backend in real time.\n   Backend reads from spreadsheet \"Tarifas\" sheet and computes final prices with surcharges.\n   NO local pricing logic is allowed.\n*\/\nfunction bindPricing(n){\n  ['#s'+n+'Lugar', '#s'+n+'Vehiculo', '#s'+n+'Servicio', '#cliNum', '#cliTax']\n    .forEach(sel=>{\n      const el = document.querySelector(sel);\n      if (!el) return;\n      ['change','input','blur'].forEach(ev=> el.addEventListener(ev, ()=>computeTotals()));\n    });\n}\n\/\/ Vincula para S1 inmediatamente. Para S2 y S3 al abrirse o al cambio del toggle.\n\/* Precios por tabla desactivados: el cotizador din\u00e1mico controla #pagoTotal *\/\n\/\/ bindPricing(1); bindPricing(2); bindPricing(3);\ndocument.querySelector('#t2')?.addEventListener('change', ()=>{\n  if (typeof window.cotizar === 'function') {\n    window.cotizar('1');\n    window.cotizar('2');\n    window.cotizar('3');\n  }\n});\ndocument.querySelector('#t3')?.addEventListener('change', ()=>{\n  if (typeof window.cotizar === 'function') {\n    window.cotizar('1');\n    window.cotizar('2');\n    window.cotizar('3');\n  }\n});\n\/\/ Inicializar precios para todos los servicios al cargar (con delay para asegurar que cotizar est\u00e9 definido)\nsetTimeout(()=>{\n  if (typeof window.cotizar === 'function') {\n    window.cotizar('1');\n    window.cotizar('2');\n    window.cotizar('3');\n  }\n}, 200);\n\n\/* === Integraci\u00f3n con env\u00edo: nota de cotizaci\u00f3n + (opcional) total === *\/\n(function patchSubmitComentarios(){\n  const form = document.querySelector('#bukasForm');\n  if (!form) return;\n\n  form.addEventListener('submit', (e)=>{\n    try{\n      \/\/ 1) Forzar rec\u00e1lculo justo antes de leer el total\n      if (typeof window.cotizar === 'function') {\n        window.cotizar('1');\n        window.cotizar('2');\n        window.cotizar('3');\n      }\n      if (typeof actualizarTotales === 'function') {\n        actualizarTotales();\n      }\n\n    \/\/ Aseguramos que el aviso de servicios adicionales est\u00e9 actualizado\n      if (typeof window._bukasSyncExtrasToComentarios === 'function') {\n        window._bukasSyncExtrasToComentarios();\n      }\n\n          const ta = form.querySelector('textarea[name=\"Comentarios u observaciones\"]');\n      if (!ta) return;\n\n      let txt = ta.value || '';\n\n      \/\/ 2) A partir de ahora NO a\u00f1adimos el aviso de cotizaci\u00f3n personalizada\n      \/\/    al textarea; solo nos aseguramos de que no quede ning\u00fan resto antiguo.\n      txt = txt.replace(\n        \/\\n?\u2014 .*(cotizaci\u00f3n personalizada|custom quote|pressupost personalitzat|devis personnalis\u00e9).*$\/i,\n        ''\n      );\n\n      \/\/ 3) (Opcional) a\u00f1adir tambi\u00e9n el total estimado si alg\u00fan d\u00eda lo quieres\n      \/*\n      const totalTxt = (document.querySelector('#pagoTotal')?.textContent || '').trim();\n      ...\n      *\/\n\n      ta.value = txt;\n\n    }catch(_){\n      \/\/ silencio: que nunca bloquee el env\u00edo\n    }\n  }, true); \/\/ captura: antes que otros listeners\n})();\n<\/script>\n\n\n\n<!-- === PATCH BEGIN :: AUTOCOMPLETAR LUGARES + DISTANCIA (limpio) === -->\n<script>\n\/* ========= Helpers de ciudad \/ distancia \/ escritura ================== *\/\nfunction _ciudadDesdeDetalle_(det){\n  const c = (det && det.cityDetected || '').toLowerCase();\n  if (!c) return '';\n  if (\/\\bbarcelona\\b\/.test(c)) return 'Barcelona';\n  if (\/\\bmadrid\\b\/.test(c))    return 'Madrid';\n  return '';\n}\nfunction _setLugarSiConviene_(idx, ciudad){\n  const sel = document.getElementById(`s${idx}Lugar`);\n  if (!sel || !ciudad) return;\n  const prev = (sel.value || '').trim();\n  if (!prev || prev.toLowerCase() !== ciudad.toLowerCase()){\n    sel.value = ciudad;\n    sel.dispatchEvent(new Event('change', { bubbles:true }));\n  }\n}\nfunction _writeExactAddress_(inputEl, idx, formatted){\n  if (!inputEl) return;\n  const isPickup  = \/Pickup$\/.test(inputEl.id);\n  const suffix    = isPickup ? 'PickupExact' : 'DropoffExact';\n  const exactEl   = document.getElementById(`s${idx}${suffix}`);\n  if (exactEl) exactEl.value = formatted || '';\n}\nfunction _addressSuffix_(inputEl){\n  return (\/Pickup$\/i).test(inputEl?.id || '') ? 'Pickup' : 'Dropoff';\n}\nfunction _setHiddenAddressData_(idx, suffix, candidate){\n  const latEl = document.getElementById(`s${idx}${suffix}Lat`);\n  const lngEl = document.getElementById(`s${idx}${suffix}Lng`);\n  const idEl  = document.getElementById(`s${idx}${suffix}PlaceId`);\n  const lat = candidate && candidate.lat != null ? String(candidate.lat) : '';\n  const lng = candidate && candidate.lng != null ? String(candidate.lng) : '';\n  const placeId = candidate && candidate.placeId ? String(candidate.placeId) : '';\n  if (latEl) latEl.value = lat;\n  if (lngEl) lngEl.value = lng;\n  if (idEl)  idEl.value  = placeId;\n}\nfunction _markAddressChoicePending_(inputEl, pending){\n  if (!inputEl) return;\n  if (pending){\n    inputEl.dataset.addressChoice = 'pending';\n    inputEl.classList.add('is-invalid');\n    inputEl.setCustomValidity(' ');\n  } else if (inputEl.dataset.addressChoice){\n    delete inputEl.dataset.addressChoice;\n    inputEl.classList.remove('is-invalid');\n    inputEl.setCustomValidity('');\n  }\n}\nfunction _clearAddressCandidates_(inputEl){\n  if (!inputEl) return;\n  const boxId = `${inputEl.id}Candidates`;\n  const box = document.getElementById(boxId);\n  if (box) box.remove();\n  delete inputEl.dataset.addressChoice;\n}\nfunction _resetAddressData_(inputEl, idx){\n  if (!inputEl) return;\n  const suffix = _addressSuffix_(inputEl);\n  delete inputEl.dataset.lat;\n  delete inputEl.dataset.lng;\n  delete inputEl.dataset.placeId;\n  _setHiddenAddressData_(idx, suffix, null);\n  _writeExactAddress_(inputEl, idx, '');\n  _updateDistance_(idx);\n  if (typeof window._bukasCustomQuoteCheck === 'function') {\n    window._bukasCustomQuoteCheck(idx);\n  }\n  if (typeof window.cotizar === 'function') {\n    window.cotizar(String(idx));\n  }\n}\nfunction _storeAddressSelection_(inputEl, idx, candidate){\n  if (!inputEl) return;\n  const suffix = _addressSuffix_(inputEl);\n  \/\/ Set visible input to formatted address (Google Maps style - single box shows exact address)\n  const formatted = candidate && candidate.formatted ? candidate.formatted : inputEl.value || '';\n  if (formatted && inputEl.value !== formatted) {\n    inputEl.value = formatted;\n  }\n  _writeExactAddress_(inputEl, idx, formatted);\n  if (candidate && candidate.lat != null) inputEl.dataset.lat = String(candidate.lat); else delete inputEl.dataset.lat;\n  if (candidate && candidate.lng != null) inputEl.dataset.lng = String(candidate.lng); else delete inputEl.dataset.lng;\n  if (candidate && candidate.placeId) inputEl.dataset.placeId = String(candidate.placeId); else delete inputEl.dataset.placeId;\n  _setHiddenAddressData_(idx, suffix, candidate);\n  const ciudad = candidate && candidate.cityDetected ? candidate.cityDetected : '';\n  if (ciudad) _setLugarSiConviene_(idx, ciudad);\n  _updateDistance_(idx);\n  if (typeof window._bukasCustomQuoteCheck === 'function') {\n    window._bukasCustomQuoteCheck(idx);\n  }\n  if (typeof window.cotizar === 'function') {\n    window.cotizar(String(idx));\n  }\n  _markAddressChoicePending_(inputEl, false);\n  _clearAddressCandidates_(inputEl);\n  try { window.__bukasStepper?.refreshNext?.(); } catch (_) {}\n}\nfunction _renderAddressCandidates_(inputEl, idx, candidates, titleKey){\n  if (!inputEl) return;\n  _clearAddressCandidates_(inputEl);\n  const parent = inputEl.closest('label') || inputEl.parentElement;\n  if (!parent) return;\n  const box = document.createElement('div');\n  box.id = `${inputEl.id}Candidates`;\n  box.className = 'address-candidates';\n  if (Array.isArray(candidates) && candidates.length){\n    const title = document.createElement('p');\n    title.className = 'address-candidates__title';\n    title.textContent = t(titleKey || 'address.choose');\n    box.appendChild(title);\n    candidates.forEach(cand => {\n      const btn = document.createElement('button');\n      btn.type = 'button';\n      btn.className = 'address-candidates__option';\n      btn.textContent = cand.formatted || '';\n      btn.addEventListener('click', () => _storeAddressSelection_(inputEl, idx, cand));\n      box.appendChild(btn);\n    });\n  } else {\n    const msg = document.createElement('p');\n    msg.className = 'address-candidates__message';\n    msg.textContent = t('address.noResults');\n    box.appendChild(msg);\n  }\n  parent.appendChild(box);\n  _markAddressChoicePending_(inputEl, true);\n  try { window.__bukasStepper?.refreshNext?.(); } catch (_) {}\n}\n\n\/**\n * Debug logging helper (guarded by window.__debugPlaces)\n *\/\nconst dlog = (...a) => {\n  if (window.__debugPlaces) console.debug('[places]', ...a);\n};\n\n\/**\n * Clear place selection for an input (clears all hidden fields and triggers events)\n *\/\nfunction clearPlaceSelection(anchorInput) {\n  if (!anchorInput) return;\n  \n  anchorInput.value = '';\n  const match = anchorInput.id && anchorInput.id.match(\/^s(\\d+)(Pickup|Dropoff)$\/);\n  if (match) {\n    const idx = Number(match[1]);\n    const suffix = match[2];\n    const exactEl = document.getElementById(`s${idx}${suffix}Exact`);\n    const nameEl = document.getElementById(`s${idx}${suffix}Name`);\n    const latEl = document.getElementById(`s${idx}${suffix}Lat`);\n    const lngEl = document.getElementById(`s${idx}${suffix}Lng`);\n    const placeIdEl = document.getElementById(`s${idx}${suffix}PlaceId`);\n    if (exactEl) exactEl.value = '';\n    if (nameEl) nameEl.value = '';\n    if (latEl) latEl.value = '';\n    if (lngEl) lngEl.value = '';\n    if (placeIdEl) placeIdEl.value = '';\n    _setHiddenAddressData_(idx, suffix, null);\n    \n    \/\/ Trigger downstream events\n    anchorInput.dispatchEvent(new Event('input', { bubbles: true }));\n    anchorInput.dispatchEvent(new Event('change', { bubbles: true }));\n    if (exactEl) {\n      exactEl.dispatchEvent(new Event('input', { bubbles: true }));\n      exactEl.dispatchEvent(new Event('change', { bubbles: true }));\n    }\n    _updateDistance_(idx);\n    if (typeof window._bukasCustomQuoteCheck === 'function') {\n      window._bukasCustomQuoteCheck(idx);\n    }\n    if (typeof window.cotizar === 'function') {\n      window.cotizar(String(idx));\n    }\n    try { window.__bukasStepper?.refreshNext?.(); } catch (_) {}\n  }\n}\n\nfunction getOrCreatePlaceChip(anchorInput){\n  let chip = anchorInput.nextElementSibling;\n  if (!chip || !chip.classList.contains('place-chip')) {\n    chip = document.createElement('div');\n    chip.className = 'place-chip';\n    chip.innerHTML = `\n      <div class=\"place-chip__icon\" aria-hidden=\"true\">\ud83d\udccd<\/div>\n      <div class=\"place-chip__body\">\n        <div class=\"place-chip__name\"><\/div>\n        <div class=\"place-chip__addr\"><\/div>\n      <\/div>\n      <button type=\"button\" class=\"place-chip__clear\" title=\"Limpiar\">\u00d7<\/button>\n    `;\n    anchorInput.insertAdjacentElement('afterend', chip);\n\n    chip.querySelector('.place-chip__clear').addEventListener('click', () => {\n      clearPlaceSelection(anchorInput);\n      hidePlaceChip(anchorInput);\n      anchorInput.focus();\n    });\n\n    chip.addEventListener('click', (e) => {\n      if (e.target.closest('.place-chip__clear')) return;\n      anchorInput.focus();\n    });\n  }\n  return chip;\n}\n\nfunction updatePlaceChip(anchorInput, name, formatted){\n  const chip = getOrCreatePlaceChip(anchorInput);\n  chip.querySelector('.place-chip__name').textContent = name || '';\n  chip.querySelector('.place-chip__addr').textContent = formatted || '';\n  chip.style.display = (name || formatted) ? 'inline-flex' : 'none';\n}\n\nfunction hidePlaceChip(anchorInput){\n  const chip = anchorInput.nextElementSibling;\n  if (chip && chip.classList.contains('place-chip')) {\n    chip.style.display = 'none';\n  }\n}\n\n\/**\n * Global flag to detect clicks on PAC dropdown\n *\/\nlet __pacClicking = false;\n\n\/\/ Helper: cerrar todos los dropdowns de Google Places (sin romper futuros autocompletes)\nfunction closeAllPacDropdowns() {\n  try {\n    document.querySelectorAll('.pac-container').forEach(pc => {\n      pc.style.display = 'none';\n    });\n  } catch (_) {\n    \/\/ no-op\n  }\n}\n\n\/\/ Helper: cerrar los dropdowns varias veces en un corto intervalo\nfunction closeAllPacDropdownsSoon() {\n  \/\/ Google a veces reabre o reajusta el dropdown tras place_changed;\n  \/\/ hacemos varios cierres espaciados para cubrir esos casos.\n  [0, 80, 160, 260].forEach(delay => {\n    setTimeout(closeAllPacDropdowns, delay);\n  });\n}\n\n\/\/ Listen for mousedown on PAC dropdown items\ndocument.addEventListener('mousedown', (e) => {\n  const el = e.target;\n  if (el && (el.closest('.pac-item') || el.closest('.pac-container'))) {\n    __pacClicking = true;\n    \/\/ reset shortly after so blur can run later if needed\n    setTimeout(() => { __pacClicking = false; }, 300);\n  }\n}, true);\n\n\/\/ Tras hacer click en una sugerencia, cerrar el dropdown de Google\ndocument.addEventListener('click', (e) => {\n  const el = e.target;\n  if (el && el.closest('.pac-item')) {\n    \/\/ Dejamos que Google procese el click y luego cerramos\n    setTimeout(() => {\n      closeAllPacDropdownsSoon();\n      try {\n        if (document.activeElement && document.activeElement.tagName === 'INPUT') {\n          document.activeElement.blur();\n        }\n      } catch (_) {}\n    }, 150);\n  }\n}, true);\n\n\/**\n * Robust Promise-based loader for Google Maps Places API\n *\/\nfunction waitForMapsPlaces(timeoutMs = 10000) {\n  return new Promise((resolve, reject) => {\n    const t0 = Date.now();\n    (function tick() {\n      if (window.google?.maps?.places && window.google.maps.places.Autocomplete) {\n        return resolve();\n      }\n      if (Date.now() - t0 > timeoutMs) {\n        console.error('[places] timeout waiting for google.maps.places');\n        return reject(new Error('maps-places-timeout'));\n      }\n      setTimeout(tick, 100);\n    })();\n  });\n}\n\n\/**\n * Compute best display name from a place object and formatted address\n * @param {Object} place - Google Places place object\n * @param {string} formatted - Formatted address string\n * @returns {string} Best display name or empty string\n *\/\nfunction bestPlaceName(place, formatted) {\n  \/\/ PRIORITY 1: Always use place.name if it exists - this is the official name from Google Places\n  if (place && place.name && place.name.trim()) {\n    const name = place.name.trim();\n    \/\/ Use place.name UNLESS it's exactly the same as the full formatted address (very rare)\n    \/\/ In that case, it's likely not a real name but just the address repeated\n    if (name !== formatted) {\n      return name;\n    }\n    \/\/ Even if name equals formatted, if it's a POI\/lodging, it's probably still the correct name\n    if (place.types && Array.isArray(place.types)) {\n      const t = place.types;\n      if (t.includes('lodging') || t.includes('point_of_interest') || t.includes('establishment') || \n          t.includes('restaurant') || t.includes('store') || t.includes('shopping_mall')) {\n        return name; \/\/ For POIs, use place.name even if it matches formatted\n      }\n    }\n  }\n\n  \/\/ PRIORITY 2: Only if place.name is NOT available, try to extract from formatted address\n  \/\/ This should rarely happen, but it's a fallback\n  if (!place || !place.name || !place.name.trim()) {\n    const s = (formatted || '').trim();\n    const first = s.split(',')[0] || '';\n    \/\/ Only extract if it looks like a venue name (contains hotel\/POI keywords)\n    const poiHints = \/hotel|nh|hilton|melia|ibis|ac hotel|parador|hostal|majestic|palace|ritz|sheraton|marriott|radisson|novotel|mercure|barcel\u00f3|sol|gran|royal|princesa|princep|pr\u00edncipe|aeropuerto|airport|estaci\u00f3n|estacio|station|museu|museum|hospital|ikea|corte ingl\u00e9s|ciutat|camp nou|palau|auditori|universitat\/i;\n    if (poiHints.test(first)) {\n      return first.trim();\n    }\n  }\n\n  \/\/ Fallback: return empty to allow email builder to shortLabel(address)\n  return '';\n}\n\n\/**\n * Unified Google Maps-style address autocomplete helper\n * @param {HTMLElement} inputEl - The visible input field\n * @param {Object} refs - Object with exactEl, latEl, lngEl, placeIdEl, nameEl\n * @param {Function} getCityFn - Function that returns the selected city name\n *\/\nfunction attachUnifiedPlaces(inputEl, refs, getCityFn) {\n  if (!inputEl || !refs || !refs.exactEl) {\n    dlog('attach-skip', inputEl?.id, 'missing elements');\n    return;\n  }\n  if (inputEl._autocompleteAttached) {\n    dlog('attach-skip', inputEl.id, 'already attached');\n    return;\n  }\n  if (!window.google?.maps?.places?.Autocomplete) {\n    dlog('attach-skip', inputEl.id, 'Places API not ready');\n    return;\n  }\n\n  try { inputEl.setAttribute('autocomplete', 'off'); } catch(_) {}\n\n  \/\/ Create Autocomplete\n  \/\/ Note: fields are only needed when calling getPlace(), not for the dropdown suggestions\n  \/\/ The dropdown will show suggestions automatically as the user types\n  const ac = new google.maps.places.Autocomplete(inputEl, {\n    fields: ['place_id', 'geometry', 'name', 'formatted_address', 'types'],\n    \/\/ optional: componentRestrictions: { country: ['ES'] },\n  });\n  dlog('ac-created', inputEl.id);\n  \n  \/\/ Ensure the autocomplete is working - log when suggestions appear\n  if (window.__debugPlaces) {\n    \/\/ The autocomplete dropdown is managed by Google, we just need to ensure it's not blocked\n    console.log('[places] Autocomplete created for', inputEl.id, 'dropdown should appear automatically');\n  }\n\n  \/\/ On selection: update all fields\n  ac.addListener('place_changed', () => {\n    const place = ac.getPlace();\n    dlog('place_changed', inputEl.id, place?.place_id, place?.formatted_address, 'place.name:', place?.name);\n    \n    if (!place || !place.geometry || !place.formatted_address) {\n      dlog('place_changed-invalid', inputEl.id);\n      return;\n    }\n\n    const formatted = place.formatted_address || inputEl.value || '';\n    const name = bestPlaceName(place, formatted);\n    dlog('bestPlaceName result', inputEl.id, { name, placeName: place?.name, formatted });\n    const lat = place.geometry.location.lat();\n    const lng = place.geometry.location.lng();\n    const placeId = place.place_id || '';\n\n    \/\/ Set visible input to formatted address (Google Maps style)\n    inputEl.value = formatted;\n    \n    \/\/ Update hidden fields\n    refs.exactEl.value = formatted;\n    \/\/ ALWAYS use place.name if available - this is the official name from Google Places\n    \/\/ bestPlaceName should have returned it, but we double-check here\n    let finalName = name || '';\n    \/\/ If bestPlaceName didn't return place.name (shouldn't happen), use it directly\n    if (!finalName && place && place.name && place.name.trim()) {\n      finalName = place.name.trim();\n    }\n    \/\/ DO NOT extract from formatted address - that would give us street names, not place names\n    \/\/ Only use bestPlaceName result or place.name directly\n    if (refs.nameEl) refs.nameEl.value = finalName;\n    if (refs.latEl) refs.latEl.value = String(lat);\n    if (refs.lngEl) refs.lngEl.value = String(lng);\n    if (refs.placeIdEl) refs.placeIdEl.value = placeId;\n    \n    \/\/ Debug warning if name is missing\n    if (!refs.nameEl?.value) {\n      console.warn('[places] missing Name; will fall back to shortLabel in email', {id: inputEl.id, formatted});\n    }\n    \n    \/\/ Update chip with final name (use the one we set in nameEl)\n    const chipName = refs.nameEl?.value || name || '';\n    updatePlaceChip(inputEl, chipName, formatted || '');\n\n    \/\/ Update dataset for compatibility\n    inputEl.dataset.lat = String(lat);\n    inputEl.dataset.lng = String(lng);\n    inputEl.dataset.placeId = placeId;\n\n    \/\/ Detect city and update via existing helper\n    let cityDetected = '';\n    if (Array.isArray(place.address_components)) {\n      place.address_components.forEach(component => {\n        const types = component.types || [];\n        if (types.includes('locality') || types.includes('postal_town')) {\n          if (!cityDetected) cityDetected = component.long_name || '';\n        }\n      });\n    }\n\n    const match = inputEl.id && inputEl.id.match(\/^s(\\d+)(Pickup|Dropoff)$\/);\n    const idx = match ? Number(match[1]) : null;\n    if (idx != null) {\n      const candidate = {\n        formatted,\n        lat,\n        lng,\n        placeId,\n        cityDetected\n      };\n      _setHiddenAddressData_(idx, match[2], candidate);\n      if (cityDetected) _setLugarSiConviene_(idx, cityDetected);\n      _updateDistance_(idx);\n      if (typeof window._bukasCustomQuoteCheck === 'function') {\n        window._bukasCustomQuoteCheck(idx);\n      }\n      if (typeof window.cotizar === 'function') {\n        window.cotizar(String(idx));\n      }\n    }\n\n    \/\/ Clear any validation errors since the field now has a valid value\n    if (typeof clearInvalid === 'function') {\n      clearInvalid(inputEl);\n    }\n    \n  \/\/ Trigger downstream logic (distance\/price\/pax\/etc.)\n    inputEl.dispatchEvent(new Event('input', { bubbles: true }));\n    inputEl.dispatchEvent(new Event('change', { bubbles: true }));\n    refs.exactEl.dispatchEvent(new Event('input', { bubbles: true }));\n    refs.exactEl.dispatchEvent(new Event('change', { bubbles: true }));\n    try { window.__bukasStepper?.refreshNext?.(); } catch (_) {}\n\n    \/\/ Forzar cierre del dropdown de Google (PC y m\u00f3vil)\n  closeAllPacDropdownsSoon();\n    try {\n      inputEl.blur();\n    } catch (_) {}\n  });\n\n  \/\/ When the user types, clear stale hidden values so we don't use old data\n  \/\/ Use a small delay to avoid interfering with Google's autocomplete dropdown\n  let inputTimeout = null;\n  inputEl.addEventListener('input', () => {\n    \/\/ Clear any pending timeout\n    if (inputTimeout) clearTimeout(inputTimeout);\n    \n    \/\/ Delay clearing to allow autocomplete dropdown to appear first\n    inputTimeout = setTimeout(() => {\n      \/\/ Only clear if no place is currently selected (no placeId)\n      if (!refs.placeIdEl || !refs.placeIdEl.value) {\n        refs.exactEl.value = '';\n        if (refs.nameEl) refs.nameEl.value = '';\n        if (refs.latEl) refs.latEl.value = '';\n        if (refs.lngEl) refs.lngEl.value = '';\n        if (refs.placeIdEl) refs.placeIdEl.value = '';\n        delete inputEl.dataset.lat;\n        delete inputEl.dataset.lng;\n        delete inputEl.dataset.placeId;\n        updatePlaceChip(inputEl, '', ''); \/\/ hide chip\n        const match = inputEl.id && inputEl.id.match(\/^s(\\d+)(Pickup|Dropoff)$\/);\n        if (match) {\n          _setHiddenAddressData_(Number(match[1]), match[2], null);\n        }\n      }\n    }, 100); \/\/ Small delay to not interfere with autocomplete\n  });\n\n  \/\/ Prevent accidental form submit on Enter, but allow autocomplete to work\n  inputEl.addEventListener('keydown', (e) => {\n    \/\/ Only prevent Enter if a suggestion is not being selected\n    \/\/ Allow Enter to work normally for autocomplete selection\n    if (e.key === 'Enter' && !e.target.value.trim()) {\n      e.preventDefault();\n    }\n  });\n\n  \/\/ Blur fallback: if user left free text and no placeId, ask backend to resolve\n  \/\/ Deferred to run AFTER place_changed when clicking a suggestion\n  inputEl.addEventListener('blur', () => {\n    \/\/ if a PAC click is in progress, skip (place_changed will run)\n    if (__pacClicking) return;\n\n    \/\/ small deferral to let place_changed fire first\n    setTimeout(async () => {\n      \/\/ re-check guards\n      if (__pacClicking) return;\n      if (refs.placeIdEl && refs.placeIdEl.value) return; \/\/ already selected a place\n      const q = inputEl.value.trim();\n      if (!q) return;\n\n      const city = (getCityFn && getCityFn()) || '';\n      dlog('blur-fallback', inputEl.id, { q, city });\n\n      try {\n        \/\/ Use existing fetch helper\/webapp in this project\n        if (!window.WEBAPP_URL) {\n          dlog('blur-fallback-no-url', inputEl.id);\n          return;\n        }\n\n        const params = new URLSearchParams({ action: 'lugar', query: q });\n        if (city) params.set('city', city);\n\n        const url = `${window.WEBAPP_URL}?${params.toString()}`;\n        const r = await fetchWithTimeout(url, {\n          cache: 'no-store',\n          timeout: 5000\n        });\n        if (!r || !r.ok) {\n          dlog('blur-fallback-fetch-failed', inputEl.id);\n          return;\n        }\n\n        const j = await r.json().catch(() => null);\n        if (!j || j.ok === false) {\n          dlog('blur-fallback-no-resolve', inputEl.id, j);\n          return;\n        }\n\n      \/\/ Expected response shape: { ok:true, formatted, lat, lng, placeId, name } or { ok:false }\n      const formatted = j.formatted || (j.candidates && j.candidates[0]?.formatted) || q;\n      const firstCandidate = j.candidates && j.candidates[0];\n      \/\/ PRIORITY: Use name from backend response if available\n      const rawName = j.name || firstCandidate?.name || '';\n      let name = rawName && rawName.trim() ? rawName.trim() : '';\n      \n      \/\/ Only use bestPlaceName as last resort if backend didn't provide a name\n      \/\/ But DO NOT extract from formatted address - that would give street names, not place names\n      \/\/ bestPlaceName with null place will only extract if it contains POI keywords\n      if (!name) {\n        name = bestPlaceName(null, formatted);\n      }\n      \n      const lat = typeof j.lat === 'number' ? j.lat : ((j.candidates && j.candidates[0]?.lat) ?? null);\n      const lng = typeof j.lng === 'number' ? j.lng : ((j.candidates && j.candidates[0]?.lng) ?? null);\n      const placeId = j.placeId || (j.candidates && j.candidates[0]?.placeId) || '';\n      const cityDetected = j.cityDetected || (j.candidates && j.candidates[0]?.cityDetected) || '';\n\n      \/\/ Update visible input to formatted address\n      inputEl.value = formatted;\n      \n      \/\/ Update hidden fields\n      refs.exactEl.value = formatted;\n      if (refs.nameEl) refs.nameEl.value = name || '';\n      if (refs.latEl && lat != null) refs.latEl.value = String(lat);\n      if (refs.lngEl && lng != null) refs.lngEl.value = String(lng);\n      if (refs.placeIdEl) refs.placeIdEl.value = placeId;\n      \n      \/\/ Update chip with the name we set\n      const chipName = refs.nameEl?.value || name || '';\n      updatePlaceChip(inputEl, chipName, formatted);\n\n        \/\/ Update dataset\n        if (lat != null) inputEl.dataset.lat = String(lat);\n        if (lng != null) inputEl.dataset.lng = String(lng);\n        if (placeId) inputEl.dataset.placeId = placeId;\n\n        const match = inputEl.id && inputEl.id.match(\/^s(\\d+)(Pickup|Dropoff)$\/);\n        const idx = match ? Number(match[1]) : null;\n        if (idx != null) {\n          const selected = { formatted, lat, lng, placeId, cityDetected };\n          _setHiddenAddressData_(idx, match[2], selected);\n          if (cityDetected) _setLugarSiConviene_(idx, cityDetected);\n          _updateDistance_(idx);\n          if (typeof window._bukasCustomQuoteCheck === 'function') {\n            window._bukasCustomQuoteCheck(idx);\n          }\n          if (typeof window.cotizar === 'function') {\n            window.cotizar(String(idx));\n          }\n        }\n\n        \/\/ Dispatch events\n        inputEl.dispatchEvent(new Event('input', { bubbles: true }));\n        inputEl.dispatchEvent(new Event('change', { bubbles: true }));\n        refs.exactEl.dispatchEvent(new Event('input', { bubbles: true }));\n        refs.exactEl.dispatchEvent(new Event('change', { bubbles: true }));\n        try { window.__bukasStepper?.refreshNext?.(); } catch (_) {}\n        \n        dlog('blur-resolved', inputEl.id, formatted);\n        \/\/ Cerrar dropdown de Google tambi\u00e9n tras blur-fallback\n        closeAllPacDropdownsSoon();\n        try {\n          inputEl.blur();\n        } catch (_) {}\n      } catch (e) {\n        console.error('[places] blur fallback failed', e);\n        dlog('blur-fallback-error', inputEl.id, e);\n      }\n    }, 200); \/\/ 150\u2013250ms is enough to let place_changed win\n  });\n\n  inputEl._autocompleteAttached = true;\n}\n\n\/**\n * Initialize unified Places autocomplete for all services (1, 2, 3)\n *\/\nfunction initUnifiedPlacesForAllServices() {\n  if (!window.google || !google.maps || !google.maps.places) {\n    dlog('init-skip', 'Places API not ready');\n    return;\n  }\n  \n  dlog('init-start');\n  [1, 2, 3].forEach(n => {\n    \/\/ Pickup elements\n    const pickup = document.getElementById(`s${n}Pickup`);\n    const pickupExact = document.getElementById(`s${n}PickupExact`);\n    const pickupName = document.getElementById(`s${n}PickupName`);\n    const pickupLat = document.getElementById(`s${n}PickupLat`);\n    const pickupLng = document.getElementById(`s${n}PickupLng`);\n    const pickupPlaceId = document.getElementById(`s${n}PickupPlaceId`);\n    \n    \/\/ Dropoff elements\n    const dropoff = document.getElementById(`s${n}Dropoff`);\n    const dropoffExact = document.getElementById(`s${n}DropoffExact`);\n    const dropoffName = document.getElementById(`s${n}DropoffName`);\n    const dropoffLat = document.getElementById(`s${n}DropoffLat`);\n    const dropoffLng = document.getElementById(`s${n}DropoffLng`);\n    const dropoffPlaceId = document.getElementById(`s${n}DropoffPlaceId`);\n    \n    \/\/ City getter function\n    const getCityFn = () => {\n      const citySelect = document.getElementById(`s${n}Lugar`);\n      return citySelect ? (citySelect.value || '').trim() : '';\n    };\n    \n    \/\/ Set autocomplete off\n    if (pickup) try { pickup.setAttribute('autocomplete', 'off'); } catch(_) {}\n    if (dropoff) try { dropoff.setAttribute('autocomplete', 'off'); } catch(_) {}\n    \n    \/\/ Attach to pickup\n    if (pickup && pickupExact) {\n      attachUnifiedPlaces(pickup, {\n        exactEl: pickupExact,\n        nameEl: pickupName,\n        latEl: pickupLat,\n        lngEl: pickupLng,\n        placeIdEl: pickupPlaceId\n      }, getCityFn);\n    }\n    \n    \/\/ Attach to dropoff\n    if (dropoff && dropoffExact) {\n      attachUnifiedPlaces(dropoff, {\n        exactEl: dropoffExact,\n        nameEl: dropoffName,\n        latEl: dropoffLat,\n        lngEl: dropoffLng,\n        placeIdEl: dropoffPlaceId\n      }, getCityFn);\n    }\n  });\n  dlog('init-complete');\n}\n\n\/\/ Legacy function name for compatibility (calls new unified init)\nlet __placesInitOnce = false;\nfunction initPlacesAutocompleteForBukas(){\n  if (__placesInitOnce) return;\n  __placesInitOnce = true;\n  initUnifiedPlacesForAllServices();\n}\n\nfunction showAddressCandidates(targetInput, addresses, kind){\n  if (!targetInput) return;\n  const arr = Array.isArray(addresses) ? addresses.filter(a => !!String(a || '').trim()) : [];\n  const match = targetInput.id && targetInput.id.match(\/^s(\\d+)(Pickup|Dropoff)Exact$\/);\n  if (!match) {\n    if (!arr.length) {\n      _clearAddressCandidates_(targetInput);\n      _markAddressChoicePending_(targetInput, false);\n    }\n    return;\n  }\n  const idx = Number(match[1]);\n  if (!arr.length){\n    _clearAddressCandidates_(targetInput);\n    _markAddressChoicePending_(targetInput, false);\n    return;\n  }\n  targetInput.value = '';\n  const titleKey = kind === 'pickup'\n    ? 'addressCandidates.pickupTitle'\n    : 'addressCandidates.dropoffTitle';\n  const candidates = arr.map(addr => ({\n    formatted: String(addr),\n    lat: null,\n    lng: null,\n    placeId: '',\n    cityDetected: ''\n  }));\n  _renderAddressCandidates_(targetInput, idx, candidates, titleKey);\n  targetInput.focus?.();\n}\n\n\/* Distancia Haversine (l\u00ednea recta) *\/\nfunction _haversineKm_(lat1, lon1, lat2, lon2){\n  const toRad = d => d * Math.PI \/ 180;\n  const R = 6371; \/\/ km\n  const dLat = toRad(lat2 - lat1);\n  const dLon = toRad(lon2 - lon1);\n  const a = Math.sin(dLat\/2)**2 +\n            Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *\n            Math.sin(dLon\/2)**2;\n  return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));\n}\n\/* Recalcula y pinta la distancia de un servicio (si hay coords en ambos) *\/\nfunction _updateDistance_(idx){\n  const p   = document.getElementById(`s${idx}Pickup`);\n  const d   = document.getElementById(`s${idx}Dropoff`);\n  const out = document.getElementById(`s${idx}Distance`);\n  if(!p || !d || !out) return;\n  const la  = parseFloat(p?.dataset?.lat);\n  const lo  = parseFloat(p?.dataset?.lng);\n  const la2 = parseFloat(d?.dataset?.lat);\n  const lo2 = parseFloat(d?.dataset?.lng);\n  if([la,lo,la2,lo2].every(Number.isFinite)){\n    const km = _haversineKm_(la,lo,la2,lo2);\n    out.value = km.toFixed(1).replace('.', ','); \/\/ estilo es-ES\n  }else{\n    out.value = ''; \/\/ sin coordenadas, sin distancia\n  }\n}\n\/* =================== Lookup + autocompletar (como el antiguo) =========================== *\/\nasync function _lookupYAutocompleta_(inputEl, idx){\n  if (!inputEl || !window.WEBAPP_URL) return;\n  const q = String(inputEl.value || '').trim();\n  if (!q){\n    _markAddressChoicePending_(inputEl, false);\n    _clearAddressCandidates_(inputEl);\n    _resetAddressData_(inputEl, idx);\n    return;\n  }\n  const params = new URLSearchParams({ action: 'lugar', query: q });\n  const citySel = document.getElementById(`s${idx}Lugar`)?.value?.trim();\n  if (citySel) params.set('city', citySel);\n\n  try {\n    const url = `${WEBAPP_URL}?${params.toString()}`;\n    const r = await fetchWithTimeout(url, {\n      cache: 'no-store',\n      timeout: 5000\n    });\n    if (!r || !r.ok) return;\n\n    const j = await r.json().catch(() => null);\n    if (!j || j.ok === false) {\n      _resetAddressData_(inputEl, idx);\n      _renderAddressCandidates_(inputEl, idx, []);\n      return;\n    }\n\n    const candidates = Array.isArray(j.candidates) ? j.candidates.slice(0, 5) : [];\n    if (j.needsAddressChoice && candidates.length > 1) {\n      _resetAddressData_(inputEl, idx);\n      _renderAddressCandidates_(inputEl, idx, candidates);\n      const cityHint = (candidates.find(c => c.cityDetected)?.cityDetected) || j.cityDetected || '';\n      if (cityHint) _setLugarSiConviene_(idx, cityHint);\n      return;\n    }\n    if (j.needsAddressChoice && candidates.length === 0) {\n      _resetAddressData_(inputEl, idx);\n      _renderAddressCandidates_(inputEl, idx, []);\n      return;\n    }\n\n    const selected = {\n      formatted: j.formatted || (candidates[0]?.formatted) || q,\n      lat: typeof j.lat === 'number' ? j.lat : (candidates[0]?.lat ?? null),\n      lng: typeof j.lng === 'number' ? j.lng : (candidates[0]?.lng ?? null),\n      placeId: j.placeId || candidates[0]?.placeId || '',\n      cityDetected: j.cityDetected || candidates[0]?.cityDetected || ''\n    };\n    _storeAddressSelection_(inputEl, idx, selected);\n  } catch(e){ \/* silencio *\/ }\n}\n\/* ======================= Enlazador de inputs ========================== *\/\nfunction _bindLookup_(inputSel, idx){\n  const el = document.querySelector(inputSel);\n  if (!el) return;\n  let t;\n  el.addEventListener('input', () => {\n    clearTimeout(t);\n    t = setTimeout(() => _lookupYAutocompleta_(el, idx), 700); \/\/ como en el antiguo\n  });\n  el.addEventListener('blur', () => _lookupYAutocompleta_(el, idx));\n}\n\/* Old _bindLookup_ calls removed - now handled by initUnifiedPlacesForAllServices() *\/\n<\/script>\n<!-- === PATCH END ========================================================= -->\n<script>\n\/* === COTIZADOR (usa TARIFAS; backend opcional) === *\/\n(function(){\n  \/\/ ========== 1) Modo debug y helpers de n\u00famero\/moneda ==========\n  window.DEBUG_COTIZADOR = window.DEBUG_COTIZADOR ?? false;\n  const logC = (...a) => window.DEBUG_COTIZADOR && console.log('[COTIZADOR]', ...a);\n\n  \/\/ Normaliza \"35,7\" \u2192 35.7 y ignora strings vac\u00edos\n  const toNumberEU = (v) => {\n    if (v == null) return 0;\n    if (typeof v === 'number') return isFinite(v) ? v : 0;\n    const s = String(v).trim().replace(\/\\.\/g, '').replace(',', '.');\n    const n = parseFloat(s);\n    return isFinite(n) ? n : 0;\n  };\n\n  const formatEUR = (n) =>\n    (new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'EUR' })).format(toNumberEU(n) || 0);\n\n  \/\/ ========== 2) Utilidades DOM seguras ==========\n  const $ = (sel, root=document) => root.querySelector(sel);\n  const $$ = (sel, root=document) => Array.from(root.querySelectorAll(sel));\n  const text = (el, v) => { if (!el) return; el.textContent = v; };\n  const show = (el, on=true) => { if (!el) return; el.hidden = !on; el.style.display = on ? '' : 'none'; };\n\n  \/\/ Reuse global qs\/qsa helpers defined earlier (mantener compatibilidad)\n  const qs = window.qs || $;\n  const qsa = window.qsa || $$;\n  const fmt2 = (v)=> new Intl.NumberFormat('es-ES',{minimumFractionDigits:2, maximumFractionDigits:2}).format(v);\n    window.__precios = { 1:null, 2:null, 3:null }; \/\/ {base, rec_noct, rec_fest, total}\n  function listoCore(n){\n    return [`#s${n}Lugar`,`#s${n}Vehiculo`,`#s${n}Servicio`].every(sel=>{\n      const el=qs(sel); return el && el.value && el.checkValidity();\n    });\n  }\n  function servicioCompleto(n){\n    return [\n      `#s${n}Lugar`,`#s${n}Vehiculo`,`#s${n}Servicio`,\n      `#s${n}Pax`,`#s${n}Date`,`#s${n}Time`,`#s${n}Pickup`,`#s${n}Dropoff`\n    ].every(sel=>{ const el=qs(sel); return el && el.value && el.checkValidity(); });\n  }\n  \/\/ ========== Helper para pintar precio del servicio ==========\n  \/\/ CRITICAL: Price 0 is a VALID price (free service, promotions, etc.). Only null\/undefined\/empty\/NaN means \"no price\".\n  function pintarPrecioServicio(n, value){\n    const out = $(`#s${n}Price`);\n    if (!out) return;\n    const span = out.querySelector('[data-c=\"EUR\"]') || out;\n    \/\/ Show \"\u2014\" only if value is null\/undefined\/empty string\/NaN\n    \/\/ Show \"0,00 \u20ac\" if backend returned 0 (valid price - free service, promotions, etc.)\n    \/\/ Show actual price if backend found a match\n    if (value == null || value === '' || (typeof value === 'number' && isNaN(value))) {\n      span.textContent = '\u2014';\n      out.hidden = true;\n    } else {\n      \/\/ value is a valid number (including 0) - display it\n      span.textContent = formatEUR(value);\n      out.hidden = false;  \/\/ Always show when we have a valid value (including 0)\n    }\n  }\n\n  \/\/ Lectura robusta del precio mostrado (para sumar totales)\n  function leerPrecioServicio(n){\n    const out = $(`#s${n}Price`);\n    if (!out) return 0;\n    const txt = (out.querySelector('[data-c=\"EUR\"]') || out).textContent || '';\n    return toNumberEU(txt);\n  }\n\n  \/\/ NO local pricing - all prices come from backend\n  \/\/ Backend: devuelve el MISMO precio que usa el PDF (con recargos)\n\/\/ Enviamos tambi\u00e9n fecha\/hora y pax para que aplique nocturno\/festivo, etc.\n  async function precioBackend(n, fallback){\n    try{\n      if (!window.WEBAPP_URL) return fallback;\n      \/\/ Use raw values from selects (only .trim() - no normalization)\n      const city   = String(qs(`#s${n}Lugar`)?.value || '').trim();\n      const vehicle = String(qs(`#s${n}Vehiculo`)?.value || '').trim();\n      const service = String(qs(`#s${n}Servicio`)?.value || '').trim();\n      const date   = String(qs(`#s${n}Date`)?.value || '').trim();\n      const time   = String(qs(`#s${n}Time`)?.value || '').trim();\n      \n      \/\/ Always use canonical param names: numCliente (not num, cliente, etc.)\n      const numCliente = String(qs('#cliNum')?.value || '').trim();\n      const taxId      = String(qs('#cliTax')?.value || '').trim();\n\n      \/\/ Get pickup\/dropoff exact addresses if available\n      const pickupExactRaw = String(qs(`#s${n}PickupExact`)?.value || '').trim();\n      const dropoffExactRaw = String(qs(`#s${n}DropoffExact`)?.value || '').trim();\n      const pickupRaw = String(qs(`#s${n}Pickup`)?.value || '').trim();\n      const dropoffRaw = String(qs(`#s${n}Dropoff`)?.value || '').trim();\n\n      \/\/ Construcci\u00f3n final de par\u00e1metros (raw values, canonical names only)\n      const params = new URLSearchParams({\n        action: 'precio',\n        city,\n        vehicle,    \/\/ raw value from select\n        service,    \/\/ raw value from select\n        date,\n        time,\n        numCliente,   \/\/ canonical API param (always sent)\n        taxId,         \/\/ optional\n        pickupAddress: pickupExactRaw || pickupRaw || '',\n        dropoffAddress: dropoffExactRaw || dropoffRaw || ''\n      });\n\n\n      const r = await fetchWithTimeout(`${WEBAPP_URL}?${params.toString()}`, {\n        cache: 'no-store',\n        timeout: 7000\n      });\n      \n      if (!r || !r.ok) return fallback;\n      \n      const j = await r.json().catch(() => null);\n      if (!j || j.ok === false) return fallback;\n\n\/\/ Toma el primer campo de importe disponible (var\u00eda entre deploys de la App)\nconst pick = (obj, ...names) => {\n  for (const k of names) {\n    const v = obj && obj[k];\n    if (v !== undefined && v !== null && v !== '') return v;\n  }\n  return null;\n};\n\n\/\/ Convierte \"3,30\" o \"1.234,56\" a n\u00famero (usa toNumberEU)\n\/\/ CRITICAL: 0 is a VALID price. Only null\/NaN\/undefined means \"no price\".\nconst parseNum = (v) => {\n  const n = toNumberEU(v);\n  return (typeof n === 'number' && !isNaN(n)) ? n : null;\n};\n\nconst rawTotal = pick(j, 'total', 'totalConIva', 'precioFinal');\nconst totalNum = parseNum(rawTotal);\n\nif (totalNum != null) {\n  return {\n    base:     parseNum(pick(j, 'base', 'importeBase')) ?? (fallback?.base ?? 0),\n    rec_noct: parseNum(pick(j, 'rec_noct', 'recNoct')) ?? 0,\n    rec_fest: parseNum(pick(j, 'rec_fest', 'recFest')) ?? 0,\n    total:    totalNum\n  };\n}\nreturn fallback;\n\n    }catch{\n      return fallback;\n    }\n  }\n    \/\/ Main pricing function - ALWAYS calls backend, NO local pricing\n    async function cotizar(n='1'){\n    logC('cotizar:start', n);\n    \n    \/\/ Si falta el contenedor del servicio, no romper\n    const root = $(`#s${n}`);\n    if (!root) { logC('sin root', n); actualizarTotales(); return; }\n\n    \/\/ Validate inputs - if not ready, clear price and return\n    if (!listoCore(n)){\n      pintarPrecioServicio(n, null);\n      window.__precios[n] = null;\n      actualizarTotales();\n      return;\n    }\n\n    \/\/ If this service needs a custom quote (pickup\/dropoff too far),\n    \/\/ do NOT call backend pricing. Just clear the price and exit.\n    \/\/ Only block if we have coordinates AND they are far from the city\n    if (typeof window._bukasCustomQuoteCheck === 'function') {\n      try {\n        const isFar = window._bukasCustomQuoteCheck(n);\n        if (isFar === true) {\n          logC('cotizar: custom-quote only, skipping backend', n);\n          pintarPrecioServicio(n, null);        \/\/ show \"\u2014\" and hide price\n          window.__precios[n] = null;           \/\/ no price -> excluded from totals\n          actualizarTotales();\n          return;\n        }\n      } catch(e) {\n        \/\/ If check fails, allow normal pricing to proceed\n        logC('cotizar: check error, allowing pricing', n, e);\n      }\n    }\n\n    \/\/ Set UI to loading state (show \"\u2014\" while waiting)\n    pintarPrecioServicio(n, null);\n    window.__precios[n] = { base: 0, rec_noct: 0, rec_fest: 0, total: 0 };\n\n    \/\/ ALWAYS call backend - no local fallback\n    \/\/ Backend calculates price from \"Tarifas\" sheet using exact matching: Lugar, Servicio, Veh\u00edculo, Lista\n    try {\n      const det = await precioBackend(n, window.__precios[n]);\n      if (det && typeof det.total === 'number' && !isNaN(det.total)) {\n        \/\/ Backend returned valid price (0 is valid - free service, promotions, etc.)\n        window.__precios[n] = det;\n        \/\/ Show price (0.00 \u20ac is valid, actual price if found)\n        pintarPrecioServicio(n, det.total);\n        logC('precio recibido', n, { total: det.total, base: det.base, lista: det.lista || 'N\/A' });\n      } else {\n        \/\/ Backend returned invalid data - show empty\n        window.__precios[n] = { base: 0, rec_noct: 0, rec_fest: 0, total: 0 };\n        pintarPrecioServicio(n, null);\n        logC('precio inv\u00e1lido', n, det);\n      }\n      actualizarTotales();\n    } catch(e) {\n      logC('precioBackend error', n, e);\n      \/\/ On error, show empty price (no fallback to local pricing)\n      window.__precios[n] = { base: 0, rec_noct: 0, rec_fest: 0, total: 0 };\n      pintarPrecioServicio(n, null);\n      actualizarTotales();\n    }\n  }\n  function leerChipPrecio(n){\n    const txt = qs(`#s${n}Price [data-c=\"EUR\"]`)?.textContent || '';\n    const val = Number((txt.match(\/[\\d.,]+\/)?.[0]||'0').replace(\/\\.\/g,'').replace(',','.'));\n    return Number.isFinite(val) ? val : 0;\n  }\n\/\/ ========== 6) Contador y total infalibles ==========\nfunction servicioActivo(n){\n  const root = $(`#s${n}`) || document;\n  const lugar = $(`#s${n}Lugar`);\n  const veh   = $(`#s${n}Vehiculo`);\n  const serv  = $(`#s${n}Servicio`);\n  const toggleOk = (n==='1') || !!($(`#t${n}`) && $(`#t${n}`).value === 'S\u00ed');\n  const coreOk = [lugar,veh,serv].every(el => el && String(el.value||'').trim() !== '');\n  return toggleOk && coreOk;\n}\n\n\/\/ Update totals using ONLY backend values from window.__precios\n\/\/ CRITICAL: Price 0 is a VALID price. Only null\/undefined\/NaN means \"no price\".\nfunction actualizarTotales(){\n  const activos = ['1','2','3'].filter(servicioActivo);\n  \/\/ Sum ONLY backend totals - no local pricing\n  const total = activos.reduce((acc, n) => {\n    const precio = window.__precios[n];\n    \/\/ Accept 0 as valid price (free service, promotions, etc.)\n    if (precio && typeof precio.total === 'number' && !isNaN(precio.total)) {\n      return acc + precio.total;\n    }\n    return acc;\n  }, 0);\n  const pagoTotal = $('#pagoTotal');             \/\/ existe en tu HTML\n  const pagoCount = $('#pagoServiciosCount span');\n  if (pagoTotal) pagoTotal.textContent = formatEUR(total);        \/\/ \"Precio total:\"\n  if (pagoCount) pagoCount.textContent = String(activos.length);  \/\/ \"Servicios contratados:\"\n  logC('totales', {activos, total});\n}\n\n\n  \/\/ ========== 3) Observadores para chips y autocompletes ==========\n  \/\/ DESACTIVADO: Los chips est\u00e1n ocultos con display:none !important y no emiten eventos visibles\n  \/\/ El wiring directo a selects\/inputs es suficiente\n  \/*\n  function observeChips(n){\n    const wrap = $(`#s${n} .chips`, document) || $(`#s${n} .chips-wrap`, document) || $(`#s${n}`, document);\n    if (!wrap) return;\n    try {\n      const obs = new MutationObserver(() => { \n        logC('chips changed', n); \n        cotizar(String(n)); \n        actualizarTotales(); \n      });\n      obs.observe(wrap, { childList: true, subtree: true, attributes: true, attributeFilter: ['class','data-value'] });\n    } catch(e){ logC('observer error', e); }\n  }\n  ['1','2','3'].forEach(observeChips);\n  *\/\n\n  \/\/ ========== 4) Reenlazar eventos que deben recalcular ==========\n  \/\/ DESACTIVADO: Sistema antiguo que puede causar bucles infinitos\n  \/\/ Se usa el nuevo sistema wireServicio m\u00e1s abajo\n  \/*\n  function onChange(sel, n, ev='change'){\n    $$(sel).forEach(el=>{\n      if (!el) return;\n      el.removeEventListener(ev, el.__cotEv);\n      el.__cotEv = () => { logC('event', n, sel, ev); cotizar(String(n)); actualizarTotales(); };\n      el.addEventListener(ev, el.__cotEv, { passive: true });\n    });\n  }\n\n  function wireServicio(n){\n    \/\/ selects \/ radios \/ checkboxes\n    onChange(`#s${n} select, #s${n} input[type=\"radio\"], #s${n} input[type=\"checkbox\"]`, n, 'change');\n    \/\/ inputs de texto\/fecha\/hora\/autocomplete\/direcciones\/distancia\n    onChange(`#s${n} input[type=\"text\"], #s${n} input[type=\"number\"], #s${n} input[type=\"date\"], #s${n} input[type=\"time\"]`, n, 'input');\n    \/\/ clicks en chips\n    onChange(`#s${n} .chip`, n, 'click');\n  }\n  ['1','2','3'].forEach(wireServicio);\n\n  \/\/ toggles S2\/S3 - deben recalcular todos los servicios\n  ['2','3'].forEach(n=>{\n    ['#t'+n, `#s${n}Toggle`, `#s${n} .toggle input`].forEach(sel => {\n      $$(sel).forEach(el => {\n        if (!el) return;\n        el.removeEventListener('change', el.__cotEvAll);\n        el.__cotEvAll = () => { \n          logC('toggle changed', n, sel); \n          cotizar('1'); \n          cotizar('2'); \n          cotizar('3'); \n          actualizarTotales(); \n        };\n        el.addEventListener('change', el.__cotEvAll, { passive: true });\n      });\n    });\n  });\n  *\/\n\n  \/\/ ========== Gancho cotizar(n) seguro ==========\n  function cotizarSafe(n){\n    try{\n      if (typeof cotizar === 'function') {\n        cotizar(String(n));            \/\/ usa tu cotizar existente\n      }\n    } catch(e){ logC('cotizar error', n, e); }\n    actualizarTotales();\n  }\n\n  \/\/ ========== Wiring de eventos sobre selects\/inputs (no chips) ==========\n  function wireServicio(n){\n    const root = document;\n    \/\/ Selects core\n    ['#s'+n+'Lugar','#s'+n+'Vehiculo','#s'+n+'Servicio'].forEach(sel=>{\n      const el = $(sel, root); if (!el) return;\n      \/\/ Evitar a\u00f1adir listeners duplicados\n      if (el.__cotSafeWired) return;\n      el.addEventListener('change', ()=> cotizarSafe(n), {passive:true});\n      el.__cotSafeWired = true;\n    });\n    \/\/ Extras checkboxes - show\/hide detail fields\n    ['CartelNombre', 'AsientoInfantil', 'ChoferIdioma'].forEach(extra => {\n      const checkbox = $(`#s${n}${extra}`, root);\n      const detailField = $(`#s${n}${extra}Detalle`, root);\n      if (checkbox && detailField) {\n        checkbox.addEventListener('change', function() {\n          detailField.style.display = this.checked ? 'block' : 'none';\n        }, {passive:true});\n        \/\/ Initialize state\n        detailField.style.display = checkbox.checked ? 'block' : 'none';\n      }\n    });\n    \/\/ Fecha\/hora\/pax\/pickup\/dropoff\n    ['#s'+n+'Date','#s'+n+'Time','#s'+n+'Pax','#s'+n+'Pickup','#s'+n+'Dropoff'].forEach(sel=>{\n      const el = $(sel, root); if (!el) return;\n      if (el.__cotSafeWired) return;\n      el.addEventListener('input',  ()=> cotizarSafe(n), {passive:true});\n      el.addEventListener('change', ()=> cotizarSafe(n), {passive:true});\n      el.__cotSafeWired = true;\n    });\n  }\n  ['1','2','3'].forEach(wireServicio);\n\n  \/\/ Toggles S2\/S3 (select \"S\u00ed\/No\")\n  ['2','3'].forEach(n=>{\n    const t = $('#t'+n); if (!t) return;\n    if (t.__cotSafeWired) return;\n    t.addEventListener('change', ()=>{\n      \/\/ recalcula los tres para mantener totales coherentes\n      ['1','2','3'].forEach(cotizarSafe);\n    }, {passive:true});\n    t.__cotSafeWired = true;\n  });\n\n  \/\/ Exponer funciones globalmente\n  window.cotizar = cotizar;\n  window.actualizarTotales = actualizarTotales;\n  window.cotizarSafe = cotizarSafe;\n\n  \/\/ ========== 7) Inicializaci\u00f3n segura ==========\n  \/\/ Evitar m\u00faltiples inicializaciones\n  if (window.__cotizadorInicializado) {\n    logC('ya inicializado, saltando');\n  } else {\n    window.__cotizadorInicializado = true;\n    \n    function initCotizador(){\n      logC('DOMContentLoaded');\n      \/\/ Primera pasada\n      ['1','2','3'].forEach(cotizarSafe);\n      \/\/ Reintentos por si Autocomplete escribe distancia con retraso\n      setTimeout(()=>['1','2','3'].forEach(cotizarSafe), 150);\n      setTimeout(()=>['1','2','3'].forEach(cotizarSafe), 600);\n    }\n\n    \/\/ Inicializar cuando el DOM est\u00e9 listo\n    if (document.readyState === 'loading') {\n      document.addEventListener('DOMContentLoaded', initCotizador);\n    } else {\n      initCotizador();\n    }\n  }\n\n  \/\/ ========== 8) Manejo de errores global ==========\n  if (!window.__cotizadorErrorHandlers) {\n    window.__cotizadorErrorHandlers = true;\n    window.addEventListener('error', e => {\n      if (e.message && (e.message.includes('cotizar') || e.message.includes('COTIZADOR'))) {\n        console.error('[COTIZADOR][window.error]', e.message, e.filename, e.lineno);\n      }\n    });\n    window.addEventListener('unhandledrejection', e => {\n      if (e.reason && (String(e.reason).includes('cotizar') || String(e.reason).includes('COTIZADOR'))) {\n        console.error('[COTIZADOR][unhandled]', e.reason);\n      }\n    });\n  }\n})();\n<\/script>\n<script>\n(function(){\n  const select  = document.getElementById('pagoMetodo');\n  const btns    = Array.from(document.querySelectorAll('.pago-btn'));\n  const bankBox = document.getElementById('bankBox');\n  const cardInfo = document.getElementById('cardInfo');\n  const form    = document.querySelector('form');\n  function setPago(value){\n    \/\/ Sincroniza el <select> oculto\n    select.value = value;\n    \/\/ Marca visualmente el bot\u00f3n activo\n    btns.forEach(b=>{\n      const on = b.dataset.value === value;\n      b.classList.toggle('active', on);\n      b.setAttribute('aria-checked', on ? 'true':'false');\n    });\n    \/\/ Muestra\/oculta datos bancarios\n    const showBank = \/transferencia|cargo a cuenta\/i.test(value);\n    if (bankBox) bankBox.hidden = !showBank;\n\n    const isCard = \/tarjeta\/i.test(value);\n    if (cardInfo) cardInfo.hidden = !isCard;\n  }\n  \/\/ Click de los botones\n  btns.forEach(b=> b.addEventListener('click', ()=> setPago(b.dataset.value)));\n  \/\/ Valor por defecto para evitar que el navegador enfoque el <select> oculto\n  if(!select.value) setPago('Transferencia bancaria'); else setPago(select.value);\n  \/\/ Por si acaso, fijar valor antes de enviar\n  if(form){\n    form.addEventListener('submit', ()=>{\n      if(!select.value) setPago('Transferencia bancaria');\n    });\n  }\n})();\n<\/script>\n\n<script type=\"module\" id=\"ux-js\">\n\n\n\nfunction pagoAccesible(){\n  const real = qs('#pagoMetodo'); if(!real) return;\n  const box = qs('#bankBox');\n  const cardInfo = qs('#cardInfo');\n  const btns = qsa('.pago-btn');\n  const set = (val)=>{\n    real.value = val;\n    btns.forEach(b=>{\n      const on = b.dataset.value===val;\n      b.classList.toggle('active', on);\n      b.setAttribute('aria-checked', on?'true':'false');\n    });\n    const showBank = (val==='Transferencia bancaria' || val==='Cargo a cuenta del cliente');\n    if (box) box.hidden = !showBank;\n    const isCard = (val==='Tarjeta de cr\u00e9dito');\n    if (cardInfo) cardInfo.hidden = !isCard;\n  };\n\/\/ Click\/teclado\nbtns.forEach(b=>{\nb.addEventListener('click', ()=> set(b.dataset.value));\nb.addEventListener('keydown', (e)=>{ if(e.key==='Enter' || e.key===' '){ e.preventDefault(); set(b.dataset.value); }});\n});\n\/\/ Inicializa con el valor actual\nset(real.value || 'Transferencia bancaria');\n}\n\nfunction totales(){\n  if (typeof window.actualizarTotales === 'function'){\n    window.actualizarTotales();\n    return;\n  }\n}\n\n\nfunction bindTotals(){\n  ['#t2','#t3'].forEach(sel => {\n    const el = qs(sel);\n    if (el) el.addEventListener('change', totales);\n  });\n  totales();\n  if (typeof window.actualizarTotales !== 'function'){\n    window.actualizarTotales = totales;\n  }\n}\n\n\nfunction init(){\nconst form = qs('#bukasForm'); if(!form) return;\n\/\/ Optional: autosaveBind and friendlyValidation if they exist\nif (typeof autosaveBind === 'function') autosaveBind(form);\nif (typeof friendlyValidation === 'function') friendlyValidation(form);\npagoAccesible();\nbindTotals();\n\/\/ Si el stepper expone refresh, sincroniza para desbloquear siguiente bot\u00f3n\ntry{ window.__bukasStepper?.refreshNext?.(); }catch(_){}\n}\n\n\n\/\/ Espera DOM listo (ya est\u00e1s en type=module)\nif(document.readyState==='loading') document.addEventListener('DOMContentLoaded', init); else init();\n\n\/\/ Expone namespace por si hace falta apagar algo\nwindow.BukasUX = { restore:true };\n<\/script>\n<script>\n(function(){\n  function ensureReservaId(){\n    let id = sessionStorage.getItem('reservaId');\n    if(!id){\n      \/\/ AAAAMMDDhhmmss (estable durante la sesi\u00f3n)\n      const d = new Date();\n      id = d.getFullYear().toString().padStart(4,'0')\n        + String(d.getMonth()+1).padStart(2,'0')\n        + String(d.getDate()).padStart(2,'0')\n        + String(d.getHours()).padStart(2,'0')\n        + String(d.getMinutes()).padStart(2,'0')\n        + String(d.getSeconds()).padStart(2,'0');\n      sessionStorage.setItem('reservaId', id);\n    }\n    \/\/ pinta\n    const span = document.getElementById('bankResNum');\n    if(span) span.textContent = id;\n    \/\/ si existe el input oculto, actual\u00edzalo\n    const hid = document.getElementById('resNum');\n    if(hid) hid.value = id;\n    return id;\n  }\n\n  function updateBankConcept(){\n    \/\/ N\u00ba de cliente\n    const cli = (document.getElementById('cliNum')?.value || '').trim();\n    const cliSpan = document.getElementById('bankCliNum');\n    if(cliSpan) cliSpan.textContent = cli || '___';\n    \/\/ N\u00ba de reserva\n    ensureReservaId();\n  }\n\n  document.addEventListener('DOMContentLoaded', function(){\n    \/\/ inicial\n    updateBankConcept();\n\n    \/\/ cuando cambie el N\u00ba de cliente\n    ['input','change','blur'].forEach(ev=>{\n      document.getElementById('cliNum')?.addEventListener(ev, updateBankConcept);\n    });\n\n    \/\/ y cada vez que navegues entre pasos\n    document.getElementById('bukasForm')?.addEventListener('click', (e)=>{\n      if(e.target.closest('[data-next],[data-prev]')) setTimeout(updateBankConcept,0);\n    });\n\n    \/\/ por si el autocompletado de cliente te rellena #cliNum m\u00e1s tarde\n    document.addEventListener('bukas:cliente-autocompletado', updateBankConcept);\n  });\n})();\n<\/script>\n\n<script>\n(function () {\n  function removeBtn() {\n    var b = document.getElementById('floating-button');\n    if (b) b.remove();\n    var m = document.getElementById('custom-modal');\n    if (m) m.remove();\n  }\n  \/\/ eliminalo ahora\u2026\n  removeBtn();\n  \/\/ \u2026y tambi\u00e9n si alg\u00fan script lo reinyecta\n  new MutationObserver(removeBtn).observe(document.body, { childList: true, subtree: true });\n})();\n<\/script>\n\n<script id=\"ux-auto-advance-patch\">\n\/* ===== UX PATCH (no rompe nada existente) =====\n   - Auto-avanza S1\u2192S2 al elegir \"S\u00ed\" en #t2\n   - Auto-avanza S2\u2192S3 al elegir \"S\u00ed\" en #t3\n   - Inicializa chips\/hints del servicio destino si a\u00fan no estaban\n   - Renombra bot\u00f3n del paso 4 a \"Elegir forma de pago y enviar\"\n   - Valida como har\u00eda \"Siguiente\"; si falta algo, muestra aviso suave\n*\/\n(function(){\n\n  return; \/\/ \u2b05\ufe0f desactiva el parche de auto-avance\n  \n  const doc = document;\n  const blocks = Array.from(doc.querySelectorAll('.form-block'));\n\n  \/\/ Helper: visibilidad real\n  const _vis = el => {\n    if (!el || el.disabled) return false;\n    let n = el;\n    while (n && n !== document.body){\n      const cs = getComputedStyle(n);\n      if (cs.display === 'none' || cs.visibility === 'hidden') return false;\n      n = n.parentElement;\n    }\n    return true;\n  };\n  const _reqIn = blk => Array.from(blk.querySelectorAll('input,select,textarea')).filter(el => _vis(el) && el.required);\n  const _idx = () => blocks.findIndex(b => b.dataset.active === '1');\n\n  \/\/ Usa tu stepper si existe; si no, fallback manual\n  const goTo = (idx) => {\n    if (window.__bukasStepper?.setActive) {\n      window.__bukasStepper.setActive(idx);\n    } else {\n      const steps = Array.from(doc.querySelectorAll('.stepper__item'));\n      blocks.forEach((b,i)=>{ b.dataset.active = (i===idx?'1':'0'); b.hidden = i!==idx; });\n      steps.forEach((s,i)=> s.dataset.state = i<idx?'done':(i===idx?'active':'idle'));\n    }\n    \/\/ foco\n    const first = blocks[idx]?.querySelector('input,select,textarea');\n    first?.focus?.();\n  };\n\n  \/\/ Validaci\u00f3n como el bot\u00f3n Siguiente\n  const canAdvance = (idx) => {\n    const blk = blocks[idx]; if (!blk) return false;\n    \/\/ Respeta tu excepci\u00f3n actual: si S2\/S3 no est\u00e1 habilitado, no bloquea\n    if (blk.dataset.block==='3' && (doc.querySelector('#t2')?.value!=='S\u00ed' || doc.querySelector('#serv2')?.hidden)) return true;\n    if (blk.dataset.block==='4' && (doc.querySelector('#t3')?.value!=='S\u00ed' || doc.querySelector('#serv3')?.hidden)) return true;\n    return _reqIn(blk).every(el => el.checkValidity());\n  };\n\n  const softWarn = (selectEl, msg) => {\n    const id = selectEl.id + '-warn';\n    let hint = doc.getElementById(id);\n    if (!hint) {\n      hint = doc.createElement('small');\n      hint.id = id;\n      hint.className = 'hint';\n      selectEl.insertAdjacentElement('afterend', hint);\n    }\n    hint.textContent = msg;\n    clearTimeout(hint.__t);\n    hint.__t = setTimeout(()=> hint.textContent = '', 4000);\n  };\n\n  \/\/ Inicializa servicio destino si hay utilidades disponibles; si no, repinta chips b\u00e1sica\n  const ensureInitServicio = (n) => {\n    const root = doc.getElementById('serv'+n);\n    if (!root) return;\n    try {\n      if (!root.__init && typeof window.initServicio === 'function') {\n        root.hidden = false;\n        root.dataset.open = '1';\n        window.initServicio(n);\n        root.__init = true;\n      } else if (!root.__init) {\n        \/\/ Fallback: al menos renderizar chips de selects core\n        ['Lugar','Vehiculo','Servicio'].forEach(k => {\n          const sel = doc.getElementById(`s${n}${k}`);\n          if (sel && typeof window.renderChipsForSelect === 'function') window.renderChipsForSelect(sel);\n        });\n        root.__init = true;\n      }\n    } catch(_) {}\n  };\n\n  const announce = (msg) => {\n    let live = doc.getElementById('ux-live');\n    if (!live) {\n      live = doc.createElement('div');\n      live.id = 'ux-live';\n      live.setAttribute('aria-live','polite');\n      Object.assign(live.style, {position:'absolute',width:'1px',height:'1px',overflow:'hidden',clip:'rect(1px,1px,1px,1px)'});\n      doc.body.appendChild(live);\n    }\n    live.textContent = msg;\n  };\n\n  \/\/ Auto-avance gen\u00e9rico (toggleId = t2\/t3)\n  const bindAutoAdvance = (toggleId, targetIndex, servicioN) => {\n    const sel = doc.getElementById(toggleId);\n    if (!sel) return;\n\n    const maybeAdvance = () => {\n      if ((sel.value || '').trim() !== 'S\u00ed') return;\n      const i = _idx();\n      if (canAdvance(i)) {\n        ensureInitServicio(servicioN);\n        goTo(targetIndex);\n        announce(`Servicio ${servicioN} habilitado`);\n      } else {\n        softWarn(sel, t('hint.stepRequired'));\n      }\n    };\n\n    sel.addEventListener('change', maybeAdvance);\n\n    \/\/ Si usas el toggle accesible .yn-toggle sincronizado con el <select>, escucha tambi\u00e9n\n    const yn = sel.parentElement?.querySelector('.yn-toggle');\n    yn?.addEventListener('click', () => {\n      if ((sel.value || '').trim() === 'S\u00ed') maybeAdvance();\n    });\n  };\n\n  \/\/ S1\u2192S2 (t2)   \u00edndice destino 2 (bloque data-block=\"3\")\n  bindAutoAdvance('t2', 2, 2);\n  \/\/ S2\u2192S3 (t3)   \u00edndice destino 3 (bloque data-block=\"4\")\n  bindAutoAdvance('t3', 3, 3);\n\n\/\/ NUEVO: si en Servicio 2 decides NO agregar Servicio 3,\n\/\/ y el bloque de S2 est\u00e1 completo, salta directamente a la pantalla final (Pago\/Enviar).\nconst t3Sel = doc.getElementById('t3');\nt3Sel?.addEventListener('change', () => {\n  if ((t3Sel.value || '').trim() !== 'No') return;\n  const i = _idx(); \/\/ \u00edndice del bloque activo\n  if (i === 2 && canAdvance(i)) { \/\/ est\u00e1s en Servicio 2 y est\u00e1 v\u00e1lido\n    \/\/ por coherencia visual, aseg\u00farate de ocultar S3\n    const s3Root = doc.getElementById('serv3');\n    if (s3Root) { s3Root.hidden = true; s3Root.dataset.open = '0'; }\n    goTo(4); \/\/ ir a \"Final\"\n    announce('Servicio 3 desactivado: avanzamos a pago y env\u00edo');\n  }\n});\n\n\n  \n\n  \/\/ Microcopy paso 4\n  const btnNextS3 = doc.querySelector('.form-block[data-block=\"4\"] [data-next]');\n  if (btnNextS3) {\n    btnNextS3.textContent = 'Elegir forma de pago y enviar';\n    btnNextS3.setAttribute('aria-label','Elegir forma de pago y enviar');\n  }\n})();\n<\/script>\n<script>\n\/* Aviso de extras + sincronizaci\u00f3n con Comentarios\/Email *\/\n(function(){\n  \/\/ Texto traducido del aviso\nfunction extraCostMsg(){\n  \/\/ Usamos el idioma guardado por el selector (ES, EN, CA, FR)\n  const lang = localStorage.getItem('bukas_lang') || 'es';\n\n  const msgs = {\n    es: 'Ciertos servicios podr\u00edan suponer un coste adicional.',\n    en: 'Some services may entail an additional cost.',\n    ca: 'Alguns serveis poden comportar un cost addicional.',\n    fr: 'Certains services peuvent entra\u00eener un co\u00fbt suppl\u00e9mentaire.'\n  };\n\n  return msgs[lang] || msgs.es;\n}\n\n\n  \/\/ Quita del texto cualquier l\u00ednea autom\u00e1tica de este aviso (en varios idiomas)\n  function removeExtraCostLine(txt){\n    return (txt || '').replace(\n      \/\\n?\u2014 .*(coste adicional|additional cost|cost addicional|co\u00fbt suppl\u00e9mentaire).*$\/i,\n      ''\n    );\n  }\n\n  \/\/ Returns true ONLY if any \"Other (specify)\" field has text\n  function hayExtrasOOtro(){\n    return [1,2,3].some(function(idx){\n      const otro = document.getElementById('s' + idx + 'Otro');\n      const hasOther = !!(otro && otro.value.trim());\n      return hasOther;\n    });\n  }\n\n  \/\/ Sincroniza una \u00fanica l\u00ednea de aviso en el textarea de comentarios\n  function syncExtrasToComentarios(){\n    const ta =\n      document.querySelector('#comentarios textarea[name=\"Comentarios u observaciones\"]')\n      || document.querySelector('textarea[name=\"Comentarios u observaciones\"]');\n    if (!ta) return;\n\n    let txt = ta.value || '';\n    txt = removeExtraCostLine(txt);  \/\/ limpiamos restos anteriores\n\n    if (hayExtrasOOtro()){\n      const msg = extraCostMsg();\n      if (msg){\n        if (txt && !txt.endsWith('\\n')) txt += '\\n';\n        txt += '\u2014 ' + msg;\n      }\n    }\n\n    ta.value = txt;\n  }\n\n  \/\/ La exponemos para poder llamarla justo antes de enviar el formulario\n  window._bukasSyncExtrasToComentarios = syncExtrasToComentarios;\n\n  function setupExtrasWarning(idx){\n    const wrap = document.getElementById('s' + idx + 'ExtrasWrap');\n    if (!wrap) return;\n\n    let aviso = document.getElementById('extrasAviso' + idx);\n    if (!aviso){\n      aviso = document.createElement('div');\n      aviso.id = 'extrasAviso' + idx;\n      aviso.className = 'aviso-otros span-12';\n      aviso.hidden = true;\n\n      aviso.dataset.i18n = 'hint.extraCost';\n      aviso.textContent = extraCostMsg();\n\n      wrap.insertAdjacentElement('afterend', aviso);\n    }\n\n    const otro = document.getElementById('s' + idx + 'Otro');\n\n    const update = () => {\n      const hasOther = !!(otro && otro.value.trim());\n      const show     = hasOther;\n\n      aviso.hidden = !show;\n      \/\/ Still do NOT touch the textarea here; Comments will be updated\n      \/\/ only via syncExtrasToComentarios() on submit or language change.\n    };\n\n    wrap.addEventListener('change', update);\n    if (otro){\n      ['input','change','blur'].forEach(function(ev){\n        otro.addEventListener(ev, update);\n      });\n    }\n\n    update();\n  }\n\n  document.addEventListener('DOMContentLoaded', function(){\n    [1,2,3].forEach(setupExtrasWarning);\n\n    \/\/ Si cambias el idioma, re-traduce los avisos visuales\n    \/\/ y tambi\u00e9n la l\u00ednea autom\u00e1tica en el textarea de Comentarios\n    const langButtons = document.querySelectorAll('.lang-switcher [data-lang]');\n    langButtons.forEach(function(btn){\n      btn.addEventListener('click', function(){\n        setTimeout(function(){\n          \/\/ 1) Actualizar los avisos amarillos de extras\n          [1,2,3].forEach(function(idx){\n            const aviso = document.getElementById('extrasAviso' + idx);\n            if (aviso) aviso.textContent = extraCostMsg();\n          });\n\n          \/\/ 2) Regenerar la l\u00ednea de \"coste adicional\" en Comentarios\n          if (window._bukasSyncExtrasToComentarios) {\n            window._bukasSyncExtrasToComentarios();\n          }\n        }, 100);\n      });\n    });\n  });\n})();\n<\/script>\n\n\n\n<script>\n\/* Aviso de \u201ccotizaci\u00f3n personalizada\u201d si alg\u00fan punto est\u00e1 lejos de la ciudad *\/\n\n(function(){\n  const CITY_COORDS = {\n    Barcelona: { lat: 41.3874, lng: 2.1686 },\n    Madrid:    { lat: 40.4168, lng: -3.7038 }\n  };\n\n  function haversineKm(lat1, lon1, lat2, lon2){\n    const toRad = d => d * Math.PI \/ 180;\n    const R = 6371; \/\/ km\n    const dLat = toRad(lat2 - lat1);\n    const dLon = toRad(lon2 - lon1);\n    const a = Math.sin(dLat\/2)**2 +\n              Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *\n              Math.sin(dLon\/2)**2;\n    return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));\n  }\n\n  function ensureWarn(idx){\n    const distInput = document.getElementById('s' + idx + 'Distance');\n    if (!distInput) return null;\n\n    let aviso = document.getElementById('distWarn' + idx);\n    if (!aviso){\n      aviso = document.createElement('div');\n      aviso.id = 'distWarn' + idx;\n      aviso.className = 'aviso-otros span-12';\n      aviso.hidden = true;\n\n      \n      \/\/ i18n: usamos t() SOLO si existe; si no, texto fijo en castellano\n      aviso.dataset.i18n = 'hint.customQuote';\n      const msg = (typeof t === 'function')\n        ? t('hint.customQuote')\n        : 'Por la ubicaci\u00f3n de los puntos de origen o destino, este servicio podr\u00eda necesitar una cotizaci\u00f3n personalizada.';\n      aviso.textContent = msg;\n\n      const label = distInput.closest('label') || distInput.parentElement;\n      label.insertAdjacentElement('afterend', aviso);\n    }\n    return aviso;\n  }\n\n \/\/ Sincroniza el aviso de cotizaci\u00f3n personalizada con el textarea de Comentarios\n\/\/ \u26a0\ufe0f NUEVA VERSI\u00d3N: solo LIMPIA, no a\u00f1ade nada\nfunction syncCustomQuoteToComentarios(){\n  const ta = document.querySelector('#comentarios textarea[name=\"Comentarios u observaciones\"]')\n          || document.querySelector('textarea[name=\"Comentarios u observaciones\"]');\n  if (!ta) return;\n\n  let txt = ta.value || '';\n\n  \/\/ Borra cualquier l\u00ednea autom\u00e1tica de \u201ccotizaci\u00f3n personalizada\u201d (cualquier idioma)\n  txt = txt.replace(\n    \/\\n?\u2014 .*(cotizaci\u00f3n personalizada|custom quote|pressupost personalitzat|devis personnalis\u00e9).*$\/i,\n    ''\n  );\n\n  \/\/ Por si ha quedado una l\u00ednea suelta con solo \"\u2014\"\n  txt = txt.replace(\/\\n?\u2014\\s*$\/,'');\n\n  ta.value = txt;\n}\n\n\n  function check(idx){\n    const lugar = (document.getElementById('s' + idx + 'Lugar')?.value || '').trim();\n    const city  = CITY_COORDS[lugar];\n    const aviso = ensureWarn(idx);\n    if (!aviso || !city){\n      if (aviso) aviso.hidden = true;\n      \/\/ clear any old note in comments\n      syncCustomQuoteToComentarios();\n      return false;\n    }\n\n    const toNum = v => {\n      const n = Number(v);\n      return Number.isFinite(n) ? n : null;\n    };\n\n    const kinds = ['Pickup', 'Dropoff'];\n    let far = false;\n    let hasAnyCoords = false;\n\n    kinds.forEach(kind => {\n      const vis    = document.getElementById(`s${idx}${kind}`);\n      const latHid = document.getElementById(`s${idx}${kind}Lat`);\n      const lngHid = document.getElementById(`s${idx}${kind}Lng`);\n\n      \/\/ 1) Try visible input dataset first\n      let lat = null;\n      let lng = null;\n      \n      if (vis) {\n        if (vis.dataset && vis.dataset.lat) lat = toNum(vis.dataset.lat);\n        if (vis.dataset && vis.dataset.lng) lng = toNum(vis.dataset.lng);\n      }\n\n      \/\/ 2) Fallback to hidden inputs if dataset is missing\n      if ((lat == null || lng == null) && latHid && lngHid) {\n        const latVal = latHid.value;\n        const lngVal = lngHid.value;\n        \/\/ Try to parse values - handle both string and number formats\n        if (latVal && String(latVal).trim()) {\n          const parsed = toNum(String(latVal).trim());\n          if (parsed != null) lat = parsed;\n        }\n        if (lngVal && String(lngVal).trim()) {\n          const parsed = toNum(String(lngVal).trim());\n          if (parsed != null) lng = parsed;\n        }\n      }\n\n      \/\/ If we have coords for this point, check distance\n      if (lat != null && lng != null) {\n        hasAnyCoords = true;\n        const km = haversineKm(city.lat, city.lng, lat, lng);\n        \/\/ Usamos el valor global, con 50 km como respaldo por si no estuviera definido\n        const threshold = (typeof window.MAX_DISTANCE_KM === 'number') ? window.MAX_DISTANCE_KM : 50;\n        if (km > threshold) {\n          far = true;\n        }\n      }\n    });\n\n    \/\/ Only return true if we have coordinates AND at least one point is far\n    \/\/ If no coordinates available, return false to allow normal pricing\n    const shouldBlock = hasAnyCoords && far;\n\n    aviso.hidden = !shouldBlock;\n    \/\/ keep comments textarea cleaned\/synced as before\n    syncCustomQuoteToComentarios();\n    return shouldBlock;\n  }\n\n\n\/\/ \u2b07\u2b07 ESTA ES LA L\u00cdNEA NUEVA\n  window._bukasCustomQuoteCheck = check;\n\n\n  function bind(idx){\n    ['Lugar','Pickup','Dropoff'].forEach(field => {\n      const el = document.getElementById('s' + idx + field);\n      if (!el) return;\n      ['change','input','blur'].forEach(ev =>\n        el.addEventListener(ev, () => check(idx))\n      );\n    });\n    \n    \/\/ Also listen to hidden lat\/lng fields in case they're set programmatically\n    ['PickupLat','PickupLng','DropoffLat','DropoffLng'].forEach(field => {\n      const el = document.getElementById('s' + idx + field);\n      if (!el) return;\n      ['change','input'].forEach(ev =>\n        el.addEventListener(ev, () => check(idx))\n      );\n    });\n    \n    \/\/ Also check when distance changes (indicates coords are available)\n    const distEl = document.getElementById('s' + idx + 'Distance');\n    if (distEl) {\n      ['change','input'].forEach(ev =>\n        distEl.addEventListener(ev, () => check(idx))\n      );\n    }\n  }\n\n  document.addEventListener('DOMContentLoaded', function(){\n    [1,2,3].forEach(function(i){\n      bind(i);\n    });\n    \/\/ Check immediately\n    [1,2,3].forEach(function(i){\n      check(i);\n    });\n    \/\/ Delay checks to ensure hidden fields are populated if data loads asynchronously\n    setTimeout(() => {\n      [1,2,3].forEach(function(i){\n        check(i);\n      });\n    }, 100);\n    setTimeout(() => {\n      [1,2,3].forEach(function(i){\n        check(i);\n      });\n      syncCustomQuoteToComentarios();\n    }, 500);\n    \/\/ Final check after everything should be loaded\n    setTimeout(() => {\n      [1,2,3].forEach(function(i){\n        check(i);\n      });\n    }, 1000);\n  });\n})();\n\n\n\/* === Mantiene sincronizada la traducci\u00f3n del aviso en los Comentarios === *\/\n(function(){\n  \/\/ Devuelve el texto traducido seg\u00fan el idioma activo\n  function customQuoteMsg(){\n    if (typeof t === 'function') return t('hint.customQuote');\n    return 'Por la ubicaci\u00f3n de los puntos de origen o destino, este servicio podr\u00eda necesitar una cotizaci\u00f3n personalizada.';\n  }\n\n\/\/ Mantiene limpio el textarea si cambias de idioma\n\/\/ \u26a0\ufe0f NUEVA VERSI\u00d3N: solo limpia, no a\u00f1ade el mensaje\nfunction refreshCustomQuoteInTextarea(){\n  const ta = document.querySelector('#comentarios textarea[name=\"Comentarios u observaciones\"]')\n        || document.querySelector('textarea[name=\"Comentarios u observaciones\"]');\n  if (!ta) return;\n\n  let txt = ta.value || '';\n\n  \/\/ Borra cualquier l\u00ednea autom\u00e1tica de \u201ccotizaci\u00f3n personalizada\u201d (cualquier idioma)\n  txt = txt.replace(\n    \/\\n?\u2014 .*(cotizaci\u00f3n personalizada|custom quote|pressupost personalitzat|devis personnalis\u00e9).*$\/i,\n    ''\n  );\n\n  \/\/ Y por si queda un \"\u2014\" suelto\n  txt = txt.replace(\/\\n?\u2014\\s*$\/,'');\n\n  ta.value = txt;\n}\n\n\n    \/\/ Escucha cambio de idioma\n  document.addEventListener('DOMContentLoaded', () => {\n    \/\/ Tus botones reales est\u00e1n dentro de .lang-switcher y usan data-lang\n    const langButtons = document.querySelectorAll('.lang-switcher [data-lang]');\n    langButtons.forEach(btn => {\n      btn.addEventListener('click', () => {\n        \/\/ Recalcula el texto del aviso en el textarea seg\u00fan el idioma actual\n        setTimeout(refreshCustomQuoteInTextarea, 100);\n      });\n    });\n  });\n})();\n\n\n<\/script>\n\n<script src=\".\/20251110_Funciona_App.js\"><\/script>\n\n<script>\n(function(){\n  \/\/ Reuse visibility rule\n  function isVisible(el){ return el && !el.closest('[hidden]'); }\n  \n  function isEmpty(el){\n    const v = (el.value ?? '').trim();\n    if (el.tagName === 'SELECT') return v === '' || v === '\u2014';\n    return v === '';\n  }\n\n  \/\/ Where to attach the helper:\n  \/\/ 1) If input is inside a <label> or a wrapper with class .form-control, append there.\n  \/\/ 2) Else append to input.parentElement.\n  function hostFor(el){\n    return el.closest('label, .form-control') || el.parentElement || null;\n  }\n\n  function ensureHelper(el){\n    const host = hostFor(el);\n    if (!host) {\n      console.warn('[req-helper] No host found for', el.id || el.name);\n      return;\n    }\n    if (!host.classList.contains('has-field-error')) host.classList.add('has-field-error');\n\n    \/\/ Use data attribute to distinguish our helper from other field-error elements\n    let hint = host.querySelector('.field-error[data-required-helper]');\n    if (!hint){\n      hint = document.createElement('small');\n      hint.className = 'field-error';\n      hint.setAttribute('data-required-helper', 'true');\n      \/\/ Access t() from global scope (it's defined outside this IIFE)\n      const getRequiredMsg = () => {\n        if (typeof window.t === 'function') return window.t('required.msg');\n        if (typeof t === 'function') return t('required.msg');\n        return 'Campo obligatorio';\n      };\n      hint.textContent = getRequiredMsg();\n      \/\/ Insert after the input\/select\/textarea, not at the end\n      if (el.nextSibling) {\n        host.insertBefore(hint, el.nextSibling);\n      } else {\n        host.appendChild(hint);\n      }\n      \/\/ Accessibility: tie helper to control\n      const id = el.id || (el.name ? 'fld_'+el.name : '');\n      if (id) {\n        if (!el.id) el.id = id;\n        const prev = (el.getAttribute('aria-describedby') || '').trim();\n        el.setAttribute('aria-describedby', (prev ? prev+' ' : '') + id + '_err');\n        hint.id = id + '_err';\n      }\n    } else {\n      \/\/ Ensure text is correct (update on language change)\n      const getRequiredMsg = () => {\n        if (typeof window.t === 'function') return window.t('required.msg');\n        if (typeof t === 'function') return t('required.msg');\n        return 'Campo obligatorio';\n      };\n      const desired = getRequiredMsg();\n      if (hint.textContent !== desired) {\n        hint.textContent = desired;\n      }\n    }\n  }\n\n  function removeHelper(el){\n    const host = hostFor(el);\n    if (!host) return;\n    \/\/ Only remove our specific helper, not other field-error elements\n    const hint = host.querySelector(':scope > .field-error[data-required-helper]');\n    if (hint) hint.remove();\n    \/\/ Only remove has-field-error if no other field-error exists\n    if (!host.querySelector(':scope > .field-error')) {\n      host.classList.remove('has-field-error');\n    }\n    \/\/ (Keep aria-describedby; it's fine if present)\n  }\n\n  function refreshOne(el){\n    try{\n      if (!isVisible(el)) { \n        el.classList.remove('req-empty'); \n        removeHelper(el); \n        return; \n      }\n      if (isEmpty(el)){\n        el.classList.add('req-empty');\n        ensureHelper(el);\n      } else {\n        el.classList.remove('req-empty');\n        removeHelper(el);\n      }\n    }catch(e){\n      console.warn('[req-helper] refreshOne error:', e, el);\n    }\n  }\n\n  function requiredControls(root){\n    return Array.from(root.querySelectorAll('input[required], select[required], textarea[required]'));\n  }\n  function refreshAll(container){ requiredControls(container).forEach(refreshOne); }\n  \n  \/\/ Expose refreshAll globally so language switcher can call it\n  window.refreshRequiredHelpers = refreshAll;\n\n  \/\/ Non-intrusive listeners; do NOT touch Places handlers\n  function wire(container){\n    \/\/ Initial pass - wait a bit to ensure DOM is ready\n    setTimeout(() => refreshAll(container), 100);\n    \n    container.addEventListener('input',  ev => {\n      const target = ev.target;\n      if (target && target.matches('input[required],select[required],textarea[required]')) {\n        setTimeout(() => refreshOne(target), 0);\n      }\n    }, { passive:true });\n\n    container.addEventListener('change', ev => {\n      const target = ev.target;\n      if (target && target.matches('input[required],select[required],textarea[required]')) {\n        setTimeout(() => refreshOne(target), 0);\n      }\n    }, { passive:true });\n\n    \/\/ After step nav, re-check visible fields\n    container.addEventListener('click', ev => {\n      const isNav = ev.target && (ev.target.closest('.btn-next') || ev.target.closest('.btn-prev'));\n      if (isNav) setTimeout(()=>refreshAll(container), 100);\n    }, { passive:true });\n\n    \/\/ Gentle re-checks after Places autocomplete might fill values\n    setTimeout(()=>refreshAll(container), 400);\n    setTimeout(()=>refreshAll(container), 900);\n    setTimeout(()=>refreshAll(container), 1500);\n  }\n\n  \/\/ Ensure we run after DOM is ready\n  if (document.readyState === 'loading'){\n    document.addEventListener('DOMContentLoaded', () => {\n      setTimeout(() => wire(document), 200);\n    }, { once:true });\n  } else {\n    setTimeout(() => wire(document), 200);\n  }\n})();\n<\/script>\n\n<\/body>\n<\/html>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>","protected":false},"excerpt":{"rendered":"<p>MVIPS \u2014 Bukas (todo en uno, con chips+auto) Reservas aaa ES EN CA FR Descarga nuestra App Cliente Servicio 1 Servicio 2 Servicio 3 Final Datos del cliente N\u00ba de cliente (opcional) CIF, VAT o Pasaporte* Nombre del cliente* Tel\u00e9fono* Domicilio de facturaci\u00f3n* Correo electr\u00f3nico* Anterior Siguiente Servicio 1 Lugar* BarcelonaMadrid Veh\u00edculo* Servicio* \u2014 Detalles [&hellip;]<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"footnotes":""},"class_list":["post-2965","page","type-page","status-publish","hentry"],"blocksy_meta":[],"_links":{"self":[{"href":"https:\/\/mvips.net\/en\/wp-json\/wp\/v2\/pages\/2965","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mvips.net\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/mvips.net\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/mvips.net\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mvips.net\/en\/wp-json\/wp\/v2\/comments?post=2965"}],"version-history":[{"count":259,"href":"https:\/\/mvips.net\/en\/wp-json\/wp\/v2\/pages\/2965\/revisions"}],"predecessor-version":[{"id":3397,"href":"https:\/\/mvips.net\/en\/wp-json\/wp\/v2\/pages\/2965\/revisions\/3397"}],"wp:attachment":[{"href":"https:\/\/mvips.net\/en\/wp-json\/wp\/v2\/media?parent=2965"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}