1<!DOCTYPE html><html lang="en-US"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><style id="wpconvert-shadcn-vars">:root { 2 --background: 0 0% 100%; 3 --foreground: 222.2 84% 4.9%; 4 --card: 0 0% 100%; 5 --card-foreground: 222.2 84% 4.9%; 6 --primary: 222.2 47.4% 11.2%; 7 --primary-foreground: 210 40% 98%; 8 --muted: 210 40% 96.1%; 9 --muted-foreground: 215.4 16.3% 46.9%; 10 --accent: 210 40% 96.1%; 11 --accent-foreground: 222.2 47.4% 11.2%; 12 --input: 214.3 31.8% 91.4%; 13 --border: 214.3 31.8% 91.4%; 14 --ring: 222.2 84% 4.9%; 15} 16.dark, [class*="bg-slate-950"], [class*="bg-gray-950"], [class*="bg-zinc-950"], [class*="bg-neutral-950"] { 17 --background: 222.2 84% 4.9%; 18 --foreground: 210 40% 98%; 19 --card: 217.2 32.6% 17.5%; 20 --card-foreground: 210 40% 98%; 21 --primary: 210 40% 98%; 22 --primary-foreground: 222.2 47.4% 11.2%; 23 /* EC-NAV-139: Light ink over dark heroes; unlike --primary-foreground in .dark (dark ink for buttons). */ 24 --wpconvert-transparent-nav-ink: 210 40% 98%; 25 --muted: 217.2 32.6% 17.5%; 26 --muted-foreground: 215 20.2% 65.1%; 27 --accent: 217.2 32.6% 17.5%; 28 --accent-foreground: 210 40% 98%; 29 --input: 217.2 32.6% 17.5%; 30 --border: 217.2 32.6% 17.5%; 31 --ring: 212.7 26.8% 83.9%; 32} 33/* EC-FAQ-DARK-001: FAQ answers keep light-theme text-slate-* utilities on dark pages (html.dark). */ 34html.dark [data-wp-faq-answer="true"] p.text-slate-300, 35html.dark [data-wp-faq-answer="true"] p.text-slate-400, 36html.dark [data-wp-faq-answer="true"] p.text-slate-500, 37html.dark [data-wp-faq-answer="true"] .text-slate-400, 38html.dark [data-wp-faq-answer="true"] .text-slate-500 { 39 color: rgba(255, 255, 255, 0.74) !important; 40}</style><meta name='robots' content='index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1' /><link media="all" href="https://www.choosetosucceed.org/wp-content/cache/autoptimize/css/autoptimize_16c046c69a3f47d4e3884663096543e9.css" rel="stylesheet"><title>Consider Bexar County - Choose to Succeed</title><link rel="canonical" href="https://www.choosetosucceed.org/consider-bexar-county/" /><meta property="og:locale" content="en_US" /><meta property="og:type" content="article" /><meta property="og:title" content="Consider Bexar County - Choose to Succeed" /><meta property="og:description" content="This page is automatically generated by WPConvert. The content is displayed using the page template." /><meta property="og:url" content="https://www.choosetosucceed.org/consider-bexar-county/" /><meta property="og:site_name" content="Choose to Succeed" /><meta property="article:modified_time" content="2026-06-04T21:40:46+00:00" /><meta name="twitter:card" content="summary_large_image" /><meta name="twitter:label1" content="Est. reading time" /><meta name="twitter:data1" content="1 minute" />
40<script type="application/ld+json" class="yoast-schema-graph">{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.choosetosucceed.org\/consider-bexar-county\/","url":"https:\/\/www.choosetosucceed.org\/consider-bexar-county\/","name":"Consider Bexar County - Choose to Succeed","isPartOf":{"@id":"https:\/\/www.choosetosucceed.org\/#website"},"datePublished":"2026-06-04T21:27:17+00:00","dateModified":"2026-06-04T21:40:46+00:00","breadcrumb":{"@id":"https:\/\/www.choosetosucceed.org\/consider-bexar-county\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.choosetosucceed.org\/consider-bexar-county\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.choosetosucceed.org\/consider-bexar-county\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.choosetosucceed.org\/"},{"@type":"ListItem","position":2,"name":"Consider Bexar County"}]},{"@type":"WebSite","@id":"https:\/\/www.choosetosucceed.org\/#website","url":"https:\/\/www.choosetosucceed.org\/","name":"Choose to Succeed","description":"Bringing World-class, tuition-free public charter schools to San Antonio","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.choosetosucceed.org\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"}]}</script>
40 <link rel="alternate" title="oEmbed (JSON)" type="application/json+oembed" href="https://www.choosetosucceed.org/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwww.choosetosucceed.org%2Fconsider-bexar-county%2F" /><link rel="alternate" title="oEmbed (XML)" type="text/xml+oembed" href="https://www.choosetosucceed.org/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fwww.choosetosucceed.org%2Fconsider-bexar-county%2F&format=xml" /> <style id="wp-img-auto-sizes-contain-inline-css">img:is([sizes=auto i],[sizes^="auto," i]){contain-intrinsic-size:3000px 1500px} 41/*# sourceURL=wp-img-auto-sizes-contain-inline-css */</style><style id="wp-emoji-styles-inline-css">img.wp-smiley, img.emoji { 42 display: inline !important; 43 border: none !important; 44 box-shadow: none !important; 45 height: 1em !important; 46 width: 1em !important; 47 margin: 0 0.07em !important; 48 vertical-align: -0.1em !important; 49 background: none !important; 50 padding: 0 !important; 51 } 52/*# sourceURL=wp-emoji-styles-inline-css */</style><style id="wp-block-library-inline-css">:root{--wp-block-synced-color:#7a00df;--wp-block-synced-color--rgb:122,0,223;--wp-bound-block-color:var(--wp-block-synced-color);--wp-editor-canvas-background:#ddd;--wp-admin-theme-color:#007cba;--wp-admin-theme-color--rgb:0,124,186;--wp-admin-theme-color-darker-10:#006ba1;--wp-admin-theme-color-darker-10--rgb:0,107,160.5;--wp-admin-theme-color-darker-20:#005a87;--wp-admin-theme-color-darker-20--rgb:0,90,135;--wp-admin-border-width-focus:2px}@media (min-resolution:192dpi){:root{--wp-admin-border-width-focus:1.5px}}.wp-element-button{cursor:pointer}:root .has-very-light-gray-background-color{background-color:#eee}:root .has-very-dark-gray-background-color{background-color:#313131}:root .has-very-light-gray-color{color:#eee}:root .has-very-dark-gray-color{color:#313131}:root .has-vivid-green-cyan-to-vivid-cyan-blue-gradient-background{background:linear-gradient(135deg,#00d084,#0693e3)}:root .has-purple-crush-gradient-background{background:linear-gradient(135deg,#34e2e4,#4721fb 50%,#ab1dfe)}:root .has-hazy-dawn-gradient-background{background:linear-gradient(135deg,#faaca8,#dad0ec)}:root .has-subdued-olive-gradient-background{background:linear-gradient(135deg,#fafae1,#67a671)}:root .has-atomic-cream-gradient-background{background:linear-gradient(135deg,#fdd79a,#004a59)}:root .has-nightshade-gradient-background{background:linear-gradient(135deg,#330968,#31cdcf)}:root .has-midnight-gradient-background{background:linear-gradient(135deg,#020381,#2874fc)}:root{--wp--preset--font-size--normal:16px;
52--wp--preset--font-size--huge:42px}.has-regular-font-size{font-size:1em}.has-larger-font-size{font-size:2.625em}.has-normal-font-size{font-size:var(--wp--preset--font-size--normal)}.has-huge-font-size{font-size:var(--wp--preset--font-size--huge)}:root .has-text-align-center{text-align:center}:root .has-text-align-left{text-align:left}:root .has-text-align-right{text-align:right}.has-fit-text{white-space:nowrap!important}#end-resizable-editor-section{display:none}.aligncenter{clear:both}.items-justified-left{justify-content:flex-start}.items-justified-center{justify-content:center}.items-justified-right{justify-content:flex-end}.items-justified-space-between{justify-content:space-between}.screen-reader-text{word-wrap:normal!important;border:0;clip-path:inset(50%);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.screen-reader-text:focus{background-color:#ddd;clip-path:none;color:#444;display:block;font-size:1em;height:auto;left:5px;line-height:normal;padding:15px 23px 14px;text-decoration:none;top:5px;width:auto;z-index:100000}html :where(.has-border-color){border-style:solid}html :where([style*=border-color]){border-style:solid}html :where([style*=border-top-color]){border-top-style:solid}html :where([style*=border-right-color]){border-right-style:solid}html :where([style*=border-bottom-color]){border-bottom-style:solid}html :where([style*=border-left-color]){border-left-style:solid}html :where([style*=border-width]){border-style:solid}html :where([style*=border-top-width]){border-top-style:solid}html :where([style*=border-right-width]){border-right-style:solid}html :where([style*=border-bottom-width]){border-bottom-style:solid}html :where([style*=border-left-width]){border-left-style:solid}html :where(img[class*=wp-image-]){height:auto;max-width:100%}:where(figure){margin:0 0 1em}html :where(.is-position-sticky){--wp-admin--admin-bar--position-offset:var(--wp-admin--admin-bar--height,0px)}@media screen and (max-width:600px){html :where(.is-position-sticky){--wp-admin--admin-bar--position-offset:0px}} 53 54/*# sourceURL=/wp-includes/css/dist/block-library/common.min.css */</style><style id="global-styles-inline-css">:root{--wp--preset--aspect-ratio--square: 1;--wp--preset--aspect-ratio--4-3: 4/3;--wp--preset--aspect-ratio--3-4: 3/4;--wp--preset--aspect-ratio--3-2: 3/2;--wp--preset--aspect-ratio--2-3: 2/3;--wp--preset--aspect-ratio--16-9: 16/9;--wp--preset--aspect-ratio--9-16: 9/16;--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgb(6,147,227) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgb(252,185,0) 0%,rgb(255,105,0) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgb(255,105,0) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);
54--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;--wp--preset--spacing--20: 0.44rem;--wp--preset--spacing--30: 0.67rem;--wp--preset--spacing--40: 1rem;--wp--preset--spacing--50: 1.5rem;--wp--preset--spacing--60: 2.25rem;--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgb(255, 255, 255), 6px 6px rgb(0, 0, 0);--wp--preset--shadow--crisp: 6px 6px 0px rgb(0, 0, 0);}:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}body{padding-top: 0px;padding-right: 0px;padding-bottom: 0px;padding-left: 0px;}:root :where(.wp-element-button, .wp-block-button__link){background-color: #32373c;border-width: 0;color: #fff;font-family: inherit;font-size: inherit;font-style: inherit;font-weight: inherit;letter-spacing: inherit;line-height: inherit;padding-top: calc(0.667em + 2px);padding-right: calc(1.333em + 2px);padding-bottom: calc(0.667em + 2px);padding-left: calc(1.333em + 2px);text-decoration: none;text-transform: inherit;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}
54.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;} 55/*# sourceURL=global-styles-inline-css */</style><style id="wpconvert-ecnav088-inline-css">/* EC-ANIM-008c/009: carousel-only scroll-reveal override */.scroll-fade-up:has([aria-roledescription="carousel"]),.scroll-fade-up:has([data-wpconvert-blog-filter-bar]),.scroll-fade-left:has([aria-roledescription="carousel"]),.scroll-fade-right:has([aria-roledescription="carousel"]),.scroll-scale-in:has([aria-roledescription="carousel"]){opacity:1!important;transform:none!important}/* EC-ACCORDION-STATE: Sync panel visibility with trigger state â Puppeteer snapshots *//* can capture panels as data-state=open while their trigger is data-state=closed */h3[data-state="closed"]+[role="region"],h3[data-state="closed"]+[data-state][role="region"]{display:none!important;max-height:0!important;overflow:hidden!important;animation:none!important} 56/*# sourceURL=wpconvert-ecnav088-inline-css */</style>
56<script id="jquery-core-js" src="https://www.choosetosucceed.org/wp-includes/js/jquery/jquery.min.js?ver=3.7.1"></script>
56
56<script id="jquery-js-before">// Prevent React createRoot/hydrateRoot from executing if root element doesn't exist 57 (function() { 58 if (typeof window !== "undefined") { 59 // Intercept createRoot calls before React loads 60 var originalCreateRoot = null; 61 var originalHydrateRoot = null; 62 63 // Override when React loads 64 Object.defineProperty(window, 'React', { 65 set: function(value) { 66 if (value && value.createRoot) { 67 originalCreateRoot = value.createRoot; 68 value.createRoot = function(container) { 69 if (!container || !document.contains(container)) { 70 console.warn("[WPConvert] React createRoot prevented - container not found"); 71 return { render: function() {}, unmount: function() {} }; 72 } 73 return originalCreateRoot.call(this, container); 74 }; 75 } 76 this._react = value; 77 }, 78 get: function() { 79 return this._react; 80 } 81 }); 82 83 Object.defineProperty(window, 'ReactDOM', { 84 set: function(value) { 85 if (value) { 86 if (value.createRoot) { 87 originalCreateRoot = value.createRoot; 88 value.createRoot = function(container) { 89 if (!container || !document.contains(container)) { 90 console.warn("[WPConvert] ReactDOM createRoot prevented"); 91 return { render: function() {}, unmount: function() {} }; 92 } 93 return originalCreateRoot.call(this, container); 94 }; 95 } 96 if (value.hydrateRoot) { 97 originalHydrateRoot = value.hydrateRoot; 98 value.hydrateRoot = function(container) { 99 if (!container || !document.contains(container)) { 100 console.warn("[WPConvert] ReactDOM hydrateRoot prevented"); 101 return { render: function() {}, unmount: function() {} }; 102 } 103 return originalHydrateRoot.call(this, container); 104 }; 105 } 106 } 107 this._reactDOM = value; 108 }, 109 get: function() { 110 return this._reactDOM; 111 } 112 }); 113 } 114 })(); 115 (function() { 116 // Run as early as possible, before DOMContentLoaded 117 function revealHiddenElements() { 118 console.log('[WPConvert] Running animation fallback to reveal hidden sections...'); 119 120 var revealed = 0; 121 var elements = document.querySelectorAll('*'); 122 123 elements.forEach(function(el) { 124 // Skip elements that should stay hidden 125 var tagName = el.tagName.toLowerCase(); 126 if (tagName === 'script' || tagName === 'style' || tagName === 'noscript') { 127 return; 128 } 129 130 // Skip WordPress admin bar and menus 131 if (el.id === 'wpadminbar' || 132 el.classList.contains('sub-menu') || 133 el.classList.contains('submenu') || 134 el.hasAttribute('data-wpconvert-popup-disabled')) { 135 return; 136 } 137 138 // EC-GALLERY-FLASH-001: Skip elements inside stacked gallery containers. 139 // These images intentionally use opacity:0 for crossfade transitions. 140 // Revealing them causes a flash before the carousel handler resets them. 141 if (el.closest('[data-wpconvert-stacked-gallery]') || 142 el.closest('[data-wpc-carousel]')) { 143 return; 144 } 145 146 var style = el.style; 147 var computedStyle = window.getComputedStyle(el); 148 var hasHidingStyle = false;
149 150 // Check for hiding inline styles (Framer Motion patterns) 151 if (style.opacity === '0' || style.opacity === '0.0') { 152 // Only reveal if element has content/dimensions 153 if (el.offsetHeight > 0 || el.offsetWidth > 0 || el.textContent.trim().length > 0) { 154 style.opacity = '1'; 155 hasHidingStyle = true; 156 } 157 } 158 159 // Check for visibility hidden 160 if (style.visibility === 'hidden' && computedStyle.display !== 'none') { 161 style.visibility = 'visible'; 162 hasHidingStyle = true; 163 } 164 165 // Check for transform that moves element off-screen 166 if (style.transform && ( 167 style.transform.includes('translateY') || 168 style.transform.includes('translateX') || 169 style.transform.includes('scale(0)') 170 )) { 171 // EC-CAROUSEL-009: Embla/shadcn carousels set translateX on the flex track via WPConvert JS. 172 // Stripping it (especially on the delayed run) breaks the culture image slider. 173 if (el.closest('[aria-roledescription="carousel"]')) { 174 /* skip */ 175 } else { 176 // Reset problematic transforms (preserve intentional ones like rotate) 177 var transform = style.transform; 178 transform = transform.replace(/translateY\([^)]+\)/g, 'translateY(0)'); 179 transform = transform.replace(/translateX\([^)]+\)/g, 'translateX(0)'); 180 transform = transform.replace(/scale\(0(?:\.\d+)?\)/g, 'scale(1)'); 181 182 if (transform !== style.transform) { 183 style.transform = transform; 184 hasHidingStyle = true; 185 } 186 } 187 } 188 189 if (hasHidingStyle) { 190 revealed++; 191 } 192 }); 193 194 if (revealed > 0) { 195 console.log('[WPConvert] â Revealed ' + revealed + ' hidden elements (Framer Motion fallback)'); 196 } 197 } 198 199 // Run immediately (for inline styles in initial HTML) 200 if (document.readyState === 'loading') { 201 document.addEventListener('DOMContentLoaded', revealHiddenElements); 202 } else { 203 revealHiddenElements(); 204 } 205 206 // Also run after a short delay to catch late-loading elements 207 setTimeout(revealHiddenElements, 100); 208 })(); 209//# sourceURL=jquery-js-before</script>
209
209<script id="jquery-js-after">jQuery(document).ready(function($) { 210 // EC-TESTIMONIAL-004b: Shared by universal carousel + EC-CAROUSEL-002 â skip only inner JSON testimonial card 211 function wpcInsideJsonTestimonialCard($el) { 212 var $tsec = $el.closest("[data-wpconvert-testimonials]"); 213 if (!$tsec.length) return false; 214 var $ariaPrev = $tsec.find('button[aria-label*="Prev"], button[aria-label*="Previous"], button[aria-label*="previous"]').first(); 215 if (!$ariaPrev.length) return true; 216 var $innerCard = $ariaPrev.closest('[class*="rounded-3xl"],[class*="rounded-2xl"],[class*="rounded-xl"]').first(); 217 if (!$innerCard.length) return true; 218 return $el.closest($innerCard).length > 0; 219 } 220 try { // [WPConvert: Smooth Scrolling] 221 // Smooth scrolling for anchor links 222 // EC-EDITOR-005: Skip when WPConvert editor is active to avoid scroll conflict 223 $('a[href^="#"]').on("click", function(e) { 224 if (window.location.search.indexOf("wpc_edit=1") !== -1) return; 225 e.preventDefault(); 226 var target = $(this).attr("href"); 227 if (target === "#") return; 228 229 var $target = $(target); 230 if ($target.length) { 231 $("html, body").animate({ 232 scrollTop: $target.offset().top - 80 233 }, 800); 234 } 235 }); 236 } catch(e) { console.warn("[WPConvert] Smooth scrolling error:", e); } 237 238 try { // [WPConvert: Accordion/Collapsible] 239 // Universal accordion/collapsible functionality 240 // Works for Radix UI, Material UI, Bootstrap, Ant Design, custom accordions 241 // CRITICAL: Only target actual trigger elements (buttons, summary) - NOT wrapper divs with data-state 242 // Using [data-state] in selector caused double-firing because parent wrappers also have data-state 243 // EC-ACCORDION-011: Also target h3[data-state] > a â Radix accordion triggers 244 // that were converted from <button> to <a> during snapshot processing. 245 $(document).on("click", "button[aria-controls], button[aria-expanded], h3[data-state] > button, h3[data-state] > a, .accordion-trigger, summary", function(e) { 246 e.stopPropagation(); // Prevent event bubbling to parent wrappers 247 248 var $trigger = $(this); 249 250 // EC-ACCORDION-012: image-card CTA buttons (e.g. MenuCard "+ Tilføj") 251 // are explicitly guarded â never treat them as accordion triggers. 252 if ($trigger.attr("data-wpc-no-accordion-trigger") === "1") { 253 return; 254 } 255 256 // CRITICAL: Only handle actual trigger elements (button, summary, a inside accordion h3, or explicit accordion-trigger) 257 if (!$trigger.is("button, summary, a") && !$trigger.hasClass("accordion-trigger")) { 258 console.log("Skipping non-trigger element:", $trigger.prop("tagName")); 259 return; 260 } 261 262 // EC-ACCORDION-011: Track <a> accordion triggers â defer preventDefault 263 // until we confirm a content panel exists. If no panel, let the link navigate. 264 var isAccordionLink = $trigger.is("a") && $trigger.closest("h3[data-state]").length; 265 266 // CRITICAL: Skip menu/hamburger buttons - these are handled separately 267 var ariaLabel = ($trigger.attr("aria-label") || "").toLowerCase(); 268 if (ariaLabel.indexOf("menu") !== -1 || $trigger.hasClass("menu-toggle") || $trigger.hasClass("hamburger-menu")) { 269 console.log("Skipping accordion handler for menu button"); 270 return; // Let the hamburger menu handler deal with this 271 } 272 273 var targetId = $trigger.attr("aria-controls"); 274 var triggerId = $trigger.attr("id"); 275 var $content = null; 276 277 console.log("Accordion clicked, targetId:", targetId, "triggerId:", triggerId); 278 279 // Method 1: aria-controls attribute points to content ID 280 if (targetId) { 281 $content = $(document.getElementById(targetId)); 282 if ($content.length) { 283 console.log("Found content via aria-controls"); 284 } 285 } 286 287 // Method 2: Radix UI pattern - look for [role="region"] sibling with aria-labelledby matching trigger 288 if ((!$content || !$content.length) && triggerId) { 289 var $radixContent = $trigger.siblings('[role="region"][aria-labelledby="' + triggerId + '"]'); 290 if ($radixContent.length) { 291 $content = $radixContent; 292 console.log("Found content via Radix aria-labelledby"); 293 } 294 } 295 296 // Method 3: Look for next sibling with [role="region"] or [data-state] (Radix pattern) 297 if (!$content || !$content.length) { 298 var $radixSibling = $trigger.next('[role="region"], [data-state]'); 299 if ($radixSibling.length) { 300 $content = $radixSibling; 301 console.log("Found content via Radix sibling role/data-state"); 302 } 303 } 304 305 // Method 4: Look for next sibling element (FAQ-style accordions) 306 // Common pattern: <button aria-expanded="false">Question</button><div style="max-height:0">Answer</div> 307 if (!$content || !$content.length) { 308 var $nextSibling = $trigger.next('div, [class*="content"], [class*="panel"], [class*="answer"]'); 309 if ($nextSibling.length) { 310 $content = $nextSibling; 311 console.log("Using sibling element as accordion content"); 312 } 313 } 314 315 // Method 5: Content is inside a parent container, sibling of the trigger wrapper 316 if (!$content || !$content.length) { 317 var $parent = $trigger.parent(); 318 var $siblingContent = $parent.next('div, [role="region"], [class*="content"], [class*="panel"]'); 319 if ($siblingContent.length) { 320 $content = $siblingContent; 321 console.log("Using parent sibling as accordion content"); 322 } 323 } 324 325 // Method 6: Look within the accordion item container for content 326 if (!$content || !$content.length) { 327 var $accordionItem = $trigger.closest('[data-state], [class*="accordion-item"], [class*="AccordionItem"]'); 328 if ($accordionItem.length) { 329 var $itemContent = $accordionItem.find('[role="region"], [data-state="open"], [data-state="closed"], [class*="content"], [class*="panel"]').not($trigger); 330 if ($itemContent.length) { 331 $content = $itemContent.first(); 332 console.log("Found content within accordion item container"); 333 } 334 } 335 } 336 337 // Method 7: Global search for aria-labelledby matching trigger ID (Radix UI) 338 if ((!$content || !$content.length) && triggerId) { 339 var $globalContent = $('[aria-labelledby="' + triggerId + '"]'); 340 if ($globalContent.length) { 341 $content = $globalContent.first(); 342 console.log("Found content via global aria-labelledby search"); 343 } 344 } 345 346 // Method 8: Last resort - find any sibling div that has content 347 if (!$content || !$content.length) { 348 var $anySibling = $trigger.siblings('div').filter(function() { 349 return $(this).text().trim().length > 20; 350 }); 351 if ($anySibling.length) { 352 $content = $anySibling.first(); 353 console.log("Found content via any sibling div with content"); 354 } 355 } 356 357 if ($content && $content.length) { 358 // EC-ACCORDION-011: Now that we confirmed a panel exists, 359 // prevent <a> link navigation so the accordion can toggle 360 if (isAccordionLink) e.preventDefault(); 361 362 // Also update data-state on the parent h3 trigger wrapper 363 var $h3 = $trigger.closest("h3[data-state]"); 364 365 // Detect if this accordion uses CSS max-height/opacity transitions 366 var usesMaxHeight = $content.css("max-height") !== undefined && $content.css("max-height") !== "none"; 367 var currentMaxHeight = $content.css("max-height"); 368 var currentHeight = $content.css("height"); 369 var isHidden = $content.attr("hidden") !== undefined || 370 $content.css("display") === "none" || 371 (usesMaxHeight && (currentMaxHeight === "0px" || currentMaxHeight === "0")) || 372 (currentHeight === "0px" || currentHeight === "0"); 373 374 // Also check aria-expanded on trigger as source of truth
375 if ($trigger.attr("aria-expanded") === "false") { 376 isHidden = true; 377 } else if ($trigger.attr("aria-expanded") === "true") { 378 isHidden = false; 379 } 380 381 // Also check data-state attribute 382 if ($content.attr("data-state") === "closed") { 383 isHidden = true; 384 } else if ($content.attr("data-state") === "open") { 385 isHidden = false; 386 } 387 388 if (isHidden) { 389 // Show - handle both hidden attribute and CSS transitions 390 // CRITICAL: Remove ALL hiding first, then measure, then apply final styles 391 $content.removeAttr("hidden"); 392 $content.removeAttr("aria-hidden"); 393 394 // CRITICAL: Must make element visible BEFORE measuring scrollHeight 395 // Otherwise scrollHeight will be 0 for display:none elements 396 $content.css({ 397 "display": "block", 398 "visibility": "visible", 399 "opacity": "1", 400 "height": "auto", 401 "max-height": "none", 402 "overflow": "visible", 403 "animation": "none" 404 }); 405 406 // Now get scroll height (element is visible so this will work) 407 var scrollHeight = $content.prop("scrollHeight"); 408 console.log("Accordion scrollHeight:", scrollHeight); 409 410 // If scrollHeight is still 0, the content might be truly empty 411 // In that case, just leave it visible with auto height 412 if (scrollHeight === 0) { 413 console.log("Warning: scrollHeight is 0, content may be empty"); 414 } 415 416 $content.attr("data-state", "open"); 417 $content.removeAttr("aria-hidden"); 418 $trigger.attr("aria-expanded", "true"); 419 $trigger.attr("data-state", "open"); 420 if ($h3 && $h3.length) $h3.attr("data-state", "open"); 421 422 // Rotate chevron icon if present 423 $trigger.find('svg, .chevron, [class*="chevron"]').css("transform", "rotate(180deg)"); 424 if ($h3 && $h3.length) $h3.find('svg').last().css("transform", "rotate(180deg)"); 425 426 console.log("Accordion expanded"); 427 } else { 428 // Hide - handle both hidden attribute and CSS transitions 429 $content.css({ 430 "max-height": "0px", 431 "height": "0px", 432 "opacity": "0", 433 "overflow": "hidden", 434 "display": "block", 435 "animation": "none" 436 }); 437 438 $content.attr("data-state", "closed"); 439 $trigger.attr("aria-expanded", "false"); 440 $trigger.attr("data-state", "closed"); 441 if ($h3 && $h3.length) $h3.attr("data-state", "closed"); 442 443 // Reset chevron icon rotation 444 $trigger.find('svg, .chevron, [class*="chevron"]').css("transform", "rotate(0deg)"); 445 if ($h3 && $h3.length) $h3.find('svg').last().css("transform", "rotate(0deg)"); 446 447 console.log("Accordion collapsed"); 448 } 449 } else { 450 console.warn("Content div not found for accordion trigger"); 451 } 452 }); 453 454 // On page load, check for empty accordion content 455 $("[aria-controls]").each(function() { 456 var targetId = $(this).attr("aria-controls"); 457 if (targetId) { 458 var $content = $(document.getElementById(targetId)); 459 if ($content.length && $content.html().trim() === "") { 460 console.warn("Empty accordion content detected for:", targetId); 461 } 462 } 463 }); 464 } catch(e) { console.warn("[WPConvert] Accordion error:", e); } 465 466 try { // [WPConvert: CSS Max-Height Accordion] EC-FAQ-002b 467 // Handles FAQ/accordion buttons that use CSS max-h-0/max-h-96 toggling 468 // instead of aria attributes. These buttons have a sibling div with 469 // Tailwind max-height + opacity transition classes. 470 (function() { 471 function isCssAccordionBtn(btn) { 472 var $btn = $(btn); 473 var $sib = $btn.next("div"); 474 if (!$sib.length) return false;
475 var cls = " " + ($sib.attr("class") || "") + " "; 476 return (cls.indexOf("max-h-0") !== -1 || cls.indexOf("max-h-96") !== -1) && cls.indexOf("transition") !== -1; 477 } 478 479 $(document).on("click", "button", function(e) { 480 var btn = this; 481 // EC-ACCORDION-012: image-card CTA buttons are guarded. 482 if ($(btn).attr("data-wpc-no-accordion-trigger") === "1") return; 483 if (!isCssAccordionBtn(btn)) return; 484 485 // Already handled by aria accordion handler 486 if ($(btn).attr("aria-controls") || $(btn).attr("aria-expanded")) return; 487 488 e.preventDefault(); 489 e.stopPropagation(); 490 491 var $btn = $(btn); 492 var $content = $btn.next("div"); 493 var cls = $content.attr("class") || ""; 494 var isOpen = cls.indexOf("max-h-96") !== -1 && cls.indexOf("max-h-0") === -1; 495 496 // Close all siblings in the same container first (accordion behavior) 497 var $container = $btn.parent().parent(); 498 $container.find("> div > button").each(function() { 499 if (this === btn) return; 500 var $otherContent = $(this).next("div"); 501 var otherCls = $otherContent.attr("class") || ""; 502 if (otherCls.indexOf("max-h-96") !== -1) { 503 $otherContent.attr("class", otherCls.replace(/max-h-96/g, "max-h-0").replace(/opacity-100/g, "opacity-0")); 504 // Reset icon rotation 505 $(this).find(".rotate-45").removeClass("rotate-45"); 506 $(this).find(".bg-primary.text-white").removeClass("bg-primary text-white"); 507 // Reset parent border 508 var $item = $(this).closest("div[class*='border']"); 509 if ($item.length) { 510 var ic = $item.attr("class") || ""; 511 $item.attr("class", ic.replace(/border-primary(?!\/)(?! shadow)/g, "border-primary/5").replace(/ shadow-md/g, "")); 512 } 513 // Reset question text style 514 var $qspan = $(this).find("span").first(); 515 var qc = $qspan.attr("class") || ""; 516 $qspan.attr("class", qc.replace(/text-primary font-bold/, "text-primary/70")); 517 } 518 }); 519 520 if (isOpen) { 521 // Close this item 522 $content.attr("class", cls.replace(/max-h-96/g, "max-h-0").replace(/opacity-100/g, "opacity-0")); 523 $btn.find(".rotate-45").removeClass("rotate-45"); 524 $btn.find(".bg-primary.text-white").removeClass("bg-primary text-white"); 525 var $parentItem = $btn.closest("div[class*='border']"); 526 if ($parentItem.length) { 527 var pc = $parentItem.attr("class") || ""; 528 $parentItem.attr("class", pc.replace(/border-primary(?!\/)(?! shadow)/g, "border-primary/5").replace(/ shadow-md/g, "")); 529 } 530 var $qs = $btn.find("span").first(); 531 var qsc = $qs.attr("class") || ""; 532 $qs.attr("class", qsc.replace(/text-primary font-bold/, "text-primary/70")); 533 } else { 534 // Open this item 535 $content.attr("class", cls.replace(/max-h-0/g, "max-h-96").replace(/opacity-0/g, "opacity-100")); 536 // Rotate plus icon to X 537 var $iconSpan = $btn.find("span").last();
538 if ($iconSpan.find("svg").length) { 539 $iconSpan.addClass("rotate-45 bg-primary text-white"); 540 } 541 var $parentItem = $btn.closest("div[class*='border']"); 542 if ($parentItem.length) { 543 var pc = $parentItem.attr("class") || ""; 544 $parentItem.attr("class", pc.replace(/border-primary\/5/g, "border-primary") + " shadow-md"); 545 } 546 var $qs = $btn.find("span").first(); 547 var qsc = $qs.attr("class") || ""; 548 $qs.attr("class", qsc.replace(/text-primary\/70/, "text-primary font-bold")); 549 } 550 }); 551 })(); 552 } catch(e) { console.warn("[WPConvert] CSS accordion error:", e); } 553 554 try { // [WPConvert: Pill Tab Group Toggle] EC-FAQ-002b 555 // Handles pill-style tab button groups (rounded-full buttons in a flex container) 556 // where one button is "active" (bg-primary text-white) and others are inactive. 557 // Toggles active styling on click. Content filtering is separate (EC-FILTER-001). 558 (function() { 559 $("button.rounded-full, button[class*='rounded-full']").each(function() { 560 var $btn = $(this); 561 var $container = $btn.parent(); 562 if ($container.data("wpc-pill-init")) return; 563 var $pills = $container.find("> button[class*='rounded-full']"); 564 if ($pills.length < 3) return; 565 // Verify it's a pill group: check for flex container + buttons with similar sizing 566 var containerCls = $container.attr("class") || ""; 567 if (containerCls.indexOf("flex") === -1) return; 568 $container.data("wpc-pill-init", true); 569 570 // Find active and inactive class patterns 571 var activeBtn = null, inactiveBtn = null; 572 $pills.each(function() { 573 var cls = $(this).attr("class") || ""; 574 if (cls.indexOf("bg-primary") !== -1 && cls.indexOf("text-white") !== -1 && cls.indexOf("text-white/") === -1) activeBtn = this; 575 else if (!inactiveBtn) inactiveBtn = this; 576 }); 577 if (!activeBtn || !inactiveBtn) return; 578 579 var activeCls = $(activeBtn).attr("class"); 580 var inactiveCls = $(inactiveBtn).attr("class"); 581 582 $pills.on("click", function(e) { 583 e.preventDefault(); 584 $pills.each(function() { $(this).attr("class", inactiveCls); }); 585 $(this).attr("class", activeCls); 586 var cat = $(this).attr("data-wpconvert-faq-cat"); 587 if (cat) { 588 var $faq = $("#faq"); 589 if ($faq.length) { 590 $faq.find("[data-wpconvert-faq-panel]").each(function() { 591 var on = ($(this).attr("data-wpconvert-faq-panel") || "") === cat; 592 $(this).toggle(on); 593 }); 594 } 595 } 596 }); 597 }); 598 })(); 599 } catch(e) { console.warn("[WPConvert] Pill tab error:", e); } 600 601 try { // [WPConvert: Category/Filter Buttons] 602 // EC-FILTER-001: Universal category/filter button functionality 603 // Detects filter button groups near grids/lists and enables click-to-filter 604 // Searches section, main, container, and generic wrapper elements 605 (function() { 606 var filterInitialized = {}; 607 608 function initFilter($btnGroup, $gridContainer, allowNoAll) { 609 var groupId = $btnGroup.get(0); 610 if (filterInitialized[groupId]) return; 611 if ($btnGroup.attr("data-wpc-filter-group")) return; 612 613 var $filterButtons = $btnGroup.find("> button, > [role='button'], > a").filter(function() { 614 var t = $(this).text().trim(); 615 return t.length > 0 && t.length < 30 && !/submit|send|cancel|close|search|login|sign/i.test(t); 616 });
617 // EC-FILTER-006: Allow 2+ buttons when explicitly permitted (tab-style groups) 618 if ($filterButtons.length < 2) return; 619 if ($filterButtons.length < 3 && !allowNoAll) return; 620 621 var hasAllBtn = false; 622 $filterButtons.each(function() { 623 var t = $(this).text().trim().toLowerCase(); 624 // EC-FILTER-010: also match "all products", "all items", etc. 625 if (t === "all" || t === "view all" || t === "show all" || /^all\b/.test(t)) hasAllBtn = true; 626 }); 627 // EC-FILTER-006: Skip "All" requirement when caller explicitly allows it 628 if (!hasAllBtn && !allowNoAll) return; 629 630 var $filterableItems = $gridContainer.find("[class*='card'], [class*='product'], [class*='item'], article"); 631 if ($filterableItems.length === 0) { 632 $filterableItems = $gridContainer.children("a, div, article, li"); 633 } 634 if ($filterableItems.length < 2) return; 635 636 filterInitialized[groupId] = true; 637 console.log("[WPConvert] Category filter detected:", $filterButtons.length, "buttons,", $filterableItems.length, "items"); 638 639 // EC-FILTER-004: Inject category badges at runtime for cards with data-category 640 $filterableItems.each(function() { 641 var $item = $(this); 642 var cat = $item.attr("data-category"); 643 if (!cat) return; 644 if ($item.find(".wpconvert-category-badge").length) return; 645 var $imgBox = $item.find("[class*='aspect-video']").first(); 646 if (!$imgBox.length) $imgBox = $item.find("div.relative").filter(function() { return $(this).find("img").length > 0; }).first(); 647 if (!$imgBox.length) $imgBox = $item.find("div").filter(function() { return $(this).find("img").length > 0; }).first(); 648 if ($imgBox.length) { 649 if ($imgBox.css("position") !== "absolute" && $imgBox.css("position") !== "relative") $imgBox.css("position", "relative"); 650 $imgBox.append('<div class="wpconvert-category-badge" style="position:absolute;top:1rem;right:1rem;padding:0.25rem 0.75rem;border-radius:9999px;font-size:0.75rem;font-weight:500;z-index:10;pointer-events:none;background:var(--primary,hsl(186 100% 50%));color:var(--primary-foreground,#000);opacity:0.9">' + $("<span>").text(cat).html() + "</div>"); 651 } 652 }); 653 654 function matchesCategory($item, filterText) { 655 var dataCat = ($item.attr("data-category") || $item.attr("data-filter") || $item.attr("data-type") || "").toLowerCase(); 656 if (dataCat && dataCat === filterText) return true; 657 if (dataCat && dataCat.indexOf(filterText) !== -1) return true; 658 659 var fullText = $item.text().toLowerCase(); 660 var imgAlts = []; 661 $item.find("img").each(function() { imgAlts.push(($(this).attr("alt") || "").toLowerCase()); }); 662 var searchText = fullText + " " + imgAlts.join(" ") + " " + ($item.attr("href") || "").toLowerCase() + " " + ($item.attr("class") || "").toLowerCase(); 663 664 if (searchText.indexOf(filterText) !== -1) return true; 665 666 var words = filterText.split(/\s+/).filter(function(w) { return w.length > 1; }); 667 if (words.length > 1) { 668 var allMatch = true; 669 for (var i = 0; i < words.length; i++) { 670 var w = words[i]; 671 if (searchText.indexOf(w) === -1) { 672 var stem = w.replace(/s$/, ""); 673 if (stem.length > 2 && searchText.indexOf(stem) !== -1) continue; 674 allMatch = false; break; 675 } 676 } 677 if (allMatch) return true; 678 } 679 return false;
680 } 681 682 var origBtnStyles = []; 683 $filterButtons.each(function() { origBtnStyles.push($(this).attr("class") || ""); }); 684 685 $filterButtons.on("click", function(e) { 686 e.preventDefault(); 687 e.stopPropagation(); 688 var $clicked = $(this); 689 var filterText = $clicked.text().trim().toLowerCase(); 690 691 $filterButtons.each(function(i) { 692 $(this).attr("class", origBtnStyles[i]); 693 $(this).css("opacity", "0.6"); 694 }); 695 $clicked.css("opacity", "1"); 696 697 // EC-FILTER-005: Iterate grid children (not filterable items) so 698 // each cell is toggled exactly once. Finds the categorized element 699 // inside each cell for matching, hides/shows the cell itself so 700 // CSS Grid reflows with no gaps from empty wrapper divs. 701 // EC-FILTER-006/010: Also check if this is the "show all" equivalent 702 if (filterText === "all" || filterText === "view all" || filterText === "show all" || /^all\b/.test(filterText)) { 703 $gridContainer.children().each(function() { 704 $(this).css("display", ""); 705 }); 706 return; 707 } 708 // EC-FILTER-006: For tab-style groups without "All", if no grid items 709 // match ANY button text, treat buttons as content panel toggles instead 710 // of category filters (handled by pricing toggle / tab toggle below) 711 712 var visibleCount = 0; 713 $gridContainer.children().each(function() { 714 var $cell = $(this); 715 var $primary = $cell.is("[data-category]") ? $cell : $cell.find("[data-category]").first(); 716 if (!$primary.length) $primary = $cell; 717 if (matchesCategory($primary, filterText)) { 718 $cell.css("display", ""); 719 visibleCount++; 720 } else { 721 $cell.css("display", "none"); 722 } 723 }); 724 console.log("[WPConvert] Filtered to", filterText + ":", visibleCount, "items visible"); 725 }); 726 } 727 728 // Search broad set of containers (not just section) 729 $("section, main, [role='main'], [class*='container'], [class*='wrapper'], [class*='content'], [class*='page']").each(function() { 730 var $root = $(this); 731 732 // Strategy 1: Find div with 3+ child buttons (one being "All") followed by a grid 733 $root.find("div, [class*='flex'], [class*='tabs'], [class*='button-group'], [class*='filter']").each(function() { 734 var $candidate = $(this); 735 var directBtns = $candidate.find("> button, > [role='button'], > a[class*='button']"); 736 if (directBtns.length < 3) return; 737 var hasAll = false; 738 directBtns.each(function() { 739 var t = $(this).text().trim().toLowerCase(); 740 // EC-FILTER-010: also match "all products", "all items", etc. 741 if (t === "all" || t === "view all" || t === "show all" || /^all\b/.test(t)) hasAll = true; 742 }); 743 if (!hasAll) return; 744 745 // Find the grid: next sibling, parent siblings, ancestors, or section-wide 746 var $grid = $candidate.nextAll("[class*='grid']").first(); 747 if (!$grid.length) $grid = $candidate.nextAll("div").find("[class*='grid']").first(); 748 if (!$grid.length) $grid = $candidate.parent().find("[class*='grid'], [class*='list']").not($candidate).first(); 749 if (!$grid.length) $grid = $candidate.parent().nextAll("[class*='grid']").first(); 750 if (!$grid.length) $grid = $candidate.parent().parent().find("> [class*='grid']").first(); 751 if (!$grid.length) $grid = $candidate.closest("section, [class*='container'], [class*='wrapper']").find("[class*='grid']").filter(function() { return $(this).children().length >= 2; }).first();
752 // EC-FILTER-010: cross-section â grid may be in the next sibling section 753 if (!$grid.length) { 754 var $sect = $candidate.closest("section"); 755 if ($sect.length) $grid = $sect.next("section").find("[class*='grid']").filter(function() { return $(this).children().length >= 2; }).first(); 756 } 757 if (!$grid.length) $grid = $candidate.nextAll("div, ul").first(); 758 if ($grid.length) initFilter($candidate, $grid); 759 }); 760 761 // Strategy 2: Heading-based detection (original approach, broadened) 762 var $heading = $root.find("h1, h2, h3").first(); 763 if (!$heading.length) return; 764 var $headingParent = $heading.parent(); 765 var $btnGroup = $headingParent.next("div, [class*='flex']").filter(function() { 766 return $(this).find("> button, > [role='button']").length >= 3; 767 }).first(); 768 if (!$btnGroup.length) { 769 $btnGroup = $headingParent.nextAll("div, [class*='flex']").filter(function() { 770 return $(this).find("> button, > [role='button']").length >= 3; 771 }).first(); 772 } 773 if ($btnGroup.length) { 774 var $grid = $btnGroup.nextAll("[class*='grid'], [class*='list'], div").first(); 775 if (!$grid.length) $grid = $btnGroup.parent().nextAll("[class*='grid']").first(); 776 if (!$grid.length) $grid = $btnGroup.closest("section, [class*='container']").find("[class*='grid']").filter(function() { return $(this).children().length >= 2; }).first(); 777 if ($grid.length) initFilter($btnGroup, $grid); 778 } 779 }); 780 781 // EC-FILTER-006 Strategy 3: Tab-style button groups without "All" button 782 // Detects 2+ sibling buttons in a flex container near a grid, even without "All" 783 // EC-FILTER-008: Only initializes when grid items have data-category attributes, 784 // preventing false positives on tab-panel UIs (services, pricing) where clicking 785 // a tab should switch content panels, not filter a grid. 786 $("section, main, [role='main']").each(function() { 787 var $root = $(this); 788 $root.find("div, [class*='flex'], [class*='tabs']").each(function() { 789 var $candidate = $(this); 790 if (filterInitialized[$candidate.get(0)]) return; 791 if ($candidate.attr("data-wpc-filter-group")) return; 792 var directBtns = $candidate.find("> button, > [role='button'], > a[class*='button']").filter(function() { 793 var t = $(this).text().trim(); 794 return t.length > 0 && t.length < 30 && !/submit|send|cancel|close|search|login|sign/i.test(t); 795 }); 796 if (directBtns.length < 2 || directBtns.length > 8) return; 797 // Must NOT already have an "All" button (handled by strategies 1/2) 798 var hasAll = false; 799 directBtns.each(function() { 800 var t = $(this).text().trim().toLowerCase(); 801 // EC-FILTER-010: also match "all products", "all items", etc. 802 if (t === "all" || t === "view all" || t === "show all" || /^all\b/.test(t)) hasAll = true; 803 }); 804 if (hasAll) return; 805 // Look for a nearby grid 806 var $grid = $candidate.nextAll("[class*='grid']").first(); 807 if (!$grid.length) $grid = $candidate.parent().find("[class*='grid']").not($candidate).first(); 808 if (!$grid.length) $grid = $candidate.parent().nextAll("[class*='grid']").first(); 809 if (!$grid.length) $grid = $candidate.parent().parent().find("> [class*='grid']").first(); 810 if (!$grid.length) $grid = $candidate.closest("section, [class*='container']").find("[class*='grid']").filter(function() { return $(this).children().length >= 2; }).first();
811 // EC-FILTER-010: cross-section â grid may be in the next sibling section 812 if (!$grid.length) { 813 var $sect = $candidate.closest("section"); 814 if ($sect.length) $grid = $sect.next("section").find("[class*='grid']").filter(function() { return $(this).children().length >= 2; }).first(); 815 } 816 if (!$grid.length) $grid = $candidate.nextAll("div").first(); 817 if ($grid.length && $grid.children().length >= 2) { 818 // EC-FILTER-008: Only initialize as filter when items are actually 819 // categorized. Tab panels (service tabs, pricing tabs) have a single 820 // content area without data-category â filtering would hide everything. 821 var categorizedCount = 0; 822 $grid.children().each(function() { 823 if ($(this).is("[data-category]") || $(this).find("[data-category]").length) categorizedCount++; 824 }); 825 if (categorizedCount < 2) { 826 console.log("[WPConvert] Strategy 3 skipped: only " + categorizedCount + " categorized items in grid (need 2+). Likely a tab panel, not a filter."); 827 return; 828 } 829 initFilter($candidate, $grid, true); 830 } 831 }); 832 }); 833 })(); 834 } catch(e) { console.warn("[WPConvert] Category filter error:", e); } 835 836 try { // [WPConvert: EC-PRICING-001 Pricing Toggle] 837 // EC-PRICING-001: Universal pricing toggle (Monthly/Annual, Monthly/Yearly, etc.) 838 // Handles Radix Switch (button[role="switch"]) and 2-button/label toggle patterns 839 (function() { 840 var periodRx = /monthly|annual|yearly|quarterly|weekly/i; 841 function findPricingGrid($from) { 842 var $g = $from.nextAll("[class*='grid']").first(); 843 if (!$g.length) $g = $from.parent().nextAll("[class*='grid']").first(); 844 if (!$g.length) $g = $from.parent().parent().find("[class*='grid']").filter(function() { return $(this).children().length >= 2; }).first(); 845 if (!$g.length) $g = $from.closest("section").find("[class*='grid']").filter(function() { return $(this).children().length >= 2; }).first(); 846 // EC-PRICING-002: Also search the NEXT sibling section. 847 // Common pattern: hero section has the toggle, separate pricing section has the grid. 848 if (!$g.length) { 849 var $sec = $from.closest("section"); 850 if ($sec.length) { 851 $g = $sec.nextAll("section").first().find("[class*='grid']").filter(function() { 852 return $(this).children().length >= 2; 853 }).first(); 854 } 855 } 856 return $g; 857 } 858 // Pattern A: Radix Switch â span(Monthly) + button[role=switch] + span(Annual) 859 $('button[role="switch"]').each(function() { 860 var $sw = $(this); 861 if ($sw.data("pricing-wired")) return; 862 var $parent = $sw.parent(); 863 var kids = $parent.children(); 864 var labels = []; 865 kids.each(function() { 866 var tag = (this.tagName || "").toLowerCase(); 867 var txt = $(this).text().trim(); 868 if ((tag === "span" || tag === "label" || tag === "p") && periodRx.test(txt)) { 869 labels.push({ el: $(this), text: txt.toLowerCase() }); 870 } 871 }); 872 if (labels.length < 2) return; 873 var $grid = findPricingGrid($parent); 874 if (!$grid.length) return; 875 $sw.data("pricing-wired", true); 876 var isAnnual = $sw.attr("data-state") === "checked" || $sw.attr("aria-checked") === "true"; 877 // Read pricing data from section attribute if available 878 var pricingData = null; 879 var rawPD = $sw.closest("section, [class*='pricing']").attr("data-wpconvert-pricing"); 880 // EC-PRICING-002: Also check the grid's parent section (toggle may be in hero, 881 // pricing data attribute on the pricing section) 882 if (!rawPD && $grid.length) rawPD = $grid.closest("section, [class*='pricing']").attr("data-wpconvert-pricing"); 883 if (rawPD) try { pricingData = JSON.parse(rawPD); } catch(e) {} 884 // Find price elements in each card 885 var cardPrices = []; 886 $grid.children().each(function(ci) { 887 var $card = $(this); 888 var $priceEl = $card.find("span, div").filter(function() { 889 return /^\$[\d,]+/.test($(this).text().trim()) && $(this).children().length === 0; 890 }).first(); 891 if (!$priceEl.length) $priceEl = $card.find("[class*='font-black'], [class*='font-extrabold']").filter(function() { 892 return /\$/.test($(this).text().trim()); 893 }).first(); 894 var monthly = null, annual = null; 895 if (pricingData && pricingData[ci]) { 896 monthly = pricingData[ci].monthly; 897 annual = pricingData[ci].annual; 898 } 899 cardPrices.push({ el: $priceEl, monthly: monthly, annual: annual }); 900 }); 901 // Toggle label styling 902 function updateLabels(ann) {
903 labels.forEach(function(l) { 904 var isMonthly = /month/i.test(l.text); 905 if ((isMonthly && !ann) || (!isMonthly && ann)) { 906 l.el.removeClass("text-slate-500 text-gray-500 text-muted").addClass("text-white"); 907 } else { 908 l.el.removeClass("text-white").addClass("text-slate-500"); 909 } 910 }); 911 } 912 function updatePrices(ann) { 913 cardPrices.forEach(function(cp) { 914 if (!cp.el.length) return; 915 var price = ann ? cp.annual : cp.monthly; 916 if (price !== null) cp.el.text("$" + Number(price).toLocaleString()); 917 }); 918 } 919 $sw.on("click", function() { 920 isAnnual = !isAnnual; 921 var newState = isAnnual ? "checked" : "unchecked"; 922 $sw.attr("data-state", newState).attr("aria-checked", String(isAnnual)); 923 $sw.find("[data-state]").attr("data-state", newState); 924 updateLabels(isAnnual); 925 updatePrices(isAnnual); 926 console.log("[EC-PRICING-001] Switch toggled:", isAnnual ? "annual" : "monthly"); 927 }); 928 console.log("[EC-PRICING-001] Radix Switch pricing toggle initialized, cards:", cardPrices.length); 929 }); 930 // Pattern B: Two buttons/labels with pricing period text (non-switch) 931 $("section, [class*='pricing']").each(function() { 932 var $sec = $(this); 933 if ($sec.data("pricing-toggle-init")) return; 934 $sec.find("div").each(function() { 935 var $group = $(this); 936 var $btns = $group.find("> button, > [role='button'], > label, > a").filter(function() { 937 return periodRx.test($(this).text().trim()); 938 }); 939 if ($btns.length !== 2) return; 940 if ($btns.filter('[role="switch"]').length > 0) return; 941 var $grid = findPricingGrid($group); 942 if (!$grid.length) return; 943 $sec.data("pricing-toggle-init", true); 944 var pricingData = null; 945 var rawPD = $sec.attr("data-wpconvert-pricing"); 946 if (rawPD) try { pricingData = JSON.parse(rawPD); } catch(e) {} 947 var cardPrices = []; 948 $grid.children().each(function(ci) { 949 var $card = $(this); 950 var $priceEl = $card.find("span, div").filter(function() { 951 return /^\$[\d,]+/.test($(this).text().trim()) && $(this).children().length === 0; 952 }).first(); 953 var monthly = null, annual = null; 954 if (pricingData && pricingData[ci]) { 955 monthly = pricingData[ci].monthly; 956 annual = pricingData[ci].annual; 957 } 958 cardPrices.push({ el: $priceEl, monthly: monthly, annual: annual }); 959 }); 960 var fc = ($btns.eq(0).attr("class")||"").split(/\s+/); 961 var sc = ($btns.eq(1).attr("class")||"").split(/\s+/); 962 var ao = fc.filter(function(c){return sc.indexOf(c)===-1&&c;}); 963 var io = sc.filter(function(c){return fc.indexOf(c)===-1&&c;}); 964 $btns.on("click", function(e) { 965 e.preventDefault(); 966 var idx = $btns.index($(this)); 967 var ann = idx === 1; 968 $btns.each(function(bi) { 969 var $b = $(this);
970 if (bi === idx) { io.forEach(function(c){$b.removeClass(c);}); ao.forEach(function(c){$b.addClass(c);}); } 971 else { ao.forEach(function(c){$b.removeClass(c);}); io.forEach(function(c){$b.addClass(c);}); } 972 }); 973 cardPrices.forEach(function(cp) { 974 if (!cp.el.length) return; 975 var price = ann ? cp.annual : cp.monthly; 976 if (price !== null) cp.el.text("$" + Number(price).toLocaleString()); 977 }); 978 }); 979 console.log("[EC-PRICING-001] Button pricing toggle initialized"); 980 }); 981 }); 982 })(); 983 } catch(e) { console.warn("[WPConvert] Pricing toggle error:", e); } 984 985 try { // [WPConvert: EC-BLOG-030 Inline Blog Search] 986 // EC-BLOG-030: Wire inline search inputs near post/card grids for client-side filtering 987 // Covers static blog pages where search input exists but isn't connected to anything 988 // EC-BLOG-030b: Skip inputs inside [data-wpconvert-form], skip <form> grids, 989 // and use word boundaries for "post" so "Postcode" does not false-positive. 990 (function() { 991 var allInputs = $('input[type="text"], input[type="search"], input:not([type])').not('#wpconvert-search-input'); 992 allInputs.each(function() { 993 var $inp = $(this); 994 if ($inp.data("wpc-search-init")) return; 995 if ($inp.closest("[data-wpconvert-form]").length) return; 996 var ph = ($inp.attr("placeholder") || "").toLowerCase(); 997 if (!/search|filter|find|blog|\bposts?\b|article/i.test(ph)) return; 998 // Find nearby card grid (skip <form> elements â they are real forms, not card grids) 999 var $section = $inp.closest("section, main, [class*='container']"); 1000 if (!$section.length) $section = $inp.parent().parent().parent(); 1001 var $grid = $section.find("[class*='grid']").filter(function() { 1002 return $(this).children().length >= 2 && !$(this).is("form"); 1003 }).first(); 1004 if (!$grid.length) return; 1005 var $cards = $grid.children(); 1006 if ($cards.length < 2) return; 1007 $inp.data("wpc-search-init", true); 1008 var debounce; 1009 $inp.on("input", function() { 1010 clearTimeout(debounce); 1011 debounce = setTimeout(function() { 1012 var q = $inp.val().trim().toLowerCase(); 1013 $cards.each(function() { 1014 var text = $(this).text().toLowerCase(); 1015 $(this).toggle(!q || text.indexOf(q) !== -1); 1016 }); 1017 }, 150); 1018 }); 1019 console.log("[EC-BLOG-030] Blog search input wired:", $cards.length, "cards"); 1020 }); 1021 })(); 1022 } catch(e) { console.warn("[WPConvert] Blog search error:", e); } 1023 1024 try { // [WPConvert: Nav Link Injection] 1025 // Inject navigation links from menus.json (for React/Vite sites with direct <a> links) 1026 var $navContainer = $("[data-wpconvert-nav-container]"); 1027 if ($navContainer.length) { 1028 console.log("Found navigation container, loading menu items..."); 1029 1030 // Load menu data 1031 $.getJSON("<?php echo get_template_directory_uri(); ?>/assets/data/menus.json", function(menuData) { 1032 if (menuData && menuData.primary && menuData.primary.items) { 1033 var items = menuData.primary.items; 1034 var $template = $navContainer.find("[data-wpconvert-nav-template]"); 1035 var templateClasses = $template.attr("class") || ""; 1036 var $parent = $template.parent(); 1037 1038 console.log("Menu items to inject:", items.length); 1039 1040 // Remove the template link 1041 $template.remove(); 1042 1043 // Inject menu items
1044 items.forEach(function(item) { 1045 var $link = $("<a></a>"); 1046 $link.attr("href", item.url); 1047 $link.text(item.title); 1048 1049 // Preserve original classes from template 1050 if (item.classes && item.classes.length > 0) { 1051 $link.attr("class", item.classes.join(" ")); 1052 } else { 1053 $link.attr("class", templateClasses); 1054 } 1055 1056 // Add any custom attributes 1057 if (item.attributes) { 1058 Object.keys(item.attributes).forEach(function(key) { 1059 if (key !== "href" && key !== "class") { 1060 $link.attr(key, item.attributes[key]); 1061 } 1062 }); 1063 } 1064 1065 $parent.append($link); 1066 }); 1067 1068 console.log("Navigation links injected successfully"); 1069 } 1070 }).fail(function() { 1071 console.warn("Could not load menus.json, navigation may not work correctly"); 1072 }); 1073 } 1074 } catch(e) { console.warn("[WPConvert] Nav link injection error:", e); } 1075 1076 try { // [WPConvert: Slide-out Panels & Sidebar Layout] 1077 // EC-SB-001: Handle slide-out navigation panels (sidebar-controlled menus) 1078 // These are full-height panels that slide in from the side, controlled by sidebar buttons 1079 var $slideOutPanels = $('nav[data-wpconvert-slideout-panel="true"]'); 1080 var $slideOutToggleButtons = $('button[data-wpconvert-slideout-toggle]'); 1081 1082 // EC-SB-003: Hide default footer for full-viewport sidebar layouts 1083 // These sites have fixed sidebar + full-height content - no traditional footer 1084 var hasFixedSidebar = false; 1085 var hasFullHeightLayout = false; 1086 1087 // Check for fixed sidebar - look for aside with fixed positioning classes 1088 $('aside').each(function() { 1089 var classes = ($(this).attr('class') || ''); 1090 if (classes.indexOf('fixed') !== -1 && 1091 (classes.indexOf('left-0') !== -1 || classes.indexOf('left-') !== -1)) { 1092 hasFixedSidebar = true; 1093 console.log("EC-SB-003: Found fixed sidebar"); 1094 return false; 1095 } 1096 }); 1097 1098 // Also check for slide-out panels as indicator of sidebar layout 1099 if ($('nav[data-wpconvert-slideout-panel]').length > 0) { 1100 hasFixedSidebar = true; 1101 console.log("EC-SB-003: Found slide-out panel (indicates sidebar layout)"); 1102 } 1103 1104 // Check for full-height elements in content area 1105 $('div, section, article, main').each(function() { 1106 var classes = ($(this).attr('class') || ''); 1107 if (classes.indexOf('h-screen') !== -1 || 1108 classes.indexOf('min-h-screen') !== -1 || 1109 (classes.indexOf('h-full') !== -1 && $(this).closest('aside, nav').length === 0)) { 1110 hasFullHeightLayout = true; 1111 return false; 1112 } 1113 }); 1114 1115 // Also check if main content has full viewport height via fixed positioning 1116 // EC-CANVAS-001: exclude <canvas> â NeonScrollLine-style overlays use fixed inset-0 but are not layout shells 1117 if ($('[class*="fixed"][class*="w-full"][class*="h-screen"]').not('canvas').length > 0 || 1118 $('[class*="fixed"][class*="inset-0"]').not('canvas').length > 0) { 1119 hasFullHeightLayout = true; 1120 } 1121 1122 console.log("EC-SB-003: hasFixedSidebar=" + hasFixedSidebar + ", hasFullHeightLayout=" + hasFullHeightLayout); 1123 1124 if (hasFixedSidebar && hasFullHeightLayout) { 1125 console.log("EC-SB-003: Hiding default footer for full-viewport sidebar layout"); 1126 // Hide the fallback footer for full-viewport layouts 1127 $('footer.site-footer').hide(); 1128 $('.site-info').hide(); 1129 1130 // Remove padding/margin that creates white space 1131 $('main.site-main, .site-main, #primary, .content-area, body, html').css({ 1132 'padding-bottom': '0', 1133 'margin-bottom': '0' 1134 }); 1135 1136 // Hide any footer containing default text 1137 $('footer').each(function() { 1138 var footerText = $(this).text().toLowerCase(); 1139 if (footerText.indexOf('all rights reserved') !== -1 || 1140 footerText.indexOf('converted theme') !== -1) { 1141 $(this).hide(); 1142 console.log("EC-SB-003: Hidden footer with default text"); 1143 } 1144 }); 1145 1146 // Also ensure the main wrapper doesn't create scroll space 1147 $('#primary, .content-area').css({ 1148 'min-height': '0', 1149 'overflow': 'hidden' 1150 }); 1151 } 1152 1153 // EC-SB-001: Slide-out panel handling DISABLED - using WPConvert mobile nav instead 1154 // The sidebar menu button will be wired to the mobile nav by hamburger detection 1155 if (false && $slideOutPanels.length > 0 && $slideOutToggleButtons.length > 0) { 1156 console.log("EC-SB-001: Found slide-out navigation panel(s):", $slideOutPanels.length); 1157 1158 $slideOutToggleButtons.each(function() { 1159 var $btn = $(this); 1160 var targetId = $btn.attr("data-wpconvert-slideout-toggle"); 1161 var $targetPanel = targetId ? $("#" + targetId) : $slideOutPanels.first(); 1162 1163 if ($targetPanel.length === 0) { 1164 $targetPanel = $slideOutPanels.first(); 1165 } 1166 1167 console.log("EC-SB-001: Wiring slide-out toggle button to panel:", targetId || "first-panel"); 1168 1169 // Set initial state 1170 var isInitiallyOpen = $targetPanel.attr("aria-hidden") !== "true"; 1171 $btn.attr("aria-expanded", isInitiallyOpen ? "true" : "false"); 1172 1173 // Create close button inside panel if not exists 1174 if ($targetPanel.find(".wpconvert-slideout-close").length === 0) { 1175 var closeBtn = $('<button class="wpconvert-slideout-close absolute top-4 right-4 p-2 text-slate-600 hover:text-slate-900 z-50" aria-label="Menü schlieÃen">' + 1176 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
1176' + 1177 '</button>'); 1178 $targetPanel.prepend(closeBtn); 1179 } 1180 1181 // Toggle function - SIMPLE: just toggle display none/block 1182 // Panel was captured with correct open-state positioning classes 1183 // We simply show/hide it with display property 1184 function toggleSlideOutPanel(open) { 1185 // Re-select panel each time to ensure we have the right element 1186 var $panel = $("#" + targetId); 1187 if ($panel.length === 0) { 1188 $panel = $('nav[data-wpconvert-slideout-panel="true"]').first(); 1189 } 1190 1191 console.log("EC-SB-001: toggleSlideOutPanel called with open=" + open + ", panel found=" + $panel.length); 1192 1193 if ($panel.length === 0) { 1194 console.error("EC-SB-001: Could not find slide-out panel!"); 1195 return; 1196 } 1197 1198 if (open) { 1199 $panel.attr("aria-hidden", "false"); 1200 // Use multiple methods to ensure visibility 1201 $panel.css({ 1202 "display": "block", 1203 "visibility": "visible", 1204 "opacity": "1", 1205 "z-index": "9998" // High z-index to ensure it's on top 1206 }); 1207 $panel.show(); 1208 $btn.attr("aria-expanded", "true"); 1209 // Change to X icon 1210 var $svg = $btn.find("svg"); 1211 if ($svg.length > 0) { 1212 $svg.attr("class", "lucide lucide-x w-8 h-8"); 1213 $svg.html('<path d="M18 6 6 18"></path><path d="m6 6 12 12"></path>'); 1214 } 1215 console.log("EC-SB-001: Slide-out panel OPENED, display=" + $panel.css("display")); 1216 } else { 1217 $panel.attr("aria-hidden", "true"); 1218 $panel.css({ 1219 "display": "none", 1220 "visibility": "hidden" 1221 }); 1222 $panel.hide(); 1223 $btn.attr("aria-expanded", "false"); 1224 // Change to Menu icon 1225 var $svg = $btn.find("svg"); 1226 if ($svg.length > 0) { 1227 $svg.attr("class", "lucide lucide-menu w-8 h-8"); 1228 $svg.html('<line x1="4" x2="20" y1="12" y2="12"></line><line x1="4" x2="20" y1="6" y2="6"></line><line x1="4" x2="20" y1="18" y2="18"></line>'); 1229 } 1230 console.log("EC-SB-001: Slide-out panel CLOSED"); 1231 } 1232 } 1233 1234 // Toggle button click handler - use event delegation on document to survive DOM changes 1235 $(document).off("click.wpconvert-slideout-" + targetId).on("click.wpconvert-slideout-" + targetId, '[data-wpconvert-slideout-toggle="' + targetId + '"]', function(e) { 1236 e.preventDefault(); 1237 e.stopPropagation(); 1238 e.stopImmediatePropagation(); 1239 var $clickedBtn = $(this); 1240 var isOpen = $clickedBtn.attr("aria-expanded") === "true"; 1241 toggleSlideOutPanel(!isOpen); 1242 return false; 1243 }); 1244 1245 // Close button click handler - use delegation 1246 $(document).off("click.wpconvert-slideout-close-" + targetId).on("click.wpconvert-slideout-close-" + targetId, "#" + targetId + " .wpconvert-slideout-close", function(e) { 1247 e.preventDefault(); 1248 e.stopPropagation(); 1249 toggleSlideOutPanel(false); 1250 return false;
1251 }); 1252 1253 // Close when clicking a link inside the panel (but allow navigation) 1254 $targetPanel.find("a").on("click.wpconvert-slideout-nav", function() { 1255 // Close panel after a brief delay to allow navigation 1256 setTimeout(function() { 1257 toggleSlideOutPanel(false); 1258 }, 100); 1259 }); 1260 1261 // Simple initialization: ensure panel is hidden via display:none 1262 $targetPanel.css("display", "none"); 1263 $targetPanel.attr("aria-hidden", "true"); 1264 $btn.attr("aria-expanded", "false"); 1265 1266 console.log("EC-SB-001: Panel initialized in CLOSED state (display:none)"); 1267 }); 1268 } 1269 } catch(e) { console.warn("[WPConvert] Slide-out panels error:", e); } 1270 1271 try { // [WPConvert: Hamburger Menu] 1272 // EC-MOBILE-DRAWER: Skip entire hamburger handler when preserved drawer exists. 1273 // The preserved drawer's inline script handles all toggle logic independently. 1274 if (document.querySelector('[data-wpc-preserved-drawer]')) { 1275 console.log('[EC-MOBILE-DRAWER] Preserved drawer detected â skipping fallback hamburger handler'); 1276 throw 'skip'; // jumps to catch block, which just warns and continues 1277 } 1278 // Fix empty hamburger menu buttons (React icon components not captured during snapshot) 1279 // Inject SVG icon and make menu functional 1280 // EXPANDED SELECTOR: Also detect Tailwind mobile buttons (lg:hidden, md:hidden) and data-wpconvert-trigger 1281 // EC-SB-001: EXCLUDE buttons with data-wpconvert-slideout-toggle (those are handled above) 1282 var hamburgerSelector = [ 1283 'button[aria-label*="Menu"]', 1284 'button[aria-label*="menu"]', 1285 'button[aria-label*="navigation" i]', 1286 'button[aria-label*="toggle" i]', 1287 '.menu-toggle', 1288 '.hamburger-menu', 1289 '[class*="hamburger"]', 1290 'button[data-wpconvert-trigger]', 1291 'button.lg\\:hidden', 1292 'button.md\\:hidden', 1293 'button[class*="lg:hidden"]', 1294 'button[class*="md:hidden"]' 1295 ].join(', '); 1296 1297 $(hamburgerSelector).each(function() { 1298 var $btn = $(this); 1299 1300 // EC-SB-001: Skip buttons that have inline onclick for slide-out toggle 1301 if ($btn.attr("onclick") && $btn.attr("onclick").indexOf("main-menu") !== -1) { 1302 console.log("Skipping button with inline slide-out toggle (EC-SB-001)"); 1303 return; 1304 } 1305 1306 // EC-NAV-MOBILE-003: Skip buttons that already have an inline onclick for wpconvert-mobile-nav. 1307 // EC-NAV-MOBILE-002 injects inline onclick on hamburger buttons for jQuery-free reliability. 1308 // If we also attach a jQuery handler, the two conflict: inline opens the menu, then 1309 // jQuery sees aria-expanded="true" and immediately closes it in the same click cycle. 1310 if ($btn.attr("onclick") && $btn.attr("onclick").indexOf("wpconvert-mobile-nav") !== -1) { 1311 console.log("Skipping button with inline mobile nav toggle (EC-NAV-MOBILE-003)"); 1312 return; 1313 } 1314 1315 // CRITICAL: Skip buttons inside <aside> elements - they use inline onclick handlers 1316 if ($btn.closest("aside").length > 0) { 1317 console.log("Skipping button inside <aside> - uses inline onclick"); 1318 return; 1319 } 1320 1321 // EC-SB-001: Skip cookie/settings buttons - these should NOT get hamburger icons 1322 var ariaLabel = ($btn.attr("aria-label") || "").toLowerCase(); 1323 var btnHtml = ($btn.html() || "").toLowerCase(); 1324 var btnText = ($btn.text() || "").toLowerCase(); 1325 var isCookieButton = ariaLabel.indexOf("cookie") !== -1 || 1326 btnText.indexOf("cookie") !== -1 || 1327 btnHtml.indexOf("lucide-cookie") !== -1 || 1328 ariaLabel.indexOf("datenschutz") !== -1 || 1329 ariaLabel.indexOf("privacy") !== -1; 1330 1331 if (isCookieButton) { 1332 console.log("Skipping cookie button - not a hamburger menu:", ariaLabel || btnText.substring(0, 30)); 1333 return; 1334 } 1335 1336 // EC-THEME-002b: Skip dark/light mode toggle buttons. 1337 // Theme toggles use 'aria-label="Toggle dark mode"' (matched by the 1338 // 'button[aria-label*="toggle" i]' selector) and a sun/moon SVG. 1339 // EC-THEME-002 wires its own click handler in header.php; if the 1340 // hamburger handler also attaches, the first user click opens the 1341 // mobile menu and the theme only toggles on the second click. 1342 if ($btn.attr("data-wpc-theme-toggle")) { 1343 console.log("Skipping theme toggle button - has data-wpc-theme-toggle (EC-THEME-002b):", ariaLabel); 1344 return; 1345 } 1346 var isThemeToggleByAria = /toggles*(?:tos*)?(?:dark|theme|mode|light)/.test(ariaLabel) || 1347 /�(?:dark|light|theme)s*mode�/.test(ariaLabel) || 1348 /switchs*(?:dark|theme|mode|light)/.test(ariaLabel); 1349 var isThemeToggleByIcon = btnHtml.indexOf("lucide-sun") !== -1 || 1350 btnHtml.indexOf("lucide-moon") !== -1; 1351 if (isThemeToggleByAria || isThemeToggleByIcon) { 1352 console.log("Skipping theme toggle button by shape (EC-THEME-002b):", ariaLabel || btnText.substring(0, 30)); 1353 return; 1354 } 1355 1356 // CRITICAL: Skip SEARCH buttons - these should open search modal, not hamburger menu 1357 // Detect by: aria-label containing "search", or button contains search SVG icon 1358 var ariaLabel = ($btn.attr("aria-label") || "").toLowerCase(); 1359 var btnHtml = ($btn.html() || "").toLowerCase(); 1360 var isSearchButton = ariaLabel.indexOf("search") !== -1 || 1361 (btnHtml.indexOf("circle") !== -1 && btnHtml.indexOf("m21 21") !== -1) || // Lucide search icon path 1362 btnHtml.indexOf("lucide-search") !== -1; 1363 1364 if (isSearchButton) { 1365 console.log("Skipping search button (will be handled by search modal):", ariaLabel); 1366 return; // Skip - this is a search button, not a hamburger menu 1367 } 1368 1369 // EC-NAV-MOBILE-001: Skip Radix Sheet/Dialog/Select triggers 1370 // These are component triggers (e.g., mobile filter panels) that should NOT open the hamburger menu 1371 var ariaHaspopup = ($btn.attr("aria-haspopup") || "").toLowerCase(); 1372 if (ariaHaspopup === "dialog" || ariaHaspopup === "listbox" || ariaHaspopup === "menu") { 1373 console.log("Skipping Radix component trigger (aria-haspopup=" + ariaHaspopup + "):", $btn.text().trim().substring(0, 30)); 1374 return; 1375 } 1376 // Skip buttons with data-state + aria-controls pointing to Radix IDs (pattern: radix-:rN:)
1377 var ariaControls = ($btn.attr("aria-controls") || ""); 1378 if ($btn.attr("data-state") && /^radix-/.test(ariaControls)) { 1379 console.log("Skipping Radix-controlled button (aria-controls=" + ariaControls + ")"); 1380 return; 1381 } 1382 // Skip buttons whose text content is clearly a filter/sort trigger 1383 var btnTextLower = ($btn.text() || "").trim().toLowerCase(); 1384 if (/^filter/i.test(btnTextLower) || /^sort/i.test(btnTextLower)) { 1385 console.log("Skipping filter/sort trigger button:", btnTextLower.substring(0, 30)); 1386 return; 1387 } 1388 1389 // CRITICAL: Skip dropdown trigger buttons - these should NOT open the mobile nav 1390 // Dropdown triggers are buttons inside .group/.relative containers that have a sibling dropdown panel 1391 // This prevents the "first click opens sidebar instead of dropdown" bug 1392 var $parent = $btn.parent(); 1393 var parentClasses = ($parent.attr("class") || "").toLowerCase(); 1394 var isInsideDropdownGroup = parentClasses.indexOf("group") !== -1 || parentClasses.indexOf("relative") !== -1; 1395 1396 if (isInsideDropdownGroup) { 1397 // Check if there's a sibling dropdown panel (div with absolute/invisible/opacity-0/scale-y-0 classes) 1398 var $siblingDropdown = $btn.siblings("div").filter(function() { 1399 var sibClasses = ($(this).attr("class") || "").toLowerCase(); 1400 return sibClasses.indexOf("absolute") !== -1 || 1401 sibClasses.indexOf("invisible") !== -1 || 1402 sibClasses.indexOf("opacity-0") !== -1 || 1403 sibClasses.indexOf("scale-y-0") !== -1; 1404 }); 1405 1406 if ($siblingDropdown.length > 0) { 1407 console.log("Skipping dropdown trigger button (has sibling dropdown panel):", $btn.text().trim().substring(0, 30)); 1408 return; // Skip this button - it's a dropdown trigger, not a hamburger menu 1409 } 1410 } 1411 1412 // Also skip if button has ChevronDown icon (common dropdown indicator) 1413 var hasChevronIcon = $btn.find("svg").length > 0 && $btn.text().trim().length > 0; 1414 var btnText = $btn.text().trim().toLowerCase(); 1415 var looksLikeDropdownTrigger = hasChevronIcon && ( 1416 btnText.indexOf("services") !== -1 || 1417 btnText.indexOf("products") !== -1 || 1418 btnText.indexOf("solutions") !== -1 || 1419 btnText.indexOf("resources") !== -1 || 1420 btnText.indexOf("about") !== -1 || 1421 btnText.indexOf("more") !== -1 1422 ); 1423 1424 if (looksLikeDropdownTrigger && isInsideDropdownGroup) { 1425 console.log("Skipping dropdown trigger button (has chevron + nav text):", btnText.substring(0, 30)); 1426 return; 1427 } 1428 1429 // Check if button is empty or has no visible content 1430 if ($.trim($btn.html()) === "" || $btn.children().length === 0) { 1431 console.log("Found empty menu button, injecting hamburger icon"); 1432 1433 // Inject hamburger SVG icon (matching lucide-react Menu icon style) 1434 var hamburgerSvg = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-8 h-8"><line x1="4" x2="20" y1="12" y2="12"></line><line x1="4" x2="20" y1="6" y2="6"></line><line x1="4" x2="20" y1="18" y2="18"></line></svg>'; 1435 $btn.html(hamburgerSvg); 1436 } 1437 1438 // Detect which side of the screen the hamburger button is on 1439 var $mobileNav = $("#wpconvert-mobile-nav"); 1440 var btnRect = $btn[0].getBoundingClientRect(); 1441 var windowWidth = $(window).width(); 1442 var btnCenterX = btnRect.left + (btnRect.width / 2); 1443 1444 // If button is in left half of screen, slide menu from left; otherwise from right 1445 if (btnCenterX < windowWidth / 2) { 1446 $mobileNav.removeClass("slide-right").addClass("slide-left"); 1447 console.log("Hamburger on LEFT side - menu will slide from left"); 1448 } else { 1449 $mobileNav.removeClass("slide-left").addClass("slide-right"); 1450 console.log("Hamburger on RIGHT side - menu will slide from right"); 1451 } 1452 1453 // Make the button toggle a mobile navigation menu 1454 $btn.off("click.wpconvert").on("click.wpconvert", function(e) { 1455 e.preventDefault(); 1456 e.stopPropagation(); 1457 1458 var isOpen = $btn.attr("aria-expanded") === "true"; 1459 1460 if (isOpen) { 1461 // Close menu 1462 $mobileNav.removeClass("open").addClass("closed"); 1463 $btn.attr("aria-expanded", "false"); 1464 $("body").removeClass("mobile-nav-open"); 1465 } else { 1466 // Open menu 1467 $mobileNav.removeClass("closed").addClass("open"); 1468 $btn.attr("aria-expanded", "true"); 1469 $("body").addClass("mobile-nav-open"); 1470 } 1471 }); 1472 }); 1473 } catch(e) { console.warn("[WPConvert] Hamburger menu error:", e); } 1474 1475 try { // [WPConvert: Search Modal] 1476 // UNIVERSAL SEARCH MODAL - works for any site with search icons 1477 // Handles search button clicks, Ctrl+K shortcut, and provides live search 1478 (function() { 1479 var $searchModal = $("#wpconvert-search-modal"); 1480 var $searchInput = $("#wpconvert-search-input"); 1481 var $searchResults = $("#wpconvert-search-results"); 1482 var searchData = null; 1483 1484 if (!$searchModal.length) return; 1485 1486 // Load searchable pages from routes.json and menus.json 1487 function loadSearchData() { 1488 if (searchData && searchData.length > 0) return Promise.resolve(searchData); 1489 1490 searchData = []; 1491 var promises = []; 1492 1493 // Load routes.json (can be object or array) 1494 promises.push( 1495 $.getJSON("<?php echo get_template_directory_uri(); ?>/assets/data/routes.json") 1496 .then(function(routes) { 1497 if (routes) { 1498 // Handle both object format {"/path": {...}}
1498 and array format [{...}] 1499 if (Array.isArray(routes)) { 1500 routes.forEach(function(route) { 1501 if (route.path && route.title) { 1502 searchData.push({ 1503 title: route.title, 1504 path: route.path || route.url, 1505 keywords: (route.title + " " + (route.path || route.url || "")).toLowerCase() 1506 }); 1507 } 1508 }); 1509 } else if (typeof routes === "object") { 1510 // Object format: {"/path": {title, url, slug}} 1511 Object.keys(routes).forEach(function(path) { 1512 var route = routes[path]; 1513 if (route && route.title && !path.includes(":")) { // Skip dynamic routes 1514 searchData.push({ 1515 title: route.title, 1516 path: path, 1517 keywords: (route.title + " " + path).toLowerCase() 1518 }); 1519 } 1520 }); 1521 } 1522 } 1523 }) 1524 .catch(function() { /* routes.json may not exist */ }) 1525 ); 1526 1527 // Also load menus.json 1528 promises.push( 1529 $.getJSON("<?php echo get_template_directory_uri(); ?>/assets/data/menus.json") 1530 .then(function(menus) { 1531 if (menus && menus.primary && menus.primary.items) { 1532 menus.primary.items.forEach(function(item) { 1533 if (item.url && !searchData.some(function(s) { return s.path === item.url; })) { 1534 searchData.push({ 1535 title: item.title, 1536 path: item.url, 1537 keywords: item.title.toLowerCase() 1538 }); 1539 } 1540 // Add children 1541 if (item.children && Array.isArray(item.children)) { 1542 item.children.forEach(function(child) { 1543 if (child.url && !searchData.some(function(s) { return s.path === child.url; })) { 1544 searchData.push({ 1545 title: child.title, 1546 path: child.url, 1547 keywords: (child.title + " " + item.title).toLowerCase() 1548 }); 1549 } 1550 }); 1551 } 1552 }); 1553 } 1554 // Also check footer menu 1555 if (menus && menus.footer && menus.footer.items) { 1556 menus.footer.items.forEach(function(item) { 1557 if (item.url && !searchData.some(function(s) { return s.path === item.url; })) { 1558 searchData.push({ 1559 title: item.title, 1560 path: item.url, 1561 keywords: item.title.toLowerCase() 1562 }); 1563 } 1564 }); 1565 } 1566 }) 1567 .catch(function() { /* menus.json may not exist */ }) 1568 ); 1569 1570 return Promise.all(promises).then(function() { 1571 console.log("WPConvert Search: Loaded " + searchData.length + " searchable pages"); 1572 return searchData; 1573 }); 1574 } 1575 1576 function openSearch() { 1577 $searchModal.addClass("open"); 1578 $searchInput.val("").focus(); 1579 $searchResults.empty(); 1580 $("body").css("overflow", "hidden"); 1581 loadSearchData(); 1582 } 1583 1584 function closeSearch() { 1585 $searchModal.removeClass("open"); 1586 $("body").css("overflow", ""); 1587 } 1588 1589 function performSearch(query) { 1590 if (!query || query.length < 1) { 1591 $searchResults.empty(); 1592 return; 1593 } 1594 1595 loadSearchData().then(function(data) { 1596 var q = query.toLowerCase(); 1597 var results = data.filter(function(item) { 1598 return item.keywords.indexOf(q) !== -1 || 1599 item.title.toLowerCase().indexOf(q) !== -1; 1600 }).slice(0, 8); 1601 1602 $searchResults.empty(); 1603 1604 if (results.length === 0) { 1605 $searchResults.html('<div style="padding: 1rem; text-align: center; color: hsl(var(--muted-foreground));">No results found for "' + query + '"</div>'); 1606 } else {
1607 results.forEach(function(item) { 1608 var $result = $('<a class="wpconvert-search-result" href="<?php echo home_url(); ?>' + item.path + '"></a>'); 1609 $result.html('<div class="wpconvert-search-result-title">' + item.title + '</div><div class="wpconvert-search-result-url">' + item.path + '</div>'); 1610 $result.on("click", function() { 1611 closeSearch(); 1612 }); 1613 $searchResults.append($result); 1614 }); 1615 } 1616 }); 1617 } 1618 1619 // Expose global functions 1620 window.wpconvertOpenSearch = openSearch; 1621 window.wpconvertCloseSearch = closeSearch; 1622 1623 // Bind close button and backdrop AFTER functions are defined 1624 $("#wpconvert-search-close").off("click.wpconvert").on("click.wpconvert", function(e) { 1625 e.preventDefault(); 1626 e.stopPropagation(); 1627 closeSearch(); 1628 }); 1629 1630 $("#wpconvert-search-backdrop").off("click.wpconvert").on("click.wpconvert", function(e) { 1631 e.preventDefault(); 1632 closeSearch(); 1633 }); 1634 1635 // Handle search button clicks - find search buttons in HEADER/NAV only 1636 // EC-SEARCH-002: Restrict .lucide-search matching to header/nav context 1637 // to avoid false positives on content buttons (e.g. "SEO Optimization" tab 1638 // with a search icon triggers modal instead of filtering) 1639 var searchSelector = [ 1640 'header button[aria-label*="Search" i]', 1641 'nav button[aria-label*="Search" i]', 1642 'header button[aria-label*="search" i]', 1643 'nav button[aria-label*="search" i]', 1644 '[data-wpconvert-trigger][aria-label*="search" i]', 1645 'header button:has(.lucide-search)', 1646 'nav button:has(.lucide-search)', 1647 '[class*="navbar"] button:has(.lucide-search)', 1648 'header a[aria-label*="Search" i]', 1649 'nav a[aria-label*="Search" i]' 1650 ].join(", "); 1651 1652 $(searchSelector).off("click.wpconvert-search").on("click.wpconvert-search", function(e) { 1653 e.preventDefault(); 1654 e.stopPropagation(); 1655 openSearch(); 1656 }); 1657 1658 // Keyboard shortcuts 1659 $(document).on("keydown.wpconvert-search", function(e) { 1660 // Ctrl+K or Cmd+K to open 1661 if ((e.ctrlKey || e.metaKey) && e.key === "k") { 1662 e.preventDefault(); 1663 openSearch(); 1664 } 1665 // Escape to close 1666 if (e.key === "Escape" && $searchModal.hasClass("open")) { 1667 closeSearch(); 1668 } 1669 }); 1670 1671 // Search input handler 1672 var searchTimeout; 1673 $searchInput.on("input", function() { 1674 clearTimeout(searchTimeout); 1675 var query = $(this).val(); 1676 searchTimeout = setTimeout(function() { 1677 performSearch(query); 1678 }, 150); 1679 }); 1680 1681 // Enter key on input navigates to first result 1682 $searchInput.on("keydown", function(e) { 1683 if (e.key === "Enter") { 1684 var $firstResult = $searchResults.find("a").first(); 1685 if ($firstResult.length) { 1686 window.location.href = $firstResult.attr("href"); 1687 } 1688 } 1689 }); 1690 1691 console.log("WPConvert Search Modal initialized"); 1692 })(); 1693 } catch(e) { console.warn("[WPConvert] Search modal error:", e); } 1694 1695 try { // [WPConvert: Mobile Menu Toggle] 1696 // UNIVERSAL MOBILE MENU TOGGLE (for preserved mobile menus with data-wpc-* attributes) 1697 // This handles mobile menus that were preserved from the original site with wp_nav_menu() integration 1698 (function() { 1699 var toggles = document.querySelectorAll("[data-wpc-menu-toggle]"); 1700 var panel = document.querySelector("[data-wpc-menu-panel]");
1701 var overlay = document.querySelector("[data-wpc-menu-overlay]"); 1702 1703 if (!toggles.length || !panel) return; 1704 1705 console.log("WPC Mobile Menu: Found preserved mobile menu with data attributes"); 1706 1707 function openMenu() { 1708 panel.classList.add("is-open"); 1709 if (overlay) overlay.classList.add("is-open"); 1710 document.body.classList.add("wpc-menu-open"); 1711 toggles.forEach(function(t) { t.setAttribute("aria-expanded", "true"); }); 1712 } 1713 1714 function closeMenu() { 1715 panel.classList.remove("is-open"); 1716 if (overlay) overlay.classList.remove("is-open"); 1717 document.body.classList.remove("wpc-menu-open"); 1718 toggles.forEach(function(t) { t.setAttribute("aria-expanded", "false"); }); 1719 } 1720 1721 function toggleMenu() { 1722 if (panel.classList.contains("is-open")) { 1723 closeMenu(); 1724 } else { 1725 openMenu(); 1726 } 1727 } 1728 1729 // Toggle button click 1730 toggles.forEach(function(toggle) { 1731 toggle.addEventListener("click", function(e) { 1732 e.preventDefault(); 1733 e.stopPropagation(); 1734 toggleMenu(); 1735 }); 1736 }); 1737 1738 // Close on overlay click 1739 if (overlay) { 1740 overlay.addEventListener("click", closeMenu); 1741 } 1742 1743 // Close on ESC key 1744 document.addEventListener("keydown", function(e) { 1745 if (e.key === "Escape" && panel.classList.contains("is-open")) { 1746 closeMenu(); 1747 } 1748 }); 1749 1750 // Close on link click (important for mobile UX - menu closes after navigation) 1751 panel.querySelectorAll("a").forEach(function(link) { 1752 link.addEventListener("click", function() { 1753 // Small delay to allow navigation to start 1754 setTimeout(closeMenu, 100); 1755 }); 1756 }); 1757 })(); 1758 } catch(e) { console.warn("[WPConvert] Mobile menu toggle error:", e); } 1759 1760 try { // [WPConvert: URL Tab Switching] 1761 // URL-based tab switching (for services, contact pages with ?tab= parameter) 1762 // GENERIC: Works with any color scheme by detecting active/inactive classes dynamically 1763 var urlParams = new URLSearchParams(window.location.search); 1764 var activeTab = urlParams.get("tab"); 1765 if (activeTab) { 1766 console.log("URL has tab parameter:", activeTab); 1767 1768 // Find tab container (group of sibling buttons) 1769 var $tabGroups = $("button").parent().filter(function() { 1770 return $(this).find("button").length >= 2; 1771 }); 1772 1773 $tabGroups.each(function() { 1774 var $container = $(this); 1775 var $buttons = $container.find("button"); 1776 1777 // Detect active/inactive classes from existing buttons 1778 // First button is typically "active" by default, others are "inactive" 1779 var $firstBtn = $buttons.first(); 1780 var $otherBtn = $buttons.eq(1); 1781 1782 if ($firstBtn.length && $otherBtn.length) { 1783 var activeClasses = $firstBtn.attr("class") || ""; 1784 var inactiveClasses = $otherBtn.attr("class") || ""; 1785 1786 // Find and activate the matching tab 1787 $buttons.each(function() { 1788 var $tabBtn = $(this); 1789 var btnText = $tabBtn.text().toLowerCase().trim().replace(/\s+/g, "-"); 1790 var tabMatch = btnText.includes(activeTab.toLowerCase()) || 1791 btnText.replace(/[^a-z0-9]/g, "").includes(activeTab.toLowerCase().replace(/[^a-z0-9]/g, "")) || 1792 $tabBtn.attr("data-tab") === activeTab || 1793 $tabBtn.attr("id") === "tab-" + activeTab; 1794 1795 if (tabMatch) { 1796 console.log("Found matching tab button:", btnText); 1797 1798 // Swap classes: make all buttons inactive, then make matched one active 1799 $buttons.attr("class", inactiveClasses); 1800 $tabBtn.attr("class", activeClasses); 1801 1802 // Trigger click for any JavaScript handlers 1803 $tabBtn.trigger("click"); 1804 } 1805 }); 1806 } 1807 }); 1808 } 1809 } catch(e) { console.warn("[WPConvert] URL tab switching error:", e); } 1810 1811 try { // [WPConvert: Universal Carousel] 1812 // Universal Carousel/Slider Functionality 1813 // Detects and enables carousels/sliders (testimonials, product sliders, etc.) 1814 1815 // Helper function to initialize carousel behavior 1816 function initializeCarousel($carousel, $prevBtn, $nextBtn) { 1817 // EC-GALLERY-FLASH-001: Never carousel-ify a stacked gallery section. 1818 if ($carousel.closest("[data-wpconvert-stacked-gallery-section]").length || 1819 $carousel.find("[data-wpconvert-stacked-gallery]").length) { 1820 return; 1821 } 1822 var $track = null; 1823 var $items = null; 1824 1825 $track = $carousel.find('[class*="grid"]').first(); 1826 if ($track.length) { 1827 $items = $track.children().filter(function() { 1828 var $el = $(this); 1829 // Include items that have testimonial/review content (quotes, names, ratings) 1830 var hasContent = $el.find('p, h1, h2, h3, h4, h5, h6, [class*="quote"], [class*="testimonial"], [class*="review"]').length > 0; 1831 var isNotButton = !$el.is("button") && !$el.find("button[aria-label]").length; 1832 return isNotButton && hasContent; 1833 }); 1834 console.log("Found " + $items.length + " carousel items in grid"); 1835 } 1836 1837 if (!$items || $items.length <= 1) { 1838 $track = $carousel.find('[class*="track"], [class*="slider-inner"], [class*="items"]').first(); 1839 if ($track.length) { 1840 $items = $track.children().not("button"); 1841 } 1842 } 1843 1844 if (!$items || $items.length <= 1) { 1845 $items = $carousel.children().not("button").filter(function() { 1846 var $el = $(this); 1847 return !$el.find("button[aria-label]").length; 1848 }); 1849 } 1850 1851 if (!$items || $items.length === 0) { 1852 return; 1853 } 1854 1855 var totalItems = $items.length; 1856 1857 function parseColumns($el) { 1858 var configs = { base: null, sm: null, md: null, lg: null, xl: null }; 1859 var classString = ($el.attr("class") || "") + " " + ($el.parent().attr("class") || "");
1860 classString.split(/\s+/).forEach(function(cls) { 1861 var match = cls.match(/^(sm:|md:|lg:|xl:)?grid-cols-(\d+)/); 1862 if (match) { 1863 var bp = match[1] ? match[1].replace(":", "") : "base"; 1864 configs[bp] = parseInt(match[2], 10); 1865 } 1866 }); 1867 return configs; 1868 } 1869 1870 function columnsForWidth(configs, width) { 1871 if (width >= 1280 && configs.xl) return configs.xl; 1872 if (width >= 1024 && configs.lg) return configs.lg; 1873 if (width >= 768 && configs.md) return configs.md; 1874 if (width >= 640 && configs.sm) return configs.sm; 1875 return configs.base || configs.lg || configs.md || 1; 1876 } 1877 1878 var columnConfig = parseColumns($track.length ? $track : $carousel); 1879 var itemsPerView = Math.max(1, Math.min(totalItems, columnsForWidth(columnConfig, window.innerWidth))); 1880 var currentIndex = 0; 1881 1882 function updateDots(activeIndex) { 1883 var $dots = $carousel.find('[class*="dot"], [class*="pagination"] button, button[role="tab"]'); 1884 if ($dots.length === 0) return; 1885 $dots.removeClass("active bg-arta-red w-6"); 1886 $dots.eq(activeIndex % $dots.length).addClass("active bg-arta-red w-6"); 1887 } 1888 1889 function showCurrentItems() { 1890 $items.hide(); 1891 for (var i = 0; i < itemsPerView; i++) { 1892 var idx = (currentIndex + i) % totalItems; 1893 $items.eq(idx).fadeIn(200); 1894 } 1895 updateDots(currentIndex); 1896 } 1897 1898 function recalcItemsPerView() { 1899 var newView = Math.max(1, Math.min(totalItems, columnsForWidth(columnConfig, window.innerWidth))); 1900 if (newView !== itemsPerView) { 1901 itemsPerView = newView; 1902 currentIndex = 0; 1903 showCurrentItems(); 1904 } 1905 } 1906 1907 // EC-CAROUSEL-002 FIX: Remove stale disabled attrs from React initial state 1908 $prevBtn.removeAttr("disabled").css("opacity", ""); 1909 $nextBtn.removeAttr("disabled").css("opacity", ""); 1910 1911 var autoAdvanceInterval = null; 1912 1913 if (totalItems > itemsPerView) { 1914 showCurrentItems(); 1915 autoAdvanceInterval = setInterval(function() { 1916 currentIndex = (currentIndex + itemsPerView) % totalItems; 1917 showCurrentItems(); 1918 }, 6000); 1919 } else { 1920 $items.show(); 1921 updateDots(0); 1922 // When all items fit, disable buttons since there's nothing to cycle 1923 $prevBtn.prop("disabled", true).css("opacity", 0.3); 1924 $nextBtn.prop("disabled", true).css("opacity", 0.3); 1925 console.log("All carousel items fit on screen, buttons disabled"); 1926 } 1927 1928 $items.each(function(index) { 1929 $(this).attr('data-original-index', index); 1930 }); 1931 1932 $prevBtn.off("click.wpconvertCarousel").on("click.wpconvertCarousel", function(e) { 1933 e.preventDefault(); 1934 e.stopPropagation(); 1935 if (totalItems <= itemsPerView) return; 1936 if (autoAdvanceInterval) clearInterval(autoAdvanceInterval); 1937 1938 var $last = $items.last(); 1939 $items = $items.not($last); 1940 1941 $last.hide().prependTo($track).fadeIn(300); 1942 $items = $last.add($items); 1943 1944 var currentOriginalIndex = parseInt($items.first().attr('data-original-index') || 0); 1945 updateDots(currentOriginalIndex); 1946 1947 if (totalItems >
1947 itemsPerView) { 1948 startAutoAdvance(); 1949 } 1950 }); 1951 1952 $nextBtn.off("click.wpconvertCarousel").on("click.wpconvertCarousel", function(e) { 1953 e.preventDefault(); 1954 e.stopPropagation(); 1955 if (totalItems <= itemsPerView) return; 1956 if (autoAdvanceInterval) clearInterval(autoAdvanceInterval); 1957 1958 var $first = $items.first(); 1959 $items = $items.not($first); 1960 1961 $first.hide().appendTo($track).fadeIn(300); 1962 $items = $items.add($first); 1963 1964 var currentOriginalIndex = parseInt($items.first().attr('data-original-index') || 0); 1965 updateDots(currentOriginalIndex); 1966 1967 if (totalItems > itemsPerView) { 1968 startAutoAdvance(); 1969 } 1970 }); 1971 1972 function startAutoAdvance() { 1973 autoAdvanceInterval = setInterval(function() { 1974 $nextBtn.trigger('click'); 1975 }, 6000); 1976 } 1977 1978 if (totalItems > itemsPerView) { 1979 startAutoAdvance(); 1980 } 1981 1982 var $dots = $carousel.find('[class*="dot"], [class*="pagination"] button, button[role="tab"]'); 1983 if ($dots.length > 0) { 1984 $dots.on("click", function(e) { 1985 e.preventDefault(); 1986 clearInterval(autoAdvanceInterval); 1987 currentIndex = $(this).index(); 1988 showCurrentItems(); 1989 }); 1990 } 1991 1992 var resizeTimer = null; 1993 $(window).on("resize.wpconvertCarousel", function() { 1994 clearTimeout(resizeTimer); 1995 resizeTimer = setTimeout(function() { 1996 recalcItemsPerView(); 1997 }, 200); 1998 }); 1999 2000 // CRITICAL: Add drag/swipe support for touch and mouse 2001 var isDragging = false; 2002 var startX = 0; 2003 var startY = 0; 2004 var scrollLeft = 0; 2005 var scrollTop = 0; 2006 2007 // Mouse drag support 2008 $track.on("mousedown.wpconvertCarousel", function(e) { 2009 isDragging = true; 2010 startX = e.pageX - $(this).offset().left; 2011 startY = e.pageY - $(this).offset().top; 2012 scrollLeft = $(this).scrollLeft(); 2013 scrollTop = $(this).scrollTop(); 2014 $(this).css("cursor", "grabbing"); 2015 }); 2016 2017 $(document).on("mousemove.wpconvertCarousel", function(e) { 2018 if (!isDragging) return; 2019 e.preventDefault(); 2020 var x = e.pageX - $track.offset().left; 2021 var y = e.pageY - $track.offset().top; 2022 var walkX = (x - startX) * 2; 2023 var walkY = (y - startY) * 2; 2024 2025 // If horizontal drag is greater than vertical, it's a swipe 2026 if (Math.abs(walkX) > Math.abs(walkY) && Math.abs(walkX) > 50) { 2027 if (walkX > 0) { 2028 // Swipe right = previous 2029 $prevBtn.trigger("click"); 2030 } else { 2031 // Swipe left = next 2032 $nextBtn.trigger("click"); 2033 } 2034 isDragging = false; 2035 $track.css("cursor", ""); 2036 } 2037 }); 2038 2039 $(document).on("mouseup.wpconvertCarousel", function() { 2040 isDragging = false; 2041 $track.css("cursor", ""); 2042 }); 2043 2044 // Touch swipe support 2045 var touchStartX = 0; 2046 var touchStartY = 0; 2047 2048 $track.on("touchstart.wpconvertCarousel", function(e) { 2049 var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; 2050 touchStartX = touch.clientX; 2051 touchStartY = touch.clientY; 2052 }); 2053 2054 $track.on("touchend.wpconvertCarousel", function(e) { 2055 var touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]; 2056 if (!touch) { 2057 touch = e.originalEvent.changedTouches[0]; 2058 } 2059 if (!touch) return; 2060 2061 var touchEndX = touch.clientX; 2062 var touchEndY = touch.clientY; 2063 var diffX = touchStartX - touchEndX; 2064 var diffY = touchStartY - touchEndY; 2065 2066 // If horizontal swipe is greater than vertical, it's a swipe 2067 if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) { 2068 if (diffX > 0) { 2069 // Swipe left = next 2070 $nextBtn.trigger("click"); 2071 } else { 2072 // Swipe right = previous 2073 $prevBtn.trigger("click"); 2074 } 2075 } 2076 }); 2077 } 2078 2079 // Method 1: Find all buttons with Previous/Next aria-labels and trace back to their carousel container 2080 $("button[aria-label*='Previous'], button[aria-label*='Prev'], button[aria-label*='previous']").each(function() { 2081 var $prevBtn = $(this); 2082 if ($prevBtn.data("carousel-processed")) return; 2083 // EC-CAROUSEL-004 + EC-TESTIMONIAL-004b: Skip inner testimonial card only (EC-CAROUSEL-001) 2084 if (wpcInsideJsonTestimonialCard($prevBtn)) return; 2085 // EC-CAROUSEL-016: Skip stacked-image / video galleries (crossfade, NOT horizontal carousels). 2086 // Buttons like "Previous photo", "Previous video" live inside a container where 2+ 2087 // children are absolutely positioned at inset-0. Wrapping the parent grid in a flex 2088 // track causes blank flashes and layout glitches. 2089 var prevLabel = ($prevBtn.attr("aria-label") || "").toLowerCase(); 2090 if (/\bphoto\b|\bvideo\b|\bimage\b|\bpicture\b|\bgallery\b|\bslide\b/.test(prevLabel)) { 2091 var $galBox = $prevBtn.parent(); 2092 var stackedImgs = $galBox.find("img.absolute, img[class*='absolute'][class*='inset'], video.absolute, video[class*='absolute'][class*='inset'], [class*='absolute'][class*='inset-0']"); 2093 if (stackedImgs.length >= 2) { 2094 $prevBtn.data("carousel-processed", true); 2095 var $sibNext = $galBox.find("button[aria-label*='Next'], button[aria-label*='next']").not($prevBtn); 2096 if ($sibNext.length) $sibNext.data("carousel-processed", true); 2097 // EC-CAROUSEL-016f: Wire a proper opacity-cycle handler instead 2098 // of leaving dead buttons. Crossfade between stacked images. 2099 (function($box, $prev, $next) { 2100 var $imgs = $box.find("img.absolute, img[class*='absolute'][class*='inset-0'], video.absolute, video[class*='absolute'][class*='inset-0']"); 2101 if ($imgs.length < 2) return; 2102 var idx = 0; 2103 $imgs.css({ transition: "opacity 0.5s ease" }); 2104 $imgs.each(function(i) { $(this).css("opacity", i === 0 ? 1 : 0); }); 2105 var $dots = $box.find("button[aria-label*='View']"); 2106 function show(n) { 2107 idx = ((n % $imgs.length) + $imgs.length) % $imgs.length; 2108 $imgs.each(function(i) { $(this).css("opacity", i === idx ? 1 : 0); }); 2109 if ($dots.length) $dots.each(function(i) { 2110 var $d = $(this); 2111 $d.css("opacity", i === idx ? 1 : 0.5); 2112 if (i === idx) $d.css("width", "1.25rem"); else $d.css("width", "0.5rem"); 2113 }); 2114 } 2115 show(0); 2116 $prev.off("click.wpcGallery").on("click.wpcGallery", function(e) { e.preventDefault(); e.stopPropagation(); clearInterval(timer); show(idx - 1); startTimer(); }); 2117 $next.off("click.wpcGallery").on("click.wpcGallery", function(e) { e.preventDefault(); e.stopPropagation(); clearInterval(timer); show(idx + 1); startTimer(); }); 2118 $dots.each(function(i) { $(this).data("carousel-processed", true).css("cursor","pointer").off("click.wpcGallery").on("click.wpcGallery", function(e) { e.preventDefault(); clearInterval(timer); show(i); startTimer(); }); }); 2119 var timer = setInterval(function() { show(idx + 1); }, 5000); 2120 function startTimer() { clearInterval(timer); timer = setInterval(function() { show(idx + 1); }, 5000); } 2121 $box.data("carousel-processed", true).on("mouseenter", function() { clearInterval(timer); }).on("mouseleave", function() { startTimer(); }); 2122 // EC-CAROUSEL-016h: Mark the ENTIRE enclosing section as carousel-processed. 2123 // The stacked gallery lives inside a grid column. Without this guard, 2124 // later carousel methods (3/5/6) can find other buttons or overflow-hidden 2125 // elements in the same section and call initializeOverflowCarousel on a 2126 // parent container â turning the hero grid itself into a carousel that 2127 // scrolls the text and gallery columns as separate "slides." 2128 var $galSection = $box.closest("section"); 2129 if ($galSection.length) { 2130 $galSection.find("button, .overflow-hidden, [style*='overflow']").data("carousel-processed", true); 2131 } 2132 console.log("[EC-CAROUSEL-016f] Stacked gallery opacity-cycle: " + $imgs.length + " images"); 2133 })($galBox, $prevBtn, $sibNext.length ? $sibNext.first() : $()); 2134 return; 2135 } 2136 // EC-CAROUSEL-016d: Also skip horizontal thumbnail scroll controls. 2137 // Buttons labelled "Previous video" / "Next photo" sit beside an
2138 // overflow-x-auto scrollable strip of thumbnails â they are scroll 2139 // controls, not carousel prev/next buttons. 2140 var $scrollSibling = $galBox.find("[class*='overflow-x-auto'], [class*='overflow-auto'], [class*='scrollbar-hide']"); 2141 if ($scrollSibling.length && $scrollSibling.find("button, a").length >= 2) { 2142 $prevBtn.data("carousel-processed", true); 2143 var $sibNext2 = $galBox.find("button[aria-label*='Next'], button[aria-label*='next']").not($prevBtn); 2144 if ($sibNext2.length) $sibNext2.data("carousel-processed", true); 2145 console.log("[EC-CAROUSEL-016d] Skipped thumbnail scroll strip (aria-label: " + prevLabel + ")"); 2146 return; 2147 } 2148 } 2149 2150 // Find the common parent container (usually a div with "relative" class that contains both buttons and grid) 2151 var $parentContainer = $prevBtn.parent(); 2152 2153 // Find the corresponding Next button - check siblings first (most common case) 2154 var $nextBtn = $prevBtn.siblings("button[aria-label*='Next'], button[aria-label*='next'], button[aria-label*='testimonial']"); 2155 if (!$nextBtn.length) { 2156 // If not siblings, search within the parent container 2157 $nextBtn = $parentContainer.find("button[aria-label*='Next'], button[aria-label*='next']").not($prevBtn); 2158 } 2159 2160 if (!$nextBtn.length) return; // No next button found 2161 2162 // Find the grid/carousel container - look for grid within the parent container 2163 // CRITICAL: The carousel container should be the parent that contains BOTH buttons AND grid 2164 var $gridContainer = $parentContainer.find('[class*="grid"], [class*="carousel"], [class*="slider"]').first(); 2165 2166 // Use the parent container as carousel if it contains a grid (buttons are siblings of grid) 2167 // Otherwise, try to find a section parent 2168 var $carousel = $gridContainer.length ? $parentContainer : $prevBtn.closest('section, [class*="carousel"], [class*="slider"], [class*="testimonial"], [class*="client"]'); 2169 2170 // Final fallback: use parent container if nothing else found 2171 if (!$carousel.length) { 2172 $carousel = $parentContainer; 2173 } 2174 2175 if ($nextBtn.length && $carousel.length) { 2176 console.log("Found carousel with prev/next buttons:", { 2177 carousel: $carousel.attr("class"), 2178 prevBtn: $prevBtn.attr("aria-label"), 2179 nextBtn: $nextBtn.attr("aria-label"), 2180 hasGrid: $gridContainer.length > 0, 2181 carouselLength: $carousel.length, 2182 nextBtnLength: $nextBtn.length 2183 }); 2184 2185 try { 2186 $prevBtn.data("carousel-processed", true); 2187 $nextBtn.data("carousel-processed", true); 2188 initializeCarousel($carousel.first(), $prevBtn.first(), $nextBtn.first()); 2189 console.log("â Carousel initialized successfully"); 2190 } catch (error) { 2191 console.error("â Error initializing carousel:", error); 2192 } 2193 } else { 2194 console.log("â ï¸ Carousel not initialized - missing requirements:", { 2195 hasNextBtn: $nextBtn.length > 0, 2196 hasCarousel: $carousel.length > 0 2197 }); 2198 } 2199 }); 2200 2201 // Method 2: Detect by class name patterns (fallback) 2202 var $carousels = $('[class*="carousel"], [class*="slider"], [class*="swiper"], [data-carousel]'); 2203 console.log("Found " + $carousels.length + " potential carousels by class"); 2204 $carousels.each(function() { 2205 var $carousel = $(this); 2206 if ($carousel.data("carousel-processed")) return; 2207 if (wpcInsideJsonTestimonialCard($carousel)) return; 2208 2209 var $prevBtn = $carousel.find('button[aria-label*="Previous"], button[aria-label*="Prev"], [class*="prev"], [class*="left"]'); 2210 var $nextBtn = $carousel.find('button[aria-label*="Next"], [class*="next"], [class*="right"]'); 2211 2212 if ($prevBtn.length && $nextBtn.length) { 2213 console.log("Found carousel by class pattern:", $carousel.attr("class")); 2214 $carousel.data("carousel-processed", true); 2215 initializeCarousel($carousel, $prevBtn.first(), $nextBtn.first()); 2216 } 2217 }); 2218 } catch(e) { console.warn("[WPConvert] Universal carousel error:", e); } 2219 2220 try { // [WPConvert: EC-CAROUSEL-002 SVG Arrow + Overflow Carousel Detection] 2221 // EC-CAROUSEL-002: Enhanced carousel detection for AI-builder patterns 2222 // Covers: Embla/ShadCN (overflow-hidden+flex), Lucide/Heroicon SVG buttons, 2223 // sr-only text, and generic scrollable card containers. 2224 2225 function initializeOverflowCarousel($region, $viewport, $track, $slides, $prevBtn, $nextBtn) { 2226 if (!$slides.length) return; 2227 // EC-CAROUSEL-016h: Never convert a section that contains a stacked gallery 2228 // into an overflow carousel â the gallery already has its own 016f handler. 2229 if ($region.closest("[data-wpconvert-stacked-gallery-section]").length || 2230 $viewport.closest("[data-wpconvert-stacked-gallery-section]").length) { 2231 console.log("[EC-CAROUSEL-016h] Skipped initializeOverflowCarousel inside stacked gallery section"); 2232 return; 2233 } 2234 var currentSlide = 0; 2235 2236 // Remove stale inline onclick handlers and take full control
2237 [$prevBtn, $nextBtn].forEach(function($b) { 2238 var oc = $b.attr("onclick") || ""; 2239 if (oc) { 2240 var fnName = oc.split("(")[0].replace(/[^a-zA-Z0-9_$]/g, ""); 2241 if (fnName && typeof window[fnName] === "function") { 2242 $b[0].onclick = null; 2243 } 2244 } 2245 $b.removeAttr("onclick"); 2246 }); 2247 2248 // EC-CAROUSEL-003: Clear stale Embla/ShadCN inline transforms 2249 // Embla loop mode applies translate3d() on both the track AND individual slides 2250 // at runtime. Puppeteer captures these mid-animation values, positioning slides 2251 // off-screen on initial load (blank carousel). We must clear them before init. 2252 $track.css("transform", ""); 2253 $slides.css("transform", ""); 2254 2255 // Ensure viewport has overflow hidden (might be set via CSS class or style) 2256 if ($viewport.css("overflow") !== "hidden") { 2257 $viewport.css("overflow", "hidden"); 2258 } 2259 // Ensure track has flex display for translateX to work 2260 if ($track.css("display") !== "flex" && $track.css("display") !== "inline-flex") { 2261 $track.css({ display: "flex", flexWrap: "nowrap" }); 2262 } 2263 2264 // Detect dot/pagination elements within the region 2265 var $dots = $region.find('[class*="dot"], [class*="pagination"] > *, [class*="progress"] > *').filter(function() { 2266 var $d = $(this); 2267 if ($d.find("p, h1, h2, h3, img").length) return false; 2268 if ($d.children().length > 3) return false; 2269 return true; 2270 }); 2271 2272 function getMetrics() { 2273 var vw = $viewport.outerWidth(); 2274 var sw = $slides.first().outerWidth(true); 2275 if (!sw) sw = vw; 2276 // Account for flex gap (not included in outerWidth) 2277 var gap = parseFloat($track.css("gap") || $track.css("column-gap")) || 0; 2278 var step = sw + gap; 2279 var perView = Math.max(1, Math.round(vw / step)); 2280 var maxIdx = Math.max(0, $slides.length - perView); 2281 return { slideWidth: step, perView: perView, maxIdx: maxIdx }; 2282 } 2283 2284 // EC-CAROUSEL-014: Images/layout can report 0 width for one frame after paint; 2285 // translating with bad metrics blanks the hero or shifts the wrong flex row. 2286 var wpcMetricsRetries = 0; 2287 function goTo(idx) { 2288 var m = getMetrics(); 2289 var bad = !m.slideWidth || m.slideWidth < 2 || !isFinite(m.slideWidth) || !isFinite(m.maxIdx); 2290 if (bad && wpcMetricsRetries < 32) { 2291 wpcMetricsRetries++; 2292 window.setTimeout(function() { goTo(idx); }, 40); 2293 return; 2294 } 2295 wpcMetricsRetries = 0; 2296 if (!m.slideWidth || m.slideWidth < 2 || !isFinite(m.slideWidth)) { 2297 console.warn("[EC-CAROUSEL-014] Overflow carousel: slide metrics invalid after retries, skip transform"); 2298 return; 2299 } 2300 currentSlide = Math.max(0, Math.min(idx, m.maxIdx)); 2301 var offset = -(currentSlide * m.slideWidth); 2302 $track.css({ transform: "translateX(" + offset + "px)", transition: "transform 0.5s cubic-bezier(0.25,0.46,0.45,0.94)" }); 2303 $prevBtn.prop("disabled", currentSlide <= 0).css("opacity", currentSlide <= 0 ? 0.3 : 1); 2304 $nextBtn.prop("disabled", currentSlide >= m.maxIdx).css("opacity", currentSlide >= m.maxIdx ? 0.3 : 1); 2305 // Update dots 2306 if ($dots.length) { 2307 $dots.removeClass("active"); 2308 var dotIdx = Math.min(currentSlide, $dots.length - 1); 2309 $dots.eq(dotIdx).addClass("active"); 2310 } 2311 } 2312 2313 $prevBtn.off("click.wpcOverflow").on("click.wpcOverflow", function(e) { 2314 e.preventDefault(); e.stopPropagation(); 2315 goTo(currentSlide - 1); 2316 }); 2317 $nextBtn.off("click.wpcOverflow").on("click.wpcOverflow", function(e) { 2318 e.preventDefault(); e.stopPropagation(); 2319 goTo(currentSlide + 1); 2320 }); 2321 2322 // Dot click navigation 2323 if ($dots.length) { 2324 $dots.css("cursor", "pointer").on("click.wpcOverflow", function(e) { 2325 e.preventDefault(); 2326 goTo($(this).index()); 2327 }); 2328 } 2329 2330 // Touch/swipe support 2331 var touchX = 0; 2332 $viewport.on("touchstart.wpcOverflow", function(e) { 2333 touchX = (e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]).clientX; 2334 }); 2335 $viewport.on("touchend.wpcOverflow", function(e) { 2336 var t = e.originalEvent.changedTouches[0]; if (!t) return; 2337 var diff = touchX - t.clientX; 2338 if (Math.abs(diff) > 50) goTo(currentSlide + (diff > 0 ? 1 : -1)); 2339 }); 2340 2341 $(window).on("resize.wpcOverflow", function() { goTo(currentSlide); }); 2342 goTo(0); 2343 console.log("[EC-CAROUSEL-002] â Overflow carousel: " + $slides.length + " slides, dots: " + $dots.length); 2344 } 2345 2346 // Detect left/right arrow icon inside a button 2347 function classifyArrowBtn($btn) { 2348 var iconCls = ""; 2349 $btn.find("svg, i").each(function() { iconCls += " " + ($(this).attr("class") || ""); }); 2350 iconCls = iconCls.toLowerCase(); 2351 var srText = ($btn.find(".sr-only").text() || "").toLowerCase(); 2352 var btnText = ($btn.text() || "").replace(/\s+/g, "").toLowerCase(); 2353 2354 var leftPats = ["chevron-left", "arrow-left", "angle-left", "caret-left", "move-left"]; 2355 var rightPats = ["chevron-right", "arrow-right", "angle-right", "caret-right", "move-right"]; 2356 2357 for (var i = 0; i < leftPats.length; i++) { 2358 if (iconCls.indexOf(leftPats[i]) >= 0) return "prev"; 2359 } 2360 for (var i = 0; i < rightPats.length; i++) { 2361 if (iconCls.indexOf(rightPats[i]) >= 0) return "next"; 2362 } 2363 if (/\bprev|previous\b/.test(srText)) return "prev"; 2364 if (/\bnext\b/.test(srText)) return "next"; 2365 if (/^[<â¹â]$/.test(btnText)) return "prev"; 2366 if (/^[>âºâ]$/.test(btnText)) return "next"; 2367 // Check inline onclick for carousel function calls (e.g., slideCards(-1), slideCards(1)) 2368 var oc = ($btn.attr("onclick") || ""); 2369 if (/slide|scroll|carousel|rotate/i.test(oc)) { 2370 if (/\(\s*-/.test(oc) || /prev|back|left/i.test(oc)) return "prev"; 2371 if (/\(\s*1\b/.test(oc) || /next|forward|right/i.test(oc)) return "next"; 2372 } 2373 return null; 2374 } 2375 2376 // EC-CAROUSEL-008: Prefer direct-child overflow viewport (Embla/ShadCN). A plain .find() can match 2377 // slide-inner .overflow-hidden before the real track wrapper in some DOM orders. 2378 // EC-CAROUSEL-014: The first .overflow-hidden in a wide section may be a 2-col grid shell; 2379 // translating that track shifts copy + images together (blank or "flipped" layout on slide change).
2380 function wpcResolveCarouselViewport($root) { 2381 function scoreViewport($vp) { 2382 var cls = $vp.attr("class") || ""; 2383 if (/\bgrid\b/.test(cls) && /grid-cols-/.test(cls)) return -100; 2384 var $track = $vp.children().first(); 2385 if (!$track.length) return 0; 2386 var trackCls = $track.attr("class") || ""; 2387 var disp = ($track.css("display") || "").toLowerCase(); 2388 var flexLike = trackCls.indexOf("flex") >= 0 || disp.indexOf("flex") >= 0; 2389 var $kids = $track.children(); 2390 if ($kids.length < 2) return flexLike ? 6 : 0; 2391 // EC-CAROUSEL-015: Two-column hero (image track + prose) â not a horizontal slide strip; 2392 // translating this viewport blanks the image column or shifts copy with the flex row. 2393 if ($kids.length === 2) { 2394 var withImg = 0, textHeavy = 0; 2395 $kids.each(function() { 2396 var imgs = $(this).find("img").length; 2397 if (imgs > 0) withImg++; 2398 var tl = $(this).text().trim().length; 2399 if (imgs === 0 && tl > 100) textHeavy++; 2400 }); 2401 if (withImg === 1 && textHeavy >= 1) return -80; 2402 } 2403 var ariaSlides = $track.children('[aria-roledescription="slide"]').length; 2404 var slideLike = 0; 2405 $kids.each(function() { 2406 var c = $(this).attr("class") || ""; 2407 if (c.indexOf("shrink-0") >= 0 || c.indexOf("min-w-") >= 0 || c.indexOf("basis-") >= 0) slideLike++; 2408 if ($(this).find("img").length) slideLike++; 2409 }); 2410 var score = 0; 2411 if (flexLike) score += 20; 2412 if (ariaSlides >= 2) score += 45; 2413 else score += 12; 2414 if (slideLike >= 2) score += 28; 2415 if ($vp.closest('[role="region"][aria-roledescription="carousel"]').length) score += 32; 2416 return score; 2417 } 2418 var $all = $root.find(".overflow-hidden"); 2419 var best = null, bestS = -9999; 2420 if ($all.length) { 2421 $all.each(function() { 2422 var $cand = $(this); 2423 var s = scoreViewport($cand); 2424 if (s > bestS) { bestS = s; best = $cand; } 2425 }); 2426 } 2427 if (best && bestS >= 20) { 2428 console.log("[EC-CAROUSEL-014] wpcResolveCarouselViewport picked score=" + bestS); 2429 return best; 2430 } 2431 var $vp = $root.children(".overflow-hidden").first(); 2432 if ($vp.length) return $vp; 2433 $vp = $root.find("> .overflow-hidden").first(); 2434 if ($vp.length) return $vp; 2435 return $root.find(".overflow-hidden").first(); 2436 } 2437 2438 // Method 3: Detect by SVG icon class on buttons (lucide, heroicons, FA) 2439 // EC-CAROUSEL-013: Also match <i> with arrow icon classes (Remix Icon, etc.) 2440 var svgProcessed = []; 2441 $("button:has(svg), button:has(i[class*='fa-']), button:has(i[class*='arrow'])").each(function() { 2442 var $btn = $(this); 2443 if ($btn.data("carousel-processed")) return; 2444 if ($btn.closest("[data-wpconvert-stacked-gallery]").length) { $btn.data("carousel-processed", true); return; } 2445 if (wpcInsideJsonTestimonialCard($btn)) return; 2446 // EC-CAROUSEL-016e: Skip Radix/HeadlessUI accordion/collapsible triggers. 2447 // These have data-state="open"/"closed" + aria-controls and use rotated 2448 // chevrons that classifyArrowBtn would wrongly classify as carousel arrows. 2449 if ($btn.attr("data-state") && $btn.attr("aria-controls")) return; 2450 var role = classifyArrowBtn($btn); 2451 if (role !== "prev") return; // start from prev buttons 2452 2453 // Find matching next button: sibling buttons or nearby buttons in same parent 2454 var $nextBtn = null; 2455 $btn.parent().children("button").each(function() { 2456 if ($(this).is($btn)) return; 2457 if (classifyArrowBtn($(this)) === "next") { $nextBtn = $(this); return false; } 2458 }); 2459 2460 if (!$nextBtn || !$nextBtn.length) { 2461 // Try grandparent scope (buttons may be in separate wrapper divs) 2462 $btn.parent().parent().find("button").each(function() { 2463 if ($(this).is($btn)) return; 2464 if ($(this).data("carousel-processed")) return; 2465 if (classifyArrowBtn($(this)) === "next") { $nextBtn = $(this); return false; } 2466 }); 2467 } 2468 if (!$nextBtn || !$nextBtn.length) return; 2469 2470 // EC-CAROUSEL-016b: Skip stacked-image/video galleries in Method 3 too. 2471 // The button pair lives inside a container where 2+ children overlap at inset-0 2472 // (opacity-crossfade). Treating it as a horizontal carousel causes blank flashes. 2473 var $galParent = $btn.parent(); 2474 var stackedKids = $galParent.find("[class*='absolute'][class*='inset-0']"); 2475 if (stackedKids.length >= 2 && $galParent.find("img, video").length >= 2) { 2476 $btn.data("carousel-processed", true); 2477 $nextBtn.data("carousel-processed", true); 2478 svgProcessed.push("skip-stacked-gallery"); 2479 console.log("[EC-CAROUSEL-016b] Method 3 skipped stacked gallery (" + stackedKids.length + " layers)"); 2480 return; 2481 } 2482 2483 // EC-CAROUSEL-016c: Skip category filter bar sections. A sticky/scrollable 2484 // row of <button> pills with chevron scroll arrows is NOT a carousel. 2485 // Detect: closest section has overflow-x-auto child with 3+ buttons and no card grid. 2486 var $closestSec = $btn.closest("section"); 2487 if ($closestSec.length) { 2488 var $scrollRow = $closestSec.find("[class*='overflow-x-auto'], [class*='overflow-auto'], [class*='scrollbar-hide']"); 2489 if ($scrollRow.length && $scrollRow.find("button").length >= 3) { 2490 var sectionHasCardGrid = $closestSec.find("[class*='grid-cols']").length > 0; 2491 if (!sectionHasCardGrid) { 2492 $btn.data("carousel-processed", true); 2493 $nextBtn.data("carousel-processed", true); 2494 svgProcessed.push("skip-filter-bar"); 2495 console.log("[EC-CAROUSEL-016c] Method 3 skipped category filter bar section"); 2496 return; 2497 } 2498 } 2499 } 2500 2501 // Find the carousel container â walk up to section or a container with cards 2502 var $container = $btn.closest('[role="region"][aria-roledescription="carousel"]'); 2503 if (!$container.length) $container = $btn.closest("section"); 2504 if (!$container.length) $container = $btn.parent().parent().parent(); 2505 if ($container.data("carousel-processed")) return; 2506 2507 // EC-CAROUSEL-013b: Run centered-transform detection BEFORE overflow-carousel. 2508 // wpcResolveCarouselViewport uses the first .overflow-hidden in the section; nested 2509 // card chrome inside slides matches first and wrongly triggers flex-overflow mode. 2510 var centeredTransformOk = (function() { 2511 // EC-CAROUSEL-013: Centered-transform carousel â absolutely-positioned 2512 // slides using translateX/scale for a "card fan" effect. 2513 var $wrapper = null; 2514 $container.find(".absolute.inset-0").each(function() { 2515 var $w = $(this); 2516 var absKids = $w.children(".absolute[style*='translateX'], .absolute[style*='translate']"); 2517 if (absKids.length >= 2) { $wrapper = $w; return false; } 2518 }); 2519 if (!$wrapper) return false;
2520 2521 var $slides = $wrapper.children(".absolute"); 2522 if ($slides.length < 2) return false; 2523 2524 var positions = []; 2525 $slides.each(function(idx) { 2526 var s = $(this).attr("style") || ""; 2527 var tx = 0, sc = 1, op = 1, z = 10; 2528 var txM = s.match(/translateX(([^)]+))/); 2529 if (txM) tx = parseFloat(txM[1]) || 0; 2530 var scM = s.match(/scale(([^)]+))/); 2531 if (scM) sc = parseFloat(scM[1]) || 1; 2532 var opM = s.match(/opacity:s*([0-9.]+)/); 2533 if (opM) op = parseFloat(opM[1]); 2534 var zM = s.match(/z-index:s*(d+)/); 2535 if (zM) z = parseInt(zM[1], 10); 2536 positions.push({ idx: idx, tx: tx, scale: sc, opacity: op, zIndex: z }); 2537 }); 2538 2539 positions.sort(function(a, b) { return Math.abs(a.tx) - Math.abs(b.tx); }); 2540 var centerOrigIdx = positions[0].idx; 2541 var sorted = positions.slice().sort(function(a, b) { return a.tx - b.tx; }); 2542 var gap = sorted.length > 1 ? Math.abs(sorted[1].tx - sorted[0].tx) : 768; 2543 2544 var scaleCenter = 1, scaleAdj = 0.83, scaleFar = 0.68; 2545 var opCenter = 1, opAdj = 0.7, opFar = 0.35; 2546 var zCenter = 30, zAdj = 20, zFar = 10; 2547 2548 $btn.data("carousel-processed", true); 2549 $nextBtn.data("carousel-processed", true); 2550 $container.data("carousel-processed", true); 2551 2552 var total = $slides.length; 2553 var current = centerOrigIdx; 2554 // Dots live in the row below the slider (sibling of .relative wrapper), not inside $container.find(all rounded-full) 2555 var $dotRow = $btn.parent().nextAll("div").filter(function() { 2556 return $(this).find("button.rounded-full").length >= 2; 2557 }).first(); 2558 var $dots = $dotRow.length ? $dotRow.find("button.rounded-full") : $(); 2559 2560 function renderTransform(idx) { 2561 $slides.each(function(si) { 2562 var offset = si - idx; 2563 var absOff = Math.abs(offset); 2564 var tx = offset * gap; 2565 var sc = absOff === 0 ? scaleCenter : absOff === 1 ? scaleAdj : scaleFar; 2566 var op = absOff === 0 ? opCenter : absOff === 1 ? opAdj : opFar; 2567 var z = absOff === 0 ? zCenter : absOff === 1 ? zAdj : zFar; 2568 $(this).css({ 2569 transform: "translateX(" + tx + "px) scale(" + sc + ")", 2570 opacity: op, 2571 zIndex: z, 2572 pointerEvents: absOff <= 1 ? "auto" : "none" 2573 }); 2574 }); 2575 if ($dots.length) { 2576 $dots.css({ width: "8px" }).removeClass("bg-revo-orange bg-white").addClass("bg-white/20"); 2577 $dots.eq(idx % $dots.length).css({ width: "32px" }).removeClass("bg-white/20").addClass("bg-revo-orange"); 2578 } 2579 } 2580 2581 $btn.off("click.wpcTransform").on("click.wpcTransform", function(e) { 2582 e.preventDefault(); e.stopPropagation(); 2583 current = (current - 1 + total) % total; 2584 renderTransform(current); 2585 }); 2586 $nextBtn.off("click.wpcTransform").on("click.wpcTransform", function(e) { 2587 e.preventDefault(); e.stopPropagation(); 2588 current = (current + 1) % total; 2589 renderTransform(current); 2590 }); 2591 if ($dots.length) { 2592 $dots.css("cursor", "pointer").on("click.wpcTransform", function(e) { 2593 e.preventDefault(); 2594 current = $(this).index(); 2595 renderTransform(current); 2596 }); 2597 } 2598 2599 renderTransform(current); 2600 console.log("[EC-CAROUSEL-013] â Centered-transform carousel: " + total + " slides, gap=" + ga
2600p); 2601 return true; 2602 })(); 2603 2604 if (centeredTransformOk) { 2605 svgProcessed.push("centered-transform:" + $container.find(".absolute.inset-0 > .absolute").length); 2606 } else { 2607 var $viewport = wpcResolveCarouselViewport($container); 2608 var $track = $viewport.children().first(); 2609 var $slides = $track.children('[aria-roledescription="slide"]'); 2610 2611 if ($viewport.length && $track.length && $slides.length > 1) { 2612 $btn.data("carousel-processed", true); 2613 $nextBtn.data("carousel-processed", true); 2614 $container.data("carousel-processed", true); 2615 initializeOverflowCarousel($container, $viewport, $track, $slides, $btn, $nextBtn); 2616 svgProcessed.push("overflow:" + $slides.length); 2617 } else if ($viewport.length && $track.length && $track.children().length > 1) { 2618 $slides = $track.children(); 2619 $btn.data("carousel-processed", true); 2620 $nextBtn.data("carousel-processed", true); 2621 $container.data("carousel-processed", true); 2622 initializeOverflowCarousel($container, $viewport, $track, $slides, $btn, $nextBtn); 2623 svgProcessed.push("flex-overflow:" + $slides.length); 2624 } else { 2625 // No overflow viewport found â check for a CSS grid with cards 2626 // and convert it to a scrollable overflow carousel 2627 var $grid = $container.find('[class*="grid"]').first(); 2628 if ($grid.length && $grid.children().length >= 2) { 2629 // EC-CAROUSEL-016: Two-column hero (copy + stacked image gallery with chevrons) â never 2630 // re-wrap the section grid as a horizontal overflow track (blank flashes, copy/image swap sides). 2631 var skipHeroGrid = (function() { 2632 var $kids = $grid.children(); 2633 if ($kids.length !== 2) return false; 2634 var galleryIdx = -1; 2635 $kids.each(function(i) { 2636 if ($.contains(this, $btn[0])) { galleryIdx = i; return false; } 2637 }); 2638 if (galleryIdx < 0) return false; 2639 var $gal = $kids.eq(galleryIdx); 2640 if ($gal.find("img").length < 2) return false; 2641 var $copy = $kids.not($gal).first(); 2642 if (!$copy.length || $copy.find("h1,h2").length === 0) return false; 2643 if (($copy.text() || "").trim().length < 80) return false; 2644 return true; 2645 })(); 2646 if (skipHeroGrid) { 2647 $btn.data("carousel-processed", true); 2648 $nextBtn.data("carousel-processed", true); 2649 $container.data("carousel-processed", true); 2650 svgProcessed.push("skip-hero-grid"); 2651 } else { 2652 var containerW = $grid.outerWidth(); 2653 var numKids = $grid.children().length; 2654 var origChildW = $grid.children().first().outerWidth(); 2655 var gapVal = parseFloat($grid.css("gap") || $grid.css("column-gap")) || 20; 2656 2657 // Size children so at least one is partially off-screen 2658 // (shows N-1 full items, ensuring the carousel has content to scroll) 2659 var idealW = numKids > 1 ? (containerW / Math.max(1, numKids - 1)) * 0.95 : origChildW; 2660 var childW = Math.max(origChildW, idealW); 2661 2662 // Wrap all children in a new flex track 2663 $grid.children().wrapAll('<div class="wpc-carousel-track" style="display:flex;gap:' + gapVal + 'px;flex-wrap:nowrap;transition:transform 0.5s cubic-bezier(0.25,0.46,0.45,0.94)"></div>'); 2664 var $newTrack = $grid.find(".wpc-carousel-track"); 2665 $newTrack.children().css({ flexShrink: 0, width: childW + "px" }); 2666 2667 // Convert grid container to overflow viewport 2668 var oldCls = $grid.attr("class") || ""; 2669 $grid.attr("class", oldCls.replace(/\b(grid|grid-cols-\S+|gap-\S+)\b/g, "").trim()); 2670 $grid.css({ overflow: "hidden", display: "block" }); 2671 2672 $btn.data("carousel-processed", true); 2673 $nextBtn.data("carousel-processed", true); 2674 $container.data("carousel-processed", true); 2675 initializeOverflowCarousel($container, $grid, $newTrack, $newTrack.children(), $btn, $nextBtn); 2676 svgProcessed.push("grid-to-overflow:" + $newTrack.children().length); 2677 } 2678 } else { 2679 $btn.data("carousel-processed", true); 2680 $nextBtn.data("carousel-processed", true); 2681 $container.data("carousel-processed", true); 2682 initializeCarousel($container, $btn, $nextBtn); 2683 svgProcessed.push("grid:" + ($container.find('[class*="grid"]').first().children().length || "?")); 2684 } 2685 } 2686 } 2687 }); 2688 if (svgProcessed.length) console.log("[EC-CAROUSEL-002] Method 3 (SVG icons): " + svgProcessed.join(", ")); 2689 2690 // Method 4: Detect by aria-roledescription="carousel" (Embla/ShadCN semantic) 2691 $('[role="region"][aria-roledescription="carousel"]').each(function() { 2692 var $region = $(this); 2693 if ($region.data("carousel-processed")) return; 2694 2695 var $viewport = wpcResolveCarouselViewport($region); 2696 var $track = $viewport.children().first(); 2697 var $slides = $track.children('[aria-roledescription="slide"]'); 2698 if (!$slides.length) $slides = $track.children(); 2699 if ($slides.length < 2) return; 2700 2701 var $prevBtn = $region.find("button").filter(function() { return classifyArrowBtn($(this)) === "prev"; }).first(); 2702 var $nextBtn = $region.find("button").filter(function() { return classifyArrowBtn($(this)) === "next"; }).first(); 2703 2704 if (!$prevBtn.length || !$nextBtn.length) { 2705 // Embla buttons may be outside the overflow container but inside region 2706 $prevBtn = $region.children("button").filter(function() { return classifyArrowBtn($(this)) === "prev"; }).first(); 2707 $nextBtn = $region.children("button").filter(function() { return classifyArrowBtn($(this)) === "next"; }).first(); 2708 } 2709 2710 if ($prevBtn.length && $nextBtn.length && $viewport.length && $track.length) { 2711 $region.data("carousel-processed", true); 2712 $prevBtn.data("carousel-processed", true); 2713 $nextBtn.data("carousel-processed", true); 2714 initializeOverflowCarousel($region, $viewport, $track, $slides, $prevBtn, $nextBtn); 2715 console.log("[EC-CAROUSEL-002] Method 4 (ARIA): " + $slides.length + " slides"); 2716 } 2717 }); 2718 2719 // Method 5: Generic overflow-hidden scrollable containers with nearby arrow buttons 2720 $(".overflow-hidden, [style*='overflow: hidden'], [style*='overflow:hidden']").each(function() { 2721 var $viewport = $(this); 2722 if ($viewport.data("carousel-processed")) return; 2723 if ($viewport.attr("data-wpconvert-stacked-gallery")) return; 2724 var $track = $viewport.children().first(); 2725 if (!$track.length) return; 2726 var $children = $track.children(); 2727 if ($children.length < 3) return; 2728 2729 // Check if children form a horizontal row (flex or inline) 2730 var hasFlex = ($track.attr("class") || "").indexOf("flex") >= 0; 2731 var hasInline = ($track.css("display") || "").indexOf("flex") >= 0; 2732 if (!hasFlex && !hasInline) return; 2733 2734 // Check children have shrink-0 or min-w-0 (carou
2734sel slide indicators) 2735 var looksLikeSlides = $children.filter(function() { 2736 var cls = $(this).attr("class") || ""; 2737 return cls.indexOf("shrink-0") >= 0 || cls.indexOf("min-w-0") >= 0 || cls.indexOf("basis-") >= 0; 2738 }).length >= 2; 2739 if (!looksLikeSlides) return; 2740 2741 // Find nearby arrow buttons (sibling of viewport, or in parent) 2742 var $parent = $viewport.parent(); 2743 var $prevBtn = $parent.find("button").filter(function() { return classifyArrowBtn($(this)) === "prev"; }).first(); 2744 var $nextBtn = $parent.find("button").filter(function() { return classifyArrowBtn($(this)) === "next"; }).first(); 2745 if (!$prevBtn.length || !$nextBtn.length) return; 2746 if ($prevBtn.data("carousel-processed") || $nextBtn.data("carousel-processed")) return; 2747 2748 $viewport.data("carousel-processed", true); 2749 $prevBtn.data("carousel-processed", true); 2750 $nextBtn.data("carousel-processed", true); 2751 initializeOverflowCarousel($parent, $viewport, $track, $children, $prevBtn, $nextBtn); 2752 console.log("[EC-CAROUSEL-002] Method 5 (generic overflow): " + $children.length + " items"); 2753 }); 2754 2755 // Method 6: onclick handlers + text arrow buttons + computed CSS overflow 2756 // Catches custom-coded carousels (not React/Tailwind based) like FormAcademy 2757 $("button").each(function() { 2758 var $btn = $(this); 2759 if ($btn.data("carousel-processed")) return; 2760 if ($btn.closest("[data-wpconvert-stacked-gallery-section]").length) { $btn.data("carousel-processed", true); return; } 2761 if ($btn.find("svg").length > 0) return; 2762 var role = classifyArrowBtn($btn); 2763 if (role !== "prev") return; 2764 2765 var $nextBtn = null; 2766 $btn.siblings("button").each(function() { 2767 if ($(this).is($btn) || $(this).data("carousel-processed")) return; 2768 if (classifyArrowBtn($(this)) === "next") { $nextBtn = $(this); return false; } 2769 }); 2770 if (!$nextBtn || !$nextBtn.length) { 2771 $btn.parent().parent().find("button").not($btn).each(function() { 2772 if ($(this).data("carousel-processed")) return; 2773 if (classifyArrowBtn($(this)) === "next") { $nextBtn = $(this); return false; } 2774 }); 2775 } 2776 if (!$nextBtn || !$nextBtn.length) return; 2777 2778 var $section = $btn.closest("section"); 2779 if (!$section.length) $section = $btn.parent().parent().parent(); 2780 if ($section.data("carousel-processed")) return; 2781 2782 var $viewport = null, $track = null, $slides = null; 2783 // Strategy 1: detect by computed CSS overflow + flex 2784 $section.find("div").each(function() { 2785 var $el = $(this); 2786 if ($el.data("carousel-processed")) return; 2787 var ov = $el.css("overflow") || $el.css("overflow-x") || ""; 2788 var hasOverflow = ov === "hidden" || $el.hasClass("overflow-hidden"); 2789 if (!hasOverflow) return; 2790 var $t = $el.children().first(); 2791 if (!$t.length) return; 2792 var disp = $t.css("display") || ""; 2793 if (disp !== "flex" && disp !== "inline-flex") return; 2794 if ($t.children().length < 2) return; 2795 $viewport = $el; 2796 $track = $t; 2797 $slides = $t.children(); 2798 return false; 2799 }); 2800 // Strategy 2: detect by element ID / class name patterns (fallback for CSS timing) 2801 if (!$viewport || !$viewport.length || !$slides || $slides.length < 2) { 2802 $viewport = null; $track = null; $slides = null; 2803 var vpPat = /viewport|slider-?wrap|carousel-?wrap|slide-?contain/i; 2804 var trPat = /track|slider-?inner|carousel-?inner|slides-?row/i; 2805 $section.find("div").each(function() { 2806 var $el = $(this); 2807 var elId = $el.attr("id") || ""; 2808 var elCls = $el.attr("class") || ""; 2809 if (!vpPat.test(elId) && !vpPat.test(elCls)) return; 2810 var $t = $el.children().first(); 2811 if (!$t.length || $t.children().length < 2) return; 2812 var tId = $t.attr("id") || ""; 2813 var tCls = $t.attr("class") || "";
2814 if (trPat.test(tId) || trPat.test(tCls) || $t.css("display") === "flex") { 2815 $viewport = $el; 2816 $track = $t; 2817 $slides = $t.children(); 2818 return false; 2819 } 2820 }); 2821 } 2822 2823 if (!$viewport || !$viewport.length || !$slides || $slides.length < 2) return; 2824 2825 $btn.data("carousel-processed", true); 2826 $nextBtn.data("carousel-processed", true); 2827 $section.data("carousel-processed", true); 2828 initializeOverflowCarousel($section, $viewport, $track, $slides, $btn, $nextBtn); 2829 console.log("[EC-CAROUSEL-002] Method 6 (onclick/text): " + $slides.length + " items"); 2830 }); 2831 2832 // EC-CAROUSEL-003: Method 7 â Testimonial/card grids WITHOUT arrow buttons 2833 // Detects sections with 3+ similar child elements (testimonial cards, review cards) 2834 // in a grid that exceeds the viewport, and enables auto-advance carousel with dots/swipe 2835 $("section").each(function() { 2836 var $sec = $(this); 2837 if ($sec.data("carousel-processed")) return; 2838 var secCls = ($sec.attr("class") || "").toLowerCase(); 2839 var secText = ($sec.find("h2, h3").first().text() || "").toLowerCase(); 2840 var isTestimonialSection = /testimonial|review|feedback|client|what.*say|hear.*from/i.test(secCls + " " + secText); 2841 if (!isTestimonialSection) return; 2842 // Must have NO prev/next buttons already (those are handled above) 2843 var hasPrevNext = $sec.find("button").filter(function() { 2844 return classifyArrowBtn($(this)) !== null; 2845 }).length >= 2; 2846 if (hasPrevNext) return; 2847 // Find the grid/flex container with 3+ card children 2848 var $grid = $sec.find("[class*='grid'], [class*='flex']").filter(function() { 2849 var kids = $(this).children(); 2850 if (kids.length < 3) return false; 2851 // Children should be similarly sized (cards) 2852 var hasContent = kids.filter(function() { 2853 return $(this).find("p, blockquote, [class*='quote']").length > 0; 2854 }).length >= 3; 2855 return hasContent; 2856 }).first(); 2857 if (!$grid.length) return; 2858 var $items = $grid.children(); 2859 if ($items.length < 3) return; 2860 var totalItems = $items.length; 2861 var currentIndex = 0; 2862 var columnConfig = { base: 1, sm: 1, md: null, lg: null, xl: null }; 2863 var cls = $grid.attr("class") || ""; 2864 cls.split(/\s+/).forEach(function(c) { 2865 var m = c.match(/^(sm:|md:|lg:|xl:)?grid-cols-(\d+)/); 2866 if (m) { 2867 var bp = m[1] ? m[1].replace(":", "") : "base"; 2868 columnConfig[bp] = parseInt(m[2], 10); 2869 } 2870 // EC-CAROUSEL-018: Parse non-Tailwind CSS grid class names (grid--2, grid--3, etc.) 2871 var cssGridMatch = c.match(/^grid--(\d+)$/); 2872 if (cssGridMatch) { 2873 var n = parseInt(cssGridMatch[1], 10); 2874 if (n > (columnConfig.base || 1)) columnConfig.base = n; 2875 } 2876 }); 2877 // EC-CAROUSEL-006: Skip sections where the grid already shows all items. 2878 // If any breakpoint defines enough columns to display all items, 2879 // this is a static testimonial grid, not a carousel. 2880 var maxCols = Math.max( 2881 columnConfig.base || 1, columnConfig.sm || 0, 2882 columnConfig.md || 0, columnConfig.lg || 0, columnConfig.xl || 0 2883 ); 2884 // EC-CAROUSEL-006b: For CSS-defined grids (no Tailwind grid-cols-N classes), 2885 // check computed gridTemplateColumns to detect multi-column layouts. 2886 // Sites built with custom CSS (e.g. Claude/AI-generated static HTML) define 2887 // columns via stylesheet rules, not utility classes.
2888 if (maxCols <= 1 && $grid[0]) { 2889 try { 2890 var gtc = window.getComputedStyle($grid[0]).gridTemplateColumns; 2891 if (gtc && gtc !== "none") { 2892 var cssCols = gtc.split(/\s+/).filter(function(v) { return v && v !== "0px"; }).length; 2893 if (cssCols > maxCols) { 2894 maxCols = cssCols; 2895 console.log("[EC-CAROUSEL-006b] Detected " + cssCols + " CSS grid columns via computed style"); 2896 } 2897 } 2898 } catch(e) {} 2899 } 2900 if (maxCols >= totalItems) { 2901 console.log("[EC-CAROUSEL-006] Skipped static testimonial grid: " + totalItems + " items, max " + maxCols + " cols"); 2902 return; 2903 } 2904 // EC-CAROUSEL-007/018: Multi-row grids with 2+ cols that fit in â¤4 rows are static. 2905 // Testimonial grids often have 4+ rows (e.g. 8 items in 2 cols) â these are 2906 // intentional scrollable page sections, not carousels. 2907 if (maxCols >= 2 && totalItems <= maxCols * 4) { 2908 console.log("[EC-CAROUSEL-007] Skipped static multi-row testimonial grid: " + totalItems + " items, max " + maxCols + " cols"); 2909 return; 2910 } 2911 // EC-CAROUSEL-003 FIX: Skip flex-row layouts â these render all items 2912 // side-by-side at the responsive breakpoint, not as a scrollable carousel. 2913 // Flex-row has no grid-cols-N classes, so columnConfig stays at {base:1} 2914 // which falsely makes it look like it needs a carousel. 2915 var gridCls = cls; 2916 if (/\b(flex-row|md:flex-row|lg:flex-row|sm:flex-row)\b/.test(gridCls)) { 2917 console.log("[EC-CAROUSEL-003] Skipped flex-row testimonial layout (" + totalItems + " items) â static side-by-side display"); 2918 return; 2919 } 2920 // Also check the parent: flex flex-col md:flex-row means items are rows on mobile 2921 // but side-by-side on md+. The grid container itself may be the flex parent. 2922 var parentCls = ($grid.parent().attr("class") || ""); 2923 if (/\b(flex-row|md:flex-row|lg:flex-row|sm:flex-row)\b/.test(parentCls)) { 2924 console.log("[EC-CAROUSEL-003] Skipped flex-row testimonial layout via parent (" + totalItems + " items)"); 2925 return; 2926 } 2927 // EC-CAROUSEL-019: Skip CSS marquee tracks. A marquee is already an 2928 // auto-scrolling carousel (translateX animation on a width:max-content 2929 // flex track) â it is NOT a paginated grid. If we treat it like one, 2930 // showItems() hides all-but-perView siblings with display:none, which 2931 // collapses the track from sum-of-card-widths down to a single card's 2932 // width. The remaining card then animates -50% (~ -170px) and snaps 2933 // back, producing the "one card sliding left, then switching" symptom 2934 // (Lovable / shadcn site marquees: .marquee-track, .marquee-track-*). 2935 var isMarqueeTrack = /\bmarquee\b|\bmarquee-[a-z0-9_-]+\b|\bw-max\b/i.test(cls); 2936 if (!isMarqueeTrack && $grid[0]) { 2937 try { 2938 var gcs = window.getComputedStyle($grid[0]); 2939 var animName = gcs.animationName; 2940 var widthBag = (gcs.width || "") + " " + (gcs.minWidth || "") + " " + (gcs.maxWidth || ""); 2941 // width:max-content + a running CSS animation = unambiguous marquee 2942 if (animName && animName !== "none" && /max-content|fit-content/.test(widthBag)) { 2943 isMarqueeTrack = true; 2944 } 2945 } catch(e) {} 2946 } 2947 if (isMarqueeTrack) { 2948 console.log("[EC-CAROUSEL-019] Skipped marquee track (" + totalItems + " items) â CSS auto-scrolling animation, not a paginated carousel"); 2949 return; 2950 }
2951 // EC-CAROUSEL-020: Skip native horizontal-scroll testimonial rows. 2952 // Lovable / shadcn projects emit testimonials as a flex track with 2953 // overflow-x-auto + scrollbar-hide + snap-x + snap-mandatory and 2954 // child cards carrying flex-shrink-0 + w-[Npx] + snap-start. The 2955 // user scrolls horizontally with the trackpad / touch / programmatic 2956 // arrow buttons; every card is meant to be visible by design. 2957 // 2958 // Earlier skips do NOT catch this: 2959 // - EC-CAROUSEL-006/007/018 (grid math): no grid-cols-N classes, 2960 // so columnConfig stays base:1 and maxCols < totalItems. 2961 // - EC-CAROUSEL-003 flex-row: class is "flex" not "flex-row". 2962 // - EC-CAROUSEL-019 marquee: no marquee/w-max class, no CSS animation. 2963 // 2964 // Without this guard EC-CAROUSEL-003 calls items.hide() and 2965 // showItems(perView=1) â only the first card stays visible while a 2966 // 5-second autoplay rotates the rest, replacing the original 2967 // multi-card scrolling layout with a single-item slideshow. 2968 // 2969 // Detection (cheap class check first, then computed style): 2970 // - track class contains overflow-x-auto, overflow-x-scroll, 2971 // snap-x, or scrollbar-hide 2972 // - OR computed overflow-x is auto or scroll 2973 var isOverflowScrollRow = /\boverflow-x-(auto|scroll)\b|\bsnap-x\b|\bscrollbar-hide\b/i.test(cls); 2974 if (!isOverflowScrollRow && $grid[0]) { 2975 try { 2976 var ovs = window.getComputedStyle($grid[0]); 2977 var overflowX = ovs.overflowX || ""; 2978 if (/^(auto|scroll)$/.test(overflowX)) { 2979 isOverflowScrollRow = true; 2980 } 2981 } catch(e) {} 2982 } 2983 if (isOverflowScrollRow) { 2984 console.log("[EC-CAROUSEL-020] Skipped native overflow-x scroll row (" + totalItems + " items) â horizontal scroll-snap layout, not a paginated carousel"); 2985 return; 2986 } 2987 $sec.data("carousel-processed", true); 2988 function colsForWidth(w) { 2989 if (w >= 1280 && columnConfig.xl) return columnConfig.xl; 2990 if (w >= 1024 && columnConfig.lg) return columnConfig.lg; 2991 if (w >= 768 && columnConfig.md) return columnConfig.md; 2992 if (w >= 640 && columnConfig.sm) return columnConfig.sm; 2993 return columnConfig.base || columnConfig.lg || columnConfig.md || 1; 2994 } 2995 var perView = Math.max(1, Math.min(totalItems, colsForWidth(window.innerWidth))); 2996 function showItems() { 2997 $items.hide(); 2998 for (var i = 0; i < perView; i++) { 2999 $items.eq((currentIndex + i) % totalItems).fadeIn(200); 3000 } 3001 // Update dots if present 3002 var $dots = $sec.find("[class*='dot'], [class*='pagination'] button"); 3003 if ($dots.length > 0) { 3004 $dots.css("opacity", "0.4"); 3005 $dots.eq(currentIndex % $dots.length).css("opacity", "1"); 3006 } 3007 } 3008 if (totalItems > perView) { 3009 showItems(); 3010 var autoInterval = setInterval(function() { 3011 currentIndex = (currentIndex + perView) % totalItems; 3012 showItems(); 3013 }, 5000); 3014 // Touch swipe support 3015 var touchX = 0; 3016 $grid.on("touchstart.wpcTestimonial", function(e) { 3017 var t = e.originalEvent.touches[0]; 3018 if (t) touchX = t.clientX; 3019 }); 3020 $grid.on("touchend.wpcTestimonial", function(e) { 3021 var t = e.originalEvent.changedTouches[0]; if (!t) return; 3022 var diff = touchX - t.clientX; 3023 if (Math.abs(diff) > 50) { 3024 clearInterval(autoInterval); 3025 currentIndex = (currentIndex + (diff > 0 ? perView : -perView) + totalItems) % totalItems; 3026 showItems(); 3027 autoInterval = setInterval(function() { 3028 currentIndex = (currentIndex + perView) % totalItems; 3029 showItems(); 3030 }, 5000); 3031 } 3032 }); 3033 // Dot click 3034 $sec.find("[class*='dot'], [class*='pagination'] button").on("click", function(e) { 3035 e.preventDefault(); 3036 clearInterval(autoInterval); 3037 currentIndex = $(this).index(); 3038 showItems(); 3039 autoInterval = setInterval(function() { 3040 currentIndex = (currentIndex + perView) % totalItems; 3041 showItems(); 3042 }, 5000); 3043 }); 3044 $(window).on("resize.wpcTestimonial", function() { 3045 var newView = Math.max(1, Math.min(totalItems, colsForWidth(window.innerWidth))
3045); 3046 if (newView !== perView) { perView = newView; currentIndex = 0; showItems(); } 3047 }); 3048 console.log("[EC-CAROUSEL-003] Testimonial carousel (no arrows): " + totalItems + " items, " + perView + " per view"); 3049 } 3050 }); 3051 3052 } catch(e) { console.warn("[WPConvert] EC-CAROUSEL-002 error:", e); } 3053 3054 try { // [WPConvert: EC-CAROUSEL-001 Testimonials] 3055 // EC-CAROUSEL-001: Single-item testimonial carousel handler 3056 // React state-based carousels only render one item â data is injected 3057 // as a JSON attribute by the snapshot phase. 3058 $("section[data-wpconvert-testimonials]").each(function() { 3059 var $section = $(this); 3060 if ($section.data("carousel-processed")) return; 3061 3062 var rawData = $section.attr("data-wpconvert-testimonials"); 3063 if (!rawData) return; 3064 3065 // EC-CAROUSEL-017: Decode Base64-encoded JSON (prevents cheerio mangling double quotes). 3066 // Fallback: try raw JSON for legacy themes. 3067 var testimonials; 3068 try { 3069 testimonials = JSON.parse(decodeURIComponent(escape(atob(rawData)))); 3070 } catch (e) { 3071 try { testimonials = JSON.parse(rawData); } catch (e2) { return; } 3072 } 3073 if (!testimonials || testimonials.length < 2) return; 3074 3075 console.log("[WPConvert] Initializing testimonial carousel with " + testimonials.length + " items"); 3076 3077 var activeIndex = 0; 3078 3079 // EC-TESTIMONIAL-004: Prefer aria-label Prev/Next (inner quote card) before any <i> scan. 3080 // Revo nests an outer slide track (first ri-arrow-* in DOM) + inner card controls; scanning 3081 // icons first bound the wrong buttons and .first() rounded-xl matched empty slide cards. 3082 var $prevBtn = $section.find('button[aria-label*="Previous"], button[aria-label*="Prev"], button[aria-label*="previous"]').first(); 3083 var $nextBtn = $section.find('button[aria-label*="Next"], button[aria-label*="next"]').first(); 3084 3085 // Prev/Next buttons: find buttons containing chevron SVGs 3086 if (!$prevBtn.length || !$nextBtn.length) { 3087 $prevBtn = null; 3088 $nextBtn = null; 3089 $section.find("button").each(function() { 3090 var $btn = $(this); 3091 var $svg = $btn.find("svg"); 3092 if (!$svg.length) return; 3093 var svgClass = $svg.attr("class") || ""; 3094 var pathD = ""; 3095 $svg.find("path").each(function() { pathD += ($(this).attr("d") || ""); }); 3096 if (/chevron-left|lucide-chevron-left/.test(svgClass) || /m\s*15\s+18|15\s*18\s*-6\s*-6/i.test(pathD)) { 3097 if (!$prevBtn) $prevBtn = $btn; 3098 } 3099 if (/chevron-right|lucide-chevron-right/.test(svgClass) || /m\s*9\s+18|9\s*18\s*6\s*-6/i.test(pathD)) { 3100 if (!$nextBtn) $nextBtn = $btn; 3101 } 3102 }); 3103 } 3104 3105 // EC-TESTIMONIAL-002: <i> icon arrow buttons (Remix Icon, etc.) â only if aria/SVG pair missing 3106 if (!$prevBtn || !$prevBtn.length || !$nextBtn || !$nextBtn.length) { 3107 $prevBtn = null; 3108 $nextBtn = null; 3109 $section.find("button").each(function() { 3110 var $btn = $(this); 3111 var $icon = $btn.find("i"); 3112 if (!$icon.length) return; 3113 var iconCls = ($icon.attr("class") || "").toLowerCase(); 3114 if (/arrow-left|chevron-left|angle-left|caret-left/.test(iconCls)) { 3115 if (!$prevBtn) $prevBtn = $btn; 3116 } 3117 if (/arrow-right|chevron-right|angle-right|caret-right/.test(iconCls)) { 3118 if (!$nextBtn) $nextBtn = $btn; 3119 } 3120 }); 3121 } 3122 3123 if (!$prevBtn || !$prevBtn.length || !$nextBtn || !$nextBtn.length) { 3124 console.log("[WPConvert] Testimonial carousel: no prev/next buttons found"); 3125 return; 3126 } 3127 3128 // Card = rounded shell that contains the quote/author content. 3129 // First try: card that contains the button (buttons inside card). 3130 var $cardArea = $prevBtn.closest('[class*="rounded-3xl"], [class*="rounded-2xl"], [class*="rounded-xl"], blockquote').first(); 3131 // Fallback: buttons may be siblings of the card (e.g. Bloom layout where 3132 // prev/next sit below the card in a separate flex row). Find the first 3133 // rounded card within the section that contains quote-length text. 3134 if (!$cardArea.length || $cardArea.is($section)) { 3135 $cardArea = $section.find('[class*="rounded-3xl"], [class*="rounded-2xl"], [class*="rounded-xl"]').filter(function() { 3136 return $(this).find("p").length > 0 && $(this).text().trim().length > 40; 3137 }).first(); 3138 } 3139 if (!$cardArea.length) $cardArea = $section; 3140 3141 // Author name: bold/semibold text inside author area 3142 var $authorContainer = $cardArea.find('[class*="items-center"]').filter(function() { 3143 return $(this).find("button").length === 0; 3144 }).first(); 3145 if (!$authorContainer.length) $authorContainer = $cardArea.find('[class*="items-center"]').first(); 3146 3147 // Quote: longest <p> in card, excluding paragraphs inside the author row 3148 var $quoteEl = null; 3149 var bestQLen = 0; 3150 $cardArea.find("p").each(function() { 3151 var $p = $(this); 3152 if ($authorContainer.length && $p.closest($authorContainer).length) return; 3153 var txt = $p.text().trim(); 3154 if (txt.length > bestQLen && txt.length > 40) { 3155 bestQLen = txt.length; 3156 $quoteEl = $p; 3157 } 3158 }); 3159 if (!$quoteEl || !$quoteEl.length) { 3160 $cardArea.find("blockquote").each(function() { 3161 var txt = $(this).text().trim(); 3162 if (txt.length > bestQLen && txt.length > 40) { 3163 bestQLen = txt.length; 3164 $quoteEl = $(this); 3165 } 3166 }); 3167 if (!$quoteEl && $cardArea.is("blockquote") && $cardArea.text().trim().length > 40) { 3168 $quoteEl = $cardArea; 3169 } 3170 } 3171 3172 if (!$quoteEl || !$quoteEl.length) { 3173 console.log("[WPConvert] Testimonial carousel: no quote element found"); 3174 return; 3175 } 3176 3177 var $authorName = null; 3178 var $authorMeta = null; 3179 3180 // Search for author name/role in the author container (items-center flex row) 3181 var $authorSearchArea = $authorContainer.length ? $authorContainer : null; 3182 if ($authorSearchArea) { 3183 $authorSearchArea.find("p, div, span, h4, h5").each(function() { 3184 var cls = $(this).attr("class") || ""; 3185 if (/font-semibold|font-bold|font-extrabold|font-medium/.test(cls) && !$authorName) { 3186 var txt = $(this).text().trim(); 3187 if (txt.length > 1 && txt.length < 60) $authorName = $(this); 3188 } else if (/text-sm|text-xs|opacity|text-cream|text-gray|text-slate|text-muted/.test(cls) && !$authorMeta) { 3189 $authorMeta = $(this); 3190 } 3191 }); 3192 } 3193 3194 // EC-CAROUSEL-017b: Fallback â when no items-center container exists, 3195 // search directly in the card area for bold author names and small/muted role text. 3196 // Skip elements that are already identified as the quote or inside buttons. 3197 if (!$authorName) { 3198 $cardArea.find("p, h4, h5, span").each(function() { 3199 var $el = $(this); 3200 if ($el.closest("button").length) return; 3201 if ($quoteEl && $el.is($quoteEl)) return; 3202 if ($quoteEl && $el.find($quoteEl).length) return; 3203 var cls = $el.attr("class") || ""; 3204 var txt = $el.text().trim(); 3205 if (/font-semibold|font-bold|font-extrabold|font-medium/.test(cls) && !$authorName) { 3206 if (txt.length > 1 && txt.length < 60 && txt.length < bestQLen) $authorName = $el; 3207 } 3208 }); 3209 } 3210 if (!$authorMeta) { 3211 $cardArea.find("p, span").each(function() { 3212 var $el = $(this); 3213 if ($el.closest("button").length) return; 3214 if ($quoteEl && $el.is($quoteEl)) return; 3215 if ($authorName && $el.is($authorName)) return; 3216 var cls = $el.attr("class") || ""; 3217 var txt = $el.text().trim(); 3218 if (/text-sm|text-xs|opacity|text-cream|text-gray|text-slate|text-muted/.test(cls) && !$authorMeta) { 3219 if (txt.length > 0 && txt.length < 60) $authorMeta = $el; 3220 } 3221 }); 3222 } 3223 3224 var $avatarImg = ($authorSearchArea || $cardArea).find("img[class*='rounded-full'], img[class*='rounded']").first(); 3225 var $avatar = null; 3226 if (!$avatarImg.length) { 3227 $avatar = ($authorSearchArea || $cardArea).find('[class*="rounded-full"]').filter(function() { 3228 return $(this).text().trim().length <= 2 && $(this).children("img").length === 0; 3229 }).first(); 3230 } 3231 3232 var $dots = $section.find("button").filter(function() { 3233 var cls = $(this).attr("class") || ""; 3234 return /rounded-full/.test(cls) && /w-2|h-2/.test(cls); 3235 }); 3236 3237 $section.data("carousel-processed", true); 3238 3239 function updateTestimonial(index) { 3240 var item = testimonials[index]; 3241 if (!item) return; 3242 3243 // Animate out then in 3244 var $fadeTargets = $quoteEl; 3245 if ($authorName) $fadeTargets = $fadeTargets.add($authorName); 3246 if ($authorMeta) $fadeTargets = $fadeTargets.add($authorMeta); 3247 3248 $fadeTargets.animate({ opacity: 0 }, 150, function() { 3249 // Update quote 3250 var quoteText = item.quote; 3251 if (!/^[""â]/.test(quoteText)) quoteText = "â" + quoteText; 3252 if (!/[""â]$/.test(quoteText)) quoteText = quoteText + "â"; 3253 $quoteEl.text(quoteText); 3254 3255 // Update author 3256 if ($authorName) $authorName.text(item.author); 3257 3258 // Update meta (role + location) 3259 // EC-CAROUSEL-017b: Only update meta text if the new value is non-empty. 3260 // Some sites have a static label (e.g. "Parent") that applies to all 3261 // testimonials â clearing it when role is empty looks broken. 3262 if ($authorMeta) { 3263 var meta = ""; 3264 if (item.role) meta += item.role; 3265 if (item.role && item.location) meta += " ⢠"; 3266 if (item.location) meta += item.location; 3267 if (meta) $authorMeta.text(meta); 3268 } 3269 3270 // Update avatar (img src or text initial) 3271 if ($avatarImg && $avatarImg.length && item.avatar) { 3272 $avatarImg.attr("src", item.avatar); 3273 } else if ($avatar && $avatar.length && item.author) { 3274 $avatar.text(item.author.charAt(0).toUpperCase()); 3275 } 3276 3277 $fadeTargets.animate({ opacity: 1 }, 200); 3278 }); 3279 3280 // Update dots 3281 if ($dots.length >= testimonials.length) { 3282 $dots.each(function(i) { 3283 var $dot = $(this); 3284 if (i === index) { 3285 $dot.addClass("w-8").css("background-color", ""); 3286 $dot.removeClass("bg-cream/30"); 3287 } else { 3288 $dot.removeClass("w-8"); 3289 } 3290 }); 3291 } 3292 } 3293
3294 // Wire up prev/next 3295 if ($prevBtn && $prevBtn.length) { 3296 $prevBtn.off("click.wpconvertTestimonial").on("click.wpconvertTestimonial", function(e) { 3297 e.preventDefault(); 3298 e.stopPropagation(); 3299 activeIndex = (activeIndex - 1 + testimonials.length) % testimonials.length; 3300 updateTestimonial(activeIndex); 3301 }); 3302 } 3303 3304 if ($nextBtn && $nextBtn.length) { 3305 $nextBtn.off("click.wpconvertTestimonial").on("click.wpconvertTestimonial", function(e) { 3306 e.preventDefault(); 3307 e.stopPropagation(); 3308 activeIndex = (activeIndex + 1) % testimonials.length; 3309 updateTestimonial(activeIndex); 3310 }); 3311 } 3312 3313 // Wire up dot clicks 3314 if ($dots.length >= testimonials.length) { 3315 $dots.each(function(i) { 3316 $(this).off("click.wpconvertTestimonial").on("click.wpconvertTestimonial", function(e) { 3317 e.preventDefault(); 3318 e.stopPropagation(); 3319 if (i < testimonials.length) { 3320 activeIndex = i; 3321 updateTestimonial(activeIndex); 3322 } 3323 }); 3324 }); 3325 } 3326 3327 // Optional auto-advance every 8 seconds 3328 var autoTimer = setInterval(function() { 3329 activeIndex = (activeIndex + 1) % testimonials.length; 3330 updateTestimonial(activeIndex); 3331 }, 8000); 3332 3333 // Pause auto-advance on hover 3334 $section.on("mouseenter.wpconvertTestimonial", function() { clearInterval(autoTimer); }); 3335 $section.on("mouseleave.wpconvertTestimonial", function() { 3336 autoTimer = setInterval(function() { 3337 activeIndex = (activeIndex + 1) % testimonials.length; 3338 updateTestimonial(activeIndex); 3339 }, 8000); 3340 }); 3341 3342 // Touch swipe support 3343 var touchStartX = 0; 3344 $cardArea.on("touchstart.wpconvertTestimonial", function(e) { 3345 touchStartX = (e.originalEvent.touches[0] || e.originalEvent.changedTouches[0]).clientX; 3346 }); 3347 $cardArea.on("touchend.wpconvertTestimonial", function(e) { 3348 var touch = e.originalEvent.changedTouches[0]; 3349 if (!touch) return; 3350 var diff = touchStartX - touch.clientX; 3351 if (Math.abs(diff) > 50) { 3352 if (diff > 0) { activeIndex = (activeIndex + 1) % testimonials.length; } 3353 else { activeIndex = (activeIndex - 1 + testimonials.length) % testimonials.length; } 3354 updateTestimonial(activeIndex); 3355 } 3356 }); 3357 3358 console.log("[WPConvert] â Testimonial carousel ready: " + testimonials.length + " items, prev=" + !!($prevBtn && $prevBtn.length) + ", next=" + !!($nextBtn && $nextBtn.length) + ", dots=" + $dots.length); 3359 }); 3360 } catch(e) { console.warn("[WPConvert] Testimonial carousel error:", e); } 3361 3362 3363 // No parallax libraries detected - skipping parallax initialization 3364 3365 3366 3367 // No animation libraries detected - skipping animation initialization 3368 3369 3370 try { // [WPConvert: Tabs Component] 3371 // WPConvert Tabs Component 3372 // Handles tabbed content sections converted from React components 3373 document.querySelectorAll('.wpconvert-tabs').forEach(function(container) { 3374 var tabCount = parseInt(container.dataset.tabCount) || 0; 3375 var currentTab = 0; 3376 3377 var buttons = container.querySelectorAll('.wpconvert-tab-btn'); 3378 var panels = container.querySelectorAll('.wpconvert-tab-panel'); 3379 var dots = container.querySelectorAll('.wpconvert-tab-dot'); 3380 var prevBtn = container.querySelector('.wpconvert-tab-prev'); 3381 var nextBtn = container.querySelector('.wpconvert-tab-next'); 3382 3383 function showTab(index) { 3384 if (index < 0) index = tabCount - 1; 3385 if (index >= tabCount) index = 0; 3386 currentTab = index; 3387 3388 buttons.forEach(function(btn, i) { 3389 btn.classList.toggle('active', i === index); 3390 btn.setAttribute('aria-selected', i === index ? 'true' : 'false'); 3391 }); 3392
3393 panels.forEach(function(panel, i) { 3394 panel.classList.toggle('active', i === index); 3395 panel.hidden = i !== index; 3396 }); 3397 3398 dots.forEach(function(dot, i) { 3399 dot.classList.toggle('active', i === index); 3400 }); 3401 } 3402 3403 buttons.forEach(function(btn, i) { 3404 btn.addEventListener('click', function() { showTab(i); }); 3405 }); 3406 3407 dots.forEach(function(dot, i) { 3408 dot.addEventListener('click', function() { showTab(i); }); 3409 }); 3410 3411 if (prevBtn) { 3412 prevBtn.addEventListener('click', function() { showTab(currentTab - 1); }); 3413 } 3414 3415 if (nextBtn) { 3416 nextBtn.addEventListener('click', function() { showTab(currentTab + 1); }); 3417 } 3418 3419 // Auto-rotate every 5 seconds 3420 var autoPlay = setInterval(function() { 3421 showTab(currentTab + 1); 3422 }, 5000); 3423 3424 // Pause on hover 3425 container.addEventListener('mouseenter', function() { 3426 clearInterval(autoPlay); 3427 }); 3428 3429 container.addEventListener('mouseleave', function() { 3430 autoPlay = setInterval(function() { 3431 showTab(currentTab + 1); 3432 }, 5000); 3433 }); 3434 }); 3435 } catch(e) { console.warn("[WPConvert] Tabs component error:", e); } 3436 3437 try { // [WPConvert: Multi-Step Form] 3438 3439/* EC-MSTEP-002f: Multi-step form â preserve original design. 3440 Wires up the original Continue/Back buttons and adds selection toggle. */ 3441(function() { 3442 function initMultiStepForms() { 3443 document.querySelectorAll('.wpconvert-multistep-form').forEach(function(form) { 3444 var steps = Array.from(form.querySelectorAll('[data-wpc-form-step]')); 3445 if (steps.length < 2) return; 3446 var current = 0; 3447 3448 function showStep(n) { 3449 if (n < 0 || n >= steps.length) return; 3450 current = n; 3451 steps.forEach(function(s, i) { s.style.display = i === current ? '' : 'none'; }); 3452 wireButtons(); 3453 } 3454 3455 function wireButtons() { 3456 var step = steps[current]; 3457 var allBtns = Array.from(step.querySelectorAll('button')); 3458 allBtns.forEach(function(btn) { 3459 var t = (btn.textContent || '').trim().toLowerCase(); 3460 /* EC-MSTEP-002g: Scope grid check to step element only. 3461 .closest() traverses the entire DOM â if the form sits inside a 3462 CSS-grid page layout (e.g. "grid lg:grid-cols-3"), Continue/Back 3463 buttons incorrectly appear "in a grid" and never get wired. */ 3464 var gridAnc = btn.closest('[class*="grid"]'); 3465 var inGrid = gridAnc && step.contains(gridAnc); 3466 /* Wire Continue/Next/Submit buttons (not in selection grids) */ 3467 if (!inGrid && (/^continue|^next|^send.*request|^submit|^get started/i.test(t)) && t.length < 50) { 3468 btn.onclick = function(e) { 3469 e.preventDefault(); 3470 if (current < steps.length - 1) showStep(current + 1); 3471 }; 3472 btn.removeAttribute('disabled'); 3473 btn.style.opacity = '1'; 3474 btn.style.pointerEvents = 'auto'; 3475 btn.classList.remove('disabled:opacity-40', 'disabled:pointer-events-none'); 3476 } 3477 /* Wire Back/Previous buttons */ 3478 if (!inGrid && /^back$|^previous$|^prev$/i.test(t)) { 3479 btn.onclick = function(e) { 3480 e.preventDefault(); 3481 if (current > 0) showStep(current - 1); 3482 }; 3483 } 3484 }); 3485 wireSelections(step); 3486 } 3487 3488 /* Selection toggle for card-style buttons (services, budget, timeline) */ 3489 function wireSelections(step) { 3490 var grids = step.querySelectorAll('[class*="grid"]'); 3491 grids.forEach(function(grid) { 3492 var btns = Array.from(grid.querySelectorAll('button')).filter(function(b) { 3493 if (b.closest('[class*="grid"]') !== grid) return false;
3494 var t = (b.textContent || '').trim().toLowerCase(); 3495 return !/^(continue|next|previous|back|prev|submit|get started|send)/i.test(t) && t.length > 1 && t.length < 60; 3496 }); 3497 if (btns.length < 2) return; 3498 var isMulti = btns.length > 4; 3499 /* Detect accent color from Continue button's inline style */ 3500 var accentColor = '', accentBg = ''; 3501 var continueBtn = Array.from(step.querySelectorAll('button')).find(function(b) { 3502 var t = (b.textContent || '').trim().toLowerCase(); 3503 var gAnc = b.closest('[class*="grid"]'); 3504 var bInGrid = gAnc && step.contains(gAnc); 3505 return /continue|next|send|submit/i.test(t) && !bInGrid && b.style.backgroundColor; 3506 }); 3507 if (continueBtn) { 3508 accentColor = continueBtn.style.backgroundColor; 3509 accentBg = accentColor.replace('rgb(', 'rgba(').replace(')', ', 0.1)'); 3510 } 3511 if (!accentColor) { accentColor = '#3b82f6'; accentBg = 'rgba(59,130,246,0.1)'; } 3512 /* Capture default styles from first button */ 3513 var defBorder = btns[0] && btns[0].style.borderColor || ''; 3514 var defBg = btns[0] && btns[0].style.backgroundColor || ''; 3515 var defColor = btns[0] && btns[0].style.color || ''; 3516 btns.forEach(function(btn) { 3517 if (btn._wpcBound) return; 3518 btn._wpcBound = true; 3519 btn.style.cursor = 'pointer'; 3520 btn.addEventListener('click', function(e) { 3521 e.preventDefault(); 3522 if (isMulti) { 3523 btn.toggleAttribute('data-selected'); 3524 } else { 3525 btns.forEach(function(b) { b.removeAttribute('data-selected'); }); 3526 btn.setAttribute('data-selected', ''); 3527 } 3528 btns.forEach(function(b) { 3529 var sel = b.hasAttribute('data-selected'); 3530 b.style.borderColor = sel ? accentColor : defBorder; 3531 b.style.backgroundColor = sel ? accentBg : defBg; 3532 if (sel) b.style.color = 'white'; 3533 else if (defColor) b.style.color = defColor; 3534 }); 3535 }); 3536 }); 3537 }); 3538 } 3539 3540 showStep(0); 3541 }); 3542 } 3543 /* EC-MSTEP-002h: Run immediately if DOM is already loaded (e.g. inside jQuery.ready), 3544 otherwise wait for DOMContentLoaded. jQuery.ready fires after DOMContentLoaded, 3545 so adding a DOMContentLoaded listener inside it would silently never execute. */ 3546 if (document.readyState === 'loading') { 3547 document.addEventListener('DOMContentLoaded', initMultiStepForms); 3548 } else { 3549 initMultiStepForms(); 3550 } 3551})(); 3552 3553 } catch(e) { console.warn("[WPConvert] Multi-step form error:", e); } 3554 3555 try { // [WPConvert: Signature Canvas] 3556 3557/* EC-CANVAS-002: Activate canvas drawing pads only with a strong positive signal. 3558 See generate.js > generateSignatureCanvasInitJS for the full rationale. */ 3559(function(){ 3560 var DECORATIVE_TOKEN = /(^|[-_\s])(hero|particle|particles|background|bg|webgl|three|chart|graph|noise|ripple|aurora|stars|matrix|fluid|wave|globe|sphere|mesh|shader|fbo|gl|canvas-bg|main-canvas)([-_\s]|$)/; 3561 function hasStrongPositiveSignal(canvas) { 3562 var id = (canvas.id || '').toLowerCase(); 3563 var rawCls = canvas.className; 3564 var cls = (rawCls && typeof rawCls === 'string' ? rawCls : (rawCls && rawCls.baseVal) || '').toLowerCase(); 3565 /* 1. explicit attribute */ 3566 if (canvas.hasAttribute('data-signature') || canvas.hasAttribute('data-sig-pad')) return true; 3567 /* 2. id/class spells "signature", "sigpad", "sig-pad" */ 3568 if (/sig(?:nature|pad|-pad)/.test(id) || /sig(?:nature|pad|-pad)/.test(cls)) return true; 3569 /* 3. <label for="canvasId"> with "signature" text */ 3570 if (canvas.id) { 3571 try { 3572 var safeId = canvas.id.replace(/(["\\])/g, '\\$1'); 3573 var lbl = canvas.ownerDocument.querySelector('label[for="' + safeId + '"]'); 3574 if (lbl && /signature/i.test(lbl.textContent || '')) return true; 3575 } catch (e) { /* invalid selector â fall through */ } 3576 } 3577 /* 4. canvas inside form/multistep panel AND a label/legend/heading in that 3578 scope mentions "signature". We deliberately scope to the form so we 3579 do not match unrelated text elsewhere on the page (e.g. a footer 3580 newsletter "Sign up" or a pricing line "60% on signature"). */ 3581 var formScope = canvas.closest('form, .wpconvert-step-panel, .wpconvert-multistep-form'); 3582 if (formScope) { 3583 var labels = formScope.querySelectorAll('label, legend, h1, h2, h3, h4, h5, h6'); 3584 for (var i = 0; i < labels.length; i++) { 3585 if (/signature/i.test(labels[i].textContent || '')) return true; 3586 } 3587 } 3588 return false;
3589 } 3590 3591 document.querySelectorAll('canvas').forEach(function(canvas) { 3592 if (canvas.dataset.wpcSigInit) return; 3593 /* Strong negative: well-known decorative canvas ids/classes. 3594 Checked BEFORE positive signals â even inside a <form> a canvas 3595 named "hero-canvas" or "particle-canvas" must never become a 3596 signature pad. */ 3597 var id = (canvas.id || '').toLowerCase(); 3598 var rawCls = canvas.className; 3599 var cls = (rawCls && typeof rawCls === 'string' ? rawCls : (rawCls && rawCls.baseVal) || '').toLowerCase(); 3600 if (DECORATIVE_TOKEN.test(id) || DECORATIVE_TOKEN.test(cls)) return; 3601 3602 if (!hasStrongPositiveSignal(canvas)) return; 3603 3604 canvas.dataset.wpcSigInit = 'true'; 3605 canvas.width = canvas.offsetWidth || 600; 3606 canvas.height = canvas.offsetHeight || 150; 3607 canvas.style.touchAction = 'none'; 3608 canvas.style.cursor = 'crosshair'; 3609 var ctx = canvas.getContext && canvas.getContext('2d'); 3610 if (!ctx) return; 3611 ctx.strokeStyle = '#1a1a1a'; 3612 ctx.lineWidth = 2; 3613 ctx.lineCap = 'round'; 3614 ctx.lineJoin = 'round'; 3615 var drawing = false; 3616 function getPos(e) { 3617 var rect = canvas.getBoundingClientRect(); 3618 var x = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left; 3619 var y = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top; 3620 return { x: x, y: y }; 3621 } 3622 canvas.addEventListener('mousedown', function(e) { drawing = true; ctx.beginPath(); var p = getPos(e); ctx.moveTo(p.x, p.y); }); 3623 canvas.addEventListener('mousemove', function(e) { if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); }); 3624 canvas.addEventListener('mouseup', function() { drawing = false; }); 3625 canvas.addEventListener('mouseleave', function() { drawing = false; }); 3626 canvas.addEventListener('touchstart', function(e) { e.preventDefault(); drawing = true; ctx.beginPath(); var p = getPos(e); ctx.moveTo(p.x, p.y); }, { passive: false }); 3627 canvas.addEventListener('touchmove', function(e) { e.preventDefault(); if (!drawing) return; var p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); }, { passive: false }); 3628 canvas.addEventListener('touchend', function() { drawing = false; }); 3629 /* Wire a "Clear" button next to the canvas if present. */ 3630 var container = canvas.closest('div'); 3631 if (container) { 3632 var clearBtn = container.querySelector('button'); 3633 if (!clearBtn) clearBtn = container.parentElement ? container.parentElement.querySelector('button') : null; 3634 if (clearBtn && /clear/i.test(clearBtn.textContent || '')) { 3635 clearBtn.addEventListener('click', function(e) { 3636 e.preventDefault(); 3637 ctx.clearRect(0, 0, canvas.width, canvas.height); 3638 }); 3639 } 3640 } 3641 /* Persist signature data into a hidden input so the form submission 3642 includes the drawn image. */ 3643 var hiddenInput = canvas.ownerDocument.createElement('input'); 3644 hiddenInput.type = 'hidden'; 3645 hiddenInput.name = 'signature_data'; 3646 if (canvas.parentElement) canvas.parentElement.appendChild(hiddenInput); 3647 canvas.addEventListener('mouseup', function() { hiddenInput.value = canvas.toDataURL ? canvas.toDataURL() : ''; }); 3648 canvas.addEventListener('touchend', function() { hiddenInput.value = canvas.toDataURL ? canvas.toDataURL() : ''; }); 3649 }); 3650})(); 3651 3652 } catch(e) { console.warn("[WPConvert] Signature canvas error:", e); } 3653 3654 try { // [WPConvert: Calculator] 3655 // EC-CALC-001: Interactive calculator 3656 document.querySelectorAll('[data-wpc-calc]').forEach(function(calcEl) { 3657 var configStr = calcEl.getAttribute('data-wpc-calc-formula'); 3658 if (!configStr) return; 3659 var config; 3660 try { config = JSON.parse(configStr); } catch(e) { return; } 3661 var state = {}; 3662 var sliders = calcEl.querySelectorAll('input[type="range"][data-wpc-calc-var]'); 3663 sliders.forEach(function(s) { 3664 var sVar = s.getAttribute('data-wpc-calc-var'); 3665 var inputKey = null; 3666 for (var k in config.inputs) { 3667 if (config.inputs[k].sliderVar === sVar) { inputKey = k; break; } 3668 } 3669 if (inputKey) { 3670 state[inputKey] = parseFloat(s.value) || 0; 3671 s.addEventListener('input', function() { 3672 state[inputKey] = parseFloat(s.value) || 0; 3673 recalc(); 3674 }); 3675 } 3676 }); 3677 var toggleBtns = calcEl.querySelectorAll('[data-wpc-calc-toggle]');
3678 toggleBtns.forEach(function(btn) { 3679 var tVar = btn.getAttribute('data-wpc-calc-toggle'); 3680 var idx = parseInt(btn.getAttribute('data-wpc-calc-toggle-idx'), 10) || 0; 3681 var inputKey = null; 3682 for (var k in config.inputs) { 3683 if (config.inputs[k].toggleVar === tVar) { inputKey = k; break; } 3684 } 3685 if (!inputKey) return; 3686 if (!state[inputKey]) { 3687 state[inputKey] = config.inputs[inputKey]['default'] || config.inputs[inputKey].options[0]; 3688 } 3689 btn.addEventListener('click', function() { 3690 var opts = config.inputs[inputKey].options; 3691 state[inputKey] = opts[idx] || opts[0]; 3692 calcEl.querySelectorAll('[data-wpc-calc-toggle="' + tVar + '"]').forEach(function(b, bi) { 3693 var parts = (b.getAttribute('class') || '').split(' '); 3694 if (bi === idx) { 3695 parts = parts.map(function(c) { 3696 if (c === 'bg-card') return 'bg-accent/10'; 3697 if (c === 'border-border/50') return 'border-accent'; 3698 if (c === 'text-muted-foreground') return 'text-accent'; 3699 return c; 3700 }); 3701 b.classList.add('wpc-calc-toggle-active'); 3702 } else { 3703 parts = parts.map(function(c) { 3704 if (c === 'bg-accent/10') return 'bg-card'; 3705 if (c === 'border-accent') return 'border-border/50'; 3706 if (c === 'text-accent' && c.indexOf('text-accent-') !== 0) return 'text-muted-foreground'; 3707 return c; 3708 }); 3709 b.classList.remove('wpc-calc-toggle-active'); 3710 } 3711 b.setAttribute('class', parts.join(' ')); 3712 }); 3713 recalc(); 3714 }); 3715 }); 3716 function formatValue(val, fmt) { 3717 if (typeof val === 'string') return val; 3718 var n = parseFloat(val); 3719 if (isNaN(n)) return String(val); 3720 switch (fmt) { 3721 case 'currency': return '$' + n.toLocaleString('en-US', {minimumFractionDigits: 0, maximumFractionDigits: 0}); 3722 case 'decimal2': return n.toFixed(2); 3723 case 'percent': return n + '%'; 3724 case 'months': return n + ' months'; 3725 default: return n.toLocaleString('en-US'); 3726 } 3727 } 3728 function recalc() { 3729 var computed = {}; 3730 for (var k in state) computed[k] = state[k]; 3731 if (config.formulas) { 3732 for (var i = 0; i < config.formulas.length; i++) { 3733 var f = config.formulas[i]; 3734 try { 3735 var fn = new Function(f.deps.join(','), 'return (' + f.expr + ')'); 3736 var args = f.deps.map(function(d) { return computed[d]; }); 3737 computed[f['var']] = fn.apply(null, args); 3738 } catch(e) { /* skip */ } 3739 } 3740 } 3741 sliders.forEach(function(s) { 3742 var sVar = s.getAttribute('data-wpc-calc-var'); 3743 var display = calcEl.querySelector('[data-wpc-calc-display="' + sVar + '"]'); 3744 if (!display) return; 3745 var inputKey = null; 3746 for (var k in config.inputs) { 3747 if (config.inputs[k].sliderVar === sVar) { inputKey = k; break; } 3748 } 3749 if (inputKey && config.inputs[inputKey]) { 3750 display.textContent = formatValue(parseFloat(s.value), config.inputs[inputKey].displayFormat || config.inputs[inputKey].format); 3751 } 3752 }); 3753 var outputEls = calcEl.querySelectorAll('[data-wpc-calc-output]');
3754 outputEls.forEach(function(el) { 3755 var oVar = el.getAttribute('data-wpc-calc-output'); 3756 var outCfg = config.outputs[oVar]; 3757 if (!outCfg) return; 3758 var val = computed[outCfg.formulaVar]; 3759 if (val !== undefined && val !== null) { 3760 el.textContent = formatValue(val, outCfg.format); 3761 } 3762 }); 3763 } 3764 recalc(); 3765 }); 3766 } catch(e) { console.warn("[WPConvert] Calculator error:", e); } 3767 }); 3768 (function() { 3769 var selectors = '.scroll-fade-up, .scroll-fade-left, .scroll-fade-right, .scroll-scale-in'; 3770 function initScrollReveal() { 3771 var els = document.querySelectorAll(selectors); 3772 if (!els.length || typeof IntersectionObserver === 'undefined') return; 3773 var io = new IntersectionObserver(function(entries) { 3774 entries.forEach(function(entry) { 3775 if (entry.isIntersecting) { 3776 entry.target.classList.add('visible'); 3777 io.unobserve(entry.target); 3778 } 3779 }); 3780 }, { threshold: 0.15 }); 3781 els.forEach(function(el) { 3782 if (el.closest('[aria-roledescription="carousel"]')) { 3783 el.classList.add('visible'); 3784 } else { 3785 io.observe(el); 3786 } 3787 }); 3788 } 3789 if (document.readyState === 'loading') { 3790 document.addEventListener('DOMContentLoaded', initScrollReveal); 3791 } else { 3792 initScrollReveal(); 3793 } 3794 })(); 3795//# sourceURL=jquery-js-after</script>
3795 <link rel="https://api.w.org/" href="https://www.choosetosucceed.org/wp-json/" /><link rel="alternate" title="JSON" type="application/json" href="https://www.choosetosucceed.org/wp-json/wp/v2/pages/8585" /><link rel="EditURI" type="application/rsd+xml" title="RSD" href="https://www.choosetosucceed.org/xmlrpc.php?rsd" /><meta name="generator" content="WordPress 7.0.6" /><link rel='shortlink' href='https://www.choosetosucceed.org/?p=8585' />
3795<script src="https://cdnjs.cloudflare.com/ajax/libs/slick-carousel/1.9.0/slick.min.js" integrity="sha512-HGOnQO9+SP1V92SrtZfjqxxtLmVzqZpjFFekvzZVWoiASSQgSr4cw9Kqd2+l8Llp4Gm0G8GIFJ4ddwZilcdb8A==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
3795 <link rel="icon" href="https://www.choosetosucceed.org/wp-content/uploads/2026/06/cropped-CTS-square-1-32x32.jpg" sizes="32x32" /><link rel="icon" href="https://www.choosetosucceed.org/wp-content/uploads/2026/06/cropped-CTS-square-1-192x192.jpg" sizes="192x192" /><link rel="apple-touch-icon" href="https://www.choosetosucceed.org/wp-content/uploads/2026/06/cropped-CTS-square-1-180x180.jpg" /><meta name="msapplication-TileImage" content="https://www.choosetosucceed.org/wp-content/uploads/2026/06/cropped-CTS-square-1-270x270.jpg" /> <style id="wpconvert-scroll-carousel-critical">.scroll-fade-up:has([aria-roledescription="carousel"]), 3796 .scroll-fade-up:has([data-wpconvert-carousel]), 3797 .scroll-fade-up:has([data-wpc-carousel="true"]), 3798 /* EC-BLOG-146: Blog category strip uses horizontal scroll + chevrons, not a carousel â IO scroll-reveal 3799 * still hid the bar at opacity:0 (looks like a âcarousel flashâ). */ 3800 .scroll-fade-up:has([data-wpconvert-blog-filter-bar]) { 3801 opacity: 1 !important; 3802 transform: none !important; 3803 visibility: visible !important; 3804 } 3805 [role="region"][aria-roledescription="carousel"], 3806 [data-wpconvert-carousel="aria"], 3807 [data-wpc-carousel="true"] { 3808 opacity: 1 !important; 3809 visibility: visible !important; 3810 }</style><style id="wpconvert-mobile-sticky-cta-critical">#wpconvert-mobile-sticky-cta { 3811 display: none !important; 3812 visibility: hidden !important; 3813 pointer-events: none !important; 3814 } 3815 @media (max-width: 1023px) { 3816 #wpconvert-mobile-sticky-cta { 3817 display: block !important; 3818 visibility: visible !important; 3819 pointer-events: auto !important; 3820 position: fixed !important; 3821 left: 0 !important; 3822 right: 0 !important; 3823 bottom: 0 !important; 3824 z-index: 50 !important; 3825 } 3826 }</style><style id="wpconvert-header-metrics-critical">:root { 3827 --wpconvert-header-bar-height: 4rem; 3828 }</style><style id="wpconvert-header-nowrap-critical">header .container.flex.items-center.justify-between, 3829 header .container.flex { 3830 flex-wrap: nowrap !important; 3831 flex-direction: row !important; 3832 } 3833 header .container.flex.h-16, 3834 header .container.flex.min-h-16 { 3835 min-height: var(--wpconvert-header-bar-height) !important; 3836 }</style><style id="wpconvert-header-nav-cluster-contents-critical">[data-wpconvert-merge-nav-cluster] { 3837 display: none !important; 3838 } 3839 @media (min-width: 640px) { 3840 [data-wpconvert-merge-nav-cluster="sm"] { 3841 display: contents !important; 3842 } 3843 } 3844 @media (min-width: 768px) { 3845 [data-wpconvert-merge-nav-cluster="md"] { 3846 display: contents !important; 3847 } 3848 } 3849 @media (min-width: 1024px) { 3850 [data-wpconvert-merge-nav-cluster="lg"] { 3851 display: contents !important; 3852 } 3853 } 3854 @media (min-width: 1280px) { 3855 [data-wpconvert-merge-nav-cluster="xl"] { 3856 display: contents !important; 3857 } 3858 }</style><style id="wpconvert-flat-nav-nowrap-critical">/* EC-NAV-179b: target BOTH the menu_id span (when items_wrap preserves it) 3859 AND the stable .wpconvert-flat-nav-items marker class, so the rule keeps 3860 matching even if a future consumer overrides menu_id. Using > a picks up 3861 the walker-emitted bare anchors regardless of whether the span is 3862 display:contents or a real box. */ 3863 #wpconvert-flat-nav > a, 3864 .wpconvert-flat-nav-items > a, 3865 [data-wpconvert-merge-nav-cluster] #wpconvert-flat-nav > a, 3866 [data-wpconvert-merge-nav-cluster] .wpconvert-flat-nav-items > a { 3867 white-space: nowrap; 3868 flex: 0 0 auto; 3869 width: auto; 3870 }</style>
3870<script id="wpconvert-parallax-scroll">// EC-ANIM-012: Scroll-driven parallax translateX handler. 3871 // Replaces Framer Motion useScroll+useTransform for horizontal parallax text sections. 3872 (function() { 3873 function initParallax() { 3874 var els = document.querySelectorAll('[data-wpconvert-parallax-x]'); 3875 if (!els.length) return; 3876 var entries = []; 3877 els.forEach(function(el) { 3878 var parts = el.getAttribute('data-wpconvert-parallax-x').split(','); 3879 entries.push({ el: el, start: parseFloat(parts[0]), end: parseFloat(parts[1]) }); 3880 }); 3881 var ticking = false; 3882 function update() { 3883 var vh = window.innerHeight; 3884 entries.forEach(function(e) { 3885 var parent = e.el.closest('section') || e.el.parentElement; 3886 var rect = parent.getBoundingClientRect(); 3887 var progress = Math.max(0, Math.min(1, (vh - rect.top) / (vh + rect.height))); 3888 var x = e.start + (e.end - e.start) * progress; 3889 e.el.style.transform = 'translateX(' + x + '%)'; 3890 }); 3891 ticking = false; 3892 } 3893 window.addEventListener('scroll', function() { 3894 if (!ticking) { ticking = true; requestAnimationFrame(update); } 3895 }, { passive: true }); 3896 update(); 3897 } 3898 if (document.readyState === 'loading') { 3899 document.addEventListener('DOMContentLoaded', initParallax); 3900 } else { 3901 initParallax(); 3902 } 3903 })(); 3904 // EC-ANIM-011c: Overlap fix handled at build time by demoting z-10 on centered pulse cards.</script>
3904 <style id="wpconvert-popup-hide-critical">[data-wpconvert-popup-disabled="true"] { 3905 display: none !important; 3906 visibility: hidden !important; 3907 opacity: 0 !important; 3908 pointer-events: none !important; 3909 position: absolute !important; 3910 left: -99999px !important; 3911 top: -99999px !important; 3912 width: 0 !important; 3913 height: 0 !important; 3914 max-width: 0 !important; 3915 max-height: 0 !important; 3916 overflow: hidden !important; 3917 clip: rect(0,0,0,0) !important; 3918 z-index: -99999 !important; 3919 transform: scale(0) !important; 3920 margin: 0 !important; 3921 padding: 0 !important; 3922 } 3923 [data-wpconvert-popup-disabled="true"] * { 3924 display: none !important; 3925 visibility: hidden !important; 3926 opacity: 0 !important; 3927 } 3928 /* Block any JavaScript from showing popups */ 3929 [data-wpconvert-popup-disabled="true"][style*="display: block"], 3930 [data-wpconvert-popup-disabled="true"][style*="display:block"], 3931 [data-wpconvert-popup-disabled="true"][class*="open"], 3932 [data-wpconvert-popup-disabled="true"][class*="show"], 3933 [data-wpconvert-popup-disabled="true"][class*="active"] { 3934 display: none !important; 3935 } 3936 /* Mobile navigation - HIDDEN by default, only visible when .open class is added */ 3937 /* Uses CSS variables to match site theme (dark/light mode) */ 3938 #wpconvert-mobile-nav { 3939 display: none !important; /* Hidden by default - JavaScript will show it */ 3940 position: fixed !important; /* !important needed: framework CSS sets nav:not(.sticky):not(.fixed) to relative !important */ 3941 top: 0 !important; 3942 bottom: 0 !important; 3943 width: 280px; 3944 max-width: 85vw; 3945 z-index: 99999 !important; /* MUST be higher than backdrop (99998) */ 3946 /* Theme-aware colors using CSS variables with fallbacks */ 3947 background: hsl(var(--background, 0 0% 100%)); 3948 color: hsl(var(--foreground, 0 0% 10%)); 3949 overflow-y: auto; 3950 transition: transform 0.3s ease-in-out; 3951 /* Glassmorphism effect for modern sites */ 3952 backdrop-filter: blur(12px); 3953 -webkit-backdrop-filter: blur(12px); 3954 } 3955 /* Dark theme detection - if body/html has dark class or data attribute */ 3956 .dark #wpconvert-mobile-nav, 3957 [data-theme="dark"] #wpconvert-mobile-nav, 3958 html.dark #wpconvert-mobile-nav { 3959 background: hsl(var(--background, 222 47% 11%)); 3960 color: hsl(var(--foreground, 210 40% 98%)); 3961 } 3962 3963 /* Ensure menu content is clickable and ABOVE backdrop */ 3964 /* Note: Do NOT set position on #wpconvert-mobile-nav here - it must stay fixed */ 3965 #wpconvert-mobile-nav .nav-inner, 3966 #wpconvert-mobile-nav a, 3967 #wpconvert-mobile-nav button { 3968 pointer-events: auto !important; 3969 position: relative; 3970 z-index: 1; 3971 } 3972 /* Slide from RIGHT (default - for right-side hamburger buttons) */ 3973 #wpconvert-mobile-nav.slide-right { 3974 right: 0; 3975 left: auto; 3976 border-left: 1px solid hsl(var(--border, 0 0% 80%) / 0.3); 3977 box-shadow: -4px 0 15px rgba(0, 0, 0, 0.2); 3978 transform: translateX(100%); 3979 } 3980 /* Slide from LEFT (for left-side hamburger buttons) */ 3981 #wpconvert-mobile-nav.slide-left { 3982 left: 0; 3983 right: auto; 3984 border-right: 1px solid hsl(var(--border, 0 0% 80%) / 0.3); 3985 box-shadow: 4px 0 15px rgba(0, 0, 0, 0.2); 3986 transform: translateX(-100%); 3987 } 3988 /* Only show mobile nav when explicitly opened via JavaScript */ 3989 /* Higher specificity to override slide-right/slide-left transforms */ 3990 #wpconvert-mobile-nav.open, 3991 #wpconvert-mobile-nav.slide-right.open, 3992 #wpconvert-mobile-nav.slide-left.open { 3993 display: block !important; 3994 visibility: visible !important; 3995 opacity: 1 !important; 3996 transform: translateX(0) !important; 3997 } 3998 #wpconvert-mobile-nav .nav-inner { 3999 padding: 1rem; 4000 padding-top: 3.5rem; 4001 } 4002 #wpconvert-mobile-nav .close-btn { 4003 position: absolute; 4004 top: 1rem; 4005 right: 1rem; 4006 background: none; 4007 border: none; 4008 cursor: pointer; 4009 padding: 0.5rem; 4010 color: inherit; 4011 } 4012 #wpconvert-mobile-nav ul { 4013 list-style: none; 4014 padding: 0; 4015 margin: 0; 4016 display: flex; 4017 flex-direction: column; 4018 gap: 0; 4019 } 4020 #wpconvert-mobile-nav li { 4021 margin: 0; 4022 border-bottom: 1px solid hsl(var(--border, 0 0% 80%) / 0.15); 4023 } 4024 #wpconvert-mobile-nav li:last-child { 4025 border-bottom: none; 4026 } 4027 #wpconvert-mobile-nav a { 4028 display: block; 4029 padding: 0.875rem 0.5rem; 4030 text-decoration: none; 4031 color: hsl(var(--foreground, 0 0% 10%)); 4032 transition: background-color 0.2s; 4033 } 4034 #wpconvert-mobile-nav a:hover { 4035 background: hsl(var(--accent, 0 0% 95%) / 0.5); 4036 } 4037 .dark #wpconvert-mobile-nav a, 4038 [data-theme="dark"] #wpconvert-mobile-nav a { 4039 color: hsl(var(--foreground, 210 40% 98%)); 4040 } 4041 .dark #wpconvert-mobile-nav a:hover, 4042 [data-theme="dark"] #wpconvert-mobile-nav a:hover { 4043 background: hsl(var(--accent, 217 33% 17%) / 0.5); 4044 } 4045 /* Current menu item highlighting */ 4046 #wpconvert-mobile-nav .current-menu-item > a, 4047 #wpconvert-mobile-nav .current_page_item > a { 4048 color: hsl(var(--primary, 217 91% 60%)); 4049 background: hsl(var(--primary, 217 91% 60%) / 0.1); 4050 } 4051 body.mobile-nav-open { 4052 overflow: hidden; 4053 } 4054 /* Mobile nav backdrop - hidden by default, MUST be below the menu */ 4055 #wpconvert-mobile-nav-backdrop { 4056 display: none !important; 4057 position: fixed !important; /* !important needed: framework CSS sets nav/div to relative !important */ 4058 inset: 0 !important; 4059 background: rgba(0, 0, 0, 0.4); 4060 z-index: 99998 !important; /* Below menu (99999) but above everything else */ 4061 } 4062 body.mobile-nav-open #wpconvert-mobile-nav-backdrop { 4063 display: block !important; 4064 } 4065 /* Universal Search Modal - works for any site with search icon */ 4066 #wpconvert-search-modal { 4067 display: none; 4068 position: fixed !important; 4069 inset: 0; 4070 z-index: 100000; 4071 padding: 1rem; 4072 padding-top: 15vh; 4073 } 4074 #wpconvert-search-modal.open { 4075 display: flex; 4076 flex-direction: column; 4077 align-items: center; 4078 } 4079 #wpconvert-search-backdrop { 4080 position: absolute; 4081 inset: 0; 4082 background: rgba(0, 0, 0, 0.5); 4083 backdrop-filter: blur(4px); 4084 -webkit-backdrop-filter: blur(4px); 4085 } 4086 #wpconvert-search-container { 4087 position: relative; 4088 width: 100%; 4089 max-width: 32rem; 4090 background: hsl(var(--background, 0 0% 100%)); 4091 border-radius: 0.75rem; 4092 box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); 4093 overflow: hidden; 4094 animation: wpconvert-search-slide-down 0.2s ease-out; 4095 } 4096 @keyframes wpconvert-search-slide-down { 4097 from { opacity: 0; transform: translateY(-10px) scale(0.98); } 4098 to { opacity: 1; transform: translateY(0) scale(1); } 4099 } 4100 .dark #wpconvert-search-container { 4101 background: hsl(var(--background, 222 47% 11%)); 4102 } 4103 #wpconvert-search-header { 4104 display: flex; 4105 align-items: center; 4106 padding: 1rem; 4107 border-bottom: 1px solid hsl(var(--border, 0 0% 80%) / 0.3); 4108 } 4109 #wpconvert-search-icon { 4110 flex-shrink: 0; 4111 margin-right: 0.75rem; 4112 color: hsl(var(--muted-foreground, 0 0% 45%)); 4113 } 4114 #wpconvert-search-input { 4115 flex: 1; 4116 border: none; 4117 outline: none; 4118 background: transparent; 4119 font-size: 1rem; 4120 color: hsl(var(--foreground, 0 0% 10%)); 4121 } 4122 #wpconvert-search-input::placeholder { 4123 color: hsl(var(--muted-foreground, 0 0% 45%)); 4124 } 4125 .dark #wpconvert-search-input { 4126 color: hsl(var(--foreground, 210 40% 98%)); 4127 } 4128 #wpconvert-search-close { 4129 flex-shrink: 0; 4130 padding: 0.5rem; 4131 margin-left: 0.5rem; 4132 background: none; 4133 border: none; 4134 cursor: pointer; 4135 color: hsl(var(--muted-foreground, 0 0% 45%)); 4136 border-radius: 0.375rem; 4137 transition: background-color 0.2s; 4138 } 4139 #wpconvert-search-close:hover { 4140 background: hsl(var(--accent, 0 0% 95%) / 0.5); 4141 } 4142 #wpconvert-search-results { 4143 max-height: 50vh; 4144 overflow-y: auto; 4145 padding: 0.5rem; 4146 } 4147 #wpconvert-search-results:empty::after { 4148 content: "Type to search..."; 4149 display: block; 4150 padding: 1rem; 4151 text-align: center; 4152 color: hsl(var(--muted-foreground, 0 0% 45%)); 4153 font-size: 0.875rem; 4154 } 4155 .wpconvert-search-result { 4156 display: block; 4157 padding: 0.75rem 1rem; 4158 text-decoration: none; 4159 color: hsl(var(--foreground, 0 0% 10%)); 4160 border-radius: 0.375rem; 4161 transition: background-color 0.2s; 4162 } 4163 .wpconvert-search-result:hover { 4164 background: hsl(var(--accent, 0 0% 95%) / 0.5); 4165 } 4166 .wpconvert-search-result-title { 4167 font-weight: 500; 4168 margin-bottom: 0.25rem; 4169 } 4170 .wpconvert-search-result-url { 4171 font-size: 0.75rem; 4172 color: hsl(var(--muted-foreground, 0 0% 45%)); 4173 } 4174 .dark .wpconvert-search-result { 4175 color: hsl(var(--foreground, 210 40% 98%)); 4176 } 4177 #wpconvert-search-hint { 4178 padding: 0.75rem 1rem; 4179 font-size: 0.75rem; 4180 color: hsl(var(--muted-foreground, 0 0% 45%)); 4181 border-top: 1px solid hsl(var(--border, 0 0% 80%) / 0.3); 4182 }
4183 #wpconvert-search-hint kbd { 4184 display: inline-block; 4185 padding: 0.125rem 0.375rem; 4186 margin: 0 0.125rem; 4187 font-size: 0.7rem; 4188 font-family: inherit; 4189 background: hsl(var(--muted, 0 0% 95%)); 4190 border-radius: 0.25rem; 4191 border: 1px solid hsl(var(--border, 0 0% 80%) / 0.5); 4192 } 4193 /* Gradient text fix - ensures background-clip works correctly */ 4194 [style*="background"][style*="text-fill-color"] { 4195 -webkit-background-clip: text !important; 4196 background-clip: text !important; 4197 } 4198 /* CSS-only dropdown menus for converted React SPAs */ 4199 /* Works with Tailwind's group/group-hover pattern */ 4200 /* EC-NAV-072: Exclude .wpconvert-dropdown-panel â those have their own rules 4201 and transform: scaleY(1) would clobber -translate-x-1/2 centering on mega menus */ 4202 .group:hover > div[class*="scale-y-0"]:not(.wpconvert-dropdown-panel), 4203 .group:focus-within > div[class*="scale-y-0"]:not(.wpconvert-dropdown-panel), 4204 .relative.group:hover > div[class*="invisible"]:not(.wpconvert-dropdown-panel), 4205 .relative.group:focus-within > div[class*="invisible"]:not(.wpconvert-dropdown-panel), 4206 .relative.group:hover > div[class*="opacity-0"]:not(.wpconvert-dropdown-panel), 4207 .relative.group:focus-within > div[class*="opacity-0"]:not(.wpconvert-dropdown-panel) { 4208 opacity: 1 !important; 4209 transform: scaleY(1) !important; 4210 visibility: visible !important; 4211 } 4212 /* Dropdown triggers */ 4213 .group > button, 4214 .relative.group > button { 4215 cursor: pointer; 4216 } 4217 /* Keep dropdown visible while hovering on dropdown content */ 4218 .group > div:hover:not(.wpconvert-dropdown-panel), 4219 .relative.group > div:hover:not(.wpconvert-dropdown-panel) { 4220 opacity: 1 !important; 4221 transform: scaleY(1) !important; 4222 visibility: visible !important; 4223 } 4224 /* WPConvert injected dropdown panels. 4225 EC-NAV-DROPDOWN-002: the outer .wpconvert-dropdown-wrapper now 4226 owns position + opacity + transition + the hoverable pt-2 gap 4227 between trigger and panel. The inner .wpconvert-dropdown-panel 4228 only carries visible surface styling. Hovering the parent 4229 .group keeps the wrapper visible, and the wrapper's bottom 4230 padding stays inside the hover box â so traversing the gap 4231 from trigger to panel never drops :hover. */ 4232 .wpconvert-dropdown-wrapper { 4233 position: absolute; 4234 left: 0; 4235 top: 100%; 4236 padding-top: 0.5rem; 4237 opacity: 0; 4238 visibility: hidden; 4239 transition: opacity 0.2s, visibility 0.2s; 4240 z-index: 50; 4241 } 4242 .group:hover > .wpconvert-dropdown-wrapper, 4243 .group:focus-within > .wpconvert-dropdown-wrapper, 4244 .wpconvert-dropdown-wrapper:hover { 4245 opacity: 1 !important; 4246 visibility: visible !important; 4247 } 4248 .wpconvert-dropdown-panel { 4249 min-width: 12rem; 4250 border-radius: 0.375rem; 4251 background: hsl(var(--background, 0 0% 100%)); 4252 border: 1px solid hsl(var(--border, 0 0% 80%)); 4253 /* shadow-card equivalent: 1px ring + soft shadow defines edge 4254 against hero photos / colored backgrounds. */ 4255 box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06), 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 4256 } 4257 /* EC-NAV-072: Mega menu centering is now handled by Tailwind 4258 utilities on the wrapper itself (left-1/2 + -translate-x-1/2). 4259 The defensive rule here just documents the contract for the 4260 inner panel class â visible styling is unchanged. */ 4261 .wpconvert-mega-menu { 4262 /* outer .wpconvert-dropdown-wrapper provides centering */ 4263 } 4264 /* EC-NAV-141: Flat dropdown with many links (no category grandchildren) â 2-column flow */ 4265 .wpconvert-dropdown-panel.wpconvert-dropdown-panel--flat-mega { 4266 min-width: min(520px, 92vw); 4267 max-width: 92vw; 4268 } 4269 .wpconvert-dropdown-flat-mega-inner { 4270 column-count: 2; 4271 column-gap: 2.5rem; 4272 } 4273 .wpconvert-dropdown-flat-mega-inner > a { 4274 break-inside: avoid; 4275 -webkit-column-break-inside: avoid; 4276 page-break-inside: avoid; 4277 } 4278 .wpconvert-dropdown-panel a { 4279 display: block; 4280 padding: 0.5rem 1rem; 4281 font-size: 0.875rem; 4282 color: hsl(var(--foreground, 0 0% 10%)); 4283 text-decoration: none; 4284 transition: background-color 0.2s; 4285 } 4286 .wpconvert-dropdown-panel a:hover { 4287 background: hsl(var(--accent, 0 0% 95%) / 0.5); 4288 } 4289 .dark .wpconvert-dropdown-panel { 4290 background: hsl(var(--card, 222 47% 11%)); 4291 } 4292 .dark .wpconvert-dropdown-panel a { 4293 color: hsl(var(--foreground, 210 40% 98%)); 4294 } 4295 .dark .wpconvert-dropdown-panel a:hover { 4296 background: hsl(var(--accent, 217 33% 17%) / 0.5); 4297 } 4298 /* EC-NAV-DROPDOWN-001: Ensure Tailwind group-hover utilities are always available. 4299 Bundled CSS from JIT-compiled Tailwind may not include these if they weren't used 4300 in the original source â but our dropdown walker/fallback menus rely on them. 4301 NOTE: this whole style block is emitted inside a JavaScript template literal, so 4302 a single colon escape would be silently dropped during interpolation. We write 4303 a double backslash to preserve the literal backslash that CSS requires to
4304 escape the colon in compound class names like .group-hover:opacity-100. */ 4305 .group:hover .group-hover\:opacity-100 { opacity: 1; } 4306 .group:hover .group-hover\:visible { visibility: visible; } 4307 .group:hover .group-hover\:scale-y-100 { transform: scaleY(1); } 4308 /* WPC Mobile Menu Panel States (for preserved mobile menus) */ 4309 [data-wpc-menu-panel] { 4310 /* Original styling preserved via classes */ 4311 } 4312 [data-wpc-menu-panel].is-open { 4313 display: block !important; 4314 visibility: visible !important; 4315 opacity: 1 !important; 4316 transform: translateX(0) !important; 4317 } 4318 [data-wpc-menu-overlay] { 4319 display: none; 4320 position: fixed !important; 4321 inset: 0; 4322 background: rgba(0, 0, 0, 0.5); 4323 z-index: 99998; 4324 } 4325 [data-wpc-menu-overlay].is-open { 4326 display: block; 4327 } 4328 body.wpc-menu-open { 4329 overflow: hidden; 4330 } 4331 .wpc-mobile-menu { 4332 list-style: none; 4333 padding: 0; 4334 margin: 0; 4335 } 4336 .wpc-mobile-menu li { 4337 margin: 0; 4338 } 4339 .wpc-mobile-menu a { 4340 display: block; 4341 padding: 0.75rem 1rem; 4342 text-decoration: none; 4343 }</style></head><body class="wp-singular page-template page-template-page-consider-bexar-county page-template-page-consider-bexar-county-php page page-id-8585 wp-theme-cts-website-revised6-4-26-theme"><div id="wpconvert-search-modal" role="dialog" aria-modal="true" aria-label="Search"><div id="wpconvert-search-backdrop"></div><div id="wpconvert-search-container"><div id="wpconvert-search-header"> <svg id="wpconvert-search-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg> <input type="text" id="wpconvert-search-input" placeholder="Search pages..." autocomplete="off" /> <button id="wpconvert-search-close" type="button" aria-label="Close search"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" x2="6" y1="6" y2="18"></line><line x1="6" x2="18" y1="6" y2="18"></line></svg> </button></div><div id="wpconvert-search-results"></div><div id="wpconvert-search-hint">Press <kbd>Esc</kbd> to close • <kbd>Ctrl</kbd>+<kbd>K</kbd> to open</div></div></div><div id="wpconvert-mobile-nav-backdrop" onclick="document.body.classList.remove('mobile-nav-open'); document.getElementById('wpconvert-mobile-nav').classList.remove('open');"></div><nav id="wpconvert-mobile-nav" class="closed" aria-label="Mobile Navigation"><div class="nav-inner"> <button class="close-btn" onclick="document.body.classList.remove('mobile-nav-open'); this.parentElement.parentElement.classList.remove('open');" aria-label="Close menu"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" x2="6" y1="6" y2="18"></line><line x1="6" x2="18" y1="6" y2="18"></line></svg> </button><ul id="menu-main-menu" class="mobile-menu"><li id="menu-item-8606" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-8606"><a href="https://www.choosetosucceed.org/about/">About Choose to Succeed</a></li><li id="menu-item-8603" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-8603"><a href="https://www.choosetosucceed.org/find-a-school/">Find a School</a></li><li id="menu-item-8605" class="menu-item menu-item-type-post_type menu-item-object-page current-menu-item page_item page-item-8585 current_page_item menu-item-8605"><a href="https://www.choosetosucceed.org/consider-bexar-county/" aria-current="page">Consider Bexar County</a></li><li id="menu-item-8604" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-8604"><a href="https://www.choosetosucceed.org/contact/">Contact Us</a></li></ul></div></nav><nav class="nav"> <a href="https://www.choosetosucceed.org/" class="nav-logo"><img src="https://www.choosetosucceed.org/wp-content/themes/cts-website-revised6-4-26-theme/assets/images/CTS_Logo.jpg" alt="Choose to Succeed"></a><div class="nav-links"><span id="wpconvert-flat-nav" class="wpconvert-flat-nav-items" style="display:contents"><a href="https://www.choosetosucceed.org/about/" class="nav-link">About Choose to Succeed</a><a href="https://www.choosetosucceed.org/find-a-school/" class="nav-link">Find a School</a><a href="https://www.choosetosucceed.org/consider-bexar-county/" class="nav-link wpconvert-active-link current-menu-item">Consider Bexar County</a><a href="https://www.choosetosucceed.org/contact/" class="btn-p" data-wpc-id="wpc_navbtn_e8324f2a4b" data-wpc-editable="button">Contact Us</a></span></div> <a href="https://www.choosetosucceed.org/contact/" class="btn-p">Contact us</a></nav><main class="site-main"><div class="page-consider-bexar-county"><div class="page-content"><div class="section-hero"><div class="hero" id="hero"><div class="eyebrow" data-wpc-id="wpc_d83a5211bf" data-wpc-editable="heading">Consider Bexar County</div><h1 class="h1" data-wpc-id="wpc_c4847b178c" data-wpc-editable="heading">Why San Antonio is the<br>right place to <em>grow.</em></h1><p class="hero-sub" data-wpc-id="wpc_3bd682eb68" data-wpc-editable="text">Bexar County is one of the fastest-growing regions in the United States â with surging population, booming housing development, and a cost of living that makes sustainable operations possible. If you run a high-performing school network, San Antonio is ready for you.</p> <a href="https://www.choosetosucceed.org/contact/" class="btn-p" data-wpc-id="wpc_f3f593844e" data-wpc-editable="button">Start a conversation</a></div></div><div class="section-main"><div class="section" id="main"><div class="two-col"><div><div class="section-label" data-wpc-id="wpc_87596bee3c" data-wpc-editable="heading">Why operators choose San Antonio</div><div class="section-title" data-wpc-id="wpc_d346ff6ce1" data-wpc-editable="heading">The conditions are right</div><div class="inline-cards"><div class="inline-card"><div class="card-icon"><svg viewBox="0 0 24 24">
4343<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg></div><div><div class="card-title" data-wpc-id="wpc_df8d83d3fc" data-wpc-editable="heading">New housing = new families</div><div class="card-text">Rapidly developing suburbs in the north, south, east, and west of the city are generating demand for quality schools in neighborhoods that don't yet have them.</div></div></div><div class="inline-card"><div class="card-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"></circle><path d="M12 8v4l3 3"></path></svg></div><div><div class="card-title" data-wpc-id="wpc_d80fcb1ff2" data-wpc-editable="heading">Affordable to operate</div><div class="card-text">Lower real estate costs, competitive labor market, and Texas's lack of a state income tax make San Antonio a financially sustainable place to scale.</div></div></div><div class="inline-card"><div class="card-icon"><svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 00-3-3.87"></path><path d="M16 3.13a4 4 0 010 7.75"></path></svg></div><div><div class="card-title" data-wpc-id="wpc_10e549ba5c" data-wpc-editable="heading">Unmet demand is documented</div><div class="card-text">Consistent growth across many of our portfolio schools demonstrates that families are actively seeking better options than what's currently available.</div></div></div><div class="inline-card"><div class="card-icon"><svg viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path></svg></div><div><div class="card-title" data-wpc-id="wpc_208a8887f1" data-wpc-editable="heading">Strong civic infrastructure</div><div class="card-text">A network of local foundations, philanthropists, and civic leaders is ready to support new quality schools with relationships, funding, and advocacy.</div></div></div></div></div><div><div class="section-label" data-wpc-id="wpc_b2bb7a90cf" data-wpc-editable="heading">How CtS supports new operators</div><div class="section-title" data-wpc-id="wpc_aa36982156" data-wpc-editable="heading">More than a funder â a true partner</div><p class="section-sub" data-wpc-id="wpc_1109a2b3aa" data-wpc-editable="text">We work actively with highly promising and proven school operators to help their expansion into Bexar County.</p><div class="step-list" style="margin-bottom:24px;"><div class="step-item"><div class="step-num">1</div><div><div class="step-title" data-wpc-id="wpc_eaefe51e44" data-wpc-editable="heading">Exploratory conversation</div><div class="step-text" data-wpc-id="wpc_9b886d90e6" data-wpc-editable="text">We learn about your model, results, and vision for San Antonio.</div></div></div><div class="step-item"><div class="step-num">2</div><div><div class="step-title" data-wpc-id="wpc_26cc18cae5" data-wpc-editable="heading">Community fit assessment</div><div class="step-text">We help identify neighborhoods with the greatest unmet need aligned to your model.</div></div></div><div class="step-item"><div class="step-num">3</div><div><div class="step-title" data-wpc-id="wpc_f1aa897c4b" data-wpc-editable="heading">Partnership agreement</div><div class="step-text" data-wpc-id="wpc_f0f01ff1b1" data-wpc-editable="text">We define shared goals, support commitments, and accountability structures.</div></div></div><div class="step-item"><div class="step-num">4</div><div><div class="step-title" data-wpc-id="wpc_d2c0bd104f" data-wpc-editable="heading">Launch & ongoing support</div><div class="step-text">Advocacy, enrollment support, and community introductions as you open your doors.</div></div></div></div><div class="checklist"><div class="checklist-title" data-wpc-id="wpc_d30526cd7b" data-wpc-editable="heading">What we look for in partners</div><div class="check-item"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg><span data-wpc-id="wpc_06d57d5d1c" data-wpc-editable="text">Demonstrated academic results, ideally in comparable demographics</span></div><div class="check-item"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg><span data-wpc-id="wpc_d76478dc92" data-wpc-editable="text">Strong financials and a sound financial plan</span></div><div class="check-item"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg><span data-wpc-id="wpc_1ae1397253" data-wpc-editable="text">Track record of leadership success growing schools, and a seasoned leader to launch new schools</span></div><div class="check-item"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"></polyline></svg><span data-wpc-id="wpc_a0328a6382" data-wpc-editable="text">A model that answers an unmet need, and realistic early assumptions about enrollment</span></div></div></div></div></div></div><div class="grid-layout-container section-main"><div class="section" id="main"><div class="section-label" data-wpc-id="wpc_10de7ecaad" data-wpc-editable="heading">Bexar County by the numbers</div><div class="section-title" data-wpc-id="wpc_1c6112d16c" data-wpc-editable="heading">A fast-growing region hungry for better school options</div><div class="bexar-grid"><div class="bexar-card"><div class="bexar-num" data-wpc-id="wpc_c991cfe875" data-wpc-editable="heading">~2.1M</div><div class="bexar-label" data-wpc-id="wpc_3eda50df40" data-wpc-editable="heading">residents in Bexar County</div><div class="bexar-fn">Source: <a href="https://www.texas-demographics.com/bexar-county-demographics" target="_blank" data-wpc-id="wpc_a2abe70da8" data-wpc-editable="link">U.S. Census Bureau via Texas Demographics, 2024 ACS</a></div></div><div class="bexar-card"><div class="bexar-num" data-wpc-id="wpc_9f828ff38a" data-wpc-editable="heading">26%</div><div class="bexar-label" data-wpc-id="wpc_b669ef6b8c" data-wpc-editable="heading">population growth since 2010 â one of the fastest large counties in Texas</div><div class="bexar-fn">Source: <a href="https://worldpopulationreview.com/us-counties/texas/bexar-county" target="_blank" data-wpc-id="wpc_268c14f067" data-wpc-editable="link">World Population Review, U.S. Census Bureau data, 2026</a></div></div><div class="bexar-card"><div class="bexar-num" data-wpc-id="wpc_8eb758095c" data-wpc-editable="heading">42,500+</div><div class="bexar-label" data-wpc-id="wpc_32e6e62fb7" data-wpc-editable="heading">new private housing units permitted 2022â2025 </div><div class="bexar-fn">Source: <a href="https://fred.stlouisfed.org/series/BPPRIV048029" target="_blank" data-wpc-id="wpc_7dfede1fb4" data-wpc-editable="link">U.S. Census Bureau, Building Permits Survey via FRED, 2025</a></div></div><div class="bexar-card"><div class="bexar-num" data-wpc-id="wpc_b433566300" data-wpc-editable="heading">9% below</div><div class="bexar-label" data-wpc-id="wpc_59cc8b04f5" data-wpc-editable="heading">national average cost of living. It gets better: housing is 25% cheaper than the U.S. average</
4343div><div class="bexar-fn">Source: <a href="https://www.rentcafe.com/cost-of-living-calculator/us/tx/bexar-county/san-antonio/" target="_blank" data-wpc-id="wpc_7731aad44b" data-wpc-editable="link">RentCafe Cost of Living Index, 2026</a></div></div><div class="bexar-card"><div class="bexar-num" data-wpc-id="wpc_4f24b2843e" data-wpc-editable="heading">361,278</div><div class="bexar-label" data-wpc-id="wpc_d09a82d593" data-wpc-editable="heading">Kâ12 students enrolled in Bexar County public schools (and growing)</div><div class="bexar-fn">Source: <a href="https://www.publicschoolreview.com/texas/bexar-county" target="_blank" data-wpc-id="wpc_015c08b9e6" data-wpc-editable="link">Public School Review, citing TEA / NCES data, 2025</a></div></div><div class="bexar-card"><div class="bexar-num" data-wpc-id="wpc_d6522285ca" data-wpc-editable="heading">34.8</div><div class="bexar-label" data-wpc-id="wpc_d476ed59b7" data-wpc-editable="heading">median age â one of the youngest large cities in Texas</div><div class="bexar-fn">Source: <a href="https://worldpopulationreview.com/us-counties/texas/bexar-county" target="_blank" data-wpc-id="wpc_5f5fda0985" data-wpc-editable="link">World Population Review, U.S. Census Bureau data, 2026</a></div></div></div></div></div></div></div></main><footer class="site-footer"><div class="footer-l">© 2026 Choose to Succeed · 700 N. St. Mary's Street, Suite 875, San Antonio, TX 78205</div><div class="footer-links"> <a href="https://www.choosetosucceed.org/" data-wpc-id="wpc_93577c7077" data-wpc-editable="link">School choice</a> <a href="https://www.choosetosucceed.org/about/" data-wpc-id="wpc_e29658b18d" data-wpc-editable="link">About CtS</a> <a href="https://www.choosetosucceed.org/find-a-school/" data-wpc-id="wpc_416166cf12" data-wpc-editable="link">Find a school</a> <a href="https://www.choosetosucceed.org/consider-bexar-county/" data-wpc-id="wpc_a84e9c4f99" data-wpc-editable="link">Consider Bexar County</a></div></footer>
4343<script type="speculationrules">{"prefetch":[{"source":"document","where":{"and":[{"href_matches":"/*"},{"not":{"href_matches":["/wp-*.php","/wp-admin/*","/wp-content/uploads/*","/wp-content/*","/wp-content/plugins/*","/wp-content/themes/cts-website-revised6-4-26-theme/*","/*\\?(.+)"]}},{"not":{"selector_matches":"a[rel~=\"nofollow\"]"}},{"not":{"selector_matches":".no-prefetch, .no-prefetch a"}}]},"eagerness":"conservative"}]}</script>
4343
4343<script id="wp-hooks-js" src="https://www.choosetosucceed.org/wp-includes/js/dist/hooks.min.js?ver=7496969728ca0f95732d"></script>
4343
4343<script id="wp-i18n-js" src="https://www.choosetosucceed.org/wp-includes/js/dist/i18n.min.js?ver=781d11515ad3d91786ec"></script>
4343
4343<script id="wp-i18n-js-after">wp.i18n.setLocaleData( { 'text direction\u0004ltr': [ 'ltr' ] } ); 4344//# sourceURL=wp-i18n-js-after</script>
4344
4344<script id="contact-form-7-js-before">var wpcf7 = { 4345 "api": { 4346 "root": "https:\/\/www.choosetosucceed.org\/wp-json\/", 4347 "namespace": "contact-form-7\/v1" 4348 } 4349}; 4350//# sourceURL=contact-form-7-js-before</script>
4350
4350<script id="wp-emoji-settings" type="application/json">{"baseUrl":"https://s.w.org/images/core/emoji/17.0.2/72x72/","ext":".png","svgUrl":"https://s.w.org/images/core/emoji/17.0.2/svg/","svgExt":".svg","source":{"concatemoji":"https://www.choosetosucceed.org/wp-includes/js/wp-emoji-release.min.js?ver=7.0.6"}}</script>
4350
4350<script type="module">/*! This file is auto-generated */ 4351var e="script#wp-emoji-settings",t=document.querySelector(e);if(!(t instanceof HTMLScriptElement))throw new Error("Element missing: "+e);const r=JSON.parse(t.text),s=(window._wpemojiSettings=r,"wpEmojiSettingsSupports"),o=["flag","emoji"];function i(e){try{var t={supportTests:e,timestamp:(new Date).valueOf()};sessionStorage.setItem(s,JSON.stringify(t))}catch(e){}}function c(e,t,n){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);t=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(n,0,0);const r=new Uint32Array(e.getImageData(0,0,e.canvas.width,e.canvas.height).data);return t.every((e,t)=>e===r[t])}function p(e,t){e.clearRect(0,0,e.canvas.width,e.canvas.height),e.fillText(t,0,0);var n=e.getImageData(16,16,1,1);for(let e=0;e<n.data.length;e++)if(0!==n.data[e])return!1;return!0}function u(e,t,n,r){switch(t){case"flag":return n(e,"\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f","\ud83c\udff3\ufe0f\u200b\u26a7\ufe0f")?!1:!n(e,"\ud83c\udde8\ud83c\uddf6","\ud83c\udde8\u200b\ud83c\uddf6")&&!n(e,"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f","\ud83c\udff4\u200b\udb40\udc67\u200b\udb40\udc62\u200b\udb40\udc65\u200b\udb40\udc6e\u200b\udb40\udc67\u200b\udb40\udc7f");case"emoji":return!r(e,"\ud83e\u1fac8")}return!1}function f(e,t,n,r){let a;const s=(a="undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?new OffscreenCanvas(300,150):document.createElement("canvas")).getContext("2d",{willReadFrequently:!0}),o=(s.textBaseline="top",s.font="600 32px Arial",{});return e.forEach(e=>{o[e]=t(s,e,n,r)}),o}function a(e){var t=document.createElement("script");t.src=e,t.defer=!0,document.head.appendChild(t)}r.supports={everything:!0,everythingExceptFlag:!0},new Promise(t=>{let n=function(){try{var e=JSON.parse(sessionStorage.getItem(s));if("object"==typeof e&&"number"==typeof e.timestamp&&(new Date).valueOf()<e.timestamp+604800&&"object"==typeof e.supportTests)return e.supportTests}catch(e){}return null}();if(!n){if("undefined"!=typeof Worker&&"undefined"!=typeof OffscreenCanvas&&"undefined"!=typeof URL&&URL.createObjectURL&&"undefined"!=typeof Blob)try{var e="postMessage("+f.toString()+"("+[JSON.stringify(o),u.toString(),c.toString(),p.toString()].join(",")+"));",r=new Blob([e],{type:"text/javascript"});const a=new Worker(URL.createObjectURL(r),{name:"wpTestEmojiSupports"});return void(a.onmessage=e=>{i(n=e.data),a.terminate(),t(n)})}catch(e){}i(n=f(o,u,c,p))}t(n)}).then(e=>{for(const n in e)r.supports[n]=e[n],r.supports.everything=r.supports.everything&&r.supports[n],"flag"!==n&&(r.supports.everythingExceptFlag=r.supports.everythingExceptFlag&&r.supports[n]);var t;r.supports.everythingExceptFlag=r.supports.everythingExceptFlag&&!r.supports.flag,r.supports.everything||((t=r.source||{}).concatemoji?a(t.concatemoji):t.wpemoji&&t.twemoji&&(a(t.twemoji),a(t.wpemoji)))}); 4352//# sourceURL=https://www.choosetosucceed.org/wp-includes/js/wp-emoji-loader.min.js</script>
4352
4352<script id="wpconvert-video-lightbox">(function() { 4353 'use strict'; 4354 4355 function getEmbedUrl(rawUrl) { 4356 var url = (rawUrl || '').trim(); 4357 if (!url || /\bexample\d*\b/i.test(url)) return ''; 4358 4359 if (url.indexOf('vimeo.com') !== -1) { 4360 var vm = url.match(/vimeo\.com\/(?:video\/)?(\d+)/); 4361 return vm ? 'https://player.vimeo.com/video/' + vm[1] + '?autoplay=1&title=0&byline=0&portrait=0&dnt=1' : ''; 4362 } 4363 if (url.indexOf('youtube.com') !== -1 || url.indexOf('youtu.be') !== -1) { 4364 var yt = url.match(/youtu\.be\/([^?&\/]+)/) || 4365 url.match(/[?&]v=([^?&\/]+)/) || 4366 url.match(/youtube\.com\/embed\/([^?&\/]+)/); 4367 return yt ? 'https://www.youtube-nocookie.com/embed/' + yt[1] + '?autoplay=1&rel=0&modestbranding=1' : ''; 4368 } 4369 return url; 4370 } 4371 4372 function createModal() { 4373 if (document.getElementById('wpconvert-video-modal')) return; 4374 var overlay = document.createElement('div'); 4375 overlay.id = 'wpconvert-video-modal'; 4376 overlay.style.cssText = 'display:none;position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.85);backdrop-filter:blur(4px);align-items:center;justify-content:center;padding:2rem;'; 4377 overlay.innerHTML = '<div style="position:relative;width:100%;max-width:960px;"><button id="wpconvert-video-close" style="position:absolute;top:-40px;right:0;background:none;border:none;color:#fff;font-size:28px;cursor:pointer;padding:4px 8px;z-index:1;" aria-label="Close video">×</button><div style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:8px;overflow:hidden;"><iframe id="wpconvert-video-frame" style="position:absolute;top:0;left:0;width:100%;height:100%;border:none;" allow="accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture;fullscreen" allowfullscreen></iframe></div></div>'; 4378 document.body.appendChild(overlay); 4379 4380 var closeBtn = document.getElementById('wpconvert-video-close'); 4381 closeBtn.addEventListener('click', closeModal); 4382 overlay.addEventListener('click', function(e) { 4383 if (e.target === overlay) closeModal(); 4384 }); 4385 document.addEventListener('keydown', function(e) { 4386 if (e.key === 'Escape') closeModal(); 4387 }); 4388 } 4389 4390 function openModal(videoUrl) { 4391 var modal = document.getElementById('wpconvert-video-modal'); 4392 var iframe = document.getElementById('wpconvert-video-frame'); 4393 if (!modal || !iframe) return; 4394 var embedUrl = getEmbedUrl(videoUrl); 4395 if (!embedUrl) return; 4396 iframe.src = embedUrl; 4397 modal.style.display = 'flex'; 4398 document.body.style.overflow = 'hidden'; 4399 } 4400 4401 function closeModal() { 4402 var modal = document.getElementById('wpconvert-video-modal'); 4403 var iframe = document.getElementById('wpconvert-video-frame'); 4404 if (!modal) return; 4405 modal.style.display = 'none'; 4406 if (iframe) iframe.src = ''; 4407 document.body.style.overflow = ''; 4408 } 4409 4410 function init() { 4411 if (window.location.search.indexOf('wpc_edit=1') !== -1) return; 4412 var cardsFound = 0; 4413 4414 // Strategy 1: Cards with data-video-url (set during snapshot extraction) 4415 document.querySelectorAll('[data-video-url]').forEach(function(card) { 4416 var videoUrl = card.getAttribute('data-video-url'); 4417 if (!videoUrl) return; 4418 card.style.cursor = 'pointer'; 4419 card.addEventListener('click', function(e) { 4420 e.preventDefault(); 4421 e.stopPropagation(); 4422 openModal(videoUrl); 4423 }); 4424 cardsFound++; 4425 }); 4426 4427 // Strategy 2: Cards with play button SVGs but no data-vi
4427deo-url 4428 // Auto-detect video URLs from thumbnail image sources 4429 var playIcons = document.querySelectorAll('.lucide-play, [class*="lucide-play"]'); 4430 playIcons.forEach(function(icon) { 4431 var card = icon.closest('[class*="cursor-pointer"]') || 4432 icon.closest('[class*="card"]') || 4433 icon.closest('[class*="group"]'); 4434 if (!card || card.hasAttribute('data-video-url') || card.hasAttribute('data-wpconvert-video-bound')) return; 4435 4436 var img = card.querySelector('img'); 4437 if (!img) return; 4438 var src = img.getAttribute('src') || ''; 4439 4440 var videoUrl = ''; 4441 // YouTube thumbnail -> YouTube embed 4442 var ytMatch = src.match(/img\.youtube\.com\/vi\/([a-zA-Z0-9_-]+)\//); 4443 if (ytMatch) { 4444 videoUrl = 'https://www.youtube.com/watch?v=' + ytMatch[1]; 4445 } 4446 // Vimeo CDN thumbnail â can't derive URL, skip 4447 4448 if (videoUrl) { 4449 card.setAttribute('data-wpconvert-video-bound', 'true'); 4450 card.addEventListener('click', function(e) { 4451 e.preventDefault(); 4452 e.stopPropagation(); 4453 openModal(videoUrl); 4454 }); 4455 cardsFound++; 4456 } 4457 }); 4458 4459 if (cardsFound > 0) { 4460 createModal(); 4461 console.log('[WPConvert] Video lightbox initialized for ' + cardsFound + ' card(s)'); 4462 } 4463 } 4464 4465 if (document.readyState === 'loading') { 4466 document.addEventListener('DOMContentLoaded', init); 4467 } else { 4468 init(); 4469 } 4470 })();</script>
4470
4470<script>(function() { 4471 'use strict'; 4472 4473 function init() { 4474 // Find button groups that look like tab selectors 4475 // Pattern: multiple sibling buttons in a flex container with gap 4476 var containers = document.querySelectorAll('div[class*="flex"][class*="gap"]'); 4477 var tabGroupsInitialized = 0; 4478 4479 containers.forEach(function(container, containerIndex) { 4480 var buttons = container.querySelectorAll(':scope > button'); 4481 4482 // Need at least 2 buttons to be a tab selector 4483 if (buttons.length < 2) return; 4484 4485 // Check if buttons have similar structure (tab selector pattern) 4486 // Only compare SIZE classes (w-, h-), ignore color/styling differences 4487 var hasSimilarButtons = true; 4488 var firstButtonSizeClasses = buttons[0].className.split(' ').filter(function(c) { 4489 // Only check size classes - ignore border colors, bg colors, hover states 4490 return /^(w-[0-9]+|h-[0-9]+)$/.test(c); 4491 }).sort().join(' '); 4492 4493 buttons.forEach(function(btn) { 4494 var btnSizeClasses = btn.className.split(' ').filter(function(c) { 4495 return /^(w-[0-9]+|h-[0-9]+)$/.test(c); 4496 }).sort().join(' '); 4497 if (btnSizeClasses !== firstButtonSizeClasses) { 4498 hasSimilarButtons = false; 4499 } 4500 }); 4501 4502 // Also require that buttons have size classes (not just any flex container) 4503 if (!firstButtonSizeClasses || !hasSimilarButtons) return; 4504 4505 // Skip if already handled (navigation buttons with < > arrows) 4506 var hasNavArrows = container.querySelector('svg[class*="chevron-left"], svg[class*="chevron-right"]'); 4507 var isNavContainer = container.querySelectorAll(':scope > button').length === 2 && hasNavArrows; 4508 if (isNavContainer) return; 4509 4510 // Find the active button (has gold/primary styling) 4511 var activeButton = null; 4512 var activeIndex = 0; 4513 buttons.forEach(function(btn, index) { 4514 if (/border-gold|bg-gold|border-primary|bg-primary/.test(btn.className)) { 4515 activeButton = btn; 4516 activeIndex = index; 4517 } 4518 }); 4519 4520 // Find associated content area 4521 // Look for text that shows selected item info 4522 // It could be: sibling of container, sibling of parent, or in ancestor section 4523 var infoText = null; 4524 var searchContext = container.parentElement; 4525 4526 // Try parent's siblings first (common pattern) 4527 if (searchContext && searchContext.parentElement) { 4528 var grandparent = searchContext.parentElement; 4529 infoText = grandparent.querySelector('div[class*="text-center"] p[class*="text-gold"], div[class*="text-center"] p[class*="font-serif"]'); 4530 } 4531 4532 // If not found, try the whole section 4533 if (!infoText) { 4534 var section = container.closest('section'); 4535 if (section) { 4536 infoText = section.querySelector('div[class*="text-center"][class*="mt-"] p[class*="text-gold"], div[class*="text-center"][class*="mt-"] p[class*="font-serif"]'); 4537 } 4538 } 4539 4540 // Mark as initialized 4541 container.setAttribute('data-wpconvert-tabs', 'true'); 4542 4543 // Store button labels for updating info text 4544 var buttonLabels = []; 4545 buttons.forEach(function(btn) { 4546 buttonLabels.push(btn.textContent.trim()); 4547 }); 4548 4549 // Add click handlers to each button 4550 buttons.forEach(function(btn, index) { 4551 btn.addEventListener('click', function(e) { 4552 e.preventDefault(); 4553 e.stopPropagation(); 4554 4555 // Update active styling on all buttons 4556 buttons.forEach(function(b, i) { 4557 if (i === index) { 4558 // Make this button active - use RegExp constructor to avoid escape issues 4559 b.className = b.className 4560 .replace(new RegExp('border-white/[0-9]+', 'g'), 'border-gold') 4561 .replace(new RegExp('hover:border-white/[0-9]+', 'g'), ''); 4562 if (b.className.indexOf('bg-gold') === -1) { 4563 b.className = b.className.replace('border-gold', 'border-gold bg-gold/20'); 4564 } 4565 } else { 4566 // Make this button inactive 4567 b.className = b.className 4568 .replace(/border-gold/g, 'border-white/20') 4569 .replace(new RegExp('bg-gold/[0-9]+', 'g'), '') 4570 .replace(new RegExp(' +', 'g'), ' ') 4571 .trim(); 4572 if (b.className.indexOf('hover:border') === -1) { 4573 b.className += ' hover:border-white/40'; 4574 } 4575 } 4576 }); 4577 4578 // Update info text if found 4579 if (infoText && buttonLabels[index]) { 4580 infoText.textContent = buttonLabels[index]; 4581 } 4582 4583 // Store active index 4584 container.setAttribute('data-active-index', index); 4585 }); 4586 }); 4587 4588 tabGroupsInitialized++; 4589 }); 4590 4591 if (tabGroupsInitialized > 0) { 4592 console.log('WPConvert Tab Selector: Initialized ' + tabGroupsInitialized + ' tab group(s)'); 4593 } 4594 } 4595 4596 // Initialize when DOM is ready 4597 if (document.readyState === 'loading') { 4598 document.addEventListener('DOMContentLoaded', init); 4599 } else { 4600 init(); 4601 } 4602 4603 })();</script>
4603
4603<script>(function() { 4604 'use strict'; 4605 4606 function init() { 4607 // Find all range inputs that might be comparison sliders 4608 var rangeInputs = document.querySelectorAll('input[type="range"]'); 4609 var slidersInitialized = 0; 4610 4611 rangeInputs.forEach(function(input) { 4612 // Look for comparison slider pattern: 4613 // - Range input inside a container with overlapping elements 4614 // - Sibling or nearby element with clip-path styling 4615 var container = input.closest('div[class*="relative"], div[class*="overflow"]'); 4616 if (!container) return; 4617 4618 // Find elements that use clip-path (the "after" reveal layer) 4619 var clipPathElements = container.querySelectorAll('[style*="clip-path"], [style*="clipPath"]'); 4620 if (clipPathElements.length === 0) return; 4621 4622 // Find the divider line (vertical bar that moves with slider) 4623 var dividerLine = container.querySelector('[class*="bg-gold"], [class*="bg-primary"], div[style*="left:"]'); 4624 4625 // Mark as initialized 4626 input.setAttribute('data-wpconvert-comparison', 'true'); 4627 4628 // Make input visible and interactive 4629 input.style.opacity = '0'; 4630 input.style.cursor = 'ew-resize'; 4631 input.style.zIndex = '20'; 4632 4633 // Add input handler 4634 input.addEventListener('input', function(e) { 4635 var value = e.target.value; 4636 4637 // Update clip-path on reveal elements 4638 clipPathElements.forEach(function(el) { 4639 // clip-path: inset(0 RIGHT% 0 0) - RIGHT is 100 - value 4640 el.style.clipPath = 'inset(0 ' + (100 - value) + '% 0 0)'; 4641 }); 4642 4643 // Update divider line position 4644 if (dividerLine) { 4645 dividerLine.style.left = value + '%'; 4646 } 4647 }); 4648 4649 // Trigger initial positioning based on current value 4650 input.dispatchEvent(new Event('input')); 4651 4652 slidersInitialized++; 4653 }); 4654 4655 if (slidersInitialized > 0) { 4656 console.log('WPConvert Comparison Slider: Initialized ' + slidersInitialized + ' slider(s)'); 4657 } 4658 } 4659 4660 // Initialize when DOM is ready 4661 if (document.readyState === 'loading') { 4662 document.addEventListener('DOMContentLoaded', init); 4663 } else { 4664 init(); 4665 } 4666 4667 })();</script>
4667
4667<script>(function() { 4668 'use strict'; 4669 4670 // EC-ACCORDION-003: Find the content panel for an accordion button. 4671 // Handles two DOM patterns: 4672 // 1. Direct sibling: <button/> <div class="overflow-hidden"> (custom accordions) 4673 // 2. Header-wrapped: <h3><button/></h3> <div role="region"> (Radix UI / shadcn) 4674 // 4675 // EC-ACCORDION-012: An accordion content panel always comes AFTER its 4676 // trigger and is real content (not an img/picture/video-only wrapper). 4677 // Otherwise we mis-detect image-card layouts where the CTA button is 4678 // the LAST child of the card (MenuCard "+ Tilfoj"): the old 4679 // third-branch loop returned the image wrapper at the TOP of the 4680 // card and the init collapsed it to height:0. 4681 function isImageOnlyPanel(el) { 4682 if (!el || !el.children) return false; 4683 var hasText = (el.textContent || '').replace(/\s+/g, '').length > 0; 4684 if (hasText) return false; 4685 var kids = el.children; 4686 if (!kids.length) return false; 4687 for (var i = 0; i < kids.length; i++) { 4688 var tag = kids[i].tagName; 4689 if (tag !== 'IMG' && tag !== 'PICTURE' && tag !== 'VIDEO' && tag !== 'SOURCE') { 4690 return false; 4691 } 4692 } 4693 return true; 4694 } 4695 function findContentPanel(btn) { 4696 var sibling = btn.nextElementSibling; 4697 if (sibling && sibling.tagName === 'DIV') { 4698 if (isImageOnlyPanel(sibling)) return null; 4699 return sibling; 4700 } 4701 4702 var parent = btn.parentElement; 4703 if (parent && /^H[1-6]$/.test(parent.tagName)) { 4704 var parentSibling = parent.nextElementSibling; 4705 if (parentSibling && parentSibling.tagName === 'DIV') { 4706 if (isImageOnlyPanel(parentSibling)) return null; 4707 return parentSibling; 4708 } 4709 } 4710 // EC-ACCORDION-012: third branch -- button has no nextElementSibling. 4711 // The original intent (button is first child of wrapper, wrapper 4712 // contains a hidden div) is enforced by ONLY returning DIVs that 4713 // come AFTER the button in DOM order. Image-only DIVs are skipped. 4714 if (parent && !sibling) { 4715 var kids = parent.children; 4716 var btnIdx = -1; 4717 for (var k = 0; k < kids.length; k++) { 4718 if (kids[k] === btn) { btnIdx = k; break; } 4719 } 4720 if (btnIdx >= 0) { 4721 for (var j = btnIdx + 1; j < kids.length; j++) { 4722 if (kids[j].tagName === 'DIV' && !isImageOnlyPanel(kids[j])) { 4723 return kids[j]; 4724 } 4725 } 4726 } 4727 } 4728 return null; 4729 } 4730 4731 function init() { 4732 var buttons = document.querySelectorAll('button'); 4733 var accordionsInitialized = 0; 4734 4735 buttons.forEach(function(btn) { 4736 // EC-ACCORDION-012: Image-card CTA buttons (e.g. MenuCard 4737 // + Tilfoj) are explicitly stamped at build time as 4738 // non-accordion triggers. Honour the marker here so the 4739 // accordion init can never wire them up regardless of 4740 // what findContentPanel would otherwise return. 4741 if (btn.getAttribute('data-wpc-no-accordion-trigger') === '1') return; 4742 4743 // EC-ACCORDION-007: Skip buttons with aria-controls pointing to existing 4744 // Radix content. The jQuery accordion handler already manages these via 4745 // aria-controls â role="region". Attaching a second handler on the 4746 // injected data-wpc-faq-answer div would show duplicate answers. 4747 var ariaCtrl = btn.getAttribute('aria-controls'); 4748 if (ariaCtrl && document.getElementById(ariaCtrl)) return; 4749 4750 var sibling = findContentPanel(btn); 4751 if (!sibling) return; 4752 4753 var siblingClasses = sibling.className || ''; 4754 var siblingStyle = sibling.getAttribute('style') || ''; 4755 var hasHidden = sibling.hasAttribute('hidden'); 4756 var hasRoleRegion = sibling.getAttribute('role') === 'regi
4756on'; 4757 4758 // EC-ACCORDION-005: Also check first child for overflow-hidden (grid-rows pattern) 4759 var firstChild = sibling.firstElementChild; 4760 var childClasses = firstChild ? (firstChild.className || '') : ''; 4761 4762 // Detect accordion pattern: 4763 // 1. Sibling has overflow-hidden class (common accordion pattern) 4764 // 2. Sibling has height: 0 or height: auto in inline style 4765 // 3. Sibling has opacity: 0 or opacity: 1 in inline style 4766 // EC-ACCORDION-003: Also detect Radix UI pattern (role="region" + hidden + data-state) 4767 // EC-ACCORDION-005: Tailwind grid-rows-[0fr] pattern (sibling is grid, child has overflow-hidden) 4768 var isGridRowsAccordion = /grid-rows-\[0fr\]|grid-rows-\[1fr\]/.test(siblingClasses); 4769 var hasChildOverflowHidden = /overflow-hidden/.test(childClasses); 4770 4771 var isFaqAnswer = sibling.hasAttribute('data-wpc-faq-answer'); 4772 var isAccordion = /overflow-hidden|collapse/i.test(siblingClasses) || 4773 /height:[ ]*(0|auto)|opacity:[ ]*(0|1)/i.test(siblingStyle) || 4774 (hasRoleRegion && (hasHidden || sibling.hasAttribute('data-state'))) || 4775 (isGridRowsAccordion && hasChildOverflowHidden) || 4776 isFaqAnswer; 4777 4778 if (!isAccordion) return; 4779 4780 // Check current state 4781 var isExpanded = false; 4782 if (isGridRowsAccordion) { 4783 isExpanded = /grid-rows-\[1fr\]/.test(siblingClasses); 4784 } else if (hasHidden || /display:[ ]*none/i.test(siblingStyle)) { 4785 isExpanded = false; 4786 } else if (/height:[ ]*auto|opacity:[ ]*1/i.test(siblingStyle) && 4787 !/height:[ ]*0|opacity:[ ]*0/i.test(siblingStyle)) { 4788 isExpanded = true; 4789 } 4790 4791 btn.setAttribute('data-wpconvert-accordion', 'true'); 4792 btn.setAttribute('data-expanded', isExpanded ? 'true' : 'false'); 4793 sibling.setAttribute('data-wpconvert-accordion-content', 'true'); 4794 4795 // EC-ACCORDION-005: Grid-rows accordion uses grid-template-rows for animation 4796 if (isGridRowsAccordion) { 4797 sibling.setAttribute('data-wpconvert-grid-accordion', 'true'); 4798 sibling.style.transition = 'grid-template-rows 0.3s ease'; 4799 if (!isExpanded) { 4800 sibling.style.gridTemplateRows = '0fr'; 4801 } else { 4802 sibling.style.gridTemplateRows = '1fr'; 4803 } 4804 } else { 4805 // EC-ACCORDION-003: Remove hidden attribute and display:none so CSS can control visibility. 4806 // The accordion starts collapsed via height:0 + opacity:0 instead. 4807 sibling.removeAttribute('hidden'); 4808 sibling.style.display = ''; 4809 4810 sibling.style.overflow = 'hidden'; 4811 sibling.style.transition = 'height 0.3s ease, opacity 0.3s ease'; 4812 4813 // EC-ACCORDION-004: Tailwind max-h-0 class sets max-height:0px which 4814 // blocks expansion regardless of inline height. Override with a 4815 // large value instead of 'none' â some browsers treat 'none' (the 4816 // initial value) as removing the inline declaration, letting the 4817 // class rule win. 4818 if (/max-h-/.test(siblingClasses)) { 4819 sibling.style.maxHeight = '9999px'; 4820 } 4821 4822 if (!isExpanded) { 4823 sibling.style.height = '0px'; 4824 sibling.style.opacity = '0'; 4825 } else { 4826 sibling.style.height = 'auto'; 4827 sibling.style.opacity = '1'; 4828 } 4829 } 4830 4831 btn.addEventListener('click', function(e) { 4832 e.preventDefault(); 4833 4834 var currentlyExpanded = btn.getAttribute('data-expanded') === 'true'; 4835 var content = findContentPanel(btn); 4836 if (!content) return; 4837 4838 // EC-ACCORDION-005: Grid-rows toggle 4839 if (content.hasAttribute('data-wpconvert-grid-accordion')) { 4840 if (currentlyExpanded) { 4841 content.style.gridTemplateRows = '0fr'; 4842 btn.setAttribute('data-expanded', 'false'); 4843 } else { 4844 content.style.gridTemplateRows = '1fr'; 4845 btn.setAttribute('data-expanded', 'true'); 4846 } 4847 } else if (currentlyExpanded) { 4848 var currentHeight = content.scrollHeight || content.offsetHeight; 4849 content.style.height = currentHeight + 'px'; 4850 content.offsetHeight; 4851 content.style.height = '0px'; 4852 content.style.opacity = '0'; 4853 btn.setAttribute('data-expanded', 'false'); 4854 } else { 4855 // EC-ACCORDION-004: Robust height measurement. 4856 // Some browsers return scrollHeight=0 when height:0+overflow:hidden. 4857 // Temporarily reveal to measure, then animate from 0. 4858 var targetHeight = content.scrollHeight; 4859 if (!targetHeight) {
4860 content.style.transition = 'none'; 4861 content.style.height = 'auto'; 4862 content.style.maxHeight = '9999px'; 4863 content.style.opacity = '0'; 4864 targetHeight = content.scrollHeight || content.offsetHeight; 4865 content.style.height = '0px'; 4866 content.offsetHeight; // force reflow 4867 content.style.transition = 'height 0.3s ease, opacity 0.3s ease'; 4868 } 4869 if (targetHeight > 0) { 4870 content.style.height = targetHeight + 'px'; 4871 } else { 4872 content.style.height = 'auto'; 4873 } 4874 content.style.opacity = '1'; 4875 btn.setAttribute('data-expanded', 'true'); 4876 4877 setTimeout(function() { 4878 if (btn.getAttribute('data-expanded') === 'true') { 4879 content.style.height = 'auto'; 4880 } 4881 }, 300); 4882 } 4883 4884 var plusIcon = btn.querySelector('.lucide-plus, [class*="plus"]'); 4885 var minusIcon = btn.querySelector('.lucide-minus, [class*="minus"]'); 4886 4887 if (plusIcon && minusIcon) { 4888 if (currentlyExpanded) { 4889 plusIcon.style.display = ''; 4890 minusIcon.style.display = 'none'; 4891 } else { 4892 plusIcon.style.display = 'none'; 4893 minusIcon.style.display = ''; 4894 } 4895 } 4896 4897 var chevron = btn.querySelector('.lucide-chevron-down, .lucide-chevron-up, [class*="chevron"]'); 4898 if (chevron) { 4899 chevron.style.transition = 'transform 0.3s ease'; 4900 chevron.style.transform = currentlyExpanded ? 'rotate(0deg)' : 'rotate(180deg)'; 4901 } 4902 }); 4903 4904 var plusIcon = btn.querySelector('.lucide-plus, [class*="plus"]'); 4905 var minusIcon = btn.querySelector('.lucide-minus, [class*="minus"]'); 4906 if (plusIcon && minusIcon) { 4907 if (isExpanded) { 4908 plusIcon.style.display = 'none'; 4909 minusIcon.style.display = ''; 4910 } else { 4911 plusIcon.style.display = ''; 4912 minusIcon.style.display = 'none'; 4913 } 4914 } 4915 4916 accordionsInitialized++; 4917 }); 4918 4919 if (accordionsInitialized > 0) { 4920 console.log('WPConvert Accordion: Initialized ' + accordionsInitialized + ' accordion(s)'); 4921 } 4922 4923 // EC-ACCORDION-007: Enhance <details>/<summary> elements for consistent FAQ styling 4924 // Sub-pages may have FAQ sections converted to native <details>/<summary>. 4925 // Add animated +/- icon toggle and smooth open/close transitions. 4926 var detailsEnhanced = 0; 4927 document.querySelectorAll('details').forEach(function(det) { 4928 if (det.hasAttribute('data-wpc-enhanced')) return; 4929 det.setAttribute('data-wpc-enhanced', 'true'); 4930 var summary = det.querySelector('summary'); 4931 if (!summary) return; 4932 // Add toggle icon if not already present 4933 var toggleSpan = summary.querySelector('.wpc-toggle-icon'); 4934 if (!toggleSpan) { 4935 var existingIcon = summary.querySelector('span:last-child'); 4936 if (existingIcon && /^[+-Ã]$/.test(existingIcon.textContent.trim())) { 4937 toggleSpan = existingIcon; 4938 toggleSpan.classList.add('wpc-toggle-icon'); 4939 } else { 4940 toggleSpan = document.createElement('span'); 4941 toggleSpan.className = 'wpc-toggle-icon'; 4942 toggleSpan.textContent = '+'; 4943 toggleSpan.style.cssText = 'margin-left:auto;transition:transform 0.3s ease;font-size:1.25em;'; 4944 summary.appendChild(toggleSpan); 4945 } 4946 } 4947 function updateIcon() { 4948 if (det.open) { 4949 toggleSpan.textContent = toggleSpan.textContent === '+' ? 'â' : toggleSpan.textContent; 4950 toggleSpan.style.transform = 'rotate(45deg)'; 4951 } else { 4952 toggleSpan.textContent = toggleSpan.textContent === 'â' ? '+' : toggleSpan.textContent; 4953 toggleSpan.style.transform = 'rotate(0deg)'; 4954 } 4955 } 4956 det.addEventListener('toggle', updateIcon); 4957 updateIcon(); 4958 detailsEnhanced++; 4959 }); 4960 if (detailsEnhanced > 0) { 4961 console.log('WPConvert Accordion: Enhanced ' + detailsEnhanced + ' <details> element(s)'); 4962 } 4963 } 4964 4965 // Initialize when DOM is ready 4966 if (document.readyState === 'loading') { 4967 document.addEventListener('DOMContentLoaded', init); 4968 } else { 4969 init(); 4970 } 4971 4972 })();</script>
4972
4972<script id="wpconvert-smooth-scroll">(function() { 4973 'use strict'; 4974 4975 // Configuration 4976 const CONFIG = { 4977 offset: 80, // Offset from top (accounts for sticky headers) 4978 duration: 800, // Scroll duration in ms 4979 easing: 'easeInOutCubic' // Easing function 4980 }; 4981 4982 // Easing functions 4983 const easings = { 4984 linear: function(t) { return t; }, 4985 easeInOutCubic: function(t) { 4986 return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; 4987 }, 4988 easeInOutQuad: function(t) { 4989 return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; 4990 } 4991 }; 4992 4993 /** 4994 * Smooth scroll to target element 4995 */ 4996 function smoothScrollTo(target, duration, offset, easing) { 4997 const targetPosition = target.getBoundingClientRect().top + window.pageYOffset - offset; 4998 const startPosition = window.pageYOffset; 4999 const distance = targetPosition - startPosition; 5000 const easingFunction = easings[easing] || easings.easeInOutCubic; 5001 let startTime = null; 5002 5003 function animation(currentTime) { 5004 if (startTime === null) startTime = currentTime; 5005 const timeElapsed = currentTime - startTime; 5006 const progress = Math.min(timeElapsed / duration, 1); 5007 const ease = easingFunction(progress); 5008 5009 window.scrollTo(0, startPosition + distance * ease); 5010 5011 if (timeElapsed < duration) { 5012 requestAnimationFrame(animation); 5013 } else { 5014 // Update URL hash without jumping 5015 if (target.id && window.history && window.history.pushState) { 5016 window.history.pushState(null, null, '#' + target.id); 5017 } 5018 } 5019 } 5020 5021 requestAnimationFrame(animation); 5022 } 5023 5024 /** 5025 * Calculate header offset dynamically 5026 * Accounts for sticky/fixed headers 5027 */ 5028 function getHeaderOffset() { 5029 const fixedElements = document.querySelectorAll('header, nav, [class*="sticky"], [class*="fixed"]'); 5030 let maxHeight = CONFIG.offset; 5031 5032 fixedElements.forEach(function(el) { 5033 const styles = window.getComputedStyle(el); 5034 const position = styles.position; 5035 5036 if (position === 'fixed' || position === 'sticky') { 5037 const rect = el.getBoundingClientRect(); 5038 // Only consider elements at top of page 5039 if (rect.top < 100) { 5040 maxHeight = Math.max(maxHeight, rect.height + 20); 5041 } 5042 } 5043 }); 5044 5045 return maxHeight; 5046 } 5047 5048 /** 5049 * Find target element by ID or data attribute 5050 * Handles cases where IDs were modified during conversion 5051 */ 5052 function findTarget(hash) { 5053 // Remove # from hash 5054 const id = hash.replace('#', ''); 5055 if (!id) return null; 5056 5057 // Try direct ID match 5058 let target = document.getElementById(id); 5059 if (target) return target; 5060 5061 // Try case-insensitive match (IDs might have been lowercased) 5062 const elements = document.querySelectorAll('[id]'); 5063 for (let i = 0; i < elements.length; i++) { 5064 if (elements[i].id.toLowerCase() === id.toLowerCase()) { 5065 return elements[i]; 5066 } 5067 } 5068 5069 // Try data-section attribute (common in converted SPAs) 5070 target = document.querySelector('[data-section="' + id + '"]'); 5071 if (target) return target; 5072 5073 // Try finding by heading text content (last resort) 5074 const headings = document.querySelectorAll('h1, h2, h3'); 5075 const searchText = id.replace(/-/g, ' ').toLowerCase(); 5076 for (let i = 0; i < headings.length; i++) { 5077 const headingText = headings[i].textContent.toLowerCase(); 5078 if (headingText.includes(searchText)) { 5079 return headings[i]; 5080 } 5081 } 5082 5083 return null; 5084 } 5085 5086 /** 5087 * Handle anchor link clicks 5088 */ 5089 function handleAnchorClick(e) { 5090 // EC-EDITOR-005: Skip when WPConvert editor is active to avoid scroll conflict 5091 if (window.location.search.indexOf('wpc_edit=1') !== -1) return; 5092 5093 const href = this.getAttribute('href'); 5094 5095 // Only handle anchor links (starting with #) 5096 if (!href || !href.startsWith('#')) return; 5097 5098 // Skip empty hashes 5099 if (href === '#' || href === '#!') return; 5100 5101 const target = findTarget(href); 5102 5103 if (target) { 5104 e.preventDefault(); 5105 5106 // Get dynamic header offset 5107 const offset = getHeaderOffset(); 5108 5109 // Smooth scroll to target 5110 smoothScrollTo(target, CONFIG.duration, offset, CONFIG.easing); 5111 5112 // Update focus for accessibility 5113 target.setAttribute('tabindex', '-1');
5114 target.focus(); 5115 } else { 5116 // Target not found - log warning but don't break 5117 console.warn('WPConvert Smooth Scroll: Target not found for', href); 5118 } 5119 } 5120 5121 /** 5122 * Initialize smooth scroll on page load 5123 */ 5124 function init() { 5125 // Handle all anchor links 5126 const anchorLinks = document.querySelectorAll('a[href^="#"]'); 5127 anchorLinks.forEach(function(link) { 5128 link.addEventListener('click', handleAnchorClick); 5129 }); 5130 5131 // CONSERVATIVE FALLBACK: Handle buttons with explicit data attributes 5132 // Only activates if button has data-scroll-to or similar attributes 5133 const scrollButtons = document.querySelectorAll('button[data-scroll-to], button[data-anchor]'); 5134 scrollButtons.forEach(function(btn) { 5135 const scrollTarget = btn.getAttribute('data-scroll-to') || btn.getAttribute('data-anchor'); 5136 if (scrollTarget) { 5137 btn.addEventListener('click', function(e) { 5138 const targetId = scrollTarget.replace('#', ''); 5139 const target = document.getElementById(targetId); 5140 if (target) { 5141 e.preventDefault(); 5142 const offset = getHeaderOffset(); 5143 smoothScrollTo(target, CONFIG.duration, offset, CONFIG.easing); 5144 console.log('WPConvert Smooth Scroll: Button scrolled to #' + targetId); 5145 } 5146 }); 5147 } 5148 }); 5149 5150 // Handle hash in URL on page load 5151 if (window.location.hash) { 5152 // Small delay to ensure page is fully loaded 5153 setTimeout(function() { 5154 const target = findTarget(window.location.hash); 5155 if (target) { 5156 const offset = getHeaderOffset(); 5157 smoothScrollTo(target, CONFIG.duration, offset, CONFIG.easing); 5158 } 5159 }, 100); 5160 } 5161 } 5162 5163 // Initialize when DOM is ready 5164 if (document.readyState === 'loading') { 5165 document.addEventListener('DOMContentLoaded', init); 5166 } else { 5167 init(); 5168 } 5169 5170 })();</script>
5170 <style id="wpconvert-carousel-visibility-footer">.scroll-fade-up:has([aria-roledescription="carousel"]), 5171 .scroll-fade-up:has([data-wpconvert-carousel]), 5172 .scroll-fade-up:has([data-wpconvert-blog-filter-bar]) { 5173 opacity: 1 !important; 5174 transform: none !important; 5175 visibility: visible !important; 5176 } 5177 [role="region"][aria-roledescription="carousel"], 5178 [data-wpconvert-carousel="aria"] { 5179 opacity: 1 !important; 5180 visibility: visible !important; 5181 }</style>
5181<script defer src="https://www.choosetosucceed.org/wp-content/cache/autoptimize/js/autoptimize_e2784d36383430c65bb2b3325d08b990.js"></script>
5181</body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.