
PianoMode
Always the Right Time to Play
New Lesson
Diatonic Triads in a Major Key
Next Lesson →
') >= 0 ? html.replace('', _ts + '') : (html + _ts); } } catch (e) {} booted.push(f); f.addEventListener('load', function () { reveal(f); }, { once: true }); try { var blob = new Blob([html], { type: 'text/html' }); var url = URL.createObjectURL(blob); f.addEventListener('load', function () { setTimeout(function () { try { URL.revokeObjectURL(url); } catch (e) {} }, 5000); }, { once: true }); f.src = url; } catch (e) { // Older engines without blob-URL iframe support: srcdoc fallback. f.srcdoc = html; } }// Sequential, viewport-gated: one frame parses the engine before the // next starts so several embeds never thrash the main thread at once. var queue = [], working = false; function pump() { if (working) return; var f = queue.shift(); if (!f) return; working = true; var released = false; var release = function () { if (released) return; released = true; working = false; setTimeout(pump, 150); }; f.addEventListener('load', release, { once: true }); setTimeout(release, 6000); // never let one slow frame stall the queue boot(f); } if ('IntersectionObserver' in window) { var io = new IntersectionObserver(function (ents) { ents.forEach(function (en) { if (en.isIntersecting) { io.unobserve(en.target); queue.push(en.target); pump(); } }); }, { rootMargin: '400px 0px' }); frames.forEach(function (f) { io.observe(f); }); } else { frames.forEach(function (f) { queue.push(f); }); pump(); }// Live theme sync: when the visitor toggles the site theme, push it into // every booted embed so they flip with the page. function pushTheme() { var t = hostTheme(); booted.forEach(function (f) { try { f.contentWindow && f.contentWindow.postMessage({ pmSrtTheme: t }, '*'); } catch (e) {} }); } if ('MutationObserver' in window) { new MutationObserver(pushTheme).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); }// Apply a theme to the WHOLE SITE (mirrors the theme toggle in // functions.php) when an embed reports a user-initiated flip, so the // trainer's own light/dark button drives the entire website. function applySiteTheme(t) { var root = document.documentElement; if (root.getAttribute('data-theme') === t) return; // already there → stop the loop root.setAttribute('data-theme', t); try { localStorage.setItem('pm-theme', t); } catch (e) {} try { document.querySelectorAll('.pm-theme-toggle').forEach(function (b) { var l = (t === 'light'); b.setAttribute('aria-pressed', l ? 'true' : 'false'); b.classList.toggle('is-light', l); }); } catch (e) {} try { window.dispatchEvent(new CustomEvent('pm:themechange', { detail: { theme: t } })); } catch (e) {} }// Messages coming UP from the frames: a theme flip to mirror site-wide, // and the height the trainer needs so the keyboard + its bottom bar fit // with nothing clipped (capped to the viewport so it stays responsive). window.addEventListener('message', function (e) { var d = e && e.data; if (!d) return; if (d.pmSrtThemeSet === 'light' || d.pmSrtThemeSet === 'dark') { applySiteTheme(d.pmSrtThemeSet); } if (typeof d.pmSrtHeight === 'number' && d.pmSrtHeight > 0) { for (var i = 0; i < booted.length; i++) { var f = booted[i]; if (f.contentWindow !== e.source) continue; var wrap = f.closest ? f.closest('.pm-srt-embed__frame') : null; if (!wrap) break; var maxH = Math.round((window.innerHeight || 800) * 0.92); var h = Math.max(360, Math.min(Math.round(d.pmSrtHeight), maxH)); wrap.style.height = h + 'px'; wrap.style.maxHeight = 'none'; break; } } }); })();