Learn to taste like you mean it. Spin through aroma wheels built from real tasting descriptors, land on a term, and learn what it actually means — the exact vocabulary you need on the floor and in the room for your certification.
LearnSomm
Please sign in to continue
build r296 · 2026-10-14
No wheel loadedPick a wheel, or drop a wheel here
pairing
No wheel loadedPick a wheel, or drop a wheel here
Design
2D changes go to the wheel picked under “Designing”; font/color/size/border also apply to 3D. Alternate colors here are 2D — in 3D use each ring’s panel.
Add to flashcard'\n"
+ " +''\n"
+ " +''\n"
+ " +'';\n"
+ " pop.style.left=Math.max(4,Math.min(e.clientX, (window.innerWidth-232)))+'px';\n"
+ " pop.style.top=Math.max(4,Math.min(e.clientY, (window.innerHeight-124)))+'px';\n"
+ " document.body.appendChild(pop);\n"
+ " try{ pop.querySelector('.lsfc-f').focus(); }catch(_){}\n"
+ " pop.querySelector('.lsfc-save').addEventListener('click', function(){\n"
+ " try{\n"
+ " var KEY='wow_flashcards_v1';\n"
+ " var arr=JSON.parse(localStorage.getItem(KEY)||'[]');\n"
+ " arr.push({f:pop.querySelector('.lsfc-f').value.trim(), b:pop.querySelector('.lsfc-b').value.trim(), img:(img.currentSrc||img.src)});\n"
+ " localStorage.setItem(KEY, JSON.stringify(arr));\n"
+ " }catch(_){}\n"
+ " close();\n"
+ " });\n"
+ " setTimeout(function(){ document.addEventListener('pointerdown', function h(ev){ if(pop&&!pop.contains(ev.target)){ close(); document.removeEventListener('pointerdown',h,true); } }, true); }, 30);\n"
+ " });\n"
+ "})();";
(doc.body||doc.documentElement).appendChild(script);
}catch(_){}
}
function mountSlot(s){ if(!s._mounted){ s._mounted=true; try{ s.iframe.srcdoc=ENGINE_HTML; }catch(e){} } } /* lazy: engines parse only when a slot first loads a wheel */
let MODE='2d';
/* worlds for 3D mode (only the wine opener for now) + Build-your-own */
const WORLD_BGS=[]; const STORY_CHAPTERS=[{id:'ch1',name:'Chapter 1 \u2014 Coffee & Breakfast',bg:'chicago-cafe'},{id:'ch2',name:'Chapter 2 \u2014 The Raw Bar',bg:'story-raw-bar'},{id:'ch3',name:'Chapter 3 \u2014 The Kitchen & Cellar',bg:'story-kitchen-cellar'},{id:'ch5',name:'Chapter 4 \u2014 The Chef\u2019s Table',bg:'story-chefs-table'}];
/* Baked-in default style per chapter. Chapters without an entry here keep
whatever style is already active. r251: Chapter 1's look is its world style
(worlds-engine WOW_WORLD_STYLES['chicago-cafe']), so no preset is laid over it. */
const STORY_CHAPTER_STYLE={}; /* r263: every chapter's look is its baked world style */
function applyChapterExtras(chId){
try{
var mk=STORY_CHAPTER_STYLE[chId]; if(mk){ wowSend('WOW_APPLY_PRESET', mk()); }
/* r211: chapters no longer start music on their own */
}catch(_){}
} const WORLDS=WORLD_BGS.map(function(w){return {id:'world:'+w.bg,name:w.name};});
const BUILD_SRC='world-of-wine-opener_199_.html';
/* ── wheel selects (top nav) ── */
function fillWheelSel(sel, emptyLabel){
sel.innerHTML='';
const b=document.createElement('option'); b.value=''; b.textContent=emptyLabel; sel.appendChild(b);
(function(){var __catOrder=['lsfood','lsbeverage','lsgrapes','lssomm','lscuisine'];return __catOrder.filter(function(k){return WHEELS[k];}).concat(Object.keys(WHEELS).filter(function(k){return __catOrder.indexOf(k)<0;}));})().forEach(g=>{
const og=document.createElement('optgroup'); og.label=GROUP_TITLES[g]||g;
WHEELS[g].forEach(w=>{ const o=document.createElement('option'); o.value=w.id; o.textContent=w.name; og.appendChild(o); });
sel.appendChild(og);
});
[].forEach.call(sel.querySelectorAll('optgroup'),function(g){ if(/^(subjects|wine\s*&\s*beverage)$/i.test((g.label||'').trim())) g.remove(); }); }
const wheel1=document.getElementById('wheel1'), wheel2=document.getElementById('wheel2');
fillWheelSel(wheel1,'— choose wheel —');
fillWheelSel(wheel2,'— choose wheel —');
const f1=document.createElement('option'); f1.value='__file'; f1.textContent='Load from your computer…'; wheel1.appendChild(f1);
const f2=document.createElement('option'); f2.value='__file'; f2.textContent='Load from your computer…'; wheel2.appendChild(f2);
wheel1.addEventListener('change', ()=> loadById(slots[0], wheel1.value));
wheel2.addEventListener('change', ()=> loadById(slots[1], wheel2.value));
/* ── world + ring selects (3D) ── */
const worldPick=document.getElementById('worldPick'), ringDrops=document.getElementById('ringDrops');
/* r210: whatever path picked the world (mouse on the native list, keys, a story
chapter, a file load), applying it closes the Worlds menu and lets go of focus */
function lsCloseWorldPop(){ try{ var wp=document.getElementById('worldPop'); if(wp) wp.style.display='none'; var a=document.activeElement; if(a && (a.id==='worldPick'||a.id==='storyPick')) a.blur(); }catch(_){ } }
['input','change'].forEach(function(t){ try{ worldPick.addEventListener(t, function(){ setTimeout(lsCloseWorldPop, 0); }); }catch(_){ } });
function applyWorldOrStory(){ try{
var _wpv=(typeof worldPick!=='undefined'&&worldPick)?worldPick.value:''; if(_wpv && _wpv.indexOf('story:')===0){ var _cid=_wpv.slice(6); var _sch=STORY_CHAPTERS.filter(function(c){return c.id===_cid;})[0]||STORY_CHAPTERS[0]; wowSend('WOW_SET_BG', _sch.bg); setTimeout(function(){applyChapterExtras(_sch.id);}, 60); toast('Story \u00b7 '+_sch.name); return; }
if(MODE==='story'){ var sp=document.getElementById('storyPick'); var id=sp&&sp.value; var ch=STORY_CHAPTERS.filter(function(c){return c.id===id;})[0]||STORY_CHAPTERS[0]; wowSend('WOW_SET_BG', ch.bg); setTimeout(function(){applyChapterExtras(ch.id);}, 60); toast('Story · '+ch.name); return; }
var v=(typeof worldPick!=='undefined'&&worldPick)?worldPick.value:'';
if(v && v.indexOf('world:')===0){ wowSend('WOW_SET_BG', v.slice(6)); toast('World loaded'); }
else { toast('3D controls linked'); }
}catch(_){ } }
(function fillWorld(){
/* r249: Worlds lists the Story chapters only */
worldPick.innerHTML='';
STORY_CHAPTERS.forEach(function(c){ var o=document.createElement('option'); o.value='story:'+c.id; o.textContent=c.name; worldPick.appendChild(o); });
})();
(function fillStory(){ var sp=document.getElementById('storyPick'); if(!sp)return; sp.innerHTML=''; STORY_CHAPTERS.forEach(function(c){ var o=document.createElement('option'); o.value=c.id; o.textContent=c.name; sp.appendChild(o); }); })();
(function(){ var sp=document.getElementById('storyPick'); if(!sp)return; sp.addEventListener('change', function(){ if(WOW_READY){ applyWorldOrStory(); } else { var f=document.getElementById('frame3d'); lsWorldsSrcdoc(f); f.setAttribute('data-3d','1'); applyThemeToFrame(f); WOW_READY=false; wowExternalizeSoon(); } }); })();
['A','B','C','D'].forEach(L=>{ const s=document.getElementById('ring'+L); fillWheelSel(s,'— empty —'); s.addEventListener('change', ()=>{ if(!s.value){ try{ window.__lsGsUnmount && window.__lsGsUnmount(L); }catch(_){ } } loadRing(L, s.value); }); }); /* r222: Empty unloads the wheel from that slot completely, GPU / 3D included */
/* Scenic auto-load: on 3D boot, restore each ring's most recent wheel; if rings 1+2 (A/B) have
no history, fall back to the first wheels in the registry so the world never opens empty. */
function _allWheelIds(){ var out=[]; try{ var co=['lsfood','lsbeverage','lsgrapes','lssomm','lscuisine']; var ks=co.filter(function(k){return WHEELS[k];}).concat(Object.keys(WHEELS).filter(function(k){return co.indexOf(k)<0;})); ks.forEach(function(g){ (WHEELS[g]||[]).forEach(function(w){ out.push(w.id); }); }); }catch(e){} return out; }
[['wow3dCheat','WOW_OPEN_CHEAT'],['wow3dCards','WOW_OPEN_CARDS'],['wow3dReset','WOW_RESET_LAYOUT']].forEach(function(p){ var b=document.getElementById(p[0]); if(b) b.addEventListener('click', function(){ wowSend(p[1]); }); });
/* r116: simple Auto Spin toggle for Ring 1 / Ring 2 -- minimal text, just "1"/"2" + a spin glyph. */
(function(){
function injCSS(){
if(document.getElementById('ls-autospin-css')) return;
var st=document.createElement('style'); st.id='ls-autospin-css';
st.textContent='.as-wrap{display:inline-flex;gap:3px;align-items:center;margin:0 2px}'
+'.as-btn{cursor:pointer;border:1px solid var(--line);background:none;color:var(--muted);border-radius:7px;padding:4px 7px;font:600 12px system-ui,sans-serif;line-height:1;display:inline-flex;align-items:center;gap:3px}'
+'.as-btn[aria-pressed="true"]{background:var(--accent);color:#fff;border-color:var(--accent)}'
+'.as-btn .as-ic{display:inline-block;transition:transform .4s linear}'
+'.as-btn[aria-pressed="true"] .as-ic{animation:asSpin 2.2s linear infinite}'
+'@keyframes asSpin{to{transform:rotate(360deg)}}';
document.head.appendChild(st);
}
function keyFor(ring){ return 'ls-autospin-'+ring; }
function getState(ring){ try{ var v=localStorage.getItem(keyFor(ring)); return v===null?true:v==='1'; }catch(_){ return true; } }
function setState(ring,on){ try{ localStorage.setItem(keyFor(ring), on?'1':'0'); }catch(_){} }
function paint(btn,on){ btn.setAttribute('aria-pressed', on?'true':'false'); btn.title='Ring '+btn.dataset.ring+' auto-spin: '+(on?'On':'Off')+' (tap to toggle)'; }
function build(){
injCSS();
var host=document.getElementById('wow3dReset'); if(!host || document.getElementById('as-wrap')) return;
var wrap=document.createElement('span'); wrap.className='as-wrap'; wrap.id='as-wrap';
['A','B'].forEach(function(ring,i){
var b=document.createElement('button'); b.type='button'; b.className='as-btn'; b.dataset.ring=(i+1);
b.innerHTML='\u27f3'+(i+1);
var on=getState(ring); paint(b,on);
b.addEventListener('click', function(){
var now=b.getAttribute('aria-pressed')!=='true'; paint(b,now); setState(ring,now);
wowSend('WOW_SET_RING_SPIN', {ring:ring, on:now});
});
wrap.appendChild(b);
});
host.parentNode.insertBefore(wrap, host);
}
window.LS_SYNC_AUTOSPIN=function(){ ['A','B'].forEach(function(ring){ wowSend('WOW_SET_RING_SPIN', {ring:ring, on:getState(ring)}); }); };
function syncNavHeight(){ try{ var nav=document.querySelector('nav.nav'); if(nav) wowSend('WOW_SET_NAV_HEIGHT', Math.ceil(nav.getBoundingClientRect().height)); }catch(_){} }
window.LS_SYNC_NAV_HEIGHT=syncNavHeight;
window.addEventListener('resize', function(){ clearTimeout(window.__navHT); window.__navHT=setTimeout(syncNavHeight, 150); });
if(document.readyState!=='loading') syncNavHeight(); else document.addEventListener('DOMContentLoaded', syncNavHeight);
if(document.readyState!=='loading') build(); else document.addEventListener('DOMContentLoaded', build);
})();
/* r209: every world's settings in one file. Saves each world's look (the
per-world style the engine keeps) and its rings, plus the shared default, so
the whole set can be sent back and baked in — or loaded again here. */
(function(){
var SP='ls-worldstyle::', RP='ls-worldrings::';
function worldNames(){
var out={};
try{ Array.prototype.forEach.call(worldPick.options, function(o){ if(o.value && o.value!=='__build') out[o.value]=o.textContent; }); }catch(_){ }
return out;
}
function flushCurrent(){
try{ if(__lsWorldKey) _lsSaveWorldRings(__lsWorldKey); }catch(_){ }
try{ /* the engine autosaves after a pause; take it now */
var fw=document.getElementById('frame3d').contentWindow, d=fw.document, sel=d.getElementById('bg-photo');
if(fw.__WOW_SNAPSHOT && sel && sel.value && sel.value!=='none' && !fw.__WOW_STYLE_BUSY && fw.__lsWorldReady===sel.value)
localStorage.setItem(SP+sel.value, JSON.stringify(fw.__WOW_SNAPSHOT()));
}catch(_){ }
}
function saveAll(){
flushCurrent();
var out={_ls_all_worlds:1, saved:new Date().toISOString(), names:worldNames(), styles:{}, phoneStyles:{}, rings:{}, photoPicks:{}, defaultStyle:null};
for(var i=0;i'
+'';
document.body.appendChild(ov);
sel=ov.querySelector('select'); frame=ov.querySelector('iframe'); info=ov.querySelector('.pv-info');
DEVICES.forEach(function(d,i){ var o=document.createElement('option'); o.value=i; o.textContent=d[0]+' \u2014 '+d[1]+'\u00d7'+d[2]; sel.appendChild(o); });
try{ var sv=localStorage.getItem('ls-prev-dev'); if(sv!==null) sel.value=sv; }catch(_){ }
sel.addEventListener('change', function(){ try{ localStorage.setItem('ls-prev-dev', sel.value); }catch(_){ } load(); });
ov.querySelector('.pv-reload').addEventListener('click', load);
function engine(){ try{ return frame.contentWindow.document.getElementById('frame3d').contentWindow; }catch(_){ return null; } }
ov.querySelector('.pv-save').addEventListener('click', function(){
var e=engine(); if(!e || !e.__lsSavePhoneLayout){ toast('Open a world in the preview first'); return; }
var r=e.__lsSavePhoneLayout(); toast(r==='saved' ? 'Phone layout saved for this world' : r); state();
});
ov.querySelector('.pv-clear').addEventListener('click', function(){
var e=engine(); if(!e || !e.__lsClearPhoneLayout){ toast('Open a world in the preview first'); return; }
e.__lsClearPhoneLayout(); toast('This world uses the desktop layout on phones again'); state();
});
setInterval(function(){ if(ov.classList.contains('on')) state(); }, 1500);
ov.querySelector('.pv-x').addEventListener('click', function(){ ov.classList.remove('on'); frame.src='about:blank'; });
window.addEventListener('resize', fit);
}
function state(){
var el=ov&&ov.querySelector('.pv-state'); if(!el) return;
var e=null; try{ e=frame.contentWindow.document.getElementById('frame3d').contentWindow; }catch(_){ }
el.textContent = (e && e.__lsHasPhoneLayout) ? (e.__lsHasPhoneLayout() ? 'Phone layout: saved' : 'Phone layout: using desktop') : '';
}
function fit(){
if(!ov || !ov.classList.contains('on')) return;
var d=DEVICES[+sel.value||0], sh=ov.querySelector('.pv-shell');
frame.style.width=d[1]+'px'; frame.style.height=d[2]+'px';
var k=Math.min(1, (window.innerHeight-90)/(d[2]+20), (window.innerWidth-40)/(d[1]+20));
sh.style.transform='scale('+k.toFixed(3)+')';
info.textContent=d[1]+'\u00d7'+d[2]+(k<1?(' shown at '+Math.round(k*100)+'%'):'');
}
function load(){
var mode=MODE, world=(typeof worldPick!=='undefined'&&worldPick)?worldPick.value:'';
fit();
frame.onload=function(){
try{
var w=frame.contentWindow, d=w.document;
var g=d.getElementById('ss-gate'); if(g) g.remove(); d.documentElement.style.overflow='';
var sp=d.getElementById('lsSplash'); if(sp) sp.remove();
if(mode==='3d'){
var b=d.querySelector('#modeSeg button[data-mode="3d"]'); if(b) b.click();
var tries=0; (function pick(){ try{
if(w.WOW_READY && w.document.getElementById('worldPick')){ var wp=w.document.getElementById('worldPick'); w.document.querySelectorAll('.nav-pop').forEach(function(x){ x.style.display='none'; });
if(world && wp.value!==world){ wp.value=world; wp.dispatchEvent(new w.Event('change')); } return; }
}catch(_){ } if(tries++<60) setTimeout(pick, 250); })();
}
}catch(_){ }
};
frame.src=location.pathname+'?preview=1&t='+Date.now();
}
function open(){
if((window.innerWidth||9999)<=820){ toast('This screen: '+window.innerWidth+'\u00d7'+window.innerHeight+' (browser window, CSS px)'); return; }
build(); ov.classList.add('on'); load();
}
function wire(){ var b=document.getElementById('phonePrevBtn'); if(b) b.addEventListener('click', open); }
if(document.readyState!=='loading') wire(); else document.addEventListener('DOMContentLoaded', wire);
})();
/* r209: per-world rings */
var __lsWorldKey=null, __lsWorldLockT=0;
function _lsRingsNow(){
var o={};
['A','B','C','D'].forEach(function(L){
var sel=document.getElementById('ring'+L), id=sel&&sel.value;
if(id && id!=='__file') o[L]={id:id, mode:(window.__lsGsMode?window.__lsGsMode(L):'off')};
});
return o;
}
function _lsSaveWorldRings(key){ if(!key || /[?&]preview=1/.test(location.search)) return; try{ localStorage.setItem('ls-worldrings::'+key, JSON.stringify(_lsRingsNow())); }catch(_){ } }
/* r211: built-in rings from the saved all-worlds file */
var LS_BAKED_RINGS={"story:ch1":{"A":{"id":"grains","mode":"flat"},"B":{"id":"nuts-berries","mode":"flat"}},"story:ch2":{"A":{"id":"oyster","mode":"flat"},"B":{"id":"beer-styles","mode":"3d"}},"story:ch3":{"A":{"id":"varietals","mode":"flat"},"B":{"id":"wine-aroma","mode":"flat"}},"story:ch5":{"A":{"id":"charcuterie","mode":"flat"},"B":{"id":"dairy","mode":"3d"}}};
function _lsApplyWorldRings(key){
var m={}, raw=null; try{ raw=localStorage.getItem('ls-worldrings::'+key); }catch(_){ }
if(raw===null) m=LS_BAKED_RINGS[key]||{}; else { try{ m=JSON.parse(raw)||{}; }catch(_){ m={}; } }
window.__lsRingModeWant={}; window.__lsRingsAppliedAt=Date.now();
['A','B','C','D'].forEach(function(L){
var sel=document.getElementById('ring'+L), want=m[L];
if(want && want.id){
if(want.mode==='off' && window.__lsGsUnmount) window.__lsGsUnmount(L);
window.__lsRingModeWant[L]={id:want.id, mode:want.mode||'off'};
if(sel) sel.value=want.id;
try{ loadRing(L, want.id); }catch(_){ }
} else {
if(window.__lsGsUnmount) window.__lsGsUnmount(L);
if(sel) sel.value='';
try{ loadRing(L, ''); }catch(_){ }
}
});
}
/* hold saving while a switch settles, so a half-loaded world is never written */
var __lsRingsGen=0;
function _lsWorldRingsSwitch(newKey){
if(__lsWorldKey) _lsSaveWorldRings(__lsWorldKey);
__lsWorldKey=null; clearTimeout(__lsWorldLockT);
/* r226: a newer world switch cancels this one, so a world you passed through
(the phone preview opens the default world first) can't put its rings back */
var gen=++__lsRingsGen;
/* restore once the world's style has finished landing (it applies in chunks) */
var t0=Date.now();
(function wait(){
if(gen!==__lsRingsGen) return;
var busy=false; try{ busy=!!document.getElementById('frame3d').contentWindow.__WOW_STYLE_BUSY; }catch(_){ }
if((Date.now()-t0<700 || busy) && Date.now()-t0<6000){ setTimeout(wait,150); return; }
_lsApplyWorldRings(newKey);
__lsWorldLockT=setTimeout(function(){ if(worldPick.value===newKey) __lsWorldKey=newKey; }, 2500);
})();
}
setInterval(function(){
if(MODE!=='3d' || !WOW_READY || !__lsWorldKey || __lsWorldKey!==worldPick.value) return;
_lsSaveWorldRings(__lsWorldKey);
}, 1500);
worldPick.addEventListener('change', ()=>{
if(__lsWorldKey && __lsWorldKey!==worldPick.value) _lsSaveWorldRings(__lsWorldKey); /* leaving through a full reload */
const f=document.getElementById('frame3d'); const v=worldPick.value;
/* live swap: if the 3D engine is already running, route through the same
bg-photo change pipeline dev tools uses (photo + matching world style),
no frame reload. Reload only for build-your-own or first 3D entry. */
if(v!=='__build' && WOW_READY && f.getAttribute('data-3d')==='1' && (v.indexOf('world:')===0 || v.indexOf('story:')===0)){
/* r209: rings belong to the world they were loaded in. Leaving a world
stores its rings (wheel + render mode; their look is in the world's style);
arriving restores that world's own rings, or none. */
try{ if(window.lsCurtain) window.lsCurtain.show(v); }catch(_){ }
_lsWorldRingsSwitch(v);
applyWorldOrStory();
return;
}
lsWorldsSrcdoc(f); f.setAttribute('data-3d','1'); applyThemeToFrame(f); WOW_READY=false; wowExternalizeSoon();
});
/* === cheat-sheet auto-load for the 3D center-field pane (manifest-driven, sibling fallback, silent on 404) === */
let _cheatMap=null, _cheatMapP=null;
function _loadCheatManifest(){
if(_cheatMapP) return _cheatMapP;
_cheatMapP = fetch('CHEAT_PAIR_MANIFEST.json',{cache:'no-store'})
.then(r=> r.ok ? r.json() : null)
.then(j=>{ _cheatMap=new Map();
try{
var arr = j ? (Array.isArray(j) ? j : (j.pairs||j.cheats||j.wheels||[])) : [];
if(arr && arr.length){ arr.forEach(function(p){ var wj=p.wheel||p.wheel_json||p.json||p.w; var cj=p.cheat||p.cheat_json||p.c; if(wj&&cj){ _cheatMap.set(wj,cj); _cheatMap.set(String(wj).split('/').pop(),cj); } }); }
else if(j && typeof j==='object'){ Object.keys(j).forEach(function(k){ var cj=j[k]; if(typeof cj==='string'){ _cheatMap.set(k,cj); _cheatMap.set(k.split('/').pop(),cj); } }); }
}catch(e){}
return _cheatMap;
})
.catch(function(){ _cheatMap=new Map(); return _cheatMap; });
return _cheatMapP;
}
function _cheatPathFor(wheelJson){
var m=_cheatMap;
if(m){ if(m.has(wheelJson)) return m.get(wheelJson); var b=wheelJson.split('/').pop(); if(m.has(b)) return m.get(b); }
{ var _b=wheelJson.split('/').pop().replace(/\.json.*$/,''); var _q=(wheelJson.split('.json')[1]||''); var _d=wheelJson.slice(0, wheelJson.length-wheelJson.split('/').pop().length).replace(/\/$/,'').replace(/\/wheels$/,''); return (_d?_d+'/':'')+'cheats/'+_b+'-cheat.json'+_q; }
}
async function _autoLoadCheat(f, wheelJson){
try{ await _loadCheatManifest(); }catch(e){}
var url=_cheatPathFor(wheelJson), cj=null;
try{ cj=JSON.parse(await lsGetText(url)); }catch(e){ return; }
var n=0; (function push(){ try{ if(f && f.contentWindow && f.contentWindow.WOW_SET_CHEATSHEET){ f.contentWindow.WOW_SET_CHEATSHEET(cj); return; } }catch(e){} if(n++<40) setTimeout(push,150); })();
}
function _saveRingPick(letter, id){ try{ var m=JSON.parse(localStorage.getItem('ls_rings3d')||'{}'); if(id){ m[letter]=id; } else { delete m[letter]; } localStorage.setItem('ls_rings3d', JSON.stringify(m)); }catch(e){} }
var __lsRingReq={};
async function loadRing(letter, id){
const f=document.getElementById('frame3d');
const req=__lsRingReq[letter]=(__lsRingReq[letter]||0)+1; /* r226: only the newest request for a ring lands */
_saveRingPick(letter, id);
window.__lsLoaded=window.__lsLoaded||{}; if(id&&id!=='__file'){ window.__lsLoaded[letter]=id; } else { delete window.__lsLoaded[letter]; }
if(!id){ try{ f.contentWindow.WOW_EJECT_RING && f.contentWindow.WOW_EJECT_RING(letter); }catch(e){} return; }
if(id==='__file'){ return; }
const w=findWheel(id); if(!w) return;
let obj;
try{ obj=await lsWheelJSON(w); }
catch(e){ toast('Couldn\u2019t fetch '+w.name+' for the ring (serve the folder).'); return; }
_autoLoadCheat(f, w.json);
/* r218: the centre field titles each section from this wheel's own labels */
try{ const _tt=titlesFor(wheelTitles(obj)); (obj.categories||[]).forEach(c=>(c.sub||[]).forEach(su=>(su.descriptors||[]).forEach(d=>{ if(d.panel&&typeof d.panel==='object') d.panel.__titles=_tt; }))); }catch(_){ }
try{ wowSend('WOW_SET_RING_SUBJECT',{ring:letter,name:w.name}); }catch(_){}
let tries=0;
(function go(){
if(req!==__lsRingReq[letter]) return;
var cw=f.contentWindow;
if(cw && cw.WOW_LOAD_PAYLOAD){
/* r225: a ring is a GPU ring or 3D wheel unless you picked SVG yourself (never by
default). Those mount straight away and swap in for the old wheel in one step,
with no SVG stand-in drawn first (that stand-in was the extra ring). */
var _wm0=window.__lsRingModeWant&&window.__lsRingModeWant[letter];
var _ov0=null; try{ _ov0=localStorage.getItem('ls-ring-style-'+letter); }catch(_){ }
var _mode=(_wm0 && _wm0.mode && (_wm0.mode!=='off' || _wm0.user)) ? _wm0.mode : ((_ov0 && _ov0!=='off') ? _ov0 : 'flat');
if(_mode!=='off'){
try{
if(window.__lsGsMount) window.__lsGsMount(letter, _mode);
cw.postMessage({type:'ls:gs-state',ring:letter,mode:_mode},'*');
}catch(_){ }
return;
}
try{
const res=cw.WOW_LOAD_PAYLOAD(letter, obj);
if(res&&res.ok===false) toast(res.msg||('Ring '+letter+' load failed'));
}catch(err){
var em=(err&&err.message)||String(err);
toast('Ring '+letter+' failed to draw: '+em);
try{ console.error('[loadRing] WOW_LOAD_PAYLOAD threw for ring '+letter+':', err); }catch(_){}
}
/* r208: a wheel dropped into a ring comes up as the GPU ring unless you
have picked a different style for that ring yourself. */
try{
var _ov=null; try{ _ov=localStorage.getItem('ls-ring-style-'+letter); }catch(_){ }
var _want=_ov||'flat';
/* r224: the ring keeps its render type (the world's saved one, or the one you
last picked for this ring) whatever wheel is loaded into it */
var _wm=window.__lsRingModeWant&&window.__lsRingModeWant[letter];
if(_wm && _wm.mode) _want=_wm.mode;
if(false){ /* r225: GPU / 3D rings now mount above, before any SVG is drawn */
setTimeout(function(){
try{
if(window.__lsGsMount) window.__lsGsMount(letter, _want);
var w3=f.contentWindow;
if(w3) w3.postMessage({type:'ls:gs-state',ring:letter,mode:_want},'*');
}catch(_){ }
}, 650);
}
}catch(_){ }
return;
}
if(tries++<40) setTimeout(go,150); else toast('3D world not ready for ring '+letter+' \u2014 reopen 3D view');
})();
}
/* ── left Browse drawer removed: redundant with the nav Wheel pickers ── */
function markActive(id){ document.querySelectorAll('.item').forEach(function(n){ n.classList.toggle('active', n.dataset.id===id); }); }
/* ── engine messaging (2D) ── */
window.addEventListener('message', ev=>{
const d=ev&&ev.data||{};
if(d.type==='wow:ready'){ WOW_READY=true; try{ _lsWorldRingsSwitch(worldPick.value); __lsWorldKey=null; }catch(_){ } wowSend('WOW_EXTERNALIZE'); try{ applyThemeToFrame(document.getElementById('frame3d')); }catch(_){} try{ var _f3=document.getElementById('frame3d'); if(_f3) _lsInjectCtxMenu(_f3.contentWindow); }catch(_){} try{ setTimeout(applyWorldOrStory, 80); }catch(_){ } /* auto ring loading disabled by request */ try{ if(!localStorage.getItem('ls-perf-r212')){ if(localStorage.getItem('ls-perf')==='lite') localStorage.removeItem('ls-perf'); localStorage.setItem('ls-perf-r212','1'); } }catch(_){ } try{ var _sp=null; try{ _sp=localStorage.getItem('ls-perf'); }catch(_){} /* r212: phones no longer drop to Lightweight on their own — a world looks the same on every screen; only its background file changes */ if(_sp && window.LS_SET_PERF){ setTimeout(function(){ window.LS_SET_PERF(_sp); }, 550); } }catch(_){} try{ setTimeout(function(){ if(window.LS_SYNC_AUTOSPIN) window.LS_SYNC_AUTOSPIN(); if(window.LS_SYNC_NAV_HEIGHT) window.LS_SYNC_NAV_HEIGHT(); }, 300); }catch(_){} return; }
const s=slots.find(s=> s.iframe.contentWindow===ev.source); if(!s) return;
if(d.type==='wheel:ready'){ s.ready=true; try{ applyThemeToFrame(s.iframe); }catch(e){} try{ _lsInjectCtxMenu(s.iframe.contentWindow); }catch(e){} if(s.pending) flush(s); try{ if(window.__lsAutospinSlot) window.__lsAutospinSlot(s.i); }catch(e){} }
else if(d.type==='LS_READ' && d.slug){ /* r42: the engine's center readout is the single info surface — the shell side-frame no longer opens on landing */ if(s.index && s.index[d.slug]) s.currentSlug=d.slug; }
});
function flush(s){ if(!s.pending) return; try{ setTimeout(function(){ try{ lsOfficeCatSend(s.i); }catch(_){ } try{ lsSkSend(s.i); }catch(_){ } }, 400); }catch(_){ } try{ if(s.iframe.contentWindow) s.iframe.contentWindow.postMessage({type:'wheel:load', payload:s.pending}, '*'); try{ var __d=window.__lsDepth; if(__d){ setTimeout(function(){ try{ s.iframe.contentWindow && s.iframe.contentWindow.postMessage({type:'ls:depth', depth:__d}, '*'); }catch(_){ } }, 120); } }catch(_){ } }catch(e){} }
function indexData(cats){ const idx={}; (cats||[]).forEach(c=>(c.sub||[]).forEach(su=>(su.descriptors||[]).forEach(d=>{ idx[d.slug]={d,cat:c,sub:su}; }))); return idx; }
function setPayload(s, data, title){
const cats = data && (data.categories || (data.payload&&data.payload.categories));
if(!cats||!cats.length){ toast('That file doesn\u2019t look like a wheel.'); return; }
mountSlot(s); s.wheelId=null; s.index=indexData(cats); s.ui=data.ui||null; s.titles=wheelTitles(data);
s.pending={categories:cats, title:title||data.title||'', labels:s.titles};
s.currentName=title||data.title||'';
s.empty.classList.add('hidden'); closeInfo(s);
flush(s); // engine loaded at init; post now. wheel:ready reflushes if it wasn't ready yet.
}
function loc(obj, base){ return obj[base+sfx()] || obj[base] || ''; }
/* r218: a wheel's panel titles. "labels" (per-wheel headings) win, then the older "ui"
block; blank entries fall through. Returns {lang:{field:title}} for every language given. */
function wheelTitles(data){
const out={}; if(!data) return out;
[data.ui, data.labels].forEach(src=>{ if(!src||typeof src!=='object') return;
Object.keys(src).forEach(L=>{ const set=src[L]; if(!set||typeof set!=='object') return;
FIELD_ORDER.forEach(f=>{ const t=set[f]; if(typeof t==='string' && t.trim()){ (out[L]=out[L]||{})[f]=t; } }); }); });
return out;
}
function titlesFor(t){ return (t && (t[LANG]||t.en)) || null; }
function labelFor(s, f){ const t=titlesFor(s.titles); if(t && t[f]) return t[f]; const u=s.ui && (s.ui[LANG]||s.ui.en); return (u && typeof u[f]==='string') ? u[f] : FALLBACK_LAB[f]; }
function showInfo(s, slug){
const hit=s.index[slug]; if(!hit) return;
const d=hit.d, p=d.panel||{}; s.currentSlug=slug;
const head=s.info.querySelector('.info-head');
head.querySelector('.kicker').textContent = loc(hit.sub,'name')||loc(hit.cat,'name')||'';
head.querySelector('h3').textContent = loc(d,'name')||d.name||slug;
const body=s.info.querySelector('.info-body'); body.innerHTML=''; let any=false;
FIELD_ORDER.forEach(fld=>{
const lab=labelFor(s,fld); const v=loc(p,fld);
if(lab && String(lab).trim() && v && String(v).trim()){
any=true;
const b=document.createElement('div'); b.className='fld-block';
const L=document.createElement('div'); L.className='lab'; L.textContent=lab;
const V=document.createElement('div'); V.className='val'; V.textContent=v;
b.appendChild(L); b.appendChild(V); body.appendChild(b);
}
});
if(!any){ const e=document.createElement('div'); e.className='val'; e.style.color='var(--muted)'; e.textContent='No details for this entry yet.'; body.appendChild(e); }
s.info.querySelector('.info-foot').textContent=s.currentName;
s.info.classList.add('open');
}
function closeInfo(s){ s.info.classList.remove('open'); s.currentSlug=null; }
function rerenderOpen(){ slots.forEach(s=>{ if(s.info.classList.contains('open')&&s.currentSlug) showInfo(s,s.currentSlug); }); }
slots.forEach(s=> s.info.querySelector('.x').addEventListener('click', ()=> closeInfo(s)));
/* ── loaders ── */
async function loadById(s, id){
if(!id) return;
if(id==='__file'){ pickFileFor(s); (s.i===0?wheel1:wheel2).value=''; return; }
const w=findWheel(id); if(!w) return;
(s.i===0?wheel1:wheel2).value=id; markActive(id);
try{ setPayload(s, JSON.parse(await lsGetText(entFile(w))), w.name); s.wheelId=id; (function(win){ var tries=0; (function pl(){ try{ if(win.UI&&win.UI.en){ win.UI.en.pairing='Pairing'; if(win.UI.fr&&win.UI.fr.pairing==='Accord boisson') win.UI.fr.pairing='Accord'; win.UI.en.examples='Notes'; if(win.UI.es) win.UI.es.examples='Notas'; if(win.UI.fr) win.UI.fr.examples='Notes'; if(win.UI.it) win.UI.it.examples='Note'; return; } }catch(_){} if(++tries<40) setTimeout(pl,250); })(); })(s.iframe.contentWindow); { var __dk=(s.i===0?'dock-left':'dock-right'); setTimeout(function(){ try{ s.iframe.contentWindow.postMessage({type:'LS_CMD',cmd:__dk}, '*'); }catch(e){} }, 160); } }
catch(err){ toast('Couldn\u2019t reach '+w.json+' — serve the folder (the dropdown reads external .json files), or drag the file onto the wheel.'); }
}
function loadFileInto(s, file){
const rd=new FileReader();
rd.onload=()=>{ try{ setPayload(s, JSON.parse(rd.result), file.name.replace(/\.json$/i,'')); }catch(e){ toast('That file doesn\u2019t look like a wheel.'); } };
rd.onerror=()=> toast('Could not read that file.');
rd.readAsText(file);
}
const filePick=document.createElement('input'); filePick.type='file'; filePick.accept='.json,application/json'; filePick.style.display='none'; document.body.appendChild(filePick);
let fileTarget=0;
function pickFileFor(s){ fileTarget=s.i; filePick.value=''; filePick.click(); }
filePick.addEventListener('change', ()=>{ if(filePick.files[0]) loadFileInto(slots[fileTarget], filePick.files[0]); });
/* slot Load buttons + drag/drop */
slots.forEach(s=>{
['dragenter','dragover'].forEach(ev=> s.el.addEventListener(ev, e=>{ e.preventDefault(); s.el.classList.add('drop'); }));
s.el.addEventListener('dragleave', e=>{ if(!s.el.contains(e.relatedTarget)) s.el.classList.remove('drop'); });
s.el.addEventListener('drop', e=>{ e.preventDefault(); s.el.classList.remove('drop'); const f=e.dataTransfer.files[0]; if(f) loadFileInto(s,f); });
});
document.getElementById('langPick').addEventListener('change', e=>{ LANG=e.target.value; rerenderOpen(); broadcast({type:'ls:lang', lang:LANG}); /* r101: propagate language to the wheel engines */ try{ [slots[0],slots[1]].forEach(function(s){ try{ var w=s&&s.iframe&&s.iframe.contentWindow; if(w&&typeof w.setLang==='function') w.setLang(LANG); }catch(_){} }); }catch(_){} try{ var __f3=document.getElementById('frame3d'); var __w3=__f3&&__f3.contentWindow; if(__w3&&typeof __w3.WOW_SET_LANG==='function') __w3.WOW_SET_LANG(LANG); }catch(_){}
(function(){ var lp=document.getElementById('levelPick'); if(!lp)return; try{ var sv=localStorage.getItem('ls_depth'); if(sv){ var n=parseInt(sv,10)||2; if(n>2)n=2; if(n<1)n=1; lp.value=String(n); } }catch(_){ } window.__lsDepth=Math.min(2,Math.max(1,parseInt(lp.value,10)||2)); lp.addEventListener('change', function(e){ var t=parseInt(e.target.value,10)||2; if(t>2)t=2; if(t<1)t=1; window.__lsDepth=t; try{ localStorage.setItem('ls_depth', String(t)); }catch(_){ } broadcast({type:'ls:depth', depth:t}); }); })(); });
/* pairing — wheel2 + slot2 appear together */
document.getElementById('pairToggle').addEventListener('change', e=>{
(function(){var __db=document.getElementById('dockBtns'); if(__db) __db.style.display=(MODE==='2d'&&e.target.checked)?'inline-flex':'none';})();
if(e.target.checked){
stage2d.classList.remove('single'); stage2d.classList.add('pair');
wheel2.style.setProperty('display','none','important'); /* the ② button picks wheel 2 */ (function(){var __dt=document.getElementById('dockTarget'); if(__dt) __dt.style.display='inline-flex';})();
if(!slots[1].pending){
/* r176: JSON_WHEELS is a const, so window.JSON_WHEELS was always undefined and
wheel 2 never got a default. Pick the first LearnSomm wheel that isn't already
in slot 1, matching the picker's own group order. */
var _pool=[]; ['lsfood','lsbeverage','lsgrapes','lssomm','lscuisine'].forEach(function(k){ if(WHEELS[k]) _pool=_pool.concat(WHEELS[k]); });
if(!_pool.length && typeof JSON_WHEELS!=='undefined') _pool=JSON_WHEELS;
var _cur=wheel1&&wheel1.value, def=null;
for(var _i=0;_i<_pool.length;_i++){ if(_pool[_i].id!==_cur){ def=_pool[_i]; break; } }
if(def){ wheel2.value=def.id; loadById(slots[1], def.id); }
}
/* r205: two wheels side by side read small, so pairing opens them 60%
bigger than the single-wheel default. Done once per entry into pairing;
your own zooming afterwards is left alone. */
setTimeout(function(){
try{
if((window.innerWidth||9999)<=820){
/* r213: phones stack the pair; each wheel gets the SAME size, 1.3x its panel's
height, so both show their readable half (a scale multiplied whatever size
wheel 1 already had, which blew it up) */
slots.forEach(function(sl){ try{ var r=sl.iframe.getBoundingClientRect(); if(r.height>0) sl.iframe.contentWindow.postMessage({type:'LS_SIZE', px:Math.round(r.height*1.3)},'*'); }catch(_){ } });
} else window.lsFitWhenReady(); /* r279: both wheels whole, centred, the same size */
}catch(_){ }
try{ if(window.__lsSpinRow2) window.__lsSpinRow2(); }catch(_){ }
try{ if(window.__lsAutospinSync) window.__lsAutospinSync(); }catch(_){ }
}, 500);
}else{
try{ if(window.__lsSpinRow2) setTimeout(window.__lsSpinRow2,0); }catch(_){ }
stage2d.classList.add('single'); stage2d.classList.remove('pair');
try{ setTimeout(window.lsFitWhenReady, 300); }catch(_){ }
wheel2.style.setProperty('display','none','important'); (function(){var __dt=document.getElementById('dockTarget'); if(__dt){ __dt.style.display='none'; __dt.value='0'; }})();
setTimeout(function(){ broadcast({type:'LS_CMD',cmd:'dock-left'}); }, 120);
}
buildRingBtns();
});
function closeRingPop(){ var p=document.getElementById('ringPop'); if(p) p.remove(); }
function mkRingItem(o, sel, btn){ var d=document.createElement('div'); d.className='ring-pop-i'; d.textContent=o.textContent||'(empty)'; if(o.value===sel.value) d.classList.add('on'); d.addEventListener('click', function(ev){ ev.stopPropagation(); sel.value=o.value; sel.dispatchEvent(new Event('change',{bubbles:true})); if(btn) btn.classList.toggle('set', !!o.value); closeRingPop(); }); return d; }
function openRingPop(btn, selId){ var cur=document.getElementById('ringPop'); var same=cur&&cur.dataset.sel===selId; closeRingPop(); if(same) return; var sel=document.getElementById(selId); if(!sel) return; var pop=document.createElement('div'); pop.className='ring-pop'; pop.id='ringPop'; pop.dataset.sel=selId; Array.prototype.forEach.call(sel.children, function(node){ if(node.tagName==='OPTGROUP'){ var h=document.createElement('div'); h.className='ring-pop-h'; h.textContent=node.label; pop.appendChild(h); Array.prototype.forEach.call(node.children, function(o){ pop.appendChild(mkRingItem(o, sel, btn)); }); } else if(node.tagName==='OPTION'){ pop.appendChild(mkRingItem(node, sel, btn)); } }); if(/^ring[ABCD]$/.test(selId)){ var __L=selId.slice(-1); var __cat=document.createElement('div'); __cat.className='ring-pop-h'; __cat.textContent='Tools'; var __dv=document.createElement('div'); __dv.className='ring-pop-i ring-pop-dev'; __dv.innerHTML='\u2699 Dev Tools PRO'; __dv.addEventListener('click', function(ev){ ev.stopPropagation(); try{ wowSend('WOW_RING_DEV', __L); }catch(e){} closeRingPop(); }); var __kids=pop.children, __empty=null; for(var __k=0;__k<__kids.length;__k++){ var __t=(__kids[__k].textContent||'').trim().toLowerCase(); if(__t==='(empty)'||__t==='none'||__t==='no wheel'){ __empty=__kids[__k]; } } if(__empty){ pop.insertBefore(__cat,__empty); pop.insertBefore(__dv,__empty); } else { pop.appendChild(__cat); pop.appendChild(__dv); } } document.body.appendChild(pop); var r=btn.getBoundingClientRect(); pop.style.left=Math.max(8, Math.min(r.left, window.innerWidth-pop.offsetWidth-8))+'px'; pop.style.top=(r.bottom+5)+'px'; }
function buildRingBtns(){ var box=document.getElementById('ringBtns'); if(!box) return; var items; if(MODE==='3d'){ items=[]; } else { var __ptEl=document.getElementById('pairToggle'); var paired=!!(__ptEl&&__ptEl.checked); items=[['1','wheel1']]; if(paired) items.push(['2','wheel2']); } box.innerHTML=''; items.forEach(function(it){ var sel=document.getElementById(it[1]); var b=document.createElement('button'); b.className='ring-btn'; b.type='button'; b.dataset.sel=it[1]; if(sel && sel.value) b.classList.add('set'); b.title='Ring '+it[0]; b.innerHTML=''+it[0]+''; b.addEventListener('click', function(ev){ ev.stopPropagation(); openRingPop(b, it[1]); }); box.appendChild(b); }); }
document.addEventListener('click', closeRingPop);
/* Pairing: wheel 1 docks into the left edge of the page, wheel 2 into the right,
and pressing a descriptor on either one opens ONE shared photo/article window
(the same draggable, resizable window a single wheel uses) in the open middle.
The window's code is the wheel engine's own lookup script, run once in this page. */
(function(){
var lastPaired=null;
function sw(i){ try{ return slots[i]&&slots[i].iframe.contentWindow; }catch(_){ return null; } }
function isPaired(){ var pt=document.getElementById('pairToggle'); return !!(pt&&pt.checked) && MODE==='2d'; }
/* docking writes the wheel's saved layout; pairing must not change what a single wheel comes back to */
function dock(w, m){
if(!w || !w.__lsSetMode || w.__lsPairDock===m) return;
var prev=null; try{ prev=localStorage.getItem('wheelMode'); }catch(_){ }
try{ w.__lsSetMode(m); w.__lsPairDock=m; }catch(_){ }
try{ if(prev===null) localStorage.removeItem('wheelMode'); else localStorage.setItem('wheelMode', prev); }catch(_){ }
}
function ensureShared(){
if(window.__lsLookup) return true;
var w=sw(0)||sw(1); if(!w) return false;
try{
var src=w.document.getElementById('lsLookupPanel'); if(!src) return false;
var sc=document.createElement('script'); sc.textContent=src.textContent; document.body.appendChild(sc);
}catch(_){ return false; }
return !!window.__lsLookup;
}
setInterval(function(){
var on=isPaired(), w0=sw(0), w1=sw(1);
[w0,w1].forEach(function(w){ try{ if(w) w.postMessage({type:'ls:paired', on:on && w!==null},'*'); }catch(_){ } });
if(on){ dock(w0,'dock-left'); dock(w1,'dock-right'); }
else if(lastPaired){
if(w0){ w0.__lsPairDock=null; try{ var sm=localStorage.getItem('wheelMode'); w0.__lsSetMode(sm && sm.indexOf('dock')===0 ? sm : 'dock-left'); }catch(_){ } }
try{ if(window.__lsLookupOpen && window.__lsLookupOpen()) document.querySelector('.lk-x').click(); }catch(_){ }
}
lastPaired=on;
}, 600);
window.addEventListener('message', function(ev){
var d=ev&&ev.data; if(!d || d.type!=='LS_READ' || !isPaired()) return;
if((window.innerWidth||9999)<=820) return; /* r213: phones use each wheel's own Info | Photos tabs */
var w=null; [0,1].forEach(function(i){ if(sw(i)===ev.source) w=sw(i); }); if(!w) return;
if(!ensureShared()) return;
try{
/* the wheel's own photo-search hints travel with the descriptor */
window.__LS_IMG_TERMS=w.__LS_IMG_TERMS; window.__LS_IMG_SUBJ=w.__LS_IMG_SUBJ; window.__LS_IMG_SUBJ_DEFAULT=w.__LS_IMG_SUBJ_DEFAULT;
var lang=(typeof LANG!=='undefined'&&LANG)||'en', term=d.label||d.slug, ctx=w.__lsSubj||'';
window.__lsLookupRefresh(term, lang, ctx); /* r232: refresh if open — it opens only from the Photos / Article link */
}catch(_){ }
});
/* r232: in desktop pairing a wheel's Photos / Article link opens the shared window */
window.addEventListener('message', function(ev){
var d=ev&&ev.data; if(!d || d.type!=='ls:lookup-open' || !isPaired()) return;
var w=null; [0,1].forEach(function(i){ if(sw(i)===ev.source) w=sw(i); }); if(!w || !ensureShared()) return;
try{ window.__LS_IMG_TERMS=w.__LS_IMG_TERMS; window.__LS_IMG_SUBJ=w.__LS_IMG_SUBJ; window.__LS_IMG_SUBJ_DEFAULT=w.__LS_IMG_SUBJ_DEFAULT;
window.__LS_IMG_PIN=w.__LS_IMG_PIN; window.__LS_IMG_BLOCK=w.__LS_IMG_BLOCK;
window.__lsPairSrc=w; window.__lsPairLang=d.lang||'en'; window.__lsPairCtx=d.ctx||'';
window.__lsLookup(d.term, d.lang||'en', d.mode||'photos', d.ctx||''); }catch(_){ }
/* r272: ◎ puts it back inside the wheel it came from */
try{ var U=document.getElementById('lsLookup'); if(U && !U.querySelector('.lk-back2')){ var x=U.querySelector('.lk-x'), bb=document.createElement('button'); bb.type='button'; bb.className='lk-back2 lk-tab'; bb.textContent='\u25ce'; bb.title='Back into the wheel\u2019s info window';
bb.addEventListener('click',function(e){ e.stopPropagation(); try{ localStorage.setItem('ls-office-lk-pop','0'); }catch(_){ }
var tt=(U.querySelector('.lk-title')||{}).textContent||'', tb=U.querySelector('.lk-tab.on[data-t]'), mm=tb?tb.getAttribute('data-t'):'photos';
if(x) x.click(); try{ if(window.__lsPairSrc) window.__lsPairSrc.postMessage({type:'ls:lookup-dock', term:tt, lang:window.__lsPairLang, mode:mm, ctx:window.__lsPairCtx},'*'); }catch(_){ } });
x.parentNode.insertBefore(bb,x); } }catch(_){ }
});
})();
/* pairing: both wheels set their descriptors at the same size (the smaller of the
two best fits), so neither looks oversized next to the other. Acts once the
pair has been stable for two checks, so a wheel mid-load is never measured. */
(function(){
var applied='', seen='', seenN=0;
setInterval(function(){
try{
var pt=document.getElementById('pairToggle'), paired=!!(pt&&pt.checked) && MODE==='2d';
var w0=slots[0]&&slots[0].iframe.contentWindow, w1=slots[1]&&slots[1].iframe.contentWindow;
var key=paired+'|'+(slots[0]&&slots[0].currentName)+'|'+(paired&&slots[1]?slots[1].currentName:'');
if(key!==seen){ seen=key; seenN=0; return; }
if(++seenN<2 || key===applied) return;
if(!w0||!w0.LS_FS_CAP) return;
if(paired){
if(!w1||!w1.LS_FS_OWN) return;
var a=w0.LS_FS_OWN(), b=w1.LS_FS_OWN(); if(!a||!b) return;
var m=Math.min(a,b); w0.LS_FS_CAP(m); w1.LS_FS_CAP(m);
} else { w0.LS_FS_CAP(0); if(w1&&w1.LS_FS_CAP) w1.LS_FS_CAP(0); }
applied=key;
}catch(_){ }
}, 700);
})();
var __dkL=document.getElementById('dockL'), __dkR=document.getElementById('dockR');
function dockTo(cmd){ try{ if(MODE==='3d'){ var f3=document.getElementById('frame3d'); if(f3&&f3.contentWindow) f3.contentWindow.postMessage({type:'LS_CMD',cmd:cmd},'*'); return; } var dt=document.getElementById('dockTarget'); var idx=(stage2d.classList.contains('pair')&&dt)?(parseInt(dt.value,10)||0):0; var f=slots[idx]&&slots[idx].iframe; if(f&&f.contentWindow) f.contentWindow.postMessage({type:'LS_CMD',cmd:cmd},'*'); }catch(e){} }
if(__dkL) __dkL.addEventListener('click', function(){ dockTo('dock-left'); });
if(__dkR) __dkR.addEventListener('click', function(){ dockTo('dock-right'); });
/* mode */
function setMode(m){
MODE=m;
document.querySelectorAll('#modeSeg button').forEach(b=> b.classList.toggle('on', b.dataset.mode===m));
stage2d.classList.toggle('hidden', m!=='2d');
stage3d.classList.toggle('hidden', !(m==='3d'||m==='story'));
document.getElementById('ctl2d').style.display = m==='2d'?'inline-flex':'none';
document.getElementById('ctl3d').style.display = (m==='3d'||m==='story')?'inline-flex':'none';
var __db=document.getElementById('dockBtns'); if(__db){ var _pt=document.getElementById('pairToggle'); __db.style.display = (m==='2d' && _pt && _pt.checked)?'inline-flex':'none'; }
buildRingBtns();
document.getElementById('designBtn').style.display = 'inline-flex'; /* r217: Design holds the centre-field text settings in both modes */
document.getElementById('devBtn').style.display = (m==='3d'||m==='story')?'inline-flex':'none';
ringDrops.style.display = (m==='3d'||m==='story')?'inline-flex':'none';
try{worldPick.style.display=''; var __wb=document.getElementById('worldWrap'); if(__wb) __wb.style.display=(m==='3d')?'inline-flex':'none'; var __ps=document.getElementById('perfToggle'); if(__ps) __ps.style.display='inline-flex'; /* r222: Office too */ var __sp=document.getElementById('storyPick'); if(__sp)__sp.style.display=(m==='story')?'':'none';}catch(e){} var __ws=document.getElementById('wow3dSearch'); if(__ws) __ws.style.display='none';
var __pt=document.getElementById('pairToggle'); if(__pt){ var __pl=__pt.closest('label'); if(__pl) __pl.style.display = (m==='3d'||m==='story')?'none':'inline-flex'; }
if(m==='3d'||m==='story'){ const f=document.getElementById('frame3d'); var __wl=!!f.getAttribute('data-3d'); if(!__wl){ lsWorldsSrcdoc(f); f.setAttribute('data-3d','1'); WOW_READY=false; } applyThemeToFrame(f); wowExternalizeSoon(); if(__wl && WOW_READY){ setTimeout(applyWorldOrStory,30); } lsLoadWorldsEngine().then(function(){ setTimeout(function(){ if(!WOW_READY) toast('3D engine did not respond \u2014 reload the page.'); },4500); }).catch(function(){}); }
}
document.querySelectorAll('#modeSeg button').forEach(b=> b.addEventListener('click', ()=> setMode(b.dataset.mode)));
var WOW_READY=false;
function wowSend(fn,arg){ try{ var w=document.getElementById('frame3d').contentWindow; if(w) w.postMessage({type:'wow',fn:fn,arg:arg},'*'); }catch(e){} }(function(){ var t=document.getElementById('perfToggle'); if(!t) return;
function setPerf(p){ p=(p==='lite')?'lite':'full'; t.setAttribute('data-perf',p); t.setAttribute('aria-pressed', p==='lite'?'true':'false'); var u=t.querySelector('use'); if(u) u.setAttribute('href', p==='lite'?'#i-lite':'#i-full'); t.title=(p==='lite')?'Lightweight — tap for Full experience':'Full experience — tap for Lightweight'; wowSend('WOW_SET_PERF', p); try{ localStorage.setItem('ls-perf', p); }catch(_){} try{ window.__lsAutospinLite && window.__lsAutospinLite(p==='lite'); }catch(_){} }
window.LS_SET_PERF=setPerf;
t.addEventListener('click', function(){ setPerf(t.getAttribute('data-perf')==='full'?'lite':'full'); });
(function(){ function popWire(bid,pid,selId){ var b=document.getElementById(bid), pop=document.getElementById(pid); if(!b||!pop) return; function place(){ var r=b.getBoundingClientRect(); pop.style.top=Math.round(r.bottom+6)+'px'; var pw=pop.offsetWidth||210, l=r.left; if(l+pw>window.innerWidth-8) l=Math.max(8, window.innerWidth-8-pw); pop.style.left=Math.round(l)+'px'; } function open(){ pop.style.display='flex'; place();
/* One click should do the whole job: open the popover AND its native select's
own dropdown list, instead of requiring a second click on the select itself. */
if(selId){ var sels=[document.getElementById(selId)]; var sib=pop.querySelectorAll('select'); sib.forEach(function(s){ if(sels.indexOf(s)<0) sels.push(s); });
var target=sels.filter(function(s){ return s && s.offsetParent!==null && s.style.display!=='none'; })[0] || sels[0];
if(target){ setTimeout(function(){ try{ target.showPicker(); }catch(_){ try{ target.focus(); }catch(__){} } }, 0); }
}
} function close(){ pop.style.display='none'; } b.addEventListener('click', function(e){ e.stopPropagation(); if(pop.style.display==='none'){ open(); } else { close(); } }); document.addEventListener('click', function(e){ if(pop.style.display!=='none' && !pop.contains(e.target) && !b.contains(e.target)) close(); }); window.addEventListener('resize', function(){ if(pop.style.display!=='none') place(); }); /* r207: close on ANY select inside the popover, not only the one named —
the Worlds wheel menu has four and none of them used to dismiss it. */
pop.querySelectorAll('select').forEach(function(s){ s.addEventListener('change', function(){ setTimeout(close,0); }); });
if(selId){ var s=document.getElementById(selId); if(s) s.addEventListener('change', close); } } popWire('wheelsBtn','wheelsPop'); popWire('scenicBtn','worldPop'); popWire('ext2dBtn','ext2dPop');
/* a choice made in any top-bar menu (world, wheel, auto-spin, brand menu...)
collapses every open menu, not just the one it was made in */
(function(){
function closeAll(){ setTimeout(function(){ document.querySelectorAll('.nav-pop').forEach(function(p){ p.style.display='none'; }); try{ document.getElementById('navItems').classList.remove('open'); document.getElementById('navToggle').setAttribute('aria-expanded','false'); }catch(_){ } }, 0); }
function inPop(t){ return t && t.closest && t.closest('.nav-pop'); }
document.addEventListener('change', function(e){ if(inPop(e.target)) closeAll(); }, true);
document.addEventListener('click', function(e){ var t=e.target; if(inPop(t) && t.closest('button,a')) closeAll(); }, true);
/* a click inside the 3D world or a wheel happens in its own frame, which this
page never hears about; the page losing focus to a frame is the signal */
window.addEventListener('blur', function(){ setTimeout(function(){ var a=document.activeElement; if(a && a.tagName==='IFRAME') closeAll(); }, 0); });
})();
/* r210: the Worlds menu is a plain list. A native dropdown only reports a
CHANGE, so picking the world you were already on left the menu hanging open.
Every click here picks and closes, including the current world. The hidden