1/* LingoVoice LUMEN â shared interactions */ 2/* Sign-up source (APP-12): a visitor from a QR code or campaign lands on a 3 marketing page first, and the source used to be lost by the time they 4 reached /app. Store it for this tab; /app keeps the first one it finds. */ 5(function(){ 6 try{ 7 if(sessionStorage.getItem('lv_attribution'))return; 8 var p=new URLSearchParams(location.search),a={},has=false; 9 ['utm_source','utm_medium','utm_campaign','utm_content','utm_term'].forEach(function(k){if(p.get(k)){a[k]=p.get(k);has=true}}); 10 if(document.referrer&&document.referrer.indexOf(location.hostname)===-1){a.referrer=document.referrer;has=true} 11 if(!has)return; 12 a.landing_page=location.pathname+location.search; 13 sessionStorage.setItem('lv_attribution',JSON.stringify(a)); 14 }catch(e){} 15})(); 16(function(){ 17 var reduce = window.matchMedia('(prefers-reduced-motion:reduce)').matches; 18 var sleep = function(ms){return new Promise(function(r){setTimeout(r,ms)})}; 19 20 /* ---- live caption stream (only if a room stage exists) ---- */ 21 var stage = document.getElementById('stage'); 22 if(stage){ 23 var latEl = document.getElementById('lat'); 24 /* three rotating scenarios: healthcare, council services, business */ 25 var scripts = [ 26 [ 27 {cls:'a',av:'AM',nm:'Amina',lg:'Pashto',oL:'ps',tL:'en',orig:'ÚØ§Ú©Ù¼Ø±ÙØ Ø²Ù Ø¯Ø±Û ÙØ±ÚÛ Ù٠سرخÙÚ Ú Ø®Ù Ú©ÚÛÚÙ ',origAr:true,trans:"Doctor, I've had a headache for three days.",transAr:false,lat:'1.9s'}, 28 {cls:'b',av:'DH',nm:'Dr Hughes',lg:'English',oL:'en',tL:'ps',orig:'I understand. Any fever, nausea, or medication?',origAr:false,trans:'ز٠پÙÙÛÚÙ . Ø¢ÛØ§ ØªØ¨ÙØ زÚÙ Ø¨Ø¯ÙØ§ÙÛ ÛØ§ Ú©ÙÙ Ø¯Ø±Ù Ù Ø´ØªÙØ',transAr:true,lat:'1.7s'}, 29 {cls:'a',av:'AM',nm:'Amina',lg:'Pashto',oL:'ps',tL:'en',orig:'ÙÙ ØªØ¨ÙØ خ٠زÚÙ Ù Û Ø¨Ø¯ÛÚÙ',origAr:true,trans:'No fever, but I feel nauseous.',transAr:false,lat:'1.8s'} 30 ], 31 [ 32 {cls:'a',av:'OM',nm:'Oksana',lg:'Ukrainian',oL:'uk',tL:'en',orig:'ÐобÑого днÑ, Ñ Ð´Ð·Ð²Ð¾Ð½Ñ Ñодо ÑвоÑÑ Ð·Ð°Ñвки на жиÑло',origAr:false,trans:"Hello, I'm calling about my housing application.",transAr:false,lat:'1.8s'}, 33 {cls:'b',av:'SC',nm:'Sam Carter',lg:'English',oL:'en',tL:'uk',orig:'Of course. Could you give me your reference number?',origAr:false,trans:'ÐвиÑайно. ÐазвÑÑÑ, бÑÐ´Ñ Ð»Ð°Ñка, Ð½Ð¾Ð¼ÐµÑ Ð²Ð°ÑÐ¾Ñ Ð·Ð°Ñвки.',transAr:false,lat:'1.6s'}, 34 {cls:'a',av:'OM',nm:'Oksana',lg:'Ukrainian',oL:'uk',tL:'en',orig:'Так, Ð½Ð¾Ð¼ÐµÑ Ð·Ð°Ñвки 2284',origAr:false,trans:'Yes, the reference number is 2284.',transAr:false,lat:'1.7s'} 35 ], 36 [ 37 {cls:'a',av:'LW',nm:'Li Wei',lg:'Mandarin',oL:'zh',tL:'en',orig:'ç¬¬ä¸æ¹è´§å¨äºä¹åå¯ä»¥ååº',origAr:false,trans:'The first shipment can go out by Friday.',transAr:false,lat:'1.9s'}, 38 {cls:'b',av:'ER',nm:'Emma Reid',lg:'English',oL:'en',tL:'zh',orig:'Perfect. And what are the payment terms?',origAr:false,trans:'太好äºã仿¬¾æ¡ä»¶æ¯ææ ·çï¼',transAr:false,lat:'1.7s'}, 39 {cls:'a',av:'LW',nm:'Li Wei',lg:'Mandarin',oL:'zh',tL:'en',orig:'ç¾åä¹ä¸åé¢ä»ï¼å ¶ä½è´§å°åç»æ¸ ',origAr:false,trans:'Thirty percent upfront, the balance on delivery.',transAr:false,lat:'2.0s'} 40 ] 41 ]; 42 var turns = scripts[0]; 43 /* Plain-language pair label per scenario so the room header always matches 44 the conversation on screen (the old static "PS -> EN" never did). */ 45 var pairs = ['Pashto \u21C4 English', 'Ukrainian \u21C4 English', 'Mandarin \u21C4 English']; 46 var pairEl = document.getElementById('hd-pair'); 47 var setPair = function(i){ if(pairEl) pairEl.textContent = pairs[i % pairs.length]; }; 48 setPair(0); 49 var mk = function(tag,cls,text){ var n=document.createElement(tag); if(cls)n.className=cls; if(text!=null)n.textContent=text; return n; }; 50 var turnNode = function(t){ 51 var el=mk('div','turn '+t.cls); 52 var spk=mk('div','spk'); 53 spk.appendChild(mk('span','av '+t.cls,t.av)); 54 spk.appendChild(mk('span','nm',t.nm)); 55 spk.appendChild(mk('span','lg',t.lg)); 56 var sp=mk('span','speaking'); sp.appendChild(mk('span','pulse live')); sp.appendChild(document.createTextNode('speaking')); 57 spk.appendChild(sp); 58 el.appendChild(spk); 59 var oNode=mk('div','orig'+(t.origAr?' ar':'')); if(t.oL && t.oL!=='en') oNode.lang=t.oL; 60 el.appendChild(oNode); 61 var tr=mk('div','translating'); tr.style.display='none'; tr.appendChild(mk('b')); tr.appendChild(mk('b')); tr.appendChild(mk('b')); 62 el.appendChild(tr); 63 var tNode=mk('div','trans'+(t.transAr?' ar':'')); if(t.tL && t.tL!=='en') tNode.lang=t.tL; 64 el.appendChild(tNode); 65 return el; 66 }; 67 var typeInto = async function(node,text,withCaret){ 68 node.textContent=''; 69 var caret=document.createElement('span'); caret.className='caret'; 70 if(withCaret) node.appendChild(caret); 71 for(var i=0;i<text.length;i++){ 72 node.insertBefore(document.createTextNode(text[i]), withCaret?caret:null); 73 await sleep(14+Math.random()*24); 74 } 75 if(withCaret) caret.remove(); 76 }; 77 /* keep the room looking mid-conversation: a faded prior turn instead of an empty void */ 78 var toPrior=function(node){ 79 if(!node) return; 80 node.classList.add('prior'); 81 var sp=node.querySelector('.speaking'); if(sp) sp.remove(); 82 var tr=node.querySelector('.translating'); if(tr) tr.remove(); 83 }; 84 var seed=turnNode(turns[turns.length-1]); 85 seed.querySelector('.orig').textContent=turns[turns.length-1].orig; 86 seed.querySelector('.trans').textContent=turns[turns.length-1].trans; 87 seed.classList.add('show'); 88 toPrior(seed); 89 stage.appendChild(seed); 90 /* the live hero demo (hero-demo.js) can take this stage over on first tap */ 91 var stageStopped=false; 92 window.lvStopStageLoop=function(){ stageStopped=true; }; 93 (async function run(){ 94 var loopN=0; 95 while(true){ 96 if(stageStopped) return; 97 var idx=loopN++%scripts.length; 98 turns=scripts[idx]; 99 /* Each scenario is its own fresh, coherent conversation. Fade the old 100 one out, clear it (never leave a different room's message behind), 101 then set the header to this scene's languages. */ 102 if(!reduce && stage.firstChild){ stage.style.transition='opacity .35s ease'; stage.style.opacity='0'; await sleep(360); } 103 if(stageStopped) return; 104 while(stage.firstChild){ stage.removeChild(stage.firstChild); } 105 setPair(idx); 106 stage.style.opacity='1'; 107 for(var k=0;k<turns.length;k++){ 108 if(stageStopped) return; 109 var t=turns[k], node=turnNode(t); 110 if(stageStopped) return; 111 stage.appendChild(node); 112 var origEl=node.querySelector('.orig'), trEl=node.querySelector('.translating'), transEl=node.querySelector('.trans'); 113 /* Opening line of each scenario appears INSTANTLY (already-said context) 114 so the room is never a single lonely bubble with a big void above it. */ 115 if(k===0 && !reduce){ 116 /* place the opening line instantly and fall straight through to the 117 next turn (no await) so the column is never a single lonely bubble */ 118 (function(n){requestAnimationFrame(function(){n.classList.add('show')})})(node); 119 origEl.textContent=t.orig; transEl.textContent=t.trans; 120 if(latEl)latEl.textContent=t.lat; 121 continue; 122 } 123 (function(n){requestAnimationFrame(function(){n.classList.add('show','talking')}
123)})(node); 124 if(reduce){ origEl.textContent=t.orig; transEl.textContent=t.trans; if(latEl)latEl.textContent=t.lat; await sleep(1600); continue; } 125 await typeInto(origEl,t.orig,true); 126 if(stageStopped) return; 127 await sleep(240); 128 trEl.style.display='inline-flex'; await sleep(800); trEl.style.display='none'; 129 if(latEl)latEl.textContent=t.lat; 130 if(stageStopped) return; 131 await typeInto(transEl,t.trans,true); 132 node.classList.remove('talking'); 133 /* keep the immediately-prior turn (same conversation) faded for context */ 134 while(stage.children.length>2){ stage.removeChild(stage.firstChild); } 135 toPrior(stage.firstChild); 136 await sleep(1500); 137 } 138 await sleep(1100); 139 } 140 })(); 141 } 142 143 /* ---- nav scroll shadow ---- */ 144 var nav=document.getElementById('nav'); 145 if(nav){ window.addEventListener('scroll',function(){ nav.classList.toggle('scrolled', window.scrollY>8) }); } 146 147 /* ---- mobile nav: hamburger + sheet (built from existing links) ---- */ 148 (function(){ 149 if(!nav) return; 150 var navIn=nav.querySelector('.nav-in'), links=nav.querySelector('.nav-links'), cta=nav.querySelector('.nav-cta'); 151 if(!navIn||!links) return; 152 var mk=function(tag,cls){var n=document.createElement(tag); if(cls)n.className=cls; return n;}; 153 154 var burger=mk('button','nav-burger'); 155 burger.type='button'; 156 burger.setAttribute('aria-label','Open menu'); 157 burger.setAttribute('aria-expanded','false'); 158 burger.setAttribute('aria-controls','navSheet'); 159 for(var i=0;i<3;i++){ burger.appendChild(mk('span')); } 160 161 var sheet=mk('div','nav-sheet'); sheet.id='navSheet'; 162 links.querySelectorAll('a').forEach(function(a){ sheet.appendChild(a.cloneNode(true)); }); 163 var row=mk('div','nav-sheet-cta'); 164 var signin=cta&&cta.querySelector('.signin'); if(signin){ row.appendChild(signin.cloneNode(true)); } 165 var start=cta&&cta.querySelector('.btn'); if(start){ var s2=start.cloneNode(true); s2.classList.remove('btn--sm'); row.appendChild(s2); } 166 if(row.children.length) sheet.appendChild(row); 167 168 var scrim=mk('div','nav-scrim'); 169 170 var setOpen=function(open){ 171 nav.classList.toggle('open',open); 172 document.body.classList.toggle('nav-open',open); 173 burger.setAttribute('aria-expanded',open?'true':'false'); 174 burger.setAttribute('aria-label',open?'Close menu':'Open menu'); 175 }; 176 burger.addEventListener('click',function(){ setOpen(!nav.classList.contains('open')); }); 177 scrim.addEventListener('click',function(){ setOpen(false); }); 178 sheet.addEventListener('click',function(e){ if(e.target.closest('a')) setOpen(false); }); 179 document.addEventListener('keydown',function(e){ if(e.key==='Escape') setOpen(false); }); 180 window.addEventListener('resize',function(){ if(window.innerWidth>940) setOpen(false); }); 181 182 if(cta){ cta.appendChild(burger); } else { navIn.appendChild(burger); } 183 nav.appendChild(scrim); 184 nav.appendChild(sheet); 185 })(); 186 187 /* ---- spotlight caption strip: rotate language pairs (gated to viewport) ---- */ 188 document.querySelectorAll('.spot-cap[data-rotate]').forEach(function(cap){ 189 var pairs=[ 190 {ln:'DzieÅ dobry, mam umówionÄ wizytÄ o dziewiÄ tej', big:'Good morning, I have an appointment at nine', ar:false}, 191 {ln:'ÐÐµÐ½Ñ Ð¿Ð¾ÑÑÑбен пеÑеклад ÑÑого докÑменÑа', big:'I need this document translated', ar:false}, 192 {ln:'ææ³å¨è¯¢ä¸ä¸ç¾è¯çäºæ ', big:'I would like to ask about my visa', ar:false} 193 ]; 194 if(reduce) return; 195 var live=cap.querySelector('.cap-live')||cap; 196 var i=0, lnEl=live.querySelector('.ln'), bigEl=live.querySelector('.big'), cur=bigEl.querySelector('.cur'); 197 var visible=false, timer=null; 198 var show=function(p){ 199 lnEl.textContent=p.ln; 200 lnEl.classList.toggle('ar',p.ar); 201 bigEl.textContent=p.big; 202 if(cur) bigEl.appendChild(cur); 203 }; 204 var tick=function(){ 205 cap.classList.add('swap'); 206 setTimeout(function(){ i=(i+1)%pairs.length; show(pairs[i]); cap.classList.remove('swap'); },460); 207 };
208 var io2=new IntersectionObserver(function(es){ es.forEach(function(e){ 209 visible=e.isIntersecting; 210 if(visible && !timer){ timer=setInterval(tick,4600); } 211 if(!visible && timer){ clearInterval(timer); timer=null; } 212 }) },{threshold:.3}); 213 io2.observe(cap); 214 }); 215 216 /* ---- reveals + how-it-works thread ---- */ 217 var io=new IntersectionObserver(function(es){ es.forEach(function(e){ if(e.isIntersecting){ e.target.classList.add('in'); 218 var th=e.target.querySelector&&e.target.querySelector('#thread'); if(th){ th.style.width='66.666%'; } 219 io.unobserve(e.target); } }) }, {threshold:.16}); 220 document.querySelectorAll('.reveal, .darkband .scn').forEach(function(n){ io.observe(n) }); 221 setTimeout(function(){ document.querySelectorAll('.reveal:not(.in), .darkband .scn:not(.in)').forEach(function(n){ n.classList.add('in') }) }, 2400); 222 223 /* ---- generic form submit (contact etc.) -> data-endpoint as JSON ---- */ 224 document.querySelectorAll('form[data-endpoint]').forEach(function(form){ 225 form.addEventListener('submit', async function(e){ 226 e.preventDefault(); 227 var btn=form.querySelector('button[type="submit"]'); 228 var status=form.querySelector('.form-status'); 229 var orig=btn?btn.textContent:''; 230 if(btn){ btn.disabled=true; btn.textContent='Sending'; } 231 if(status){ status.className='form-status'; } 232 try{ 233 var data=Object.fromEntries(new FormData(form)); 234 var res=await fetch(form.getAttribute('data-endpoint'),{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)}); 235 var json={}; try{ json=await res.json(); }catch(_){} 236 if(res.ok){ 237 if(status){ status.className='form-status success'; status.textContent="Thanks, message received. We will reply within one working day."; } 238 form.reset(); 239 } else { 240 if(status){ status.className='form-status error'; status.textContent=(json&&json.error)||'Something went wrong. Please try again.'; } 241 } 242 }catch(err){ 243 if(status){ status.className='form-status error'; status.textContent='Network error. Please try again.'; } 244 } 245 if(btn){ btn.disabled=false; btn.textContent=orig; } 246 }); 247 }); 248 249 /* ---- faq accordion ---- */ 250 document.querySelectorAll('.qa button').forEach(function(b){ 251 b.setAttribute('aria-expanded','false'); 252 b.addEventListener('click',function(){ 253 var qa=b.parentElement, open=qa.classList.contains('open'); 254 document.querySelectorAll('.qa').forEach(function(x){ x.classList.remove('open'); x.querySelector('.ans').style.maxHeight=null; 255 var xb=x.querySelector('button'); if(xb) xb.setAttribute('aria-expanded','false'); }); 256 if(!open){ qa.classList.add('open'); var ans=qa.querySelector('.ans'); ans.style.maxHeight=ans.scrollHeight+'px'; b.setAttribute('aria-expanded','true'); } 257 }); 258 }); 259})();
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.