). // // The CSS-only "stretched link" approach was tried first but couldn't // work here: .mm-bento__foot / .mm-bento__head already have // `position: relative; z-index: 2` (set so they sit above the locked // card's diagonal-lattice ::after), so a `.mm-bento__cta::after` // resolves against the foot — not the card — and never reaches the // top half of the tile. Touching those z-indices would risk breaking // the lock overlay; instead we navigate via JS. // // Click rules: // • Locked cards: no handler attached → nothing happens (no error). // • Live cards: handler navigates UNLESS the click started on a // real / inside (e.g. the VIEW MONSTER link). That // keeps the inner CTA fully native — Cmd+click for a new tab, // middle-click, right-click context menu all work because the // wrapper handler simply yields when the inner link is the // actual target. const onCardClick = (e) => { if (!href) return; if (e.defaultPrevented) return; // Don't hijack modifier-clicks / non-primary buttons — let the // browser handle Cmd/Ctrl/Shift/middle-click on the wrapper too. if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; // If the click landed on a real interactive descendant (inner // or ), let that element's native behaviour take over. if (e.target.closest('a, button')) return; window.location.href = href; }; const onCardKey = (e) => { if (!href) return; // Native links inside (VIEW MONSTER) handle their own Enter. if (e.target.closest('a, button')) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); window.location.href = href; } }; return ( {/* TOP — badge + product name + character name */} {m.badge.label} {m.sku} {m.sub ? 「{m.sub}」 : null} {/* MIDDLE — character (left) + package (right), baseline-aligned, no overlap */} {m.video ? ( // WebM (currently non-alpha, VP8 + yuv420p, black bg). // No mix-blend-mode — character renders in its true colors. The // CSS `mask-image: radial-gradient(...)` on .mm-bento__char-video // softly fades the corners of the video out, hiding the black // background against whatever card color sits behind it. // When alpha-channel WebMs ship the mask can be relaxed/removed. {m.photo ? : null} ) : m.photo ? ( ) : ( )} {m.pkg ? ( ) : ( PACKAGE TBD )} {/* BOTTOM — tagline + CTA */} {m.tag} {locked ? ( → COMING SOON ) : ( → VIEW MONSTER )} ); } // PAIN counter — LIVE TICKER. Already high on arrival (grows with real time // since launch at a slow ~+1/hour pace so it stays in the 150,000s for the // brand promise), then ticks up +1–3 every 2–5s while on screen with a // damage-number "+N" pop, a heartbeat, a bounce, and a red glow pulse. function computeLivePainCount() { const BASE = 1526; const REF_DATE = new Date('2026-06-04T00:00:00Z').getTime(); const elapsedHours = Math.max(0, (Date.now() - REF_DATE) / (1000 * 60 * 60)); return BASE + Math.floor(elapsedHours); // ~ +1 per hour — stays in the 1,500s } function PainCounterCard() { const ref = _useRef(null); const numRef = _useRef(null); const wrapRef = _useRef(null); const glowRef = _useRef(null); const timerRef = _useRef(null); const [val, setVal] = _useState(computeLivePainCount); _useEffect(() => { const el = ref.current; if (!el) return; let active = false; const reduceMotion = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); // (c) bounce the whole number const bounce = () => { const n = numRef.current; if (!n) return; n.classList.remove('is-bump'); void n.offsetWidth; // reflow to restart n.classList.add('is-bump'); }; // (b) damage-number "+N" that floats up and fades const spawnFloat = (inc) => { const wrap = wrapRef.current; if (!wrap) return; const f = document.createElement('span'); f.className = 'mm-counter__float'; f.textContent = '+' + inc; f.style.setProperty('--fx', (Math.random() * 26 - 9).toFixed(0) + 'px'); wrap.appendChild(f); setTimeout(() => { if (f.parentNode) f.parentNode.removeChild(f); }, 1000); }; // (d) red glow vignette pulse const glowPulse = () => { const g = glowRef.current; if (!g) return; g.classList.remove('is-pulse'); void g.offsetWidth; g.classList.add('is-pulse'); }; const schedule = () => { timerRef.current = setTimeout(tick, 2000 + Math.random() * 3000); // 2–5s }; function tick() { const inc = 1 + Math.floor(Math.random() * 3); // 1–3 setVal((v) => v + inc); if (!reduceMotion) { bounce(); spawnFloat(inc); glowPulse(); } schedule(); } const start = () => { if (active) return; active = true; el.classList.add('is-live'); // (a) enables the heartbeat loop schedule(); }; const stop = () => { active = false; el.classList.remove('is-live'); if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } }; const inView = () => { const r = el.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; return r.top < vh && r.bottom > 0; }; // Start WITHOUT depending on IntersectionObserver (silent in some iframes): // start if in view, scroll fallback, plus a safety timer. if (inView()) start(); const onScroll = () => { if (inView()) start(); else stop(); }; window.addEventListener('scroll', onScroll, { passive: true }); const safety = setTimeout(() => { if (!active) start(); }, 1500); // IntersectionObserver ONLY pauses off-screen / resumes on-screen. let io; try { io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) start(); else stop(); }); }, { threshold: 0 }); io.observe(el); } catch (_) {} return () => { stop(); window.removeEventListener('scroll', onScroll); clearTimeout(safety); if (io) io.disconnect(); }; }, []); return ( MONSTERS HAVE EATEN モンスター達が食べた、PAIN の数 {val.toLocaleString()}+ PAINS ); } function BentoGrid() { const live = MONSTERS.filter(m => m.live); const soon = MONSTERS.filter(m => !m.live); return ( {/* Section title — added so the bento reads unambiguously as the product index. Keeps the small chapter line (03 / MONSTERS) and the right-side eyebrow (モンスターたち · CHOOSE YOUR HUNGER) intact below, as a sub-header. */} PRODUCTS あなたのPAINに合う一体を、選ぼう。 {live.map((m) => )} {soon.map((m) => )} ); } // ============= PAIN counter banner — full-width capstone ============= function PainCounterBanner() { return ( ); } Object.assign(window, { AwardsRow, BentoGrid, PainCounterBanner, CharMark });
{/* TOP — badge + product name + character name */} {m.badge.label} {m.sku} {m.sub ? 「{m.sub}」 : null} {/* MIDDLE — character (left) + package (right), baseline-aligned, no overlap */} {m.video ? ( // WebM (currently non-alpha, VP8 + yuv420p, black bg). // No mix-blend-mode — character renders in its true colors. The // CSS `mask-image: radial-gradient(...)` on .mm-bento__char-video // softly fades the corners of the video out, hiding the black // background against whatever card color sits behind it. // When alpha-channel WebMs ship the mask can be relaxed/removed. {m.photo ? : null} ) : m.photo ? ( ) : ( )} {m.pkg ? ( ) : ( PACKAGE TBD )} {/* BOTTOM — tagline + CTA */} {m.tag} {locked ? ( → COMING SOON ) : ( → VIEW MONSTER )}