Family Games
Spending quality time with family strengthens your sense of togetherness, deepens your bonds, and creates space for shared experiences. It also helps you better understand and support one another in everyday life.
, which is BEFORE // window.Shopify.currency exists — an eager HUF gate here silently exits on // every page load (live-verified 2026-08-16: the preview param never stuck). // So: arm resolution and the settings flip happen now (they need no // currency), and the HUF gate runs at DOMContentLoaded, reverting the flip // for non-HUF visitors before the component initialises. // ── Preview plumbing (sticky per session, excluded from bookkeeping) ────── var preview = null; try { var m = window.location.search.match(/[?&]consentexp=([a-z]+)/); if (m) { if (m[1] === 'off') sessionStorage.removeItem('kvce_prev'); else sessionStorage.setItem('kvce_prev', m[1] === 'control' ? 'control' : 'treatment'); } preview = sessionStorage.getItem('kvce_prev'); } catch (e) { /* storage blocked: preview simply unavailable */ } if (MODE === 'preview' && !preview) return; // ── Arm ─────────────────────────────────────────────────────────────────── var arm; var bookkeeping = MODE === 'ab' && !preview; if (preview) arm = preview; else if (MODE === 'on') arm = 'treatment'; else { try { arm = localStorage.getItem('kv_consent_arm'); if (arm !== 'control' && arm !== 'treatment') { arm = Math.random() < 0.5 ? 'control' : 'treatment'; localStorage.setItem('kv_consent_arm', arm); } } catch (e) { // Storage blocked: unassignable and unmeasurable, so leave the default // banner and stay out of the books rather than pollute an arm. return; } } function visitorId() { try { var v = localStorage.getItem('otto_visitor_id'); if (!v) { v = window.crypto && crypto.randomUUID ? crypto.randomUUID() : 'v-' + Date.now() + '-' + Math.random().toString(16).slice(2); localStorage.setItem('otto_visitor_id', v); } return v; } catch (e) { return null; } } function track(name, idSuffix, extra) { if (!bookkeeping) return; var vid = visitorId(); if (!vid) return; var data = { experimentKey: KEY, variant: arm }; if (extra) for (var k in extra) data[k] = extra[k]; try { fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, keepalive: true, body: JSON.stringify({ events: [{ id: KEY + ':' + vid + (idSuffix ? ':' + idSuffix : ''), eventName: name, clientId: vid, occurredAt: new Date().toISOString(), data: data }] }) }).catch(function () {}); } catch (e) { /* bookkeeping must never break the page */ } } function dayKey() { return new Date().toISOString().slice(0, 10); } // ── Treatment step 1: flip Pandectes' own modality settings pre-init ────── // Their component snapshots window.pandectesBannerSettings at init; this // snippet renders before , which in practice runs first. If it does // not, step 3's fallback covers the gap. var flipped = false; if (arm === 'treatment') { try { var s = window.pandectesBannerSettings; if (s && s.theme) { s.theme.isModal = false; s.theme.scrollLock = false; flipped = true; } } catch (e) { /* fall through to the CSS fallback */ } } function revertFlip() { if (!flipped) return; try { var s2 = window.pandectesBannerSettings; if (s2 && s2.theme) { s2.theme.isModal = true; s2.theme.scrollLock = true; } } catch (e) {} } var COMPACT_CSS = // Scoped to the FIRST-VISIT view only. Live-verified 2026-08-16 at // 375x812: this store's first-visit view renders `.dialog.notice` // (231px tall by default; the button row is `.actions`, the text is // `.intro`/#message inside `.body`). This CSS measures 182px on the // real banner with both buttons fully visible. `.banner` is kept as an // alias in case a Pandectes config change renames the view. The // preferences dialog (a different view class) keeps its full layout, // so the granular consent choices render exactly as Pandectes designed // them. // // DELIBERATELY NO max-height/overflow cap: a cap that guesses low clips // the consent BUTTONS (caught live at 148px), and a banner whose accept // and decline are unreachable is a compliance bug, not a compact layout. // Height comes from shrinking the parts instead. '.dialog.notice,.dialog.banner{padding:10px 14px 12px;}' + '.dialog.notice .header,.dialog.banner .header{margin:0;padding:0;min-height:0;}' + '.dialog.notice .header .title,.dialog.banner .header .title{font-size:14px;margin:0;}' + '.dialog.notice #message,.dialog.banner #message{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;font-size:12px;line-height:1.35;}' + '.dialog.notice .intro ul,.dialog.notice .details,.dialog.banner .intro ul,.dialog.banner .details{display:none;}' + '.dialog.notice .actions,.dialog.banner .actions{display:flex;flex-wrap:nowrap;gap:8px;margin:8px 0 0;padding:0;}' + '.dialog.notice .actions button,.dialog.banner .actions button{flex:1;padding:9px 8px;font-size:13px;min-height:0;margin:0;}' + '.overlay{display:none!important;}'; var seenFired = false; function onBannerAppears(host) { // Treatment step 2: inject the compact stylesheet into the open shadow // root. A separate node, so Pandectes' own #pandectes-cmp-custom-styles // (their dashboard CSS) is never touched. if (arm === 'treatment') { try { var root = host.shadowRoot; if (root && !root.getElementById('kv-consent-compact')) { var st = document.createElement('style'); st.id = 'kv-consent-compact'; st.textContent = COMPACT_CSS; root.appendChild(st); } } catch (e) { /* selectors missed: default banner, fail open */ } // Treatment step 3: if the settings flip lost the init race, the // component may have locked body scroll before we got here. One // restore + one delayed recheck — not an observer war. var unlock = function () { try { if (getComputedStyle(document.body).overflow === 'hidden') document.body.style.overflow = 'visible'; if (getComputedStyle(document.documentElement).overflow === 'hidden') document.documentElement.style.overflow = 'visible'; } catch (e) {} }; unlock(); setTimeout(unlock, 600); } if (!seenFired) { seenFired = true; track('experiment_exposure', '', null); // once per visitor (upsert on stable id) track('consent_seen', 'seen-' + dayKey(), null); // daily denominator } } // The component mounts asynchronously; watch for it rather than racing it. // A visitor with stored consent never mounts a banner → observer sees // nothing → no exposure, which is exactly the right denominator. function watch() { var existing = document.querySelector('pandectes-cmp'); if (existing) { onBannerAppears(existing); return; } var mo = new MutationObserver(function () { var el = document.querySelector('pandectes-cmp'); if (el) { mo.disconnect(); onBannerAppears(el); } }); mo.observe(document.documentElement, { childList: true, subtree: true }); // A banner that has not mounted within a minute is not mounting on this // page view. Stop observing rather than watching forever. setTimeout(function () { mo.disconnect(); }, 60000); } // The deferred HUF gate: non-HU visitors get the flip reverted (before the // component initialises, which also happens after DOMContentLoaded) and are // never bucketed, watched, or stamped. function boot() { if (!(window.Shopify && window.Shopify.currency && window.Shopify.currency.active === 'HUF')) { revertFlip(); return; } watch(); stampCart(); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot); else boot(); // ── Choice — Shopify's documented event, not Pandectes internals ────────── document.addEventListener('visitorConsentCollected', function (e) { var d = (e && e.detail && e.detail.customerPrivacy) || {}; track('consent_choice', 'choice-' + dayKey(), { analytics: d.analyticsProcessingAllowed === true, marketing: d.marketingAllowed === true }); }); // ── Cart attribute stamp (both arms — a one-armed experiment cannot be read) // Fired once per session when a cart exists. A cart created later on this // same page view gets stamped on the next navigation, which every checkout // path includes. function stampCart() { if (!bookkeeping) return; try { if (/(^|;\s*)cart=/.test(document.cookie) && !sessionStorage.getItem('kvce_stamped')) { sessionStorage.setItem('kvce_stamped', '1'); fetch('/cart/update.js', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ attributes: { _experiment_consent: KEY + ':' + arm } }) }).catch(function () { try { sessionStorage.removeItem('kvce_stamped'); } catch (e) {} }); } } catch (e) { /* never block the page on bookkeeping */ } } })();