Wedding Budget Calculator

This wedding budget calculator needs JavaScript enabled. / Esta calculadora necesita JavaScript.

/* ---------- Scoped design tokens ---------- */ .bwc{ --bwc-ivory:#fcfaf5; --bwc-champ:#f3ebdc; --bwc-blush:#f7e3df; --bwc-blush2:#d8a39e; --bwc-blush3:#f0c9c4; --bwc-gold:#b9955a; --bwc-gold2:#e8dbbf; --bwc-gold3:#f7f0e2; --bwc-ink:#3b332f; --bwc-muted:#7d7169; --bwc-card:#fffdf9; --bwc-line:#e8dfd0; --bwc-line2:#f1eadf; --bwc-green:#5f9b71; --bwc-green2:#e4f1e7; --bwc-amber:#cf9e3f; --bwc-amber2:#f8ecd3; --bwc-red:#c65d57; --bwc-red2:#f8e1df; --bwc-shadow:0 10px 30px rgba(96,74,40,.09); --bwc-shadow-sm:0 2px 8px rgba(96,74,40,.07); --bwc-r:18px; --bwc-r-sm:12px; --bwc-serif:"Cormorant Garamond","Playfair Display","Libre Baskerville",Georgia,"Times New Roman",serif; --bwc-sans:"Segoe UI",-apple-system,BlinkMacSystemFont,"Helvetica Neue",Helvetica,Arial,sans-serif; font-family:var(--bwc-sans); font-size:15px; line-height:1.45; color:var(--bwc-ink); background:linear-gradient(180deg,var(--bwc-ivory) 0%,var(--bwc-champ) 100%); border-radius:26px; padding:22px; max-width:1240px; margin:0 auto; box-sizing:border-box; position:relative; -webkit-font-smoothing:antialiased; } .bwc *,.bwc *::before,.bwc *::after{box-sizing:border-box} .bwc h1,.bwc h2,.bwc h3,.bwc h4{font-family:var(--bwc-serif);font-weight:600;margin:0;color:var(--bwc-ink);line-height:1.2;letter-spacing:.01em} .bwc h1{font-size:34px}.bwc h2{font-size:26px}.bwc h3{font-size:20px}.bwc h4{font-size:17px} .bwc p{margin:0 0 10px} .bwc input,.bwc select,.bwc textarea,.bwc button{font-family:var(--bwc-sans);font-size:14px;color:var(--bwc-ink);margin:0} .bwc input[type=text],.bwc input[type=number],.bwc input[type=date],.bwc input[type=email],.bwc input[type=tel],.bwc input[type=url],.bwc select,.bwc textarea{ width:100%;padding:9px 11px;border:1px solid var(--bwc-line);border-radius:10px;background:#fff;min-height:40px;line-height:1.3;box-shadow:none;appearance:auto;-webkit-appearance:auto;transition:border-color .15s,box-shadow .15s } .bwc textarea{min-height:64px;resize:vertical} .bwc input:focus,.bwc select:focus,.bwc textarea:focus,.bwc button:focus-visible{outline:none;border-color:var(--bwc-gold);box-shadow:0 0 0 3px rgba(185,149,90,.22)} .bwc button{cursor:pointer} .bwc input[type=checkbox]{width:18px;height:18px;accent-color:var(--bwc-gold);margin:0;cursor:pointer;flex:none} .bwc input[type=number]{-moz-appearance:textfield} .bwc input:disabled,.bwc select:disabled{background:var(--bwc-gold3);color:var(--bwc-muted)} .bwc-sr{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} .bwc-muted{color:var(--bwc-muted);font-size:13px} .bwc-small{font-size:12.5px} .bwc-right{text-align:right} .bwc-nowrap{white-space:nowrap} .bwc-mono{font-variant-numeric:tabular-nums} .bwc [hidden]{display:none!important} /* ---------- Header ---------- */ .bwc-header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:14px;padding:6px 6px 18px} .bwc-brand{display:flex;flex-direction:column;gap:4px} .bwc-brand-kicker{font-size:11.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--bwc-gold);font-weight:600} .bwc-brand h1{font-size:36px;font-weight:500} .bwc-brand-sub{color:var(--bwc-muted);font-size:14px} .bwc-lang{display:inline-flex;background:#fff;border:1px solid var(--bwc-line);border-radius:999px;padding:4px;box-shadow:var(--bwc-shadow-sm)} .bwc-lang button{border:0;background:transparent;padding:7px 16px;border-radius:999px;font-weight:600;color:var(--bwc-muted);letter-spacing:.02em} .bwc-lang button[aria-pressed=true]{background:var(--bwc-ink);color:#fff} .bwc-lang button:hover{color:var(--bwc-ink)} .bwc-lang button[aria-pressed=true]:hover{color:#fff} /* ---------- Tabs ---------- */ .bwc-tabs{display:flex;gap:6px;overflow-x:auto;padding:4px 2px 10px;scrollbar-width:thin;-webkit-overflow-scrolling:touch} .bwc-tabs button{flex:none;border:1px solid transparent;background:rgba(255,255,255,.55);padding:10px 16px;border-radius:999px;font-weight:600;color:var(--bwc-muted);font-size:14px;transition:all .15s;white-space:nowrap} .bwc-tabs button:hover{background:#fff;color:var(--bwc-ink)} .bwc-tabs button[aria-selected=true]{background:#fff;color:var(--bwc-ink);border-color:var(--bwc-gold2);box-shadow:var(--bwc-shadow-sm)} .bwc-tabs button span.bwc-tab-num{display:inline-block;width:20px;height:20px;line-height:20px;border-radius:50%;background:var(--bwc-gold3);color:var(--bwc-gold);font-size:11px;text-align:center;margin-right:7px;font-weight:700} .bwc-tabs button[aria-selected=true] span.bwc-tab-num{background:var(--bwc-gold);color:#fff} /* ---------- Sticky summary ---------- */ .bwc-sticky{position:sticky;top:0;z-index:5;background:rgba(255,253,249,.96);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid var(--bwc-line);border-radius:16px;padding:12px 16px;margin-bottom:16px;box-shadow:var(--bwc-shadow-sm)} .bwc-sticky-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;align-items:end} .bwc-stat .bwc-stat-l{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--bwc-muted);font-weight:600} .bwc-stat .bwc-stat-v{font-family:var(--bwc-serif);font-size:24px;font-weight:600;line-height:1.1;margin-top:2px;font-variant-numeric:tabular-nums} .bwc-stat .bwc-stat-v.bwc-pos{color:var(--bwc-green)}.bwc-stat .bwc-stat-v.bwc-neg{color:var(--bwc-red)} .bwc-progress{height:10px;border-radius:999px;background:var(--bwc-line2);overflow:hidden;margin-top:10px;position:relative} .bwc-progress > span{display:block;height:100%;border-radius:999px;transition:width .35s ease,background .3s;background:var(--bwc-green)} .bwc-progress.bwc-near > span{background:var(--bwc-amber)} .bwc-progress.bwc-over > span{background:var(--bwc-red)} .bwc-progress-label{display:flex;justify-content:space-between;font-size:12px;color:var(--bwc-muted);margin-top:5px} .bwc-pill{display:inline-block;padding:3px 10px;border-radius:999px;font-size:11.5px;font-weight:600;letter-spacing:.03em} .bwc-pill-green{background:var(--bwc-green2);color:#3f7650}.bwc-pill-amber{background:var(--bwc-amber2);color:#8a6418}.bwc-pill-red{background:var(--bwc-red2);color:#9c3f3a} .bwc-pill-gold{background:var(--bwc-gold3);color:var(--bwc-gold)}.bwc-pill-blush{background:var(--bwc-blush);color:#a3625c} /* ---------- Cards & layout ---------- */ .bwc-card{background:var(--bwc-card);border:1px solid var(--bwc-line);border-radius:var(--bwc-r);padding:20px;box-shadow:var(--bwc-shadow);margin-bottom:16px} .bwc-card-head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px;margin-bottom:14px} .bwc-card-head h2,.bwc-card-head h3{display:flex;align-items:center;gap:8px} .bwc-grid{display:grid;gap:12px} .bwc-grid-2{grid-template-columns:repeat(2,minmax(0,1fr))} .bwc-grid-3{grid-template-columns:repeat(3,minmax(0,1fr))} .bwc-grid-4{grid-template-columns:repeat(4,minmax(0,1fr))} .bwc-field{display:flex;flex-direction:column;gap:5px;min-width:0} .bwc-field > span,.bwc-field-label{font-size:12px;font-weight:600;color:var(--bwc-muted);letter-spacing:.04em;text-transform:uppercase} .bwc-field.bwc-inline{flex-direction:row;align-items:center;gap:9px} .bwc-field.bwc-inline > span{text-transform:none;letter-spacing:0;font-size:13.5px;font-weight:500;color:var(--bwc-ink)} .bwc-note{background:var(--bwc-gold3);border:1px dashed var(--bwc-gold2);border-radius:var(--bwc-r-sm);padding:10px 14px;font-size:13px;color:#6f5b36;margin:0 0 14px} .bwc-note.bwc-note-blush{background:var(--bwc-blush);border-color:var(--bwc-blush3);color:#8b4f49} .bwc-divider{border:0;border-top:1px solid var(--bwc-line);margin:14px 0} /* ---------- Buttons ---------- */ .bwc-btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;padding:9px 16px;border-radius:999px;border:1px solid var(--bwc-line);background:#fff;font-weight:600;font-size:13.5px;transition:all .15s;color:var(--bwc-ink);line-height:1.2;min-height:38px;text-decoration:none} .bwc-btn:hover{border-color:var(--bwc-gold);box-shadow:var(--bwc-shadow-sm)} .bwc-btn-primary{background:var(--bwc-ink);color:#fff;border-color:var(--bwc-ink)} .bwc-btn-primary:hover{background:#221d1a} .bwc-btn-gold{background:var(--bwc-gold);color:#fff;border-color:var(--bwc-gold)} .bwc-btn-gold:hover{background:#a8864d} .bwc-btn-ghost{background:transparent} .bwc-btn-danger{color:var(--bwc-red)} .bwc-btn-danger:hover{border-color:var(--bwc-red);background:var(--bwc-red2)} .bwc-btn-sm{padding:6px 12px;font-size:12.5px;min-height:32px} .bwc-btn-row{display:flex;flex-wrap:wrap;gap:8px} .bwc-stepper{display:flex;align-items:center;border:1px solid var(--bwc-line);border-radius:12px;background:#fff;overflow:hidden} .bwc-stepper button{border:0;background:var(--bwc-gold3);width:40px;min-height:40px;font-size:20px;color:var(--bwc-gold);font-weight:700;flex:none} .bwc-stepper button:hover{background:var(--bwc-gold2)} .bwc-stepper input{border:0!important;border-radius:0!important;text-align:center;font-weight:700;font-size:18px;min-width:0;font-family:var(--bwc-serif)} .bwc-guest-hero{display:flex;flex-wrap:wrap;align-items:center;gap:18px;background:linear-gradient(135deg,#fff 0%,var(--bwc-blush) 100%);border:1px solid var(--bwc-blush3);border-radius:var(--bwc-r);padding:16px 20px;margin-bottom:14px} .bwc-guest-hero .bwc-guest-big{font-family:var(--bwc-serif);font-size:44px;font-weight:600;line-height:1;color:var(--bwc-ink)} .bwc-guest-hero .bwc-stepper{width:170px} /* ---------- Tooltip ---------- */ .bwc-tip{display:inline-flex;align-items:center;justify-content:center;width:17px;height:17px;border-radius:50%;background:var(--bwc-gold2);color:#7a5f2d;font-size:11px;font-weight:700;cursor:help;position:relative;vertical-align:middle;margin-left:5px;flex:none} .bwc-tip::after{content:attr(data-tip);position:absolute;left:50%;bottom:calc(100% + 8px);transform:translateX(-50%);background:var(--bwc-ink);color:#fff;font-size:12px;font-weight:400;line-height:1.4;padding:8px 10px;border-radius:8px;width:230px;text-align:left;opacity:0;pointer-events:none;transition:opacity .15s;z-index:20;white-space:normal} .bwc-tip:hover::after,.bwc-tip:focus::after,.bwc-tip:focus-visible::after{opacity:1} /* ---------- Accordion & line items ---------- */ .bwc-cat{border:1px solid var(--bwc-line);border-radius:var(--bwc-r);background:var(--bwc-card);margin-bottom:12px;box-shadow:var(--bwc-shadow-sm);overflow:hidden} .bwc-cat-head{display:flex;align-items:center;gap:12px;width:100%;border:0;background:#fff;padding:14px 18px;text-align:left;transition:background .15s} .bwc-cat-head:hover{background:var(--bwc-gold3)} .bwc-cat-letter{width:34px;height:34px;border-radius:50%;background:var(--bwc-blush);color:#9a5a54;font-family:var(--bwc-serif);font-weight:700;font-size:16px;display:flex;align-items:center;justify-content:center;flex:none} .bwc-cat-title{flex:1;min-width:0;display:flex;flex-direction:column} .bwc-cat-title strong{font-family:var(--bwc-serif);font-size:19px;font-weight:600} .bwc-cat-title small{color:var(--bwc-muted);font-size:12.5px} .bwc-cat-total{font-family:var(--bwc-serif);font-size:20px;font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap} .bwc-chev{width:22px;height:22px;flex:none;transition:transform .2s;color:var(--bwc-gold)} .bwc-cat-head[aria-expanded=true] .bwc-chev{transform:rotate(180deg)} .bwc-cat-body{padding:6px 14px 14px;border-top:1px solid var(--bwc-line2);background:#fdfbf7} .bwc-item{border:1px solid var(--bwc-line2);border-radius:14px;background:#fff;margin:8px 0;transition:opacity .15s,border-color .15s} .bwc-item.bwc-off{opacity:.62;background:#fcfaf6} .bwc-item.bwc-excluded{border-style:dashed} .bwc-item-main{display:grid;grid-template-columns:26px minmax(150px,1.6fr) 168px 118px 118px 118px 84px;gap:10px;align-items:center;padding:10px 12px} .bwc-item-main .bwc-field > span{font-size:10.5px} .bwc-item-name{display:flex;flex-direction:column;gap:2px;min-width:0} .bwc-item-name > span:first-child{font-weight:600;font-size:14px;display:flex;align-items:center;flex-wrap:wrap;gap:2px 4px} .bwc-item-name small{color:var(--bwc-muted);font-size:12px} .bwc-item-name input[type=text]{min-height:34px;padding:5px 8px;font-weight:600} .bwc-qtywrap{display:flex;align-items:center;gap:6px} .bwc-qtywrap .bwc-mult{font-size:12px;color:var(--bwc-muted);white-space:nowrap} .bwc-qtywrap input{min-width:0} .bwc-sub{text-align:right} .bwc-sub strong{font-family:var(--bwc-serif);font-size:19px;font-variant-numeric:tabular-nums;display:block} .bwc-more{border:1px solid var(--bwc-line);background:#fff;border-radius:999px;padding:6px 10px;font-size:12px;font-weight:600;color:var(--bwc-muted)} .bwc-more[aria-expanded=true]{background:var(--bwc-ink);color:#fff;border-color:var(--bwc-ink)} .bwc-item-details{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;padding:4px 12px 12px;border-top:1px dashed var(--bwc-line2);margin-top:2px;padding-top:12px} .bwc-item-details .bwc-wide{grid-column:span 2} .bwc-badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;font-size:10.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;line-height:1.4} .bwc-badge-excl{background:var(--bwc-gold3);color:var(--bwc-gold)} .bwc-badge-hotel{background:var(--bwc-blush);color:#9a5a54} .bwc-badge-custom{background:var(--bwc-green2);color:#3f7650} .bwc-cat-tools{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end;padding:8px 0 2px} /* ---------- Tables ---------- */ .bwc-table{width:100%;border-collapse:collapse;font-size:13.5px;margin:0} .bwc-table th,.bwc-table td{padding:9px 10px;border-bottom:1px solid var(--bwc-line2);text-align:left;vertical-align:middle;background:transparent} .bwc-table th{font-size:11px;letter-spacing:.1em;text-transform:uppercase;color:var(--bwc-muted);font-weight:700} .bwc-table td.bwc-num,.bwc-table th.bwc-num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap} .bwc-table tr.bwc-total td{font-weight:700;font-family:var(--bwc-serif);font-size:16px;border-top:2px solid var(--bwc-gold2)} .bwc-table-wrap{overflow-x:auto} .bwc-swatch{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:7px;vertical-align:-1px} /* ---------- Dashboard ---------- */ .bwc-kpis{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px} .bwc-kpi{background:#fff;border:1px solid var(--bwc-line);border-radius:14px;padding:12px 14px} .bwc-kpi .bwc-stat-l{font-size:10.5px} .bwc-kpi .bwc-stat-v{font-size:21px} .bwc-chart-wrap{display:grid;grid-template-columns:260px minmax(0,1fr);gap:20px;align-items:center} .bwc-donut{width:100%;max-width:260px;margin:0 auto;display:block} .bwc-legend{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px 14px;font-size:13px} .bwc-legend li{display:flex;justify-content:space-between;gap:8px;border-bottom:1px dotted var(--bwc-line);padding:4px 0} .bwc-legend li span:last-child{font-variant-numeric:tabular-nums;white-space:nowrap} .bwc-sens{width:100%;height:auto;display:block} /* ---------- Scenarios ---------- */ .bwc-scenario{border:1px solid var(--bwc-line);border-radius:var(--bwc-r);background:#fff;padding:16px 18px;margin-bottom:12px} .bwc-scenario-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px} .bwc-scenario-grid{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;align-items:stretch} .bwc-opt{border:1px solid var(--bwc-line2);border-radius:14px;padding:12px 14px;background:var(--bwc-ivory);display:flex;flex-direction:column;gap:6px} .bwc-opt.bwc-opt-current{border-color:var(--bwc-gold2);background:var(--bwc-gold3)} .bwc-opt .bwc-opt-l{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--bwc-muted);font-weight:700} .bwc-opt .bwc-opt-v{font-family:var(--bwc-serif);font-size:24px;font-weight:600;font-variant-numeric:tabular-nums} .bwc-opt .bwc-opt-d{font-size:12.5px;color:var(--bwc-muted)} .bwc-diff{display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;gap:6px} .bwc-diff .bwc-opt-v.bwc-pos{color:var(--bwc-green)}.bwc-diff .bwc-opt-v.bwc-neg{color:var(--bwc-red)} /* ---------- Checklist & vendors ---------- */ .bwc-task{display:grid;grid-template-columns:26px minmax(160px,2fr) 150px 140px minmax(140px,2fr) 40px;gap:10px;align-items:center;padding:10px 12px;border:1px solid var(--bwc-line2);border-radius:14px;background:#fff;margin:8px 0} .bwc-task.bwc-done .bwc-task-name{text-decoration:line-through;color:var(--bwc-muted)} .bwc-task-name{font-weight:600} .bwc-task-name input{font-weight:600;min-height:36px} .bwc-icon-btn{border:1px solid var(--bwc-line);background:#fff;border-radius:50%;width:34px;height:34px;display:inline-flex;align-items:center;justify-content:center;color:var(--bwc-muted);font-size:16px;flex:none} .bwc-icon-btn:hover{border-color:var(--bwc-red);color:var(--bwc-red);background:var(--bwc-red2)} .bwc-vendor{border:1px solid var(--bwc-line);border-radius:var(--bwc-r);background:#fff;padding:16px;margin-bottom:12px} .bwc-vendor-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-bottom:10px} .bwc-vendor-head h3{font-size:20px} .bwc-empty{border:1px dashed var(--bwc-line);border-radius:var(--bwc-r);padding:28px 20px;text-align:center;color:var(--bwc-muted);background:#fff} /* ---------- Toast ---------- */ .bwc-toast{position:fixed;left:50%;bottom:26px;transform:translate(-50%,20px);background:var(--bwc-ink);color:#fff;padding:11px 18px;border-radius:999px;font-size:13.5px;box-shadow:0 10px 30px rgba(0,0,0,.2);opacity:0;pointer-events:none;transition:all .25s;z-index:9999;max-width:90vw;text-align:center} .bwc-toast.bwc-show{opacity:1;transform:translate(-50%,0)} .bwc-toast.bwc-toast-err{background:var(--bwc-red)} /* ---------- Print area (hidden on screen) ---------- */ .bwc-print{display:none} /* ---------- Responsive ---------- */ @media (max-width:1024px){ .bwc-item-main{grid-template-columns:26px minmax(140px,1.4fr) 140px 110px 110px 110px 80px} .bwc-kpis{grid-template-columns:repeat(3,minmax(0,1fr))} .bwc-task{grid-template-columns:26px minmax(140px,2fr) 140px 130px minmax(120px,1.5fr) 40px} } @media (max-width:860px){ .bwc{padding:14px;border-radius:18px} .bwc-brand h1{font-size:28px} .bwc-grid-3,.bwc-grid-4{grid-template-columns:repeat(2,minmax(0,1fr))} .bwc-sticky-row{grid-template-columns:repeat(2,minmax(0,1fr))} .bwc-item-main{grid-template-columns:26px minmax(0,1fr) 84px;grid-template-areas:"chk name more" "mod mod mod" "qty qty price" "sub sub sub"} .bwc-item-main > .bwc-check{grid-area:chk}.bwc-item-main > .bwc-item-name{grid-area:name}.bwc-item-main > .bwc-more{grid-area:more} .bwc-item-main > .bwc-f-model{grid-area:mod}.bwc-item-main > .bwc-f-qty{grid-area:qty}.bwc-item-main > .bwc-f-price{grid-area:price}.bwc-item-main > .bwc-sub{grid-area:sub;text-align:left;display:flex;justify-content:space-between;align-items:center;border-top:1px dashed var(--bwc-line2);padding-top:8px} .bwc-item-details{grid-template-columns:repeat(2,minmax(0,1fr))} .bwc-chart-wrap{grid-template-columns:1fr} .bwc-legend{grid-template-columns:1fr} .bwc-scenario-grid{grid-template-columns:1fr} .bwc-task{grid-template-columns:26px minmax(0,1fr) 40px;grid-template-areas:"chk name del" "due who who" "notes notes notes"} .bwc-task > .bwc-check{grid-area:chk}.bwc-task > .bwc-task-name{grid-area:name}.bwc-task > .bwc-t-due{grid-area:due}.bwc-task > .bwc-t-who{grid-area:who}.bwc-task > .bwc-t-notes{grid-area:notes}.bwc-task > .bwc-icon-btn{grid-area:del} .bwc-kpis{grid-template-columns:repeat(2,minmax(0,1fr))} } @media (max-width:520px){ .bwc-grid-2,.bwc-grid-3,.bwc-grid-4{grid-template-columns:1fr} .bwc-item-details{grid-template-columns:1fr} .bwc-item-details .bwc-wide{grid-column:span 1} .bwc-stat .bwc-stat-v{font-size:19px} .bwc-guest-hero .bwc-guest-big{font-size:36px} .bwc-tip::after{width:190px} } /* ---------- Print ---------- */ @media print{ body.bwc-printing *{visibility:hidden!important} body.bwc-printing .bwc-print,body.bwc-printing .bwc-print *{visibility:visible!important} body.bwc-printing .bwc-print{display:block!important;position:absolute;left:0;top:0;width:100%;padding:0 10mm;background:#fff;color:#000;font-family:Georgia,"Times New Roman",serif;font-size:11.5pt;line-height:1.35} .bwc-print h1{font-size:22pt;margin:0 0 4pt;font-weight:600} .bwc-print h2{font-size:14pt;margin:14pt 0 5pt;border-bottom:1px solid #999;padding-bottom:3pt} .bwc-print p{margin:0 0 4pt} .bwc-print table{width:100%;border-collapse:collapse;margin:4pt 0 8pt;page-break-inside:auto} .bwc-print th,.bwc-print td{border-bottom:1px solid #ccc;padding:3pt 5pt;text-align:left;font-size:10pt;vertical-align:top} .bwc-print th{background:#f2f2f2;font-size:9pt;text-transform:uppercase;letter-spacing:.05em} .bwc-print td.n,.bwc-print th.n{text-align:right;white-space:nowrap} .bwc-print tr.t td{font-weight:700;border-top:2px solid #333} .bwc-print tr{page-break-inside:avoid} .bwc-print .pmeta{display:flex;flex-wrap:wrap;gap:4pt 18pt;font-size:10.5pt;margin-bottom:8pt} .bwc-print .pbox{border:1px solid #999;padding:6pt 9pt;margin:6pt 0} .bwc-print .pchk{display:inline-block;width:10pt;height:10pt;border:1px solid #333;margin-right:5pt;vertical-align:-1pt} .bwc-print .pchk.on{background:#333} } (function(){ 'use strict'; /* ============================================================ BOUTIQUE WEDDING BUDGET CALCULATOR — core script Everything lives inside this IIFE: no globals are created. ============================================================ */ var BWC_VERSION = 1; /* ------------------------------------------------------------ 1. TRANSLATION DICTIONARY (UI strings). Edit text here. Item / category / checklist names are bilingual inside their own definitions further below (section 2). ------------------------------------------------------------ */ var T = { en:{ kicker:"Private planning studio", title:"Wedding Budget Calculator", subtitle:"A boutique budgeting workspace for an intimate waterfront celebration.", lang_en:"English", lang_es:"Español", tab_overview:"Overview", tab_budget:"Budget Calculator", tab_compare:"Compare Options", tab_checklist:"Wedding Checklist", tab_vendors:"Vendors & Payments", tab_settings:"Settings", est_cost:"Estimated cost", target:"Target budget", remaining:"Remaining budget", over:"Amount over budget", per_guest:"Cost per guest", budget_used:"of budget used", guests_word:"guests", pct_used:"Budget used", status_under:"Under budget", status_near:"Approaching budget", status_over:"Over budget", dash_title:"Budget dashboard", deposits_req:"Deposits required", total_paid:"Total paid", outstanding:"Outstanding balance", contingency:"Contingency reserve", distribution:"Expense distribution by category", category_totals:"Category totals", cost_breakdown:"Cost breakdown", subtotal:"Subtotal", service_charges:"Service charges", svc_venue_line:"Venue service charge", svc_cat_line:"Catering service charge", svc_bev_line:"Beverage service charge", gratuities:"Vendor gratuities", admin_fees:"Administrative fees", taxes:"Sales tax", total:"Estimated total", items_enabled:"line items included", payments:"Deposits & payments", no_costs:"No expenses are included yet — open the Budget Calculator tab to start.", share:"Share", of_total:"of total", example_badge:"Preliminary planning estimates — not actual venue quotations. Every price is editable.", profile_title:"Wedding profile", couple:"Couple's names", date:"Wedding date", date_tbd:"Exact day to be determined", location:"Wedding location", venue:"Wedding venue", style:"Wedding style", style_elopement:"Elopement", style_intimate:"Intimate", style_boutique:"Boutique", style_traditional:"Traditional", style_luxury:"Luxury", style_destination:"Destination", guests:"Total guests", adults:"Adults", children:"Children", vendor_meals:"Vendors needing meals", party:"Wedding party members", target_budget:"Budget target", currency:"Currency", ceremony_setting:"Ceremony setting", indoor:"Indoor", outdoor:"Outdoor", reception_type:"Reception type", rec_seated:"Seated dinner", rec_buffet:"Buffet", rec_cocktail:"Cocktail-style", rec_brunch:"Brunch / lunch", events:"Wedding events", nights:"Hotel nights", hotel_payer:"Guest hotel rooms paid by", couple_pays:"The couple", guests_pay:"Each guest", seats_per_table:"Seats per table", guest_note:"Total guests always equals adults + children. Edit any of the three and the others stay consistent.", guest_hero:"Guest count", tables_word:"tables", budget_title:"Budget line items", budget_intro:"Tick a box to include an expense. Choose how each one is priced, then edit quantity and unit price. Open Details for vendor, notes, tax, service charge and payments.", include:"Include", pricing:"Pricing model", qty:"Quantity", unit_price:"Unit price", pct_label:"Percentage", subtotal_l:"Subtotal", vendor:"Vendor", notes:"Notes", taxable:"Taxable", svc:"Service charge applies", svc_none:"None", svc_venue:"Venue", svc_catering:"Catering", svc_beverage:"Beverage", deposit:"Deposit required", paid:"Amount paid", balance:"Remaining balance", details:"Details", delete:"Delete", add_custom:"Add custom expense", custom_name:"New custom expense", expense_name:"Expense name", excl_badge:"Choose one", excl_tip:"Only one option in this group can be included at a time — selecting this one unselects the others, so nothing is double-counted.", hotel_badge:"Guests pay", hotel_tip:"Excluded from the budget because guests pay their own rooms (change this in the Wedding profile).", pct_of:"Percent of", base_all:"All other included expenses", expand_all:"Expand all", collapse_all:"Collapse all", empty_cat:"No expenses included in this category yet.", included_count:"included", m_guests:"guests", m_adults:"adults", m_children:"children", m_vendors:"vendor meals", m_tables:"tables", m_nights:"nights", q_hours:"Hours", q_days:"Days", q_rooms:"Rooms", q_items:"Items", q_people:"People", q_qty:"Quantity", q_units:"Units", model_flat:"Flat fee", model_perGuest:"Per guest", model_perAdult:"Per adult", model_perChild:"Per child", model_perVendorMeal:"Per vendor meal", model_perHour:"Per hour", model_perDay:"Per day", model_perNight:"Per night", model_perTable:"Per table", model_perItem:"Per item", model_perPerson:"Per person", model_percent:"Percentage of…", model_custom:"Custom qty × price", compare_title:"What if?", compare_intro:"Each scenario is calculated with the same engine as your main budget, including taxes, service charges and contingency. Nothing changes until you choose Apply.", option_a:"Option A", option_b:"Option B", difference:"Difference", apply_a:"Apply A", apply_b:"Apply B", current:"current", saves:"Option B saves", costs_more:"Option B costs more", same:"No difference", sc_guests:"Guest count", sc_guests_d:"Adults change; children, vendors and every per-guest price stay as set.", sc_dinner:"Buffet vs plated dinner", sc_bar:"Beer & wine vs open bar", sc_music:"DJ vs live band", sc_flowers:"Basic vs premium flowers", sc_flowers_d:"Premium multiplies every Flowers & Decorations price by the factor below (planning assumption).", sc_photo:"Photographer only vs photo + video", sc_planner:"Full wedding planner vs day-of coordinator", premium_factor:"Premium flower multiplier", sensitivity:"Guest-count sensitivity", sens_note:"Estimated total at each guest count (children held constant; adults adjusted). The dashed line is your target budget.", apply_confirm:"Apply this option to your saved budget?", applied:"Scenario applied to the budget", guests_a:"Guests (A)", guests_b:"Guests (B)", buffet_o:"Buffet dinner", plated_o:"Plated dinner", beerwine_o:"Beer & wine package", openbar_o:"Open bar (per guest)", dj_o:"DJ", band_o:"Live band", basic_o:"Basic flowers (as set)", premium_o:"Premium flowers", photo_o:"Photographer only", photovideo_o:"Photography + videography", planner_o:"Full-service planner", dayof_o:"Day-of coordinator", cl_title:"Wedding checklist", cl_intro:"Track every milestone. Progress saves automatically with the rest of your budget.", cl_progress:"complete", cl_task:"Task", cl_due:"Due date", cl_who:"Responsible", who_bride:"Bride", who_groom:"Groom", who_both:"Both", who_planner:"Planner", cl_add:"Add task", cl_new:"New task", cl_print:"Print checklist", cl_done:"Done", cl_empty:"No tasks yet. Add your first task above.", cl_tasks:"tasks", v_title:"Vendors & payments", v_intro:"Keep contracts, contacts and payment schedules in one place.", v_add:"Add vendor", v_cat:"Category", v_company:"Company name", v_contact:"Contact person", v_email:"Email", v_phone:"Phone", v_web:"Website", v_amount:"Contract amount", v_dep_due:"Deposit due", v_dep_paid:"Deposit paid", v_final:"Final payment due date", v_paid:"Total paid", v_balance:"Remaining balance", v_signed:"Contract signed", v_link:"Linked budget line", v_nolink:"— Not linked —", v_sync:"Copy payments to budget line", v_synced:"Deposit and amount paid copied to the budget line", v_empty:"No vendors yet. Add your first vendor to track contracts, deposits and balances.", v_note:"Vendor contracts are tracked for reference and are never added to the budget total. The budget line items stay the single source of truth; use “Copy payments” to push a vendor's deposit and paid amounts into its linked line.", v_contracts:"Total contracted", v_new:"New vendor", v_line_cost:"Budget line estimate", s_title:"Settings", s_rates:"Taxes, service charges & fees", s_tax:"Sales tax %", s_venue_svc:"Venue service charge %", s_cat_svc:"Catering service charge %", s_bev_svc:"Beverage service charge %", s_grat:"Vendor gratuity %", s_grat_incl:"Service charges already include gratuity (do not add gratuity again)", s_admin:"Additional administrative fee % (on subtotal)", s_cont:"Contingency % (on everything above it)", s_tax_on_svc:"Apply sales tax to service charges (Florida usually taxes mandatory service charges)", s_rate_note:"Default rates are planning assumptions, not verified venue rates. Confirm every percentage against your signed venue, catering and bar agreements. Taxes apply only to lines marked Taxable; service charges only to lines assigned a Venue, Catering or Beverage service charge; gratuity applies to those same service-charge lines unless marked as already included.", s_formula:"Order of calculation: subtotal → service charges → gratuity → administrative fee → sales tax → contingency → estimated total. Contingency is applied once and never to itself.", s_data:"Save, export & print", btn_save:"Save budget", btn_reset:"Reset budget", btn_csv:"Export CSV", btn_json:"Export JSON", btn_import:"Import JSON", btn_print:"Print budget", storage_note:"Your data is saved automatically in this browser only (localStorage). It is not a cloud backup: clearing browser data, private windows or switching devices will lose it. Export a JSON file to keep a copy or move it to another device.", saved:"Budget saved in this browser", reset_confirm:"Reset everything to the example wedding? All edits, tasks and vendors in this browser will be lost.", reset_done:"Example wedding restored", import_ok:"Budget imported", import_err:"That file is not a valid wedding-budget JSON export.", storage_err:"Could not save: browser storage is unavailable or full. Export JSON to keep a copy.", s_language:"Language", s_general:"General", neg_fixed:"Negative values are not allowed — set to 0.", delete_confirm:"Delete this?", yes:"Yes", no:"No", p_report:"Wedding Budget Report", p_checklist:"Wedding Checklist", p_generated:"Generated", p_category:"Category", p_expense:"Expense", p_detail:"Pricing detail", p_amount:"Amount", p_tax_svc:"Taxes & service charges", p_deposits:"Deposits & balances", p_couple:"Couple", p_task:"Task", p_summary:"Summary", p_disclaimer:"Estimates are preliminary planning figures, not vendor quotations.", csv_cat:"Category", csv_item:"Expense", csv_model:"Pricing model", csv_mult:"Multiplier", csv_qty:"Quantity", csv_price:"Unit price", csv_sub:"Subtotal", csv_tax:"Taxable", csv_svc:"Service charge", csv_vendor:"Vendor", csv_dep:"Deposit", csv_paid:"Paid", csv_bal:"Balance", csv_notes:"Notes" }, es:{ kicker:"Estudio privado de planeación", title:"Calculadora de Presupuesto de Boda", subtitle:"Un espacio boutique para presupuestar una celebración íntima frente al mar.", lang_en:"English", lang_es:"Español", tab_overview:"Resumen", tab_budget:"Calculadora", tab_compare:"Comparar Opciones", tab_checklist:"Lista de Preparativos", tab_vendors:"Proveedores y Pagos", tab_settings:"Configuración", est_cost:"Costo estimado", target:"Presupuesto objetivo", remaining:"Presupuesto disponible", over:"Monto excedido", per_guest:"Costo por invitado", budget_used:"del presupuesto usado", guests_word:"invitados", pct_used:"Presupuesto usado", status_under:"Dentro del presupuesto", status_near:"Cerca del límite", status_over:"Presupuesto excedido", dash_title:"Panel de presupuesto", deposits_req:"Depósitos requeridos", total_paid:"Total pagado", outstanding:"Saldo pendiente", contingency:"Reserva de contingencia", distribution:"Distribución de gastos por categoría", category_totals:"Totales por categoría", cost_breakdown:"Desglose de costos", subtotal:"Subtotal", service_charges:"Cargos por servicio", svc_venue_line:"Cargo por servicio del lugar", svc_cat_line:"Cargo por servicio de catering", svc_bev_line:"Cargo por servicio de bebidas", gratuities:"Propinas a proveedores", admin_fees:"Cargos administrativos", taxes:"Impuesto a las ventas", total:"Total estimado", items_enabled:"gastos incluidos", payments:"Depósitos y pagos", no_costs:"Aún no hay gastos incluidos — abra la pestaña Calculadora para comenzar.", share:"Participación", of_total:"del total", example_badge:"Estimados preliminares de planeación — no son cotizaciones reales del lugar. Todos los precios son editables.", profile_title:"Perfil de la boda", couple:"Nombres de la pareja", date:"Fecha de la boda", date_tbd:"Día exacto por definir", location:"Ubicación de la boda", venue:"Lugar del evento", style:"Estilo de boda", style_elopement:"Fuga romántica (elopement)", style_intimate:"Íntima", style_boutique:"Boutique", style_traditional:"Tradicional", style_luxury:"De lujo", style_destination:"De destino", guests:"Total de invitados", adults:"Adultos", children:"Niños", vendor_meals:"Proveedores con comida", party:"Integrantes del cortejo", target_budget:"Presupuesto objetivo", currency:"Moneda", ceremony_setting:"Tipo de ceremonia", indoor:"Bajo techo", outdoor:"Al aire libre", reception_type:"Tipo de recepción", rec_seated:"Cena sentada", rec_buffet:"Bufé", rec_cocktail:"Estilo cóctel", rec_brunch:"Brunch / almuerzo", events:"Eventos de la boda", nights:"Noches de hotel", hotel_payer:"Habitaciones de invitados pagadas por", couple_pays:"La pareja", guests_pay:"Cada invitado", seats_per_table:"Puestos por mesa", guest_note:"El total de invitados siempre es adultos + niños. Edite cualquiera de los tres y los demás se mantienen consistentes.", guest_hero:"Número de invitados", tables_word:"mesas", budget_title:"Gastos de la boda", budget_intro:"Marque la casilla para incluir un gasto. Elija el modelo de precio y edite cantidad y precio unitario. Abra Detalles para proveedor, notas, impuesto, cargo por servicio y pagos.", include:"Incluir", pricing:"Modelo de precio", qty:"Cantidad", unit_price:"Precio unitario", pct_label:"Porcentaje", subtotal_l:"Subtotal", vendor:"Proveedor", notes:"Notas", taxable:"Gravable (IVA/impuesto)", svc:"Aplica cargo por servicio", svc_none:"Ninguno", svc_venue:"Lugar", svc_catering:"Catering", svc_beverage:"Bebidas", deposit:"Depósito requerido", paid:"Monto pagado", balance:"Saldo pendiente", details:"Detalles", delete:"Eliminar", add_custom:"Agregar gasto personalizado", custom_name:"Nuevo gasto personalizado", expense_name:"Nombre del gasto", excl_badge:"Elija uno", excl_tip:"Solo una opción de este grupo puede incluirse a la vez: al seleccionar esta se desmarcan las demás, así nada se cuenta dos veces.", hotel_badge:"Pagan los invitados", hotel_tip:"Excluido del presupuesto porque los invitados pagan sus propias habitaciones (cámbielo en el Perfil de la boda).", pct_of:"Porcentaje de", base_all:"Todos los demás gastos incluidos", expand_all:"Expandir todo", collapse_all:"Contraer todo", empty_cat:"Aún no hay gastos incluidos en esta categoría.", included_count:"incluidos", m_guests:"invitados", m_adults:"adultos", m_children:"niños", m_vendors:"comidas de proveedores", m_tables:"mesas", m_nights:"noches", q_hours:"Horas", q_days:"Días", q_rooms:"Habitaciones", q_items:"Artículos", q_people:"Personas", q_qty:"Cantidad", q_units:"Unidades", model_flat:"Tarifa fija", model_perGuest:"Por invitado", model_perAdult:"Por adulto", model_perChild:"Por niño", model_perVendorMeal:"Por comida de proveedor", model_perHour:"Por hora", model_perDay:"Por día", model_perNight:"Por noche", model_perTable:"Por mesa", model_perItem:"Por artículo", model_perPerson:"Por persona", model_percent:"Porcentaje de…", model_custom:"Cantidad × precio", compare_title:"¿Qué pasaría si…?", compare_intro:"Cada escenario se calcula con el mismo motor del presupuesto principal, incluyendo impuestos, cargos por servicio y contingencia. Nada cambia hasta que elija Aplicar.", option_a:"Opción A", option_b:"Opción B", difference:"Diferencia", apply_a:"Aplicar A", apply_b:"Aplicar B", current:"actual", saves:"La opción B ahorra", costs_more:"La opción B cuesta más", same:"Sin diferencia", sc_guests:"Número de invitados", sc_guests_d:"Cambian los adultos; niños, proveedores y todos los precios por invitado se mantienen.", sc_dinner:"Bufé vs cena servida a la mesa", sc_bar:"Cerveza y vino vs barra abierta", sc_music:"DJ vs banda en vivo", sc_flowers:"Flores básicas vs premium", sc_flowers_d:"Premium multiplica todos los precios de Flores y decoración por el factor indicado (supuesto de planeación).", sc_photo:"Solo fotógrafo vs foto + video", sc_planner:"Wedding planner completo vs coordinador del día", premium_factor:"Multiplicador de flores premium", sensitivity:"Sensibilidad al número de invitados", sens_note:"Total estimado para cada número de invitados (niños constantes; se ajustan los adultos). La línea punteada es su presupuesto objetivo.", apply_confirm:"¿Aplicar esta opción a su presupuesto guardado?", applied:"Escenario aplicado al presupuesto", guests_a:"Invitados (A)", guests_b:"Invitados (B)", buffet_o:"Cena tipo bufé", plated_o:"Cena servida a la mesa", beerwine_o:"Paquete de cerveza y vino", openbar_o:"Barra abierta (por invitado)", dj_o:"DJ", band_o:"Banda en vivo", basic_o:"Flores básicas (como están)", premium_o:"Flores premium", photo_o:"Solo fotógrafo", photovideo_o:"Fotografía + video", planner_o:"Wedding planner de servicio completo", dayof_o:"Coordinador del día", cl_title:"Lista de preparativos", cl_intro:"Siga cada hito. El progreso se guarda automáticamente junto con el presupuesto.", cl_progress:"completado", cl_task:"Tarea", cl_due:"Fecha límite", cl_who:"Responsable", who_bride:"Novia", who_groom:"Novio", who_both:"Ambos", who_planner:"Planner", cl_add:"Agregar tarea", cl_new:"Nueva tarea", cl_print:"Imprimir lista", cl_done:"Hecho", cl_empty:"Aún no hay tareas. Agregue la primera arriba.", cl_tasks:"tareas", v_title:"Proveedores y pagos", v_intro:"Contratos, contactos y calendario de pagos en un solo lugar.", v_add:"Agregar proveedor", v_cat:"Categoría", v_company:"Empresa", v_contact:"Persona de contacto", v_email:"Correo electrónico", v_phone:"Teléfono", v_web:"Sitio web", v_amount:"Valor del contrato", v_dep_due:"Depósito a pagar", v_dep_paid:"Depósito pagado", v_final:"Fecha del pago final", v_paid:"Total pagado", v_balance:"Saldo pendiente", v_signed:"Contrato firmado", v_link:"Gasto vinculado del presupuesto", v_nolink:"— Sin vincular —", v_sync:"Copiar pagos al gasto", v_synced:"Depósito y monto pagado copiados al gasto del presupuesto", v_empty:"Aún no hay proveedores. Agregue el primero para seguir contratos, depósitos y saldos.", v_note:"Los contratos de proveedores se registran solo como referencia y nunca se suman al total del presupuesto. Los gastos del presupuesto son la única fuente de verdad; use “Copiar pagos” para llevar el depósito y lo pagado al gasto vinculado.", v_contracts:"Total contratado", v_new:"Nuevo proveedor", v_line_cost:"Estimado del gasto", s_title:"Configuración", s_rates:"Impuestos, cargos por servicio y tarifas", s_tax:"Impuesto a las ventas %", s_venue_svc:"Cargo por servicio del lugar %", s_cat_svc:"Cargo por servicio de catering %", s_bev_svc:"Cargo por servicio de bebidas %", s_grat:"Propina a proveedores %", s_grat_incl:"Los cargos por servicio ya incluyen la propina (no sumarla de nuevo)", s_admin:"Cargo administrativo adicional % (sobre el subtotal)", s_cont:"Contingencia % (sobre todo lo anterior)", s_tax_on_svc:"Aplicar impuesto a los cargos por servicio (en Florida los cargos obligatorios suelen ser gravables)", s_rate_note:"Las tasas por defecto son supuestos de planeación, no tarifas verificadas del lugar. Confirme cada porcentaje con sus contratos firmados de lugar, catering y bar. El impuesto solo aplica a los gastos marcados como gravables; los cargos por servicio solo a los gastos asignados a Lugar, Catering o Bebidas; la propina aplica a esos mismos gastos salvo que esté marcada como ya incluida.", s_formula:"Orden de cálculo: subtotal → cargos por servicio → propina → cargo administrativo → impuesto → contingencia → total estimado. La contingencia se aplica una sola vez y nunca sobre sí misma.", s_data:"Guardar, exportar e imprimir", btn_save:"Guardar presupuesto", btn_reset:"Restablecer presupuesto", btn_csv:"Exportar CSV", btn_json:"Exportar JSON", btn_import:"Importar JSON", btn_print:"Imprimir presupuesto", storage_note:"Sus datos se guardan automáticamente solo en este navegador (localStorage). No es una copia de seguridad en la nube: si borra los datos del navegador, usa una ventana privada o cambia de dispositivo, se perderán. Exporte un archivo JSON para conservar una copia o llevarla a otro dispositivo.", saved:"Presupuesto guardado en este navegador", reset_confirm:"¿Restablecer todo a la boda de ejemplo? Se perderán todas las ediciones, tareas y proveedores de este navegador.", reset_done:"Boda de ejemplo restaurada", import_ok:"Presupuesto importado", import_err:"Ese archivo no es una exportación JSON válida de presupuesto de boda.", storage_err:"No se pudo guardar: el almacenamiento del navegador no está disponible o está lleno. Exporte un JSON para conservar una copia.", s_language:"Idioma", s_general:"General", neg_fixed:"No se permiten valores negativos; se ajustó a 0.", delete_confirm:"¿Eliminar esto?", yes:"Sí", no:"No", p_report:"Informe de Presupuesto de Boda", p_checklist:"Lista de Preparativos", p_generated:"Generado", p_category:"Categoría", p_expense:"Gasto", p_detail:"Detalle del precio", p_amount:"Monto", p_tax_svc:"Impuestos y cargos por servicio", p_deposits:"Depósitos y saldos", p_couple:"Pareja", p_task:"Tarea", p_summary:"Resumen", p_disclaimer:"Los estimados son cifras preliminares de planeación, no cotizaciones de proveedores.", csv_cat:"Categoría", csv_item:"Gasto", csv_model:"Modelo de precio", csv_mult:"Multiplicador", csv_qty:"Cantidad", csv_price:"Precio unitario", csv_sub:"Subtotal", csv_tax:"Gravable", csv_svc:"Cargo por servicio", csv_vendor:"Proveedor", csv_dep:"Depósito", csv_paid:"Pagado", csv_bal:"Saldo", csv_notes:"Notas" }}; /* ------------------------------------------------------------ 2. EXPENSE DEFINITIONS — the preloaded example wedding. Tuple: [id, English name, Spanish name, model, unit price, included?, options] options: qty, tax (taxable), svc ('venue'|'catering'|'beverage'), group (mutually exclusive group), hotel (guest-room line), pct (percentage base: 'all' or a category id), tip {en,es} Prices are preliminary planning estimates for a ~30-guest resort wedding; they are NOT venue quotations. ------------------------------------------------------------ */ var MODELS = ['flat','perGuest','perAdult','perChild','perVendorMeal','perHour','perDay','perNight','perTable','perItem','perPerson','percent','custom']; var SVC_TYPES = ['none','venue','catering','beverage']; var CAT_COLORS = ['#c9a56a','#d8a39e','#8fb39a','#b6a1c7','#e3b96f','#9fb9d0','#d49a7a','#a7c3b0','#c8b08a','#c29fa9','#9aa8bf','#d7c08a','#b9b9b9']; var CATS = [ {id:'venue', letter:'A', en:'Wedding Venue & Ceremony', es:'Lugar y ceremonia', items:[ ['venue_rental','Venue rental','Alquiler del lugar','flat',2000,true,{tax:true,svc:'venue',tip:{en:'Site / ceremony fee charged by the resort for the waterfront ceremony space.',es:'Tarifa del lugar que cobra el resort por el espacio de la ceremonia frente al mar.'}}], ['beach_permit','Beach ceremony permit','Permiso para ceremonia en la playa','flat',150,true,{}], ['ceremony_setup','Ceremony setup','Montaje de la ceremonia','flat',400,false,{tax:true,svc:'venue'}], ['reception_rental','Reception venue rental','Alquiler del salón de recepción','flat',1500,false,{tax:true,svc:'venue',tip:{en:'Only if the reception room is billed separately from food & beverage minimums.',es:'Solo si el salón se cobra aparte de los mínimos de comida y bebida.'}}], ['private_dining','Private dining room','Salón privado','flat',1000,false,{tax:true,svc:'venue'}], ['ceremony_arch','Ceremony arch','Arco ceremonial','flat',300,true,{tax:true}], ['ceremony_chairs','Ceremony chairs','Sillas para la ceremonia','perGuest',7,true,{tax:true,svc:'venue'}], ['chair_setup','Chair setup and breakdown','Montaje y desmontaje de sillas','flat',150,false,{}], ['aisle_decor','Aisle decorations','Decoración del pasillo','flat',300,false,{tax:true}], ['ceremony_sound','Ceremony sound system','Sistema de sonido para la ceremonia','flat',350,false,{}], ['microphones','Microphones','Micrófonos','perItem',50,false,{qty:2}], ['officiant','Officiant','Oficiante','flat',350,false,{group:'officiant'}], ['officiant_bilingual','Bilingual officiant','Oficiante bilingüe','flat',500,true,{group:'officiant',tip:{en:'An officiant who performs the ceremony in both English and Spanish.',es:'Oficiante que celebra la ceremonia en inglés y español.'}}], ['ceremony_musician','Ceremony musician','Músico para la ceremonia','flat',400,false,{group:'ceremony_music',tip:{en:'Also listed under Music & Entertainment — it can only be included in one place.',es:'También aparece en Música y entretenimiento; solo puede incluirse en un lugar.'}}], ['rehearsal','Wedding rehearsal','Ensayo de la boda','flat',200,false,{}], ['venue_coord','Venue coordination','Coordinación del lugar','flat',500,false,{tip:{en:'Resort-side coordinator fee if not already built into the venue package.',es:'Tarifa del coordinador del resort si no viene incluida en el paquete.'}}], ['event_insurance','Event insurance','Seguro del evento','flat',250,false,{group:'insurance',tip:{en:'Liability policy some venues require. Also listed under Legal as Wedding insurance — include one only.',es:'Póliza de responsabilidad que algunos lugares exigen. También aparece en Legal como Seguro de boda; incluya solo uno.'}}], ['weather_backup','Weather backup location','Lugar alterno por clima','flat',500,false,{tax:true,svc:'venue'}], ['tent','Tent rental','Alquiler de carpa','flat',1500,false,{tax:true}], ['tent_lighting','Tent lighting','Iluminación de la carpa','flat',400,false,{tax:true}], ['fans_heaters','Outdoor fans or heaters','Ventiladores o calefactores','perItem',75,false,{qty:2,tax:true}], ['setup_cleanup','Setup and cleanup fees','Tarifas de montaje y limpieza','flat',300,false,{svc:'venue'}] ]}, {id:'food', letter:'B', en:'Food & Catering', es:'Comida y catering', items:[ ['cocktail_food','Cocktail hour food','Comida de la hora del cóctel','perGuest',24,true,{tax:true,svc:'catering'}], ['passed_apps','Passed appetizers per guest','Pasabocas servidos por invitado','perGuest',12,false,{tax:true,svc:'catering'}], ['grazing','Grazing table','Mesa de picoteo','flat',600,false,{tax:true,svc:'catering'}], ['buffet','Buffet dinner','Cena tipo bufé','perGuest',78,false,{tax:true,svc:'catering',group:'dinner'}], ['plated','Plated dinner','Cena servida a la mesa','perGuest',85,true,{tax:true,svc:'catering',group:'dinner'}], ['family_style','Family-style dinner','Cena estilo familiar','perGuest',82,false,{tax:true,svc:'catering',group:'dinner'}], ['kids_meals','Children’s meals','Menú infantil','perChild',35,true,{tax:true,svc:'catering'}], ['vendor_meals','Vendor meals','Comidas para proveedores','perVendorMeal',45,true,{tax:true,svc:'catering',tip:{en:'Photographer, DJ, planner… count them in the Wedding profile.',es:'Fotógrafo, DJ, planner… cuéntelos en el Perfil de la boda.'}}], ['late_night','Late-night snacks','Pasabocas de medianoche','perGuest',12,false,{tax:true,svc:'catering'}], ['dessert','Dessert station','Estación de postres','perGuest',10,false,{tax:true,svc:'catering'}], ['coffee_tea','Coffee and tea','Café y té','perGuest',5,false,{tax:true,svc:'catering'}], ['cake','Wedding cake','Torta de bodas','flat',450,true,{tax:true,tip:{en:'Outside bakery — no resort service charge assumed.',es:'Pastelería externa; no se asume cargo por servicio del resort.'}}], ['cake_cutting','Cake cutting fee','Cargo por corte de torta','perGuest',3,true,{tax:true,svc:'catering'}], ['catering_staff','Catering staffing','Personal de catering','perHour',35,false,{qty:5}], ['chef','Chef fees','Honorarios del chef','flat',300,false,{}], ['bartender_fees','Bartender fees','Honorarios de bartender','perHour',45,false,{qty:4,group:'bartender',tip:{en:'Also listed under Alcohol & Beverages as Bartender labor — include one only.',es:'También aparece en Licor y bebidas como Mano de obra de bartender; incluya solo uno.'}}], ['outside_catering','Outside catering fees','Cargo por catering externo','flat',500,false,{}], ['catering_delivery','Catering delivery and setup','Entrega y montaje del catering','flat',250,false,{}] ]}, {id:'bar', letter:'C', en:'Alcohol & Beverages', es:'Licor y bebidas', items:[ ['open_bar_guest','Open bar per guest','Barra abierta por invitado','perGuest',62,false,{tax:true,svc:'beverage',group:'bar'}], ['open_bar_hour','Open bar per hour','Barra abierta por hora','perHour',600,false,{qty:4,tax:true,svc:'beverage',group:'bar'}], ['beer_wine','Beer and wine package','Paquete de cerveza y vino','perGuest',38,true,{tax:true,svc:'beverage',group:'bar'}], ['premium_upgrade','Premium liquor upgrade','Mejora a licor premium','perGuest',15,false,{tax:true,svc:'beverage'}], ['champagne','Champagne toast','Brindis con champaña','perGuest',7,true,{tax:true,svc:'beverage'}], ['signature','Signature cocktails','Cócteles de autor','perGuest',12,false,{tax:true,svc:'beverage'}], ['nonalc','Nonalcoholic beverages','Bebidas sin alcohol','perGuest',6,false,{tax:true,svc:'beverage'}], ['mocktail','Mocktail station','Estación de mocktails','flat',350,false,{tax:true,svc:'beverage'}], ['bev_service_fee','Beverage service fee','Cargo por servicio de bebidas','flat',150,false,{tip:{en:'Use only for a flat bar fee. Percentage service charges belong in Settings.',es:'Úselo solo para una tarifa fija del bar. Los cargos porcentuales van en Configuración.'}}], ['corkage','Corkage fees','Descorche','perItem',25,false,{qty:6}], ['bartender_labor','Bartender labor','Mano de obra de bartender','perHour',45,false,{qty:4,group:'bartender'}], ['bar_setup','Bar setup','Montaje del bar','flat',200,false,{}] ]}, {id:'flowers', letter:'D', en:'Flowers & Decorations', es:'Flores y decoración', items:[ ['bouquet','Bridal bouquet','Ramo de novia','flat',250,true,{tax:true}], ['bm_bouquets','Bridesmaids’ bouquets','Ramos de las damas de honor','perPerson',85,false,{qty:2,tax:true}], ['groom_bout','Groom boutonniere','Boutonnier del novio','flat',35,true,{tax:true}], ['gm_bouts','Groomsmen boutonnieres','Boutonniers de los caballeros de honor','perPerson',25,false,{qty:2,tax:true}], ['arch_flowers','Ceremony arch flowers','Flores del arco ceremonial','flat',400,true,{tax:true}], ['aisle_flowers','Aisle flowers','Flores del pasillo','flat',300,false,{tax:true}], ['centerpieces','Reception centerpieces','Centros de mesa','perTable',95,true,{tax:true,tip:{en:'Tables = guests ÷ seats per table (Settings), rounded up.',es:'Mesas = invitados ÷ puestos por mesa (Configuración), redondeado hacia arriba.'}}], ['sweetheart','Sweetheart table flowers','Flores de la mesa de los novios','flat',150,false,{tax:true}], ['welcome_table','Welcome table decorations','Decoración de la mesa de bienvenida','flat',120,false,{tax:true}], ['candles','Candles','Velas','flat',100,false,{tax:true}], ['lanterns','Lanterns','Faroles','flat',150,false,{tax:true}], ['linens','Table linens','Mantelería','perTable',25,false,{tax:true}], ['chair_covers','Chair covers','Forros para sillas','perGuest',4,false,{tax:true}], ['deco_lighting','Decorative lighting','Iluminación decorativa','flat',400,false,{tax:true}], ['string_lights','String lights','Luces de guirnalda','flat',300,false,{tax:true}], ['floral_labor','Floral installation labor','Mano de obra de instalación floral','flat',300,false,{}], ['flower_delivery','Delivery and setup','Entrega y montaje','flat',150,true,{}], ['deco_removal','Decoration removal','Retiro de la decoración','flat',100,false,{}] ]}, {id:'photo', letter:'E', en:'Photography & Videography', es:'Fotografía y video', items:[ ['photographer','Wedding photographer','Fotógrafo de bodas','flat',2200,true,{tip:{en:'Typical 6-hour boutique package. Add Extra coverage hours below if needed.',es:'Paquete boutique típico de 6 horas. Agregue horas extra abajo si las necesita.'}}], ['second_photog','Additional photographer','Segundo fotógrafo','perHour',150,false,{qty:6}], ['engagement','Engagement photos','Fotos de compromiso','flat',400,false,{}], ['ceremony_photo','Ceremony photography','Fotografía de la ceremonia','flat',800,false,{tip:{en:'Only if booked separately from a full-day photographer package.',es:'Solo si se contrata aparte del paquete completo del fotógrafo.'}}], ['reception_photo','Reception photography','Fotografía de la recepción','flat',1200,false,{}], ['videographer','Wedding videographer','Videógrafo de bodas','flat',2200,false,{}], ['drone','Drone coverage','Cobertura con dron','flat',400,false,{}], ['highlight_film','Highlight film','Video resumen (highlight)','flat',600,false,{}], ['full_ceremony_video','Full ceremony video','Video completo de la ceremonia','flat',300,false,{}], ['album','Wedding album','Álbum de bodas','flat',500,false,{tax:true}], ['prints','Printed photos','Fotos impresas','flat',200,false,{tax:true}], ['digital_gallery','Digital photo gallery','Galería digital de fotos','flat',150,false,{}], ['extra_hours','Extra coverage hours','Horas extra de cobertura','perHour',300,false,{qty:1}], ['photo_travel','Travel fees','Gastos de desplazamiento','flat',200,false,{}] ]}, {id:'music', letter:'F', en:'Music & Entertainment', es:'Música y entretenimiento', items:[ ['ceremony_musician_m','Ceremony musician','Músico para la ceremonia','flat',400,false,{group:'ceremony_music'}], ['cocktail_musician','Cocktail hour musician','Músico para la hora del cóctel','flat',450,false,{}], ['dj','DJ','DJ','flat',1000,true,{group:'music_main',tip:{en:'DJ and Live band are mutually exclusive here. Enable both only via a custom expense if you truly want both.',es:'DJ y Banda en vivo son excluyentes aquí. Si de verdad quiere ambos, agregue un gasto personalizado.'}}], ['band','Live band','Banda en vivo','flat',3500,false,{group:'music_main'}], ['colombian_music','Colombian music entertainment','Música colombiana (vallenato, cumbia, salsa)','flat',800,false,{}], ['italian_music','Italian music entertainment','Música italiana','flat',800,false,{}], ['sax','Saxophonist','Saxofonista','flat',500,false,{}], ['violin','Violinist','Violinista','flat',450,false,{}], ['dance_floor','Dance floor rental','Alquiler de pista de baile','flat',600,false,{tax:true}], ['uplighting','Uplighting','Iluminación ambiental (uplighting)','flat',350,false,{tax:true}], ['audio_equipment','Audio equipment','Equipo de sonido','flat',400,false,{tax:true}], ['wireless_mics','Wireless microphones','Micrófonos inalámbricos','perItem',50,false,{qty:2}], ['photo_booth','Photo booth','Cabina de fotos','flat',800,false,{}], ['ent_overtime','Entertainment overtime','Horas extra de entretenimiento','perHour',200,false,{qty:1}] ]}, {id:'attire', letter:'G', en:'Wedding Attire & Beauty', es:'Vestuario y belleza', items:[ ['dress','Wedding dress','Vestido de novia','flat',1200,true,{tax:true}], ['alterations','Dress alterations','Arreglos del vestido','flat',250,true,{}], ['bridal_shoes','Bridal shoes','Zapatos de la novia','flat',150,false,{tax:true}], ['bridal_accessories','Bridal accessories','Accesorios de la novia','flat',150,false,{tax:true}], ['groom_suit','Groom suit or tuxedo','Traje o esmoquin del novio','flat',500,true,{tax:true}], ['groom_shoes','Groom shoes','Zapatos del novio','flat',120,false,{tax:true}], ['groom_accessories','Groom accessories','Accesorios del novio','flat',100,false,{tax:true}], ['hair','Hair styling','Peinado','flat',150,true,{}], ['makeup','Makeup','Maquillaje','flat',150,true,{}], ['trials','Hair and makeup trials','Pruebas de peinado y maquillaje','flat',200,false,{}], ['bm_hair','Bridesmaids’ hair','Peinado de las damas de honor','perPerson',90,false,{qty:2}], ['bm_makeup','Bridesmaids’ makeup','Maquillaje de las damas de honor','perPerson',90,false,{qty:2}], ['groom_grooming','Groom grooming','Arreglo del novio (barbería)','flat',60,false,{}], ['rings','Wedding rings','Argollas de matrimonio','flat',1200,false,{tax:true}], ['engraving','Ring engraving','Grabado de las argollas','flat',60,false,{}], ['jewelry','Jewelry','Joyería','flat',300,false,{tax:true}], ['preservation','Dry cleaning or preservation','Lavado en seco o preservación','flat',250,false,{}] ]}, {id:'stationery', letter:'H', en:'Invitations & Stationery', es:'Invitaciones y papelería', items:[ ['save_dates','Save-the-date cards','Tarjetas “reserva la fecha”','perItem',3,false,{qty:20,tax:true}], ['invitations','Invitations','Invitaciones','perItem',6,false,{qty:20,tax:true,tip:{en:'Quantity = households, not guests.',es:'Cantidad = hogares, no invitados.'}}], ['digital_invites','Digital invitations','Invitaciones digitales','flat',50,false,{}], ['rsvp_site','RSVP website','Sitio web para confirmar asistencia (RSVP)','flat',60,false,{}], ['rsvp_cards','Printed RSVP cards','Tarjetas RSVP impresas','perItem',1.5,false,{qty:20,tax:true}], ['postage','Postage','Franqueo / estampillas','perItem',0.78,false,{qty:40}], ['programs','Wedding programs','Programas de la ceremonia','perGuest',2,false,{tax:true,group:'programs'}], ['bilingual_programs','Bilingual ceremony programs','Programas bilingües de la ceremonia','perGuest',3,true,{tax:true,group:'programs'}], ['seating_chart','Seating chart','Plano de mesas','flat',120,false,{tax:true}], ['place_cards','Place cards','Tarjetas de puesto','perGuest',2,false,{tax:true}], ['table_numbers','Table numbers','Números de mesa','perTable',8,false,{tax:true}], ['welcome_sign','Welcome sign','Letrero de bienvenida','flat',120,false,{tax:true}], ['menus','Menus','Menús impresos','perGuest',2.5,false,{tax:true}], ['thank_you','Thank-you cards','Tarjetas de agradecimiento','perItem',2,false,{qty:20,tax:true}], ['favors','Wedding favors','Recordatorios para invitados','perGuest',6,false,{tax:true}] ]}, {id:'planning', letter:'I', en:'Wedding Planning & Coordination', es:'Planeación y coordinación', items:[ ['full_planner','Full-service wedding planner','Wedding planner de servicio completo','flat',4500,false,{group:'planner'}], ['partial_planner','Partial wedding planner','Wedding planner parcial','flat',2500,false,{group:'planner'}], ['day_of','Day-of coordinator','Coordinador(a) del día','flat',1000,true,{group:'planner'}], ['rehearsal_coord','Rehearsal coordinator','Coordinador(a) del ensayo','flat',300,false,{}], ['vendor_mgmt','Vendor management','Gestión de proveedores','flat',500,false,{}], ['setup_supervision','Event setup supervision','Supervisión del montaje','flat',300,false,{}], ['timeline','Wedding timeline preparation','Preparación del cronograma','flat',150,false,{}], ['bilingual_coord','Bilingual coordination','Coordinación bilingüe','flat',300,false,{}], ['assistant_coord','Assistant coordinator','Coordinador(a) asistente','flat',250,false,{}], ['planning_software','Planning software','Software de planeación','flat',50,false,{}], ['vendor_gratuities','Vendor gratuities','Propinas a proveedores','flat',400,false,{tip:{en:'Flat cash tips. If you use the Vendor gratuity % in Settings, leave this off to avoid double-counting.',es:'Propinas fijas en efectivo. Si usa la Propina % en Configuración, deje esto apagado para no contar doble.'}}] ]}, {id:'travel', letter:'J', en:'Transportation & Accommodation', es:'Transporte y alojamiento', items:[ ['bridal_suite','Bridal suite','Suite nupcial','perNight',450,false,{qty:1,tax:true}], ['groom_suite','Groom suite','Suite del novio','perNight',300,false,{qty:1,tax:true}], ['couple_hotel','Couple’s hotel accommodations','Alojamiento de la pareja','perNight',350,false,{qty:1,tax:true}], ['guest_rooms','Guest hotel rooms','Habitaciones para invitados','perNight',220,false,{qty:10,tax:true,hotel:true,tip:{en:'Quantity = rooms. Counted only when the couple pays for guest rooms (Wedding profile).',es:'Cantidad = habitaciones. Solo cuenta cuando la pareja paga las habitaciones (Perfil de la boda).'}}], ['room_block','Hotel room block commitment','Compromiso de bloque de habitaciones','flat',0,false,{hotel:true,tip:{en:'Attrition / guaranteed-room exposure if guests do not fill the block.',es:'Riesgo por habitaciones garantizadas si los invitados no llenan el bloque.'}}], ['airport_transfers','Airport transfers','Traslados al aeropuerto','perItem',80,false,{qty:2}], ['shuttle','Guest shuttle','Transporte para invitados','perHour',150,false,{qty:4}], ['limo','Limousine','Limusina','flat',600,false,{}], ['luxury_vehicle','Luxury vehicle','Vehículo de lujo','flat',400,false,{}], ['parking','Parking','Parqueadero','perItem',20,false,{qty:10}], ['valet','Valet parking','Valet parking','perGuest',12,false,{}], ['vendor_transport','Vendor transportation','Transporte de proveedores','flat',150,false,{}], ['travel_exp','Travel expenses','Gastos de viaje','flat',500,false,{}] ]}, {id:'legal', letter:'K', en:'Legal & Administrative', es:'Legal y administrativo', items:[ ['marriage_license','Marriage license','Licencia de matrimonio','flat',86,true,{tip:{en:'Florida fee is about $86 ($61 with a premarital course certificate). Verify with the Clerk of Court.',es:'En Florida cuesta cerca de $86 ($61 con certificado de curso prematrimonial). Verifique con el Clerk of Court.'}}], ['cert_copies','Certified marriage certificate copies','Copias certificadas del acta de matrimonio','perItem',10,true,{qty:3}], ['officiant_filing','Officiant filing fee','Tarifa de registro del oficiante','flat',25,false,{}], ['name_change','Name change expenses','Gastos de cambio de nombre','flat',150,false,{}], ['doc_translation','Document translation','Traducción de documentos','perItem',45,false,{qty:2,tip:{en:'Certified translations of Colombian documents (cédula, birth certificate) if required.',es:'Traducciones certificadas de documentos colombianos (cédula, registro civil) si se requieren.'}}], ['notary','Notary expenses','Gastos de notaría','perItem',10,false,{qty:2}], ['intl_docs','International document expenses','Gastos de documentos internacionales (apostilla)','flat',200,false,{}], ['wedding_insurance','Wedding insurance','Seguro de boda','flat',275,false,{group:'insurance'}] ]}, {id:'cultural', letter:'L', en:'Special Experiences & Cultural Traditions', es:'Experiencias especiales y tradiciones culturales', items:[ ['bilingual_ceremony','Bilingual English/Spanish ceremony','Ceremonia bilingüe inglés/español','flat',200,false,{tip:{en:'Extra for scripts, translation and rehearsal beyond the officiant fee.',es:'Costo extra por guiones, traducción y ensayo, además del oficiante.'}}], ['italian_traditions','Italian cultural traditions','Tradiciones italianas','flat',300,false,{}], ['colombian_traditions','Colombian cultural traditions','Tradiciones colombianas','flat',300,false,{}], ['hora_loca','Colombian hora loca entertainment','Hora loca','flat',900,false,{tip:{en:'Costumed performers, props, confetti and high-energy music for the late reception.',es:'Animadores disfrazados, accesorios, confeti y música de alta energía para el cierre de la fiesta.'}}], ['special_dances','Special dances','Bailes especiales','flat',200,false,{}], ['cultural_musicians','Cultural musicians','Músicos tradicionales','flat',700,false,{}], ['welcome_party','Welcome party','Fiesta de bienvenida','perGuest',35,false,{tax:true}], ['rehearsal_dinner','Rehearsal dinner','Cena de ensayo','perPerson',55,false,{qty:12,tax:true}], ['farewell_brunch','Farewell brunch','Brunch de despedida','perGuest',30,false,{tax:true}], ['welcome_bags','Welcome bags','Bolsas de bienvenida','perItem',25,false,{qty:15,tax:true}], ['personalized_gifts','Personalized gifts','Regalos personalizados','perItem',40,false,{qty:4,tax:true}], ['family_gifts','Family gifts','Regalos para la familia','perItem',75,false,{qty:4,tax:true}], ['special_experiences','Special guest experiences','Experiencias especiales para invitados','flat',500,false,{}] ]}, {id:'misc', letter:'M', en:'Miscellaneous & Contingency', es:'Varios y contingencia', items:[ ['emergency_kit','Emergency supplies','Kit de emergencia','flat',100,false,{tax:true}], ['weather_contingency','Weather contingency','Contingencia por clima','flat',300,false,{}], ['last_minute','Last-minute vendor charges','Cargos de último momento','flat',300,false,{}], ['vendor_overtime','Vendor overtime','Horas extra de proveedores','perHour',250,false,{qty:1}], ['additional_rentals','Additional rentals','Alquileres adicionales','flat',300,false,{tax:true}], ['unplanned','Unplanned expenses','Gastos imprevistos','flat',500,false,{}], ['contingency_line','General contingency reserve','Reserva general de contingencia','flat',750,false,{tip:{en:'A flat reserve. The Contingency % in Settings already adds a reserve; use one or the other.',es:'Reserva fija. La Contingencia % de Configuración ya agrega una reserva; use una u otra.'}}] ]} ]; /* Default checklist (bilingual). who: bride | groom | both | planner */ var CHECKLIST_DEFAULTS = [ ['confirm_date','Confirm wedding date','Confirmar la fecha de la boda','both'], ['confirm_venue','Confirm venue','Confirmar el lugar','both'], ['sign_venue','Sign venue contract','Firmar el contrato del lugar','both'], ['venue_deposit','Pay venue deposit','Pagar el depósito del lugar','groom'], ['guest_list','Finalize guest list','Cerrar la lista de invitados','both'], ['hire_planner','Hire wedding planner','Contratar wedding planner','both'], ['book_officiant','Book officiant','Reservar el oficiante','both'], ['license','Apply for marriage license','Solicitar la licencia de matrimonio','both'], ['book_photo','Book photographer','Reservar fotógrafo','bride'], ['book_video','Book videographer','Reservar videógrafo','bride'], ['catering','Select catering','Elegir el catering','both'], ['bar_pkg','Select bar package','Elegir el paquete de bar','groom'], ['cake_choose','Choose wedding cake','Elegir la torta de bodas','bride'], ['florist','Book florist','Reservar florista','bride'], ['music','Book DJ or musicians','Reservar DJ o músicos','groom'], ['attire','Purchase wedding attire','Comprar el vestuario de la boda','both'], ['beauty','Arrange hair and makeup','Reservar peinado y maquillaje','bride'], ['order_invites','Order invitations','Encargar las invitaciones','bride'], ['send_invites','Send invitations','Enviar las invitaciones','both'], ['rsvps','Track RSVPs','Hacer seguimiento a las confirmaciones (RSVP)','planner'], ['accommodations','Arrange accommodations','Organizar el alojamiento','groom'], ['vows','Prepare ceremony vows','Preparar los votos','both'], ['bilingual','Prepare bilingual ceremony','Preparar la ceremonia bilingüe','both'], ['seating','Create seating chart','Crear el plano de mesas','both'], ['menu','Finalize menu','Definir el menú final','both'], ['payments','Confirm vendor payments','Confirmar los pagos a proveedores','groom'], ['timeline','Confirm wedding timeline','Confirmar el cronograma de la boda','planner'], ['walkthrough','Complete final venue walkthrough','Hacer el recorrido final del lugar','both'] ]; /* Lookup maps built from the definitions */ var ITEM_DEF = {}, CAT_DEF = {}; CATS.forEach(function(c){ CAT_DEF[c.id]=c; c.items.forEach(function(d){ ITEM_DEF[d[0]]={cat:c.id,en:d[1],es:d[2],model:d[3],price:d[4],enabled:d[5],opts:d[6]||{}}; }); }); var CHECK_DEF = {}; CHECKLIST_DEFAULTS.forEach(function(d){ CHECK_DEF[d[0]]={en:d[1],es:d[2],who:d[3]}; }); /* Default state factory */ function defaultState(){ return { v:BWC_VERSION, profile:{ couple:'', date:'2026-12-01', dateTbd:true, location:'Clearwater Beach, Florida', venue:'Clearwater Beach Marriott Resort on Sand Key', style:'boutique', adults:30, children:0, vendorMeals:3, party:4, target:1500000, currency:'USD', ceremonyType:'outdoor', receptionType:'seated', events:1, hotelNights:2, hotelPayer:'guests', seatsPerTable:8 }, rates:{ tax:7, venueSvc:0, cateringSvc:24, beverageSvc:24, gratuity:0, gratIncluded:true, admin:0, contingency:5, taxOnSvc:true }, categories: CATS.map(function(c){ return { id:c.id, items:c.items.map(function(d){ var o=d[6]||{}; return { id:d[0], enabled:!!d[5], model:d[3], qty:o.qty!=null?o.qty:1, price:Math.round(d[4]*100), vendor:'', notes:'', taxable:!!o.tax, svc:o.svc||'none', pctOf:o.pct||'all', deposit:0, paid:0 }; }) }; }), checklist: CHECKLIST_DEFAULTS.map(function(d){ return { id:d[0], done:false, due:'', who:d[3], notes:'' }; }), vendors: [], scenario:{ guestsA:20, guestsB:30, premiumFactor:1.6 } }; } /* ------------------------------------------------------------ 3. SHARED HELPERS ------------------------------------------------------------ */ function esc(s){ return String(s==null?'':s).replace(/[&"']/g,function(c){ return {'&':'&','':'>','"':'"',"'":'''}[c]; }); } function num(v,min,max){ var n=parseFloat(v); if(!isFinite(n)) n=0; if(min!=null&&nmax) n=max; return n; } function intv(v,min,max){ return Math.round(num(v,min,max)); } function toCents(v){ return Math.round(num(v,0)*100); } function fromCents(c){ return (num(c)/100).toFixed(2); } function clone(o){ return JSON.parse(JSON.stringify(o)); } function tget(lang,k){ var d=T[lang]||T.en; var v=d[k]; if(v==null) v=T.en[k]; return v==null?k:v; } function uid(prefix){ return prefix+'_'+Date.now().toString(36)+'_'+Math.random().toString(36).slice(2,7); } function isOneOf(v,list){ return list.indexOf(v)>=0; } function str(v,max){ return typeof v==='string'? v.slice(0,max||300) : ''; } function todayISO(){ var d=new Date(); return d.getFullYear()+'-'+('0'+(d.getMonth()+1)).slice(-2)+'-'+('0'+d.getDate()).slice(-2); } /* Validate & merge any untrusted object (storage or import) into a clean state */ function hydrate(raw){ var d=defaultState(); if(!raw||typeof raw!=='object') return d; var p=raw.profile||{}, dp=d.profile; ['couple','location','venue'].forEach(function(k){ if(typeof p[k]==='string') dp[k]=str(p[k],200); }); if(typeof p.date==='string'&&/^\d{4}-\d{2}-\d{2}$/.test(p.date)) dp.date=p.date; else if(p.date==='') dp.date=''; if(typeof p.dateTbd==='boolean') dp.dateTbd=p.dateTbd; if(isOneOf(p.style,['elopement','intimate','boutique','traditional','luxury','destination'])) dp.style=p.style; ['adults','children','vendorMeals','party','events','hotelNights','seatsPerTable'].forEach(function(k){ if(p[k]!=null&&isFinite(p[k])) dp[k]=intv(p[k],0,100000); }); if(dp.seatsPerTable0?Math.ceil(guests/p.seatsPerTable):0; switch(model){ case 'perGuest': return {mult:guests,multKey:'m_guests',qtyEditable:false}; case 'perAdult': return {mult:p.adults,multKey:'m_adults',qtyEditable:false}; case 'perChild': return {mult:p.children,multKey:'m_children',qtyEditable:false}; case 'perVendorMeal': return {mult:p.vendorMeals,multKey:'m_vendors',qtyEditable:false}; case 'perTable': return {mult:tables,multKey:'m_tables',qtyEditable:false}; case 'perNight': return {mult:p.hotelNights,multKey:'m_nights',qtyEditable:true,qtyKey:'q_rooms'}; case 'perHour': return {mult:1,qtyEditable:true,qtyKey:'q_hours'}; case 'perDay': return {mult:1,qtyEditable:true,qtyKey:'q_days'}; case 'perItem': return {mult:1,qtyEditable:true,qtyKey:'q_items'}; case 'perPerson': return {mult:1,qtyEditable:true,qtyKey:'q_people'}; case 'custom': return {mult:1,qtyEditable:true,qtyKey:'q_qty'}; case 'percent': return {mult:1,qtyEditable:false,isPercent:true}; default: return {mult:1,qtyEditable:false}; } } function calc(st){ var p=st.profile, r=st.rates, guests=p.adults+p.children; var res={items:{},cats:{},subtotal:0,svc:{venue:0,catering:0,beverage:0},svcTotal:0,gratuity:0,admin:0,tax:0,contingency:0,total:0,deposits:0,paid:0,balance:0,guests:guests,count:0,target:p.target,perGuest:0,remaining:0,pct:0}; var base1={}, allBase=0; st.categories.forEach(function(cat){ var cs=0; cat.items.forEach(function(it){ var inc=isIncluded(it,st), mi=modelInfo(it.model,st), sub=0; if(inc&&!mi.isPercent){ sub=Math.round(mi.mult*num(it.qty,0)*num(it.price,0)); cs+=sub; } res.items[it.id]={sub:sub,included:inc,svc:0,tax:0,grat:0,deposit:0,paid:0,balance:0}; }); base1[cat.id]=cs; allBase+=cs; }); st.categories.forEach(function(cat){ var cs=base1[cat.id], n=0; cat.items.forEach(function(it){ var ri=res.items[it.id]; if(ri.included&&it.model==='percent'){ var b=(it.pctOf&&it.pctOf!=='all'&&base1[it.pctOf]!=null)?base1[it.pctOf]:allBase; ri.sub=Math.round(b*num(it.price,0)/10000); cs+=ri.sub; } if(ri.included) n++; }); res.cats[cat.id]={sub:cs,count:n}; }); st.categories.forEach(function(cat){ cat.items.forEach(function(it){ var ri=res.items[it.id]; if(!ri.included) return; res.count++; res.subtotal+=ri.sub; var rate=it.svc==='venue'?num(r.venueSvc,0):it.svc==='catering'?num(r.cateringSvc,0):it.svc==='beverage'?num(r.beverageSvc,0):0; if(it.svc!=='none'&&SVC_TYPES.indexOf(it.svc)>0){ ri.svc=Math.round(ri.sub*rate/100); res.svc[it.svc]+=ri.svc; if(!r.gratIncluded&&num(r.gratuity,0)>0){ ri.grat=Math.round(ri.sub*num(r.gratuity,0)/100); res.gratuity+=ri.grat; } } if(it.taxable){ ri.tax=Math.round((ri.sub+(r.taxOnSvc?ri.svc:0))*num(r.tax,0)/100); res.tax+=ri.tax; } ri.deposit=num(it.deposit,0); ri.paid=num(it.paid,0); ri.balance=ri.sub-ri.paid; res.deposits+=ri.deposit; res.paid+=ri.paid; }); }); res.svcTotal=res.svc.venue+res.svc.catering+res.svc.beverage; res.admin=Math.round(res.subtotal*num(r.admin,0)/100); var pre=res.subtotal+res.svcTotal+res.gratuity+res.admin+res.tax; res.contingency=Math.round(pre*num(r.contingency,0)/100); res.total=pre+res.contingency; res.balance=res.total-res.paid; res.perGuest=guests>0?Math.round(res.total/guests):0; res.remaining=p.target-res.total; res.pct=p.target>0?res.total/p.target*100:(res.total>0?999:0); return res; } /* ------------------------------------------------------------ 5. ONE CALCULATOR INSTANCE ------------------------------------------------------------ */ function createCalculator(root){ var storageKey=root.getAttribute('data-bwc-key')||'bwc-default'; var LANG_KEY=storageKey+':lang'; var lang='en', state, ui={tab:'overview',openCats:{venue:true,food:true},openItems:{},saveTimer:null,storageWarned:false,toastTimer:null}; var TABS=[['overview','tab_overview'],['budget','tab_budget'],['compare','tab_compare'],['checklist','tab_checklist'],['vendors','tab_vendors'],['settings','tab_settings']]; var fmtCache={}; function tr(k){ return tget(lang,k); } function fmt(cents){ var cur=state.profile.currency||'USD', loc=lang==='es'?'es-CO':'en-US', key=loc+cur, f=fmtCache[key]; if(!f){ try{ f=new Intl.NumberFormat(loc,{style:'currency',currency:cur,minimumFractionDigits:2,maximumFractionDigits:2}); f.format(1); }catch(e){ f={format:function(n){ return cur+' '+n.toFixed(2); }}; } fmtCache[key]=f; } return f.format(num(cents)/100); } function fmtK(cents){ var cur=state.profile.currency||'USD', loc=lang==='es'?'es-CO':'en-US'; try{ return new Intl.NumberFormat(loc,{style:'currency',currency:cur,maximumFractionDigits:0}).format(num(cents)/100); }catch(e){ return cur+' '+Math.round(num(cents)/100); } } function nameOf(it){ if(it.custom){ var n=it.name||{}; return n[lang]||n.en||n.es||tr('custom_name'); } var d=ITEM_DEF[it.id]; return d?(d[lang]||d.en):it.id; } function catName(c){ var d=CAT_DEF[c.id]||c; return lang==='es'?d.es:d.en; } function taskName(c){ if(c.custom){ var n=c.name||{}; return n[lang]||n.en||n.es||tr('cl_new'); } var d=CHECK_DEF[c.id]; return d?(lang==='es'?d.es:d.en):c.id; } function storage(){ try{ var s=window.localStorage; s.setItem('__bwc_t','1'); s.removeItem('__bwc_t'); return s; }catch(e){ return null; } } function loadLang(){ var s=storage(); if(s){ try{ var v=s.getItem(LANG_KEY); if(v==='es'||v==='en') return v; }catch(e){} } return 'en'; } function loadState(){ var s=storage(); if(s){ try{ var raw=s.getItem(storageKey); if(raw){ var obj=JSON.parse(raw); if(obj&&obj.app==='bwc') return hydrate(obj.state); } }catch(e){} } return defaultState(); } function save(){ var s=storage(); if(!s){ warnStorage(); return false; } try{ s.setItem(storageKey,JSON.stringify({app:'bwc',v:BWC_VERSION,savedAt:new Date().toISOString(),state:state})); s.setItem(LANG_KEY,lang); return true; }catch(e){ warnStorage(); return false; } } function warnStorage(){ if(!ui.storageWarned){ ui.storageWarned=true; toast(tr('storage_err'),true); } } function scheduleSave(){ if(ui.saveTimer) clearTimeout(ui.saveTimer); ui.saveTimer=setTimeout(save,500); } function toast(msg,isErr){ var el=root.querySelector('.bwc-toast'); if(!el) return; el.textContent=msg; el.className='bwc-toast bwc-show'+(isErr?' bwc-toast-err':''); if(ui.toastTimer) clearTimeout(ui.toastTimer); ui.toastTimer=setTimeout(function(){ el.className='bwc-toast'; },2600); } function tipHtml(text){ return text?'?':''; } function opt(v,label,sel){ return ''+esc(label)+''; } /* ---------- status helpers ---------- */ function statusOf(res){ if(res.target0) return 'over'; if(res.pct>100) return 'over'; if(res.pct>=90) return 'near'; return 'under'; } /* ---------- RENDER: shell ---------- */ function shell(){ var h='
'+esc(tr('kicker'))+'

'+esc(tr('title'))+'

'+esc(tr('subtitle'))+'
'; h+='
'; h+='
'+TABS.map(function(tb,i){ return ''; }).join('')+'
'; h+=stickyHtml(); h+='
'+panelHtml()+'
'; h+='
'; return h; } function stickyHtml(){ return '
'+ '
'+esc(tr('est_cost'))+'
'+ '
'+esc(tr('target'))+'
'+ '
'+ '
'+esc(tr('per_guest'))+'
'+ '
'; } function panelHtml(){ switch(ui.tab){ case 'budget': return budgetHtml(); case 'compare': return compareHtml(); case 'checklist': return checklistHtml(); case 'vendors': return vendorsHtml(); case 'settings': return settingsHtml(); default: return overviewHtml(); } } /* ---------- RENDER: overview ---------- */ function kpi(label,value,cls,key){ return '
'+esc(label)+'
'+esc(value)+'
'; } function overviewHtml(){ var res=calc(state), p=state.profile, st=statusOf(res); var h='

'+esc(tr('dash_title'))+'

'+esc(tr('status_'+st))+'
'; h+='

'+esc(p.couple||'')+(p.couple?' · ':'')+esc(dateLabel())+' · '+esc(p.venue||'')+(p.venue?' · ':'')+esc(res.guests)+' '+esc(tr('guests_word'))+' · '+esc(res.count)+' '+esc(tr('items_enabled'))+'

'; h+='
'+kpi(tr('target'),fmt(res.target))+kpi(tr('est_cost'),fmt(res.total))+kpi(tr('per_guest'),fmt(res.perGuest))+ kpi(tr('remaining'),fmt(Math.max(0,res.remaining)),res.remaining>=0?'bwc-pos':'')+kpi(tr('over'),fmt(Math.max(0,-res.remaining)),res.remaining0?Math.round(res.pct):'—')+(res.target>0?'%':''))+'
'; h+='

'+esc(tr('distribution'))+'

'+donutHtml(res)+'
'; h+='

'+esc(tr('cost_breakdown'))+'

'+breakdownTable(res)+'
'; h+='

'+esc(tr('category_totals'))+'

'; state.categories.forEach(function(c,i){ var cs=res.cats[c.id].sub; if(cs<=0) return; h+=''; }); if(res.subtotal<=0) h+=''; h+='
'+esc(tr('p_category'))+''+esc(tr('subtotal'))+''+esc(tr('share'))+'
'+esc(catName(c))+''+esc(fmt(cs))+''+(res.subtotal>0?Math.round(cs/res.subtotal*100):0)+'%
'+esc(tr('no_costs'))+'
'+esc(tr('subtotal'))+''+esc(fmt(res.subtotal))+'100%
'; return h; } function breakdownTable(res){ var rows=[[tr('subtotal'),res.subtotal]]; if(res.svc.venue) rows.push([tr('svc_venue_line')+' ('+state.rates.venueSvc+'%)',res.svc.venue]); if(res.svc.catering) rows.push([tr('svc_cat_line')+' ('+state.rates.cateringSvc+'%)',res.svc.catering]); if(res.svc.beverage) rows.push([tr('svc_bev_line')+' ('+state.rates.beverageSvc+'%)',res.svc.beverage]); if(!res.svc.venue&&!res.svc.catering&&!res.svc.beverage) rows.push([tr('service_charges'),0]); rows.push([tr('gratuities')+(state.rates.gratIncluded?'':' ('+state.rates.gratuity+'%)'),res.gratuity]); rows.push([tr('admin_fees')+' ('+state.rates.admin+'%)',res.admin]); rows.push([tr('taxes')+' ('+state.rates.tax+'%)',res.tax]); rows.push([tr('contingency')+' ('+state.rates.contingency+'%)',res.contingency]); var h=''+rows.map(function(r){ return ''; }).join(''); h+='
'+esc(r[0])+''+esc(fmt(r[1]))+'
'+esc(tr('total'))+''+esc(fmt(res.total))+'
'; h+='

'+esc(tr('payments'))+'

'+esc(tr('deposits_req'))+''+esc(fmt(res.deposits))+'
'+esc(tr('total_paid'))+''+esc(fmt(res.paid))+'
'+esc(tr('outstanding'))+''+esc(fmt(res.balance))+'
'; return h; } function donutHtml(res){ var parts=[]; state.categories.forEach(function(c,i){ var v=res.cats[c.id].sub; if(v>0) parts.push({name:catName(c),v:v,color:CAT_COLORS[i%CAT_COLORS.length]}); }); var total=parts.reduce(function(a,b){return a+b.v;},0); if(!total) return '
'+esc(tr('no_costs'))+'
'; var R=70, C=2*Math.PI*R, off=0, svg=''; parts.forEach(function(pt){ var len=pt.v/total*C; svg+=''+esc(pt.name)+': '+esc(fmt(pt.v))+''; off+=len; }); svg+=''+esc(fmtK(res.subtotal))+''+esc(tr('subtotal').toUpperCase())+''; var legend='
    '+parts.map(function(pt){ return '
  • '+esc(pt.name)+''+esc(fmt(pt.v))+' · '+Math.round(pt.v/total*100)+'%
  • '; }).join('')+'
'; return '
'+svg+legend+'
'; } /* ---------- RENDER: budget ---------- */ function fld(label,inner,cls){ return ''; } function profileHtml(){ var p=state.profile, g=p.adults+p.children; var h='

'+esc(tr('profile_title'))+'

'; h+='
'+esc(tr('guest_hero'))+'
'+g+'
'; h+='
'+ '
'+esc(tr('guests'))+'
'+ '
'+esc(tr('adults'))+'
'+ '
'+esc(tr('children'))+'
'; h+='

'+esc(tr('guest_note'))+'

'; h+=fld(tr('couple'),''); h+='
'+esc(tr('date'))+'
'; h+=fld(tr('location'),''); h+=fld(tr('venue'),''); h+=fld(tr('style'),''+['elopement','intimate','boutique','traditional','luxury','destination'].map(function(s){return opt(s,tr('style_'+s),p.style);}).join('')+''); h+=fld(tr('target_budget'),''); h+=fld(tr('currency'),''+['USD','EUR','COP','MXN','CAD','GBP'].map(function(c){return opt(c,c,p.currency);}).join('')+''); h+=fld(tr('vendor_meals'),''); h+=fld(tr('party'),''); h+=fld(tr('ceremony_setting'),''+opt('outdoor',tr('outdoor'),p.ceremonyType)+opt('indoor',tr('indoor'),p.ceremonyType)+''); h+=fld(tr('reception_type'),''+['seated','buffet','cocktail','brunch'].map(function(s){return opt(s,tr('rec_'+s),p.receptionType);}).join('')+''); h+=fld(tr('events'),''); h+=fld(tr('nights'),''); h+=fld(tr('hotel_payer'),''+opt('guests',tr('guests_pay'),p.hotelPayer)+opt('couple',tr('couple_pays'),p.hotelPayer)+''); h+=fld(tr('seats_per_table'),''); h+='
'; return h; } function budgetHtml(){ var res=calc(state); var h='
'+esc(tr('example_badge'))+'
'+profileHtml(); h+='

'+esc(tr('budget_title'))+'

'; h+='

'+esc(tr('budget_intro'))+'

'; state.categories.forEach(function(c){ h+=categoryHtml(c,res); }); return h; } function categoryHtml(c,res){ var open=!!ui.openCats[c.id], def=CAT_DEF[c.id], cr=res.cats[c.id]; var h='
'; h+='
'; c.items.forEach(function(it){ h+=itemHtml(it,c,res); }); h+='
'; return h; } function itemHtml(it,c,res){ var ri=res.items[it.id], mi=modelInfo(it.model,state), grp=groupOf(it), hotel=isHotel(it), excluded=hotel&&state.profile.hotelPayer==='guests', open=!!ui.openItems[it.id], id=it.id; var h='
'; h+=''; h+='
'+(it.custom?'':esc(nameOf(it)))+tipHtml(tipOf(it,lang))+(grp?' '+esc(tr('excl_badge'))+''+tipHtml(tr('excl_tip')):'')+(hotel?' '+esc(tr('hotel_badge'))+''+(excluded?tipHtml(tr('hotel_tip')):''):'')+(it.custom?' custom':'')+''+(it.vendor?''+esc(it.vendor)+'':'')+'
'; h+=''; if(mi.isPercent){ h+='
'+esc(tr('pct_of'))+'
'+esc(pctBaseLabel(it))+'
'; } else { h+=''; } h+=''; h+='
'+esc(tr('subtotal_l'))+''+esc(fmt(ri.included?ri.sub:0))+'
'; h+='
'; h+='
'; h+=fld(tr('vendor'),''); h+=fld(tr('svc'),''+SVC_TYPES.map(function(s){return opt(s,tr('svc_'+s),it.svc);}).join('')+''); h+=''; h+=fld(tr('deposit'),''); h+=fld(tr('paid'),''); h+='
'+esc(tr('balance'))+''+esc(fmt(ri.included?ri.balance:0))+'
'; h+=fld(tr('notes'),'','bwc-wide'); if(it.custom) h+='
'; h+='
'; return h; } function multLabel(it,mi){ if(mi.multKey) return mi.mult+' '+tr(mi.multKey)+' ×'; return ''; } function pctBaseLabel(it){ if(it.pctOf&&it.pctOf!=='all'&&CAT_DEF[it.pctOf]) return catName(CAT_DEF[it.pctOf]); return tr('base_all'); } /* ---------- RENDER: compare ---------- */ function setGuests(s,n){ n=Math.max(0,intv(n,0)); if(n=0) i.enabled=on; }); }); } function groupCurrent(group){ var cur=null; state.categories.forEach(function(c){ c.items.forEach(function(i){ if(groupOf(i)===group&&i.enabled) cur=i.id; }); }); return cur; } function scenarios(){ var sc=state.scenario, g=state.profile.adults+state.profile.children, vid=findItem(state,'videographer'); return [ {id:'guests',title:tr('sc_guests'),desc:tr('sc_guests_d'),a:{label:sc.guestsA+' '+tr('m_guests'),mod:function(s){setGuests(s,sc.guestsA);},cur:g===sc.guestsA},b:{label:sc.guestsB+' '+tr('m_guests'),mod:function(s){setGuests(s,sc.guestsB);},cur:g===sc.guestsB}}, {id:'dinner',title:tr('sc_dinner'),a:{label:tr('buffet_o'),mod:function(s){pickGroup(s,'dinner','buffet');},cur:groupCurrent('dinner')==='buffet'},b:{label:tr('plated_o'),mod:function(s){pickGroup(s,'dinner','plated');},cur:groupCurrent('dinner')==='plated'}}, {id:'bar',title:tr('sc_bar'),a:{label:tr('beerwine_o'),mod:function(s){pickGroup(s,'bar','beer_wine');},cur:groupCurrent('bar')==='beer_wine'},b:{label:tr('openbar_o'),mod:function(s){pickGroup(s,'bar','open_bar_guest');},cur:groupCurrent('bar')==='open_bar_guest'}}, {id:'music',title:tr('sc_music'),a:{label:tr('dj_o'),mod:function(s){pickGroup(s,'music_main','dj');},cur:groupCurrent('music_main')==='dj'},b:{label:tr('band_o'),mod:function(s){pickGroup(s,'music_main','band');},cur:groupCurrent('music_main')==='band'}}, {id:'flowers',title:tr('sc_flowers'),desc:tr('sc_flowers_d'),a:{label:tr('basic_o'),mod:function(s){},cur:true},b:{label:tr('premium_o')+' ×'+sc.premiumFactor,mod:function(s){ s.categories.forEach(function(c){ if(c.id==='flowers') c.items.forEach(function(i){ i.price=Math.round(i.price*sc.premiumFactor); }); }); },cur:false}}, {id:'photo',title:tr('sc_photo'),a:{label:tr('photo_o'),mod:function(s){ setEnabled(s,['photographer'],true); setEnabled(s,['videographer','highlight_film','full_ceremony_video','drone'],false); },cur:!(vid&&vid.item.enabled)},b:{label:tr('photovideo_o'),mod:function(s){ setEnabled(s,['photographer','videographer','highlight_film'],true); },cur:!!(vid&&vid.item.enabled)}}, {id:'planner',title:tr('sc_planner'),a:{label:tr('planner_o'),mod:function(s){pickGroup(s,'planner','full_planner');},cur:groupCurrent('planner')==='full_planner'},b:{label:tr('dayof_o'),mod:function(s){pickGroup(s,'planner','day_of');},cur:groupCurrent('planner')==='day_of'}} ]; } function runScenario(mod){ var s=clone(state); mod(s); return calc(s).total; } function compareHtml(){ var sc=state.scenario; var h='

'+esc(tr('compare_title'))+'

'+esc(tr('compare_intro'))+'

'; h+=fld(tr('guests_a'),''); h+=fld(tr('guests_b'),''); h+=fld(tr('premium_factor'),''); h+='
'+scenarioResultsHtml()+'
'; return h; } function scenarioResultsHtml(){ var cur=calc(state).total, h=''; scenarios().forEach(function(s){ var A=runScenario(s.a.mod), B=runScenario(s.b.mod), d=B-A; h+='

'+esc(s.title)+'

'+(s.desc?''+esc(s.desc)+'':'')+'
'; h+='
'+esc(tr('option_a'))+(s.a.cur?' · '+esc(tr('current')):'')+''+esc(fmt(A))+''+esc(s.a.label)+'
'; h+='
'+esc(tr('difference'))+'<span class="bwc-opt-v '+(d0?'bwc-neg':'')+'">'+esc((d>0?'+':'')+fmt(d))+''+esc(d0?tr('costs_more')+' '+fmt(d):tr('same'))+'
'; h+='
'+esc(tr('option_b'))+(s.b.cur?' · '+esc(tr('current')):'')+''+esc(fmt(B))+''+esc(s.b.label)+'
'; h+='
'; }); h+='

'+esc(tr('sensitivity'))+'

'+esc(tr('current'))+': '+esc(fmt(cur))+'

'+esc(tr('sens_note'))+'

'+sensitivityHtml()+'
'; return h; } function sensitivityHtml(){ var g=state.profile.adults+state.profile.children, counts=[10,20,30,40,50,75,100,150,200]; if(counts.indexOf(g)maxV) maxV=p.v; }); maxV*=1.12; var W=760,H=300,L=70,R=16,Tp=18,B=46,pw=W-L-R,ph=H-Tp-B, n=pts.length, bw=pw/n*0.62, gap=pw/n; var svg=''; for(var i=0;i<=4;i++){ var y=Tp+ph-ph*i/4, val=maxV*i/4; svg+=''+esc(fmtK(val))+''; } if(target>0&&target<maxV){ var ty=Tp+ph-ph*target/maxV; svg+=''+esc(tr('target'))+' '+esc(fmtK(target))+''; } pts.forEach(function(p,i){ var x=L+gap*i+(gap-bw)/2, hgt=ph*p.v/maxV, y=Tp+ph-hgt, isCur=p.n===g, over=target>0&&p.v>target; svg+=''+p.n+' '+esc(tr('m_guests'))+': '+esc(fmt(p.v))+''; svg+=''+esc(fmtK(p.v))+''; svg+=''+p.n+''; }); svg+=''+esc(tr('guests_word').toUpperCase())+''; return svg; } /* ---------- RENDER: checklist ---------- */ function checklistHtml(){ var list=state.checklist, done=list.filter(function(c){return c.done;}).length, pct=list.length?Math.round(done/list.length*100):0; var h='

'+esc(tr('cl_title'))+'

'; h+='

'+esc(tr('cl_intro'))+'

'+pct+'% '+esc(tr('cl_progress'))+''+done+' / '+list.length+' '+esc(tr('cl_tasks'))+'
'; if(!list.length) h+='
'+esc(tr('cl_empty'))+'
'; list.forEach(function(c){ h+='
'; h+='
'+(c.custom?'':esc(taskName(c)))+'
'; h+=''; h+=''; h+=''; h+='
'; }); return h; } /* ---------- RENDER: vendors ---------- */ function vendorsHtml(){ var res=calc(state), vs=state.vendors, tc=0,tp=0; vs.forEach(function(v){ tc+=v.amount; tp+=v.paid; }); var h='

'+esc(tr('v_title'))+'

'+esc(tr('v_intro'))+'

'; h+='
'+kpi(tr('v_contracts'),fmt(tc),'','vtc')+kpi(tr('v_paid'),fmt(tp),'','vtp')+kpi(tr('v_balance'),fmt(tc-tp),'','vtb')+'

'+esc(tr('v_note'))+'

'; if(!vs.length) h+='
'+esc(tr('v_empty'))+'
'; var itemOpts=[]; state.categories.forEach(function(c){ c.items.forEach(function(i){ if(i.enabled) itemOpts.push({id:i.id,label:CAT_DEF[c.id].letter+'. '+nameOf(i)}); }); }); vs.forEach(function(v){ var id=v.id, linked=v.linkedItem?findItem(state,v.linkedItem):null; h+='

'+esc(v.company||tr('v_new'))+' '+esc(catName(CAT_DEF[v.cat]||CATS[0]))+''+(v.signed?' '+esc(tr('v_signed'))+'':'')+'

'+(linked?'':'')+'
'; h+=fld(tr('v_cat'),''+state.categories.map(function(c){return opt(c.id,catName(c),v.cat);}).join('')+''); h+=fld(tr('v_company'),''); h+=fld(tr('v_contact'),''); h+=fld(tr('v_email'),''); h+=fld(tr('v_phone'),''); h+=fld(tr('v_web'),''); h+=fld(tr('v_link'),''+opt('',tr('v_nolink'),v.linkedItem)+itemOpts.map(function(o){return opt(o.id,o.label,v.linkedItem);}).join('')+''); h+='
'+esc(tr('v_line_cost'))+''+esc(linked?fmt(res.items[linked.item.id].sub):'—')+'
'; h+=fld(tr('v_amount'),''); h+=fld(tr('v_dep_due'),''); h+=fld(tr('v_dep_paid'),''); h+=fld(tr('v_final'),''); h+=fld(tr('v_paid'),''); h+='
'+esc(tr('v_balance'))+''+esc(fmt(v.amount-v.paid))+'
'; h+=''; h+=fld(tr('notes'),''); h+='
'; }); return h; } /* ---------- RENDER: settings ---------- */ function settingsHtml(){ var r=state.rates; var h='

'+esc(tr('s_rates'))+'

'+esc(tr('s_rate_note'))+'
'; [['tax','s_tax'],['venueSvc','s_venue_svc'],['cateringSvc','s_cat_svc'],['beverageSvc','s_bev_svc'],['gratuity','s_grat'],['admin','s_admin'],['contingency','s_cont']].forEach(function(f){ h+=fld(tr(f[1]),''); }); h+='

'+esc(tr('s_formula'))+'

'; h+='

'+esc(tr('s_data'))+'

'+ ''+ ''+ ''+ '

'+esc(tr('storage_note'))+'

'; h+='

'+esc(tr('s_general'))+'

'+fld(tr('s_language'),''+opt('en',tr('lang_en'),lang)+opt('es',tr('lang_es'),lang)+'')+'
'; return h; } /* ---------- PARTIAL UPDATE (no re-render, keeps focus) ---------- */ function q(sel){ return root.querySelector(sel); } function setText(sel,txt){ var el=q(sel); if(el) el.textContent=txt; } function updateSticky(res){ var st=statusOf(res), pct=Math.max(0,Math.min(100,res.target>0?res.pct:(res.total>0?100:0))); setText('[data-k="total"]',fmt(res.total)); setText('[data-k="target"]',fmt(res.target)); setText('[data-k="remlabel"]',res.remaining>=0?tr('remaining'):tr('over')); var rem=q('[data-k="remaining"]'); if(rem){ rem.textContent=fmt(Math.abs(res.remaining)); rem.className='bwc-stat-v '+(res.remaining>=0?'bwc-pos':'bwc-neg'); } setText('[data-k="perGuest"]',fmt(res.perGuest)); setText('[data-k="guests"]',res.guests+' '+tr('guests_word')); var pr=q('[data-k="progress"]'); if(pr){ pr.className='bwc-progress'+(st==='over'?' bwc-over':st==='near'?' bwc-near':''); pr.firstChild.style.width=pct.toFixed(1)+'%'; } setText('[data-k="pct"]',(res.target>0?Math.round(res.pct)+'% ':'— ')+tr('budget_used')); setText('[data-k="status"]',tr('status_'+st)); } function collect(attr){ var m={}, els=root.querySelectorAll('['+attr+']'); for(var i=0;i<els.length;i++) m[els[i].getAttribute(attr)]=els[i]; return m; } function put(map,key,txt){ var el=map[key]; if(el) el.textContent=txt; } function recalc(){ var res=calc(state), p=state.profile, g=p.adults+p.children; updateSticky(res); var mSub=collect('data-sub'), mBal=collect('data-bal'), mMult=collect('data-mult'), mCat=collect('data-cattotal'), mCnt=collect('data-catcount'); state.categories.forEach(function(c){ var cr=res.cats[c.id]; put(mCat,c.id,fmt(cr.sub)); put(mCnt,c.id,cr.count); c.items.forEach(function(it){ var ri=res.items[it.id]; put(mSub,it.id,fmt(ri.included?ri.sub:0)); put(mBal,it.id,fmt(ri.included?ri.balance:0)); var mi=modelInfo(it.model,state); put(mMult,it.id,mi.isPercent?pctBaseLabel(it):multLabel(it,mi)); }); }); setText('[data-k="guestbig"]',g); setText('[data-k="guestsub"]',p.adults+' '+tr('m_adults')+' · '+p.children+' '+tr('m_children')+' · '+Math.ceil(g/Math.max(1,p.seatsPerTable))+' '+tr('tables_word')); var tc=0,tp=0; state.vendors.forEach(function(v){ tc+=v.amount; tp+=v.paid; setText('[data-vbal="'+v.id+'"]',fmt(v.amount-v.paid)); }); setText('[data-k="vtc"]',fmt(tc)); setText('[data-k="vtp"]',fmt(tp)); setText('[data-k="vtb"]',fmt(tc-tp)); if(ui.tab==='compare'){ var c=q('[data-sc-results]'); if(c) c.innerHTML=scenarioResultsHtml(); } } function syncGuestInputs(skipEl){ var p=state.profile; [['profile|guests',p.adults+p.children],['profile|adults',p.adults],['profile|children',p.children]].forEach(function(b){ var el=q('[data-bind="'+b[0]+'"]'); if(el&&el!==skipEl) el.value=b[1]; }); } function render(){ var active=document.activeElement, key=(active&&root.contains(active)&&active.getAttribute('data-bind'))||null; root.innerHTML=shell(); recalc(); if(key){ var el=q('[data-bind="'+key+'"]'); if(el){ try{ el.focus(); }catch(e){} } } } /* ---------- INPUT BINDING ---------- */ function clampInt(el,min){ min=min||0; var raw=el.value, n=intv(raw,min,1e6); if(raw!==''&&parseFloat(raw)<min){ el.value=min; toast(tr('neg_fixed'),true); } return n; } function clampNum(el){ var raw=el.value, n=Math.round(num(raw,0,1e9)*1000)/1000; if(raw!==''&&parseFloat(raw)<0){ el.value=0; toast(tr('neg_fixed'),true); } return n; } function clampMoney(el){ var raw=el.value, c=toCents(raw); if(raw!==''&&parseFloat(raw)<0){ el.value='0.00'; toast(tr('neg_fixed'),true); } return Math.min(c,1e13); } function clampRate(el){ var raw=el.value, n=Math.round(num(raw,0,1000)*1000)/1000; if(raw!==''&&parseFloat(raw)=0?tr('remaining'):tr('over'),Math.abs(res.remaining)],[tr('per_guest'),res.perGuest],[tr('deposits_req'),res.deposits],[tr('total_paid'),res.paid],[tr('outstanding'),res.balance]].forEach(function(r){ rows.push([r[0],'','','','','','',money2(r[1])]); }); rows.push([tr('guests'),res.guests]); var csv=''+rows.map(function(r){ return r.map(csvCell).join(','); }).join('\r\n'); download('wedding-budget-'+todayISO()+'.csv',csv,'text/csv;charset=utf-8'); } function exportJson(){ download('wedding-budget-'+todayISO()+'.json',JSON.stringify({app:'bwc',v:BWC_VERSION,exportedAt:new Date().toISOString(),lang:lang,state:state},null,2),'application/json'); } function importJson(input){ var file=input.files&&input.files[0]; if(!file) return; var reader=new FileReader(); reader.onload=function(){ var ok=false; try{ var obj=JSON.parse(String(reader.result)); if(obj&&obj.app==='bwc'&&typeof obj.v==='number'&&obj.state&&typeof obj.state==='object'&&!Array.isArray(obj.state)&&obj.state.profile&&typeof obj.state.profile==='object'&&Array.isArray(obj.state.categories)){ state=hydrate(obj.state); ok=true; } }catch(e){ ok=false; } input.value=''; if(ok){ ui.openItems={}; save(); render(); toast(tr('import_ok')); } else toast(tr('import_err'),true); }; reader.onerror=function(){ input.value=''; toast(tr('import_err'),true); }; reader.readAsText(file); } /* ---------- PRINT ---------- */ function dateLabel(){ var p=state.profile, loc=lang==='es'?'es-CO':'en-US'; if(!p.date) return tr('date_tbd'); var d=new Date(p.date+'T12:00:00'); if(isNaN(d.getTime())) return p.date; try{ return p.dateTbd? d.toLocaleDateString(loc,{month:'long',year:'numeric'})+' · '+tr('date_tbd') : d.toLocaleDateString(loc,{weekday:'long',year:'numeric',month:'long',day:'numeric'}); }catch(e){ return p.date; } } function reportHeader(title){ var p=state.profile, g=p.adults+p.children; return '

'+esc(title)+'

'+esc(tr('p_couple'))+': '+esc(p.couple||'—')+''+esc(tr('date'))+': '+esc(dateLabel())+''+esc(tr('venue'))+': '+esc(p.venue||'—')+''+esc(tr('location'))+': '+esc(p.location||'—')+''+esc(tr('guests'))+': '+g+' ('+p.adults+' '+esc(tr('m_adults'))+', '+p.children+' '+esc(tr('m_children'))+')'+esc(tr('p_generated'))+': '+esc(new Date().toLocaleString(lang==='es'?'es-CO':'en-US'))+'
'; } function budgetReportHtml(){ var res=calc(state), h=reportHeader(tr('p_report'))+'

'+esc(tr('p_disclaimer'))+'

'; h+='

'+esc(tr('p_summary'))+'

'+esc(tr('target'))+''+esc(fmt(res.target))+''+esc(tr('total'))+''+esc(fmt(res.total))+'
'+esc(res.remaining>=0?tr('remaining'):tr('over'))+''+esc(fmt(Math.abs(res.remaining)))+''+esc(tr('per_guest'))+''+esc(fmt(res.perGuest))+'
'; h+='

'+esc(tr('category_totals'))+'

'; state.categories.forEach(function(c){ var v=res.cats[c.id].sub; if(v>0) h+=''; }); h+='
'+esc(tr('p_category'))+''+esc(tr('subtotal'))+''+esc(tr('share'))+'
'+esc(catName(c))+''+esc(fmt(v))+''+(res.subtotal?Math.round(v/res.subtotal*100):0)+'%
'+esc(tr('subtotal'))+''+esc(fmt(res.subtotal))+'100%
'; h+='

'+esc(tr('budget_title'))+'

'; state.categories.forEach(function(c){ var inc=c.items.filter(function(i){ return res.items[i.id].included; }); if(!inc.length) return; h+=''; inc.forEach(function(it){ var ri=res.items[it.id], mi=modelInfo(it.model,state), det=mi.isPercent?(it.price/100)+'% · '+pctBaseLabel(it):(mi.multKey?mi.mult+' '+tr(mi.multKey)+' × ':'')+(mi.qtyEditable?it.qty+' '+tr(mi.qtyKey).toLowerCase()+' × ':'')+fmt(it.price); h+=''; }); }); h+='
'+esc(tr('p_expense'))+''+esc(tr('p_detail'))+''+esc(tr('vendor'))+''+esc(tr('subtotal'))+''+esc(tr('deposit'))+''+esc(tr('paid'))+''+esc(tr('balance'))+'
'+esc(CAT_DEF[c.id].letter+'. '+catName(c))+'
'+esc(nameOf(it))+(it.notes?'
'+esc(it.notes)+'':'')+'
'+esc(det)+''+esc(it.vendor)+''+esc(fmt(ri.sub))+''+esc(fmt(it.deposit))+''+esc(fmt(it.paid))+''+esc(fmt(ri.balance))+'

'+esc(tr('p_tax_svc'))+'

'; [[tr('subtotal'),res.subtotal],[tr('svc_venue_line')+' ('+state.rates.venueSvc+'%)',res.svc.venue],[tr('svc_cat_line')+' ('+state.rates.cateringSvc+'%)',res.svc.catering],[tr('svc_bev_line')+' ('+state.rates.beverageSvc+'%)',res.svc.beverage],[tr('gratuities'),res.gratuity],[tr('admin_fees')+' ('+state.rates.admin+'%)',res.admin],[tr('taxes')+' ('+state.rates.tax+'%)',res.tax],[tr('contingency')+' ('+state.rates.contingency+'%)',res.contingency]].forEach(function(r){ h+=''; }); h+='
'+esc(r[0])+''+esc(fmt(r[1]))+'
'+esc(tr('total'))+''+esc(fmt(res.total))+'
'; h+='

'+esc(tr('p_deposits'))+'

'+esc(tr('deposits_req'))+''+esc(fmt(res.deposits))+'
'+esc(tr('total_paid'))+''+esc(fmt(res.paid))+'
'+esc(tr('outstanding'))+''+esc(fmt(res.balance))+'
'; return h; } function checklistReportHtml(){ var list=state.checklist, done=list.filter(function(c){return c.done;}).length, h=reportHeader(tr('p_checklist')); h+='

'+done+' / '+list.length+' '+esc(tr('cl_tasks'))+' · '+(list.length?Math.round(done/list.length*100):0)+'% '+esc(tr('cl_progress'))+'

'; list.forEach(function(c){ h+=''; }); return h+'
'+esc(tr('p_task'))+''+esc(tr('cl_due'))+''+esc(tr('cl_who'))+''+esc(tr('notes'))+'
'+esc(taskName(c))+''+esc(c.due)+''+esc(tr('who_'+c.who))+''+esc(c.notes)+'
'; } function printReport(html){ var host=q('.bwc-print'); if(!host) return; host.innerHTML=html; document.body.classList.add('bwc-printing'); var done=false; function cleanup(){ if(done) return; done=true; document.body.classList.remove('bwc-printing'); host.innerHTML=''; } window.addEventListener('afterprint',cleanup,{once:true}); setTimeout(function(){ try{ window.print(); }catch(e){ cleanup(); } setTimeout(cleanup,60000); },50); } /* ---------- INIT ---------- */ lang=loadLang(); state=loadState(); root.setAttribute('lang',lang); root.addEventListener('click',onClick); root.addEventListener('input',onInput); root.addEventListener('change',onChange); render(); return {root:root}; } /* ------------------------------------------------------------ 6. BOOT — one instance per .bwc[data-bwc-key] wrapper ------------------------------------------------------------ */ function boot(){ var roots=document.querySelectorAll('.bwc[data-bwc-key]'); for(var i=0;i<roots.length;i++){ var r=roots[i]; if(r.getAttribute('data-bwc-init')) continue; r.setAttribute('data-bwc-init','1'); try{ createCalculator(r); }catch(e){ if(window.console) console.error('Wedding calculator failed to start:',e); } } } if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',boot); else boot(); })();