{"id":4213,"date":"2026-05-16T06:11:32","date_gmt":"2026-05-16T06:11:32","guid":{"rendered":"https:\/\/primetoolhub.com\/?page_id=4213"},"modified":"2026-08-08T10:01:56","modified_gmt":"2026-08-08T10:01:56","slug":"interactive-tour-builder","status":"publish","type":"page","link":"https:\/\/schoolict.net\/tools\/interactive-tour-builder\/","title":{"rendered":"Interactive Tour Builder | Create No-Code Guided Website Tours &amp; Product Walkthroughs"},"content":{"rendered":"<div class=\"pth-hero-section\">\n<div class=\"pth-hero-content\">\n<h2><b data-path-to-node=\"1,1,0\" data-index-in-node=\"0\">Guide Every User Step-by-Step<\/b> \u2014 Create seamless interactive tours and product walkthroughs effortlessly.<\/h2>\n<p>Build lightning-fast interactive website tours with the ultimate interactive tour builder. Paste any HTML, auto-detect elements, live preview, drag-and-drop steps, and export 100% self-contained production code. Perfect for SaaS onboarding, e-commerce demos, and premium conversions.<\/p>\n<div id=\"pth-toc-placeholder\">\u00a0<\/div>\n<\/div>\n<div class=\"pth-hero-image\"><img decoding=\"async\" src=\"https:\/\/schoolict.net\/tools\/wp-content\/uploads\/2026\/05\/interactive-tour-builder-800x447.jpeg\" alt=\"interactive tour builder\" width=\"450\" height=\"253\" data-no-lazy=\"1\" \/><\/div>\n<\/div>\n\n\n<div class=\"wp-block-rank-math-toc-block\" id=\"rank-math-toc\"><h2>Table of Contents<\/h2><nav><ul><li><a href=\"#\ud83d\udd34-four-tabs-and-why-each-one-exists\">\ud83d\udd34 Four Tabs, and Why Each One Exists<\/a><ul><li><a href=\"#build-write-the-steps-against-real-markup\">Build \u2014 write the steps against real markup<\/a><\/li><li><a href=\"#preview-test-where-tours-actually-break\">Preview &amp; Test \u2014 where tours actually break<\/a><\/li><li><a href=\"#hotspots-checklists-for-people-who-already-dismissed-the-tour\">Hotspots &amp; Checklists \u2014 for people who already dismissed the tour<\/a><\/li><li><a href=\"#export-embed-working-code-not-a-config-file\">Export &amp; Embed \u2014 working code, not a config file<\/a><\/li><\/ul><\/li><li><a href=\"#\ud83d\udfe1-mistakes-worth-avoiding\">\ud83d\udfe1 Mistakes Worth Avoiding<\/a><ul><li><a href=\"#building-the-tour-on-class-names\">Building the tour on class names<\/a><\/li><li><a href=\"#making-the-tour-too-long\">Making the tour too long<\/a><\/li><li><a href=\"#explaining-the-obvious\">Explaining the obvious<\/a><\/li><li><a href=\"#never-testing-on-a-phone\">Never testing on a phone<\/a><\/li><\/ul><\/li><li><a href=\"#\ud83d\udfe2-honest-limits\">\ud83d\udfe2 Honest Limits<\/a><\/li><li><a href=\"#\ud83d\udd34-going-deeper\">\ud83d\udd34 Going Deeper<\/a><\/li><\/ul><\/nav><\/div>\n\n\n<style>\r\n\/* ==========================================================================\r\n   PRIME TOOL HUB - TOUR & ONBOARDING STUDIO v10.0\r\n   Every rule is scoped to #pth-tour-wrapper. No global *, body or bare class\r\n   selectors - those leak into the WordPress theme and break unrelated pages.\r\n   ========================================================================== *\/\r\n#pth-tour-wrapper { font-family: 'Inter', system-ui, -apple-system, sans-serif; max-width: 1400px; margin: 0 auto; background: #f8fafc; padding: 20px; border-radius: 16px; color: #0f172a; line-height: 1.6; opacity: 1; }\r\n#pth-tour-wrapper * { box-sizing: border-box; min-width: 0; }\r\n\r\n#pth-tour-wrapper .pth-head { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; margin-bottom: 18px; }\r\n#pth-tour-wrapper .pth-h2 { font-size: 30px; font-weight: 800; color: #1d4ed8; margin: 0 0 8px 0; letter-spacing: -0.5px; }\r\n#pth-tour-wrapper .pth-sub { font-size: 15px; color: #475569; margin: 0 0 10px 0; font-weight: 500; }\r\n#pth-tour-wrapper .pth-badges { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }\r\n#pth-tour-wrapper .pth-version-badge { background: #dbeafe; color: #1e40af; border: 1px solid #bfdbfe; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }\r\n#pth-tour-wrapper .pth-status-offline { background: #dcfce7; color: #166534; border: 1px solid #bbf7d0; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }\r\n#pth-tour-wrapper .pth-head-btns { display: flex; gap: 8px; flex-wrap: wrap; }\r\n\r\n#pth-tour-wrapper .pth-btn { padding: 9px 16px; border-radius: 8px; font-weight: 700; cursor: pointer; border: 1px solid #e2e8f0; font-family: inherit; font-size: 14px; transition: background 0.15s, color 0.15s; background: #ffffff; color: #334155; }\r\n#pth-tour-wrapper .pth-btn:hover { background: #f1f5f9; color: #0f172a; }\r\n#pth-tour-wrapper .pth-btn.primary { background: #1d4ed8; color: #ffffff; border-color: #1d4ed8; }\r\n#pth-tour-wrapper .pth-btn.primary:hover { background: #1e40af; color: #ffffff; }\r\n#pth-tour-wrapper .pth-btn.success { background: #047857; color: #ffffff; border-color: #047857; }\r\n#pth-tour-wrapper .pth-btn.success:hover { background: #065f46; color: #ffffff; }\r\n#pth-tour-wrapper .pth-btn.danger { background: #fee2e2; color: #991b1b; border-color: #fecaca; }\r\n#pth-tour-wrapper .pth-btn.danger:hover { background: #fecaca; color: #7f1d1d; }\r\n#pth-tour-wrapper .pth-btn.sm { padding: 5px 10px; font-size: 12px; }\r\n#pth-tour-wrapper .pth-btn:focus-visible { outline: 3px solid #93c5fd; outline-offset: 2px; }\r\n#pth-tour-wrapper .pth-btn[disabled] { opacity: 0.55; cursor: not-allowed; }\r\n\r\n#pth-tour-wrapper .pth-tabbar { display: flex; gap: 6px; flex-wrap: wrap; border-bottom: 2px solid #e2e8f0; margin-bottom: 20px; }\r\n#pth-tour-wrapper .pth-tab { padding: 11px 18px; border: none; background: transparent; font-family: inherit; font-size: 14px; font-weight: 700; color: #64748b; cursor: pointer; border-bottom: 3px solid transparent; margin-bottom: -2px; }\r\n#pth-tour-wrapper .pth-tab:hover { color: #1d4ed8; }\r\n#pth-tour-wrapper .pth-tab.active { color: #1d4ed8; border-bottom-color: #1d4ed8; }\r\n#pth-tour-wrapper .pth-tab:focus-visible { outline: 3px solid #93c5fd; outline-offset: -3px; }\r\n#pth-tour-wrapper .pth-panel { display: none; }\r\n#pth-tour-wrapper .pth-panel.active { display: block; }\r\n\r\n#pth-tour-wrapper .pth-grid { display: grid; grid-template-columns: 1.8fr 1fr; gap: 22px; align-items: start; }\r\n#pth-tour-wrapper .pth-grid.pth-full-width { grid-template-columns: 1fr; }\r\n#pth-tour-wrapper .pth-card { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); margin-bottom: 18px; }\r\n#pth-tour-wrapper .pth-card:last-child { margin-bottom: 0; }\r\n#pth-tour-wrapper .pth-card-title { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 15px; font-weight: 800; color: #0f172a; margin: 0 0 14px 0; }\r\n\r\n#pth-tour-wrapper .pth-label { font-size: 13px; font-weight: 700; color: #334155; display: block; margin-bottom: 5px; }\r\n#pth-tour-wrapper .pth-inp, #pth-tour-wrapper .pth-select, #pth-tour-wrapper .pth-textarea { width: 100%; padding: 9px 12px; border: 1px solid #cbd5e1; border-radius: 8px; font-family: inherit; font-size: 14px; color: #0f172a; background: #ffffff; }\r\n#pth-tour-wrapper .pth-textarea { font-family: 'Consolas', 'Monaco', monospace; font-size: 13px; line-height: 1.7; resize: vertical; min-height: 150px; }\r\n#pth-tour-wrapper .pth-inp:focus, #pth-tour-wrapper .pth-select:focus, #pth-tour-wrapper .pth-textarea:focus { outline: 3px solid #93c5fd; outline-offset: 1px; border-color: #1d4ed8; }\r\n#pth-tour-wrapper .pth-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }\r\n#pth-tour-wrapper .pth-field { margin-bottom: 12px; }\r\n#pth-tour-wrapper .pth-ctrlbar { display: flex; gap: 9px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }\r\n#pth-tour-wrapper .pth-stat { background: #f1f5f9; color: #475569; padding: 3px 10px; border-radius: 999px; font-size: 12px; font-weight: 700; }\r\n#pth-tour-wrapper .pth-check { display: flex; align-items: center; gap: 8px; font-size: 13.5px; color: #334155; font-weight: 600; padding: 5px 0; }\r\n#pth-tour-wrapper .pth-check input { width: 16px; height: 16px; accent-color: #1d4ed8; cursor: pointer; }\r\n\r\n\/* Step list *\/\r\n#pth-tour-wrapper .pth-steps { border: 1px solid #e2e8f0; border-radius: 8px; max-height: 340px; overflow: auto; min-height: 90px; }\r\n#pth-tour-wrapper .pth-step { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-bottom: 1px solid #f1f5f9; cursor: pointer; }\r\n#pth-tour-wrapper .pth-step:last-child { border-bottom: none; }\r\n#pth-tour-wrapper .pth-step:hover { background: #f8fafc; }\r\n#pth-tour-wrapper .pth-step.sel { background: #eff6ff; border-left: 3px solid #1d4ed8; }\r\n#pth-tour-wrapper .pth-step-n { width: 24px; height: 24px; border-radius: 50%; background: #1d4ed8; color: #ffffff; font-size: 11.5px; font-weight: 800; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }\r\n#pth-tour-wrapper .pth-step-body { flex: 1; min-width: 0; }\r\n#pth-tour-wrapper .pth-step-t { font-size: 13.5px; font-weight: 700; color: #0f172a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\r\n#pth-tour-wrapper .pth-step-s { font-size: 11.5px; color: #64748b; font-family: 'Consolas', monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\r\n#pth-tour-wrapper .pth-step-acts { display: flex; gap: 4px; flex-shrink: 0; }\r\n#pth-tour-wrapper .pth-mini { border: 1px solid #e2e8f0; background: #ffffff; color: #475569; border-radius: 5px; padding: 3px 7px; font-size: 12px; cursor: pointer; font-family: inherit; }\r\n#pth-tour-wrapper .pth-mini:hover { background: #f1f5f9; color: #0f172a; }\r\n#pth-tour-wrapper .pth-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }\r\n#pth-tour-wrapper .pth-dot.ok { background: #16a34a; }\r\n#pth-tour-wrapper .pth-dot.bad { background: #dc2626; }\r\n#pth-tour-wrapper .pth-dot.idle { background: #cbd5e1; }\r\n\r\n\/* Stage: the sandboxed target markup *\/\r\n#pth-tour-wrapper .pth-stage-wrap { border: 1px solid #e2e8f0; border-radius: 8px; background: #ffffff; overflow: hidden; }\r\n#pth-tour-wrapper .pth-stage-bar { display: flex; justify-content: space-between; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 12px; background: #f1f5f9; border-bottom: 1px solid #e2e8f0; font-size: 12px; font-weight: 700; color: #475569; }\r\n#pth-tour-wrapper .pth-stage { position: relative; padding: 20px; min-height: 260px; overflow: auto; }\r\n#pth-tour-wrapper .pth-stage.w-mobile { max-width: 380px; margin: 0 auto; border-left: 1px dashed #cbd5e1; border-right: 1px dashed #cbd5e1; }\r\n#pth-tour-wrapper .pth-stage.w-tablet { max-width: 780px; margin: 0 auto; border-left: 1px dashed #cbd5e1; border-right: 1px dashed #cbd5e1; }\r\n#pth-tour-wrapper .pth-stage.picking { cursor: crosshair; }\r\n#pth-tour-wrapper .pth-stage.picking .pth-hover-hit { outline: 2px dashed #1d4ed8; outline-offset: 2px; }\r\n\r\n\/* Spotlight overlay lives inside the stage, not the page *\/\r\n#pth-tour-wrapper .pth-spot { position: absolute; border-radius: 8px; box-shadow: 0 0 0 9999px rgba(15,23,42,0.62); pointer-events: none; transition: all 0.28s ease; z-index: 40; }\r\n#pth-tour-wrapper .pth-bubble { position: absolute; z-index: 41; background: #ffffff; border: 1px solid #e2e8f0; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.18); padding: 14px 16px; width: 260px; max-width: 90%; }\r\n#pth-tour-wrapper .pth-bubble-t { font-size: 14.5px; font-weight: 800; color: #0f172a; margin: 0 0 5px 0; }\r\n#pth-tour-wrapper .pth-bubble-x { font-size: 13px; color: #475569; margin: 0 0 11px 0; line-height: 1.55; }\r\n#pth-tour-wrapper .pth-bubble-f { display: flex; justify-content: space-between; align-items: center; gap: 8px; }\r\n#pth-tour-wrapper .pth-bubble-c { font-size: 11.5px; color: #64748b; font-weight: 700; }\r\n\r\n\/* Widget previews *\/\r\n#pth-tour-wrapper .pth-hotspot { position: absolute; width: 22px; height: 22px; border-radius: 50%; background: #1d4ed8; border: 3px solid #ffffff; box-shadow: 0 0 0 0 rgba(29,78,216,0.55); z-index: 30; cursor: pointer; }\r\n#pth-tour-wrapper .pth-hotspot::after { content: ''; position: absolute; top: 50%; left: 50%; width: 22px; height: 22px; margin: -11px 0 0 -11px; border-radius: 50%; background: rgba(29,78,216,0.35); animation: pth-pulse 2s infinite; }\r\n@keyframes pth-pulse { 0% { transform: scale(1); opacity: 0.8; } 100% { transform: scale(2.4); opacity: 0; } }\r\n#pth-tour-wrapper .pth-checklist { border: 1px solid #e2e8f0; border-radius: 10px; background: #ffffff; padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); }\r\n#pth-tour-wrapper .pth-cl-head { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px; }\r\n#pth-tour-wrapper .pth-cl-title { font-size: 14px; font-weight: 800; color: #0f172a; }\r\n#pth-tour-wrapper .pth-cl-prog { background: #f1f5f9; border-radius: 999px; height: 7px; overflow: hidden; margin-bottom: 12px; }\r\n#pth-tour-wrapper .pth-cl-fill { height: 100%; background: #16a34a; border-radius: 999px; transition: width 0.3s ease; }\r\n#pth-tour-wrapper .pth-cl-item { display: flex; align-items: center; gap: 9px; padding: 6px 0; font-size: 13px; color: #334155; }\r\n#pth-tour-wrapper .pth-cl-item.done { color: #64748b; text-decoration: line-through; }\r\n#pth-tour-wrapper .pth-cl-tick { width: 18px; height: 18px; border-radius: 50%; border: 2px solid #cbd5e1; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-size: 11px; color: #ffffff; }\r\n#pth-tour-wrapper .pth-cl-item.done .pth-cl-tick { background: #16a34a; border-color: #16a34a; }\r\n#pth-tour-wrapper .pth-banner { border-radius: 10px; padding: 13px 16px; display: flex; align-items: center; gap: 12px; font-size: 13.5px; font-weight: 600; }\r\n#pth-tour-wrapper .pth-banner.info { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e40af; }\r\n#pth-tour-wrapper .pth-banner.ok { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }\r\n#pth-tour-wrapper .pth-banner.warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }\r\n\r\n#pth-tour-wrapper .pth-note { padding: 12px 15px; border-radius: 8px; font-size: 13.5px; margin-bottom: 14px; line-height: 1.6; }\r\n#pth-tour-wrapper .pth-note.info { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e40af; }\r\n#pth-tour-wrapper .pth-note.ok { background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534; }\r\n#pth-tour-wrapper .pth-note.warn { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }\r\n#pth-tour-wrapper .pth-note.err { background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; }\r\n\r\n#pth-tour-wrapper .pth-tablewrap { border: 1px solid #e2e8f0; border-radius: 8px; overflow: auto; max-height: 320px; min-height: 90px; }\r\n#pth-tour-wrapper table { width: 100%; border-collapse: collapse; font-size: 13px; }\r\n#pth-tour-wrapper th { background: #f1f5f9; color: #334155; text-align: left; padding: 8px 11px; font-weight: 800; font-size: 11.5px; text-transform: uppercase; letter-spacing: 0.4px; position: sticky; top: 0; }\r\n#pth-tour-wrapper td { padding: 8px 11px; border-top: 1px solid #f1f5f9; color: #334155; vertical-align: top; }\r\n#pth-tour-wrapper .pth-pill { display: inline-block; padding: 2px 9px; border-radius: 999px; font-size: 11px; font-weight: 800; white-space: nowrap; }\r\n#pth-tour-wrapper .pth-pill.ok { background: #dcfce7; color: #166534; }\r\n#pth-tour-wrapper .pth-pill.bad { background: #fee2e2; color: #991b1b; }\r\n#pth-tour-wrapper .pth-pill.warn { background: #fef3c7; color: #92400e; }\r\n\r\n#pth-tour-wrapper .pth-side h3 { font-size: 15px; font-weight: 800; color: #1e293b; margin: 0 0 9px 0; }\r\n#pth-tour-wrapper .pth-side h3.spaced { margin-top: 22px; }\r\n#pth-tour-wrapper .pth-side p { font-size: 13.5px; color: #475569; margin: 0 0 12px 0; }\r\n#pth-tour-wrapper .pth-side ul { margin: 0 0 12px 0; padding-left: 18px; }\r\n#pth-tour-wrapper .pth-side li { font-size: 13.5px; color: #475569; margin-bottom: 6px; }\r\n#pth-tour-wrapper .pth-side a { color: #1d4ed8; font-weight: 700; text-decoration: none; }\r\n#pth-tour-wrapper .pth-side a:hover { text-decoration: underline; }\r\n#pth-tour-wrapper code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-family: 'Consolas', monospace; font-size: 12.5px; color: #1e293b; }\r\n#pth-tour-wrapper .pth-tip { background: #eff6ff; border: 1px solid #bfdbfe; padding: 13px; border-radius: 8px; font-size: 13px; color: #1e40af; line-height: 1.6; }\r\n\r\n@media (max-width: 900px) {\r\n    #pth-tour-wrapper { padding: 14px; }\r\n    #pth-tour-wrapper .pth-grid { grid-template-columns: 1fr; }\r\n    #pth-tour-wrapper .pth-h2 { font-size: 24px; }\r\n    #pth-tour-wrapper .pth-tab { padding: 10px 12px; font-size: 13px; }\r\n    #pth-tour-wrapper .pth-row { grid-template-columns: 1fr; }\r\n}\r\n<\/style>\r\n\r\n<div id=\"pth-tour-toast\" style=\"position:fixed; bottom:24px; right:24px; background:#0f172a; color:#ffffff; padding:13px 22px; border-radius:10px; font-size:14px; font-weight:700; box-shadow:0 10px 25px rgba(0,0,0,0.2); z-index:2147483000; display:none; font-family:'Inter',system-ui,sans-serif; max-width:320px;\" role=\"status\" aria-live=\"polite\">Done<\/div>\r\n\r\n<div id=\"pth-tour-wrapper\">\r\n\r\n    <div class=\"pth-head\">\r\n        <div>\r\n            <h2 class=\"pth-h2\">\ud83c\udfaf Tour &amp; Onboarding Studio<\/h2>\r\n            <p class=\"pth-sub\">Build product tours, hotspots and onboarding checklists, test them against your own markup, then export ready-to-paste code.<\/p>\r\n            <div class=\"pth-badges\">\r\n                <span class=\"pth-version-badge\">v10.0<\/span>\r\n                <span class=\"pth-status-offline\">\ud83d\udd12 100% Offline<\/span>\r\n            <\/div>\r\n        <\/div>\r\n        <div class=\"pth-head-btns\">\r\n            <button type=\"button\" class=\"pth-btn\" id=\"pth-t-sample\">\ud83d\udca1 Load Sample<\/button>\r\n            <button type=\"button\" class=\"pth-btn\" id=\"pth-t-fw\">\u26f6 Full Width<\/button>\r\n            <button type=\"button\" class=\"pth-btn danger\" id=\"pth-t-clear\">\ud83d\uddd1\ufe0f Clear<\/button>\r\n        <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"pth-tabbar\" role=\"tablist\">\r\n        <button type=\"button\" class=\"pth-tab active\" data-pth-panel=\"build\" role=\"tab\">\ud83c\udfaf Build<\/button>\r\n        <button type=\"button\" class=\"pth-tab\" data-pth-panel=\"preview\" role=\"tab\">\ud83d\udc41\ufe0f Preview &amp; Test<\/button>\r\n        <button type=\"button\" class=\"pth-tab\" data-pth-panel=\"widgets\" role=\"tab\">\ud83e\udde9 Hotspots &amp; Checklists<\/button>\r\n        <button type=\"button\" class=\"pth-tab\" data-pth-panel=\"export\" role=\"tab\">\ud83d\udce4 Export &amp; Embed<\/button>\r\n    <\/div>\r\n\r\n    <div class=\"pth-grid\" id=\"pth-t-grid\">\r\n\r\n        <div>\r\n            <!-- ============ PANEL 1: BUILD ============ -->\r\n            <div class=\"pth-panel active\" id=\"pth-panel-build\" role=\"tabpanel\">\r\n\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\">\r\n                        <span>\ud83d\udce6 Target markup<\/span>\r\n                        <div style=\"display:flex; gap:7px; flex-wrap:wrap;\">\r\n                            <select id=\"pth-t-sample-pick\" class=\"pth-select\" style=\"width:auto; padding:5px 9px; font-size:12px;\" aria-label=\"Sample markup\">\r\n                                <option value=\"app\">Sample: app dashboard<\/option>\r\n                                <option value=\"form\">Sample: signup form<\/option>\r\n                                <option value=\"shop\">Sample: product page<\/option>\r\n                            <\/select>\r\n                            <button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-detect\">\ud83d\udd0d Auto-detect steps<\/button>\r\n                        <\/div>\r\n                    <\/div>\r\n                    <p style=\"font-size:12.5px; color:#64748b; margin:0 0 9px 0;\">Paste the HTML of the page you are building a tour for. Only the markup matters \u2014 scripts are stripped before anything is rendered.<\/p>\r\n                    <textarea id=\"pth-t-html\" class=\"pth-textarea\" placeholder=\"Paste your page markup here, or pick a sample above.\" aria-label=\"Target HTML markup\"><\/textarea>\r\n                    <div class=\"pth-ctrlbar\" style=\"margin-top:11px; margin-bottom:0;\">\r\n                        <button type=\"button\" class=\"pth-btn primary\" id=\"pth-t-render\">\u25b6 Render stage<\/button>\r\n                        <button type=\"button\" class=\"pth-btn\" id=\"pth-t-pick\">\ud83d\uddb1\ufe0f Pick element<\/button>\r\n                        <span class=\"pth-stat\" id=\"pth-t-html-stat\">No markup loaded<\/span>\r\n                    <\/div>\r\n                <\/div>\r\n\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\"><span>\ud83d\uddbc\ufe0f Stage<\/span><span class=\"pth-stat\" id=\"pth-t-pick-hint\">Rendered markup appears here<\/span><\/div>\r\n                    <div class=\"pth-stage-wrap\">\r\n                        <div class=\"pth-stage\" id=\"pth-t-stage\"><p style=\"color:#94a3b8; font-size:13.5px; margin:0;\">Paste markup above and press Render stage.<\/p><\/div>\r\n                    <\/div>\r\n                <\/div>\r\n\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\">\r\n                        <span>\ud83d\udcdd Steps<\/span>\r\n                        <div style=\"display:flex; gap:7px; flex-wrap:wrap;\">\r\n                            <button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-add\">\uff0b Add step<\/button>\r\n                            <button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-validate\">\u2713 Check selectors<\/button>\r\n                        <\/div>\r\n                    <\/div>\r\n                    <div class=\"pth-steps\" id=\"pth-t-steplist\"><\/div>\r\n\r\n                    <div id=\"pth-t-editor\" style=\"margin-top:16px; display:none;\">\r\n                        <div class=\"pth-field\">\r\n                            <label class=\"pth-label\" for=\"pth-t-f-sel\">CSS selector<\/label>\r\n                            <input type=\"text\" id=\"pth-t-f-sel\" class=\"pth-inp\" placeholder=\"#signup-button or .card .title\" aria-label=\"CSS selector\">\r\n                        <\/div>\r\n                        <div class=\"pth-field\">\r\n                            <label class=\"pth-label\" for=\"pth-t-f-title\">Step title<\/label>\r\n                            <input type=\"text\" id=\"pth-t-f-title\" class=\"pth-inp\" placeholder=\"Upload your first file\" aria-label=\"Step title\">\r\n                        <\/div>\r\n                        <div class=\"pth-field\">\r\n                            <label class=\"pth-label\" for=\"pth-t-f-text\">Step text<\/label>\r\n                            <textarea id=\"pth-t-f-text\" class=\"pth-textarea\" style=\"min-height:80px; font-family:inherit; font-size:14px;\" placeholder=\"Explain what this control does, in one or two sentences.\" aria-label=\"Step text\"><\/textarea>\r\n                        <\/div>\r\n                        <div class=\"pth-row\">\r\n                            <div>\r\n                                <label class=\"pth-label\" for=\"pth-t-f-pos\">Bubble position<\/label>\r\n                                <select id=\"pth-t-f-pos\" class=\"pth-select\" aria-label=\"Bubble position\">\r\n                                    <option value=\"bottom\">Below the element<\/option>\r\n                                    <option value=\"top\">Above the element<\/option>\r\n                                    <option value=\"right\">To the right<\/option>\r\n                                    <option value=\"left\">To the left<\/option>\r\n                                <\/select>\r\n                            <\/div>\r\n                            <div>\r\n                                <label class=\"pth-label\" for=\"pth-t-f-adv\">Advance when<\/label>\r\n                                <select id=\"pth-t-f-adv\" class=\"pth-select\" aria-label=\"Advance trigger\">\r\n                                    <option value=\"next\">Next is pressed<\/option>\r\n                                    <option value=\"click\">The element is clicked<\/option>\r\n                                    <option value=\"input\">The field is filled<\/option>\r\n                                <\/select>\r\n                            <\/div>\r\n                        <\/div>\r\n                        <label class=\"pth-check\"><input type=\"checkbox\" id=\"pth-t-f-scroll\" checked> Scroll the element into view<\/label>\r\n                        <label class=\"pth-check\"><input type=\"checkbox\" id=\"pth-t-f-optional\"> Skip this step if the element is missing<\/label>\r\n                        <div class=\"pth-ctrlbar\" style=\"margin-top:12px; margin-bottom:0;\">\r\n                            <button type=\"button\" class=\"pth-btn primary\" id=\"pth-t-save\">Save step<\/button>\r\n                            <button type=\"button\" class=\"pth-btn\" id=\"pth-t-cancel\">Cancel<\/button>\r\n                        <\/div>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- ============ PANEL 2: PREVIEW ============ -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-preview\" role=\"tabpanel\">\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\">\r\n                        <span>\ud83d\udc41\ufe0f Run the tour<\/span>\r\n                        <div style=\"display:flex; gap:7px; flex-wrap:wrap;\">\r\n                            <button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-w-mobile\">\ud83d\udcf1 Mobile<\/button>\r\n                            <button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-w-tablet\">\ud83d\udcbb Tablet<\/button>\r\n                            <button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-w-full\">\ud83d\udda5\ufe0f Full<\/button>\r\n                        <\/div>\r\n                    <\/div>\r\n                    <div class=\"pth-ctrlbar\">\r\n                        <button type=\"button\" class=\"pth-btn primary\" id=\"pth-t-play\">\u25b6 Start tour<\/button>\r\n                        <button type=\"button\" class=\"pth-btn\" id=\"pth-t-prev\">\u2190 Prev<\/button>\r\n                        <button type=\"button\" class=\"pth-btn\" id=\"pth-t-next\">Next \u2192<\/button>\r\n                        <button type=\"button\" class=\"pth-btn danger\" id=\"pth-t-stop\">\u25a0 Stop<\/button>\r\n                        <span class=\"pth-stat\" id=\"pth-t-play-stat\">Not running<\/span>\r\n                    <\/div>\r\n                    <div class=\"pth-stage-wrap\">\r\n                        <div class=\"pth-stage-bar\"><span id=\"pth-t-view-label\">Full width<\/span><span id=\"pth-t-step-label\">No tour running<\/span><\/div>\r\n                        <div class=\"pth-stage\" id=\"pth-t-stage2\"><p style=\"color:#94a3b8; font-size:13.5px; margin:0;\">Render your markup on the Build tab first.<\/p><\/div>\r\n                    <\/div>\r\n                <\/div>\r\n\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\"><span>\ud83e\ude7a Selector health<\/span><button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-health\">Run check<\/button><\/div>\r\n                    <div class=\"pth-note info\" id=\"pth-t-health-note\">A tour breaks silently when a selector stops matching. This check tells you which steps would fail, and which match more than one element.<\/div>\r\n                    <div class=\"pth-tablewrap\">\r\n                        <table>\r\n                            <thead><tr><th style=\"width:46px;\">#<\/th><th>Selector<\/th><th style=\"width:82px;\">Matches<\/th><th style=\"width:110px;\">Verdict<\/th><\/tr><\/thead>\r\n                            <tbody id=\"pth-t-health-rows\"><\/tbody>\r\n                        <\/table>\r\n                    <\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- ============ PANEL 3: WIDGETS ============ -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-widgets\" role=\"tabpanel\">\r\n                <div class=\"pth-note info\">A guided tour interrupts. These three patterns do not \u2014 they sit on the page and wait to be noticed, which suits returning users far better than a tour they have already dismissed once.<\/div>\r\n\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\"><span>\ud83d\udccd Hotspots<\/span><button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-hs-add\">\uff0b Add hotspot<\/button><\/div>\r\n                    <p style=\"font-size:12.5px; color:#64748b; margin:0 0 11px 0;\">A small pulsing marker anchored to an element. Click reveals the tip. Good for features people would otherwise never find.<\/p>\r\n                    <div class=\"pth-steps\" id=\"pth-t-hs-list\" style=\"max-height:220px;\"><\/div>\r\n                <\/div>\r\n\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\"><span>\u2705 Onboarding checklist<\/span><button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-cl-add\">\uff0b Add task<\/button><\/div>\r\n                    <div class=\"pth-field\">\r\n                        <label class=\"pth-label\" for=\"pth-t-cl-title\">Checklist title<\/label>\r\n                        <input type=\"text\" id=\"pth-t-cl-title\" class=\"pth-inp\" value=\"Getting started\" aria-label=\"Checklist title\">\r\n                    <\/div>\r\n                    <div class=\"pth-steps\" id=\"pth-t-cl-list\" style=\"max-height:220px; margin-bottom:14px;\"><\/div>\r\n                    <div class=\"pth-card-title\" style=\"margin-bottom:9px;\"><span style=\"font-size:13px;\">Preview<\/span><\/div>\r\n                    <div class=\"pth-checklist\" id=\"pth-t-cl-preview\"><\/div>\r\n                <\/div>\r\n\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\"><span>\ud83d\udce3 Announcement banner<\/span><\/div>\r\n                    <div class=\"pth-row\">\r\n                        <div>\r\n                            <label class=\"pth-label\" for=\"pth-t-bn-text\">Message<\/label>\r\n                            <input type=\"text\" id=\"pth-t-bn-text\" class=\"pth-inp\" value=\"New: batch export is live. Try it from the toolbar.\" aria-label=\"Banner message\">\r\n                        <\/div>\r\n                        <div>\r\n                            <label class=\"pth-label\" for=\"pth-t-bn-style\">Style<\/label>\r\n                            <select id=\"pth-t-bn-style\" class=\"pth-select\" aria-label=\"Banner style\">\r\n                                <option value=\"info\">Information<\/option>\r\n                                <option value=\"ok\">Success<\/option>\r\n                                <option value=\"warn\">Warning<\/option>\r\n                            <\/select>\r\n                        <\/div>\r\n                    <\/div>\r\n                    <div class=\"pth-field\">\r\n                        <label class=\"pth-label\" for=\"pth-t-bn-icon\">Icon<\/label>\r\n                        <input type=\"text\" id=\"pth-t-bn-icon\" class=\"pth-inp\" value=\"\ud83d\udce3\" maxlength=\"4\" style=\"width:90px;\" aria-label=\"Banner icon\">\r\n                    <\/div>\r\n                    <div class=\"pth-card-title\" style=\"margin-bottom:9px;\"><span style=\"font-size:13px;\">Preview<\/span><\/div>\r\n                    <div id=\"pth-t-bn-preview\"><\/div>\r\n                <\/div>\r\n            <\/div>\r\n\r\n            <!-- ============ PANEL 4: EXPORT ============ -->\r\n            <div class=\"pth-panel\" id=\"pth-panel-export\" role=\"tabpanel\">\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\">\r\n                        <span>\ud83d\udce4 Export format<\/span>\r\n                        <div style=\"display:flex; gap:7px; flex-wrap:wrap;\">\r\n                            <button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-copy\">\ud83d\udccb Copy<\/button>\r\n                            <button type=\"button\" class=\"pth-btn sm success\" id=\"pth-t-download\">\ud83d\udcbe Download<\/button>\r\n                        <\/div>\r\n                    <\/div>\r\n                    <div class=\"pth-row\">\r\n                        <div>\r\n                            <label class=\"pth-label\" for=\"pth-t-fmt\">Format<\/label>\r\n                            <select id=\"pth-t-fmt\" class=\"pth-select\" aria-label=\"Export format\">\r\n                                <option value=\"json\">JSON \u2014 tour definition only<\/option>\r\n                                <option value=\"js\">Standalone JavaScript \u2014 drop into any page<\/option>\r\n                                <option value=\"react\">React component<\/option>\r\n                                <option value=\"vue\">Vue 3 component<\/option>\r\n                                <option value=\"wp\">WordPress shortcode snippet<\/option>\r\n                            <\/select>\r\n                        <\/div>\r\n                        <div>\r\n                            <label class=\"pth-label\" for=\"pth-t-opt-analytics\">Options<\/label>\r\n                            <label class=\"pth-check\"><input type=\"checkbox\" id=\"pth-t-opt-analytics\"> Include analytics hooks<\/label>\r\n                            <label class=\"pth-check\"><input type=\"checkbox\" id=\"pth-t-opt-once\" checked> Remember dismissal per visitor<\/label>\r\n                        <\/div>\r\n                    <\/div>\r\n                    <div class=\"pth-note info\" id=\"pth-t-fmt-note\"><\/div>\r\n                    <textarea id=\"pth-t-out\" class=\"pth-textarea\" style=\"min-height:300px;\" readonly placeholder=\"Pick a format and the code appears here.\" aria-label=\"Exported code\"><\/textarea>\r\n                <\/div>\r\n\r\n                <div class=\"pth-card\">\r\n                    <div class=\"pth-card-title\"><span>\ud83d\udce5 Import a saved tour<\/span><button type=\"button\" class=\"pth-btn sm\" id=\"pth-t-import\">Import JSON<\/button><\/div>\r\n                    <textarea id=\"pth-t-import-box\" class=\"pth-textarea\" style=\"min-height:110px;\" placeholder=\"Paste a tour JSON export here and press Import.\" aria-label=\"Import JSON\"><\/textarea>\r\n                <\/div>\r\n            <\/div>\r\n        <\/div>\r\n\r\n        <!-- ============ SIDEBAR ============ -->\r\n        <div class=\"pth-card pth-side\" id=\"pth-t-side\">\r\n            <div id=\"pth-side-build\">\r\n                <h3>Writing a tour people finish<\/h3>\r\n                <p>Five steps is a sensible ceiling. Past that, completion falls off sharply \u2014 people came to do a job, not to read a manual, and every extra step is another chance to press Exit.<\/p>\r\n                <p>Point at controls that are genuinely hard to find. A tour step over a button labelled Save teaches nobody anything; a step over the setting buried three menus deep earns its interruption.<\/p>\r\n                <div class=\"pth-tip\"><strong>Selector advice:<\/strong> prefer a dedicated attribute such as <code>data-tour=\"upload\"<\/code> over a class name. Classes get renamed during a redesign and the tour breaks quietly; a data attribute exists only for this and tends to survive.<\/div>\r\n                <h3 class=\"spaced\">Auto-detect<\/h3>\r\n                <p>The detect button walks your markup for interactive elements \u2014 buttons, inputs, links with an id \u2014 and drafts a step for each. It is a starting point to edit down, not a finished tour.<\/p>\r\n            <\/div>\r\n            <div id=\"pth-side-preview\" style=\"display:none;\">\r\n                <h3>Test at the width people use<\/h3>\r\n                <p>A bubble positioned to the right of an element looks fine on a desktop and falls off the screen at 380 pixels. Run the tour at mobile width before shipping it; that is where most tours break.<\/p>\r\n                <h3 class=\"spaced\">Why selectors fail<\/h3>\r\n                <p>A selector matching nothing is the common failure, and it fails silently \u2014 the step is skipped or the tour stalls with no error anywhere. A selector matching several elements is subtler: the tour highlights the first one, which is not always the one you meant.<\/p>\r\n                <div class=\"pth-tip\"><strong>Run the health check<\/strong> after any change to the target page. It takes a second and catches both problems before your users do.<\/div>\r\n            <\/div>\r\n            <div id=\"pth-side-widgets\" style=\"display:none;\">\r\n                <h3>Not everything needs a tour<\/h3>\r\n                <p>Tours suit a first session. They are poorly suited to everything after it, because a returning user has already dismissed one and will dismiss the next faster.<\/p>\r\n                <p><strong>Hotspots<\/strong> wait to be noticed rather than demanding attention, which makes them right for a feature that is useful but not urgent.<\/p>\r\n                <p><strong>Checklists<\/strong> work because progress is visible. Three of five done is a much stronger pull than a tour that has to be restarted from the beginning.<\/p>\r\n                <div class=\"pth-tip\"><strong>Banners<\/strong> are for one thing: something changed. Keep them to a single sentence and let people close them for good.<\/div>\r\n            <\/div>\r\n            <div id=\"pth-side-export\" style=\"display:none;\">\r\n                <h3>What the exports give you<\/h3>\r\n                <p><strong>JSON<\/strong> is the definition on its own \u2014 useful if you already have a tour library and only want the content.<\/p>\r\n                <p><strong>Standalone JavaScript<\/strong> is self-contained: markup, styles and logic in one file with no dependencies. Paste it before the closing body tag and it runs.<\/p>\r\n                <p><strong>React and Vue<\/strong> exports are components with the steps inlined, ready to drop into a project.<\/p>\r\n                <div class=\"pth-tip\"><strong>Analytics hooks<\/strong> fire a custom event on each step shown, completed or skipped. Where people leave a tour tells you which step is confusing far better than a completion rate does.<\/div>\r\n            <\/div>\r\n\r\n            <h3 class=\"spaced\">Related tools<\/h3>\r\n            <ul>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/interactive-learning-studio-pro\/\">Interactive Learning Studio<\/a> \u2014 quizzes and lessons rather than UI tours<\/li>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/offline-html-editor-with-css-js\/\">Offline HTML Editor<\/a> \u2014 build the target page itself<\/li>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/screen-recorder-studio\/\">Screen Recorder Studio<\/a> \u2014 when a video suits better than a tour<\/li>\r\n                <li><a href=\"https:\/\/schoolict.net\/tools\/responsive-website-tester-pro\/\">Responsive Website Tester<\/a> \u2014 check the page under the tour<\/li>\r\n            <\/ul>\r\n        <\/div>\r\n\r\n    <\/div>\r\n<\/div>\r\n\r\n<script data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\">\r\n(function() {\r\n    'use strict';\r\n\r\n    \/* Closing tags are assembled at run time. A literal closing tag inside a JS\r\n       string is rewritten by WordPress and LiteSpeed, which dumps the raw script\r\n       onto the page as visible text. *\/\r\n    var CT = '<' + '\/';\r\n\r\n    window.pthTourApp = {\r\n        initialized: false,\r\n        bound: false,\r\n        currentTab: 'build',\r\n        steps: [],\r\n        hotspots: [],\r\n        tasks: [],\r\n        editing: -1,\r\n        picking: false,\r\n        playing: false,\r\n        playIdx: 0,\r\n        viewMode: 'full',\r\n\r\n        SAMPLES: {\r\n            app: '<div style=\"font-family:sans-serif\">' +\r\n                '<div style=\"display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#0f172a;color:#fff;border-radius:8px 8px 0 0\">' +\r\n                '<strong id=\"app-logo\">Acme Console<' + '\/strong>' +\r\n                '<button id=\"app-profile\" style=\"background:#334155;color:#fff;border:none;padding:6px 12px;border-radius:6px\">Profile<' + '\/button>' + CT + 'div>' +\r\n                '<div style=\"border:1px solid #e2e8f0;border-top:none;padding:16px;border-radius:0 0 8px 8px\">' +\r\n                '<label style=\"font-size:12px;font-weight:700;color:#64748b\">PROJECT<' + '\/label>' +\r\n                '<select id=\"app-project\" style=\"width:100%;padding:8px;margin:6px 0 14px;border:1px solid #e2e8f0;border-radius:6px\"><option>Website redesign<' + '\/option>' + CT + 'select>' +\r\n                '<div style=\"display:flex;gap:10px;margin-bottom:14px\">' +\r\n                '<button id=\"app-upload\" style=\"background:#1d4ed8;color:#fff;border:none;padding:9px 16px;border-radius:6px;font-weight:700\">Upload file<' + '\/button>' +\r\n                '<button id=\"app-share\" style=\"background:#f1f5f9;color:#334155;border:1px solid #e2e8f0;padding:9px 16px;border-radius:6px;font-weight:700\">Share<' + '\/button>' + CT + 'div>' +\r\n                '<div id=\"app-usage\" style=\"background:#f8fafc;border:1px solid #e2e8f0;padding:12px;border-radius:6px;font-size:13px\">Storage used: 2.1 GB of 10 GB' + CT + 'div>' + CT + 'div>' + CT + 'div>',\r\n            form: '<div style=\"font-family:sans-serif;max-width:380px\">' +\r\n                '<h3 id=\"form-heading\" style=\"margin:0 0 14px\">Create your account' + CT + 'h3>' +\r\n                '<label style=\"font-size:12px;font-weight:700;color:#64748b\">EMAIL' + CT + 'label>' +\r\n                '<input id=\"form-email\" type=\"email\" placeholder=\"you@company.com\" style=\"width:100%;padding:9px;margin:5px 0 12px;border:1px solid #e2e8f0;border-radius:6px\">' +\r\n                '<label style=\"font-size:12px;font-weight:700;color:#64748b\">PASSWORD' + CT + 'label>' +\r\n                '<input id=\"form-pass\" type=\"password\" placeholder=\"At least 12 characters\" style=\"width:100%;padding:9px;margin:5px 0 12px;border:1px solid #e2e8f0;border-radius:6px\">' +\r\n                '<label style=\"font-size:13px;display:flex;gap:7px;align-items:center;margin-bottom:14px\"><input id=\"form-terms\" type=\"checkbox\"> I accept the terms' + CT + 'label>' +\r\n                '<button id=\"form-submit\" style=\"width:100%;background:#1d4ed8;color:#fff;border:none;padding:11px;border-radius:6px;font-weight:700\">Create account' + CT + 'button>' + CT + 'div>',\r\n            shop: '<div style=\"font-family:sans-serif\">' +\r\n                '<div style=\"display:flex;gap:16px;flex-wrap:wrap\">' +\r\n                '<div id=\"shop-image\" style=\"width:150px;height:150px;background:#e2e8f0;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#94a3b8\">Photo' + CT + 'div>' +\r\n                '<div style=\"flex:1;min-width:200px\">' +\r\n                '<h3 id=\"shop-name\" style=\"margin:0 0 6px\">Wireless headphones' + CT + 'h3>' +\r\n                '<div id=\"shop-price\" style=\"font-size:22px;font-weight:800;color:#0f172a;margin-bottom:10px\">$149' + CT + 'div>' +\r\n                '<select id=\"shop-colour\" style=\"padding:8px;border:1px solid #e2e8f0;border-radius:6px;margin-bottom:12px\"><option>Black' + CT + 'option><option>White' + CT + 'option>' + CT + 'select>' +\r\n                '<div style=\"display:flex;gap:9px\">' +\r\n                '<button id=\"shop-cart\" style=\"background:#1d4ed8;color:#fff;border:none;padding:10px 18px;border-radius:6px;font-weight:700\">Add to basket' + CT + 'button>' +\r\n                '<button id=\"shop-wish\" style=\"background:#fff;border:1px solid #e2e8f0;padding:10px 16px;border-radius:6px;font-weight:700\">Save' + CT + 'button>' + CT + 'div>' + CT + 'div>' + CT + 'div>' + CT + 'div>'\r\n        },\r\n\r\n        \/* ---------- start up ---------- *\/\r\n        init: function() {\r\n            var self = this;\r\n            var failures = [];\r\n            var step = function(name, fn) {\r\n                try { fn(); } catch (err) {\r\n                    failures.push(name);\r\n                    if (window.console && window.console.error) {\r\n                        window.console.error('[Tour Studio] ' + name + ' failed', err);\r\n                    }\r\n                }\r\n            };\r\n            step('portalToast', function() { self.portalToast(); });\r\n            step('bindAll', function() { self.bindAll(); });\r\n            step('seedWidgets', function() { self.seedWidgets(); });\r\n            step('renderAll', function() { self.renderSteps(); self.renderHotspots(); self.renderTasks(); self.renderBanner(); self.renderExport(); });\r\n            if (failures.length && window.console && window.console.warn) {\r\n                window.console.warn('[Tour Studio] steps failed: ' + failures.join(', '));\r\n            }\r\n        },\r\n\r\n        portalToast: function() {\r\n            var t = document.getElementById('pth-tour-toast');\r\n            if (t && t.parentNode !== document.body) { document.body.appendChild(t); }\r\n        },\r\n\r\n        seedWidgets: function() {\r\n            this.tasks = [\r\n                { text: 'Upload your first file', done: true },\r\n                { text: 'Invite a teammate', done: false },\r\n                { text: 'Connect a storage provider', done: false }\r\n            ];\r\n        },\r\n\r\n        \/* ---------- delegation ---------- *\/\r\n        bindAll: function() {\r\n            var self = this;\r\n            if (this.bound) { return; }\r\n            this.bound = true;\r\n\r\n            document.addEventListener('click', function(e) {\r\n                if (!self.inside(e.target)) { return; }\r\n\r\n                if (self.picking) {\r\n                    var hit = self.closestIn(e.target, 'pth-t-stage');\r\n                    if (hit) { e.preventDefault(); e.stopPropagation(); self.pickElement(e.target); return; }\r\n                }\r\n\r\n                var tab = self.closestClass(e.target, 'pth-tab');\r\n                if (tab) { e.preventDefault(); self.switchTab(tab.getAttribute('data-pth-panel')); return; }\r\n\r\n                var act = self.closestAttr(e.target, 'data-pth-act');\r\n                if (act) {\r\n                    e.preventDefault();\r\n                    self.rowAction(act.getAttribute('data-pth-act'), parseInt(act.getAttribute('data-pth-i'), 10));\r\n                    return;\r\n                }\r\n\r\n                var row = self.closestClass(e.target, 'pth-step');\r\n                if (row && row.getAttribute('data-pth-row') === 'step') {\r\n                    self.editStep(parseInt(row.getAttribute('data-pth-i'), 10));\r\n                    return;\r\n                }\r\n\r\n                var btn = self.closestTag(e.target, 'BUTTON');\r\n                if (btn && btn.id) { self.route(btn.id, btn, e); }\r\n            }, false);\r\n\r\n            document.addEventListener('input', function(e) {\r\n                var n = e.target;\r\n                if (!n || !n.id || !self.inside(n)) { return; }\r\n                if (n.id === 'pth-t-html') { self.updateHtmlStat(); }\r\n                else if (n.id === 'pth-t-bn-text' || n.id === 'pth-t-bn-icon') { self.renderBanner(); }\r\n                else if (n.id === 'pth-t-cl-title') { self.renderTasks(); }\r\n            }, false);\r\n\r\n            document.addEventListener('change', function(e) {\r\n                var n = e.target;\r\n                if (!n || !n.id || !self.inside(n)) { return; }\r\n                if (n.id === 'pth-t-fmt' || n.id === 'pth-t-opt-analytics' || n.id === 'pth-t-opt-once') { self.renderExport(); }\r\n                else if (n.id === 'pth-t-bn-style') { self.renderBanner(); }\r\n            }, false);\r\n        },\r\n\r\n        inside: function(node) {\r\n            while (node && node.nodeType === 1) {\r\n                if (node.id === 'pth-tour-wrapper') { return true; }\r\n                node = node.parentNode;\r\n            }\r\n            return false;\r\n        },\r\n\r\n        closestIn: function(node, id) {\r\n            while (node && node.nodeType === 1) {\r\n                if (node.id === id) { return node; }\r\n                if (node.id === 'pth-tour-wrapper') { return null; }\r\n                node = node.parentNode;\r\n            }\r\n            return null;\r\n        },\r\n\r\n        closestClass: function(node, cls) {\r\n            while (node && node.nodeType === 1) {\r\n                if (node.className && typeof node.className === 'string' && (' ' + node.className + ' ').indexOf(' ' + cls + ' ') >= 0) { return node; }\r\n                if (node.id === 'pth-tour-wrapper') { return null; }\r\n                node = node.parentNode;\r\n            }\r\n            return null;\r\n        },\r\n\r\n        closestTag: function(node, tag) {\r\n            while (node && node.nodeType === 1) {\r\n                if (node.tagName === tag) { return node; }\r\n                if (node.id === 'pth-tour-wrapper') { return null; }\r\n                node = node.parentNode;\r\n            }\r\n            return null;\r\n        },\r\n\r\n        closestAttr: function(node, attr) {\r\n            while (node && node.nodeType === 1) {\r\n                if (node.getAttribute && node.getAttribute(attr)) { return node; }\r\n                if (node.id === 'pth-tour-wrapper') { return null; }\r\n                node = node.parentNode;\r\n            }\r\n            return null;\r\n        },\r\n\r\n        route: function(id, btn, e) {\r\n            if (id === 'pth-t-sample') { this.loadSample(); }\r\n            else if (id === 'pth-t-fw') { this.toggleWidth(); }\r\n            else if (id === 'pth-t-clear') { this.clearAll(); }\r\n            else if (id === 'pth-t-render') { this.renderStage(); }\r\n            else if (id === 'pth-t-pick') { this.togglePick(); }\r\n            else if (id === 'pth-t-detect') { this.autoDetect(); }\r\n            else if (id === 'pth-t-add') { this.newStep(); }\r\n            else if (id === 'pth-t-save') { this.saveStep(); }\r\n            else if (id === 'pth-t-cancel') { this.closeEditor(); }\r\n            else if (id === 'pth-t-validate') { this.switchTab('preview'); this.runHealth(); }\r\n            else if (id === 'pth-t-health') { this.runHealth(); }\r\n            else if (id === 'pth-t-play') { this.playTour(); }\r\n            else if (id === 'pth-t-next') { this.movePlay(1); }\r\n            else if (id === 'pth-t-prev') { this.movePlay(-1); }\r\n            else if (id === 'pth-t-stop') { this.stopTour(); }\r\n            else if (id === 'pth-t-w-mobile') { this.setView('mobile'); }\r\n            else if (id === 'pth-t-w-tablet') { this.setView('tablet'); }\r\n            else if (id === 'pth-t-w-full') { this.setView('full'); }\r\n            else if (id === 'pth-t-hs-add') { this.addHotspot(); }\r\n            else if (id === 'pth-t-cl-add') { this.addTask(); }\r\n            else if (id === 'pth-t-copy') { this.copyText(this.val('pth-t-out'), 'Code'); }\r\n            else if (id === 'pth-t-download') { this.downloadExport(); }\r\n            else if (id === 'pth-t-import') { this.importJson(); }\r\n            else { return; }\r\n            if (e && e.preventDefault) { e.preventDefault(); }\r\n        },\r\n\r\n        rowAction: function(act, i) {\r\n            if (act === 'up') { this.moveStep(i, -1); }\r\n            else if (act === 'down') { this.moveStep(i, 1); }\r\n            else if (act === 'dup') { this.dupStep(i); }\r\n            else if (act === 'del') { this.delStep(i); }\r\n            else if (act === 'hsdel') { this.hotspots.splice(i, 1); this.renderHotspots(); this.renderExport(); }\r\n            else if (act === 'tkdel') { this.tasks.splice(i, 1); this.renderTasks(); this.renderExport(); }\r\n            else if (act === 'tktog') { this.tasks[i].done = !this.tasks[i].done; this.renderTasks(); }\r\n        },\r\n\r\n        \/* ---------- helpers ---------- *\/\r\n        val: function(id) { var el = document.getElementById(id); return el ? el.value : ''; },\r\n        checked: function(id) { var el = document.getElementById(id); return el ? el.checked : false; },\r\n        setText: function(id, t) { var el = document.getElementById(id); if (el) { el.innerHTML = t; } },\r\n        setVal: function(id, v) { var el = document.getElementById(id); if (el) { el.value = v; } },\r\n\r\n        esc: function(s) {\r\n            return String(s == null ? '' : s)\r\n                .replace(\/&\/g, '&amp;')\r\n                .replace(new RegExp(String.fromCharCode(60), 'g'), '&lt;')\r\n                .replace(new RegExp(String.fromCharCode(62), 'g'), '&gt;')\r\n                .replace(\/\"\/g, '&quot;');\r\n        },\r\n\r\n        jsStr: function(s) {\r\n            return String(s == null ? '' : s).replace(\/\\\\\/g, '\\\\\\\\').replace(\/'\/g, \"\\\\'\").replace(\/\\r?\\n\/g, '\\\\n');\r\n        },\r\n\r\n        toast: function(msg, isErr) {\r\n            var t = document.getElementById('pth-tour-toast');\r\n            if (!t) { return; }\r\n            t.innerHTML = msg;\r\n            t.style.background = isErr ? '#991b1b' : '#0f172a';\r\n            t.style.display = 'block';\r\n            if (this.toastTimer) { clearTimeout(this.toastTimer); }\r\n            this.toastTimer = setTimeout(function() { t.style.display = 'none'; }, 3000);\r\n        },\r\n\r\n        copyText: function(v, label) {\r\n            var self = this;\r\n            if (!v) { this.toast('Nothing to copy yet', true); return; }\r\n            if (navigator.clipboard && navigator.clipboard.writeText) {\r\n                navigator.clipboard.writeText(v).then(function() { self.toast(label + ' copied'); })['catch'](function() { self.fallbackCopy(v, label); });\r\n            } else { this.fallbackCopy(v, label); }\r\n        },\r\n\r\n        fallbackCopy: function(v, label) {\r\n            var ta = document.createElement('textarea');\r\n            ta.value = v;\r\n            ta.setAttribute('readonly', 'readonly');\r\n            ta.style.position = 'fixed';\r\n            ta.style.left = '-9999px';\r\n            document.body.appendChild(ta);\r\n            ta.select();\r\n            try { document.execCommand('copy'); this.toast(label + ' copied'); }\r\n            catch (err) { this.toast('Copy failed - select and copy manually', true); }\r\n            document.body.removeChild(ta);\r\n        },\r\n\r\n        switchTab: function(name) {\r\n            var tabs = document.querySelectorAll('#pth-tour-wrapper .pth-tab');\r\n            var panels = document.querySelectorAll('#pth-tour-wrapper .pth-panel');\r\n            var sides = ['build', 'preview', 'widgets', 'export'];\r\n            var i, el;\r\n            for (i = 0; i < tabs.length; i++) {\r\n                tabs[i].classList.remove('active');\r\n                if (tabs[i].getAttribute('data-pth-panel') === name) { tabs[i].classList.add('active'); }\r\n            }\r\n            for (i = 0; i < panels.length; i++) { panels[i].classList.remove('active'); }\r\n            el = document.getElementById('pth-panel-' + name);\r\n            if (el) { el.classList.add('active'); }\r\n            for (i = 0; i < sides.length; i++) {\r\n                el = document.getElementById('pth-side-' + sides[i]);\r\n                if (el) { el.style.display = (sides[i] === name) ? 'block' : 'none'; }\r\n            }\r\n            this.currentTab = name;\r\n            if (name === 'preview') { this.mirrorStage(); }\r\n            if (name === 'export') { this.renderExport(); }\r\n        },\r\n\r\n        toggleWidth: function() {\r\n            var g = document.getElementById('pth-t-grid');\r\n            var s = document.getElementById('pth-t-side');\r\n            var b = document.getElementById('pth-t-fw');\r\n            if (!g || !s || !b) { return; }\r\n            if (g.classList.contains('pth-full-width')) {\r\n                g.classList.remove('pth-full-width');\r\n                s.style.display = 'block';\r\n                b.innerHTML = '\\u26F6 Full Width';\r\n            } else {\r\n                g.classList.add('pth-full-width');\r\n                s.style.display = 'none';\r\n                b.innerHTML = '\\u25E8 Split View';\r\n            }\r\n        },\r\n\r\n        \/* ---------- stage rendering ----------\r\n           Scripts, iframes and event-handler attributes are stripped before the\r\n           markup is inserted. The stage is a preview, not a place to run code. *\/\r\n        sanitize: function(html) {\r\n            var out = String(html);\r\n            out = out.replace(new RegExp(String.fromCharCode(60) + 'script[\\\\s\\\\S]*?' + CT + 'script' + String.fromCharCode(62), 'gi'), '');\r\n            out = out.replace(new RegExp(String.fromCharCode(60) + 'style[\\\\s\\\\S]*?' + CT + 'style' + String.fromCharCode(62), 'gi'), '');\r\n            out = out.replace(new RegExp(String.fromCharCode(60) + '(iframe|object|embed|link|meta|form)[^' + String.fromCharCode(62) + ']*' + String.fromCharCode(62), 'gi'), '');\r\n            out = out.replace(new RegExp(CT + '(iframe|object|embed|form)\\\\s*' + String.fromCharCode(62), 'gi'), '');\r\n            out = out.replace(\/\\shref\\s*=\\s*\"[^\"]*\"\/gi, ' href=\"#\"');\r\n            out = out.replace(\/\\shref\\s*=\\s*'[^']*'\/gi, ' href=\"#\"');\r\n            out = out.replace(\/\\son[a-z]+\\s*=\\s*\"[^\"]*\"\/gi, '');\r\n            out = out.replace(\/\\son[a-z]+\\s*=\\s*'[^']*'\/gi, '');\r\n            out = out.replace(\/javascript:\/gi, '');\r\n            return out;\r\n        },\r\n\r\n        updateHtmlStat: function() {\r\n            var v = this.val('pth-t-html');\r\n            this.setText('pth-t-html-stat', v ? (v.length + ' characters') : 'No markup loaded');\r\n        },\r\n\r\n        renderStage: function() {\r\n            var html = this.val('pth-t-html');\r\n            var stage = document.getElementById('pth-t-stage');\r\n            if (!stage) { return; }\r\n            if (!html || !html.replace(\/\\s\/g, '')) { this.toast('Paste some markup first', true); return; }\r\n            stage.innerHTML = this.sanitize(html);\r\n            this.mirrorStage();\r\n            this.toast('Stage rendered');\r\n        },\r\n\r\n        mirrorStage: function() {\r\n            var a = document.getElementById('pth-t-stage');\r\n            var b = document.getElementById('pth-t-stage2');\r\n            if (a && b && a.innerHTML) { b.innerHTML = a.innerHTML; }\r\n        },\r\n\r\n        setView: function(mode) {\r\n            var s = document.getElementById('pth-t-stage2');\r\n            if (!s) { return; }\r\n            s.className = 'pth-stage' + (mode === 'full' ? '' : ' w-' + mode);\r\n            this.viewMode = mode;\r\n            this.setText('pth-t-view-label', mode === 'full' ? 'Full width' : (mode === 'mobile' ? 'Mobile 380px' : 'Tablet 780px'));\r\n            if (this.playing) { this.showStep(this.playIdx); }\r\n        },\r\n\r\n        \/* ---------- element picking ---------- *\/\r\n        togglePick: function() {\r\n            var stage = document.getElementById('pth-t-stage');\r\n            this.picking = !this.picking;\r\n            if (stage) { stage.className = 'pth-stage' + (this.picking ? ' picking' : ''); }\r\n            this.setText('pth-t-pick-hint', this.picking ? 'Click any element on the stage' : 'Rendered markup appears here');\r\n            this.toast(this.picking ? 'Click an element to capture its selector' : 'Picking cancelled');\r\n        },\r\n\r\n        pickElement: function(el) {\r\n            var sel = this.buildSelector(el);\r\n            this.picking = false;\r\n            var stage = document.getElementById('pth-t-stage');\r\n            if (stage) { stage.className = 'pth-stage'; }\r\n            this.setText('pth-t-pick-hint', 'Rendered markup appears here');\r\n            if (!sel) { this.toast('Could not build a selector for that element', true); return; }\r\n            this.newStep();\r\n            this.setVal('pth-t-f-sel', sel);\r\n            this.setVal('pth-t-f-title', 'Step ' + (this.steps.length + 1));\r\n            this.toast('Captured ' + sel);\r\n        },\r\n\r\n        buildSelector: function(el) {\r\n            if (!el || el.nodeType !== 1) { return ''; }\r\n            if (el.id && el.id.indexOf('pth-') !== 0) { return '#' + el.id; }\r\n            var da = el.getAttribute && el.getAttribute('data-tour');\r\n            if (da) { return '[data-tour=\"' + da + '\"]'; }\r\n            var parts = [];\r\n            var node = el;\r\n            var guard = 0;\r\n            while (node && node.nodeType === 1 && node.id !== 'pth-t-stage' && guard < 6) {\r\n                var part = node.tagName.toLowerCase();\r\n                if (node.id && node.id.indexOf('pth-') !== 0) { parts.unshift('#' + node.id); break; }\r\n                if (node.className && typeof node.className === 'string') {\r\n                    var cls = node.className.replace(\/\\s+\/g, ' ').replace(\/^\\s|\\s$\/g, '').split(' ');\r\n                    var j;\r\n                    for (j = 0; j < cls.length; j++) {\r\n                        if (cls[j] && cls[j].indexOf('pth-') !== 0) { part += '.' + cls[j]; break; }\r\n                    }\r\n                }\r\n                var parent = node.parentNode;\r\n                if (parent && parent.children && parent.children.length > 1) {\r\n                    var k, n = 0, idx = 0;\r\n                    for (k = 0; k < parent.children.length; k++) {\r\n                        if (parent.children[k].tagName === node.tagName) { n++; if (parent.children[k] === node) { idx = n; } }\r\n                    }\r\n                    if (n > 1) { part += ':nth-of-type(' + idx + ')'; }\r\n                }\r\n                parts.unshift(part);\r\n                node = parent;\r\n                guard++;\r\n            }\r\n            return parts.join(' > ');\r\n        },\r\n\r\n        autoDetect: function() {\r\n            var stage = document.getElementById('pth-t-stage');\r\n            if (!stage || !stage.children.length) { this.renderStage(); stage = document.getElementById('pth-t-stage'); }\r\n            if (!stage) { return; }\r\n            var found = stage.querySelectorAll('button, input, select, textarea, a[id], [data-tour]');\r\n            if (!found.length) { this.toast('No interactive elements found in that markup', true); return; }\r\n\r\n            var added = 0;\r\n            var i, el, sel, label;\r\n            for (i = 0; i < found.length && added < 8; i++) {\r\n                el = found[i];\r\n                sel = this.buildSelector(el);\r\n                if (!sel || this.hasSelector(sel)) { continue; }\r\n                label = (el.textContent || '').replace(\/\\s+\/g, ' ').replace(\/^\\s|\\s$\/g, '');\r\n                if (!label) { label = el.getAttribute('placeholder') || el.getAttribute('aria-label') || el.tagName.toLowerCase(); }\r\n                if (label.length > 42) { label = label.substring(0, 42) + '...'; }\r\n                this.steps.push({\r\n                    sel: sel, title: label, text: 'Explain what this does and why it matters.',\r\n                    pos: 'bottom', adv: 'next', scroll: true, optional: false\r\n                });\r\n                added++;\r\n            }\r\n            this.renderSteps();\r\n            this.renderExport();\r\n            this.toast(added ? 'Drafted ' + added + ' steps - edit them down' : 'Those elements are already covered');\r\n        },\r\n\r\n        hasSelector: function(sel) {\r\n            var i;\r\n            for (i = 0; i < this.steps.length; i++) { if (this.steps[i].sel === sel) { return true; } }\r\n            return false;\r\n        },\r\n\r\n        \/* ---------- step CRUD ---------- *\/\r\n        renderSteps: function() {\r\n            var box = document.getElementById('pth-t-steplist');\r\n            if (!box) { return; }\r\n            if (!this.steps.length) {\r\n                box.innerHTML = '<p style=\"padding:16px; color:#94a3b8; font-size:13.5px; margin:0;\">No steps yet. Press Add step, use Pick element, or run Auto-detect.' + CT + 'p>';\r\n                return;\r\n            }\r\n            var html = '';\r\n            var i;\r\n            for (i = 0; i < this.steps.length; i++) {\r\n                var s = this.steps[i];\r\n                html += '<div class=\"pth-step' + (this.editing === i ? ' sel' : '') + '\" data-pth-row=\"step\" data-pth-i=\"' + i + '\">' +\r\n                    '<span class=\"pth-step-n\">' + (i + 1) + CT + 'span>' +\r\n                    '<span class=\"pth-dot ' + (s.state || 'idle') + '\">' + CT + 'span>' +\r\n                    '<div class=\"pth-step-body\">' +\r\n                    '<div class=\"pth-step-t\">' + this.esc(s.title || 'Untitled step') + CT + 'div>' +\r\n                    '<div class=\"pth-step-s\">' + this.esc(s.sel) + CT + 'div>' + CT + 'div>' +\r\n                    '<div class=\"pth-step-acts\">' +\r\n                    '<button type=\"button\" class=\"pth-mini\" data-pth-act=\"up\" data-pth-i=\"' + i + '\" aria-label=\"Move up\">\\u2191' + CT + 'button>' +\r\n                    '<button type=\"button\" class=\"pth-mini\" data-pth-act=\"down\" data-pth-i=\"' + i + '\" aria-label=\"Move down\">\\u2193' + CT + 'button>' +\r\n                    '<button type=\"button\" class=\"pth-mini\" data-pth-act=\"dup\" data-pth-i=\"' + i + '\" aria-label=\"Duplicate\">\\u29C9' + CT + 'button>' +\r\n                    '<button type=\"button\" class=\"pth-mini\" data-pth-act=\"del\" data-pth-i=\"' + i + '\" aria-label=\"Delete\">\\u2715' + CT + 'button>' +\r\n                    CT + 'div>' + CT + 'div>';\r\n            }\r\n            box.innerHTML = html;\r\n        },\r\n\r\n        newStep: function() {\r\n            this.editing = -1;\r\n            this.setVal('pth-t-f-sel', '');\r\n            this.setVal('pth-t-f-title', '');\r\n            this.setVal('pth-t-f-text', '');\r\n            this.setVal('pth-t-f-pos', 'bottom');\r\n            this.setVal('pth-t-f-adv', 'next');\r\n            var a = document.getElementById('pth-t-f-scroll');\r\n            if (a) { a.checked = true; }\r\n            var b = document.getElementById('pth-t-f-optional');\r\n            if (b) { b.checked = false; }\r\n            var ed = document.getElementById('pth-t-editor');\r\n            if (ed) { ed.style.display = 'block'; }\r\n            this.renderSteps();\r\n        },\r\n\r\n        editStep: function(i) {\r\n            var s = this.steps[i];\r\n            if (!s) { return; }\r\n            this.editing = i;\r\n            this.setVal('pth-t-f-sel', s.sel);\r\n            this.setVal('pth-t-f-title', s.title);\r\n            this.setVal('pth-t-f-text', s.text);\r\n            this.setVal('pth-t-f-pos', s.pos);\r\n            this.setVal('pth-t-f-adv', s.adv);\r\n            var a = document.getElementById('pth-t-f-scroll');\r\n            if (a) { a.checked = !!s.scroll; }\r\n            var b = document.getElementById('pth-t-f-optional');\r\n            if (b) { b.checked = !!s.optional; }\r\n            var ed = document.getElementById('pth-t-editor');\r\n            if (ed) { ed.style.display = 'block'; }\r\n            this.renderSteps();\r\n        },\r\n\r\n        saveStep: function() {\r\n            var sel = this.val('pth-t-f-sel').replace(\/^\\s+|\\s+$\/g, '');\r\n            if (!sel) { this.toast('A step needs a CSS selector', true); return; }\r\n            var s = {\r\n                sel: sel,\r\n                title: this.val('pth-t-f-title').replace(\/^\\s+|\\s+$\/g, '') || 'Untitled step',\r\n                text: this.val('pth-t-f-text'),\r\n                pos: this.val('pth-t-f-pos'),\r\n                adv: this.val('pth-t-f-adv'),\r\n                scroll: this.checked('pth-t-f-scroll'),\r\n                optional: this.checked('pth-t-f-optional')\r\n            };\r\n            if (this.editing >= 0 && this.steps[this.editing]) { this.steps[this.editing] = s; }\r\n            else { this.steps.push(s); }\r\n            this.closeEditor();\r\n            this.renderSteps();\r\n            this.renderExport();\r\n            this.toast('Step saved');\r\n        },\r\n\r\n        closeEditor: function() {\r\n            this.editing = -1;\r\n            var ed = document.getElementById('pth-t-editor');\r\n            if (ed) { ed.style.display = 'none'; }\r\n            this.renderSteps();\r\n        },\r\n\r\n        moveStep: function(i, d) {\r\n            var j = i + d;\r\n            if (j < 0 || j >= this.steps.length) { return; }\r\n            var t = this.steps[i];\r\n            this.steps[i] = this.steps[j];\r\n            this.steps[j] = t;\r\n            if (this.editing === i) { this.editing = j; }\r\n            this.renderSteps();\r\n            this.renderExport();\r\n        },\r\n\r\n        dupStep: function(i) {\r\n            var s = this.steps[i];\r\n            if (!s) { return; }\r\n            var c = { sel: s.sel, title: s.title + ' (copy)', text: s.text, pos: s.pos, adv: s.adv, scroll: s.scroll, optional: s.optional };\r\n            this.steps.splice(i + 1, 0, c);\r\n            this.renderSteps();\r\n            this.renderExport();\r\n            this.toast('Step duplicated');\r\n        },\r\n\r\n        delStep: function(i) {\r\n            this.steps.splice(i, 1);\r\n            if (this.editing === i) { this.closeEditor(); }\r\n            this.renderSteps();\r\n            this.renderExport();\r\n            this.toast('Step removed');\r\n        },\r\n\r\n        \/* ---------- selector health ---------- *\/\r\n        runHealth: function() {\r\n            var stage = document.getElementById('pth-t-stage2');\r\n            var body = document.getElementById('pth-t-health-rows');\r\n            var note = document.getElementById('pth-t-health-note');\r\n            if (!body) { return; }\r\n\r\n            if (!this.steps.length) {\r\n                body.innerHTML = '<tr><td colspan=\"4\" style=\"text-align:center; color:#94a3b8; padding:22px;\">No steps to check yet.' + CT + 'td>' + CT + 'tr>';\r\n                return;\r\n            }\r\n            if (!stage || !stage.children.length) {\r\n                if (note) { note.className = 'pth-note warn'; note.innerHTML = 'Render your markup on the Build tab first, otherwise there is nothing to match selectors against.'; }\r\n                return;\r\n            }\r\n\r\n            var rows = '';\r\n            var bad = 0, warn = 0;\r\n            var i;\r\n            for (i = 0; i < this.steps.length; i++) {\r\n                var s = this.steps[i];\r\n                var n = 0;\r\n                var verdict, cls;\r\n                try { n = stage.querySelectorAll(s.sel).length; }\r\n                catch (err) { n = -1; }\r\n\r\n                if (n === -1) { verdict = 'Invalid'; cls = 'bad'; bad++; s.state = 'bad'; }\r\n                else if (n === 0) { verdict = s.optional ? 'Skipped' : 'No match'; cls = s.optional ? 'warn' : 'bad'; if (!s.optional) { bad++; } s.state = s.optional ? 'idle' : 'bad'; }\r\n                else if (n === 1) { verdict = 'Good'; cls = 'ok'; s.state = 'ok'; }\r\n                else { verdict = 'Ambiguous'; cls = 'warn'; warn++; s.state = 'ok'; }\r\n\r\n                rows += '<tr><td>' + (i + 1) + CT + 'td>' +\r\n                    '<td style=\"font-family:Consolas,monospace;\">' + this.esc(s.sel) + CT + 'td>' +\r\n                    '<td style=\"font-family:Consolas,monospace;\">' + (n < 0 ? '-' : n) + CT + 'td>' +\r\n                    '<td><span class=\"pth-pill ' + cls + '\">' + verdict + CT + 'span>' + CT + 'td>' + CT + 'tr>';\r\n            }\r\n            body.innerHTML = rows;\r\n            this.renderSteps();\r\n\r\n            if (note) {\r\n                if (bad) {\r\n                    note.className = 'pth-note err';\r\n                    note.innerHTML = bad + ' step' + (bad === 1 ? '' : 's') + ' would fail against this markup. A selector that matches nothing skips silently, so the tour appears to work while quietly missing steps.';\r\n                } else if (warn) {\r\n                    note.className = 'pth-note warn';\r\n                    note.innerHTML = 'Every step matches, but ' + warn + ' selector' + (warn === 1 ? ' matches' : 's match') + ' more than one element. The tour will highlight the first match, which may not be the one you meant.';\r\n                } else {\r\n                    note.className = 'pth-note ok';\r\n                    note.innerHTML = 'All ' + this.steps.length + ' selectors match exactly one element. This tour is safe to ship against this markup.';\r\n                }\r\n            }\r\n        },\r\n\r\n        \/* ---------- playback ---------- *\/\r\n        playTour: function() {\r\n            var stage = document.getElementById('pth-t-stage2');\r\n            if (!this.steps.length) { this.toast('Add a step first', true); return; }\r\n            if (!stage || !stage.children.length) { this.toast('Render your markup on the Build tab first', true); return; }\r\n            this.playing = true;\r\n            this.playIdx = 0;\r\n            this.showStep(0);\r\n        },\r\n\r\n        stopTour: function() {\r\n            this.playing = false;\r\n            this.clearOverlay();\r\n            this.setText('pth-t-play-stat', 'Not running');\r\n            this.setText('pth-t-step-label', 'No tour running');\r\n        },\r\n\r\n        movePlay: function(d) {\r\n            if (!this.playing) { this.toast('Start the tour first', true); return; }\r\n            var n = this.playIdx + d;\r\n            if (n < 0) { return; }\r\n            if (n >= this.steps.length) { this.stopTour(); this.toast('Tour finished'); return; }\r\n            this.playIdx = n;\r\n            this.showStep(n);\r\n        },\r\n\r\n        clearOverlay: function() {\r\n            var stage = document.getElementById('pth-t-stage2');\r\n            if (!stage) { return; }\r\n            var old = stage.querySelectorAll('.pth-spot, .pth-bubble');\r\n            var i;\r\n            for (i = 0; i < old.length; i++) { old[i].parentNode.removeChild(old[i]); }\r\n        },\r\n\r\n        showStep: function(i) {\r\n            var stage = document.getElementById('pth-t-stage2');\r\n            var s = this.steps[i];\r\n            if (!stage || !s) { return; }\r\n            this.clearOverlay();\r\n\r\n            var target = null;\r\n            try { target = stage.querySelector(s.sel); } catch (err) { target = null; }\r\n\r\n            if (!target) {\r\n                if (s.optional) { this.movePlay(1); return; }\r\n                this.setText('pth-t-play-stat', 'Step ' + (i + 1) + ' has no match');\r\n                this.setText('pth-t-step-label', 'Selector did not match');\r\n                this.toast('Step ' + (i + 1) + ': nothing matches ' + s.sel, true);\r\n                return;\r\n            }\r\n\r\n            var sr = stage.getBoundingClientRect();\r\n            var tr = target.getBoundingClientRect();\r\n            var top = tr.top - sr.top + stage.scrollTop;\r\n            var left = tr.left - sr.left + stage.scrollLeft;\r\n\r\n            var spot = document.createElement('div');\r\n            spot.className = 'pth-spot';\r\n            spot.style.top = (top - 6) + 'px';\r\n            spot.style.left = (left - 6) + 'px';\r\n            spot.style.width = (tr.width + 12) + 'px';\r\n            spot.style.height = (tr.height + 12) + 'px';\r\n            stage.appendChild(spot);\r\n\r\n            var bub = document.createElement('div');\r\n            bub.className = 'pth-bubble';\r\n            bub.innerHTML = '<p class=\"pth-bubble-t\">' + this.esc(s.title) + CT + 'p>' +\r\n                '<p class=\"pth-bubble-x\">' + this.esc(s.text) + CT + 'p>' +\r\n                '<div class=\"pth-bubble-f\"><span class=\"pth-bubble-c\">Step ' + (i + 1) + ' of ' + this.steps.length + CT + 'span>' +\r\n                '<span class=\"pth-bubble-c\">' + (s.adv === 'next' ? 'Press Next' : (s.adv === 'click' ? 'Click the element' : 'Fill the field')) + CT + 'span>' + CT + 'div>';\r\n            stage.appendChild(bub);\r\n\r\n            var bw = bub.offsetWidth || 260;\r\n            var bh = bub.offsetHeight || 110;\r\n            var bt = top + tr.height + 14;\r\n            var bl = left;\r\n\r\n            if (s.pos === 'top') { bt = top - bh - 14; }\r\n            else if (s.pos === 'right') { bt = top; bl = left + tr.width + 14; }\r\n            else if (s.pos === 'left') { bt = top; bl = left - bw - 14; }\r\n\r\n            if (bl < 6) { bl = 6; }\r\n            if (bt < 6) { bt = top + tr.height + 14; }\r\n            if (bl + bw > stage.clientWidth - 6) { bl = stage.clientWidth - bw - 6; if (bl < 6) { bl = 6; } }\r\n\r\n            bub.style.top = bt + 'px';\r\n            bub.style.left = bl + 'px';\r\n\r\n            if (s.scroll && target.scrollIntoView) {\r\n                try { target.scrollIntoView({ block: 'nearest' }); } catch (err) { target.scrollIntoView(); }\r\n            }\r\n\r\n            this.setText('pth-t-play-stat', 'Step ' + (i + 1) + ' of ' + this.steps.length);\r\n            this.setText('pth-t-step-label', this.esc(s.title));\r\n        },\r\n\r\n        \/* ---------- widgets ---------- *\/\r\n        addHotspot: function() {\r\n            this.hotspots.push({ sel: '#app-upload', title: 'New hotspot', text: 'Explain the feature this marker points at.' });\r\n            this.renderHotspots();\r\n            this.renderExport();\r\n        },\r\n\r\n        renderHotspots: function() {\r\n            var box = document.getElementById('pth-t-hs-list');\r\n            if (!box) { return; }\r\n            if (!this.hotspots.length) {\r\n                box.innerHTML = '<p style=\"padding:14px; color:#94a3b8; font-size:13.5px; margin:0;\">No hotspots yet.' + CT + 'p>';\r\n                return;\r\n            }\r\n            var html = '';\r\n            var i;\r\n            for (i = 0; i < this.hotspots.length; i++) {\r\n                var h = this.hotspots[i];\r\n                html += '<div class=\"pth-step\">' +\r\n                    '<span class=\"pth-step-n\" style=\"background:#7c3aed;\">\\u2022' + CT + 'span>' +\r\n                    '<div class=\"pth-step-body\">' +\r\n                    '<input type=\"text\" class=\"pth-inp\" style=\"padding:5px 8px; font-size:12.5px; margin-bottom:4px;\" value=\"' + this.esc(h.title) + '\" data-pth-hs=\"title\" data-pth-i=\"' + i + '\" aria-label=\"Hotspot title\">' +\r\n                    '<input type=\"text\" class=\"pth-inp\" style=\"padding:5px 8px; font-size:12px; font-family:Consolas,monospace;\" value=\"' + this.esc(h.sel) + '\" data-pth-hs=\"sel\" data-pth-i=\"' + i + '\" aria-label=\"Hotspot selector\">' +\r\n                    CT + 'div>' +\r\n                    '<div class=\"pth-step-acts\"><button type=\"button\" class=\"pth-mini\" data-pth-act=\"hsdel\" data-pth-i=\"' + i + '\" aria-label=\"Delete hotspot\">\\u2715' + CT + 'button>' + CT + 'div>' +\r\n                    CT + 'div>';\r\n            }\r\n            box.innerHTML = html;\r\n        },\r\n\r\n        addTask: function() {\r\n            this.tasks.push({ text: 'New task', done: false });\r\n            this.renderTasks();\r\n            this.renderExport();\r\n        },\r\n\r\n        renderTasks: function() {\r\n            var box = document.getElementById('pth-t-cl-list');\r\n            var pv = document.getElementById('pth-t-cl-preview');\r\n            var i, html;\r\n\r\n            if (box) {\r\n                if (!this.tasks.length) {\r\n                    box.innerHTML = '<p style=\"padding:14px; color:#94a3b8; font-size:13.5px; margin:0;\">No tasks yet.' + CT + 'p>';\r\n                } else {\r\n                    html = '';\r\n                    for (i = 0; i < this.tasks.length; i++) {\r\n                        html += '<div class=\"pth-step\">' +\r\n                            '<button type=\"button\" class=\"pth-mini\" data-pth-act=\"tktog\" data-pth-i=\"' + i + '\" aria-label=\"Toggle done\">' + (this.tasks[i].done ? '\\u2713' : '\\u25CB') + CT + 'button>' +\r\n                            '<div class=\"pth-step-body\"><input type=\"text\" class=\"pth-inp\" style=\"padding:5px 8px; font-size:12.5px;\" value=\"' + this.esc(this.tasks[i].text) + '\" data-pth-tk=\"text\" data-pth-i=\"' + i + '\" aria-label=\"Task text\">' + CT + 'div>' +\r\n                            '<div class=\"pth-step-acts\"><button type=\"button\" class=\"pth-mini\" data-pth-act=\"tkdel\" data-pth-i=\"' + i + '\" aria-label=\"Delete task\">\\u2715' + CT + 'button>' + CT + 'div>' +\r\n                            CT + 'div>';\r\n                    }\r\n                    box.innerHTML = html;\r\n                }\r\n            }\r\n\r\n            if (pv) {\r\n                var done = 0;\r\n                for (i = 0; i < this.tasks.length; i++) { if (this.tasks[i].done) { done++; } }\r\n                var pct = this.tasks.length ? Math.round((done \/ this.tasks.length) * 100) : 0;\r\n                html = '<div class=\"pth-cl-head\"><span class=\"pth-cl-title\">' + this.esc(this.val('pth-t-cl-title') || 'Getting started') + CT + 'span>' +\r\n                    '<span style=\"font-size:12px; font-weight:700; color:#64748b;\">' + done + ' of ' + this.tasks.length + CT + 'span>' + CT + 'div>' +\r\n                    '<div class=\"pth-cl-prog\"><div class=\"pth-cl-fill\" style=\"width:' + pct + '%;\">' + CT + 'div>' + CT + 'div>';\r\n                for (i = 0; i < this.tasks.length; i++) {\r\n                    html += '<div class=\"pth-cl-item' + (this.tasks[i].done ? ' done' : '') + '\">' +\r\n                        '<span class=\"pth-cl-tick\">' + (this.tasks[i].done ? '\\u2713' : '') + CT + 'span>' +\r\n                        '<span>' + this.esc(this.tasks[i].text) + CT + 'span>' + CT + 'div>';\r\n                }\r\n                pv.innerHTML = html || '<p style=\"color:#94a3b8; font-size:13px; margin:0;\">Add a task to see the widget.' + CT + 'p>';\r\n            }\r\n        },\r\n\r\n        renderBanner: function() {\r\n            var pv = document.getElementById('pth-t-bn-preview');\r\n            if (!pv) { return; }\r\n            var style = this.val('pth-t-bn-style') || 'info';\r\n            pv.innerHTML = '<div class=\"pth-banner ' + style + '\"><span style=\"font-size:19px;\">' + this.esc(this.val('pth-t-bn-icon') || '\\uD83D\\uDCE3') + CT + 'span>' +\r\n                '<span style=\"flex:1;\">' + this.esc(this.val('pth-t-bn-text')) + CT + 'span>' +\r\n                '<span style=\"font-size:17px; cursor:pointer; opacity:0.5;\">\\u2715' + CT + 'span>' + CT + 'div>';\r\n        },\r\n\r\n        collectWidgetEdits: function() {\r\n            var i, el;\r\n            var hs = document.querySelectorAll('#pth-t-hs-list [data-pth-hs]');\r\n            for (i = 0; i < hs.length; i++) {\r\n                el = hs[i];\r\n                var hi = parseInt(el.getAttribute('data-pth-i'), 10);\r\n                if (this.hotspots[hi]) { this.hotspots[hi][el.getAttribute('data-pth-hs')] = el.value; }\r\n            }\r\n            var tk = document.querySelectorAll('#pth-t-cl-list [data-pth-tk]');\r\n            for (i = 0; i < tk.length; i++) {\r\n                el = tk[i];\r\n                var ti = parseInt(el.getAttribute('data-pth-i'), 10);\r\n                if (this.tasks[ti]) { this.tasks[ti].text = el.value; }\r\n            }\r\n        },\r\n\r\n        \/* ---------- export ---------- *\/\r\n        tourObject: function() {\r\n            this.collectWidgetEdits();\r\n            return {\r\n                version: 1,\r\n                steps: this.steps,\r\n                hotspots: this.hotspots,\r\n                checklist: { title: this.val('pth-t-cl-title') || 'Getting started', tasks: this.tasks },\r\n                banner: { text: this.val('pth-t-bn-text'), icon: this.val('pth-t-bn-icon'), style: this.val('pth-t-bn-style') },\r\n                options: { analytics: this.checked('pth-t-opt-analytics'), rememberDismissal: this.checked('pth-t-opt-once') }\r\n            };\r\n        },\r\n\r\n        renderExport: function() {\r\n            var fmt = this.val('pth-t-fmt') || 'json';\r\n            var out = document.getElementById('pth-t-out');\r\n            var note = document.getElementById('pth-t-fmt-note');\r\n            if (!out) { return; }\r\n\r\n            var obj = this.tourObject();\r\n            var code = '';\r\n            var msg = '';\r\n\r\n            if (fmt === 'json') {\r\n                code = JSON.stringify(obj, null, 2);\r\n                msg = 'The tour definition on its own. Feed it to your own player, or import it back here later.';\r\n            } else if (fmt === 'js') {\r\n                code = this.buildStandalone(obj);\r\n                msg = 'Self-contained: markup, styles and logic with no dependencies. Paste it before the closing body tag of your page.';\r\n            } else if (fmt === 'react') {\r\n                code = this.buildReact(obj);\r\n                msg = 'A single component with the steps inlined. Drop the file into your project and render it once, high in the tree.';\r\n            } else if (fmt === 'vue') {\r\n                code = this.buildVue(obj);\r\n                msg = 'A Vue 3 single-file component using the composition API.';\r\n            } else {\r\n                code = this.buildWordPress(obj);\r\n                msg = 'Register this in your theme functions file or a code-snippet plugin, then place the shortcode on any page.';\r\n            }\r\n\r\n            out.value = code;\r\n            if (note) { note.innerHTML = msg; }\r\n        },\r\n\r\n        buildStandalone: function(obj) {\r\n            var self = this;\r\n            var lines = [];\r\n            var i;\r\n\r\n            lines.push('(function () {');\r\n            lines.push(\"  'use strict';\");\r\n            lines.push('  var STEPS = [');\r\n            for (i = 0; i < obj.steps.length; i++) {\r\n                var s = obj.steps[i];\r\n                lines.push(\"    { sel: '\" + self.jsStr(s.sel) + \"', title: '\" + self.jsStr(s.title) + \"', text: '\" + self.jsStr(s.text) +\r\n                    \"', pos: '\" + s.pos + \"', adv: '\" + s.adv + \"', scroll: \" + (s.scroll ? 'true' : 'false') + ', optional: ' + (s.optional ? 'true' : 'false') + ' }' +\r\n                    (i < obj.steps.length - 1 ? ',' : ''));\r\n            }\r\n            lines.push('  ];');\r\n            lines.push('  if (!STEPS.length) { return; }');\r\n            lines.push('');\r\n            if (obj.options.rememberDismissal) {\r\n                lines.push('  var KEY = \\'tour-done-v1\\';');\r\n                lines.push('  try { if (window.localStorage.getItem(KEY)) { return; } } catch (e) {}');\r\n                lines.push('');\r\n            }\r\n            lines.push('  var idx = 0, spot = null, bubble = null;');\r\n            lines.push('');\r\n            lines.push('  var css = \\'.tour-spot{position:absolute;border-radius:8px;box-shadow:0 0 0 9999px rgba(15,23,42,.62);pointer-events:none;z-index:99998;transition:all .28s ease}\\'');\r\n            lines.push('    + \\'.tour-bubble{position:absolute;z-index:99999;background:#fff;border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.2);padding:16px;width:280px;font-family:system-ui,sans-serif}\\'');\r\n            lines.push('    + \\'.tour-bubble h4{margin:0 0 6px;font-size:15px;color:#0f172a}\\'');\r\n            lines.push('    + \\'.tour-bubble p{margin:0 0 12px;font-size:13px;color:#475569;line-height:1.55}\\'');\r\n            lines.push('    + \\'.tour-bubble button{border:none;border-radius:6px;padding:7px 14px;font-size:13px;font-weight:700;cursor:pointer;margin-left:6px}\\';');\r\n            lines.push('  var st = document.createElement(\\'style\\'); st.appendChild(document.createTextNode(css)); document.head.appendChild(st);');\r\n            lines.push('');\r\n            if (obj.options.analytics) {\r\n                lines.push('  function track(action, step) {');\r\n                lines.push('    try {');\r\n                lines.push('      document.dispatchEvent(new CustomEvent(\\'tour\\', { detail: { action: action, step: step } }));');\r\n                lines.push('      if (window.dataLayer && window.dataLayer.push) { window.dataLayer.push({ event: \\'tour_\\' + action, tour_step: step }); }');\r\n                lines.push('    } catch (e) {}');\r\n                lines.push('  }');\r\n                lines.push('');\r\n            } else {\r\n                lines.push('  function track() {}');\r\n                lines.push('');\r\n            }\r\n            lines.push('  function clear() {');\r\n            lines.push('    if (spot && spot.parentNode) { spot.parentNode.removeChild(spot); }');\r\n            lines.push('    if (bubble && bubble.parentNode) { bubble.parentNode.removeChild(bubble); }');\r\n            lines.push('    spot = null; bubble = null;');\r\n            lines.push('  }');\r\n            lines.push('');\r\n            lines.push('  function finish() { clear(); track(\\'complete\\', idx);');\r\n            if (obj.options.rememberDismissal) {\r\n                lines.push('    try { window.localStorage.setItem(KEY, \\'1\\'); } catch (e) {}');\r\n            }\r\n            lines.push('  }');\r\n            lines.push('');\r\n            lines.push('  function show(i) {');\r\n            lines.push('    clear();');\r\n            lines.push('    if (i < 0 || i >= STEPS.length) { finish(); return; }');\r\n            lines.push('    var s = STEPS[i], el = null;');\r\n            lines.push('    try { el = document.querySelector(s.sel); } catch (e) { el = null; }');\r\n            lines.push('    if (!el) { track(\\'missing\\', i); show(i + 1); return; }');\r\n            lines.push('    idx = i;');\r\n            lines.push('    if (s.scroll && el.scrollIntoView) { try { el.scrollIntoView({ block: \\'center\\', behavior: \\'smooth\\' }); } catch (e) { el.scrollIntoView(); } }');\r\n            lines.push('');\r\n            lines.push('    var r = el.getBoundingClientRect();');\r\n            lines.push('    var top = r.top + window.pageYOffset, left = r.left + window.pageXOffset;');\r\n            lines.push('');\r\n            lines.push('    spot = document.createElement(\\'div\\');');\r\n            lines.push('    spot.className = \\'tour-spot\\';');\r\n            lines.push('    spot.style.top = (top - 6) + \\'px\\'; spot.style.left = (left - 6) + \\'px\\';');\r\n            lines.push('    spot.style.width = (r.width + 12) + \\'px\\'; spot.style.height = (r.height + 12) + \\'px\\';');\r\n            lines.push('    document.body.appendChild(spot);');\r\n            lines.push('');\r\n            lines.push('    bubble = document.createElement(\\'div\\');');\r\n            lines.push('    bubble.className = \\'tour-bubble\\';');\r\n            lines.push('    var h = document.createElement(\\'h4\\'); h.appendChild(document.createTextNode(s.title));');\r\n            lines.push('    var p = document.createElement(\\'p\\'); p.appendChild(document.createTextNode(s.text));');\r\n            lines.push('    var foot = document.createElement(\\'div\\');');\r\n            lines.push('    foot.style.cssText = \\'display:flex;justify-content:space-between;align-items:center\\';');\r\n            lines.push('    var count = document.createElement(\\'span\\');');\r\n            lines.push('    count.style.cssText = \\'font-size:12px;color:#64748b;font-weight:700\\';');\r\n            lines.push('    count.appendChild(document.createTextNode((i + 1) + \\' of \\' + STEPS.length));');\r\n            lines.push('    var acts = document.createElement(\\'div\\');');\r\n            lines.push('    var skip = document.createElement(\\'button\\');');\r\n            lines.push('    skip.style.cssText = \\'background:#f1f5f9;color:#475569\\';');\r\n            lines.push('    skip.appendChild(document.createTextNode(\\'Skip\\'));');\r\n            lines.push('    skip.onclick = function () { track(\\'skip\\', i); finish(); };');\r\n            lines.push('    var next = document.createElement(\\'button\\');');\r\n            lines.push('    next.style.cssText = \\'background:#1d4ed8;color:#fff\\';');\r\n            lines.push('    next.appendChild(document.createTextNode(i === STEPS.length - 1 ? \\'Done\\' : \\'Next\\'));');\r\n            lines.push('    next.onclick = function () { show(i + 1); };');\r\n            lines.push('    acts.appendChild(skip); acts.appendChild(next);');\r\n            lines.push('    foot.appendChild(count); foot.appendChild(acts);');\r\n            lines.push('    bubble.appendChild(h); bubble.appendChild(p); bubble.appendChild(foot);');\r\n            lines.push('    document.body.appendChild(bubble);');\r\n            lines.push('');\r\n            lines.push('    var bw = bubble.offsetWidth, bh = bubble.offsetHeight;');\r\n            lines.push('    var bt = top + r.height + 14, bl = left;');\r\n            lines.push('    if (s.pos === \\'top\\') { bt = top - bh - 14; }');\r\n            lines.push('    else if (s.pos === \\'right\\') { bt = top; bl = left + r.width + 14; }');\r\n            lines.push('    else if (s.pos === \\'left\\') { bt = top; bl = left - bw - 14; }');\r\n            lines.push('    if (bl < 8) { bl = 8; }');\r\n            lines.push('    if (bl + bw > document.documentElement.clientWidth - 8) { bl = document.documentElement.clientWidth - bw - 8; }');\r\n            lines.push('    if (bt < 8) { bt = top + r.height + 14; }');\r\n            lines.push('    bubble.style.top = bt + \\'px\\'; bubble.style.left = bl + \\'px\\';');\r\n            lines.push('');\r\n            lines.push('    track(\\'show\\', i);');\r\n            lines.push('    if (s.adv === \\'click\\') { el.addEventListener(\\'click\\', function once() { el.removeEventListener(\\'click\\', once); show(i + 1); }); }');\r\n            lines.push('    else if (s.adv === \\'input\\') { el.addEventListener(\\'change\\', function once() { el.removeEventListener(\\'change\\', once); show(i + 1); }); }');\r\n            lines.push('  }');\r\n            lines.push('');\r\n            lines.push('  document.addEventListener(\\'keydown\\', function (e) {');\r\n            lines.push('    if (!bubble) { return; }');\r\n            lines.push('    if (e.key === \\'Escape\\') { track(\\'skip\\', idx); finish(); }');\r\n            lines.push('    else if (e.key === \\'ArrowRight\\') { show(idx + 1); }');\r\n            lines.push('    else if (e.key === \\'ArrowLeft\\') { show(idx - 1); }');\r\n            lines.push('  });');\r\n            lines.push('');\r\n            lines.push('  window.addEventListener(\\'resize\\', function () { if (bubble) { show(idx); } });');\r\n            lines.push('');\r\n            lines.push('  if (document.readyState === \\'loading\\') {');\r\n            lines.push('    document.addEventListener(\\'DOMContentLoaded\\', function () { show(0); });');\r\n            lines.push('  } else { show(0); }');\r\n            lines.push('})();');\r\n\r\n            return lines.join('\\n');\r\n        },\r\n\r\n        buildReact: function(obj) {\r\n            var self = this;\r\n            var lines = [];\r\n            var i;\r\n            lines.push(\"import { useState, useEffect, useRef } from 'react';\");\r\n            lines.push('');\r\n            lines.push('const STEPS = [');\r\n            for (i = 0; i < obj.steps.length; i++) {\r\n                var s = obj.steps[i];\r\n                lines.push(\"  { sel: '\" + self.jsStr(s.sel) + \"', title: '\" + self.jsStr(s.title) + \"', text: '\" + self.jsStr(s.text) + \"', pos: '\" + s.pos + \"' }\" + (i < obj.steps.length - 1 ? ',' : ''));\r\n            }\r\n            lines.push('];');\r\n            lines.push('');\r\n            lines.push('export default function ProductTour({ onFinish }) {');\r\n            lines.push('  const [idx, setIdx] = useState(0);');\r\n            lines.push('  const [rect, setRect] = useState(null);');\r\n            lines.push('  const [open, setOpen] = useState(true);');\r\n            lines.push('  const bubbleRef = useRef(null);');\r\n            lines.push('');\r\n            lines.push('  useEffect(() => {');\r\n            lines.push('    if (!open) return;');\r\n            lines.push('    const step = STEPS[idx];');\r\n            lines.push('    if (!step) { setOpen(false); onFinish && onFinish(); return; }');\r\n            lines.push('    const el = document.querySelector(step.sel);');\r\n            lines.push('    if (!el) { setIdx(i => i + 1); return; }');\r\n            lines.push(\"    el.scrollIntoView({ block: 'center', behavior: 'smooth' });\");\r\n            lines.push('    const update = () => setRect(el.getBoundingClientRect());');\r\n            lines.push('    update();');\r\n            lines.push(\"    window.addEventListener('resize', update);\");\r\n            lines.push(\"    window.addEventListener('scroll', update, true);\");\r\n            lines.push('    return () => {');\r\n            lines.push(\"      window.removeEventListener('resize', update);\");\r\n            lines.push(\"      window.removeEventListener('scroll', update, true);\");\r\n            lines.push('    };');\r\n            lines.push('  }, [idx, open, onFinish]);');\r\n            lines.push('');\r\n            lines.push('  if (!open || !rect) return null;');\r\n            lines.push('  const step = STEPS[idx];');\r\n            lines.push('  const last = idx === STEPS.length - 1;');\r\n            lines.push('');\r\n            lines.push('  const spot = {');\r\n            lines.push(\"    position: 'fixed', top: rect.top - 6, left: rect.left - 6,\");\r\n            lines.push('    width: rect.width + 12, height: rect.height + 12,');\r\n            lines.push(\"    borderRadius: 8, boxShadow: '0 0 0 9999px rgba(15,23,42,.62)',\");\r\n            lines.push(\"    pointerEvents: 'none', zIndex: 99998, transition: 'all .28s ease'\");\r\n            lines.push('  };');\r\n            lines.push('  const bubble = {');\r\n            lines.push(\"    position: 'fixed', zIndex: 99999, width: 280, background: '#fff',\");\r\n            lines.push(\"    borderRadius: 10, padding: 16, boxShadow: '0 10px 30px rgba(0,0,0,.2)',\");\r\n            lines.push(\"    top: step.pos === 'top' ? rect.top - 150 : rect.bottom + 14,\");\r\n            lines.push('    left: Math.max(8, Math.min(rect.left, window.innerWidth - 296))');\r\n            lines.push('  };');\r\n            lines.push('');\r\n            lines.push('  return (');\r\n            lines.push('    <>');\r\n            lines.push('      <div style={spot} \/>');\r\n            lines.push('      <div style={bubble} ref={bubbleRef}>');\r\n            lines.push(\"        <h4 style={{ margin: '0 0 6px', fontSize: 15 }}>{step.title}<' + '\/h4>\");\r\n            lines.push(\"        <p style={{ margin: '0 0 12px', fontSize: 13, color: '#475569' }}>{step.text}<' + '\/p>\");\r\n            lines.push(\"        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>\");\r\n            lines.push(\"          <span style={{ fontSize: 12, color: '#64748b', fontWeight: 700 }}>{idx + 1} of {STEPS.length}<' + '\/span>\");\r\n            lines.push('          <div>');\r\n            lines.push('            <button onClick={() => { setOpen(false); onFinish && onFinish(); }}>Skip<' + '\/button>');\r\n            lines.push(\"            <button style={{ marginLeft: 6, background: '#1d4ed8', color: '#fff' }}\");\r\n            lines.push('              onClick={() => last ? (setOpen(false), onFinish && onFinish()) : setIdx(idx + 1)}>');\r\n            lines.push(\"              {last ? 'Done' : 'Next'}\");\r\n            lines.push('            <' + '\/button>');\r\n            lines.push('          <' + '\/div>');\r\n            lines.push('        <' + '\/div>');\r\n            lines.push('      <' + '\/div>');\r\n            lines.push('    <' + '\/>');\r\n            lines.push('  );');\r\n            lines.push('}');\r\n            return lines.join('\\n');\r\n        },\r\n\r\n        buildVue: function(obj) {\r\n            var self = this;\r\n            var lines = [];\r\n            var i;\r\n            lines.push(String.fromCharCode(60) + 'script setup' + String.fromCharCode(62));\r\n            lines.push(\"import { ref, watch, onMounted, onUnmounted } from 'vue';\");\r\n            lines.push('');\r\n            lines.push('const STEPS = [');\r\n            for (i = 0; i < obj.steps.length; i++) {\r\n                var s = obj.steps[i];\r\n                lines.push(\"  { sel: '\" + self.jsStr(s.sel) + \"', title: '\" + self.jsStr(s.title) + \"', text: '\" + self.jsStr(s.text) + \"', pos: '\" + s.pos + \"' }\" + (i < obj.steps.length - 1 ? ',' : ''));\r\n            }\r\n            lines.push('];');\r\n            lines.push('');\r\n            lines.push('const idx = ref(0);');\r\n            lines.push('const rect = ref(null);');\r\n            lines.push('const open = ref(true);');\r\n            lines.push('');\r\n            lines.push('function locate() {');\r\n            lines.push('  const step = STEPS[idx.value];');\r\n            lines.push('  if (!step) { open.value = false; return; }');\r\n            lines.push('  const el = document.querySelector(step.sel);');\r\n            lines.push('  if (!el) { idx.value += 1; return; }');\r\n            lines.push(\"  el.scrollIntoView({ block: 'center', behavior: 'smooth' });\");\r\n            lines.push('  rect.value = el.getBoundingClientRect();');\r\n            lines.push('}');\r\n            lines.push('');\r\n            lines.push('function next() {');\r\n            lines.push('  if (idx.value >= STEPS.length - 1) { open.value = false; return; }');\r\n            lines.push('  idx.value += 1;');\r\n            lines.push('}');\r\n            lines.push('');\r\n            lines.push('watch(idx, locate);');\r\n            lines.push('onMounted(() => { locate(); window.addEventListener(\\'resize\\', locate); });');\r\n            lines.push('onUnmounted(() => window.removeEventListener(\\'resize\\', locate));');\r\n            lines.push(CT + 'script' + String.fromCharCode(62));\r\n            lines.push('');\r\n            lines.push(String.fromCharCode(60) + 'template' + String.fromCharCode(62));\r\n            lines.push('  ' + String.fromCharCode(60) + 'div v-if=\"open && rect\"' + String.fromCharCode(62));\r\n            lines.push('    ' + String.fromCharCode(60) + 'div class=\"tour-spot\" :style=\"{');\r\n            lines.push(\"      position: 'fixed', top: (rect.top - 6) + 'px', left: (rect.left - 6) + 'px',\");\r\n            lines.push(\"      width: (rect.width + 12) + 'px', height: (rect.height + 12) + 'px',\");\r\n            lines.push(\"      borderRadius: '8px', boxShadow: '0 0 0 9999px rgba(15,23,42,.62)',\");\r\n            lines.push(\"      pointerEvents: 'none', zIndex: 99998\");\r\n            lines.push('    }\" ' + CT + 'div' + String.fromCharCode(62));\r\n            lines.push('    ' + String.fromCharCode(60) + 'div class=\"tour-bubble\" :style=\"{');\r\n            lines.push(\"      position: 'fixed', zIndex: 99999, width: '280px', background: '#fff',\");\r\n            lines.push(\"      borderRadius: '10px', padding: '16px', boxShadow: '0 10px 30px rgba(0,0,0,.2)',\");\r\n            lines.push(\"      top: (rect.bottom + 14) + 'px', left: Math.max(8, rect.left) + 'px'\");\r\n            lines.push('    }\"' + String.fromCharCode(62));\r\n            lines.push('      ' + String.fromCharCode(60) + 'h4' + String.fromCharCode(62) + '{{ STEPS[idx].title }}' + CT + 'h4' + String.fromCharCode(62));\r\n            lines.push('      ' + String.fromCharCode(60) + 'p' + String.fromCharCode(62) + '{{ STEPS[idx].text }}' + CT + 'p' + String.fromCharCode(62));\r\n            lines.push('      ' + String.fromCharCode(60) + 'button @click=\"open = false\"' + String.fromCharCode(62) + 'Skip' + CT + 'button' + String.fromCharCode(62));\r\n            lines.push('      ' + String.fromCharCode(60) + 'button @click=\"next\"' + String.fromCharCode(62) + '{{ idx === STEPS.length - 1 ? \\'Done\\' : \\'Next\\' }}' + CT + 'button' + String.fromCharCode(62));\r\n            lines.push('    ' + CT + 'div' + String.fromCharCode(62));\r\n            lines.push('  ' + CT + 'div' + String.fromCharCode(62));\r\n            lines.push(CT + 'template' + String.fromCharCode(62));\r\n            return lines.join('\\n');\r\n        },\r\n\r\n        buildWordPress: function(obj) {\r\n            var lines = [];\r\n            lines.push('<?php');\r\n            lines.push('\/**');\r\n            lines.push(' * Product tour shortcode. Place [product_tour] on any page.');\r\n            lines.push(' * Add this to your theme functions file or a code-snippet plugin.');\r\n            lines.push(' *\/');\r\n            lines.push(\"function pth_product_tour_shortcode() {\");\r\n            lines.push('    ob_start();');\r\n            lines.push('    ?>');\r\n            lines.push('    ' + String.fromCharCode(60) + 'script data-no-optimize=\"1\" data-no-minify=\"1\" data-cfasync=\"false\"' + String.fromCharCode(62));\r\n            lines.push(this.buildStandalone(obj).split('\\n').join('\\n    '));\r\n            lines.push('    ' + CT + 'script' + String.fromCharCode(62));\r\n            lines.push('    <?php');\r\n            lines.push('    return ob_get_clean();');\r\n            lines.push('}');\r\n            lines.push(\"add_shortcode( 'product_tour', 'pth_product_tour_shortcode' );\");\r\n            return lines.join('\\n');\r\n        },\r\n\r\n        downloadExport: function() {\r\n            var fmt = this.val('pth-t-fmt') || 'json';\r\n            var names = { json: 'tour.json', js: 'tour.js', react: 'ProductTour.jsx', vue: 'ProductTour.vue', wp: 'tour-shortcode.php' };\r\n            var text = this.val('pth-t-out');\r\n            if (!text) { this.toast('Nothing to download yet', true); return; }\r\n            try {\r\n                var blob = new Blob([text], { type: 'text\/plain;charset=utf-8' });\r\n                var url = URL.createObjectURL(blob);\r\n                var a = document.createElement('a');\r\n                a.href = url;\r\n                a.download = names[fmt] || 'tour.txt';\r\n                document.body.appendChild(a);\r\n                a.click();\r\n                document.body.removeChild(a);\r\n                setTimeout(function() { URL.revokeObjectURL(url); }, 1000);\r\n                this.toast('File saved');\r\n            } catch (err) {\r\n                this.toast('Your browser blocked the download', true);\r\n            }\r\n        },\r\n\r\n        importJson: function() {\r\n            var raw = this.val('pth-t-import-box');\r\n            if (!raw || !raw.replace(\/\\s\/g, '')) { this.toast('Paste a tour JSON first', true); return; }\r\n            var obj;\r\n            try { obj = JSON.parse(raw); }\r\n            catch (err) { this.toast('That is not valid JSON', true); return; }\r\n            if (!obj || Object.prototype.toString.call(obj.steps) !== '[object Array]') {\r\n                this.toast('No steps array found in that JSON', true);\r\n                return;\r\n            }\r\n            this.steps = obj.steps;\r\n            if (Object.prototype.toString.call(obj.hotspots) === '[object Array]') { this.hotspots = obj.hotspots; }\r\n            if (obj.checklist) {\r\n                if (Object.prototype.toString.call(obj.checklist.tasks) === '[object Array]') { this.tasks = obj.checklist.tasks; }\r\n                if (obj.checklist.title) { this.setVal('pth-t-cl-title', obj.checklist.title); }\r\n            }\r\n            if (obj.banner) {\r\n                if (obj.banner.text) { this.setVal('pth-t-bn-text', obj.banner.text); }\r\n                if (obj.banner.icon) { this.setVal('pth-t-bn-icon', obj.banner.icon); }\r\n                if (obj.banner.style) { this.setVal('pth-t-bn-style', obj.banner.style); }\r\n            }\r\n            this.renderSteps();\r\n            this.renderHotspots();\r\n            this.renderTasks();\r\n            this.renderBanner();\r\n            this.renderExport();\r\n            this.switchTab('build');\r\n            this.toast('Imported ' + this.steps.length + ' steps');\r\n        },\r\n\r\n        \/* ---------- shared ---------- *\/\r\n        loadSample: function() {\r\n            var pick = this.val('pth-t-sample-pick') || 'app';\r\n            this.setVal('pth-t-html', this.SAMPLES[pick] || this.SAMPLES.app);\r\n            this.updateHtmlStat();\r\n            this.renderStage();\r\n\r\n            if (pick === 'app') {\r\n                this.steps = [\r\n                    { sel: '#app-project', title: 'Pick your project', text: 'Everything you upload is filed under the project selected here. Switch it before uploading, not after.', pos: 'bottom', adv: 'next', scroll: true, optional: false },\r\n                    { sel: '#app-upload', title: 'Upload your first file', text: 'Drag a file here or click to browse. Files stay private to the project until you share them.', pos: 'bottom', adv: 'next', scroll: true, optional: false },\r\n                    { sel: '#app-share', title: 'Share when you are ready', text: 'Creates a link with an expiry date. You can revoke it at any time from the file menu.', pos: 'top', adv: 'next', scroll: true, optional: false },\r\n                    { sel: '#app-usage', title: 'Keep an eye on storage', text: 'The free plan includes 10 GB. This bar updates as soon as an upload finishes.', pos: 'top', adv: 'next', scroll: true, optional: true }\r\n                ];\r\n                this.hotspots = [{ sel: '#app-profile', title: 'Account settings', text: 'Change your email, password and notification preferences here.' }];\r\n            } else if (pick === 'form') {\r\n                this.steps = [\r\n                    { sel: '#form-email', title: 'Use your work email', text: 'Team features are matched by email domain, so a work address puts you in the right workspace automatically.', pos: 'bottom', adv: 'input', scroll: true, optional: false },\r\n                    { sel: '#form-pass', title: 'Twelve characters minimum', text: 'Length matters more than symbols. A short phrase you can remember beats a mangled word.', pos: 'bottom', adv: 'input', scroll: true, optional: false },\r\n                    { sel: '#form-submit', title: 'That is everything', text: 'No card is required and the free tier does not expire.', pos: 'top', adv: 'next', scroll: true, optional: false }\r\n                ];\r\n                this.hotspots = [];\r\n            } else {\r\n                this.steps = [\r\n                    { sel: '#shop-colour', title: 'Choose a colour first', text: 'Stock differs by colour, so the delivery estimate updates once you pick one.', pos: 'right', adv: 'next', scroll: true, optional: false },\r\n                    { sel: '#shop-cart', title: 'Add to your basket', text: 'Nothing is charged yet. You can change the quantity at checkout.', pos: 'bottom', adv: 'click', scroll: true, optional: false },\r\n                    { sel: '#shop-wish', title: 'Or save it for later', text: 'Saved items are kept for 90 days and you are told if the price drops.', pos: 'bottom', adv: 'next', scroll: true, optional: true }\r\n                ];\r\n                this.hotspots = [{ sel: '#shop-price', title: 'Price match', text: 'Found it cheaper elsewhere? We match any UK retailer within 14 days.' }];\r\n            }\r\n\r\n            this.renderSteps();\r\n            this.renderHotspots();\r\n            this.renderTasks();\r\n            this.renderBanner();\r\n            this.renderExport();\r\n            this.toast('Sample tour loaded');\r\n        },\r\n\r\n        clearAll: function() {\r\n            this.steps = [];\r\n            this.hotspots = [];\r\n            this.tasks = [];\r\n            this.stopTour();\r\n            this.closeEditor();\r\n            this.setVal('pth-t-html', '');\r\n            this.setVal('pth-t-out', '');\r\n            this.setVal('pth-t-import-box', '');\r\n            this.updateHtmlStat();\r\n            var s1 = document.getElementById('pth-t-stage');\r\n            var s2 = document.getElementById('pth-t-stage2');\r\n            if (s1) { s1.innerHTML = '<p style=\"color:#94a3b8; font-size:13.5px; margin:0;\">Paste markup above and press Render stage.' + CT + 'p>'; }\r\n            if (s2) { s2.innerHTML = '<p style=\"color:#94a3b8; font-size:13.5px; margin:0;\">Render your markup on the Build tab first.' + CT + 'p>'; }\r\n            var hr = document.getElementById('pth-t-health-rows');\r\n            if (hr) { hr.innerHTML = ''; }\r\n            this.renderSteps();\r\n            this.renderHotspots();\r\n            this.renderTasks();\r\n            this.renderBanner();\r\n            this.toast('Everything cleared');\r\n        }\r\n    };\r\n\r\n    \/* ---------- 4-layer bootstrap ---------- *\/\r\n    function pthTourRunInit() {\r\n        if (window.pthTourApp.initialized) { return; }\r\n        if (!document.getElementById('pth-tour-wrapper')) { return; }\r\n        window.pthTourApp.initialized = true;\r\n        try {\r\n            window.pthTourApp.init();\r\n        } catch (err) {\r\n            if (window.console && window.console.error) {\r\n                window.console.error('[Tour Studio] startup failed', err);\r\n            }\r\n            try { window.pthTourApp.bindAll(); } catch (inner) { return; }\r\n        }\r\n    }\r\n\r\n    pthTourRunInit();\r\n\r\n    if (document.readyState === 'loading') {\r\n        document.addEventListener('DOMContentLoaded', pthTourRunInit);\r\n    }\r\n\r\n    window.addEventListener('load', function() { setTimeout(pthTourRunInit, 50); });\r\n\r\n    function pthTourWatch() {\r\n        if (typeof MutationObserver === 'undefined' || !document.body) { return; }\r\n        var observer = new MutationObserver(function() {\r\n            if (document.getElementById('pth-tour-wrapper')) { pthTourRunInit(); }\r\n        });\r\n        observer.observe(document.body, { childList: true, subtree: true });\r\n        setTimeout(function() { observer.disconnect(); }, 10000);\r\n    }\r\n\r\n    if (document.body) { pthTourWatch(); } else { document.addEventListener('DOMContentLoaded', pthTourWatch); }\r\n})();\r\n<\/script>\n\n\n\n<style>\n\/* ==========================================================\n   PART 2: FEATURES & STEPS UI (Interactive Tour Builder)\n   ========================================================== *\/\n.pth-feature-wrap { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }\n.pth-feat-box { border: 1px solid #e2e8f0; background: #ffffff; padding: 24px; border-radius: 12px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.02); transition: all 0.3s ease; }\n.pth-feat-box:hover { border-color: #bfdbfe; transform: translateY(-3px); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.05); }\n.pth-feat-title { font-size: 1.1rem; font-weight: 800; color: #0f172a; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }\n.pth-feat-text { font-size: 0.9rem; color: #475569; line-height: 1.6; margin: 0; }\n\n.pth-steps-wrap { border: 1px solid #e2e8f0; background: #ffffff; padding: 40px 30px; border-radius: 16px; text-align: center; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.02); margin-bottom: 40px; }\n.pth-steps-title { font-size: 1.2rem; font-weight: 800; color: #1e293b; text-transform: uppercase; margin-bottom: 35px; letter-spacing: 1px; }\n.pth-steps-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 30px; }\n.pth-step-item { display: flex; flex-direction: column; align-items: center; }\n.pth-step-num { width: 45px; height: 45px; background: #3b82f6; color: #ffffff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; font-size: 1.2rem; margin-bottom: 15px; box-shadow: 0 4px 10px rgba(59,130,246,0.3); }\n.pth-step-name { font-weight: 800; color: #0f172a; margin-bottom: 8px; font-size: 1.05rem; }\n.pth-step-desc { font-size: 0.9rem; color: #64748b; line-height: 1.5; margin: 0; }\n<\/style>\n\n<div class=\"pth-feature-wrap\">\n    <div class=\"pth-feat-box\">\n        <div class=\"pth-feat-title\">\ud83d\udfe2 Zero-Latency Parsing<\/div>\n        <p class=\"pth-feat-text\">Engineered to execute entirely within the local browser runtime, bypassing HTTP request overhead.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n        <div class=\"pth-feat-title\">\ud83d\udd35 Immutable Architecture<\/div>\n        <p class=\"pth-feat-text\">Generated payloads consist of vanilla JavaScript and scoped CSS variables that prevent namespace collisions.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n        <div class=\"pth-feat-title\">\ud83d\udfe3 Memory Efficiency<\/div>\n        <p class=\"pth-feat-text\">Constructed with high-performance event listeners to minimize memory leaks during continuous user interaction.<\/p>\n    <\/div>\n    <div class=\"pth-feat-box\">\n        <div class=\"pth-feat-title\">\ud83d\udfe4 Stateful Persistence<\/div>\n        <p class=\"pth-feat-text\">Employs HTML5 LocalStorage APIs to track completion states and manage asynchronous overlay rendering.<\/p>\n    <\/div>\n<\/div>\n\n<div class=\"pth-steps-wrap\">\n    <div class=\"pth-steps-title\">How to Use the Interactive Tour Builder<\/div>\n    <div class=\"pth-steps-grid\">\n        <div class=\"pth-step-item\">\n            <div class=\"pth-step-num\">1<\/div>\n            <div class=\"pth-step-name\">Inject Source Code<\/div>\n            <p class=\"pth-step-desc\">Paste your target HTML structure into the parsing module for rapid local analysis.<\/p>\n        <\/div>\n        <div class=\"pth-step-item\">\n            <div class=\"pth-step-num\">2<\/div>\n            <div class=\"pth-step-name\">Target Nodes<\/div>\n            <p class=\"pth-step-desc\">Specify exact CSS selectors to construct mathematically accurate bounding boxes.<\/p>\n        <\/div>\n        <div class=\"pth-step-item\">\n            <div class=\"pth-step-num\">3<\/div>\n            <div class=\"pth-step-name\">Define Sequences<\/div>\n            <p class=\"pth-step-desc\">Establish chronological progression logic using our built-in event handlers.<\/p>\n        <\/div>\n        <div class=\"pth-step-item\">\n            <div class=\"pth-step-num\">4<\/div>\n            <div class=\"pth-step-name\">Compile Payload<\/div>\n            <p class=\"pth-step-desc\">Generate a minified deployment block mapped to your exact specification.<\/p>\n        <\/div>\n    <\/div>\n<\/div>\n\n\n<div id=\"start-seo-article\"><\/div>\n\n\n<p class=\"wp-block-paragraph\"><strong>Last Updated: August 2026<\/strong><\/p>\n\n\n\n<h2 id=\"\ud83d\udd34-four-tabs-and-why-each-one-exists\" class=\"wp-block-heading\">\ud83d\udd34 Four Tabs, and Why Each One Exists<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Most tour builders let you write steps and hand you a JSON file. The hard parts are everywhere else: knowing whether your selectors still match after a redesign, deciding whether a tour is even the right pattern, and getting working code out at the end rather than a config file you still have to wire up.<\/p>\n\n\n\n<h3 id=\"build-write-the-steps-against-real-markup\" class=\"wp-block-heading\">Build \u2014 write the steps against real markup<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Paste the HTML of the page you are writing a tour for, or pick one of the three samples. Press&nbsp;<strong>Render stage<\/strong>&nbsp;and it appears in a sandboxed panel below the editor. Scripts, iframes and inline event handlers are stripped before anything renders \u2014 you are usually pasting markup from a page you did not write, and a preview pane is no place to execute someone else&#8217;s JavaScript.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From there, three ways to create steps:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udd35&nbsp;<strong>Pick element<\/strong>&nbsp;turns the cursor into a crosshair. Click anything on the stage and a selector is captured for you. It prefers an&nbsp;<code>id<\/code>, then a&nbsp;<code>data-tour<\/code>&nbsp;attribute, and only falls back to a structural path when neither exists.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udfe0&nbsp;<strong>Auto-detect<\/strong>&nbsp;walks the markup for buttons, inputs, selects and linked elements, and drafts a step for each with the element&#8217;s own label as the title. It caps at eight, because it is a starting point to cut down rather than a finished tour.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udfe3&nbsp;<strong>Add step<\/strong>&nbsp;by hand when you already know the selector.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Each step carries a bubble position, an advance trigger \u2014 press Next, click the element, or fill the field \u2014 and two switches worth understanding.&nbsp;<em>Scroll into view<\/em>&nbsp;matters on long pages.&nbsp;<em>Skip if the element is missing<\/em>&nbsp;is what you want for a step pointing at something that only appears for some users, such as an admin-only control.<\/p>\n\n\n\n<h3 id=\"preview-test-where-tours-actually-break\" class=\"wp-block-heading\">Preview &amp; Test \u2014 where tours actually break<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run the tour at mobile, tablet or full width. This is not decoration. A bubble set to appear to the right of an element looks correct on a desktop and falls straight off a 380 pixel screen, and that is the single most common way a tour ships broken.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The&nbsp;<strong>selector health<\/strong>&nbsp;table underneath is the part worth running before every release. It reports three states:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udd35&nbsp;<strong>Good<\/strong>&nbsp;\u2014 the selector matches exactly one element.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udfe0&nbsp;<strong>Ambiguous<\/strong>&nbsp;\u2014 it matches several. The tour will highlight the first one, which is not always the one you meant. A selector like&nbsp;<code>.card .title<\/code>&nbsp;looks specific and matches nine elements on a listing page.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\ud83d\udfe3&nbsp;<strong>No match<\/strong>&nbsp;\u2014 the step will be skipped in silence. Nothing is logged, no error appears, and the tour looks like it worked while quietly missing a step.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That last case is why the check exists. A broken tour does not announce itself.<\/p>\n\n\n\n<h3 id=\"hotspots-checklists-for-people-who-already-dismissed-the-tour\" class=\"wp-block-heading\">Hotspots &amp; Checklists \u2014 for people who already dismissed the tour<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A tour suits a first session and almost nothing after it. Someone who has already pressed Skip once will press it faster the second time. Three quieter patterns work better for returning users:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Hotspots<\/strong>&nbsp;are pulsing markers anchored to an element. They wait to be noticed instead of demanding attention, which makes them right for a feature that is genuinely useful but not urgent.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Checklists<\/strong>&nbsp;work because progress is visible and resumable. Three of five done is a much stronger pull than a tour that has to be restarted from step one. Tick items in the editor and the preview widget updates as you go.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Announcement banners<\/strong>&nbsp;are for one job only: something changed. One sentence, and let people close it for good.<\/p>\n\n\n\n<h3 id=\"export-embed-working-code-not-a-config-file\" class=\"wp-block-heading\">Export &amp; Embed \u2014 working code, not a config file<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Five formats.&nbsp;<strong>JSON<\/strong>&nbsp;is the definition alone, for when you already have a tour library.&nbsp;<strong>Standalone JavaScript<\/strong>&nbsp;is self-contained \u2014 markup, styles and logic in one file with no dependencies \u2014 and is the one most people want.&nbsp;<strong>React<\/strong>&nbsp;and&nbsp;<strong>Vue 3<\/strong>&nbsp;come out as components with the steps inlined.&nbsp;<strong>WordPress<\/strong>&nbsp;gives you a shortcode function to drop in a snippet plugin.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two options change what the export contains.&nbsp;<em>Analytics hooks<\/em>&nbsp;fire a custom DOM event on every step shown, completed or skipped, and push the same to&nbsp;<code>dataLayer<\/code>&nbsp;if one exists \u2014 the step people quit on tells you which part of your product is confusing far better than a completion rate does.&nbsp;<em>Remember dismissal<\/em>&nbsp;writes a flag to local storage so a returning visitor is not shown the tour again.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The generated JavaScript is plain ES5, keeps the bubble on screen when an element sits near an edge, repositions on resize, and supports Escape to quit plus arrow keys to move. Paste it before the closing body tag and it runs.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe1-mistakes-worth-avoiding\" class=\"wp-block-heading\">\ud83d\udfe1 Mistakes Worth Avoiding<\/h2>\n\n\n\n<h3 id=\"building-the-tour-on-class-names\" class=\"wp-block-heading\">Building the tour on class names<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the one that costs people real time. A selector such as&nbsp;<code>.btn-primary.large<\/code>&nbsp;works perfectly until a designer renames a utility class, and then the tour breaks with no error anywhere. Add a dedicated attribute to the elements you point at:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>&lt;button data-tour=\"upload\"&gt;Upload file&lt;\/button&gt;<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">and target&nbsp;<code>[data-tour=\"upload\"]<\/code>. The attribute exists for exactly this purpose, so nobody removes it during a redesign. The picker in this tool already prefers it when one is present.<\/p>\n\n\n\n<h3 id=\"making-the-tour-too-long\" class=\"wp-block-heading\">Making the tour too long<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Five steps is a sensible ceiling. Past that, completion drops sharply \u2014 people arrived to do something, not to read a manual, and every additional step is another chance to press Skip. If you genuinely have twelve things to explain, a checklist is the right pattern, not a longer tour.<\/p>\n\n\n\n<h3 id=\"explaining-the-obvious\" class=\"wp-block-heading\">Explaining the obvious<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A step over a button labelled&nbsp;<em>Save<\/em>&nbsp;teaches nobody anything and spends the goodwill you needed for the step that mattered. Point at the setting buried three menus deep, the field whose format is not obvious, the feature nobody finds. Those earn the interruption.<\/p>\n\n\n\n<h3 id=\"never-testing-on-a-phone\" class=\"wp-block-heading\">Never testing on a phone<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Covered above, but it bears repeating because it is so common. Run every tour at mobile width before shipping. If a bubble is positioned left or right of an element, check it especially carefully.<\/p>\n\n\n\n<h2 id=\"\ud83d\udfe2-honest-limits\" class=\"wp-block-heading\">\ud83d\udfe2 Honest Limits<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The stage is a static preview. Pasted scripts are stripped, so a step whose selector only exists after a modal opens or a tab is clicked cannot be tested here \u2014 check that one on the real page.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The selector health check only knows about the markup you pasted. If your live page differs, the check will pass while the tour still breaks. Paste the current markup, not last month&#8217;s.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Auto-detect stops at eight steps and reads element text for titles, so a button whose label is an icon gets a poor title. Edit them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Everything is held in the page while you work. There is no account and nothing is saved to a server, which means a refresh loses your tour \u2014 export the JSON before you close the tab. You can import it back from the Export tab whenever you need it.<\/p>\n\n\n\n<h2 id=\"\ud83d\udd34-going-deeper\" class=\"wp-block-heading\">\ud83d\udd34 Going Deeper<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you want to know why spotlight overlays are built the way they are, how element positions are calculated, and what the research actually says about onboarding completion, the companion piece covers it:&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/how-product-tours-work\/\">how product tours work under the hood<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To build or edit the page the tour runs on, the&nbsp;<a href=\"https:\/\/schoolict.net\/tools\/offline-html-editor-with-css-js\/\">Offline HTML Editor<\/a>&nbsp;is the companion tool here. The selector syntax itself is documented at&nbsp;<a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Document\/querySelector\" rel=\"noreferrer noopener\" target=\"_blank\">MDN&#8217;s querySelector reference<\/a>, and&nbsp;<a href=\"https:\/\/www.nngroup.com\/articles\/onboarding-tutorials\/\" rel=\"noreferrer noopener\" target=\"_blank\">Nielsen Norman Group&#8217;s research on onboarding tutorials<\/a>&nbsp;is worth reading before you write the first step.<\/p>\n\n\n\n<style>\n.pth-faq-wrap { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin: 24px 0; }\n.pth-faq-card { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05); }\n.pth-faq-q { font-size: 15px; font-weight: 800; color: #0f172a; margin: 0 0 8px 0; line-height: 1.4; }\n.pth-faq-a { font-size: 14px; color: #475569; margin: 0; line-height: 1.65; font-weight: 500; }\n@media (max-width: 992px) { .pth-faq-wrap { grid-template-columns: repeat(2, 1fr); } }\n@media (max-width: 640px) { .pth-faq-wrap { grid-template-columns: 1fr; } }\n<\/style>\n \n<h2>\u2753 Frequently Asked Questions<\/h2>\n \n<div class=\"pth-faq-wrap\">\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Is my markup uploaded anywhere?<\/p>\n    <p class=\"pth-faq-a\">No. Everything runs in your browser. Open DevTools, watch the Network tab and press Render \u2014 no request carries what you pasted, because none is made.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Do I need a JavaScript library to use the export?<\/p>\n    <p class=\"pth-faq-a\">Not for the standalone export. It is one self-contained file with its own styles and no dependencies. The React and Vue exports need only their own framework.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">What is the best kind of selector to use?<\/p>\n    <p class=\"pth-faq-a\">A dedicated <code>data-tour<\/code> attribute on the target element. Class names get renamed in redesigns and the tour breaks silently; an attribute added for this purpose survives.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">How many steps should a tour have?<\/p>\n    <p class=\"pth-faq-a\">Five or fewer. Completion falls off sharply beyond that. If you have more to say, use a checklist people can return to rather than a longer tour.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">What happens if an element is missing when the tour runs?<\/p>\n    <p class=\"pth-faq-a\">The step is skipped and the tour continues. Tick &#8220;skip if missing&#8221; for steps that are genuinely optional, and run the health check for the ones that are not.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Can I test a step that only appears after a click?<\/p>\n    <p class=\"pth-faq-a\">Not on the stage, which is a static preview with scripts stripped. Export the tour and test that step on the real page instead.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Will my work survive a page refresh?<\/p>\n    <p class=\"pth-faq-a\">No. Nothing is stored on a server and nothing persists between sessions. Export the JSON before closing the tab, and import it back when you return.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">What do the analytics hooks send?<\/p>\n    <p class=\"pth-faq-a\">Nothing on their own. They fire a DOM event with the action and step number, and push to <code>dataLayer<\/code> if present, so your existing analytics can pick it up.<\/p>\n  <\/div>\n \n  <div class=\"pth-faq-card\">\n    <p class=\"pth-faq-q\">Can I edit an exported tour later?<\/p>\n    <p class=\"pth-faq-a\">Yes. Export as JSON, keep the file, and paste it into the import box on the Export tab whenever you want to change the steps and re-export.<\/p>\n  <\/div>\n \n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Guide Every User Step-by-Step \u2014 Create seamless interactive tours and product walkthroughs effortlessly. Build lightning-fast interactive website tours with the ultimate interactive tour builder. Paste any HTML, auto-detect elements, live preview, drag-and-drop steps, and export 100% self-contained production code. Perfect for SaaS onboarding, e-commerce demos, and premium conversions. \u00a0 \ud83d\udfe2 Zero-Latency Parsing Engineered to execute &#8230; <a title=\"Interactive Tour Builder | Create No-Code Guided Website Tours &amp; Product Walkthroughs\" class=\"read-more\" href=\"https:\/\/schoolict.net\/tools\/interactive-tour-builder\/\" aria-label=\"Read more about Interactive Tour Builder | Create No-Code Guided Website Tours &amp; Product Walkthroughs\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":4227,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-4213","page","type-page","status-publish","has-post-thumbnail"],"_links":{"self":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/4213","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/comments?post=4213"}],"version-history":[{"count":3,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/4213\/revisions"}],"predecessor-version":[{"id":6964,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/pages\/4213\/revisions\/6964"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media\/4227"}],"wp:attachment":[{"href":"https:\/\/schoolict.net\/tools\/wp-json\/wp\/v2\/media?parent=4213"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}