1/** 2 * header-attorney-cta.js â Universal "Contact an attorney" header CTA 3 * 4 * Self-bootstrapping. On load, injects a neutral, directory-style 5 * "Contact an attorney" button next to the site brand/wordmark in the 6 * header, plus an embedded attorney-connect wizard modal (the same flow 7 * served by /connect-with-attorney.html?embed=1). Guest-facing; no auth. 8 * 9 * Mirrors the app's entry point (the round-robin attorney-connect flow), 10 * surfaced as a persistent header CTA rather than a buried link. 11 * 12 * Brand detection covers every header variant on the site: 13 * - static .tcd-wordmark (phase-1 skinned nav, ~32 pages) 14 * - async #app-nav .nav-logo (nav.js renderPublicNav, calculators/tools) 15 * - misc .nav-logo / .wordmark 16 * A MutationObserver handles navs injected after initial load. 17 * 18 * The brand + button are wrapped in an inline-flex group so the button 19 * sits immediately beside the brand without disturbing the header's 20 * space-between layout (links / audience switcher stay in the right slot). 21 * 22 * Suppression: set `<body data-no-attorney-cta="true">` on any page that 23 * shouldn't get the CTA (e.g. the connect wizard itself, attorney-facing 24 * recruitment pages, authenticated dashboards). 25 * 26 * Styling uses the active skin tokens (var(--skin-*)) with safe fallbacks 27 * so it adapts to the worker/attorney skins and degrades on unskinned pages. 28 */ 29(function injectAttorneyCta() { 30 'use strict'; 31 32 var LABEL = 'Contact an attorney'; 33 // Neutral balance-scale glyph (currentColor) â reads "legal", not salesy. 34 var ICON = 35 '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" ' + 36 'stroke="currentColor" stroke-width="1.7" stroke-linecap="round" ' + 37 'stroke-linejoin="round" aria-hidden="true" focusable="false">' + 38 '<path d="M12 3v18M7 21h10M5 7h14M12 7l-4 6a3 3 0 0 0 6 0l-4-6zm0 0l4 6a3 3 0 0 1-6 0l4-6z"/>' + 39 '</svg>'; 40 41 // Header/nav-scoped brand selectors first, so a generic `.logo` elsewhere 42 // on the page never wins over the real header brand. Covers every header 43 // variant on the site: skinned nav, renderPublicNav, and the legacy navs. 44 var BRAND_SELECTORS = [ 45 '.tcd-wordmark', 46 '#app-nav .nav-logo', 47 'nav .nav-logo', 'nav .nav-brand', 'nav .logo', 'nav .wordmark', 48 'header .nav-logo', 'header .nav-brand', 'header .logo', 'header .wordmark', 49 '.nav-logo', '.nav-brand', '.wordmark', '.logo' 50 ]; 51 52 function suppressed() { 53 return document.body && document.body.getAttribute('data-no-attorney-cta') === 'true'; 54 } 55 56 // ââ Embed src â carry UTM params through to the wizard ââââââââââââââ 57 function buildEmbedSrc() { 58 var qs = new URLSearchParams(window.location.search); 59 var out = new URLSearchParams(); 60 out.set('embed', '1'); 61 ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'].forEach(function (k) { 62 if (qs.get(k)) out.set(k, qs.get(k)); 63 }); 64 return '/connect-with-attorney.html?' + out.toString(); 65 } 66 67 // ââ Modal (built lazily on first open) ââââââââââââââââââââââââââââââ 68 var overlay = null; 69 var frame = null; 70 71 function buildModal() { 72 if (overlay) return; 73 overlay = document.createElement('div'); 74 overlay.setAttribute('data-tcd', 'attorney-connect-overlay'); 75 overlay.setAttribute('role', 'dialog'); 76 overlay.setAttribute('aria-modal', 'true'); 77 overlay.setAttribute('aria-label', 'Connect with a Workersâ Compensation Attorney'); 78 overlay.style.cssText = 79 'display:none;position:fixed;inset:0;z-index:100000;' + 80 'background:rgba(7,16,31,0.72);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);'; 81 82 var closeBtn = document.createElement('button'); 83 closeBtn.type = 'button'; 84 closeBtn.setAttribute('aria-label', 'Close'); 85 closeBtn.innerHTML = '×'; 86 closeBtn.style.cssText = 87 'position:fixed;top:16px;right:16px;z-index:100001;cursor:pointer;' + 88 'width:40px;height:40px;border-radius:8px;font-size:24px;line-height:1;' + 89 'display:flex;align-items:center;justify-content:center;' + 90 'background:rgba(20,28,44,0.92);border:1px solid rgba(255,255,255,0.18);color:#fff;'; 91 closeBtn.addEventListener('click', closeModal); 92 93 frame = document.createElement('iframe'); 94 frame.title = 'Connect with a Workersâ Compensation Attorney'; 95 frame.setAttribute('allow', 'clipboard-write'); 96 frame.setAttribute('referrerpolicy', 'same-origin'); 97 frame.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:0;background:transparent;'; 98 99 overlay.appendChild(closeBtn); 100 overlay.appendChild(frame); 101 overlay.addEventListener('click', function (e) { if (e.target === overlay) closeModal(); }); 102 document.body.appendChild(overlay); 103 } 104 105 function openModal() { 106 buildModal(); 107 var src = buildEmbedSrc(); 108 if (frame.getAttribute('src') !== src) frame.setAttribute('src', src); 109 overlay.style.display = 'block'; 110 document.body.style.overflow = 'hidden'; 111 } 112 113 function closeModal() { 114 if (!overlay) return; 115 overlay.style.display = 'none'; 116 document.body.style.overflow = ''; 117 if (frame) frame.setAttribute('src', 'about:blank'); 118 } 119 120 document.addEventListener('keydown', function (e) { 121 if (e.key === 'Escape' && overlay && overlay.style.display === 'block') closeModal(); 122 }); 123 window.addEventListener('message', function (e) { 124 if (e && e.data && e.data.type === 'compdesk:closeConnectMod
124al') closeModal(); 125 }); 126 127 // Expose for any page that wants to trigger the same flow. 128 window.tcdOpenAttorneyConnect = openModal; 129 130 // ââ Button ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 131 function buildButton() { 132 var btn = document.createElement('button'); 133 btn.type = 'button'; 134 btn.setAttribute('data-tcd', 'attorney-cta'); 135 btn.setAttribute('aria-label', LABEL); 136 btn.innerHTML = ICON + '<span>' + LABEL + '</span>'; 137 btn.style.cssText = [ 138 'display:inline-flex', 'align-items:center', 'gap:7px', 139 'padding:8px 15px', 'border-radius:999px', 140 'font-family:inherit', 'font-weight:600', 'font-size:13.5px', 'line-height:1', 141 'white-space:nowrap', 'cursor:pointer', 142 'background:transparent', 143 'color:var(--skin-text, #f4f6fa)', 144 'border:1.5px solid var(--skin-divider, rgba(255,255,255,0.20))', 145 'transition:background .18s ease, border-color .18s ease, transform .18s ease' 146 ].join(';') + ';'; 147 btn.addEventListener('mouseenter', function () { 148 btn.style.background = 'var(--skin-surface-elev, rgba(255,255,255,0.07))'; 149 btn.style.borderColor = 'var(--skin-accent, #4f8ff7)'; 150 btn.style.transform = 'translateY(-1px)'; 151 }); 152 btn.addEventListener('mouseleave', function () { 153 btn.style.background = 'transparent'; 154 btn.style.borderColor = 'var(--skin-divider, rgba(255,255,255,0.20))'; 155 btn.style.transform = 'none'; 156 }); 157 btn.addEventListener('click', openModal); 158 return btn; 159 } 160 161 function findBrand() { 162 for (var i = 0; i < BRAND_SELECTORS.length; i++) { 163 var el = document.querySelector(BRAND_SELECTORS[i]); 164 if (el) return el; 165 } 166 return null; 167 } 168 169 function placeButton() { 170 if (suppressed()) return true; // treat as "done" â nothing to place 171 if (document.querySelector('[data-tcd="attorney-cta"]')) return true; // idempotent 172 var brand = findBrand(); 173 if (!brand || !brand.parentNode) return false; 174 175 // Wrap brand + button together so the button stays beside the brand 176 // inside a space-between header (links / switcher keep the right slot). 177 var group = document.createElement('span'); 178 group.setAttribute('data-tcd', 'attorney-cta-group'); 179 group.style.cssText = 'display:inline-flex;align-items:center;gap:14px;min-width:0;'; 180 brand.parentNode.insertBefore(group, brand); 181 group.appendChild(brand); 182 group.appendChild(buildButton()); 183 return true; 184 } 185 186 function start() { 187 if (placeButton()) return; 188 // Header not in the DOM yet (e.g. nav.js renderPublicNav runs async). 189 // Watch for it, then place once and stop. 190 var obs = new MutationObserver(function () { 191 if (placeButton()) obs.disconnect(); 192 }); 193 obs.observe(document.body, { childList: true, subtree: true }); 194 // Safety: stop observing after 10s regardless. 195 setTimeout(function () { obs.disconnect(); }, 10000); 196 } 197 198 if (document.readyState === 'loading') { 199 document.addEventListener('DOMContentLoaded', start); 200 } else { 201 start(); 202 } 203})();
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.