"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function safeHref(h) {
h = String(h || "#").trim();
return /^(https?:|mailto:|tel:|#|\/|\?)/i.test(h) ? h : "#";
}
function fillVars(text, vars) {
var out = esc(text);
if (!vars.name) out = out.replace(/,?\s*\{name\}/g, "");
return out.replace(/\{(\w+)\}/g, function (m, k) { return vars[k] ? esc(vars[k]) : "—"; });
}
function scrollToEl(el) {
if (el) el.scrollIntoView({ behavior: reduceMotion ? "auto" : "smooth", block: "start" });
}
$$('[data-contact="phone"]').forEach(function (a) { a.textContent = CONFIG.contact.phone; a.setAttribute("href", CONFIG.contact.phoneHref); });
$$('[data-contact="email"]').forEach(function (a) { a.textContent = CONFIG.contact.email; a.setAttribute("href", "mailto:" + CONFIG.contact.email); });
$$('[data-contact="hours"]').forEach(function (s) { s.textContent = CONFIG.contact.hours; });
var nav = $("#mft-nav");
var menuBtn = $(".mft-menu-btn");
function closeMenu() { nav.classList.remove("is-open"); menuBtn.setAttribute("aria-expanded", "false"); }
menuBtn.addEventListener("click", function () {
var open = !nav.classList.contains("is-open");
nav.classList.toggle("is-open", open);
menuBtn.setAttribute("aria-expanded", String(open));
});
root.addEventListener("click", function (e) {
var enroll = e.target.closest("[data-enroll]");
if (enroll && root.contains(enroll)) {
var key = enroll.getAttribute("data-enroll");
var url = CONFIG.checkoutUrls[key];
if (url) return; // href already points at checkout
e.preventDefault();
if (CONFIG.confirmations[key]) selectTab(key);
scrollToEl(document.getElementById("mft-after"));
return;
}
var a = e.target.closest('a[href^="#mft-"]');
if (a && root.contains(a)) {
var target = document.getElementById(a.getAttribute("href").slice(1));
if (target) { e.preventDefault(); closeMenu(); scrollToEl(target); }
}
});
$$("[data-enroll]").forEach(function (a) {
var url = CONFIG.checkoutUrls[a.getAttribute("data-enroll")];
if (url) a.setAttribute("href", safeHref(url));
});
var posture = $("#mft-posture");
var poseTouched = false;
function setPose(pose) {
posture.setAttribute("data-pose", pose);
$$(".mft-toggle__btn", posture).forEach(function (b) { b.setAttribute("aria-pressed", String(b.getAttribute("data-pose") === pose)); });
$$("[data-up]", posture).forEach(function (el) { el.textContent = el.getAttribute(pose === "up" ? "data-up" : "data-low"); });
}
$$(".mft-toggle__btn", posture).forEach(function (b) {
b.addEventListener("click", function () { poseTouched = true; setPose(b.getAttribute("data-pose")); });
});
// One orchestrated moment: shift from "low" to "on the palate" shortly after the diagram is seen.
if (!reduceMotion && "IntersectionObserver" in window) {
var io = new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) {
io.disconnect();
setTimeout(function () { if (!poseTouched) setPose("up"); }, 1400);
}
}, { threshold: 0.5 });
io.observe(posture);
} else {
setPose("up");
}
var player = $("#mft-player");
var media = $(".mft-player__media", player);
var notice = $(".mft-player__notice", player);
function playAt(sec) {
var url = CONFIG.videoUrl || "";
if (!url || /YOUR_VIDEO_ID/.test(url)) { notice.hidden = false; return; }
var el;
if (/\.(mp4|webm|ogg)(\?|#|$)/i.test(url)) {
el = document.createElement("video");
el.src = url + "#t=" + sec;
el.controls = true; el.autoplay = true; el.playsInline = true;
} else {
var sep = url.indexOf("?") > -1 ? "&" : "?";
el = document.createElement("iframe");
el.src = /vimeo\.com/.test(url)
? url + sep + "autoplay=1#t=" + sec + "s"
: url + sep + "autoplay=1&rel=0&start=" + sec;
el.title = "Treatment options video";
el.allow = "autoplay; encrypted-media; picture-in-picture; fullscreen";
el.allowFullscreen = true;
}
media.innerHTML = "";
media.appendChild(el);
player.classList.add("is-playing");
}
$(".mft-player__poster", player).addEventListener("click", function () { playAt(0); });
$$(".mft-chapter").forEach(function (btn) {
btn.addEventListener("click", function () {
$$(".mft-chapter").forEach(function (b) { b.removeAttribute("aria-current"); });
btn.setAttribute("aria-current", "true");
playAt(parseInt(btn.getAttribute("data-start"), 10) || 0);
if (window.innerWidth < 1080) scrollToEl(player);
});
});
var form = $("#mft-wizard");
var steps = $$(".mft-step", form);
var backBtn = $("[data-back-step]", form);
var nextBtn = $("[data-next-step]", form);
var result = $("#mft-result");
var current = 0;
function showError(step, msg) {
var p = $(".mft-error", step);
if (!p) return;
p.textContent = msg || "";
p.hidden = !msg;
}
function showStep(i, focus) {
current = i;
steps.forEach(function (s, idx) { s.hidden = idx !== i; });
$("#mft-step-count").textContent = "Step " + (i + 1) + " of " + steps.length;
$("#mft-step-name").textContent = steps[i].getAttribute("data-name");
$("#mft-step-bar").style.width = ((i + 1) / steps.length * 100) + "%";
backBtn.hidden = i === 0;
nextBtn.textContent = i === 0 ? "Continue" : i === steps.length - 1 ? "See my results" : "Continue";
if (i === 3) nextBtn.textContent = hasPhotos() ? "Continue" : "Skip photos";
if (focus) {
var t = $(".mft-step__title", steps[i]);
if (t) t.focus({ preventScroll: true });
var top = form.getBoundingClientRect().top;
if (top < 0) scrollToEl(form);
}
}
function checked(name) { return form.querySelector('input[name="' + name + '"]:checked'); }
function validate(i) {
var step = steps[i], msg = "";
if (i === 0 && !checked("age")) msg = "Choose who this assessment is for to continue.";
if (i === 1 && !checked("signs")) msg = "Select at least one option, or choose \u201cNone of these\u201d.";
if (i === 2 && !(checked("rest") && checked("lift") && checked("nasal"))) msg = "Answer all three quick checks to continue.";
if (i === 4) {
var name = $("#mft-f-name"), email = $("#mft-f-email");
name.setAttribute("aria-invalid", String(!name.value.trim()));
var okEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value.trim());
email.setAttribute("aria-invalid", String(!okEmail));
if (!name.value.trim()) msg = "Enter your name.";
else if (!okEmail) msg = "Enter a valid email address, like
[email protected] .";
else if (!form.querySelector('input[name="consent"]').checked) msg = "Tick the box to share your answers with our care team.";
}
showError(step, msg);
return !msg;
}
// "None of these" clears the other signs and vice versa
$$('input[name="signs"]', form).forEach(function (cb) {
cb.addEventListener("change", function () {
if (!cb.checked) return;
$$('input[name="signs"]', form).forEach(function (o) {
if (o !== cb && (cb.value === "none" || o.value === "none")) o.checked = false;
});
showError(steps[1], "");
});
});
form.addEventListener("change", function (e) {
if (e.target.name === "age" || e.target.name === "concern") showError(steps[0], "");
});
backBtn.addEventListener("click", function () { if (current > 0) showStep(current - 1, true); });
nextBtn.addEventListener("click", function () {
if (!validate(current)) return;
if (current < steps.length - 1) showStep(current + 1, true);
else submitAssessment();
});
form.addEventListener("submit", function (e) { e.preventDefault(); nextBtn.click(); });
form.addEventListener("keydown", function (e) {
if (e.key === "Enter" && e.target.tagName === "INPUT" && e.target.type !== "checkbox" && e.target.type !== "radio") {
e.preventDefault(); nextBtn.click();
}
});
var MAX_BYTES = 10 * 1024 * 1024;
function hasPhotos() { return $$(".mft-photo input[type=file]", form).some(function (i) { return i.files && i.files.length; }); }
$$(".mft-photo", form).forEach(function (slot) {
var input = $("input[type=file]", slot);
var drop = $(".mft-photo__drop", slot);
var prev = $(".mft-photo__preview", slot);
var img = $("img", prev);
var fallback = $(".mft-photo__fallback", prev);
var err = $(".mft-photo__error", slot);
var objectUrl = null;
function clear() {
if (objectUrl) URL.revokeObjectURL(objectUrl);
objectUrl = null; input.value = ""; img.removeAttribute("src");
prev.hidden = true; drop.hidden = false;
if (current === 3) showStep(3);
}
function setFile(file) {
err.hidden = true;
if (!file) return clear();
var isImage = /^image\//.test(file.type) || /\.(heic|heif)$/i.test(file.name);
if (!isImage) { err.textContent = "That file isn't an image. Choose a JPG, PNG or HEIC photo."; err.hidden = false; input.value = ""; return; }
if (file.size > MAX_BYTES) { err.textContent = "That photo is over 10 MB. Choose a smaller one or take a new photo."; err.hidden = false; input.value = ""; return; }
if (objectUrl) URL.revokeObjectURL(objectUrl);
objectUrl = URL.createObjectURL(file);
fallback.hidden = true; img.hidden = false;
img.onerror = function () { img.hidden = true; fallback.textContent = file.name + " added. Preview isn't available for this format."; fallback.hidden = false; };
img.src = objectUrl;
prev.hidden = false; drop.hidden = true;
if (current === 3) showStep(3);
}
input.addEventListener("change", function () { setFile(input.files && input.files[0]); });
["dragenter", "dragover"].forEach(function (ev) {
drop.addEventListener(ev, function (e) { e.preventDefault(); slot.classList.add("is-drag"); });
});
["dragleave", "drop"].forEach(function (ev) {
drop.addEventListener(ev, function (e) { e.preventDefault(); slot.classList.remove("is-drag"); });
});
drop.addEventListener("drop", function (e) {
var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0];
if (!f) return;
try { var dt = new DataTransfer(); dt.items.add(f); input.files = dt.files; } catch (_) {}
setFile(f);
});
$(".mft-photo__remove", slot).addEventListener("click", function () { clear(); drop.focus && input.focus(); });
});
var PROGRAMS = {
"kids-myo": { name: "Kids Myo Foundations", text: "16 weeks of short, game-based exercises for ages 4 to 12, starting with a full evaluation.", cta: "Enroll in Kids Myo" },
"adult-myo": { name: "Adult Myofunctional Therapy", text: "12 weeks of guided sessions and daily exercises for teens and adults, starting with a full evaluation.", cta: "Enroll in Adult Myo" },
"screening": { name: "Airway and sleep screening", text: "A 30-minute visit to look at breathing, tongue posture and sleep, with a clear recommendation at the end.", cta: "Book a screening" }
};
function computeResult() {
var age = (checked("age") || {}).value || "adult";
var signs = $$('input[name="signs"]:checked', form).filter(function (c) { return c.value !== "none"; });
var score = 0;
signs.forEach(function (c) { score += parseInt(c.getAttribute("data-weight"), 10) || 0; });
["rest", "lift", "nasal"].forEach(function (n) { var c = checked(n); if (c) score += parseInt(c.getAttribute("data-weight"), 10) || 0; });
var tier = score >= 9 ? "high" : score >= 4 ? "mid" : "low";
var programKey = age === "child" ? "kids-myo" : "adult-myo";
return {
score: score,
tier: tier,
who: age,
signs: signs.map(function (c) { return c.closest("label").textContent.trim(); }),
tie: (checked("lift") || {}).value === "Can't reach",
recKey: tier === "low" ? "screening" : programKey,
programKey: programKey
};
}
function renderResult(r, sent) {
var copy = {
low: { title: "Few signs right now", text: "Nothing you've shared points strongly to a myofunctional problem. If something still feels off, a 30-minute airway screening is a quick way to check." },
mid: { title: "Some signs worth a closer look", text: "Several of your answers are common in people with low tongue posture or mouth breathing. An evaluation will show whether therapy would help, and most people at this level start there." },
high: { title: "Several signs of a myofunctional pattern", text: "Your answers match the patterns our therapists treat most often. We recommend starting the program below, which begins with a full evaluation." }
}[r.tier];
var subject = r.who === "child" ? "your child's" : "your";
var pct = Math.min(r.score / 14, 1) * 100;
var rec = PROGRAMS[r.recKey];
var html = '
Your suggested next step
' +
'
' + esc(copy.title) + ' ' +
'
' +
'
Few signs Some signs Many signs
' +
'
' + esc(copy.text) + '
';
if (r.signs.length) {
html += '
' + r.signs.map(function (s) { return '' + esc(s) + ' '; }).join("") + '
';
}
if (r.tie) {
html += '
You said the tongue tip won\'t reach the roof of the mouth when it\'s open wide. That can be a sign of a tongue-tie. We\'ll check ' + subject + ' tongue mobility at the evaluation and talk through Frenectomy Prep and Aftercare if it\'s needed.
';
}
if (sent) {
html += '
Your answers' + (hasPhotos() ? ' and photos were' : ' were') + ' sent to our care team. Expect a reply within one business day.
';
}
html += '
Recommended for ' + (r.who === "child" ? "your child" : "you") + '
' +
'
' + esc(rec.name) + '
' + esc(rec.text) + '
' +
'
' +
'';
result.innerHTML = html;
form.hidden = true;
result.hidden = false;
result.classList.remove("is-in"); void result.offsetWidth; result.classList.add("is-in");
result.focus({ preventScroll: true });
if (result.getBoundingClientRect().top < 0) scrollToEl(result);
var dot = $(".mft-meter__dot", result);
requestAnimationFrame(function () { requestAnimationFrame(function () { dot.style.left = pct + "%"; }); });
$("[data-restart]", result).addEventListener("click", resetAssessment);
}
function submitAssessment() {
var r = computeResult();
var endpoint = CONFIG.assessmentEndpoint;
if (!endpoint) { renderResult(r, false); return; }
var fd = new FormData(form);
fd.append("score", r.score);
fd.append("suggested_next_step", PROGRAMS[r.recKey].name);
nextBtn.disabled = true; nextBtn.textContent = "Sending…";
fetch(endpoint, { method: "POST", body: fd, headers: { Accept: "application/json" } })
.then(function (res) { if (!res.ok) throw new Error(res.status); renderResult(r, true); })
.catch(function () {
showError(steps[4], "Your answers weren't sent. Check your connection and try again, or call us on " + CONFIG.contact.phone + ".");
})
.then(function () { nextBtn.disabled = false; nextBtn.textContent = "See my results"; });
}
function resetAssessment() {
form.reset();
$$(".mft-photo__remove", form).forEach(function (b) { b.click(); });
$$('[aria-invalid]', form).forEach(function (el) { el.removeAttribute("aria-invalid"); });
steps.forEach(function (s) { showError(s, ""); });
result.hidden = true; result.innerHTML = "";
form.hidden = false;
showStep(0, true);
}
showStep(0);
function renderConfirm(container, key, vars, idPrefix, persist) {
var c = CONFIG.confirmations[key];
if (!c) return;
var steps = c.steps || [];
var storeKey = "mft-confirm-" + key + "-" + (vars.order || "");
var saved = [];
if (persist) { try { saved = JSON.parse(localStorage.getItem(storeKey) || "[]"); } catch (_) { saved = []; } }
var html = '
' +
'
' + ICON_CHECK + ' ' +
'
' + esc(c.badge) + ' ' +
'
' + fillVars(c.headline, vars) + ' ' +
'
' + fillVars(c.intro, vars) + '
' +
'
' +
'
Your next steps ' +
'
' +
'
';
steps.forEach(function (s, i) {
var id = idPrefix + "-" + key + "-" + i;
var done = saved.indexOf(i) > -1;
html += '' +
'' + ICON_CHECK + '
' +
'
' + (i + 1) + '. ' + fillVars(s.title, vars) + ' ' +
(s.when ? '' + esc(s.when) + ' ' : '') + '' +
'
' + fillVars(s.text, vars) + '
' +
(s.action ? '
' : '') +
'
';
});
html += ' ';
container.innerHTML = html;
var countEl = $(".mft-confirm__count", container);
var fillEl = $(".mft-bar__fill", container);
function update() {
var boxes = $$(".mft-cstep__tick input", container);
var done = boxes.filter(function (b) { return b.checked; });
countEl.textContent = done.length + " of " + boxes.length + " done";
fillEl.style.width = (boxes.length ? done.length / boxes.length * 100 : 0) + "%";
if (persist) { try { localStorage.setItem(storeKey, JSON.stringify(done.map(function (b) { return +b.getAttribute("data-i"); }))); } catch (_) {} }
}
$$(".mft-cstep__tick input", container).forEach(function (cb) {
cb.addEventListener("change", function () { cb.closest(".mft-cstep").classList.toggle("is-done", cb.checked); update(); });
});
$("[data-print]", container).addEventListener("click", function () { window.print(); });
update();
}
// Preview tabs on the landing page
var tablist = $("#mft-tablist");
var keys = Object.keys(CONFIG.confirmations);
keys.forEach(function (key, i) {
var b = document.createElement("button");
b.type = "button";
b.className = "mft-tab";
b.setAttribute("role", "tab");
b.setAttribute("data-key", key);
b.setAttribute("aria-controls", "mft-tabpanel");
b.id = "mft-tab-" + key;
b.textContent = CONFIG.confirmations[key].tab || key;
b.addEventListener("click", function () { selectTab(key); });
tablist.appendChild(b);
});
tablist.addEventListener("keydown", function (e) {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
var tabs = $$(".mft-tab", tablist);
var idx = tabs.indexOf(document.activeElement);
if (idx < 0) return;
var next = tabs[(idx + (e.key === "ArrowRight" ? 1 : tabs.length - 1)) % tabs.length];
selectTab(next.getAttribute("data-key"));
next.focus();
});
function selectTab(key) {
$$(".mft-tab", tablist).forEach(function (t) {
var on = t.getAttribute("data-key") === key;
t.setAttribute("aria-selected", String(on));
t.tabIndex = on ? 0 : -1;
});
$("#mft-tabpanel").setAttribute("aria-labelledby", "mft-tab-" + key);
$("#mft-preview-url").textContent = "yourpractice.com/welcome?confirmation=" + key;
renderConfirm($("#mft-preview"), key, CONFIG.previewVars, "mft-pv", false);
}
if (keys.length) selectTab(keys[0] === "adult-myo" ? "adult-myo" : keys[0]);
// Full confirmation page mode: ?confirmation=
&name=&order=
var params;
try { params = new URLSearchParams(window.location.search); } catch (_) { params = null; }
var confirmKey = params && params.get("confirmation");
if (confirmKey && CONFIG.confirmations[confirmKey]) {
root.classList.add("mft-mode-confirm");
$("#mft-confirm-page").hidden = false;
renderConfirm($("#mft-confirm-full"), confirmKey, {
name: (params.get("name") || "").slice(0, 40),
order: (params.get("order") || "").slice(0, 40)
}, "mft-full", true);
var back = $("[data-back]");
back.setAttribute("href", window.location.pathname);
}
})();