{"id":58254,"date":"2026-08-17T22:21:38","date_gmt":"2026-08-17T22:21:38","guid":{"rendered":"http:\/\/trueomni.com\/?page_id=58254"},"modified":"2026-08-17T22:22:08","modified_gmt":"2026-08-17T22:22:08","slug":"visit-lauderdale","status":"publish","type":"page","link":"https:\/\/trueomni.com\/spanish\/visit-lauderdale\/","title":{"rendered":"VisitLauderdale"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"58254\" class=\"elementor elementor-58254\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-8d3df9a e-con e-atomic-element e-flexbox-base e-default-div e-6a246d9 \" data-id=\"8d3df9a\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"8d3df9a\" data-e-type=\"e-flexbox\" data-id=\"8d3df9a\">\n    \t\t<div class=\"elementor-element elementor-element-c8b81b1 elementor-widget elementor-widget-html\" data-id=\"c8b81b1\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"light\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700;800;900&display=swap\" rel=\"stylesheet\">\n<title>TrueImpact \u2014 Executive Dashboard \u00b7 Visit Lauderdale<\/title>\n<!--\n  TrueImpact Executive Dashboard  \u00b7  TrueOmni  \u00b7  Destination Performance Intelligence\n  ---------------------------------------------------------------------------------\n  Single-file, dependency-free reference build. All data lives in the DATA object\n  (see <script id=\"model\">). Metrics are DERIVED from base inputs in deriveMetrics()\n  so the math is transparent and re-wireable to a live attribution API.\n  Illustrative\/sample data for demonstration. Brand + palette per TrueOmni house style.\n-->\n<style>\n:root{\n  \/* ---- TrueOmni Design System tokens (blue #0EA1DC \u00b7 ink #0A0A0A \u00b7 bg #F5F7FA \u00b7 Inter) ---- *\/\n  --navy:#0D1A2D; --navy-2:#0A1420; --azure:#0EA1DC; --azure-d:#0B8AC4;\n  --deep:#0B8AC4; --ink:#0A0A0A; --slate:#6B7280; --amber:#C2641F;\n  \/* ---- validated chart palette (dataviz skill; passes CVD + normal-vision gates) ---- *\/\n  --s-hotels:#0EA1DC; --s-dining:#EB6834; --s-attractions:#008300; --s-events:#E87BA4;\n  --s-direct:#0EA1DC; --s-influenced:#0B8AC4; --s-local:#0EA1DC; --s-state:#1BAF7A;\n  --good:#0ca30c; --warn:#C2641F;\n  \/* ---- surfaces \/ ink (light) ---- *\/\n  --bg:#F5F7FA; --surface:#ffffff; --surface-2:#F1F5F9; --line:#E5E7EB;\n  --text:#0A0A0A; --text-2:#6B7280; --muted:#9CA3AF; --grid:#E5E7EB;\n  --tint:#E0F2FE;\n  --shadow:0 1px 2px rgba(13,26,45,.06),0 8px 24px rgba(13,26,45,.06);\n  --shadow-lg:0 24px 60px rgba(13,26,45,.16);\n  --radius:16px; --radius-sm:11px;\n  --font:'Inter','Segoe UI',system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif;\n}\nhtml[data-theme=\"dark\"]{\n  --navy:#0D1A2D; --azure:#3BB8ED; --deep:#0B8AC4;\n  --s-hotels:#38b6ec; --s-dining:#f0854f; --s-attractions:#37a637; --s-events:#e493b5;\n  --s-direct:#38b6ec; --s-influenced:#2f77c4; --s-local:#38b6ec; --s-state:#22c48a;\n  --bg:#0a1420; --surface:#101d2e; --surface-2:#0c1826; --line:#1e2f45;\n  --text:#eaf1f8; --text-2:#a9b7c8; --muted:#6f8098; --grid:#182636;\n  --shadow:0 1px 2px rgba(0,0,0,.4),0 10px 30px rgba(0,0,0,.35);\n  --shadow-lg:0 24px 60px rgba(0,0,0,.55);\n}\n*{box-sizing:border-box;margin:0;padding:0}\nhtml,body{background:var(--bg);color:var(--text);font-family:var(--font);-webkit-font-smoothing:antialiased;scroll-behavior:smooth}\nbody{font-size:15px;line-height:1.5}\na{color:inherit;text-decoration:none}\nbutton{font-family:inherit;cursor:pointer;border:0;background:none;color:inherit}\n.tnum{font-variant-numeric:tabular-nums}\n\n\/* ---------- Top bar ---------- *\/\n.topbar{position:sticky;top:0;z-index:50;background:linear-gradient(100deg,var(--navy) 0%,var(--navy-2) 100%);color:#fff;box-shadow:0 6px 20px rgba(10,26,49,.28)}\n.topbar-inner{max-width:1320px;margin:0 auto;padding:12px 24px;display:flex;align-items:center;gap:18px}\n.brand{display:flex;align-items:center;gap:11px;font-weight:700;letter-spacing:.2px}\n.brand .mark{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,var(--azure),#0f6fa0);display:grid;place-items:center;box-shadow:0 4px 14px rgba(28,156,216,.45)}\n.brand .mark svg{width:17px;height:17px}\n.brand .name{font-size:16px}.brand .name b{color:var(--azure)}\n.brand .sub{font-size:10px;font-weight:600;letter-spacing:1.6px;color:#9fc7de;text-transform:uppercase;margin-top:1px;white-space:nowrap}\n.brand{flex:none}\n.segset button{white-space:nowrap}\n@media (max-width:1280px){.brand .sub{display:none}.live{display:none}}\n.destpick{display:flex;align-items:center;gap:7px;background:rgba(255,255,255,.11);border:1px solid rgba(255,255,255,.14);border-radius:9px;padding:6px 10px;color:#dceaf5}\n.destpick svg{width:15px;height:15px;flex:none;color:var(--azure)}\n.destpick select{background:transparent;border:0;color:#fff;font-family:inherit;font-size:13px;font-weight:700;outline:none;cursor:pointer;max-width:190px}\n.destpick select option{color:#12202f}\n.sample{font-size:10px;font-weight:800;letter-spacing:1.2px;color:#ffd79a;background:rgba(194,100,31,.28);border:1px solid rgba(255,183,120,.4);border-radius:6px;padding:4px 8px;white-space:nowrap}\n@media (max-width:1180px){.sample{display:none}}\n.live{display:flex;align-items:center;gap:7px;font-size:12px;color:#bfe0f0;font-weight:600;margin-left:2px}\n.dot{width:8px;height:8px;border-radius:50%;background:#3ee08a;box-shadow:0 0 0 0 rgba(62,224,138,.7);animation:pulse 2s infinite}\n@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(62,224,138,.55)}70%{box-shadow:0 0 0 9px rgba(62,224,138,0)}100%{box-shadow:0 0 0 0 rgba(62,224,138,0)}}\n.spacer{flex:1}\n.controls{display:flex;align-items:center;gap:9px}\n.segset{display:flex;background:rgba(255,255,255,.09);border-radius:10px;padding:3px}\n.segset button{font-size:12.5px;font-weight:600;color:#c5d6e6;padding:6px 12px;border-radius:7px;transition:.15s}\n.segset button.on{background:var(--azure);color:#04141f;box-shadow:0 2px 8px rgba(28,156,216,.4)}\n.icon-btn{width:36px;height:36px;border-radius:9px;background:rgba(255,255,255,.1);display:grid;place-items:center;color:#dceaf5;transition:.15s}\n.icon-btn:hover{background:rgba(255,255,255,.2)}\n.icon-btn svg{width:18px;height:18px}\n.cta{background:var(--azure);color:#04141f;font-weight:700;font-size:13px;padding:9px 15px;border-radius:9px;display:flex;align-items:center;gap:7px;transition:.15s}\n.cta:hover{background:#33abe0;transform:translateY(-1px)}\n.cta svg{width:15px;height:15px}\n\n\/* ---------- Breadcrumb \/ subnav ---------- *\/\n.wrap{max-width:1320px;margin:0 auto;padding:22px 24px 60px}\n.crumbs{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-2);margin-bottom:16px;flex-wrap:wrap}\n.crumbs a{color:var(--azure);font-weight:600}\n.crumbs .sep{color:var(--muted)}\n.pagehead{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:6px;flex-wrap:wrap}\n.pagehead h1{font-size:27px;letter-spacing:-.4px;line-height:1.15}\n.pagehead .eyebrow{font-size:11.5px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--azure);margin-bottom:5px}\n.pagehead .meta{font-size:12.5px;color:var(--muted);text-align:right}\n\n\/* ---------- Cards \/ grid ---------- *\/\n.grid{display:grid;gap:16px}\n.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px 18px 16px}\n.card.pad0{padding:0;overflow:hidden}\n.card h3{font-size:13px;font-weight:700;letter-spacing:.2px;display:flex;align-items:center;gap:8px}\n.card h3 .hint{font-size:11px;color:var(--muted);font-weight:600}\n.card .ch-sub{font-size:12px;color:var(--text-2);margin-top:2px}\n.card-h{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:14px}\n\n\/* KPI hero band *\/\n.kpis{display:grid;grid-template-columns:1.5fr 1fr 1fr 1fr;gap:16px}\n.kpi{position:relative;overflow:hidden}\n.kpi.hero{background:linear-gradient(140deg,var(--navy),#123056 90%);color:#fff;border:0}\n.kpi .k-label{font-size:12px;color:var(--text-2);font-weight:600;display:flex;align-items:center;gap:6px}\n.kpi.hero .k-label{color:#a8c6db}\n.kpi .k-val{font-size:34px;font-weight:700;letter-spacing:-1px;margin-top:7px;line-height:1}\n.kpi.hero .k-val{font-size:44px}\n.kpi .k-val .cur{font-size:.62em;opacity:.7;font-weight:600;vertical-align:.14em;margin-right:1px}\n.kpi .k-delta{font-size:12.5px;font-weight:700;margin-top:9px;display:inline-flex;align-items:center;gap:5px}\n.k-delta.up{color:var(--good)} .kpi.hero .k-delta.up{color:#5ef0a5}\n.kpi .k-foot{font-size:11.5px;color:var(--muted);margin-top:8px}\n.kpi.hero .k-foot{color:#8fb2ca}\n.spark{position:absolute;right:14px;bottom:12px;opacity:.9}\n.kpi.hero .glow{position:absolute;right:-40px;top:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(28,156,216,.5),transparent 70%);pointer-events:none}\n\n.row-2{grid-template-columns:1.35fr 1fr}\n.row-3{grid-template-columns:repeat(3,1fr)}\n.row-map{grid-template-columns:1.15fr .85fr}\n\n\/* Map *\/\n.mapwrap{position:relative;padding:8px}\n.mapwrap svg{width:100%;height:auto;display:block}\n.mk{cursor:pointer;transition:.15s}\n.mk circle.hit{fill:transparent}\n.mk .pin{transition:.15s;stroke:var(--surface);stroke-width:2}\n.mk:hover .pin,.mk.active .pin{filter:brightness(1.08)}\n.mk:hover .ring,.mk.active .ring{opacity:1}\n.mk .ring{fill:none;stroke:var(--azure);stroke-width:2;opacity:0;transition:.15s}\n.mk text{font-size:9.5px;font-weight:700;fill:var(--text);paint-order:stroke;stroke:var(--surface);stroke-width:3px;stroke-linejoin:round}\n.ca-outline{fill:var(--surface-2);stroke:var(--line);stroke-width:1.5}\nhtml[data-theme=\"dark\"] .ca-outline{fill:#0c1c2e;stroke:#20344c}\n\n\/* Location list *\/\n.loclist{display:flex;flex-direction:column;gap:2px}\n.locrow{display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:12px;padding:11px 12px;border-radius:11px;cursor:pointer;transition:.13s;border:1px solid transparent}\n.locrow:hover{background:var(--surface-2);border-color:var(--line);transform:translateX(2px)}\n.locrow .lr-name{font-weight:700;font-size:13.5px}\n.locrow .lr-sub{font-size:11.5px;color:var(--muted);margin-top:1px}\n.locrow .lr-val{font-weight:700;font-size:14px;text-align:right}\n.locrow .lr-bar{grid-column:1\/-1;height:5px;border-radius:3px;background:var(--grid);overflow:hidden;margin-top:2px}\n.locrow .lr-bar i{display:block;height:100%;border-radius:3px;background:linear-gradient(90deg,var(--azure),#0f6fa0)}\n.locrow .chev{color:var(--muted)}\n\n\/* Funnel *\/\n.funnel{display:flex;flex-direction:column;gap:9px;margin-top:4px}\n.fstage{position:relative}\n.fstage .fbar{height:38px;border-radius:9px;display:flex;align-items:center;padding:0 13px;color:#fff;font-weight:700;font-size:13px;transition:width .6s cubic-bezier(.2,.8,.2,1);box-shadow:0 2px 8px rgba(14,34,64,.12)}\n.fstage .fmeta{position:absolute;right:0;top:50%;transform:translateY(-50%);display:flex;gap:12px;align-items:center;font-size:12px;color:var(--text-2);padding-right:4px}\n.fstage .fmeta b{color:var(--text);font-size:13px}\n.fstage .frate{font-size:11px;color:var(--muted);font-weight:600}\n\n\/* Legend *\/\n.legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:12px;font-size:12px;color:var(--text-2)}\n.legend .li{display:flex;align-items:center;gap:6px;font-weight:600}\n.legend .sw{width:11px;height:11px;border-radius:3px}\n.legend .sw.line{height:3px;width:16px;border-radius:2px}\n\n\/* partner segment cards *\/\n.seg-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}\n.seg-card{border:1px solid var(--line);border-radius:var(--radius-sm);padding:14px;cursor:pointer;transition:.15s;background:var(--surface)}\n.seg-card:hover{transform:translateY(-3px);box-shadow:var(--shadow)}\n.seg-card .sc-top{display:flex;align-items:center;gap:8px;font-size:12.5px;font-weight:700}\n.seg-card .sc-ic{width:26px;height:26px;border-radius:7px;display:grid;place-items:center}\n.seg-card .sc-ic svg{width:15px;height:15px;color:#fff}\n.seg-card .sc-val{font-size:23px;font-weight:700;letter-spacing:-.5px;margin-top:11px}\n.seg-card .sc-sub{font-size:11.5px;color:var(--muted);margin-top:3px}\n.seg-card .sc-count{font-size:11.5px;color:var(--text-2);font-weight:600;margin-top:9px;display:flex;justify-content:space-between;border-top:1px solid var(--line);padding-top:8px}\n\n\/* table *\/\ntable{width:100%;border-collapse:collapse;font-size:13px}\nth,td{text-align:right;padding:10px 10px;border-bottom:1px solid var(--line)}\nth{font-size:11px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);font-weight:700}\nth:first-child,td:first-child{text-align:left}\ntbody tr{cursor:pointer;transition:.12s}\ntbody tr:hover{background:var(--surface-2)}\ntd .pill{display:inline-block;font-size:11px;font-weight:700;padding:2px 8px;border-radius:20px}\n.tag-hi{background:rgba(12,163,12,.13);color:var(--good)}\n.tag-mid{background:rgba(28,156,216,.14);color:var(--azure-d)}\n\n\/* AI insight banner *\/\n.ai-banner{display:flex;gap:14px;align-items:flex-start;background:linear-gradient(100deg,rgba(28,156,216,.08),rgba(20,80,140,.05));border:1px solid var(--line);border-left:3px solid var(--azure);border-radius:var(--radius);padding:15px 18px;margin-bottom:16px}\n.ai-banner .ai-ic{flex:none;width:34px;height:34px;border-radius:9px;background:linear-gradient(135deg,var(--azure),var(--deep));display:grid;place-items:center;box-shadow:0 4px 12px rgba(28,156,216,.35)}\n.ai-banner .ai-ic svg{width:18px;height:18px;color:#fff}\n.ai-banner .ai-txt{font-size:13.5px;color:var(--text);line-height:1.55}\n.ai-banner .ai-txt b{color:var(--text)}\n.ai-banner .ai-txt .em{color:var(--azure-d);font-weight:700}\nhtml[data-theme=\"dark\"] .ai-banner .ai-txt .em{color:var(--azure)}\n.ai-banner .ai-tag{font-size:10.5px;font-weight:700;letter-spacing:1.4px;text-transform:uppercase;color:var(--azure-d);margin-bottom:3px;display:block}\n\n\/* tooltip *\/\n.tt{position:fixed;z-index:99;pointer-events:none;background:var(--navy);color:#fff;font-size:12px;padding:9px 12px;border-radius:9px;box-shadow:var(--shadow-lg);opacity:0;transition:opacity .1s;max-width:230px;line-height:1.45}\n.tt .tt-t{font-weight:700;margin-bottom:3px;font-size:12.5px}\n.tt .tt-r{display:flex;justify-content:space-between;gap:16px;color:#bcd4e6}\n.tt .tt-r b{color:#fff}\nhtml[data-theme=\"dark\"] .tt{background:#020a14;border:1px solid #1e3350}\n\n\/* misc *\/\n.chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;padding:5px 11px;border-radius:20px;background:var(--surface-2);border:1px solid var(--line);color:var(--text-2)}\n.chip .sw{width:9px;height:9px;border-radius:50%}\n.stat-inline{display:flex;gap:26px;flex-wrap:wrap;margin-top:2px}\n.stat-inline .si b{display:block;font-size:20px;font-weight:700;letter-spacing:-.4px}\n.stat-inline .si span{font-size:11.5px;color:var(--muted);font-weight:600}\n.backbtn{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;color:var(--azure);margin-bottom:12px}\n.backbtn svg{width:15px;height:15px}\n.gauge-track{height:9px;border-radius:5px;background:var(--grid);overflow:hidden;margin-top:8px}\n.gauge-track i{display:block;height:100%;border-radius:5px;background:linear-gradient(90deg,var(--s-state),#0f9c6d)}\n.note{font-size:11px;color:var(--muted);margin-top:12px;line-height:1.5}\n\/* engagement filters *\/\n.filters{display:flex;flex-wrap:wrap;gap:10px 18px;align-items:center;background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow);padding:12px 16px;margin-bottom:16px}\n.fgroup{display:flex;align-items:center;gap:8px}\n.fgroup>.flab{font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--muted)}\n.fseg{display:flex;background:var(--surface-2);border:1px solid var(--line);border-radius:9px;padding:3px;gap:2px}\n.fseg button{font-size:12.5px;font-weight:600;color:var(--text-2);padding:6px 12px;border-radius:6px;transition:.14s;white-space:nowrap}\n.fseg button.on{background:var(--azure);color:#04141f}\n.fsel{font-family:inherit;font-size:13px;font-weight:600;color:var(--text);background:var(--surface-2);border:1px solid var(--line);border-radius:8px;padding:7px 11px;cursor:pointer;outline:none}\n.fspace{flex:1}\n\/* A\/B test card *\/\n.ab-card{border:1px solid var(--line);border-radius:var(--radius-sm);padding:15px 16px;background:var(--surface)}\n.ab-head{display:flex;justify-content:space-between;align-items:flex-start;gap:10px;margin-bottom:12px}\n.ab-head .abt{font-size:13.5px;font-weight:700}\n.ab-head .abs{font-size:11px;color:var(--muted);margin-top:2px}\n.ab-badge{font-size:11px;font-weight:800;padding:4px 9px;border-radius:20px;white-space:nowrap}\n.ab-win{background:rgba(12,163,12,.14);color:var(--good)}\n.ab-none{background:rgba(139,150,165,.16);color:var(--text-2)}\n.ab-var{display:grid;grid-template-columns:auto 1fr auto;gap:10px;align-items:center;padding:9px 0;border-top:1px solid var(--line)}\n.ab-tag{width:22px;height:22px;border-radius:6px;display:grid;place-items:center;font-size:12px;font-weight:800;color:#fff}\n.ab-nm{font-size:12.5px;font-weight:600}\n.ab-sub{font-size:11px;color:var(--muted)}\n.ab-conv{text-align:right}\n.ab-conv b{font-size:19px;font-weight:700;letter-spacing:-.5px}\n.ab-conv span{font-size:10.5px;color:var(--muted);display:block}\n.ab-bar{grid-column:1\/-1;height:6px;border-radius:3px;background:var(--grid);overflow:hidden;margin-top:-2px}\n.ab-bar i{display:block;height:100%;border-radius:3px}\n.ab-foot{display:flex;justify-content:space-between;font-size:11.5px;color:var(--text-2);margin-top:11px;border-top:1px solid var(--line);padding-top:10px}\n.ab-foot b{color:var(--text)}\n.conv-pill{font-size:11px;font-weight:700;padding:2px 8px;border-radius:20px;background:rgba(28,156,216,.14);color:var(--azure-d)}\nhtml[data-theme=\"dark\"] .conv-pill{color:var(--azure)}\n.toggle-tbl{font-size:11.5px;color:var(--azure-d);font-weight:600;cursor:pointer}\nhtml[data-theme=\"dark\"] .toggle-tbl{color:var(--azure)}\n.hidden{display:none!important}\n.fade{animation:fade .35s ease}\n@keyframes fade{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}\n.footer{max-width:1320px;margin:0 auto;padding:20px 24px 40px;color:var(--muted);font-size:11.5px;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;border-top:1px solid var(--line);margin-top:20px}\n\n@media (max-width:1080px){\n  .kpis{grid-template-columns:1fr 1fr}.row-2,.row-map,.row-3{grid-template-columns:1fr}.seg-cards{grid-template-columns:1fr 1fr}\n}\n@media print{\n  .topbar,.cta,.icon-btn,.segset,.toggle-tbl{display:none!important}\n  body{background:#fff}.card{box-shadow:none;border:1px solid #ccc;break-inside:avoid}\n}\n\n\/* ===================== TrueOmni Design System overrides ===================== *\/\n.eyebrow{color:var(--azure)!important;text-transform:uppercase;letter-spacing:.12em;font-weight:600}\n.eyebrow::before{content:'\\2500\\2500  ';color:var(--azure);font-weight:700;letter-spacing:0}\n.card{border-top:3px solid var(--azure)}\n.pagehead h1{letter-spacing:-.01em}\n.pagehead h1::after{content:'\\2022';color:var(--azure);margin-left:3px;font-weight:900}\n.crumbs a{color:var(--azure)}\n<\/style>\n<\/head>\n<body>\n\n<!-- =================== TOP BAR =================== -->\n<header class=\"topbar\">\n  <div class=\"topbar-inner\">\n    <div class=\"brand\">\n      <span class=\"mark\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M3 20h18M6 20V10m4 10V5m4 15v-7m4 7V8\" stroke=\"#fff\" stroke-width=\"2.4\" stroke-linecap=\"round\"\/><\/svg><\/span>\n      <div>\n        <div class=\"name\">True<b>Impact<\/b><\/div>\n        <div class=\"sub\">Destination Performance Intelligence<\/div>\n      <\/div>\n    <\/div>\n    <label class=\"destpick\" style=\"display:none\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M12 21s-7-6.2-7-11a7 7 0 1114 0c0 4.8-7 11-7 11z\" stroke=\"currentColor\" stroke-width=\"1.8\"\/><circle cx=\"12\" cy=\"10\" r=\"2.5\" stroke=\"currentColor\" stroke-width=\"1.8\"\/><\/svg><select id=\"destSel\"><\/select><\/label>\n    <span class=\"sample\" title=\"Modeled pre-deployment projection for a Phase-1 build of 10 kiosks + 5 signage screens. Destination context is cited from Visit Lauderdale; all network figures are projections. Direct spend only \u2014 no ripple applied.\">MODELED&nbsp;PROJECTION<\/span>\n    <div class=\"live\"><span class=\"dot\"><\/span> PHASE 1 DEPLOYMENT PLAN \u00b7 10 kiosks + 5 screens \u00b7 Greater Fort Lauderdale<\/div>\n    <div class=\"spacer\"><\/div>\n    <div class=\"controls\">\n      <div class=\"segset\" id=\"periodSel\">\n        <button data-p=\"q\" >Opening quarter<\/button>\n        <button data-p=\"ytd\">First 6 months<\/button>\n        <button data-p=\"t12\" class=\"on\">Year-one run-rate<\/button>\n      <\/div>\n      <button class=\"icon-btn\" id=\"themeBtn\" title=\"Toggle theme\">\n        <svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M21 12.8A9 9 0 1111.2 3a7 7 0 009.8 9.8z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n      <\/button>\n      <button class=\"cta\" onclick=\"window.print()\">\n        <svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 9V3h12v6M6 18H4v-6h16v6h-2M8 14h8v7H8z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n        Board Briefing\n      <\/button>\n    <\/div>\n  <\/div>\n<\/header>\n\n<div class=\"wrap\">\n  <div id=\"app\"><\/div>\n<\/div>\n\n<footer class=\"footer\">\n  <div>\u00a9 TrueOmni 2026 \u00b7 TrueImpact Executive Dashboard \u00b7 Scottsdale, AZ \u00b7 www.trueomni.com \u00b7 833-300-6664<\/div>\n  <div>Destination context cited from Visit Lauderdale (2025) \u00b7 20.9M is <b>travelers into the destination<\/b>, not overnight or unique visitors \u00b7 all network, tax, advertising and signage figures are modeled pre-deployment projections \u00b7 <b>direct visitor spend only, no ripple multiplier applied<\/b> \u00b7 figures in USD.<\/div>\n<\/footer>\n\n<div class=\"tt\" id=\"tt\"><\/div>\n\n<!-- =================== DATA MODEL =================== -->\n<script id=\"model\">\n\/* ------------------------------------------------------------------\n   TrueImpact data model \u2014 VISIT LAUDERDALE edition.  Single tenant, USD.\n   PRE-DEPLOYMENT: Phase 1 is 10 interactive kiosks + 5 digital signage\n   screens across six districts. Every network figure is a PROJECTION.\n\n   CITED \u2014 Visit Lauderdale, 2025:\n     20.9M+ TRAVELERS into the destination \u00b7 $124M Tourist Development Tax\n     revenue \u00b7 hotel demand up 1.3% \u00b7 38,000+ lodging units \u00b7 31\n     municipalities \u00b7 300+ miles of navigable waterways \u00b7 24 miles of\n     beaches \u00b7 4,000+ restaurants \u00b7 48 conventions booked representing\n     265,000 hotel room nights and $801M of economic impact.\n     Port Everglades: 4.7M cruise guests in FY2025, a record, making it one\n     of the busiest cruise homeports in the world.\n\n   TERMINOLOGY \u2014 Visit Lauderdale describes 20.9M as TRAVELERS into the\n   destination, not overnight visitors and not unique visitors. That wording\n   is preserved everywhere on this dashboard. It represents total visitor\n   opportunity, which is the right base for a kiosk network, but it is not\n   an overnight-visitor count and must never be presented as one.\n\n   DERIVED BY ARITHMETIC on cited figures (shown as derived, not cited):\n     $124M TDT \/ 6.0% Broward rate = ~$2.07B of taxable lodging revenue.\n     $2.07B \/ 20.9M travelers = ~$99 of lodging spend per traveler.\n     $801M \/ 265,000 convention room nights = ~$3,023 of economic impact\n     per convention room night.\n   NOTE: Visit Lauderdale publishes no citywide visitor-spending or total-\n   economic-impact figure that we could locate, so there is NO per-traveler\n   spend anchor of the kind most large-metro destinations publish. Per-district\n   spend below is modeled from placement type and disclosed as such.\n\n   DIRECT SPEND ONLY. INFL_RATE is set to 0 and total === direct. Neither\n   this destination nor any public source gives a total-economic-impact\n   figure we could derive a defensible ripple multiplier from, so rather\n   than import a generic 0.85 benchmark this model attributes only direct\n   visitor spend. Every headline number here is therefore SMALLER than a\n   conventional impact model would produce \u2014 and there is no multiplier\n   for anyone to argue with.\n------------------------------------------------------------------ *\/\nconst FUNNEL = { highIntent:0.17, conversion:0.14 };  \/\/ TrueOmni platform benchmarks\nconst INFL_RATE = 0;      \/\/ DIRECT SPEND ONLY \u2014 no ripple applied. See header.\n\n\/* Greater Fort Lauderdale capture mix. Lodging is weighted higher than the\n   other TrueImpact markets because the Tourist Development Tax is the\n   destination's headline public-revenue number and the network sits in\n   beach, cruise and convention districts where lodging decisions are live.\n   Rental is higher too \u2014 this is a drive-and-rent market. *\/\nconst LODGING_SHARE = 0.30;\nconst DINING_RETAIL_SHARE = 0.55;\nconst RENTAL_SHARE = 0.07;\nconst STATE_INCOME_RATE = 0.005;    \/\/ Florida levies NO personal income tax; this is a modeled corporate-income capture only\nconst RECIRC_MULT = 0;   \/\/ DIRECT SPEND ONLY \u2014 no induced\/re-circulation multiplier either.\n                         \/\/ A build that advertises \"no ripple anywhere\" cannot ship a 0.9x\n                         \/\/ induced figure on the tax page. Second-round effects are OUT.\n\n\/* Broward County \/ Florida tax schedule \u2014 rates live on each district as\n   tax:{tot,sales,rental}:\n     tot    = 6.0%  Broward County Tourist Development Tax. 100% LOCAL, and\n              the number this organisation is measured on \u2014 it funds Visit\n              Lauderdale, the Broward County Convention Center and beach\n              renourishment. The destination collected $124M of it in 2025.\n     sales  = 7.0%  combined (6.0pp Florida state + 1.0pp Broward County).\n              Local share = 1.0 of 7.0 points.\n     rental = 7.0%  sales tax as applied to vehicle rentals. Florida's flat\n              $2.00-per-day rental car surcharge is NOT modeled, which\n              understates this line.\n   Florida has no personal income tax, so the fourth line is a small modeled\n   corporate-income capture rather than a statutory personal rate. The\n   taxable base is attributed direct spend \u2014 there is no ripple in this model. *\/\nconst TAX_LOC = {tot:6.0, sales:7.0, rental:7.0};\nconst HOTEL_LOCAL_SHARE  = 1.0;\nconst SALES_LOCAL_SHARE  = 1.0\/7.0;\nconst RENTAL_LOCAL_SHARE = 1.0\/7.0;\n\/* The Tourist Development Tax is 100% local and is the mechanism that funds\n   Visit Lauderdale itself, the Convention Center and beach renourishment.\n   The full TDT line is therefore the destination-funding line. *\/\nconst DMO_SHARE = 1.0;\n\nconst PERIODS = {\n  t12:{label:'Year-one run-rate \u00b7 full Phase-1 deployment',k:1},\n  ytd:{label:'First six months live (Jan\u2013Jun shape)',k:0.5533},\n  q:{label:'Opening quarter (Jan\u2013Mar shape)',k:0.3050}\n};\n\n\/* The destination's own published figures \u2014 the only cited data here. *\/\nconst CONTEXT = [\n  {v:'20.9M+',  l:'Travelers into the destination'},\n  {v:'$124M',   l:'Tourist Development Tax'},\n  {v:'4.7M',    l:'Port Everglades cruise guests'},\n  {v:'38,000+', l:'Lodging units'},\n  {v:'31',      l:'Municipalities'},\n  {v:'4,000+',  l:'Restaurants'},\n];\n\n\/* Cruise & Port Everglades \u2014 cited figures for the secondary-segment page. *\/\nconst SEGDATA = {\n  cruiseGuests:4_700_000, tdt:124_000_000, tdtRate:0.06, travelers:20_900_000,\n  lodgingUnits:38_000, municipalities:31, waterwayMiles:300, beachMiles:24,\n  restaurants:4_000, conventions:48, conventionRoomNights:265_000,\n  conventionImpact:801_000_000, hotelDemandGrowth:0.013,\n  zoneIds:['port'], laterIds:['fll','cruiseexp'],\n};\n\nconst SEG = {\n  hotels:      {name:'Hotels & Lodging',      color:'var(--s-hotels)',      raw:'#1C9CD8'},\n  dining:      {name:'Restaurants & Dining',  color:'var(--s-dining)',      raw:'#EB6834'},\n  attractions: {name:'Attractions & Culture', color:'var(--s-attractions)', raw:'#008300'},\n  events:      {name:'Events, Cruise & Marine',color:'var(--s-events)',     raw:'#E87BA4'},\n};\n\n\/* Greater Fort Lauderdale seasonality: winter and early-spring peak (snowbird and cruise season), soft late summer. Sums to exactly 12.0; Jan-Jun = 6.64 and Jan-Mar = 3.66, which is what the ytd and q period scales are set from. *\/\nconst SEASON = { d:[1.16,1.24,1.26,1.14,.98,.86,.88,.84,.72,.86,1.00,1.06] };\nconst MONTHS=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];\n\n\/* ==================================================================\n   DESTINATION REGISTRY \u2014 single tenant.\n   locations[] = the six PHASE-1 districts (10 kiosks + 5 screens).\n   exposures  = modeled annual visitor encounters at that district.\n   screens    = non-interactive OMNI Signage displays (broadcast only).\n================================================================== *\/\nconst DESTINATIONS = {\n  d:{ name:'Visit Lauderdale', kind:'Destination Marketing Organization', scope:'Greater Fort Lauderdale', region:'Broward County, FL',\n    outline:'M150,60 L410,60 L410,560 L150,560 Z',\n    lake:'M410,60 L474,60 L474,560 L410,560 Z',\n    lakeLabel:{x:442,y:310,t:'Atlantic'},\n    locations:[\n      {id:'beach', name:'Fort Lauderdale Beach & A1A', sites:'Beachfront promenade \u00b7 A1A \u00b7 Las Olas Beach', x:382,y:262, kiosks:2, screens:1, phase:1, exposures:1_150_000, interactionRate:.014, avgSpend:215, tax:TAX_LOC, season:'d', mix:{hotels:0.34,dining:0.36,attractions:0.22,events:0.08}, note:'Twenty-four miles of beach, and the district every traveler photograph is taken in. Very high transient footfall at a low interaction rate \u2014 the archetypal signage-plus-kiosk placement.', badge:'Phase 1'},\n      {id:'lasolas', name:'Las Olas Boulevard', sites:'Boutiques \u00b7 galleries \u00b7 restaurant row', x:330,y:272, kiosks:2, screens:1, phase:1, exposures:850_000, interactionRate:.020, avgSpend:185, tax:TAX_LOC, season:'d', mix:{hotels:0.22,dining:0.50,attractions:0.20,events:0.08}, note:'The walkable dining and retail spine between downtown and the beach. Higher intent than the beachfront because visitors arrive already deciding where to eat.', badge:'Phase 1'},\n      {id:'port', name:'Port Everglades', sites:'Cruise terminals \u00b7 arrivals & departures halls', x:368,y:352, kiosks:2, screens:1, phase:1, exposures:1_100_000, interactionRate:.026, avgSpend:95, tax:TAX_LOC, season:'d', mix:{hotels:0.30,dining:0.34,attractions:0.28,events:0.08}, note:'4.7 million cruise guests in FY2025, a record, at one of the busiest homeports in the world. A captive audience in a waiting hall with hours to fill \u2014 the highest-conversion moment in the destination, and per-visitor spend is deliberately modeled low because most of the trip is spent at sea.', badge:'Phase 1'},\n      {id:'convention', name:'Broward County Convention Center', sites:'Registration \u00b7 expanded exhibit concourse', x:352,y:332, kiosks:2, screens:1, phase:1, exposures:380_000, interactionRate:.070, avgSpend:310, tax:TAX_LOC, season:'d', mix:{hotels:0.40,dining:0.28,attractions:0.12,events:0.20}, note:'The newly expanded convention centre. Visit Lauderdale booked 48 conventions here in 2025 representing 265,000 room nights and $801M of economic impact. Highest interaction rate and highest per-visitor spend in the network.', badge:'Phase 1'},\n      {id:'hollywood', name:'Hollywood Beach Broadwalk', sites:'Broadwalk promenade \u00b7 beachfront dining', x:364,y:448, kiosks:1, screens:1, phase:1, exposures:620_000, interactionRate:.015, avgSpend:150, tax:TAX_LOC, season:'d', mix:{hotels:0.28,dining:0.42,attractions:0.22,events:0.08}, note:'A 2.5-mile beachfront promenade in one of the 31 municipalities Visit Lauderdale represents \u2014 and the clearest test of whether the network can serve the whole county rather than just the city.', badge:'Phase 1'},\n      {id:'riverwalk', name:'Riverwalk & Downtown Fort Lauderdale', sites:'Riverwalk \u00b7 Broward Center \u00b7 New River', x:322,y:306, kiosks:1, screens:0, phase:1, exposures:560_000, interactionRate:.022, avgSpend:175, tax:TAX_LOC, season:'d', mix:{hotels:0.24,dining:0.38,attractions:0.26,events:0.12}, note:'The New River waterfront, the arts centre and the downtown core. Kiosk only in Phase 1 \u2014 a screen adds little on a linear riverwalk where a kiosk can anchor the route.', badge:'Phase 1'},\n    ]},\n};\n\nlet activeDest='d';\nfunction ACT(){return DESTINATIONS[activeDest];}\nfunction LOCS(){return ACT().locations;}\nfunction findLoc(id){return LOCS().find(l=>l.id===id);}\n\n\/* ==================================================================\n   EXPANSION ZONES \u2014 Phase 2 and Phase 3 candidates, MODELED. Held OUT\n   of LOCS() so every headline, tax, partner and segment page reflects\n   only the Phase-1 build. They appear solely in the growth ladder.\n   No hotel property is named anywhere.\n================================================================== *\/\nconst EXPANSION = [\n      {id:'fll', name:'Fort Lauderdale\u2013Hollywood International Airport', sites:'Terminals 1\u20134 \u00b7 baggage claim \u00b7 cruise transfer', x:280,y:398, kiosks:3, screens:2, phase:2, exposures:2_400_000, interactionRate:.016, avgSpend:230, tax:TAX_LOC, season:'d', mix:{hotels:0.34,dining:0.28,attractions:0.24,events:0.14}, note:'The airport that feeds both the beach and the cruise port, and the first surface most arriving travelers touch. Modeled as the largest single addition available in Phase 2.', badge:'Phase 2'},\n      {id:'sawgrass', name:'Sawgrass Mills & Sunrise', sites:'Outlet centre \u00b7 Amerant Bank Arena', x:186,y:300, kiosks:2, screens:1, phase:2, exposures:1_450_000, interactionRate:.012, avgSpend:195, tax:TAX_LOC, season:'d', mix:{hotels:0.14,dining:0.52,attractions:0.24,events:0.10}, note:'A major outlet centre and a well-known draw for international and cruise-passenger shopping trips.', badge:'Phase 2'},\n      {id:'pompano', name:'Pompano Beach & Deerfield Beach', sites:'Pompano Beach Pier \u00b7 Deerfield Beach Pier', x:376,y:150, kiosks:2, screens:1, phase:2, exposures:820_000, interactionRate:.014, avgSpend:165, tax:TAX_LOC, season:'d', mix:{hotels:0.30,dining:0.38,attractions:0.24,events:0.08}, note:'The northern beach municipalities \u2014 the same product as Fort Lauderdale Beach, with none of the network presence.', badge:'Phase 2'},\n      {id:'hotelsbeach', name:'Beachfront hotel lobbies', sites:'A1A and beachfront properties \u00b7 rooms TBD', x:390,y:290, kiosks:2, screens:2, phase:2, exposures:620_000, interactionRate:.055, avgSpend:290, tax:TAX_LOC, season:'d', mix:{hotels:0.36,dining:0.36,attractions:0.18,events:0.10}, note:'Lobby placements are modeled as the highest-interaction surface in this build, and they sit directly on the Tourist Development Tax base.', badge:'Phase 2'},\n      {id:'dania', name:'Hollywood & Dania Beach downtowns', sites:'Hollywood Circle \u00b7 Dania Beach Main St', x:340,y:420, kiosks:1, screens:1, phase:2, exposures:480_000, interactionRate:.018, avgSpend:145, tax:TAX_LOC, season:'d', mix:{hotels:0.24,dining:0.44,attractions:0.24,events:0.08}, note:'Two walkable municipal downtowns between the airport and the southern beaches.', badge:'Phase 2'},\n      {id:'marinas', name:'Marinas & waterway hubs', sites:'300+ miles of navigable waterways \u00b7 water taxi stops', x:300,y:340, kiosks:2, screens:2, phase:3, exposures:740_000, interactionRate:.020, avgSpend:225, tax:TAX_LOC, season:'d', mix:{hotels:0.26,dining:0.30,attractions:0.20,events:0.24}, note:'The 300 miles of navigable waterway that give the destination its identity, and the water-taxi network that already moves visitors between districts.', badge:'Phase 3'},\n      {id:'western', name:'Coral Springs, Plantation & the western cities', sites:'Municipal centres \u00b7 regional parks', x:176,y:196, kiosks:2, screens:1, phase:3, exposures:560_000, interactionRate:.014, avgSpend:130, tax:TAX_LOC, season:'d', mix:{hotels:0.18,dining:0.42,attractions:0.30,events:0.10}, note:'Extends the network into the western municipalities so the 31-city mandate is genuinely covered rather than claimed.', badge:'Phase 3'},\n      {id:'everglades', name:'Everglades gateway', sites:'Sawgrass Recreation \u00b7 airboat & eco-tour operators', x:166,y:438, kiosks:1, screens:1, phase:3, exposures:320_000, interactionRate:.028, avgSpend:185, tax:TAX_LOC, season:'d', mix:{hotels:0.14,dining:0.20,attractions:0.58,events:0.08}, note:'The eco-tourism gateway west of the county \u2014 a ticketed, out-of-town-weighted audience with a strong booking moment.', badge:'Phase 3'},\n      {id:'cruiseexp', name:'Cruise terminal expansion', sites:'Additional terminals & pre-cruise waiting halls', x:384,y:376, kiosks:2, screens:2, phase:3, exposures:1_320_000, interactionRate:.026, avgSpend:110, tax:TAX_LOC, season:'d', mix:{hotels:0.30,dining:0.34,attractions:0.28,events:0.08}, note:'Doubles the presence at the single highest-conversion moment in the destination \u2014 a cruise guest in a waiting hall.', badge:'Phase 3'},\n      {id:'hotelscounty', name:'Countywide hotel lobbies', sites:'Airport, western and northern clusters \u00b7 rooms TBD', x:252,y:240, kiosks:3, screens:2, phase:3, exposures:980_000, interactionRate:.048, avgSpend:245, tax:TAX_LOC, season:'d', mix:{hotels:0.36,dining:0.36,attractions:0.18,events:0.10}, note:'38,000+ lodging units countywide. Extends the lobby model beyond the beachfront.', badge:'Phase 3'},\n];\nfunction ALLZ(){return LOCS().concat(EXPANSION);}\n\nconst LIVE_IDS = DESTINATIONS.d.locations.map(L=>L.id);\nconst P2_IDS = ['fll','sawgrass','pompano','hotelsbeach','dania'];\nconst P3_IDS = ['marinas','western','everglades','cruiseexp','hotelscounty'];\nconst PHASES=[\n  {n:1, name:'Phase 1 \u00b7 10 kiosks + 5 screens',  window:'six districts \u00b7 beach, Las Olas, port & convention centre', ids:LIVE_IDS},\n  {n:2, name:'Phase 2 \u00b7 20 kiosks + 12 screens', window:'+10 kiosks \u00b7 airport, outlets, northern beaches, lobbies', ids:LIVE_IDS.concat(P2_IDS)},\n  {n:3, name:'Phase 3 \u00b7 30 kiosks + 20 screens', window:'+10 kiosks \u00b7 waterways, western cities, Everglades, terminals', ids:LIVE_IDS.concat(P2_IDS,P3_IDS)},\n];\nfunction phaseLocs(ph){return ALLZ().filter(L=>ph.ids.includes(L.id));}\n\n\/* ==================================================================\n   MEDIA NETWORK \u2014 interactive kiosk advertising. CPM-driven, because\n   these districts differ enormously in footfall per unit. MODELED.\n================================================================== *\/\nconst KIOSKS_P1 = 10, SCREENS_P1 = 5;\nconst CAPEX_PER_KIOSK = 9_500, CAPEX_PER_SCREEN = 4_200;\nconst SW_PER_KIOSK_YR = 1_400, SW_PER_SCREEN_YR = 600;\nconst MEDIA={\n  cpmBand:{floor:12, mid:20, strong:30},\n  imprPerEncounter:2.6,\n  fillMature:0.70,\n  fillByPhase:{1:0.50, 2:0.62, 3:0.70},\n  streamSplit:{direct:0.58, programmatic:0.42},\n  softwarePerKioskYr:SW_PER_KIOSK_YR, capitalPerKiosk:CAPEX_PER_KIOSK,\n  get softwareYr(){return SW_PER_KIOSK_YR*KIOSKS_P1 + SW_PER_SCREEN_YR*SCREENS_P1;},\n  get capital(){return CAPEX_PER_KIOSK*KIOSKS_P1 + CAPEX_PER_SCREEN*SCREENS_P1;},\n  inventory:[\n    {id:'video',    name:'Full-screen attract-loop video',        share:.30},\n    {id:'banner',   name:'Home & footer banners',                 share:.22},\n    {id:'sponsored',name:'Sponsored dining & attraction listings',share:.20},\n    {id:'rail',     name:'Events \/ listings side-rail',           share:.16},\n    {id:'qr',       name:'QR-conversion sponsorships',            share:.12},\n  ],\n};\nfunction mediaMetrics(k=1, locs=LOCS()){\n  const r=rollup(k,locs);\n  const kiosks=r.kiosks;\n  const impressions=r.exposures*MEDIA.imprPerEncounter;\n  const sold=impressions*MEDIA.fillMature;\n  const band={}; for(const s in MEDIA.cpmBand) band[s]=sold\/1000*MEDIA.cpmBand[s];\n  const rev=band.mid;\n  const cpm=impressions? rev\/(impressions\/1000):0;\n  const perKioskMo=kiosks? rev\/kiosks\/12 : 0;\n  const byStream={direct:rev*MEDIA.streamSplit.direct, programmatic:rev*MEDIA.streamSplit.programmatic};\n  const byInv=MEDIA.inventory.map(it=>({...it, v:rev*it.share}));\n  return {kiosks,impressions,sold,band,rev,cpm,perKioskMo,byStream,byInv};\n}\n\n\/* ==================================================================\n   DIGITAL SIGNAGE \u2014 non-interactive broadcast screens (OMNI Signage).\n   Screens deliver REACH and ad inventory; they do NOT transact.\n   Signage impressions never enter interactions, conversions, attributed\n   spend or tax. Only districts that actually carry a screen contribute\n   to reach.\n================================================================== *\/\nconst SIGNAGE={\n  enabled:true,\n  perScreenMo:{floor:150, mid:320, strong:560},\n  loopSec:60, spotSec:15,\n  viewShare:0.55,\n  imprPerPasserby:1.4,\n  dwellSec:8,\n  fillMature:0.68, fillByPhase:{1:0.42, 2:0.56, 3:0.68},\n  capitalPerScreen:CAPEX_PER_SCREEN, softwarePerScreenYr:SW_PER_SCREEN_YR,\n  formats:[\n    {id:'wall',     name:'Video wall \/ district hero',       share:.34},\n    {id:'portrait', name:'Portrait posters (concourse)',     share:.28},\n    {id:'wayfind',  name:'Wayfinding & directory boards',    share:.20},\n    {id:'menu',     name:'Dining & retail promo boards',     share:.12},\n    {id:'outdoor',  name:'Outdoor \/ weatherized',            share:.06},\n  ],\n};\nfunction signageMetrics(k=1, locs=LOCS()){\n  if(!SIGNAGE.enabled) return null;\n  const withScreens=locs.filter(L=>(L.screens||0)>0);\n  const screens=withScreens.reduce((s,L)=>s+(L.screens||0),0);\n  const reach=withScreens.reduce((s,L)=>s+L.exposures,0)*k*SIGNAGE.viewShare;\n  const impressions=reach*SIGNAGE.imprPerPasserby;\n  const band={}; for(const s in SIGNAGE.perScreenMo) band[s]=screens*SIGNAGE.perScreenMo[s]*12*k;\n  const rev=band.mid;\n  const cpm=impressions? rev\/(impressions\/1000):0;\n  const spotsPerLoop=Math.round(SIGNAGE.loopSec\/SIGNAGE.spotSec);\n  const shareOfVoice=1\/spotsPerLoop;\n  const capital=screens*SIGNAGE.capitalPerScreen, softwareYr=screens*SIGNAGE.softwarePerScreenYr;\n  const byFormat=SIGNAGE.formats.map(f=>({...f, v:rev*f.share}));\n  const perScreenMo=screens? rev\/screens\/12 : 0;\n  return {screens,reach,impressions,band,rev,cpm,perScreenMo,spotsPerLoop,shareOfVoice,capital,softwareYr,byFormat};\n}\nfunction totalAdRevenue(k=1, locs=LOCS()){\n  const km=mediaMetrics(k,locs), sg=signageMetrics(k,locs);\n  return {kiosk:km.rev, signage:sg?sg.rev:0, total:km.rev+(sg?sg.rev:0), km, sg};\n}\nfunction buildCost(locs){\n  const kiosks=locs.reduce((s,L)=>s+L.kiosks,0);\n  const screens=locs.reduce((s,L)=>s+(L.screens||0),0);\n  return {kiosks, screens,\n    capital:kiosks*CAPEX_PER_KIOSK + screens*CAPEX_PER_SCREEN,\n    softwareYr:kiosks*SW_PER_KIOSK_YR + screens*SW_PER_SCREEN_YR};\n}\nfunction phase1Payback(){\n  const c=buildCost(LOCS()), ad=totalAdRevenue(1, LOCS());\n  const net=ad.total-c.softwareYr;\n  return {capital:c.capital, softwareYr:c.softwareYr, ad:ad.total, net, years: net>0? c.capital\/net : 0};\n}\nfunction growthPayback(){\n  const now=buildCost(LOCS()), full=buildCost(phaseLocs(PHASES[2]));\n  const adNow=totalAdRevenue(1, LOCS()).total, adFull=totalAdRevenue(1, phaseLocs(PHASES[2])).total;\n  const capex=full.capital-now.capital;\n  const gross=adFull-adNow;\n  const gain=gross-(full.softwareYr-now.softwareYr);\n  return {capex, gross, gain, adNow, adFull, years: gain>0? capex\/gain : 0};\n}\n\n\/* ---- derive all metrics for one district at a period scale ---- *\/\nfunction deriveMetrics(L, k=1){\n  const exposures = L.exposures*k;\n  const interactions = exposures*L.interactionRate;\n  const highIntent = interactions*FUNNEL.highIntent;\n  const conversions = highIntent*FUNNEL.conversion;\n  const directSpend = conversions*L.avgSpend;    \/\/ attributed DIRECT visitor spend\n  const direct = directSpend;\n  const influenced = directSpend*INFL_RATE;      \/\/ === 0, direct-spend-only build\n  const total = direct+influenced;               \/\/ === direct\n  const r = L.tax;\n  const taxableSpend = directSpend + influenced*0.5;   \/\/ === directSpend here\n  const hotelTOT   = taxableSpend*LODGING_SHARE*(r.tot\/100);\n  const salesTax   = taxableSpend*DINING_RETAIL_SHARE*(r.sales\/100);\n  const rentalTax  = taxableSpend*RENTAL_SHARE*(r.rental\/100);\n  const inc        = total*STATE_INCOME_RATE;\n  const localSub   = hotelTOT*HOTEL_LOCAL_SHARE\n                   + salesTax*SALES_LOCAL_SHARE\n                   + rentalTax*RENTAL_LOCAL_SHARE;\n  const stateTax   = inc\n                   + hotelTOT*(1-HOTEL_LOCAL_SHARE)\n                   + salesTax*(1-SALES_LOCAL_SHARE)\n                   + rentalTax*(1-RENTAL_LOCAL_SHARE);\n  const tax        = localSub+stateTax;\n  const dmoFund    = hotelTOT*DMO_SHARE;         \/\/ the slice that reaches the DMO itself\n  const recirc     = tax*RECIRC_MULT;\n  const taxLocal=localSub, taxState=stateTax;\n  const taxBreak = {hotelTOT,salesTax,rentalTax,stateTax:inc};\n  const partners = {};\n  for(const s in L.mix){ partners[s] = directSpend*L.mix[s]; }   \/\/ no influenced tail in a direct-only model\n  const revAssist = L.kiosks*1500 + (L.kiosks>5?L.kiosks*1100:L.kiosks*700);\n  return {exposures,interactions,highIntent,conversions,directSpend,direct,influenced,total,taxLocal,taxState,tax,taxBreak,dmoFund,recirc,partners,revAssist};\n}\nfunction seriesFor(L,k=1){\n  const m=deriveMetrics(L,k); const sh=SEASON[L.season];\n  const base=m.total\/sh.reduce((a,b)=>a+b,0);\n  return [0,1,2,3,4,5,6,7,8,9,10,11].map(i=>base*sh[i]);\n}\nfunction rollup(k=1, locs=LOCS()){\n  const acc={exposures:0,interactions:0,highIntent:0,conversions:0,directSpend:0,direct:0,influenced:0,total:0,taxLocal:0,taxState:0,tax:0,dmoFund:0,recirc:0,revAssist:0,kiosks:0,screens:0,\n    partners:{hotels:0,dining:0,attractions:0,events:0}, taxBreak:{hotelTOT:0,salesTax:0,rentalTax:0,stateTax:0}};\n  locs.forEach(L=>{const m=deriveMetrics(L,k);for(const key in acc){\n    if(key==='partners'){for(const s in acc.partners)acc.partners[s]+=m.partners[s];}\n    else if(key==='taxBreak'){for(const s in acc.taxBreak)acc.taxBreak[s]+=m.taxBreak[s];}\n    else if(key==='kiosks'){acc.kiosks+=L.kiosks;}\n    else if(key==='screens'){acc.screens+=(L.screens||0);}\n    else if(m[key]!=null)acc[key]+=m[key];}});\n  return acc;\n}\nfunction rollupSeries(k=1, locs=LOCS()){\n  const t=new Array(12).fill(0);\n  locs.forEach(L=>{const s=seriesFor(L,k);s.forEach((v,i)=>t[i]+=v);});\n  return t;\n}\n\n\/* ---------- (network roll-up retained but unreachable in single-tenant) ---------- *\/\nconst NETWORK={ destinations:1, kiosks:30, targetTotal:0, targetTax:0, targetActions:0 };\nfunction mergeRollups(list){\n  const acc={exposures:0,interactions:0,highIntent:0,conversions:0,directSpend:0,direct:0,influenced:0,total:0,taxLocal:0,taxState:0,tax:0,dmoFund:0,recirc:0,revAssist:0,kiosks:0,screens:0,\n    partners:{hotels:0,dining:0,attractions:0,events:0}, taxBreak:{hotelTOT:0,salesTax:0,rentalTax:0,stateTax:0}};\n  list.forEach(m=>{for(const key in acc){\n    if(key==='partners'){for(const s in acc.partners)acc.partners[s]+=m.partners[s];}\n    else if(key==='taxBreak'){for(const s in acc.taxBreak)acc.taxBreak[s]+=m.taxBreak[s];}\n    else if(m[key]!=null)acc[key]+=m[key];}});\n  return acc;\n}\nfunction scaleRollup(m,f){const o=JSON.parse(JSON.stringify(m));for(const key in o){ if(key==='partners'||key==='taxBreak'){for(const s in o[key])o[key][s]*=f;} else o[key]*=f; }return o;}\nconst NET_IDS=['d'];\nfunction destRollup(id,k){return rollup(k, DESTINATIONS[id].locations);}\nfunction networkAgg(k=1){const parts=NET_IDS.map(id=>destRollup(id,k));const real=mergeRollups(parts);const net=scaleRollup(real,1);net.kiosks=real.kiosks;net.destinations=1;return {real,net,others:scaleRollup(real,0),scale:1,parts};}\nfunction networkSeries(k=1){return rollupSeries(k, LOCS());}\n\n\/* ==================================================================\n   ENGAGEMENT DRILL-THROUGH. Pre-deployment: module and campaign shares\n   are MODELED from the Phase-1 district mix. Nothing is measured \u2014 no\n   session has been recorded. Replace with the CMS export after go-live.\n================================================================== *\/\nconst MODULES=[\n  {id:'dine',    name:'Dining & Nightlife',            tag:'dining',      share:.200, dwell:112, intentRel:1.4, convRel:1.3},\n  {id:'beach',   name:'Beaches & Waterways',           tag:'attractions', share:.165, dwell:94,  intentRel:1.0, convRel:0.9},\n  {id:'attr',    name:'Attractions & Tours',           tag:'attractions', share:.135, dwell:98,  intentRel:1.2, convRel:1.1},\n  {id:'wayfind', name:'OMNI Wayfinding & Water Taxi',  tag:'attractions', share:.110, dwell:70,  intentRel:0.7, convRel:0.6},\n  {id:'events',  name:'Events Calendar',               tag:'events',      share:.100, dwell:88,  intentRel:1.1, convRel:1.0},\n  {id:'itin',    name:'OMNI Itinerary',                tag:'attractions', share:.080, dwell:150, intentRel:1.9, convRel:1.6},\n  {id:'tickets', name:'Tickets, Tours & Charters',     tag:'events',      share:.075, dwell:120, intentRel:2.1, convRel:1.9},\n  {id:'stay',    name:'Lodging & Hotels',              tag:'hotels',      share:.065, dwell:110, intentRel:1.7, convRel:1.6},\n  {id:'shop',    name:'Shopping & Retail',             tag:'dining',      share:.055, dwell:74,  intentRel:1.0, convRel:0.9},\n  {id:'phone',   name:'Mobile Handoff (Send to Phone)',tag:'hotels',      share:.007, dwell:34,  intentRel:2.4, convRel:2.3},\n  {id:'lang',    name:'Multi-language & ADA',          tag:'attractions', share:.008, dwell:40,  intentRel:0.5, convRel:0.4},\n];\nconst CAMP_TYPE={ 'Pass \/ loyalty':'#1C9CD8','Automation':'#1BAF7A','Partner co-op':'#EB6834','Group \/ MICE':'#14508C','Paid media':'#E87BA4','Content':'#008300','Itinerary \/ CRM':'#6B4FB0','Promotions':'#EDA100' };\nconst CAMPAIGNS=[\n  {id:'cruise',    name:'Pre & Post-Cruise Day Plan',         type:'Itinerary \/ CRM',share:.19, intentRel:1.8, convRel:1.7, cpi:1.20},\n  {id:'dining',    name:'Greater Fort Lauderdale Dining Co-op',type:'Partner co-op', share:.17, intentRel:1.4, convRel:1.3, cpi:1.05},\n  {id:'conv',      name:'Convention Delegate Welcome',        type:'Group \/ MICE',   share:.14, intentRel:1.5, convRel:1.5, cpi:0.85},\n  {id:'sponsored', name:'Sponsored Placements (ad partners)', type:'Paid media',     share:.13, intentRel:0.9, convRel:0.8, cpi:0.55},\n  {id:'water',     name:'Waterways & Water Taxi',             type:'Content',        share:.11, intentRel:1.1, convRel:1.0, cpi:0.70},\n  {id:'beaches',   name:'24 Miles of Beaches',                type:'Content',        share:.10, intentRel:1.0, convRel:0.9, cpi:0.60},\n  {id:'passes',    name:'Greater Fort Lauderdale Pass',       type:'Pass \/ loyalty', share:.09, intentRel:1.6, convRel:1.5, cpi:0.90},\n  {id:'shoulder',  name:'Summer Shoulder-Season Getaway',     type:'Automation',     share:.07, intentRel:1.3, convRel:1.4, cpi:0.60},\n];\nconst AB_TESTS=[\n  {id:'t1', camp:'cruise',    name:'Pre-cruise prompt \u2014 terminal wait',  a:'Generic welcome', b:'\u201cSailing at 4pm? Here\u2019s a 3-hour plan\u201d', split:.5, bLift:1.34, conf:0.00, status:'Ready to launch'},\n  {id:'t2', camp:'dining',    name:'Dining co-op \u2014 send-to-phone',       a:'Footer link', b:'Persistent QR card',                     split:.5, bLift:1.30, conf:0.00, status:'Ready to launch'},\n  {id:'t3', camp:'water',     name:'Water taxi \u2014 wayfinding placement',  a:'Under transport', b:'Inline on every listing',            split:.5, bLift:1.22, conf:0.00, status:'Ready to launch'},\n  {id:'t4', camp:'conv',      name:'Delegate Welcome \u2014 arrival prompt',  a:'Generic welcome', b:'\u201cYou have 3 free hours \u2014 here\u2019s what\u2019s walkable\u201d', split:.5, bLift:1.28, conf:0.00, status:'Ready to launch'},\n];\nfunction splitFunnel(m, items, segShares){\n  const sSum=items.reduce((a,b)=>a+b.share,0);\n  const rows=items.map(it=>{\n    let w=it.share\/sSum;\n    if(segShares && it.tag){ w*=Math.max(0.35, 1+0.7*((segShares[it.tag]||.25)-0.25)); }\n    return {it, w};\n  });\n  const wSum=rows.reduce((a,b)=>a+b.w,0);\n  rows.forEach(r=>r.w\/=wSum);\n  const hiRaw=rows.map(r=>r.w*r.it.intentRel), hiS=hiRaw.reduce((a,b)=>a+b,0);\n  const cvRaw=rows.map((r,i)=>hiRaw[i]*r.it.convRel), cvS=cvRaw.reduce((a,b)=>a+b,0);\n  return rows.map((r,i)=>{\n    const interactions=m.interactions*r.w;\n    const highIntent=m.highIntent*hiRaw[i]\/hiS;\n    const conversions=m.conversions*cvRaw[i]\/cvS;\n    const exposures=m.exposures*r.w;\n    const impact=m.total*cvRaw[i]\/cvS;\n    const timeHrs=r.it.dwell? interactions*r.it.dwell\/3600 : null;\n    const spend=r.it.cpi!=null? interactions*r.it.cpi : null;\n    return {...r.it, exposures, interactions, highIntent, conversions, impact, timeHrs, spend,\n      convRate:conversions\/interactions, roas:spend?impact\/spend:null, valPerK:impact\/(interactions\/1000)};\n  });\n}\nfunction segSharesOf(m){const t=Object.values(m.partners).reduce((a,b)=>a+b,0)||1;const o={};for(const s in m.partners)o[s]=m.partners[s]\/t;return o;}\n\n<\/script>\n\n<!-- =================== FORMAT + CHART HELPERS =================== -->\n<script id=\"helpers\">\nconst $=(s,r=document)=>r.querySelector(s);\nconst NS='http:\/\/www.w3.org\/2000\/svg';\nfunction el(t,a={},kids=[]){const e=document.createElementNS(NS,t);for(const k in a)e.setAttribute(k,a[k]);(Array.isArray(kids)?kids:[kids]).forEach(c=>c!=null&&e.appendChild(typeof c==='string'?document.createTextNode(c):c));return e;}\nlet CUR='USD';                 \/\/ USD only\nconst FX={USD:1};    \/\/ US destination \u2014 USD only, no FX conversion.\nfunction curSym(){return '$';}\nfunction money(v){v=v*FX[CUR];const s=v<0?'-':'';v=Math.abs(v);const c=curSym();\n  if(v>=1e9)return s+c+(v\/1e9).toFixed(v>=1e10?0:2).replace(\/\\.0+$\/,'')+'B';\n  if(v>=1e6)return s+c+(v\/1e6).toFixed(v>=1e7?1:2).replace(\/\\.?0+$\/,'')+'M';\n  if(v>=1e3)return s+c+(v\/1e3).toFixed(0)+'K';\n  return s+c+Math.round(v);}\nfunction money2(v){return curSym()+Math.round(v*FX[CUR]).toLocaleString();}\nfunction num(v){if(v>=1e6)return (v\/1e6).toFixed(v>=1e7?1:2).replace(\/\\.?0+$\/,'')+'M';if(v>=1e3)return (v\/1e3).toFixed(v>=1e4?0:1).replace(\/\\.0$\/,'')+'K';return Math.round(v).toLocaleString();}\nfunction pct(v,d=0){return (v*100).toFixed(d)+'%';}\n\nconst TT=$('#tt');\nfunction showTT(html,e){TT.innerHTML=html;TT.style.opacity=1;moveTT(e);}\nfunction moveTT(e){const p=12;let x=e.clientX+p,y=e.clientY+p;const r=TT.getBoundingClientRect();if(x+r.width>innerWidth-8)x=e.clientX-r.width-p;if(y+r.height>innerHeight-8)y=e.clientY-r.height-p;TT.style.left=x+'px';TT.style.top=y+'px';}\nfunction hideTT(){TT.style.opacity=0;}\n\n\/* ---- Area\/line trend chart with crosshair+tooltip ---- *\/\nfunction areaChart(values,labels,opt={}){\n  const w=opt.w||660,h=opt.h||210,pl=42,pr=16,pt=14,pb=26;\n  const iw=w-pl-pr,ih=h-pt-pb;\n  const max=Math.max(...values)*1.12,min=0;\n  const X=i=>pl+iw*(i\/(values.length-1));\n  const Y=v=>pt+ih*(1-(v-min)\/(max-min));\n  const svg=el('svg',{viewBox:`0 0 ${w} ${h}`,width:'100%',role:'img'});\n  \/\/ gridlines + y ticks\n  const ticks=4;\n  for(let t=0;t<=ticks;t++){const val=max*t\/ticks;const y=Y(val);\n    svg.appendChild(el('line',{x1:pl,x2:w-pr,y1:y,y2:y,stroke:'var(--grid)','stroke-width':1}));\n    svg.appendChild(el('text',{x:pl-8,y:y+3.5,'text-anchor':'end','font-size':10,fill:'var(--muted)'},money(val)));\n  }\n  \/\/ area path\n  let d='M'+X(0)+' '+Y(values[0]);values.forEach((v,i)=>{if(i)d+=' L'+X(i)+' '+Y(v);});\n  let area=d+` L${X(values.length-1)} ${Y(0)} L${X(0)} ${Y(0)} Z`;\n  const gid='g'+Math.random().toString(36).slice(2,7);\n  const defs=el('defs');const lg=el('linearGradient',{id:gid,x1:0,y1:0,x2:0,y2:1});\n  lg.appendChild(el('stop',{offset:'0%','stop-color':'var(--azure)','stop-opacity':.28}));\n  lg.appendChild(el('stop',{offset:'100%','stop-color':'var(--azure)','stop-opacity':.02}));\n  defs.appendChild(lg);svg.appendChild(defs);\n  svg.appendChild(el('path',{d:area,fill:`url(#${gid})`}));\n  svg.appendChild(el('path',{d,fill:'none',stroke:'var(--azure)','stroke-width':2.4,'stroke-linejoin':'round','stroke-linecap':'round'}));\n  \/\/ x labels (every other)\n  labels.forEach((lb,i)=>{if(i%2===0||i===labels.length-1)svg.appendChild(el('text',{x:X(i),y:h-8,'text-anchor':'middle','font-size':10,fill:'var(--muted)'},lb));});\n  \/\/ end dot\n  svg.appendChild(el('circle',{cx:X(values.length-1),cy:Y(values.at(-1)),r:4.5,fill:'var(--azure)',stroke:'var(--surface)','stroke-width':2}));\n  \/\/ crosshair layer\n  const cx=el('line',{x1:0,x2:0,y1:pt,y2:pt+ih,stroke:'var(--azure)','stroke-width':1,'stroke-dasharray':'3 3',opacity:0});\n  const cdot=el('circle',{r:5,fill:'var(--azure)',stroke:'var(--surface)','stroke-width':2,opacity:0});\n  svg.appendChild(cx);svg.appendChild(cdot);\n  const hit=el('rect',{x:pl,y:pt,width:iw,height:ih,fill:'transparent'});\n  hit.addEventListener('mousemove',ev=>{\n    const rect=svg.getBoundingClientRect();const rel=(ev.clientX-rect.left)\/rect.width*w;\n    let i=Math.round((rel-pl)\/iw*(values.length-1));i=Math.max(0,Math.min(values.length-1,i));\n    cx.setAttribute('x1',X(i));cx.setAttribute('x2',X(i));cx.setAttribute('opacity',.6);\n    cdot.setAttribute('cx',X(i));cdot.setAttribute('cy',Y(values[i]));cdot.setAttribute('opacity',1);\n    showTT(`<div class=\"tt-t\">${labels[i]} 2026<\/div><div class=\"tt-r\">Economic impact <b>${money(values[i])}<\/b><\/div>`,ev);\n  });\n  hit.addEventListener('mouseleave',()=>{cx.setAttribute('opacity',0);cdot.setAttribute('opacity',0);hideTT();});\n  svg.appendChild(hit);\n  return svg;\n}\n\n\/* ---- horizontal ranked stacked bars (tax local+state, per location) ---- *\/\nfunction rankedStacked(rows,opt={}){\n  \/\/ rows: [{label, parts:[{v,color,name}], total, sub, onClick}]\n  const fmt=opt.fmt||money, tfmt=opt.tfmt||money2;\n  const w=opt.w||620,rowH=34,gap=10,pl=opt.pl||120,pr=opt.pr||64,pt=6;\n  const h=pt+rows.length*(rowH+gap);\n  const max=Math.max(...rows.map(r=>r.total))*1.02;\n  const iw=w-pl-pr;\n  const svg=el('svg',{viewBox:`0 0 ${w} ${h}`,width:'100%'});\n  rows.forEach((r,ri)=>{\n    const y=pt+ri*(rowH+gap);\n    svg.appendChild(el('text',{x:pl-10,y:y+rowH\/2+4,'text-anchor':'end','font-size':12,'font-weight':600,fill:'var(--text)'},r.label));\n    let x=pl;\n    r.parts.forEach((p,pi)=>{\n      const bw=iw*(p.v\/max);\n      const g=el('g',{class:'barseg',style:'cursor:pointer'});\n      const rc=el('rect',{x:x,y:y,width:Math.max(0,bw-2),height:rowH,rx:(pi===r.parts.length-1?6:0),fill:p.color});\n      g.appendChild(rc);\n      g.addEventListener('mousemove',ev=>showTT(`<div class=\"tt-t\">${r.label}<\/div><div class=\"tt-r\">${p.name} <b>${tfmt(p.v)}<\/b><\/div>`,ev));\n      g.addEventListener('mouseleave',hideTT);\n      if(r.onClick)g.addEventListener('click',r.onClick);\n      svg.appendChild(g);x+=bw;\n    });\n    svg.appendChild(el('text',{x:x+8,y:y+rowH\/2+4,'font-size':12.5,'font-weight':700,fill:'var(--text)'},fmt(r.total)));\n  });\n  return svg;\n}\n\n\/* ---- donut for partner mix ---- *\/\nfunction donut(parts,opt={}){ \/\/ parts:[{v,color,name}]\n  const size=opt.size||190,r=size\/2,rin=r*0.62,cx=r,cy=r;\n  const tot=parts.reduce((a,p)=>a+p.v,0);let a0=-Math.PI\/2;\n  const svg=el('svg',{viewBox:`0 0 ${size} ${size}`,width:size,height:size});\n  parts.forEach(p=>{\n    const a1=a0+ (p.v\/tot)*Math.PI*2;\n    const x0=cx+r*Math.cos(a0),y0=cy+r*Math.sin(a0),x1=cx+r*Math.cos(a1),y1=cy+r*Math.sin(a1);\n    const xi0=cx+rin*Math.cos(a0),yi0=cy+rin*Math.sin(a0),xi1=cx+rin*Math.cos(a1),yi1=cy+rin*Math.sin(a1);\n    const large=(a1-a0)>Math.PI?1:0;\n    const d=`M${x0} ${y0} A${r} ${r} 0 ${large} 1 ${x1} ${y1} L${xi1} ${yi1} A${rin} ${rin} 0 ${large} 0 ${xi0} ${yi0} Z`;\n    const path=el('path',{d,fill:p.color,stroke:'var(--surface)','stroke-width':2,style:'cursor:pointer;transition:.15s'});\n    path.addEventListener('mousemove',ev=>showTT(`<div class=\"tt-t\">${p.name}<\/div><div class=\"tt-r\">Impact <b>${money(p.v)}<\/b><\/div><div class=\"tt-r\">Share <b>${pct(p.v\/tot,0)}<\/b><\/div>`,ev));\n    path.addEventListener('mouseleave',hideTT);\n    if(p.onClick)path.addEventListener('click',p.onClick);\n    svg.appendChild(path);a0=a1;\n  });\n  svg.appendChild(el('text',{x:cx,y:cy-3,'text-anchor':'middle','font-size':20,'font-weight':700,fill:'var(--text)'},money(tot)));\n  svg.appendChild(el('text',{x:cx,y:cy+14,'text-anchor':'middle','font-size':10.5,fill:'var(--muted)'},'partner impact'));\n  return svg;\n}\n\n\/* ---- stacked column: direct vs influenced by location ---- *\/\nfunction stackedCols(rows,opt={}){ \/\/ rows:[{label,direct,influenced,onClick}]\n  const w=opt.w||620,h=opt.h||230,pl=42,pr=10,pt=12,pb=34;\n  const iw=w-pl-pr,ih=h-pt-pb,n=rows.length;\n  const max=Math.max(...rows.map(r=>r.direct+r.influenced))*1.14;\n  const bw=Math.min(30,iw\/n*0.52),slot=iw\/n;\n  const Y=v=>pt+ih*(1-v\/max);\n  const svg=el('svg',{viewBox:`0 0 ${w} ${h}`,width:'100%'});\n  for(let t=0;t<=4;t++){const val=max*t\/4,y=Y(val);svg.appendChild(el('line',{x1:pl,x2:w-pr,y1:y,y2:y,stroke:'var(--grid)','stroke-width':1}));svg.appendChild(el('text',{x:pl-8,y:y+3.5,'text-anchor':'end','font-size':10,fill:'var(--muted)'},money(val)));}\n  rows.forEach((r,i)=>{\n    const cx=pl+slot*i+slot\/2, x=cx-bw\/2;\n    const hD=ih*r.direct\/max, hI=ih*r.influenced\/max;\n    const yD=Y(r.direct), yI=Y(r.direct+r.influenced);\n    const gI=el('rect',{x,y:yI,width:bw,height:Math.max(0,hI-1),rx:5,fill:'var(--s-influenced)',style:'cursor:pointer'});\n    const gD=el('rect',{x,y:yD,width:bw,height:hD,rx:0,fill:'var(--s-direct)',style:'cursor:pointer'});\n    [ ['Direct',r.direct,gD],['Influenced',r.influenced,gI] ].forEach(([nm,v,g])=>{\n      g.addEventListener('mousemove',ev=>showTT(`<div class=\"tt-t\">${r.label}<\/div><div class=\"tt-r\">${nm} <b>${money(v)}<\/b><\/div><div class=\"tt-r\">Total <b>${money(r.direct+r.influenced)}<\/b><\/div>`,ev));\n      g.addEventListener('mouseleave',hideTT);if(r.onClick)g.addEventListener('click',r.onClick);\n    });\n    svg.appendChild(gI);svg.appendChild(gD);\n    svg.appendChild(el('text',{x:cx,y:h-19,'text-anchor':'middle','font-size':10,'font-weight':600,fill:'var(--text-2)'},r.label));\n    svg.appendChild(el('text',{x:cx,y:h-8,'text-anchor':'middle','font-size':9.5,fill:'var(--muted)'},money(r.direct+r.influenced)));\n  });\n  return svg;\n}\n<\/script>\n\n<!-- =================== AI NARRATIVE ENGINE =================== -->\n<script id=\"ai\">\n\/* Lightweight on-device narrative generator. Reads the derived model and writes\n   the dollars-first, council-ready sentence a director takes into a budget meeting.\n   (In production this is the TrueImpact AI summarizer over the live attribution feed.) *\/\nfunction aiOverview(k){\n  const m=rollup(k), locs=LOCS();\n  const best=[...locs].sort((a,b)=>deriveMetrics(b,k).total-deriveMetrics(a,k).total)[0];\n  const rate=[...locs].sort((a,b)=>b.interactionRate-a.interactionRate)[0];\n  return `Phase 1 places <b>${m.kiosks} interactive kiosks<\/b> and <b>${m.screens} signage screens<\/b> across <b>${locs.length} districts<\/b>. The network is projected to reach <b>${num(m.exposures)}<\/b> visitor encounters, convert <b>${num(m.interactions)}<\/b> of them into kiosk sessions, and attribute <span class=\"em\">${money(m.total)}<\/span> of <b>direct visitor spend<\/b> and <span class=\"em\">${money(m.tax)}<\/span> of estimated public tax. <b>${best.name}<\/b> leads on attributed spend; <b>${rate.name}<\/b> carries the highest modeled interaction rate at <b>${pct(rate.interactionRate,1)}<\/b>. <b>No ripple multiplier is applied anywhere in this model<\/b> \u2014 most destination impact studies add an indirect and induced tail, and this one deliberately does not, so every figure is first-round spend and smaller than a conventional model would produce. There is no second-round effect anywhere in this model \u2014 no indirect tail on spending and no re-circulation multiplier on tax. <b>Every network figure is a pre-deployment projection.<\/b>`;\n}\nfunction aiTax(k){\n  const m=rollup(k),b=m.taxBreak,locs=LOCS();\n  const top=Object.entries(b).sort((x,y)=>y[1]-x[1])[0];\n  const nm={hotelTOT:'the Tourist Development Tax',salesTax:'sales &amp; use tax',rentalTax:'the vehicle-rental tax',stateTax:'Florida corporate capture capture'}[top[0]];\n  const hi=[...locs].sort((a,b)=>b.tax.tot-a.tax.tot)[0], lo=[...locs].sort((a,b)=>a.tax.tot-b.tax.tot)[0];\n  const cmp = hi.tax.tot!==lo.tax.tot ? ` Because rates are set locally, the same visitor is worth more in <b>${hi.name} (${hi.tax.tot}% TOT)<\/b> than in <b>${lo.name} (${lo.tax.tot}%)<\/b>.` : '';\n  return `Attributed direct visitor spending is projected to generate an estimated <span class=\"em\">${money(m.tax)}<\/span> in public tax revenue this period \u2014 <b>${money(m.taxLocal)}<\/b> staying local and <b>${money(m.taxState)}<\/b> to the state. The single largest line is <b>${nm}<\/b> at <span class=\"em\">${money(top[1])}<\/span>. The Tourist Development Tax is the line this organisation is measured on: it is <b>100% local<\/b>, it funded the $124M the destination collected in 2025, and it pays for Visit Lauderdale itself, the Convention Center and beach renourishment. An estimated <span class=\"em\">${money(m.dmoFund)}<\/span> of the figure above is TDT. No induced or re-circulation multiplier is applied to that figure: this build counts first-round attributed spend and the tax levied directly on it, and nothing beyond.`;\n}\nfunction aiLocation(L,k){\n  const m=deriveMetrics(L,k),r=L.tax;\n  return `<b>${L.name}<\/b> is projected to turn <b>${num(m.interactions)}<\/b> kiosk sessions into <span class=\"em\">${money(m.total)}<\/span> of attributed direct spend and <span class=\"em\">${money(m.tax)}<\/span> of public tax (lodging ${r.tot}%, sales ${r.sales}%, rental ${r.rental}%). ${L.note} Partners are projected to capture <b>${money(Object.values(m.partners).reduce((a,b)=>a+b,0))}<\/b>, led by ${SEG[Object.entries(m.partners).sort((a,b)=>b[1]-a[1])[0][0]].name.toLowerCase()}.`;\n}\nfunction aiNetwork(k){\n  const {net}=networkAgg(k);\n  return `Across the full TrueOmni network \u2014 <b>${net.destinations} destinations<\/b> and <b>${num(net.kiosks)} kiosks<\/b> \u2014 the platform attributed <span class=\"em\">${money(net.total)}<\/span> of visitor-driven economic impact and <span class=\"em\">${money(net.tax)}<\/span> in estimated public tax revenue this period. Roll-up view \u2014 unused in this single-tenant deployment. Every destination below drills down to its own kiosks.`;\n}\nfunction aiEngagement(rows, mode, scopeName){\n  const attn=[...rows].sort((a,b)=>b.interactions-a.interactions)[0];\n  const val=[...rows].sort((a,b)=>b.impact-a.impact)[0];\n  const invest= mode==='campaigns'\n      ? [...rows].filter(r=>r.roas).sort((a,b)=>b.roas-a.roas)[0]\n      : [...rows].sort((a,b)=>b.valPerK-a.valPerK)[0];\n  const gap = attn.id!==val.id\n      ? ` The gap is the story: <b>${attn.name}<\/b> draws the most attention, but <b>${val.name}<\/b> converts it into the most attributed dollars \u2014 so raw attention isn't the same as value.`\n      : ` <b>${attn.name}<\/b> both draws the most attention and drives the most dollars.`;\n  const investLine = mode==='campaigns'\n      ? ` For spend, <b>${invest.name}<\/b> returns the best ROI at <span class=\"em\">${invest.roas.toFixed(1)}\u00d7<\/span> \u2014 invest here first.`\n      : ` <b>${invest.name}<\/b> punches above its attention (${money(invest.valPerK)} attributed per 1K interactions) \u2014 the efficient place to feature and cross-promote.`;\n  return `In <b>${scopeName}<\/b>, the ${mode==='campaigns'?'campaigns':'kiosk modules'} split the funnel from exposure to attributed conversion.${gap}${investLine} Rank by any funnel stage using the metric toggle; filter by market to see how the mix shifts location to location.`;\n}\nfunction aiPartner(k){\n  const m=rollup(k),tot=Object.values(m.partners).reduce((a,b)=>a+b,0);\n  const top=Object.entries(m.partners).sort((a,b)=>b[1]-a[1])[0];\n  return `TrueImpact projects <span class=\"em\">${money(tot)}<\/span> of direct visitor spending attributable to partner businesses \u2014 hotels, restaurants, attractions and event venues \u2014 every dollar tied to a kiosk referral, QR scan, pass pull or partner-POS event. <b>${SEG[top[0]].name}<\/b> is modeled to capture the most at <span class=\"em\">${money(top[1])}<\/span>. Once live, this is the number a partner will see in their own portal \u2014 the figure that turns flat-dues membership into performance-aligned membership.`;\n}\nfunction aiBanner(tag,html){\n  return `<div class=\"ai-banner fade\"><div class=\"ai-ic\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M12 3l1.9 4.6L18 9l-4.1 1.4L12 15l-1.9-4.6L6 9l4.1-1.4zM19 14l.9 2.2L22 17l-2.1.8L19 20l-.9-2.2L16 17l2.1-.8z\" fill=\"#fff\"\/><\/svg><\/div><div><span class=\"ai-tag\">${tag}<\/span><div class=\"ai-txt\">${html}<\/div><\/div><\/div>`;\n}\n<\/script>\n\n<!-- =================== APP \/ ROUTER \/ PAGES =================== -->\n<script id=\"app-logic\">\nlet PERIOD='t12';\nlet engMode='modules', engLoc='all', engMetric='interactions';   \/\/ engagement drill-through filters\nconst app=$('#app');\nfunction K(){return PERIODS[PERIOD].k;}\nfunction periodLabel(){return PERIODS[PERIOD].label;}\n\n\/* ---------- helpers to build DOM cards ---------- *\/\nfunction h(html){const t=document.createElement('template');t.innerHTML=html.trim();return t.content.firstChild;}\nfunction card(title,hint,bodyNodes,cls=''){\n  const c=h(`<div class=\"card ${cls}\"><div class=\"card-h\"><div><h3>${title}${hint?` <span class=\"hint\">${hint}<\/span>`:''}<\/h3><\/div><\/div><\/div>`);\n  (Array.isArray(bodyNodes)?bodyNodes:[bodyNodes]).forEach(n=>n&&c.appendChild(typeof n==='string'?h(n):n));\n  return c;\n}\nfunction legend(items){const l=h('<div class=\"legend\"><\/div>');items.forEach(it=>l.appendChild(h(`<span class=\"li\"><span class=\"sw ${it.line?'line':''}\" style=\"background:${it.color}\"><\/span>${it.name}<\/span>`)));return l;}\nfunction shortName(n){return n.replace(' Lake','').replace(' Valley','').replace(' Bay','').replace('Fort ','Ft ').replace(' Springs','').replace('San Francisco','SF').replace('Los Angeles','LA').replace('Magnificent Mile','Mag Mile').replace('McCormick Place','McCormick').replace('Museum Campus','Museum').split(' (')[0];}\nfunction engShort(n){return n.replace(\/\\s*\\(.*\\)\/,'').replace('Interactive Map & Wayfinding','Map & Wayfinding').replace(' Placements','').replace('Local ','').replace('-Season Getaway','-Season').replace(' & Coupons','').replace(' Push','').replace(' Welcome','').replace(' & Restaurants','').replace(' to Do & Attractions',' to Do').replace(' Builder','').replace(' Calendar','').replace(' & Parking','');}\nfunction setDest(id){activeDest=id;engLoc='all';location.hash='#\/overview';render();}\nfunction segMiniCard(k){\n  const cm=rollup(k,segLocs()), all=rollup(k);\n  const c=card('Cruise &amp; Port','the segment this destination is measured on \u00b7 '+periodLabel(),'',' ');\n  c.appendChild(h(`<div class=\"stat-inline\" style=\"margin-top:2px\"><div class=\"si\"><b>${money(cm.total)}<\/b><span>attributed direct spend (modeled)<\/span><\/div><div class=\"si\"><b>${pct(cm.total\/all.total)}<\/b><span>of network impact<\/span><\/div><div class=\"si\"><b>${pct(cm.exposures\/all.exposures)}<\/b><span>of network traffic<\/span><\/div><div class=\"si\"><b>${cm.kiosks} + ${cm.screens}<\/b><span>kiosks + screens<\/span><\/div><\/div>`));\n  c.appendChild(h(`<div style=\"display:flex;gap:8px;margin-top:12px\"><button class=\"chip\" style=\"cursor:pointer\" onclick=\"go('#\/cruise')\">Open Cruise &amp; Port \u203a<\/button><\/div>`));\n  return c;\n}\nfunction engMiniCard(k){\n  const sc=engScope(k); const rows=splitFunnel(sc.m, MODULES, sc.seg).sort((a,b)=>b.interactions-a.interactions).slice(0,5);\n  const max=rows[0].interactions;\n  const c=card('Module &amp; campaign engagement','what draws attention \u2014 drill the funnel','',' ');\n  const list=h('<div class=\"loclist\"><\/div>');\n  rows.forEach(r=>{const row=h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div><div class=\"lr-name\" style=\"display:flex;align-items:center;gap:7px\"><span class=\"sw\" style=\"width:11px;height:11px;border-radius:3px;background:${SEG[r.tag].color}\"><\/span>${r.name}<\/div><\/div><div class=\"lr-val\">${num(r.interactions)}<div class=\"lr-sub\" style=\"font-weight:600\">${money(r.impact)}<\/div><\/div><div class=\"lr-bar\"><i style=\"width:${(r.interactions\/max*100).toFixed(0)}%\"><\/i><\/div><\/div>`);\n    row.addEventListener('click',()=>go('#\/modules'));list.appendChild(row);});\n  c.appendChild(list);\n  const btns=h('<div style=\"display:flex;gap:8px;margin-top:12px\"><\/div>');\n  btns.appendChild(h(`<button class=\"chip\" style=\"cursor:pointer\" onclick=\"go('#\/modules')\">Modules \u203a<\/button>`));\n  btns.appendChild(h(`<button class=\"chip\" style=\"cursor:pointer\" onclick=\"go('#\/campaigns')\">Campaigns &amp; A\/B tests \u203a<\/button>`));\n  c.appendChild(btns);\n  return c;\n}\n\n\/* ================= OVERVIEW ================= *\/\nfunction contextStrip(){\n  const c=card('Visit Lauderdale \u00b7 the destination\\'s own published figures','cited, not modeled','',' ');\n  const s=h('<div class=\"stat-inline\" style=\"margin-top:2px\"><\/div>');\n  CONTEXT.forEach(x=>s.appendChild(h(`<div class=\"si\"><b>${x.v}<\/b><span>${x.l}<\/span><\/div>`)));\n  c.appendChild(s);\n  c.appendChild(h(`<div class=\"note\">These are the only <b style=\"color:var(--text)\">cited<\/b> figures on this dashboard. Everything below is a projected <b style=\"color:var(--text)\">attributed slice<\/b> of the economy above \u2014 the portion a kiosk session will be able to claim once the network is live \u2014 and it is deliberately a very small fraction of it. This build also applies <b style=\"color:var(--text)\">no ripple multiplier<\/b>, so the figures are smaller again than a conventional impact model would produce.<\/div>`));\n  c.style.marginBottom='16px';\n  return c;\n}\nfunction buildStrip(k){\n  const m=rollup(k), sg=signageMetrics(1), bc=buildCost(LOCS()), p1=phase1Payback();\n  const c=card('The Phase-1 build','what would be installed, and what it costs','',' ');\n  const s=h('<div class=\"stat-inline\" style=\"margin-top:2px\"><\/div>');\n  [[String(bc.kiosks),'Interactive kiosks'],[String(bc.screens),'Signage screens'],[String(LOCS().length),'Districts'],\n   [money2(bc.capital),'One-time capital'],[money2(bc.softwareYr),'Software \/ yr'],[p1.years.toFixed(1)+' yr','Payback from advertising \u00b7 mature fill']]\n   .forEach(x=>s.appendChild(h(`<div class=\"si\"><b>${x[0]}<\/b><span>${x[1]}<\/span><\/div>`)));\n  c.appendChild(s);\n  c.appendChild(h(`<div class=\"note\"><b style=\"color:var(--text)\">Two surfaces, two jobs.<\/b> The ${bc.kiosks} kiosks transact \u2014 they take sessions, produce high-intent actions and generate every dollar of attributed spend and tax here. The ${bc.screens} signage screens broadcast: they add ${num(sg.impressions)} Screen Impressions and ${money(sg.rev)} of advertising revenue a year at mature fill (${money(sg.rev*SIGNAGE.fillByPhase[1]\/SIGNAGE.fillMature)} at Phase-1 sell-through), and are deliberately kept out of every impact and tax figure.<\/div>`));\n  return c;\n}\nfunction pageOverview(){\n  const k=K(),m=rollup(k);\n  const wrap=h('<div class=\"fade\"><\/div>');\n  wrap.appendChild(h(`<div class=\"pagehead\"><div><div class=\"eyebrow\">${ACT().kind} \u00b7 Executive Dashboard<\/div><h1>${ACT().name} \u2014 ${ACT().scope} Impact<\/h1><\/div><div class=\"meta\">Reporting period<br><b style=\"color:var(--text);font-size:14px\">${periodLabel()}<\/b><br>${m.kiosks} kiosks + ${m.screens} screens \u00b7 ${LOCS().length} districts<\/div><\/div>`));\n  wrap.appendChild(h(`<div class=\"crumbs\"><span>${ACT().name}<\/span><span class=\"sep\">\u203a<\/span><a onclick=\"go('#\/overview')\">${ACT().region}<\/a><span class=\"sep\">\u203a<\/span><span>All zones<\/span><\/div>`));\n  wrap.appendChild(contextStrip());\n  wrap.appendChild(h(aiBanner('TrueImpact AI \u00b7 Board-ready summary',aiOverview(k))));\n  wrap.appendChild(buildStrip(k)); wrap.lastChild.style.marginBottom='16px';\n  wrap.appendChild(rampCard(k)); wrap.lastChild.style.marginBottom='16px';\n\n  \/\/ KPI band\n  const kpis=h('<div class=\"grid kpis\" style=\"margin-bottom:16px\"><\/div>');\n  kpis.appendChild(h(`<div class=\"card kpi hero\"><div class=\"glow\"><\/div><div class=\"k-label\">Attributed direct visitor spend<\/div><div class=\"k-val\"><span class=\"cur\">${curSym()}<\/span>${money(m.total).replace(curSym(),'')}<\/div><div class=\"k-delta\" style=\"color:var(--muted)\">pre-deployment projection \u2014 no prior period<\/div><div class=\"k-foot\">Attributed <b>direct visitor spend only<\/b> \u2014 no ripple multiplier applied<\/div><\/div>`));\n  kpis.appendChild(kpiTile('Estimated tax revenue',money(m.tax),'',`Broward County ${money(m.taxLocal)} \u00b7 State of Florida ${money(m.taxState)}`,()=>go('#\/tax')));\n  kpis.appendChild(kpiTile('Partner-captured spend',money(Object.values(m.partners).reduce((a,b)=>a+b,0)),'','Hotels \u00b7 dining \u00b7 attractions \u00b7 events',()=>go('#\/partners')));\n  kpis.appendChild(kpiTile('Projected kiosk sessions',num(m.interactions),'',`${num(m.conversions)} attributed conversions`));\n  wrap.appendChild(kpis);\n\n  \/\/ Map + location list\n  const mapRow=h('<div class=\"grid row-map\" style=\"margin-bottom:16px\"><\/div>');\n  mapRow.appendChild(card('Impact by kiosk location','click a market to drill in',[buildMap(k),legend([{name:'Bubble size = economic impact',color:'var(--azure)'}])],'pad0'+' '));\n  const mapCard=mapRow.lastChild;mapCard.style.padding='18px';\n  mapRow.appendChild(card('Zones ranked','by economic impact',[buildLocList(k)]));\n  wrap.appendChild(mapRow);\n\n  \/\/ Funnel + Direct\/influenced by location\n  const r2=h('<div class=\"grid row-2\" style=\"margin-bottom:16px\"><\/div>');\n  r2.appendChild(card('The visitor funnel',ACT().scope.toLowerCase(),[buildFunnel(m),h('<div class=\"note\">Encounter \u2192 kiosk session \u2192 high-intent action \u2192 attributed conversion. <b style=\"color:var(--text)\">All four stages are projected.<\/b> The 17% high-intent and 14% conversion rates are TrueOmni platform benchmarks, not measurements from this destination \u2014 nothing is installed yet. At go-live every stage becomes a ledgered kiosk event.<\/div>')]));\n  const dcols=stackedCols(LOCS().map(L=>{const mm=deriveMetrics(L,k);return{label:shortName(L.name),direct:mm.direct,influenced:mm.influenced,onClick:()=>go('#\/location\/'+L.id)};}));\n  r2.appendChild(card('Attributed direct spend by district','click to drill',[dcols,legend([{name:'Direct attributed visitor spend',color:'var(--s-direct)'}]),h('<div class=\"note\">This build applies <b style=\"color:var(--text)\">no ripple multiplier<\/b>. Most destination impact models add an indirect and induced tail; this one does not, because neither a published total-economic-impact figure nor a defensible local multiplier was available. Every dollar shown is first-round visitor spend.<\/div>')]));\n  wrap.appendChild(r2);\n\n  \/\/ Partner mix + tax preview\n  const r3=h('<div class=\"grid row-2\" style=\"margin-bottom:16px\"><\/div>');\n  r3.appendChild(buildPartnerCard(m,k));\n  r3.appendChild(buildTaxPreview(m,k));\n  wrap.appendChild(r3);\n\n  \/\/ Engagement entry\n  wrap.appendChild(engMiniCard(k));\n  wrap.lastChild.style.marginBottom='16px';\n\n  \/\/ Second-segment entry\n  wrap.appendChild(segMiniCard(k));\n  wrap.lastChild.style.marginBottom='16px';\n\n  \/\/ Media Network entry\n  wrap.appendChild(mediaMiniCard(k));\n  wrap.lastChild.style.marginBottom='16px';\n\n  \/\/ Trend\n  const trend=card('Attributed direct spend trend','monthly \u00b7 '+periodLabel(),[areaChart(rollupSeries(k),MONTHS,{h:220}),h(`<div class=\"note\">The Phase-1 network is modeled to reach about ${num(rollup(1).exposures)} visitor encounters a year. Seasonality follows the destination's own shape; the shoulder months are where a kiosk network earns most, because that is when the destination has to work hardest for each visitor. Direct attributed spend only \u2014 no ripple.<\/div>`)]);\n  wrap.appendChild(trend);\n  return wrap;\n}\nfunction rampCard(k){\n  const c=card('Growth ladder \u00b7 10 \u2192 20 \u2192 30 kiosks','Phase 1 is the proposed build; Phases 2 and 3 are modeled scenarios \u00b7 '+periodLabel(),'',' ');\n  const list=h('<div class=\"loclist\"><\/div>');\n  let prevK=0;\n  PHASES.forEach(ph=>{\n    const locs=phaseLocs(ph);\n    const m=rollup(k,locs); const add=m.kiosks-prevK; prevK=m.kiosks;\n    const row=h(`<div class=\"locrow\"><div><div class=\"lr-name\">${ph.name}<\/div><div class=\"lr-sub\">${ph.window} \u00b7 ${m.kiosks} kiosks + ${m.screens} screens${add?` (+${add} kiosks)`:''}<\/div><\/div><div class=\"lr-val\">${money(m.total)}<div class=\"lr-sub\" style=\"font-weight:600\">${money(m.tax)} tax \u00b7 ${money(m.taxLocal)} stays local<\/div><\/div><div class=\"lr-bar\"><i style=\"width:${(m.kiosks\/30*100).toFixed(0)}%\"><\/i><\/div><\/div>`);\n    list.appendChild(row);\n  });\n  c.appendChild(list);\n  c.appendChild(h(`<div class=\"note\"><b style=\"color:var(--text)\">Phase 1 is the proposed build<\/b> \u2014 10 interactive kiosks and 5 signage screens across six districts. Phases 2 and 3 are <b>modeled scenarios<\/b>, not commitments, and no hotel property is named in either. Every figure here is a projection against the destination's own cited visitor base; nothing has been installed and nothing has been measured. Note that impact scales with <i>encounters<\/i>, not with kiosk count, which is why the steps are not proportional.<\/div>`));\n  return c;\n}\nfunction kpiTile(label,val,delta,foot,onClick){\n  const d=(delta||'').replace('\u25b2','').trim();\n  const c=h(`<div class=\"card kpi\"${onClick?' style=\"cursor:pointer\"':''}><div class=\"k-label\">${label}<\/div><div class=\"k-val\">${val}<\/div>${d?`<div class=\"k-delta up\">\u25b2 ${d}<\/div>`:'<div class=\"k-delta\" style=\"visibility:hidden\">\u2013<\/div>'}<div class=\"k-foot\">${foot}<\/div><\/div>`);\n  if(onClick)c.addEventListener('click',onClick);\n  return c;\n}\n\n\/* ---- Destination map (pixel-scale viewBox so marks\/labels stay true size).\n   Reads the active destination's outline + each location's x,y. Works for any tenant. ---- *\/\nfunction buildMap(k){\n  const D=ACT(), locs=LOCS();\n  const svg=el('svg',{viewBox:'0 0 480 620',role:'img','aria-label':D.region+' kiosk map'});\n  if(D.lake) svg.appendChild(el('path',{d:D.lake,fill:'var(--s-hotels)','fill-opacity':.12,stroke:'none'}));\n  svg.appendChild(el('path',{d:D.outline,class:'ca-outline'}));\n  if(D.lakeLabel) svg.appendChild(el('text',{x:D.lakeLabel.x,y:D.lakeLabel.y,'text-anchor':'middle','font-size':12,'font-style':'italic',fill:'var(--muted)',stroke:'none'},D.lakeLabel.t));\n  const max=Math.max(...locs.map(L=>deriveMetrics(L,k).total));\n  locs.forEach(L=>{\n    const mm=deriveMetrics(L,k);const rr=9+18*Math.sqrt(mm.total\/max);\n    const px=L.x,py=L.y;\n    const g=el('g',{class:'mk'+(state.loc===L.id?' active':''),transform:`translate(${px},${py})`});\n    g.appendChild(el('circle',{class:'ring',r:rr+5}));\n    g.appendChild(el('circle',{class:'pin',r:rr,fill:'var(--azure)','fill-opacity':.85}));\n    g.appendChild(el('circle',{class:'hit',r:Math.max(rr+7,16)}));\n    if(rr>16)g.appendChild(el('text',{y:4,'text-anchor':'middle','font-size':11,fill:'#fff',stroke:'none'},money(mm.total)));\n    g.appendChild(el('text',{y:rr+15,'text-anchor':'middle','font-size':11.5,'font-weight':700},L.name));\n    g.addEventListener('mousemove',ev=>showTT(`<div class=\"tt-t\">${L.name}<\/div><div class=\"tt-r\">Impact <b>${money(mm.total)}<\/b><\/div><div class=\"tt-r\">Tax <b>${money(mm.tax)}<\/b><\/div><div class=\"tt-r\">Interaction <b>${pct(L.interactionRate,1)}<\/b><\/div>`,ev));\n    g.addEventListener('mouseleave',hideTT);\n    g.addEventListener('click',()=>go('#\/location\/'+L.id));\n    svg.appendChild(g);\n  });\n  const box=h('<div class=\"mapwrap\"><\/div>');box.appendChild(svg);return box;\n}\nfunction buildLocList(k){\n  const rows=[...LOCS()].map(L=>({L,m:deriveMetrics(L,k)})).sort((a,b)=>b.m.total-a.m.total);\n  const max=rows[0].m.total;\n  const box=h('<div class=\"loclist\"><\/div>');\n  rows.forEach(({L,m})=>{\n    const row=h(`<div class=\"locrow\"><div><div class=\"lr-name\">${L.name} ${L.badge?`<span class=\"pill tag-hi\">${L.badge}<\/span>`:''}<\/div><div class=\"lr-sub\">${L.sites} \u00b7 ${L.kiosks} kiosks<\/div><\/div><div class=\"lr-val\">${money(m.total)}<div class=\"lr-sub\" style=\"font-weight:600\">${money(m.tax)} tax<\/div><\/div><span class=\"chev\">\u203a<\/span><div class=\"lr-bar\"><i style=\"width:${(m.total\/max*100).toFixed(0)}%\"><\/i><\/div><\/div>`);\n    row.addEventListener('click',()=>go('#\/location\/'+L.id));\n    box.appendChild(row);\n  });\n  return box;\n}\nfunction buildFunnel(m){\n  const stages=[\n    {n:'Kiosk exposures',v:m.exposures,rate:''},\n    {n:'Interactions',v:m.interactions,rate:'of exposed'},\n    {n:'High-intent actions',v:m.highIntent,rate:pct(FUNNEL.highIntent)+' of interactions'},\n    {n:'Attributed conversions',v:m.conversions,rate:pct(FUNNEL.conversion)+' of high-intent'},\n  ];\n  const max=stages[0].v;const cols=['#0E2240','#14508C','#1C9CD8','#33abe0'];\n  const box=h('<div class=\"funnel\"><\/div>');\n  stages.forEach((s,i)=>{\n    const w=26+40*(s.v\/max);\n    const st=h(`<div class=\"fstage\"><div class=\"fbar\" style=\"width:${w}%;background:${cols[i]}\">${s.n}<\/div><div class=\"fmeta\"><b>${num(s.v)}<\/b> <span class=\"frate\">${s.rate}<\/span><\/div><\/div>`);\n    box.appendChild(st);\n  });\n  return box;\n}\nfunction buildPartnerCard(m,k){\n  const parts=Object.entries(m.partners).map(([id,v])=>({v,name:SEG[id].name,color:SEG[id].color,onClick:()=>go('#\/partners')}));\n  const c=card('Partner economic impact','hotels \u00b7 dining \u00b7 attractions \u00b7 events','',' ');\n  const inner=h('<div style=\"display:flex;gap:20px;align-items:center;flex-wrap:wrap\"><\/div>');\n  inner.appendChild(donut(parts));\n  const list=h('<div style=\"flex:1;min-width:200px\"><\/div>');\n  parts.sort((a,b)=>b.v-a.v).forEach(p=>{\n    const row=h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div><div class=\"lr-name\" style=\"display:flex;align-items:center;gap:7px\"><span class=\"sw\" style=\"width:11px;height:11px;border-radius:3px;background:${p.color}\"><\/span>${p.name}<\/div><\/div><div class=\"lr-val\">${money(p.v)}<\/div><\/div>`);\n    row.addEventListener('click',()=>go('#\/partners'));list.appendChild(row);\n  });\n  inner.appendChild(list);\n  c.appendChild(inner);\n  c.appendChild(h('<div class=\"note\">This is the figure a partner sees in their own portal \u2014 every dollar tied to a kiosk referral, QR scan, pass pull, or partner-POS event. <span class=\"toggle-tbl\" onclick=\"go(\\'#\/partners\\')\">Open partner detail \u203a<\/span><\/div>'));\n  return c;\n}\nfunction buildTaxPreview(m,k){\n  const b=m.taxBreak;\n  const parts=[\n    {v:b.hotelTOT,name:'Tourist Development Tax',color:'var(--s-local)'},\n    {v:b.salesTax,name:'Sales & use',color:'#33abe0'},\n    {v:b.rentalTax,name:'Vehicle rental',color:'#7cc9e8'},\n    {v:b.stateTax,name:'Florida corporate capture',color:'var(--s-state)'},\n  ];\n  const c=card('Estimated tax revenue','locality-based \u00b7 click to drill','',' ');\n  const rows=parts.map(p=>({label:p.name,parts:[{v:p.v,color:p.color,name:p.name}],total:p.v,onClick:()=>go('#\/tax')}));\n  c.appendChild(rankedStacked(rows,{pl:130,w:560}));\n  c.appendChild(h(`<div class=\"gauge-track\"><i style=\"width:100%\"><\/i><\/div>`));\n  c.appendChild(h(`<div class=\"note\"><b style=\"color:var(--text)\">No re-circulation multiplier is applied<\/b> \u2014 this is a direct-spend-only build. <span class=\"toggle-tbl\" onclick=\"go('#\/tax')\">Full tax breakdown \u203a<\/span><\/div>`));\n  return c;\n}\n\n\/* ================= LOCATION DETAIL ================= *\/\nfunction pageLocation(id){\n  const L=findLoc(id);if(!L)return pageOverview();\n  const k=K(),m=deriveMetrics(L,k),r=L.tax;\n  const wrap=h('<div class=\"fade\"><\/div>');\n  wrap.appendChild(h(`<a class=\"backbtn\" onclick=\"go('#\/overview')\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M15 18l-6-6 6-6\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg> All ${ACT().region} markets<\/a>`));\n  wrap.appendChild(h(`<div class=\"crumbs\"><span>${ACT().name}<\/span><span class=\"sep\">\u203a<\/span><a onclick=\"go('#\/overview')\">${ACT().region}<\/a><span class=\"sep\">\u203a<\/span><span>${L.name}<\/span><\/div>`));\n  wrap.appendChild(h(`<div class=\"pagehead\"><div><div class=\"eyebrow\">Kiosk market${L.badge?` \u00b7 <span style=\"color:var(--good)\">${L.badge}<\/span>`:''}<\/div><h1>${L.name}<\/h1><div class=\"ch-sub\" style=\"margin-top:4px;color:var(--text-2)\">${L.sites} \u00b7 ${L.kiosks} kiosks \u00b7 ${periodLabel()}<\/div><\/div><div class=\"meta\">Tax rates<br><b style=\"color:var(--text);font-size:13px\">Lodging ${r.tot}% \u00b7 Sales ${r.sales}% \u00b7 Rental ${r.rental}%<\/b><\/div><\/div>`));\n  wrap.appendChild(h(aiBanner('TrueImpact AI \u00b7 Market read',aiLocation(L,k))));\n\n  const kpis=h('<div class=\"grid kpis\" style=\"margin-bottom:16px\"><\/div>');\n  kpis.appendChild(h(`<div class=\"card kpi hero\"><div class=\"glow\"><\/div><div class=\"k-label\">Attributed direct spend<\/div><div class=\"k-val\"><span class=\"cur\">${curSym()}<\/span>${money(m.total).replace(curSym(),'')}<\/div><div class=\"k-foot\">Attributed direct visitor spend only \u2014 no ripple applied<\/div><\/div>`));\n  kpis.appendChild(kpiTile('Estimated tax revenue',money(m.tax),'',`Broward County ${money(m.taxLocal)} \u00b7 State of Florida ${money(m.taxState)}`,()=>go('#\/tax')));\n  kpis.appendChild(kpiTile('Partner-captured spend',money(Object.values(m.partners).reduce((a,b)=>a+b,0)),'','across 4 segments',()=>go('#\/partners')));\n  kpis.appendChild(kpiTile('Interaction rate \u00b7 modeled',pct(L.interactionRate,1),'',`${num(m.interactions)} projected sessions`));\n  wrap.appendChild(kpis);\n\n  const r2=h('<div class=\"grid row-2\" style=\"margin-bottom:16px\"><\/div>');\n  r2.appendChild(card('Visitor funnel','this market',[buildFunnel(m)]));\n  r2.appendChild(buildLocTaxCard(m,L));\n  wrap.appendChild(r2);\n\n  const r3=h('<div class=\"grid row-2\" style=\"margin-bottom:16px\"><\/div>');\n  const parts=Object.entries(m.partners).map(([pid,v])=>({v,name:SEG[pid].name,color:SEG[pid].color}));\n  const pc=card('Partner impact \u2014 '+L.name,'who captured the visitor dollar','',' ');\n  const pin=h('<div style=\"display:flex;gap:20px;align-items:center;flex-wrap:wrap\"><\/div>');\n  pin.appendChild(donut(parts));\n  const pl=h('<div style=\"flex:1;min-width:200px\"><\/div>');\n  parts.sort((a,b)=>b.v-a.v).forEach(p=>pl.appendChild(h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div class=\"lr-name\" style=\"display:flex;align-items:center;gap:7px\"><span class=\"sw\" style=\"width:11px;height:11px;border-radius:3px;background:${p.color}\"><\/span>${p.name}<\/div><div class=\"lr-val\">${money(p.v)}<\/div><\/div>`)));\n  pin.appendChild(pl);pc.appendChild(pin);r3.appendChild(pc);\n  r3.appendChild(card('Monthly impact','seasonality',[areaChart(seriesFor(L,k),MONTHS,{h:210})]));\n  wrap.appendChild(r3);\n\n  \/\/ sample named partners\n  wrap.appendChild(buildSamplePartners(L,m));\n  return wrap;\n}\nfunction buildLocTaxCard(m,L){\n  const b=m.taxBreak,r=L.tax;\n  const rows=[\n    {label:`Broward County Tourist Development Tax (6.0%)`,parts:[{v:b.hotelTOT,color:'var(--s-local)',name:'Tourist Development Tax'}],total:b.hotelTOT},\n    {label:`Sales & use (${r.sales}%)`,parts:[{v:b.salesTax,color:'#33abe0',name:'Combined sales & use tax'}],total:b.salesTax},\n    {label:`Vehicle rental (${r.rental}%)`,parts:[{v:b.rentalTax,color:'#7cc9e8',name:'Vehicle-rental tax'}],total:b.rentalTax},\n    {label:'Florida corporate capture capture',parts:[{v:b.stateTax,color:'var(--s-state)',name:'Florida corporate capture'}],total:b.stateTax},\n  ];\n  const c=card('Estimated tax revenue','by jurisdiction','',' ');\n  c.appendChild(rankedStacked(rows,{pl:150,w:560}));\n  c.appendChild(h(`<div class=\"note\">Greater Fort Lauderdale rates: 6.0% Broward County Tourist Development Tax \u2014 <b style=\"color:var(--text)\">100% local<\/b>, and the tax that funds Visit Lauderdale, the Broward County Convention Center and beach renourishment; 7.0% combined sales &amp; use (6.0pp Florida \/ 1.0pp Broward); 7.0% applied to vehicle rentals, with Florida's flat $2.00-per-day surcharge not modeled. Florida levies no personal income tax, so the fourth line is a small modeled corporate-income capture rather than a statutory personal rate. <b style=\"color:var(--text)\">The taxable base<\/b> is attributed direct spend \u2014 this build applies no ripple. No re-circulation multiplier is applied.<\/div>`));\n  return c;\n}\nfunction buildSamplePartners(L,m){\n  \/* Real local businesses, institutions and partner groupings. Illustrative\n     placement of attributed dollars \u2014 no partner has been contacted, quoted\n     or committed, and no individual hotel property is named. *\/\n  const SAMPLES={\n    beach:     ['Beachfront hotel partners','A1A dining partners','Fort Lauderdale Beach','Beachfront events & festivals'],\n    lasolas:   ['Las Olas hotel partners','Las Olas restaurant row','Las Olas galleries & boutiques','Las Olas Art Fair'],\n    port:      ['Pre-cruise hotel partners','Port-area dining partners','Water taxi & shore excursions','Cruise line sailings'],\n    convention:['Convention-district hotel partners','Downtown dining partners','Riverwalk & Broward Center','Broward County Convention Center'],\n    hollywood: ['Hollywood Beach hotel partners','Broadwalk dining partners','Hollywood Beach Broadwalk','Broadwalk events'],\n    riverwalk: ['Downtown hotel partners','Riverwalk dining partners','Broward Center for the Performing Arts','Riverwalk programming'],\n  };\n  const names=SAMPLES[L.id]||['Hotel','Restaurant','Attraction','Venue'];\n  const segIds=['hotels','dining','attractions','events'];\n  const c=card('Attributed partners','illustrative \u2014 the split of dollars across them is modeled, and no partner has been contacted, quoted or committed','',' ');\n  const tbl=h('<table><thead><tr><th>Partner<\/th><th>Segment<\/th><th>Attributed spend<\/th><th>Referrals<\/th><\/tr><\/thead><tbody><\/tbody><\/table>');\n  const tb=tbl.querySelector('tbody');\n  segIds.forEach((sid,i)=>{\n    const v=m.partners[sid]*(0.34-i*0.03);\n    const refs=Math.round(m.highIntent*L.mix[sid]*0.4);\n    tb.appendChild(h(`<tr><td><b>${names[i]}<\/b><\/td><td><span class=\"pill tag-mid\">${SEG[sid].name}<\/span><\/td><td class=\"tnum\">${money2(v)}<\/td><td class=\"tnum\">${refs.toLocaleString()}<\/td><\/tr>`));\n  });\n  c.appendChild(tbl);\n  return c;\n}\n\n\/* ================= PARTNER DETAIL ================= *\/\nfunction pagePartners(){\n  const k=K(),m=rollup(k);const tot=Object.values(m.partners).reduce((a,b)=>a+b,0);\n  const wrap=h('<div class=\"fade\"><\/div>');\n  wrap.appendChild(h(`<a class=\"backbtn\" onclick=\"go('#\/overview')\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M15 18l-6-6 6-6\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg> ${ACT().region} overview<\/a>`));\n  wrap.appendChild(h(`<div class=\"crumbs\"><a onclick=\"go('#\/overview')\">${ACT().region}<\/a><span class=\"sep\">\u203a<\/span><span>Partner economic impact<\/span><\/div>`));\n  wrap.appendChild(h(`<div class=\"pagehead\"><div><div class=\"eyebrow\">${ACT().region} \u00b7 Partner economic impact<\/div><h1>What the visitor dollar did for partners<\/h1><\/div><div class=\"meta\">${periodLabel()}<br><b style=\"color:var(--text)\">${money(tot)}<\/b> attributed<\/div><\/div>`));\n  wrap.appendChild(h(aiBanner('TrueImpact AI \u00b7 Partner read',aiPartner(k))));\n\n  \/\/ segment cards\n  const icons={hotels:'M3 21V9l9-6 9 6v12M9 21v-6h6v6',dining:'M6 3v7m0 0a3 3 0 006 0V3M6 10v11M18 3c-2 0-3 2-3 5s1 4 3 4v9',attractions:'M3 21l9-18 9 18M8 21v-5h8v5',events:'M3 8h18M3 8l2-4h14l2 4M5 8v12h14V8'};\n  const cards=h('<div class=\"seg-cards\" style=\"margin-bottom:16px\"><\/div>');\n  Object.entries(m.partners).sort((a,b)=>b[1]-a[1]).forEach(([id,v])=>{\n    const c=h(`<div class=\"seg-card\"><div class=\"sc-top\"><span class=\"sc-ic\" style=\"background:${SEG[id].color}\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"${icons[id]}\" stroke=\"#fff\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg><\/span>${SEG[id].name}<\/div><div class=\"sc-val\">${money(v)}<\/div><div class=\"sc-sub\">${pct(v\/tot,0)} of partner impact<\/div><\/div>`);\n    cards.appendChild(c);\n  });\n  wrap.appendChild(cards);\n\n  \/\/ impact by segment across locations (stacked ranked)\n  const rows=[...LOCS()].map(L=>{const mm=deriveMetrics(L,k);const p=mm.partners;const t=Object.values(p).reduce((a,b)=>a+b,0);\n    return {label:L.name,total:t,onClick:()=>go('#\/location\/'+L.id),parts:['hotels','dining','attractions','events'].map(s=>({v:p[s],color:SEG[s].color,name:SEG[s].name}))};\n  }).sort((a,b)=>b.total-a.total);\n  const c1=card('Partner impact by market','stacked by segment \u00b7 click a bar to open the market','',' ');\n  c1.appendChild(rankedStacked(rows,{pl:120,w:660}));\n  c1.appendChild(legend(Object.keys(SEG).map(id=>({name:SEG[id].name,color:SEG[id].color}))));\n  wrap.appendChild(c1);\n\n  \/\/ why it matters\n  wrap.appendChild(card('What the partner portal changes','the mechanism, not a forecast',[h(`<div class=\"stat-inline\" style=\"margin-top:6px\"><div class=\"si\"><b>Per-partner<\/b><span>attributed dollars, not blended averages<\/span><\/div><div class=\"si\"><b>Referral-level<\/b><span>every dollar ties to a kiosk event<\/span><\/div><div class=\"si\"><b>Direct only<\/b><span>no ripple in the figure a partner sees<\/span><\/div><\/div>`),h('<div class=\"note\">When a business can see its own attributed dollars rather than a blended network average, flat-dues membership becomes performance-aligned membership. No uplift percentages are shown here because this destination has no partner-portal baseline yet \u2014 that is a figure to establish after go-live, not to assert before it.<\/div>')]));\n  return wrap;\n}\n\/* SEG_SHARE removed \u2014 hardcoded template constant that contradicted the computed mix. *\/\n\n\/* ================= TAX DETAIL ================= *\/\nfunction pageTax(){\n  const k=K(),m=rollup(k),b=m.taxBreak;\n  const wrap=h('<div class=\"fade\"><\/div>');\n  wrap.appendChild(h(`<a class=\"backbtn\" onclick=\"go('#\/overview')\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M15 18l-6-6 6-6\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg> ${ACT().region} overview<\/a>`));\n  wrap.appendChild(h(`<div class=\"crumbs\"><a onclick=\"go('#\/overview')\">${ACT().region}<\/a><span class=\"sep\">\u203a<\/span><span>Estimated tax revenue<\/span><\/div>`));\n  wrap.appendChild(h(`<div class=\"pagehead\"><div><div class=\"eyebrow\">${ACT().region} \u00b7 Public revenue impact<\/div><h1>Estimated tax revenue by zone<\/h1><\/div><div class=\"meta\">${periodLabel()}<br><b style=\"color:var(--text)\">${money(m.tax)}<\/b> total est.<\/div><\/div>`));\n  wrap.appendChild(h(aiBanner('TrueImpact AI \u00b7 Tax read',aiTax(k))));\n\n  \/\/ KPI tiles by tax type\n  const kpis=h('<div class=\"grid kpis\" style=\"margin-bottom:16px\"><\/div>');\n  kpis.appendChild(h(`<div class=\"card kpi hero\"><div class=\"glow\"><\/div><div class=\"k-label\">Total estimated tax revenue<\/div><div class=\"k-val\"><span class=\"cur\">${curSym()}<\/span>${money(m.tax).replace(curSym(),'')}<\/div><div class=\"k-foot\">Broward County ${money(m.taxLocal)} + State of Florida ${money(m.taxState)}<\/div><\/div>`));\n  kpis.appendChild(kpiTile('Tourist Development Tax',money(b.hotelTOT),'',`6.0% \u2014 100% local, and the tax that funds this organisation`));\n  kpis.appendChild(kpiTile('Sales & use',money(b.salesTax),'','7.0% \u2014 of which 1.0pp Broward County'));\n  kpis.appendChild(kpiTile('Vehicle rental',money(b.rentalTax),'','7.0% on vehicle rentals \u00b7 $2\/day surcharge not modeled'));\n  wrap.appendChild(kpis);\n\n  \/\/ stacked tax by location (local components + state)\n  const rows=[...LOCS()].map(L=>{const mm=deriveMetrics(L,k),bb=mm.taxBreak;\n    return {label:L.name,total:mm.tax,onClick:()=>go('#\/location\/'+L.id),parts:[\n      {v:bb.hotelTOT,color:'var(--s-local)',name:'Tourist Development Tax'},\n      {v:bb.salesTax,color:'#33abe0',name:'Sales & use'},\n      {v:bb.rentalTax,color:'#7cc9e8',name:'Vehicle rental'},\n      {v:bb.stateTax,color:'var(--s-state)',name:'Florida corporate capture'},\n    ]};}).sort((a,b)=>b.total-a.total);\n  const c1=card('Estimated tax revenue by district','lodging \u00b7 sales &amp; use \u00b7 rental \u00b7 state \u00b7 click a bar to drill','',' ');\n  c1.appendChild(rankedStacked(rows,{pl:120,w:680}));\n  c1.appendChild(legend([{name:'Tourist Development Tax',color:'var(--s-local)'},{name:'Sales & use',color:'#33abe0'},{name:'Vehicle rental',color:'#7cc9e8'},{name:'Florida corporate capture',color:'var(--s-state)'}]));\n  wrap.appendChild(c1);\n\n  \/\/ no induced \/ re-circulation multiplier in a direct-spend-only build\n  const recircCard=card('What this model does NOT count','the deliberate omissions','',' ');\n  recircCard.appendChild(h(`<div class=\"stat-inline\" style=\"margin-top:4px\"><div class=\"si\"><b>${money(m.tax)}<\/b><span>hard tax captured<\/span><\/div><div class=\"si\"><b>0\u00d7<\/b><span>ripple \/ indirect multiplier<\/span><\/div><div class=\"si\"><b>0\u00d7<\/b><span>induced re-circulation<\/span><\/div><div class=\"si\"><b>${money(m.total)}<\/b><span>attributed direct spend \u2014 the whole figure<\/span><\/div><\/div>`));\n  recircCard.appendChild(h(`<div class=\"note\">Most destination impact models add two things this one does not. The first is an <b style=\"color:var(--text)\">indirect and induced tail<\/b> on visitor spending, commonly 0.7\u20131.1\u00d7 the direct figure. The second is a <b style=\"color:var(--text)\">re-circulation multiplier<\/b> on public tax revenue, on the argument that public money funds payroll and procurement that get re-spent locally. Both are defensible and both are standard. Neither is applied here, because Visit Lauderdale publishes no total-economic-impact figure to derive a local multiplier from, and an imported benchmark is the first thing a sceptical reader attacks. Every dollar on this dashboard is first-round attributed visitor spend and the tax levied directly on it \u2014 nothing else.<\/div>`));\n  wrap.appendChild(recircCard);\n\n  \/\/ full data table\n  const c2=card('Full tax detail','modeled line items \u00b7 '+periodLabel()+' \u00b7 levied on attributed direct spend; no ripple in the base','',' ');\n  const tbl=h('<table><thead><tr><th>District<\/th><th>Lodging %<\/th><th>Tourist Development Tax<\/th><th>Sales &amp; use<\/th><th>Rental<\/th><th>State<\/th><th>Total tax<\/th><\/tr><\/thead><tbody><\/tbody><\/table>');\n  const tb=tbl.querySelector('tbody');\n  rows.forEach(r=>{const L=LOCS().find(x=>x.name===r.label);const mm=deriveMetrics(L,k),bb=mm.taxBreak,rt=L.tax;\n    const tr=h(`<tr><td><b>${L.name}<\/b><\/td><td class=\"tnum\">${rt.tot}%<\/td><td class=\"tnum\">${money2(bb.hotelTOT)}<\/td><td class=\"tnum\">${money2(bb.salesTax)}<\/td><td class=\"tnum\">${money2(bb.rentalTax)}<\/td><td class=\"tnum\">${money2(bb.stateTax)}<\/td><td class=\"tnum\"><b>${money2(mm.tax)}<\/b><\/td><\/tr>`);\n    tr.addEventListener('click',()=>go('#\/location\/'+L.id));tb.appendChild(tr);\n  });\n  c2.appendChild(tbl);\n  wrap.appendChild(c2);\n  return wrap;\n}\n\n\/* ================= NETWORK ROLL-UP (all destinations) ================= *\/\nfunction pageNetwork(){\n  const k=K(),{net,parts}=networkAgg(k);\n  const wrap=h('<div class=\"fade\"><\/div>');\n  wrap.appendChild(h(`<div class=\"pagehead\"><div><div class=\"eyebrow\">TrueOmni Network \u00b7 All destinations<\/div><h1>Total economic impact \u2014 every kiosk, every destination<\/h1><\/div><div class=\"meta\">${periodLabel()}<br><b style=\"color:var(--text);font-size:14px\">${net.destinations} destinations \u00b7 ${num(net.kiosks)} kiosks<\/b><\/div><\/div>`));\n  wrap.appendChild(h(`<div class=\"crumbs\"><span>TrueOmni Network<\/span><span class=\"sep\">\u203a<\/span><span>All destinations<\/span><\/div>`));\n  wrap.appendChild(h(aiBanner('TrueImpact AI \u00b7 Network roll-up',aiNetwork(k))));\n\n  const kpis=h('<div class=\"grid kpis\" style=\"margin-bottom:16px\"><\/div>');\n  kpis.appendChild(h(`<div class=\"card kpi hero\"><div class=\"glow\"><\/div><div class=\"k-label\">Network economic impact<\/div><div class=\"k-val\"><span class=\"cur\">${curSym()}<\/span>${money(net.total).replace(curSym(),'')}<\/div><div class=\"k-foot\">Direct ${money(net.direct)} + Influenced ${money(net.influenced)} \u00b7 ${net.destinations} destinations, ledger-level<\/div><\/div>`));\n  kpis.appendChild(kpiTile('Public tax revenue',money(net.tax),'',`Local ${money(net.taxLocal)} \u00b7 State ${money(net.taxState)}`));\n  kpis.appendChild(kpiTile('Partner-captured spend',money(Object.values(net.partners).reduce((a,b)=>a+b,0)),'','across all destinations'));\n  kpis.appendChild(kpiTile('Trackable visitor actions',num(net.interactions),'',`${num(net.conversions)} attributed conversions`));\n  wrap.appendChild(kpis);\n\n  \/\/ destinations ranked (real 3 + modeled long-tail)\n  const destRows=NET_IDS.map((id,i)=>({id,name:DESTINATIONS[id].name,region:DESTINATIONS[id].region,kind:DESTINATIONS[id].kind,m:parts[i]}))\n    .sort((a,b)=>b.m.total-a.m.total);\n  const maxD=destRows[0].m.total;\n  const dl=h('<div class=\"loclist\"><\/div>');\n  destRows.forEach(d=>{\n    const row=h(`<div class=\"locrow\"><div><div class=\"lr-name\">${d.name}<\/div><div class=\"lr-sub\">${d.kind} \u00b7 ${d.m.kiosks} kiosks \u00b7 ${d.region}<\/div><\/div><div class=\"lr-val\">${money(d.m.total)}<div class=\"lr-sub\" style=\"font-weight:600\">${money(d.m.tax)} tax<\/div><\/div><span class=\"chev\">\u203a<\/span><div class=\"lr-bar\"><i style=\"width:${(d.m.total\/maxD*100).toFixed(0)}%\"><\/i><\/div><\/div>`);\n    row.addEventListener('click',()=>setDest(d.id));dl.appendChild(row);\n  });\n  const realTot=parts.reduce((a,b)=>a+b.total,0), othersTot=net.total-realTot, othersTax=net.tax-parts.reduce((a,b)=>a+b.tax,0), othersK=net.kiosks-parts.reduce((a,b)=>a+b.kiosks,0);\n  dl.appendChild(h(`<div class=\"locrow\" style=\"opacity:.85;cursor:default\"><div><div class=\"lr-name\">Other destinations<\/div><div class=\"lr-sub\">~${num(othersK)} kiosks \u00b7 modeled long-tail<\/div><\/div><div class=\"lr-val\">${money(othersTot)}<div class=\"lr-sub\" style=\"font-weight:600\">${money(othersTax)} tax<\/div><\/div><span class=\"chev\" style=\"opacity:0\">\u203a<\/span><div class=\"lr-bar\"><i style=\"width:${(othersTot\/maxD*100).toFixed(0)}%;background:linear-gradient(90deg,#8b96a5,#5B6675)\"><\/i><\/div><\/div>`));\n\n  const r1=h('<div class=\"grid row-2\" style=\"margin-bottom:16px\"><\/div>');\n  r1.appendChild(card('Destinations ranked','click a live destination to drill into its kiosks',[dl,h('<div class=\"note\">Roll-up view, unused in this single-tenant deployment. The long-tail bucket models the remaining network to the published footprint.<\/div>')]));\n  \/\/ network partner mix\n  const parts2=Object.entries(net.partners).map(([id,v])=>({v,name:SEG[id].name,color:SEG[id].color}));\n  const pc=card('Network partner impact','hotels \u00b7 dining \u00b7 attractions \u00b7 events','',' ');\n  const pin=h('<div style=\"display:flex;gap:20px;align-items:center;flex-wrap:wrap\"><\/div>');\n  pin.appendChild(donut(parts2));\n  const plist=h('<div style=\"flex:1;min-width:190px\"><\/div>');\n  parts2.sort((a,b)=>b.v-a.v).forEach(p=>plist.appendChild(h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div class=\"lr-name\" style=\"display:flex;align-items:center;gap:7px\"><span class=\"sw\" style=\"width:11px;height:11px;border-radius:3px;background:${p.color}\"><\/span>${p.name}<\/div><div class=\"lr-val\">${money(p.v)}<\/div><\/div>`)));\n  pin.appendChild(plist);pc.appendChild(pin);\n  r1.appendChild(pc);\n  wrap.appendChild(r1);\n\n  \/\/ engagement entry (network scope)\n  wrap.appendChild(engMiniCard(k));\n  wrap.lastChild.style.marginBottom='16px';\n\n  \/\/ network trend\n  wrap.appendChild(card('Network economic impact trend','monthly \u00b7 all destinations',[areaChart(networkSeries(k),MONTHS,{h:220}),h('<div class=\"note\">Roll-up view, unused in this single-tenant deployment. Aggregate curve built from behavioral data across the network. Each destination re-circulates public tax dollars into its own community \u2014 an estimated '+money(net.recirc)+' network-wide (direct spend only, no multipliers).<\/div>')]));\n  return wrap;\n}\n\n\/* ================= ENGAGEMENT DRILL-THROUGH ================= *\/\nfunction engScope(k){\n  if(activeDest==='network'){const m=networkAgg(k).net;return {m,name:'the TrueOmni Network',seg:segSharesOf(m),locs:[]};}\n  if(engLoc!=='all'){const L=findLoc(engLoc);if(L){const m=deriveMetrics(L,k);return {m,name:L.name,seg:segSharesOf(m),locs:LOCS()};}}\n  const m=rollup(k);return {m,name:ACT().region,seg:segSharesOf(m),locs:LOCS()};\n}\nfunction engMetrics(mode){\n  const base={\n    interactions:{label:'Attention',   get:r=>r.interactions, fmt:num,   tfmt:v=>num(v)+' sessions'},\n    highIntent:  {label:'High-intent',  get:r=>r.highIntent,   fmt:num,   tfmt:v=>num(v)+' actions'},\n    conversions: {label:'Conversions',  get:r=>r.conversions,  fmt:num,   tfmt:v=>num(v)+' conv.'},\n    impact:      {label:'Attributed $', get:r=>r.impact,       fmt:money, tfmt:money2},\n  };\n  if(mode==='modules') base.time={label:'Time spent',get:r=>r.timeHrs,fmt:v=>num(v)+' h',tfmt:v=>num(v)+' hours'};\n  else base.roas={label:'ROI (ROAS)',get:r=>r.roas,fmt:v=>v.toFixed(1)+'\u00d7',tfmt:v=>v.toFixed(2)+'\u00d7 return'};\n  return base;\n}\nfunction setEng(key,val){ if(key==='mode'){engMode=val;engMetric='interactions';} else if(key==='loc'){engLoc=val;} else {engMetric=val;}\n  location.hash = '#\/'+(engMode==='modules'?'modules':'campaigns'); render(); }\nfunction pageEngagement(){\n  const k=K(), sc=engScope(k);\n  const items = engMode==='modules'?MODULES:CAMPAIGNS;\n  const rows = splitFunnel(sc.m, items, sc.seg);\n  const M = engMetrics(engMode);\n  if(!M[engMetric]) engMetric='interactions';\n  const met=M[engMetric];\n  rows.sort((a,b)=>(met.get(b)||0)-(met.get(a)||0));\n  const colorOf=r=> engMode==='modules'?SEG[r.tag].color:CAMP_TYPE[r.type];\n\n  const wrap=h('<div class=\"fade\"><\/div>');\n  wrap.appendChild(h(`<a class=\"backbtn\" onclick=\"go('#\/overview')\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M15 18l-6-6 6-6\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg> ${activeDest==='network'?'Network':ACT().region} overview<\/a>`));\n  wrap.appendChild(h(`<div class=\"crumbs\"><a onclick=\"go('#\/overview')\">${activeDest==='network'?'TrueOmni Network':ACT().region}<\/a><span class=\"sep\">\u203a<\/span><span>Engagement drill-through<\/span><\/div>`));\n  wrap.appendChild(h(`<div class=\"pagehead\"><div><div class=\"eyebrow\">Marketing \u00b7 Engagement drill-through<\/div><h1>What draws attention \u2014 and what to invest in<\/h1><div class=\"ch-sub\" style=\"margin-top:6px;color:var(--text-2);max-width:780px\">Nothing on this page is measured. Module and campaign shares are <b>modeled<\/b> from the Phase-1 district mix and the TrueOmni module set. They exist so the network launches with a hypothesis worth testing, and should be replaced with the CMS export within the first quarter of operation.<\/div><\/div><div class=\"meta\">${periodLabel()}<br><b style=\"color:var(--text)\">${sc.name}<\/b><\/div><\/div>`));\n  wrap.appendChild(h(aiBanner('TrueImpact AI \u00b7 Investment read',aiEngagement(rows,engMode,sc.name))));\n\n  \/\/ filters\n  const f=h('<div class=\"filters\"><\/div>');\n  f.appendChild(h(`<div class=\"fgroup\"><span class=\"flab\">Dimension<\/span><div class=\"fseg\">\n    <button class=\"${engMode==='modules'?'on':''}\" onclick=\"setEng('mode','modules')\">Modules<\/button>\n    <button class=\"${engMode==='campaigns'?'on':''}\" onclick=\"setEng('mode','campaigns')\">Campaigns<\/button><\/div><\/div>`));\n  if(sc.locs.length){\n    const opts=['<option value=\"all\"'+(engLoc==='all'?' selected':'')+'>All markets<\/option>']\n      .concat(sc.locs.map(L=>`<option value=\"${L.id}\"${engLoc===L.id?' selected':''}>${L.name}<\/option>`)).join('');\n    f.appendChild(h(`<div class=\"fgroup\"><span class=\"flab\">Location<\/span><select class=\"fsel\" onchange=\"setEng('loc',this.value)\">${opts}<\/select><\/div>`));\n  }\n  const mkeys=Object.keys(M);\n  f.appendChild(h(`<div class=\"fgroup\"><span class=\"flab\">Rank by<\/span><div class=\"fseg\">${mkeys.map(mk=>`<button class=\"${mk===engMetric?'on':''}\" onclick=\"setEng('metric','${mk}')\">${M[mk].label}<\/button>`).join('')}<\/div><\/div>`));\n  wrap.appendChild(f);\n\n  \/\/ ranked bar + top funnel\n  const r1=h('<div class=\"grid row-2\" style=\"margin-bottom:16px\"><\/div>');\n  const barRows=rows.map(r=>({label:engShort(r.name),parts:[{v:met.get(r)||0,color:colorOf(r),name:met.label}],total:met.get(r)||0}));\n  const rk=card(`${engMode==='modules'?'Modules':'Campaigns'} ranked by ${met.label.toLowerCase()}`,'stack ranking','',' ');\n  rk.appendChild(rankedStacked(barRows,{pl:150,w:640,pr:74,fmt:met.fmt,tfmt:met.tfmt}));\n  r1.appendChild(rk);\n  \/\/ top item funnel drill-through\n  const top=rows[0];\n  const fc=card('Funnel drill-through','top '+(engMode==='modules'?'module':'campaign')+': '+top.name,[buildEngFunnel(top),h(`<div class=\"note\">Exposures \u2192 interactions \u2192 high-intent \u2192 attributed conversions for <b>${top.name}<\/b>. Conversion rate <b>${pct(top.convRate,1)}<\/b>${top.roas?` \u00b7 ROAS <b>${top.roas.toFixed(1)}\u00d7<\/b>`:''}${top.timeHrs?` \u00b7 <b>${num(top.timeHrs)}<\/b> engaged hours`:''}.<\/div>`)]);\n  r1.appendChild(fc);\n  wrap.appendChild(r1);\n\n  \/\/ full table\n  const tc=card(`${engMode==='modules'?'Module':'Campaign'} funnel \u2014 full breakdown`,'every stage \u00b7 '+periodLabel()+' \u00b7 module shares are modeled pre-deployment','',' ');\n  tc.appendChild(buildEngTable(rows,engMode,colorOf));\n  wrap.appendChild(tc);\n\n  \/\/ A\/B tests (campaigns only)\n  if(engMode==='campaigns'){\n    const tests=AB_TESTS.map(t=>({t,row:rows.find(r=>r.id===t.camp)})).filter(x=>x.row);\n    const abWrap=card('A\/B campaign tests','conversion rate by variant \u2014 compare & contrast',[h('<div class=\"grid row-2\" id=\"abgrid\" style=\"margin-top:2px\"><\/div>')]);\n    const grid=abWrap.querySelector('#abgrid');\n    tests.forEach(({t,row})=>grid.appendChild(buildABcard(t,row)));\n    wrap.appendChild(abWrap);\n  }\n  return wrap;\n}\nfunction buildEngFunnel(r){\n  const stages=[['Exposures',r.exposures,''],['Interactions',r.interactions,pct(r.interactions\/r.exposures,0)+' of exposed'],\n    ['High-intent',r.highIntent,pct(r.highIntent\/r.interactions,0)+' of interactions'],['Conversions',r.conversions,pct(r.conversions\/r.highIntent,0)+' of high-intent']];\n  const max=stages[0][1],cols=['#0E2240','#14508C','#1C9CD8','#33abe0'];\n  const box=h('<div class=\"funnel\"><\/div>');\n  stages.forEach(([n,v,rate],i)=>box.appendChild(h(`<div class=\"fstage\"><div class=\"fbar\" style=\"width:${26+40*(v\/max)}%;background:${cols[i]}\">${n}<\/div><div class=\"fmeta\"><b>${num(v)}<\/b> <span class=\"frate\">${rate}<\/span><\/div><\/div>`)));\n  return box;\n}\nfunction buildEngTable(rows,mode,colorOf){\n  const head = mode==='modules'\n    ? '<tr><th>Module<\/th><th>Exposures<\/th><th>Interactions<\/th><th>High-intent<\/th><th>Conversions<\/th><th>Conv %<\/th><th>Time<\/th><th>Attributed $<\/th><th>$\/1K int<\/th><\/tr>'\n    : '<tr><th>Campaign<\/th><th>Interactions<\/th><th>High-intent<\/th><th>Conversions<\/th><th>Conv %<\/th><th>Spend<\/th><th>Attributed $<\/th><th>ROAS<\/th><\/tr>';\n  const tbl=h(`<table><thead>${head}<\/thead><tbody><\/tbody><\/table>`);\n  const tb=tbl.querySelector('tbody');\n  rows.forEach(r=>{\n    const dot=`<span class=\"sw\" style=\"display:inline-block;width:10px;height:10px;border-radius:3px;background:${colorOf(r)};margin-right:7px;vertical-align:-1px\"><\/span>`;\n    const conv=`<span class=\"conv-pill\">${pct(r.convRate,1)}<\/span>`;\n    if(mode==='modules'){\n      tb.appendChild(h(`<tr><td>${dot}<b>${r.name}<\/b><\/td><td class=\"tnum\">${num(r.exposures)}<\/td><td class=\"tnum\">${num(r.interactions)}<\/td><td class=\"tnum\">${num(r.highIntent)}<\/td><td class=\"tnum\">${num(r.conversions)}<\/td><td>${conv}<\/td><td class=\"tnum\">${num(r.timeHrs)} h<\/td><td class=\"tnum\"><b>${money(r.impact)}<\/b><\/td><td class=\"tnum\">${money(r.valPerK)}<\/td><\/tr>`));\n    } else {\n      tb.appendChild(h(`<tr><td>${dot}<b>${r.name}<\/b><div class=\"ab-sub\" style=\"margin-left:17px\">${r.type}<\/div><\/td><td class=\"tnum\">${num(r.interactions)}<\/td><td class=\"tnum\">${num(r.highIntent)}<\/td><td class=\"tnum\">${num(r.conversions)}<\/td><td>${conv}<\/td><td class=\"tnum\">${money(r.spend)}<\/td><td class=\"tnum\"><b>${money(r.impact)}<\/b><\/td><td class=\"tnum\"><span class=\"pill ${r.roas>=4?'tag-hi':'tag-mid'}\">${r.roas.toFixed(1)}\u00d7<\/span><\/td><\/tr>`));\n    }\n  });\n  return tbl;\n}\nfunction buildABcard(t,row){\n  \/\/ split the campaign row's traffic; blended conv rate must equal row.convRate\n  const base=row.convRate, rA=base\/(t.split + (1-t.split)*t.bLift), rB=rA*t.bLift;\n  const intA=row.interactions*t.split, intB=row.interactions*(1-t.split);\n  const cvA=intA*rA, cvB=intB*rB;\n  const impA=row.impact*(cvA\/(cvA+cvB)), impB=row.impact*(cvB\/(cvA+cvB));\n  const winnerB=t.bLift>1, decisive=t.conf>=0.9;\n  const proven=t.conf>=0.9;   \/\/ pre-launch tests have conf 0 \u2014 never show a winner\n  const maxR=Math.max(rA,rB);\n  const badge = t.status==='No winner yet' ? `<span class=\"ab-badge ab-none\">Running \u00b7 ${pct(t.conf,0)} conf.<\/span>`\n    : proven ? `<span class=\"ab-badge ab-win\">${t.status} \u00b7 ${pct(t.conf,0)} conf.<\/span>`\n    : `<span class=\"ab-badge ab-none\">${t.status} \u00b7 not yet run<\/span>`;\n  const lift=(rB\/rA-1);\n  const c=h(`<div class=\"ab-card\"><\/div>`);\n  c.appendChild(h(`<div class=\"ab-head\"><div><div class=\"abt\">${t.name}<\/div><div class=\"abs\">${row.name} \u00b7 ${num(row.interactions)} sessions split ${Math.round(t.split*100)}\/${Math.round((1-t.split)*100)}<\/div><\/div>${badge}<\/div>`));\n  const variant=(tag,label,r,cv,imp,isWin)=>`\n    <div class=\"ab-var\">\n      <div class=\"ab-tag\" style=\"background:${isWin?'var(--good)':'var(--slate)'}\">${tag}<\/div>\n      <div><div class=\"ab-nm\">${label} ${isWin&&proven?'<span class=\"conv-pill\" style=\"background:rgba(12,163,12,.14);color:var(--good)\">winner<\/span>':''}<\/div><div class=\"ab-sub\">${num(cv)} conversions \u00b7 ${money(imp)} attributed<\/div><\/div>\n      <div class=\"ab-conv\"><b>${pct(r,2)}<\/b><span>conv. rate<\/span><\/div>\n      <div class=\"ab-bar\"><i style=\"width:${(r\/maxR*100).toFixed(0)}%;background:${isWin?'var(--good)':'var(--slate)'}\"><\/i><\/div>\n    <\/div>`;\n  c.appendChild(h(variant('A',t.a,rA,cvA,impA,proven&&!winnerB)));\n  c.appendChild(h(variant('B',t.b,rB,cvB,impB,proven&&winnerB)));\n  c.appendChild(h(`<div class=\"ab-foot\"><span>Variant B vs A<\/span><span><b style=\"color:${proven?(lift>=0?'var(--good)':'var(--warn)'):'var(--muted)'}\">${lift>=0?'+':''}${pct(lift,1)}<\/b> modeled target lift (hypothesis \u2014 test not yet run)<\/span><\/div>`));\n  return c;\n}\n\n\/* ================= MEDIA NETWORK ================= *\/\n\/* The Media Network view is always an ANNUAL run-rate \u2014 scaling it by the\n   period selector would print partial-year values under \"\/ yr\" labels. *\/\nfunction aiMedia(_k){const m=mediaMetrics(1), sg=signageMetrics(1), ad=totalAdRevenue(1), p1=phase1Payback(), gp=growthPayback();\n  return `Phase 1 puts <b>${m.kiosks} interactive kiosks<\/b> and <b>${sg.screens} digital signage screens<\/b> across six districts. Together they are projected to deliver <b>${num(m.impressions+sg.impressions)}<\/b> Screen Impressions a year \u2014 ${num(m.impressions)} from the kiosk attract loop and ${num(sg.impressions)} from signage reaching ${num(sg.reach)} passers-by \u2014 for <span class=\"em\">${money(ad.total)}<\/span> of Total Advertising Revenue at mature Fill Rate. That splits <b>${money(ad.kiosk)}<\/b> kiosk (effective CPM $${m.cpm.toFixed(2)}, ${money2(m.perKioskMo)} per kiosk per month at mature fill) and <b>${money(ad.signage)}<\/b> signage (${money2(sg.perScreenMo)} per screen per month at mature fill, ${pct(sg.shareOfVoice)} share of voice on a ${SIGNAGE.loopSec}-second loop). About <b>${pct(MEDIA.streamSplit.direct)}<\/b> of kiosk revenue is direct\/native local sales, the rest programmatic through Google Ad Manager. <b>The economics that matter:<\/b> against ${money2(p1.capital)} of Phase-1 hardware and ${money2(p1.softwareYr)} a year of software, the advertising layer alone repays the entire build in about <b>${p1.years.toFixed(1)} years<\/b> at mature fill \u2014 and scaling to 30 kiosks and 20 screens lifts Total Advertising Revenue to <span class=\"em\">${money(gp.adFull)}<\/span>, repaying that expansion in roughly <b>${gp.years.toFixed(1)} years<\/b>.`;}\nfunction mediaMiniCard(_k){const m=mediaMetrics(1), sg=signageMetrics(1), ad=totalAdRevenue(1), p1=phase1Payback();\n  const c=card('Media Network \u00b7 Total Advertising Revenue','the self-funding layer \u00b7 kiosks + signage \u00b7 annual run-rate at mature fill','',' ');\n  c.appendChild(h(`<div class=\"stat-inline\" style=\"margin-top:2px\"><div class=\"si\"><b>${money(ad.total)}<\/b><span>Total Advertising Revenue \/ yr<\/span><\/div><div class=\"si\"><b>${num(m.impressions+sg.impressions)}<\/b><span>Screen Impressions \/ yr \u00b7 kiosk ${num(m.impressions)} + signage ${num(sg.impressions)}<\/span><\/div><div class=\"si\"><b>${money2(m.perKioskMo)}<\/b><span>per kiosk \/ mo \u00b7 mature fill<\/span><\/div><div class=\"si\"><b>${money2(sg.perScreenMo)}<\/b><span>per screen \/ mo \u00b7 mature fill<\/span><\/div><div class=\"si\"><b style=\"color:var(--good)\">${p1.years.toFixed(1)} yr<\/b><span>Phase-1 payback \u00b7 at mature fill<\/span><\/div><\/div>`));\n  c.appendChild(h(`<div style=\"display:flex;gap:8px;margin-top:12px\"><button class=\"chip\" style=\"cursor:pointer\" onclick=\"go('#\/media')\">Open Media Network \u203a<\/button><\/div>`));\n  return c;\n}\nfunction pageMedia(){\n  const k=1; const m=mediaMetrics(k); const sg=signageMetrics(k); const ad=totalAdRevenue(k);  \/\/ always annual run-rate\n  const wrap=h('<div class=\"fade\"><\/div>');\n  wrap.appendChild(h(`<a class=\"backbtn\" onclick=\"go('#\/overview')\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M15 18l-6-6 6-6\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg> ${ACT().region} overview<\/a>`));\n  wrap.appendChild(h(`<div class=\"crumbs\"><span>${ACT().name}<\/span><span class=\"sep\">\u203a<\/span><a onclick=\"go('#\/overview')\">${ACT().region}<\/a><span class=\"sep\">\u203a<\/span><span>Media Network<\/span><\/div>`));\n  wrap.appendChild(h(`<div class=\"pagehead\"><div><div class=\"eyebrow\">Advertising \u00b7 Media Network<\/div><h1>Media Network \u2014 Total Advertising Revenue<\/h1><div class=\"ch-sub\" style=\"margin-top:4px;color:var(--text-2)\">Interactive kiosks + OMNI digital signage \u00b7 annual run-rate, independent of the period selector<\/div><\/div><div class=\"meta\">Sell-through at maturity<br><b style=\"color:var(--text);font-size:14px\">${m.kiosks} kiosks at ${pct(MEDIA.fillMature)} \u00b7 ${sg.screens} screens at ${pct(SIGNAGE.fillMature)}<\/b><\/div><\/div>`));\n  wrap.appendChild(h(aiBanner('TrueImpact AI \u00b7 Media read', aiMedia(k))));\n\n  const kpis=h('<div class=\"grid kpis\" style=\"margin-bottom:16px\"><\/div>');\n  kpis.appendChild(h(`<div class=\"card kpi hero\"><div class=\"glow\"><\/div><div class=\"k-label\">Total Advertising Revenue \/ yr <span style=\"opacity:.62;font-weight:600\">\u00b7 at mature fill<\/span><\/div><div class=\"k-val\"><span class=\"cur\">${curSym()}<\/span>${money(ad.total).replace(curSym(),'')}<\/div><div class=\"k-delta up\">\u25b2 scales with fill, kiosks and screens<\/div><div class=\"k-foot\">Kiosk ${money(ad.kiosk)} + signage ${money(ad.signage)} \u00b7 kiosk range ${money(m.band.floor)}\u2013${money(m.band.strong)}<\/div><\/div>`));\n  kpis.appendChild(kpiTile('Screen Impressions \/ yr',num(m.impressions+sg.impressions),'',`kiosk ${num(m.impressions)} \u00b7 signage ${num(sg.impressions)}`));\n  kpis.appendChild(kpiTile('Effective CPM \u00b7 kiosk','$'+m.cpm.toFixed(2),'',`signage $${sg.cpm.toFixed(2)} \u00b7 ${pct(MEDIA.fillMature)} kiosk Fill Rate`));\n  kpis.appendChild(kpiTile('Covers annual software',(ad.total\/MEDIA.softwareYr).toFixed(1)+'\u00d7 at mature fill','',`vs ${money2(MEDIA.softwareYr)}\/yr for 10 kiosks + 5 screens`));\n  wrap.appendChild(kpis);\n\n  const r2=h('<div class=\"grid row-2\" style=\"margin-bottom:16px\"><\/div>');\n  const invCard=card('Revenue by inventory type','container-level potential (Omni Media Network)','',' ');\n  const list=h('<div class=\"loclist\"><\/div>'); const maxv=Math.max(...m.byInv.map(x=>x.v));\n  m.byInv.slice().sort((a,b)=>b.v-a.v).forEach(x=>{list.appendChild(h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div class=\"lr-name\">${x.name}<\/div><div class=\"lr-val\">${money(x.v)}<div class=\"lr-sub\" style=\"font-weight:600\">${pct(x.share)}<\/div><\/div><div class=\"lr-bar\"><i style=\"width:${(x.v\/maxv*100).toFixed(0)}%\"><\/i><\/div><\/div>`));});\n  invCard.appendChild(list); r2.appendChild(invCard);\n  const streamCard=card('Direct vs programmatic','native sales + Google Ad Manager','',' ');\n  const parts=[{v:m.byStream.direct,name:'Direct \/ native sold',color:'var(--s-hotels)'},{v:m.byStream.programmatic,name:'Programmatic (GAM \/ DTN)',color:'var(--s-events)'}];\n  const pin=h('<div style=\"display:flex;gap:20px;align-items:center;flex-wrap:wrap\"><\/div>'); pin.appendChild(donut(parts));\n  const pl=h('<div style=\"flex:1;min-width:180px\"><\/div>'); parts.forEach(p=>pl.appendChild(h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div class=\"lr-name\" style=\"display:flex;align-items:center;gap:7px\"><span class=\"sw\" style=\"width:11px;height:11px;border-radius:3px;background:${p.color}\"><\/span>${p.name}<\/div><div class=\"lr-val\">${money(p.v)}<\/div><\/div>`))); pin.appendChild(pl);\n  streamCard.appendChild(pin); r2.appendChild(streamCard);\n  wrap.appendChild(r2);\n\n  const rampC=card('Total Advertising Revenue \u00b7 10 \u2192 20 \u2192 30 kiosks','kiosk + signage, at the sell-through achievable at each phase','',' ');\n  const rl=h('<div class=\"loclist\"><\/div>');\n  PHASES.forEach(ph=>{const locs=phaseLocs(ph);const mm=mediaMetrics(k,locs);const sgm=signageMetrics(k,locs);\n    const kfill=MEDIA.fillByPhase[ph.n], sfill=SIGNAGE.fillByPhase[ph.n];\n    const krev=mm.rev*(kfill\/MEDIA.fillMature), srev=sgm?sgm.rev*(sfill\/SIGNAGE.fillMature):0;\n    rl.appendChild(h(`<div class=\"locrow\"><div><div class=\"lr-name\">${ph.name}<\/div><div class=\"lr-sub\">${mm.kiosks} kiosks at ${pct(kfill)} fill \u00b7 ${sgm?sgm.screens:0} screens at ${pct(sfill)} fill<\/div><\/div><div class=\"lr-val\">${money(krev+srev)}<div class=\"lr-sub\" style=\"font-weight:600\">kiosk ${money(krev)} \u00b7 signage ${money(srev)}<\/div><\/div><div class=\"lr-bar\"><i style=\"width:${(mm.kiosks\/30*100).toFixed(0)}%\"><\/i><\/div><\/div>`));});\n  rampC.appendChild(rl);\n  rampC.appendChild(h(`<div class=\"note\">Two surfaces, two revenue models. Kiosk revenue is CPM-driven on ${MEDIA.imprPerEncounter} addressable impressions per encounter; signage is priced per screen per month, because a broadcast display sells as a placement rather than as an audience. The rows above run at the sell-through realistically achievable at each stage; the headline tile states everything at mature fill. Greater Fort Lauderdale sits inside the Miami\u2013Fort Lauderdale DMA, which is what the CPM band reflects. <b style=\"color:var(--text)\">All modeled<\/b> \u2014 replace the CPM band, the per-screen rate and the fill curve with the destination's own rate card.<\/div>`));\n  wrap.appendChild(rampC);\n\n  \/\/ ---- Digital Signage (broadcast reach \u2014 never transacts) ----\n  const sgc=card('Digital Signage \u00b7 OMNI Signage','5 non-interactive broadcast screens \u2014 reach and ad inventory, no transactions','',' ');\n  sgc.appendChild(h(`<div class=\"stat-inline\" style=\"margin-top:2px\"><div class=\"si\"><b>${sg.screens}<\/b><span>screens deployed<\/span><\/div><div class=\"si\"><b>${num(sg.reach)}<\/b><span>reach \/ yr<\/span><\/div><div class=\"si\"><b>${num(sg.impressions)}<\/b><span>Screen Impressions \/ yr<\/span><\/div><div class=\"si\"><b>${SIGNAGE.dwellSec}s<\/b><span>avg attention<\/span><\/div><div class=\"si\"><b>${sg.spotsPerLoop} \/ ${pct(sg.shareOfVoice)}<\/b><span>spots per loop \u00b7 share of voice<\/span><\/div><div class=\"si\"><b>${money2(sg.perScreenMo)}<\/b><span>revenue per screen \/ mo \u00b7 mature fill<\/span><\/div><\/div>`));\n  const fl=h('<div class=\"loclist\" style=\"margin-top:10px\"><\/div>'); const fmax=Math.max(...sg.byFormat.map(x=>x.v));\n  sg.byFormat.slice().sort((a,b)=>b.v-a.v).forEach(x=>fl.appendChild(h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div class=\"lr-name\">${x.name}<\/div><div class=\"lr-val\">${money(x.v)}<div class=\"lr-sub\" style=\"font-weight:600\">${pct(x.share)}<\/div><\/div><div class=\"lr-bar\"><i style=\"width:${(x.v\/fmax*100).toFixed(0)}%;background:linear-gradient(90deg,var(--s-events),var(--s-dining))\"><\/i><\/div><\/div>`)));\n  sgc.appendChild(fl);\n  const scr=h('<div class=\"loclist\" style=\"margin-top:12px\"><\/div>');\n  const withS=LOCS().filter(L=>(L.screens||0)>0), noS=LOCS().filter(L=>!(L.screens||0)); const smax=Math.max(...withS.map(L=>L.exposures));\n  withS.forEach(L=>scr.appendChild(h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div class=\"lr-name\">${L.name}<\/div><div class=\"lr-val\">${L.screens} screen${L.screens>1?'s':''}<div class=\"lr-sub\" style=\"font-weight:600\">${num(L.exposures*SIGNAGE.viewShare)} reach<\/div><\/div><div class=\"lr-bar\"><i style=\"width:${(L.exposures\/smax*100).toFixed(0)}%\"><\/i><\/div><\/div>`)));\n  sgc.appendChild(scr);\n  sgc.appendChild(h(`<div class=\"note\"><b style=\"color:var(--text)\">Signage broadcasts; kiosks transact.<\/b> Screen Impressions above are <i>reach<\/i> \u2014 deliberately excluded from interactions, attributed conversions, direct spend and tax everywhere else on this dashboard, which stay kiosk-derived. ${noS.length?`${noS.map(L=>L.name).join(' and ')} run${noS.length===1?'s':''} a kiosk with no screen, so ${noS.length===1?'its':'their'} ${num(noS.reduce((s,L)=>s+L.exposures,0))} encounters are not in the reach figure above.`:''}<\/div>`));\n  wrap.appendChild(sgc);\n\n  const sf=card('The self-funding math','advertising vs. the Phase-1 build cost','',' ');\n  const p1=phase1Payback(), gp=growthPayback(), bc=buildCost(LOCS());\n  const phaseAd=ad.kiosk*MEDIA.fillByPhase[1]\/MEDIA.fillMature + ad.signage*SIGNAGE.fillByPhase[1]\/SIGNAGE.fillMature;\n  sf.appendChild(h(`<div class=\"stat-inline\" style=\"margin-top:4px\"><div class=\"si\"><b>${money(ad.total)}<\/b><span>Total Advertising Revenue \/ yr \u00b7 mature fill<\/span><\/div><div class=\"si\"><b>${money2(p1.softwareYr)}<\/b><span>annual software<\/span><\/div><div class=\"si\"><b>${money2(p1.capital)}<\/b><span>Phase-1 capital<\/span><\/div><div class=\"si\"><b>${money(p1.net)}<\/b><span>net of software \/ yr<\/span><\/div><div class=\"si\"><b style=\"color:var(--good)\">${p1.years.toFixed(1)} yr<\/b><span>Phase-1 payback \u00b7 mature fill<\/span><\/div><\/div>`));\n  sf.appendChild(h(`<div class=\"note\">Phase-1 capital of ${money2(p1.capital)} covers ${bc.kiosks} kiosks at ${money2(CAPEX_PER_KIOSK)} and ${bc.screens} screens at ${money2(CAPEX_PER_SCREEN)}; software runs ${money2(SW_PER_KIOSK_YR)} per kiosk and ${money2(SW_PER_SCREEN_YR)} per screen per year. At mature fill the advertising layer returns ${money(p1.net)} a year net of software \u2014 repaying the entire build in about <b style=\"color:var(--text)\">${p1.years.toFixed(1)} years<\/b> on that basis, or ${(p1.capital\/(phaseAd-p1.softwareYr)).toFixed(1)} years at the sell-through realistically achievable in Phase 1 \u2014 before a single dollar of attributed visitor spend or public tax is counted. Scaling to 30 kiosks and 20 screens costs a further ${money2(gp.capex)} and adds ${money(gp.gain)} a year net, a ${gp.years.toFixed(1)}-year payback. <b>All modeled.<\/b> The CPM band, the per-screen rate, the fill curve and the hardware pricing all need replacing with the destination's actual rate card and quoted build cost.<\/div>`));\n  wrap.appendChild(sf);\n  return wrap;\n}\n\n\nfunction segDetailRow(k,cl,cm,phaseNote){\n  const r2=h('<div class=\"grid row-2\" style=\"margin-bottom:16px\"><\/div>');\n  const dc=card('District detail','the Phase-1 districts serving this segment','',' ');\n  const dl=h('<div class=\"loclist\"><\/div>'); const dmax=Math.max(...cl.map(L=>deriveMetrics(L,k).total));\n  cl.forEach(L=>{const mm=deriveMetrics(L,k);\n    const row=h(`<div class=\"locrow\"><div><div class=\"lr-name\">${L.name}<\/div><div class=\"lr-sub\">${L.kiosks} kiosks \u00b7 ${L.screens} screen${L.screens===1?'':'s'} \u00b7 ${pct(L.interactionRate,1)} interaction \u00b7 ${money2(L.avgSpend)} \/ visitor<\/div><\/div><div class=\"lr-val\">${money(mm.total)}<div class=\"lr-sub\" style=\"font-weight:600\">${money(mm.tax)} tax<\/div><\/div><span class=\"chev\">\u203a<\/span><div class=\"lr-bar\"><i style=\"width:${(mm.total\/dmax*100).toFixed(0)}%\"><\/i><\/div><\/div>`);\n    row.addEventListener('click',()=>go('#\/location\/'+L.id)); dl.appendChild(row);});\n  dc.appendChild(dl); dc.appendChild(h(`<div class=\"note\">${phaseNote}<\/div>`)); r2.appendChild(dc);\n  const mc=card('Where the visitor dollar lands','partner mix across these districts','',' ');\n  const parts=Object.keys(cm.partners).map(s=>({v:cm.partners[s],name:SEG[s].name,color:SEG[s].color}));\n  const pin=h('<div style=\"display:flex;gap:20px;align-items:center;flex-wrap:wrap\"><\/div>'); pin.appendChild(donut(parts));\n  const pl=h('<div style=\"flex:1;min-width:180px\"><\/div>');\n  parts.slice().sort((a,b)=>b.v-a.v).forEach(p=>pl.appendChild(h(`<div class=\"locrow\" style=\"grid-template-columns:1fr auto\"><div class=\"lr-name\" style=\"display:flex;align-items:center;gap:7px\"><span class=\"sw\" style=\"width:11px;height:11px;border-radius:3px;background:${p.color}\"><\/span>${p.name}<\/div><div class=\"lr-val\">${money(p.v)}<\/div><\/div>`)));\n  pin.appendChild(pl); mc.appendChild(pin);\n  mc.appendChild(h('<div class=\"note\">Direct attributed spend only \u2014 no ripple multiplier is applied anywhere in this model, so these are the dollars a partner could in principle reconcile against their own till.<\/div>'));\n  r2.appendChild(mc);\n  return r2;\n}\n\n\/* ================= CRUISE & PORT EVERGLADES ================= *\/\nfunction segLocs(){return LOCS().filter(L=>SEGDATA.zoneIds.includes(L.id));}\nfunction aiSeg(k){\n  const cm=rollup(k,segLocs()), all=rollup(k);\n  const pt=findLoc('port');\n  const lodging=SEGDATA.tdt\/SEGDATA.tdtRate;\n  const perTraveler=lodging\/SEGDATA.travelers;\n  return `Port Everglades handled <b>${SEGDATA.cruiseGuests.toLocaleString()}<\/b> cruise guests in FY2025, a record, a record for the port. That is a fundamentally different visitor from a beach traveler: they arrive on a fixed schedule, they have a known and often long gap before boarding, and almost none of them have a plan for it. Phase 1 puts <b>${cm.kiosks} kiosks and ${cm.screens} screens<\/b> across the cruise terminals and the beachfront. They are modeled to carry <span class=\"em\">${money(cm.total)}<\/span> of attributed direct spend, <b>${pct(cm.total\/all.total)}<\/b> of the network, on <b>${pct(cm.exposures\/all.exposures)}<\/b> of its traffic. Per-visitor spend at the terminals is deliberately modeled <i>low<\/i> at <b>${money2(pt.avgSpend)}<\/b> \u2014 most of a cruise trip's money is spent at sea, not in Broward \u2014 but the interaction rate is <b>${pct(pt.interactionRate,1)}<\/b>, well above the open beachfront, because a captive audience in a waiting hall is the highest-conversion moment the destination has. Against the destination's own arithmetic \u2014 <b>${money(SEGDATA.tdt)}<\/b> of Tourist Development Tax at ${pct(SEGDATA.tdtRate,1)} implies roughly <b>${money(lodging)}<\/b> of taxable lodging revenue, or about <b>${money2(perTraveler)}<\/b> per traveler \u2014 the cruise segment is the clearest untapped conversion surface in Greater Fort Lauderdale. <b>Read the share honestly:<\/b> the terminals carry ${pct(cm.exposures\/all.exposures)} of the network's traffic but only ${pct(cm.total\/all.total)} of its attributed spend, and that is by design \u2014 the low per-visitor figure is the conservative choice, not a weakness in the placement. If even a modest share of these guests were converted into a pre-cruise night or a shore-side meal the ratio would invert, which is precisely the experiment two terminal kiosks are for.`;\n}\nfunction pageSeg(){\n  const k=K(); const cl=segLocs(); const cm=rollup(k,cl); const all=rollup(k);\n  const lodging=SEGDATA.tdt\/SEGDATA.tdtRate;\n  const perTraveler=lodging\/SEGDATA.travelers;\n  const perConvNight=SEGDATA.conventionImpact\/SEGDATA.conventionRoomNights;\n  const wrap=h('<div class=\"fade\"><\/div>');\n  wrap.appendChild(h(`<a class=\"backbtn\" onclick=\"go('#\/overview')\"><svg viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M15 18l-6-6 6-6\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg> ${ACT().region} overview<\/a>`));\n  wrap.appendChild(h(`<div class=\"crumbs\"><span>${ACT().name}<\/span><span class=\"sep\">\u203a<\/span><a onclick=\"go('#\/overview')\">${ACT().region}<\/a><span class=\"sep\">\u203a<\/span><span>Cruise &amp; Port<\/span><\/div>`));\n  wrap.appendChild(h(`<div class=\"pagehead\"><div><div class=\"eyebrow\">Cruise \u00b7 Port Everglades &amp; the beachfront<\/div><h1>4.7 million people with three hours to fill<\/h1><div class=\"ch-sub\" style=\"margin-top:4px;color:var(--text-2)\">${cm.kiosks} kiosks \u00b7 ${cm.screens} screens \u00b7 ${cl.length} districts \u00b7 ${periodLabel()}<\/div><\/div><div class=\"meta\">Share of network impact<br><b style=\"color:var(--text);font-size:14px\">${pct(cm.total\/all.total)}<\/b><\/div><\/div>`));\n  wrap.appendChild(h(aiBanner('TrueImpact AI \u00b7 Cruise read', aiSeg(k))));\n\n  const cited=card('Visit Lauderdale \u00b7 2025','cited, plus three figures derived from those numbers by arithmetic','',' ');\n  const cs=h('<div class=\"stat-inline\" style=\"margin-top:2px\"><\/div>');\n  [[SEGDATA.cruiseGuests.toLocaleString(),'Port Everglades cruise guests, FY2025 record (cited)'],\n   [SEGDATA.travelers.toLocaleString()+'+','Travelers into the destination (cited)'],\n   [money(SEGDATA.tdt),'Tourist Development Tax collected (cited)'],\n   [String(SEGDATA.conventions),'Conventions booked (cited)'],\n   [SEGDATA.conventionRoomNights.toLocaleString(),'Convention room nights (cited)'],\n   [money(SEGDATA.conventionImpact),'Convention economic impact (cited)'],\n   ['~'+money(lodging),'Taxable lodging revenue (derived: $124M \u00f7 6%)'],\n   ['~'+money2(perTraveler),'Lodging spend per traveler (derived)'],\n   ['~'+money2(perConvNight),'Impact per convention room night (derived)']]\n   .forEach(x=>cs.appendChild(h(`<div class=\"si\"><b>${x[0]}<\/b><span>${x[1]}<\/span><\/div>`)));\n  cited.appendChild(cs);\n  cited.appendChild(h(`<div class=\"note\"><b style=\"color:var(--text)\">Terminology matters here.<\/b> Visit Lauderdale describes 20.9M as <i>travelers into the destination<\/i> \u2014 not overnight visitors and not unique visitors \u2014 and that wording is preserved throughout this dashboard. The three derived figures are arithmetic on the cited ones and are labelled as derived. Note what is <b style=\"color:var(--text)\">missing<\/b>: Visit Lauderdale publishes no citywide visitor-spending or total-economic-impact figure, so unlike the other TrueImpact markets there is no per-traveler spend anchor. Per-district spend on this dashboard is modeled from placement type and disclosed as such.<\/div>`));\n  cited.style.marginBottom='16px'; wrap.appendChild(cited);\n\n  const kpis=h('<div class=\"grid kpis\" style=\"margin-bottom:16px\"><\/div>');\n  kpis.appendChild(h(`<div class=\"card kpi hero\"><div class=\"glow\"><\/div><div class=\"k-label\">Attributed direct spend \u00b7 cruise &amp; beachfront<\/div><div class=\"k-val\"><span class=\"cur\">${curSym()}<\/span>${money(cm.total).replace(curSym(),'')}<\/div><div class=\"k-delta\" style=\"color:var(--muted)\">${pct(cm.total\/all.total)} of network impact on ${pct(cm.exposures\/all.exposures)} of its traffic \u2014 deliberately below par, see below<\/div><div class=\"k-foot\">Direct spend only \u2014 no ripple multiplier applied<\/div><\/div>`));\n  kpis.appendChild(kpiTile('Public tax',money(cm.tax),'',`Broward County ${money(cm.taxLocal)} \u00b7 State ${money(cm.taxState)}`,()=>go('#\/tax')));\n  kpis.appendChild(kpiTile('Tourist Development Tax',money(cm.dmoFund),'','100% local \u2014 funds Visit Lauderdale',()=>go('#\/tax')));\n  kpis.appendChild(kpiTile('Projected kiosk sessions',num(cm.interactions),'',`${num(cm.conversions)} attributed conversions`));\n  wrap.appendChild(kpis);\n  wrap.appendChild(segDetailRow(k,cl,cm,'Phase 2 adds <b style=\"color:var(--text)\">Fort Lauderdale\u2013Hollywood International Airport<\/b>, which feeds this port directly; Phase 3 doubles the terminal presence. Both are modeled scenarios, not commitments.'));\n  wrap.appendChild(card('Cruise &amp; beachfront impact through the year','monthly \u00b7 these districts',[areaChart(rollupSeries(k,cl),MONTHS,{h:210}),h('<div class=\"note\">Cruise and beach seasonality run together in Greater Fort Lauderdale \u2014 a hard winter and early-spring peak, and a genuine late-summer trough. The shoulder is where a kiosk network earns its keep, because that is when the destination has to work hardest for each visitor.<\/div>')]));\n  return wrap;\n}\n\n\/* ================= ROUTER ================= *\/\nconst state={loc:null};\nfunction go(hash){location.hash=hash;}\nfunction render(){\n  buildDestSel();\n  const hash=location.hash||'#\/overview';\n  const parts=hash.replace('#\/','').split('\/');\n  if(activeDest==='network'){\n    app.innerHTML='';\n    if(parts[0]==='modules'){engMode='modules';app.appendChild(pageEngagement());}\n    else if(parts[0]==='campaigns'){engMode='campaigns';app.appendChild(pageEngagement());}\n    else app.appendChild(pageNetwork());\n    window.scrollTo({top:0,behavior:'instant'});return;\n  }\n  state.loc = parts[0]==='location'?parts[1]:null;\n  app.innerHTML='';\n  let node;\n  if(parts[0]==='location') node=pageLocation(parts[1]);\n  else if(parts[0]==='partners') node=pagePartners();\n  else if(parts[0]==='tax') node=pageTax();\n  else if(parts[0]==='modules'){engMode='modules';node=pageEngagement();}\n  else if(parts[0]==='campaigns'){engMode='campaigns';node=pageEngagement();}\n  else if(parts[0]==='media') node=pageMedia();\n  else if(parts[0]==='cruise') node=pageSeg();\n  else node=pageOverview();\n  app.appendChild(node);\n  window.scrollTo({top:0,behavior:'instant'});\n}\nwindow.addEventListener('hashchange',render);\n\n\/\/ destination selector (rebuilt each render to reflect active)\nfunction buildDestSel(){\n  const host=$('#destSel');if(!host)return;\n  const opts=[...Object.keys(DESTINATIONS).map(id=>[id,DESTINATIONS[id].name])];\n  host.innerHTML=opts.map(([id,label])=>`<option value=\"${id}\"${id===activeDest?' selected':''}>${label}<\/option>`).join('');\n}\n\n\/\/ period selector\n$('#periodSel').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return;\n  [...$('#periodSel').children].forEach(x=>x.classList.remove('on'));b.classList.add('on');PERIOD=b.dataset.p;render();});\n\/\/ currency toggle removed \u2014 US destination, USD only.\n\/\/ destination selector\n$('#destSel').addEventListener('change',e=>setDest(e.target.value));\n\/\/ theme\n$('#themeBtn').addEventListener('click',()=>{const cur=document.documentElement.getAttribute('data-theme');document.documentElement.setAttribute('data-theme',cur==='dark'?'light':'dark');render();});\nwindow.addEventListener('mousemove',e=>{if(TT.style.opacity==1)moveTT(e);});\n\nrender();\n<\/script>\n<\/body>\n<\/html>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Visit Lauderdale<\/p>","protected":false},"author":11,"featured_media":57099,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"_acf_changed":false,"inline_featured_image":false,"postBodyCss":"","postBodyMargin":[],"postBodyPadding":[],"postBodyBackground":{"backgroundType":"classic","gradient":""},"_thinkrank_schema_form_data":"","_thinkrank_selected_schema_type":"","_thinkrank_additional_schemas":"","_thinkrank_canonical_url":"","_thinkrank_og_title":"","_thinkrank_og_description":"","_thinkrank_og_image":"","_thinkrank_twitter_title":"","_thinkrank_twitter_description":"","_thinkrank_twitter_image":"","_thinkrank_imported_from":"","_thinkrank_focus_keywords":[],"_thinkrank_focus_keyword":"","_thinkrank_robots_meta_enabled":0,"_thinkrank_robots_meta":"","_thinkrank_advanced_robots_meta":"","_thinkrank_exclude_from_search":0,"_thinkrank_exclude_from_archives":0,"_thinkrank_primary_category":0,"footnotes":""},"class_list":["post-58254","page","type-page","status-publish","has-post-thumbnail","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.9 (Yoast SEO v28.7) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>VisitLauderdale - TrueOmni<\/title>\n<meta name=\"description\" content=\"Visit Lauderdale\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/trueomni.com\/spanish\/visit-lauderdale\/\" \/>\n<meta property=\"og:locale\" content=\"es_MX\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"VisitLauderdale\" \/>\n<meta property=\"og:description\" content=\"Visit Lauderdale\" \/>\n<meta property=\"og:url\" content=\"https:\/\/trueomni.com\/spanish\/visit-lauderdale\/\" \/>\n<meta property=\"og:site_name\" content=\"TrueOmni\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/trueomni\/\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-17T22:22:08+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/trueomni.com\/wp-content\/uploads\/2026\/05\/Travel-TrueOmni-TNT-IVE-Kiosks-2026-Website-1024x753.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"753\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Tiempo de lectura\" \/>\n\t<meta name=\"twitter:data1\" content=\"6 minutos\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/trueomni.com\\\/visit-lauderdale\\\/\",\"url\":\"https:\\\/\\\/trueomni.com\\\/visit-lauderdale\\\/\",\"name\":\"VisitLauderdale - TrueOmni\",\"isPartOf\":{\"@id\":\"http:\\\/\\\/trueomni.com\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/trueomni.com\\\/visit-lauderdale\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/trueomni.com\\\/visit-lauderdale\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/trueomni.com\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/Travel-TrueOmni-TNT-IVE-Kiosks-2026-Website-scaled.png\",\"datePublished\":\"2026-08-17T22:21:38+00:00\",\"dateModified\":\"2026-08-17T22:22:08+00:00\",\"description\":\"Visit Lauderdale\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/trueomni.com\\\/visit-lauderdale\\\/#breadcrumb\"},\"inLanguage\":\"es\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/trueomni.com\\\/visit-lauderdale\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"https:\\\/\\\/trueomni.com\\\/visit-lauderdale\\\/#primaryimage\",\"url\":\"https:\\\/\\\/trueomni.com\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/Travel-TrueOmni-TNT-IVE-Kiosks-2026-Website-scaled.png\",\"contentUrl\":\"https:\\\/\\\/trueomni.com\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/Travel-TrueOmni-TNT-IVE-Kiosks-2026-Website-scaled.png\",\"width\":2560,\"height\":1883,\"caption\":\"TrueOmni interactive kiosks in a hotel lobby\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/trueomni.com\\\/visit-lauderdale\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"http:\\\/\\\/trueomni.com\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"VisitLauderdale\"}]},{\"@type\":\"WebSite\",\"@id\":\"http:\\\/\\\/trueomni.com\\\/#website\",\"url\":\"http:\\\/\\\/trueomni.com\\\/\",\"name\":\"TrueOmni\",\"description\":\"TrueOmni Self-Service Digital Customer Experience Company\",\"publisher\":{\"@id\":\"http:\\\/\\\/trueomni.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"http:\\\/\\\/trueomni.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"es\"},{\"@type\":\"Organization\",\"@id\":\"http:\\\/\\\/trueomni.com\\\/#organization\",\"name\":\"TRUE OMNI LLC\",\"alternateName\":\"TRUE OMNI LLC\",\"url\":\"http:\\\/\\\/trueomni.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"es\",\"@id\":\"http:\\\/\\\/trueomni.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"http:\\\/\\\/trueomni.com\\\/wp-content\\\/uploads\\\/2024\\\/06\\\/TrueOmni-Logo-2.png\",\"contentUrl\":\"http:\\\/\\\/trueomni.com\\\/wp-content\\\/uploads\\\/2024\\\/06\\\/TrueOmni-Logo-2.png\",\"width\":1000,\"height\":230,\"caption\":\"TRUE OMNI LLC\"},\"image\":{\"@id\":\"http:\\\/\\\/trueomni.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.facebook.com\\\/trueomni\\\/\"],\"description\":\"TrueOmni is the leader in digital self-service solutions, offering innovative kiosks, digital signage, and mobile marketing platforms. Enhance customer engagement and streamline operations with our cutting-edge technology tailored for the hospitality, retail, and tourism industries.\",\"email\":\"info@trueomni.com\",\"telephone\":\"8333006664\",\"legalName\":\"True Omni LLC\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"VisitLauderdale - TrueOmni","description":"Visit Lauderdale","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/trueomni.com\/spanish\/visit-lauderdale\/","og_locale":"es_MX","og_type":"article","og_title":"VisitLauderdale","og_description":"Visit Lauderdale","og_url":"https:\/\/trueomni.com\/spanish\/visit-lauderdale\/","og_site_name":"TrueOmni","article_publisher":"https:\/\/www.facebook.com\/trueomni\/","article_modified_time":"2026-08-17T22:22:08+00:00","og_image":[{"width":1024,"height":753,"url":"https:\/\/trueomni.com\/wp-content\/uploads\/2026\/05\/Travel-TrueOmni-TNT-IVE-Kiosks-2026-Website-1024x753.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_misc":{"Tiempo de lectura":"6 minutos"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/trueomni.com\/visit-lauderdale\/","url":"https:\/\/trueomni.com\/visit-lauderdale\/","name":"VisitLauderdale - TrueOmni","isPartOf":{"@id":"http:\/\/trueomni.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/trueomni.com\/visit-lauderdale\/#primaryimage"},"image":{"@id":"https:\/\/trueomni.com\/visit-lauderdale\/#primaryimage"},"thumbnailUrl":"https:\/\/trueomni.com\/wp-content\/uploads\/2026\/05\/Travel-TrueOmni-TNT-IVE-Kiosks-2026-Website-scaled.png","datePublished":"2026-08-17T22:21:38+00:00","dateModified":"2026-08-17T22:22:08+00:00","description":"Visit Lauderdale","breadcrumb":{"@id":"https:\/\/trueomni.com\/visit-lauderdale\/#breadcrumb"},"inLanguage":"es","potentialAction":[{"@type":"ReadAction","target":["https:\/\/trueomni.com\/visit-lauderdale\/"]}]},{"@type":"ImageObject","inLanguage":"es","@id":"https:\/\/trueomni.com\/visit-lauderdale\/#primaryimage","url":"https:\/\/trueomni.com\/wp-content\/uploads\/2026\/05\/Travel-TrueOmni-TNT-IVE-Kiosks-2026-Website-scaled.png","contentUrl":"https:\/\/trueomni.com\/wp-content\/uploads\/2026\/05\/Travel-TrueOmni-TNT-IVE-Kiosks-2026-Website-scaled.png","width":2560,"height":1883,"caption":"TrueOmni interactive kiosks in a hotel lobby"},{"@type":"BreadcrumbList","@id":"https:\/\/trueomni.com\/visit-lauderdale\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"http:\/\/trueomni.com\/"},{"@type":"ListItem","position":2,"name":"VisitLauderdale"}]},{"@type":"WebSite","@id":"http:\/\/trueomni.com\/#website","url":"http:\/\/trueomni.com\/","name":"VerdaderoOmni","description":"TrueOmni es una empresa de experiencia digital de autoservicio para el cliente.","publisher":{"@id":"http:\/\/trueomni.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"http:\/\/trueomni.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"es"},{"@type":"Organization","@id":"http:\/\/trueomni.com\/#organization","name":"TRUE OMNI LLC","alternateName":"TRUE OMNI LLC","url":"http:\/\/trueomni.com\/","logo":{"@type":"ImageObject","inLanguage":"es","@id":"http:\/\/trueomni.com\/#\/schema\/logo\/image\/","url":"http:\/\/trueomni.com\/wp-content\/uploads\/2024\/06\/TrueOmni-Logo-2.png","contentUrl":"http:\/\/trueomni.com\/wp-content\/uploads\/2024\/06\/TrueOmni-Logo-2.png","width":1000,"height":230,"caption":"TRUE OMNI LLC"},"image":{"@id":"http:\/\/trueomni.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/trueomni\/"],"description":"TrueOmni is the leader in digital self-service solutions, offering innovative kiosks, digital signage, and mobile marketing platforms. Enhance customer engagement and streamline operations with our cutting-edge technology tailored for the hospitality, retail, and tourism industries.","email":"info@trueomni.com","telephone":"8333006664","legalName":"True Omni LLC"}]}},"_links":{"self":[{"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/pages\/58254","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/users\/11"}],"replies":[{"embeddable":true,"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/comments?post=58254"}],"version-history":[{"count":4,"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/pages\/58254\/revisions"}],"predecessor-version":[{"id":58265,"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/pages\/58254\/revisions\/58265"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/media\/57099"}],"wp:attachment":[{"href":"https:\/\/trueomni.com\/spanish\/wp-json\/wp\/v2\/media?parent=58254"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}