@liveloop · 5/15/2026, 2:13:08 PM
Initial version — all lines are new.
+<style>+ html, body { background: #fef9c3; color: #111; font-family: system-ui, sans-serif; }+ .wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 0.75rem; padding: 1rem; }+ h1 { margin: 0; font-size: 0.875rem; letter-spacing: 0.15em; text-transform: uppercase; color: #71717a; font-weight: 600; }+ .grid { display: grid; gap: 0.5rem; }+ /* Cards are real <button>s (reliable tap target) with the native+ button chrome reset away. A face-down card is genuinely empty —+ the emoji text is only added when flipped. The old approach hid+ the emoji with color:transparent and revealed it by switching the+ color back, which did not repaint emoji glyphs on mobile Safari+ (flipped cards showed blank white). */+ .card { appearance: none; -webkit-appearance: none; border: 0; margin: 0; padding: 0; font-family: inherit; color: inherit; aspect-ratio: 1; display: flex; align-items: center; justify-content: center; border-radius: 0.5rem; background: #fde047; font-size: 1.5rem; line-height: 1; cursor: pointer; user-select: none; -webkit-user-select: none; touch-action: manipulation; transition: background-color 0.15s; }+ .card.flipped { background: #fff; }+ .card .face-img { width: 82%; height: 82%; object-fit: cover; border-radius: 0.35rem; }+ .card.matched { background: #86efac; cursor: default; }+ .status { font-size: 0.875rem; color: #52525b; }+ .again { padding: 0.5rem 1rem; border-radius: 9999px; border: 0; background: #18181b; color: white; font-weight: 500; cursor: pointer; display: none; }+ .again.visible { display: inline-block; }+</style>+<div class="wrap">+ <h1>Memory: emojis</h1>+ <div class="grid" id="grid"></div>+ <div class="status" id="status">Tap a card to begin</div>+ <button class="again" id="again">Play again</button>+</div>+<script>+(function(){+ var pairs = ["🌟","🌈","🍕","🎲","🦊","🚀"];+ function isImg(v){ return typeof v === 'string' && v.indexOf('https://') === 0; }+ function setFace(card, v){+ if (isImg(v)) {+ var img = document.createElement('img');+ img.src = v; img.alt = ''; img.className = 'face-img';+ card.innerHTML = ''; card.appendChild(img);+ } else { card.textContent = v; }+ }+ function clearFace(card){ card.innerHTML = ''; }+ var grid = document.getElementById('grid');+ var statusEl = document.getElementById('status');+ var againBtn = document.getElementById('again');+ var deck, flipped, matched, moves;++ // Tap handling via pointer events instead of 'click'. Inside the+ // scroll-snap feed on mobile, the browser frequently classifies a+ // touch on a small target as a scroll and never synthesizes the+ // 'click' — so cards looked dead. pointerdown/up fire regardless;+ // we just confirm the finger barely moved (a tap, not a drag) and+ // bail on pointercancel (the browser took the gesture for scroll).+ function onTap(el, handler) {+ var sx = 0, sy = 0, armed = false;+ el.addEventListener('pointerdown', function(e){+ armed = true; sx = e.clientX; sy = e.clientY;+ });+ el.addEventListener('pointercancel', function(){ armed = false; });+ el.addEventListener('pointerup', function(e){+ if (!armed) return;+ armed = false;+ if (Math.abs(e.clientX - sx) < 14 && Math.abs(e.clientY - sy) < 14) {+ handler();+ }+ });+ }++ function setup() {+ deck = pairs.concat(pairs).sort(function(){ return Math.random() - 0.5; });+ flipped = [];+ matched = 0;+ moves = 0;+ var cols = pairs.length <= 4 ? 4 : 4;+ grid.style.gridTemplateColumns = 'repeat(' + cols + ', minmax(0, 1fr))';+ grid.style.width = 'min(70vmin, 320px)';+ grid.innerHTML = '';+ deck.forEach(function(emoji, i){+ var card = document.createElement('button');+ card.type = 'button';+ card.className = 'card';+ card.dataset.idx = i;+ card.dataset.emoji = emoji;+ // Face-down: empty. The emoji is added on flip (see flip()).+ card.textContent = '';+ onTap(card, function(){ flip(card); });+ grid.appendChild(card);+ });+ statusEl.textContent = 'Tap a card to begin';+ againBtn.classList.remove('visible');+ }++ function flip(card) {+ if (card.classList.contains('flipped') || card.classList.contains('matched')) return;+ if (flipped.length >= 2) return;+ card.classList.add('flipped');+ setFace(card, card.dataset.emoji);+ flipped.push(card);+ if (flipped.length === 2) {+ moves++;+ var a = flipped[0], b = flipped[1];+ if (a.dataset.emoji === b.dataset.emoji) {+ a.classList.add('matched');+ b.classList.add('matched');+ matched += 2;+ flipped = [];+ if (matched === deck.length) {+ statusEl.textContent = '✓ Cleared in ' + moves + ' moves';+ againBtn.classList.add('visible');+ }+ } else {+ setTimeout(function(){+ a.classList.remove('flipped');+ b.classList.remove('flipped');+ clearFace(a);+ clearFace(b);+ flipped = [];+ }, 700);+ }+ }+ }++ onTap(againBtn, setup);+ setup();+})();+</script>+<script>/*ll-media-controls*/+(function(){+ function wire(){+ var ll = window.liveloop; if(!ll || !ll.declareMedia) return;+ var nodes = [].slice.call(document.querySelectorAll('video,audio')).filter(function(el){+ return !(el.id && el.id.indexOf('ll-')===0) && !el.hasAttribute('data-ll-unmanaged');+ });+ if(!nodes.length) return;+ var hasVideo = nodes.some(function(el){ return el.tagName==='VIDEO'; });+ ll.declareMedia({ sound:true, playback:hasVideo });+ var paused=false;+ function play(el){ try{ var p=el.play&&el.play(); if(p&&p.catch)p.catch(function(){}); }catch(e){} }+ function applyMuted(m){ nodes.forEach(function(el){ try{ el.muted=m; }catch(e){} if(!m&&!paused) play(el); }); }+ function applyPaused(p){ paused=p; nodes.forEach(function(el){ try{ if(p){ el.pause&&el.pause(); } else { play(el); } }catch(e){} }); }+ applyMuted(!!ll.muted);+ if(ll.onMute) ll.onMute(applyMuted);+ if(ll.onPause) ll.onPause(applyPaused);+ }+ if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', wire); } else { wire(); }+})();+</script>