{"id":984,"date":"2026-04-01T15:59:05","date_gmt":"2026-04-01T13:59:05","guid":{"rendered":"https:\/\/torreshospitalityconsulting.com\/?page_id=984"},"modified":"2026-04-02T09:14:47","modified_gmt":"2026-04-02T07:14:47","slug":"calculator","status":"publish","type":"page","link":"https:\/\/torreshospitalityconsulting.com\/en\/calculator\/","title":{"rendered":"Hotel Ancillary Calculator"},"content":{"rendered":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \/>\n  <title>Session 1 \u2013 Front Desk Upsell Calculator | Torres Hospitality Consulting<\/title>\n  <style>\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n    :root {\n      --tc-red: #c0392b;\n      --tc-red-light: #fdf1f0;\n      --tc-red-mid: #f0c4c0;\n      --tc-black: #1a1a1a;\n      --tc-dark: #2c2c2c;\n      --tc-muted: #888888;\n      --tc-border: #e8e8e8;\n      --tc-border-mid: #d0d0d0;\n      --tc-bg: #f7f7f7;\n      --tc-white: #ffffff;\n      --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;\n    }\n\n    html { font-size: 16px; }\n\n    body {\n      font-family: var(--font);\n      color: var(--tc-black);\n      background: #f4f4f2;\n      min-height: 100vh;\n      padding: 3rem 1rem;\n    }\n\n    .page-wrap {\n      max-width: 860px;\n      margin: 0 auto;\n      background: var(--tc-white);\n      border-radius: 12px;\n      padding: 2.5rem 2.5rem 3rem;\n      box-shadow: 0 2px 24px rgba(0,0,0,0.07);\n    }\n\n    @media (max-width: 600px) {\n      body { padding: 1rem 0.5rem; }\n      .page-wrap { padding: 1.5rem 1.25rem 2rem; border-radius: 8px; }\n    }\n\n    \/* HEADER *\/\n    .header { margin-bottom: 2rem; }\n    .header-eyebrow { display: flex; align-items: center; gap: 10px; margin-bottom: 0.75rem; }\n    .header-line { width: 28px; height: 2px; background: var(--tc-red); flex-shrink: 0; }\n    .header-brand { font-size: 10px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--tc-muted); }\n    .header-title { font-size: 28px; font-weight: 700; color: var(--tc-black); line-height: 1.2; margin-bottom: 0.6rem; }\n    .header-title span { color: var(--tc-red); }\n    .header-sub { font-size: 13px; color: var(--tc-muted); line-height: 1.7; max-width: 540px; }\n\n    \/* CONTROLS ROW *\/\n    .controls-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; margin-bottom: 1.75rem; }\n    .pill-group { display: flex; gap: 5px; }\n    .pill { padding: 5px 14px; font-size: 12px; font-family: var(--font); border: 1px solid var(--tc-border-mid); border-radius: 20px; background: transparent; color: var(--tc-muted); cursor: pointer; transition: all 0.15s; }\n    .pill:hover { border-color: var(--tc-black); color: var(--tc-black); }\n    .pill.active { background: var(--tc-black); color: #fff; border-color: var(--tc-black); font-weight: 500; }\n\n    \/* SECTION EYEBROW *\/\n    .section-eyebrow { display: flex; align-items: center; gap: 8px; margin-bottom: 0.85rem; margin-top: 0.25rem; }\n    .section-line { width: 18px; height: 2px; background: var(--tc-red); flex-shrink: 0; }\n    .section-label { font-size: 10px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: var(--tc-muted); }\n\n    \/* INPUTS *\/\n    .input-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 1.25rem; }\n    @media (max-width: 580px) { .input-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }\n\n    .input-card { background: var(--tc-bg); border: 1px solid var(--tc-border); border-radius: 8px; padding: 0.85rem 1rem; }\n    .input-card label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--tc-muted); display: block; margin-bottom: 8px; font-weight: 500; }\n    .input-num-wrap { display: flex; align-items: baseline; justify-content: space-between; border-bottom: 1.5px solid var(--tc-border-mid); padding-bottom: 4px; }\n    .input-num-wrap input[type=number] { font-size: 20px; font-weight: 700; color: var(--tc-black); background: transparent; border: none; outline: none; width: 100%; font-family: var(--font); }\n    .input-num-wrap:focus-within { border-bottom-color: var(--tc-red); }\n    .input-unit { font-size: 12px; color: var(--tc-muted); white-space: nowrap; flex-shrink: 0; padding-left: 4px; }\n    .input-hint { font-size: 10px; color: var(--tc-muted); margin-top: 5px; }\n\n    \/* CONV SLIDER *\/\n    .conv-card { background: var(--tc-bg); border: 1px solid var(--tc-border); border-radius: 8px; padding: 0.85rem 1.25rem; margin-bottom: 1.75rem; display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }\n    .conv-card label { font-size: 12px; color: var(--tc-muted); white-space: nowrap; flex-shrink: 0; }\n    .conv-card input[type=range] { flex: 1; min-width: 100px; accent-color: var(--tc-red); cursor: pointer; height: 4px; }\n    .conv-val { font-size: 20px; font-weight: 700; color: var(--tc-black); min-width: 44px; text-align: right; }\n\n    \/* DIVIDER *\/\n    .divider { height: 1px; background: var(--tc-border); margin: 1.75rem 0; }\n\n    \/* OFFERS GRID *\/\n    .offers-intro { font-size: 12px; color: var(--tc-muted); margin-bottom: 1rem; line-height: 1.7; }\n    .offers-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(185px, 1fr)); gap: 8px; margin-bottom: 0.75rem; }\n\n    \/* OFFER CARD \u2014 toggle area vs input area are now separate click targets *\/\n    .offer-card { border: 1px solid var(--tc-border); border-radius: 8px; background: var(--tc-white); overflow: hidden; transition: border-color 0.15s; }\n    .offer-card.on { border-color: var(--tc-red); }\n\n    \/* \u25b8 ONLY the header row triggers toggle \u2014 not the fields below *\/\n    .offer-header {\n      display: flex; align-items: center; justify-content: space-between;\n      padding: 10px 12px;\n      cursor: pointer;\n      user-select: none;\n    }\n    .offer-name { font-size: 13px; font-weight: 500; color: var(--tc-dark); }\n    .offer-card.on .offer-name { color: var(--tc-red); }\n\n    .offer-toggle { width: 30px; height: 17px; border-radius: 9px; background: var(--tc-border-mid); position: relative; transition: background 0.2s; flex-shrink: 0; pointer-events: none; }\n    .offer-toggle::after { content: ''; position: absolute; width: 13px; height: 13px; border-radius: 50%; background: #fff; top: 2px; left: 2px; transition: left 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }\n    .offer-card.on .offer-toggle { background: var(--tc-red); }\n    .offer-card.on .offer-toggle::after { left: 15px; }\n\n    \/* \u25b8 Fields area: NOT a click target for toggle, inputs work normally *\/\n    .offer-fields {\n      display: none;\n      padding: 0 12px 12px;\n      border-top: 1px solid var(--tc-border);\n      \/* Prevent any click inside fields from bubbling to toggle *\/\n    }\n    .offer-card.on .offer-fields { display: block; }\n\n    .field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px; }\n    .field-wrap label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--tc-muted); display: block; margin-bottom: 4px; font-weight: 500; }\n    .field-wrap input {\n      width: 100%;\n      font-size: 16px; \/* \u226516px prevents iOS zoom *\/\n      font-weight: 700;\n      color: var(--tc-black);\n      background: var(--tc-bg);\n      border: 1px solid var(--tc-border);\n      border-radius: 5px;\n      outline: none;\n      padding: 6px 8px;\n      font-family: var(--font);\n      transition: border-color 0.15s;\n      -webkit-appearance: none;\n    }\n    .field-wrap input:focus { border-color: var(--tc-red); background: var(--tc-white); }\n    .offer-contrib { margin-top: 8px; font-size: 11px; color: var(--tc-muted); text-align: right; }\n    .offer-contrib strong { color: var(--tc-black); }\n\n    \/* FORMULA NOTE *\/\n    .formula-note { font-size: 11px; color: var(--tc-muted); margin-bottom: 1.5rem; background: var(--tc-red-light); border-left: 3px solid var(--tc-red); border-radius: 0 6px 6px 0; padding: 8px 14px; line-height: 1.7; }\n    .formula-note strong { color: var(--tc-black); }\n\n    \/* EMPTY STATE *\/\n    .empty-state { text-align: center; padding: 2.5rem; color: var(--tc-muted); font-size: 13px; background: var(--tc-bg); border-radius: 8px; border: 1px dashed var(--tc-border-mid); margin-bottom: 1rem; }\n\n    \/* RESULTS *\/\n    .big-metric { background: var(--tc-black); border-radius: 10px; padding: 1.5rem 1.75rem; margin-bottom: 12px; }\n    .big-metric-eyebrow { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }\n    .big-metric-line { width: 18px; height: 2px; background: var(--tc-red); }\n    .big-metric-label { font-size: 10px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase; color: rgba(255,255,255,0.45); }\n    .big-metric-value { font-size: 42px; font-weight: 700; color: #fff; line-height: 1; }\n    .big-metric-sub { font-size: 12px; color: rgba(255,255,255,0.4); margin-top: 6px; }\n\n    .metrics-row { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 10px; margin-bottom: 12px; }\n    @media (max-width: 480px) { .metrics-row { grid-template-columns: 1fr 1fr; } }\n\n    .metric { background: var(--tc-white); border: 1px solid var(--tc-border); border-radius: 8px; padding: 1rem 1.1rem; }\n    .metric-eyebrow { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }\n    .metric-line { width: 12px; height: 2px; background: var(--tc-red); }\n    .metric-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--tc-muted); font-weight: 500; }\n    .metric-value { font-size: 20px; font-weight: 700; color: var(--tc-black); }\n    .metric-sub { font-size: 10px; color: var(--tc-muted); margin-top: 3px; }\n\n    .breakdown-card { background: var(--tc-white); border: 1px solid var(--tc-border); border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 12px; }\n    .breakdown-eyebrow { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }\n    .brow { display: flex; justify-content: space-between; align-items: center; padding: 7px 0; border-bottom: 1px solid var(--tc-border); font-size: 13px; }\n    .brow:last-child { border-bottom: none; }\n    .brow .bl { color: var(--tc-muted); }\n    .brow.total .bl { color: var(--tc-black); font-weight: 600; }\n    .brow .bv { font-weight: 600; color: var(--tc-black); }\n    .brow.total .bv { color: var(--tc-red); }\n\n    .revpar-pill { display: inline-flex; align-items: center; gap: 8px; background: var(--tc-red-light); color: var(--tc-red); font-size: 13px; font-weight: 600; padding: 8px 16px; border-radius: 6px; border: 1px solid var(--tc-red-mid); margin-bottom: 1.5rem; }\n\n    \/* \u2500\u2500\u2500 CTA BLOCK \u2500\u2500\u2500 *\/\n    .cta-block {\n      background: var(--tc-black);\n      border-radius: 12px;\n      padding: 2rem 2.25rem;\n      margin-top: 2rem;\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      gap: 1.5rem;\n      flex-wrap: wrap;\n    }\n    .cta-block-text {}\n    .cta-eyebrow { display: flex; align-items: center; gap: 8px; margin-bottom: 0.5rem; }\n    .cta-line { width: 18px; height: 2px; background: var(--tc-red); flex-shrink: 0; }\n    .cta-label { font-size: 10px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: rgba(255,255,255,0.4); }\n    .cta-title { font-size: 20px; font-weight: 700; color: #fff; line-height: 1.25; margin-bottom: 0.4rem; }\n    .cta-title span { color: var(--tc-red); }\n    .cta-sub { font-size: 13px; color: rgba(255,255,255,0.45); line-height: 1.6; max-width: 400px; }\n    .cta-btn {\n      display: inline-flex;\n      align-items: center;\n      gap: 8px;\n      background: var(--tc-red);\n      color: #fff;\n      font-family: var(--font);\n      font-size: 14px;\n      font-weight: 600;\n      padding: 13px 24px;\n      border-radius: 8px;\n      border: none;\n      cursor: pointer;\n      text-decoration: none;\n      white-space: nowrap;\n      transition: background 0.15s, transform 0.1s;\n      flex-shrink: 0;\n    }\n    .cta-btn:hover { background: #a93224; transform: translateY(-1px); }\n    .cta-btn svg { width: 16px; height: 16px; }\n\n    \/* FOOTER *\/\n    .footer-note { font-size: 11px; color: var(--tc-muted); margin-top: 2rem; padding-top: 1.25rem; border-top: 1px solid var(--tc-border); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; }\n    .footer-note a { color: var(--tc-red); text-decoration: none; }\n    .footer-note a:hover { text-decoration: underline; }\n\n    input[type=number]::-webkit-inner-spin-button,\n    input[type=number]::-webkit-outer-spin-button { opacity: 0.4; }\n  <\/style>\n<\/head>\n<body>\n<div class=\"page-wrap\">\n\n  <div class=\"header\">\n    <div class=\"header-eyebrow\">\n      <div class=\"header-line\"><\/div>\n      <span class=\"header-brand\">Torres Hospitality Consulting<\/span>\n    <\/div>\n    <div class=\"header-title\" id=\"t-title\">What can you sell at the front desk<br>&amp; <span>how much revenue<\/span> Can you generate?<\/div>\n    <p class=\"header-sub\" id=\"t-sub\">Activate the offers your hotel sells, enter the supplement and daily units per offer. Revenue is calculated independently for each one.<\/p>\n  <\/div>\n\n  <div class=\"controls-row\">\n    <div class=\"pill-group\" id=\"lang-group\">\n      <button class=\"pill active\" onclick=\"setLang('en',this)\">English<\/button>\n      <button class=\"pill\" onclick=\"setLang('es',this)\">Spanish<\/button>\n    <\/div>\n    <div class=\"pill-group\" id=\"cur-group\">\n      <button class=\"pill active\" onclick=\"setCur('\u20ac',this)\">\u20ac EUR<\/button>\n      <button class=\"pill\" onclick=\"setCur('$',this)\">$ USD<\/button>\n      <button class=\"pill\" onclick=\"setCur('\u00a3',this)\">\u00a3 GBP<\/button>\n    <\/div>\n  <\/div>\n\n  <!-- SECTION 1 -->\n  <div class=\"section-eyebrow\">\n    <div class=\"section-line\"><\/div>\n    <span class=\"section-label\" id=\"t-s1\">Your hotel data<\/span>\n  <\/div>\n\n  <div class=\"input-grid\">\n    <div class=\"input-card\">\n      <label id=\"t-rooms\">Rooms<\/label>\n      <div class=\"input-num-wrap\">\n        <input type=\"number\" id=\"rooms\" value=\"150\" min=\"1\" oninput=\"calc()\" \/>\n        <span class=\"input-unit\" id=\"t-rooms-u\">rooms<\/span>\n      <\/div>\n      <div class=\"input-hint\" id=\"t-rooms-h\">Total inventory<\/div>\n    <\/div>\n    <div class=\"input-card\">\n      <label id=\"t-adr\">ADR<\/label>\n      <div class=\"input-num-wrap\">\n        <input type=\"number\" id=\"adr\" value=\"200\" min=\"1\" step=\"1\" oninput=\"calc()\" \/>\n        <span class=\"input-unit\" id=\"t-adr-u\">\u20ac<\/span>\n      <\/div>\n      <div class=\"input-hint\" id=\"t-adr-h\">Average daily rate<\/div>\n    <\/div>\n    <div class=\"input-card\">\n      <label id=\"t-occ\">Occupancy<\/label>\n      <div class=\"input-num-wrap\">\n        <input type=\"number\" id=\"occ\" value=\"90\" min=\"1\" max=\"100\" step=\"0.1\" oninput=\"calc()\" \/>\n        <span class=\"input-unit\">%<\/span>\n      <\/div>\n      <div class=\"input-hint\" id=\"t-occ-h\">Monthly average<\/div>\n    <\/div>\n    <div class=\"input-card\">\n      <label id=\"t-los\">Average Length of Stay<\/label>\n      <div class=\"input-num-wrap\">\n        <input type=\"number\" id=\"los\" value=\"2.3\" min=\"0.5\" step=\"0.1\" oninput=\"calc()\" \/>\n        <span class=\"input-unit\" id=\"t-los-u\">days<\/span>\n      <\/div>\n      <div class=\"input-hint\" id=\"t-los-h\">Days per booking<\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"conv-card\">\n    <label id=\"t-conv\">% of arrivals converted to upsell:<\/label>\n    <input type=\"range\" id=\"conv\" min=\"1\" max=\"30\" value=\"6\" step=\"1\"\n      oninput=\"document.getElementById('convOut').textContent=this.value+'%'; calc()\" \/>\n    <span class=\"conv-val\" id=\"convOut\">6%<\/span>\n  <\/div>\n\n  <div class=\"divider\"><\/div>\n\n  <!-- SECTION 2 -->\n  <div class=\"section-eyebrow\">\n    <div class=\"section-line\"><\/div>\n    <span class=\"section-label\" id=\"t-s2\">Offers at the front desk<\/span>\n  <\/div>\n  <p class=\"offers-intro\" id=\"t-intro\">Activate each offer, enter the average supplement per stay and estimated daily units sold.<\/p>\n\n  <div class=\"offers-grid\" id=\"offersGrid\"><\/div>\n\n  <div class=\"formula-note\">\n    <strong id=\"t-formula-title\">How it&#039;s calculated:<\/strong>\n    <span id=\"t-formula-body\">Each offer is independent \u2192 Daily revenue = units\/day \u00d7 supplement per stay. Total = sum of all active offers.<\/span>\n  <\/div>\n\n  <div class=\"divider\"><\/div>\n\n  <!-- SECTION 3 -->\n  <div class=\"section-eyebrow\">\n    <div class=\"section-line\"><\/div>\n    <span class=\"section-label\" id=\"t-s3\">Your upsell potential<\/span>\n  <\/div>\n  <div id=\"resultsArea\"><\/div>\n\n  <!-- CTA BLOCK -->\n  <div class=\"cta-block\" id=\"ctaBlock\">\n    <div class=\"cta-block-text\">\n      <div class=\"cta-eyebrow\">\n        <div class=\"cta-line\"><\/div>\n        <span class=\"cta-label\" id=\"t-cta-label\">Torres Hospitality Consulting<\/span>\n      <\/div>\n      <div class=\"cta-title\" id=\"t-cta-title\">Want to <span>unlock this revenue<\/span><br>in your hotel?<\/div>\n      <p class=\"cta-sub\" id=\"t-cta-sub\">Pablo Torres helps hotel teams build real upselling systems \u2014 from script to conversion. Let&#039;s talk about what&#039;s possible for your property.<\/p>\n    <\/div>\n    <a href=\"https:\/\/torreshospitalityconsulting.com\/en\/\" target=\"_blank\" class=\"cta-btn\" id=\"ctaBtn\">\n      <svg viewbox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 12h14M12 5l7 7-7 7\"\/><\/svg>\n      <span id=\"t-cta-btn\">Book a free call<\/span>\n    <\/a>\n  <\/div>\n\n  <div class=\"footer-note\">\n    <span>Torres Hospitality Consulting<\/span>\n    <a href=\"https:\/\/torreshospitalityconsulting.com\/en\/\" target=\"_blank\">torreshospitalityconsulting.com<\/a>\n  <\/div>\n\n<\/div>\n\n<script>\n  let lang = 'en', cur = '\u20ac';\n\n  const offers = [\n    { key:'upgrade',   en:'Room upgrade',     es:'Upgrade habitaci\u00f3n',  supp:'', units:'' },\n    { key:'earlyin',   en:'Early check-in',   es:'Early check-in',      supp:'', units:'' },\n    { key:'lateout',   en:'Late check-out',   es:'Late check-out',      supp:'', units:'' },\n    { key:'board',     en:'Board',            es:'Pensi\u00f3n \/ R\u00e9gimen',   supp:'', units:'' },\n    { key:'parking',   en:'Parking',          es:'Parking',             supp:'', units:'' },\n    { key:'spa',       en:'Spa package',      es:'Pack spa',            supp:'', units:'' },\n    { key:'welcome',   en:'Welcome pack',     es:'Pack bienvenida',     supp:'', units:'' },\n    { key:'transfer',  en:'Transfer',         es:'Transfer',            supp:'', units:'' },\n    { key:'extras',    en:'Other extras',     es:'Otros extras',        supp:'', units:'' },\n  ];\n\n  const active = new Set();\n\n  const T = {\n    en: {\n      title: 'What can you sell at the front desk<br>& <span style=\"color:var(--tc-red)\">how much revenue<\/span> can you generate?',\n      sub: 'Activate the offers your hotel sells, enter the supplement and daily units per offer. Revenue is calculated independently for each one.',\n      s1:'Your hotel data', s2:'Offers at the front desk', s3:'Your upsell potential',\n      rooms:'Rooms', roomsH:'Total inventory', roomsU:'rooms',\n      adr:'ADR', adrH:'Average daily rate',\n      occ:'Occupancy', occH:'Monthly average',\n      los:'Avg. Length of Stay', losH:'Days per booking', losU:'days',\n      conv:'% of arrivals converted to upsell:',\n      intro:'Activate each offer, enter the average supplement per stay and estimated daily units sold.',\n      formulaTitle:'How it\\'s calculated:',\n      formulaBody:'Each offer is independent \u2192 Daily revenue = units\/day \u00d7 supplement per stay. Total = sum of all active offers.',\n      suppLabel:'Supplement \/ stay', unitsLabel:'Units \/ day',\n      annRev:'Projected annual revenue', dayRev:'Daily revenue', monRev:'Monthly revenue',\n      revpar:'Current RevPAR', revparLift:'RevPAR uplift',\n      arrivals:'Daily arrivals',\n      empty:'Activate at least one offer and fill in supplement and daily units to see your results.',\n      offerBreakdown:'Revenue by offer (annual)',\n      totalAnn:'Total annual upsell', contrib:'annual',\n      offersActive: 'offers active',\n      ctaLabel: 'Torres Hospitality Consulting',\n      ctaTitle: 'Want to <span>unlock this revenue<\/span><br>in your hotel?',\n      ctaSub: 'Pablo Torres helps hotel teams build real upselling systems \u2014 from script to conversion. Let\\'s talk about what\\'s possible for your property.',\n      ctaBtn: 'Book a free call',\n    },\n    es: {\n      title: '\u00bfQu\u00e9 puedes vender en recepci\u00f3n<br>y <span style=\"color:var(--tc-red)\">cu\u00e1nto revenue<\/span> puedes generar?',\n      sub: 'Activa las ofertas que vende tu hotel, introduce el suplemento y las unidades diarias. El revenue se calcula de forma independiente por oferta.',\n      s1:'Datos de tu hotel', s2:'Ofertas en recepci\u00f3n', s3:'Tu potencial de upsell',\n      rooms:'Habitaciones', roomsH:'Inventario total', roomsU:'hab.',\n      adr:'ADR', adrH:'Precio medio diario',\n      occ:'Ocupaci\u00f3n', occH:'Media mensual',\n      los:'Estancia media', losH:'D\u00edas por reserva', losU:'d\u00edas',\n      conv:'% de llegadas que conviertes a upsell:',\n      intro:'Activa cada oferta, introduce el suplemento medio por estancia y las unidades vendidas por d\u00eda.',\n      formulaTitle:'C\u00f3mo se calcula:',\n      formulaBody:'Cada oferta es independiente \u2192 Revenue diario = unidades\/d\u00eda \u00d7 suplemento por estancia. Total = suma de todas las ofertas activas.',\n      suppLabel:'Suplemento \/ estancia', unitsLabel:'Unidades \/ d\u00eda',\n      annRev:'Revenue anual proyectado', dayRev:'Revenue diario', monRev:'Revenue mensual',\n      revpar:'RevPAR actual', revparLift:'Incremento RevPAR',\n      arrivals:'Llegadas diarias',\n      empty:'Activa al menos una oferta e introduce suplemento y unidades diarias para ver tus resultados.',\n      offerBreakdown:'Revenue por oferta (anual)',\n      totalAnn:'Upsell total anual', contrib:'anual',\n      offersActive: 'ofertas activas',\n      ctaLabel: 'Torres Hospitality Consulting',\n      ctaTitle: '\u00bfQuieres <span>activar este revenue<\/span><br>en tu hotel?',\n      ctaSub: 'Pablo Torres ayuda a equipos hoteleros a construir sistemas reales de upselling \u2014 del script a la conversi\u00f3n. Hablemos de lo que es posible en tu propiedad.',\n      ctaBtn: 'Reserva una llamada gratuita',\n    }\n  };\n\n  function setLang(l, btn) {\n    lang = l;\n    document.querySelectorAll('#lang-group .pill').forEach(b => b.classList.remove('active'));\n    btn.classList.add('active');\n    applyLang(); renderOffers(); calc();\n  }\n\n  function setCur(c, btn) {\n    cur = c;\n    document.querySelectorAll('#cur-group .pill').forEach(b => b.classList.remove('active'));\n    btn.classList.add('active');\n    document.getElementById('t-adr-u').textContent = c;\n    renderOffers(); calc();\n  }\n\n  function applyLang() {\n    const t = T[lang];\n    document.getElementById('t-title').innerHTML = t.title;\n    document.getElementById('t-sub').textContent = t.sub;\n    document.getElementById('t-s1').textContent = t.s1;\n    document.getElementById('t-s2').textContent = t.s2;\n    document.getElementById('t-s3').textContent = t.s3;\n    document.getElementById('t-rooms').textContent = t.rooms;\n    document.getElementById('t-rooms-h').textContent = t.roomsH;\n    document.getElementById('t-rooms-u').textContent = t.roomsU;\n    document.getElementById('t-adr').textContent = t.adr;\n    document.getElementById('t-adr-h').textContent = t.adrH;\n    document.getElementById('t-occ').textContent = t.occ;\n    document.getElementById('t-occ-h').textContent = t.occH;\n    document.getElementById('t-los').textContent = t.los;\n    document.getElementById('t-los-h').textContent = t.losH;\n    document.getElementById('t-los-u').textContent = t.losU;\n    document.getElementById('t-conv').textContent = t.conv;\n    document.getElementById('t-intro').textContent = t.intro;\n    document.getElementById('t-formula-title').textContent = t.formulaTitle;\n    document.getElementById('t-formula-body').textContent = t.formulaBody;\n    \/\/ CTA\n    document.getElementById('t-cta-label').textContent = t.ctaLabel;\n    document.getElementById('t-cta-title').innerHTML = t.ctaTitle;\n    document.getElementById('t-cta-sub').textContent = t.ctaSub;\n    document.getElementById('t-cta-btn').textContent = t.ctaBtn;\n  }\n\n  function fmt(n) { return cur + Math.round(n).toLocaleString('en-US'); }\n  function fmtD(n, d=2) { return cur + n.toFixed(d); }\n\n  function renderOffers() {\n    const grid = document.getElementById('offersGrid');\n    grid.innerHTML = '';\n    const t = T[lang];\n    offers.forEach(o => {\n      const isOn = active.has(o.key);\n      const daily = isOn ? (parseFloat(o.units)||0) * (parseFloat(o.supp)||0) : 0;\n      const annual = daily * 365;\n      const card = document.createElement('div');\n      card.className = 'offer-card' + (isOn ? ' on' : '');\n      card.setAttribute('data-key', o.key);\n\n      \/\/ \u2500\u2500 Header: toggle on click\n      const header = document.createElement('div');\n      header.className = 'offer-header';\n      header.innerHTML = `\n        <span class=\"offer-name\">${o[lang]}<\/span>\n        <div class=\"offer-toggle\"><\/div>\n      `;\n      header.addEventListener('click', () => toggleOffer(o.key));\n\n      \/\/ \u2500\u2500 Fields: NO click propagation upward; inputs work freely\n      const fields = document.createElement('div');\n      fields.className = 'offer-fields';\n      fields.innerHTML = `\n        <div class=\"field-row\">\n          <div class=\"field-wrap\">\n            <label>${t.suppLabel}<\/label>\n            <input type=\"number\" inputmode=\"numeric\" value=\"${o.supp}\" min=\"0\" step=\"1\" placeholder=\"0\" \/>\n          <\/div>\n          <div class=\"field-wrap\">\n            <label>${t.unitsLabel}<\/label>\n            <input type=\"number\" inputmode=\"decimal\" value=\"${o.units}\" min=\"0\" step=\"0.1\" placeholder=\"0\" \/>\n          <\/div>\n        <\/div>\n        ${isOn && daily > 0 ? `<div class=\"offer-contrib\">= <strong>${fmt(annual)}<\/strong> ${t.contrib}<\/div>` : ''}\n      `;\n\n      \/\/ Attach input listeners to the actual input elements (not via oninput attr)\n      const [suppInput, unitsInput] = fields.querySelectorAll('input');\n      suppInput.addEventListener('input', e => { saveOffer(o.key, 'supp', e.target.value); });\n      unitsInput.addEventListener('input', e => { saveOffer(o.key, 'units', e.target.value); });\n\n      \/\/ \u2500\u2500 CRITICAL: stop clicks inside fields from reaching anything else\n      fields.addEventListener('click', e => e.stopPropagation());\n      fields.addEventListener('mousedown', e => e.stopPropagation());\n      fields.addEventListener('touchstart', e => e.stopPropagation(), { passive: true });\n\n      card.appendChild(header);\n      card.appendChild(fields);\n      grid.appendChild(card);\n    });\n  }\n\n  function toggleOffer(key) {\n    if (active.has(key)) active.delete(key); else active.add(key);\n    renderOffers(); calc();\n  }\n\n  function saveOffer(key, field, val) {\n    const o = offers.find(x => x.key === key);\n    if (o) {\n      o[field] = val;\n      \/\/ Update only the contrib line without re-rendering the whole grid\n      \/\/ (avoids losing focus while typing)\n      calc();\n      updateContrib(o);\n    }\n  }\n\n  function updateContrib(o) {\n    const t = T[lang];\n    const card = document.querySelector(`.offer-card[data-key=\"${o.key}\"]`);\n    if (!card) return;\n    const daily  = (parseFloat(o.units)||0) * (parseFloat(o.supp)||0);\n    const annual = daily * 365;\n    let contrib = card.querySelector('.offer-contrib');\n    if (daily > 0 && active.has(o.key)) {\n      if (!contrib) {\n        contrib = document.createElement('div');\n        contrib.className = 'offer-contrib';\n        card.querySelector('.offer-fields').appendChild(contrib);\n      }\n      contrib.innerHTML = `= <strong>${fmt(annual)}<\/strong> ${t.contrib}`;\n    } else if (contrib) {\n      contrib.remove();\n    }\n  }\n\n  function calc() {\n    const t = T[lang];\n    const rooms = parseFloat(document.getElementById('rooms').value) || 0;\n    const adr   = parseFloat(document.getElementById('adr').value)   || 0;\n    const occ   = parseFloat(document.getElementById('occ').value) \/ 100 || 0;\n    const los   = parseFloat(document.getElementById('los').value)   || 1;\n\n    const occRooms = rooms * occ;\n    const arrivals = occRooms \/ los;\n    const revpar   = adr * occ;\n\n    let totalDaily = 0;\n    const offerLines = [];\n    offers.forEach(o => {\n      if (!active.has(o.key)) return;\n      const supp  = parseFloat(o.supp)  || 0;\n      const units = parseFloat(o.units) || 0;\n      if (supp > 0 && units > 0) {\n        const daily = units * supp;\n        totalDaily += daily;\n        offerLines.push({ name: o[lang], daily, annual: daily * 365 });\n      }\n    });\n\n    const totalMonthly = totalDaily * 30.4;\n    const totalAnnual  = totalDaily * 365;\n    const revparLift   = rooms > 0 && revpar > 0 ? (totalDaily \/ rooms \/ revpar * 100) : 0;\n    const ra = document.getElementById('resultsArea');\n\n    if (active.size === 0 || totalDaily === 0) {\n      ra.innerHTML = `<div class=\"empty-state\">${t.empty}<\/div>`;\n      return;\n    }\n\n    const breakdown = offerLines.length > 1 ? `\n      <div class=\"breakdown-card\">\n        <div class=\"breakdown-eyebrow\">\n          <div class=\"section-line\"><\/div>\n          <span class=\"section-label\">${t.offerBreakdown}<\/span>\n        <\/div>\n        ${offerLines.map(ol => `\n          <div class=\"brow\">\n            <span class=\"bl\">${ol.name}<\/span>\n            <span class=\"bv\">${fmt(ol.annual)}<\/span>\n          <\/div>`).join('')}\n        <div class=\"brow total\">\n          <span class=\"bl\">${t.totalAnn}<\/span>\n          <span class=\"bv\">${fmt(totalAnnual)}<\/span>\n        <\/div>\n      <\/div>` : '';\n\n    ra.innerHTML = `\n      <div class=\"big-metric\">\n        <div class=\"big-metric-eyebrow\">\n          <div class=\"big-metric-line\"><\/div>\n          <span class=\"big-metric-label\">${t.annRev}<\/span>\n        <\/div>\n        <div class=\"big-metric-value\">${fmt(totalAnnual)}<\/div>\n        <div class=\"big-metric-sub\">${t.arrivals}: ${arrivals.toFixed(1)} \u00b7 ${t.revpar}: ${fmtD(revpar)}<\/div>\n      <\/div>\n      <div class=\"metrics-row\">\n        <div class=\"metric\">\n          <div class=\"metric-eyebrow\"><div class=\"metric-line\"><\/div><span class=\"metric-label\">${t.dayRev}<\/span><\/div>\n          <div class=\"metric-value\">${fmt(totalDaily)}<\/div>\n          <div class=\"metric-sub\">${offerLines.length} ${t.offersActive}<\/div>\n        <\/div>\n        <div class=\"metric\">\n          <div class=\"metric-eyebrow\"><div class=\"metric-line\"><\/div><span class=\"metric-label\">${t.monRev}<\/span><\/div>\n          <div class=\"metric-value\">${fmt(totalMonthly)}<\/div>\n          <div class=\"metric-sub\">\u00d7 30.4 days<\/div>\n        <\/div>\n        <div class=\"metric\">\n          <div class=\"metric-eyebrow\"><div class=\"metric-line\"><\/div><span class=\"metric-label\">${t.revparLift}<\/span><\/div>\n          <div class=\"metric-value\" style=\"color:var(--tc-red)\">+${revparLift.toFixed(2)}%<\/div>\n          <div class=\"metric-sub\">${t.revpar}: ${fmtD(revpar)}<\/div>\n        <\/div>\n      <\/div>\n      ${breakdown}\n      <div class=\"revpar-pill\">+ ${revparLift.toFixed(2)}% ${t.revparLift} \u00b7 ${fmt(totalAnnual)} ${t.annRev.toLowerCase()}<\/div>\n    `;\n  }\n\n  function init() {\n    applyLang();\n    renderOffers();\n    calc();\n  }\n\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', init);\n  } else {\n    init();\n  }\n<\/script>\n<\/body>\n<\/html>\n\n\n\n<p><\/p>","protected":false},"excerpt":{"rendered":"<p>Session 1 \u2013 Front Desk Upsell Calculator | Torres Hospitality Consulting Torres Hospitality Consulting What can you sell at the front desk&amp; how much revenue can<span class=\"excerpt-hellip\"> [\u2026]<\/span><\/p>","protected":false},"author":5,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-984","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/torreshospitalityconsulting.com\/en\/wp-json\/wp\/v2\/pages\/984","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/torreshospitalityconsulting.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/torreshospitalityconsulting.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/torreshospitalityconsulting.com\/en\/wp-json\/wp\/v2\/users\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/torreshospitalityconsulting.com\/en\/wp-json\/wp\/v2\/comments?post=984"}],"version-history":[{"count":2,"href":"https:\/\/torreshospitalityconsulting.com\/en\/wp-json\/wp\/v2\/pages\/984\/revisions"}],"predecessor-version":[{"id":987,"href":"https:\/\/torreshospitalityconsulting.com\/en\/wp-json\/wp\/v2\/pages\/984\/revisions\/987"}],"wp:attachment":[{"href":"https:\/\/torreshospitalityconsulting.com\/en\/wp-json\/wp\/v2\/media?parent=984"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}