:root{ --fg: #0b0c0f; --bg: #ffffff; --muted: rgba(0,0,0,0.58); --border: rgba(0,0,0,0.16); --accent: #0f766e; /* CTA */ --item-h: 64px; /* row height for readability */ } @media (prefers-color-scheme: dark) { :root{ --fg: #e9eaee; --bg: #0e0f12; --muted: rgba(233,234,238,0.65); --border: rgba(255,255,255,0.18); --accent: #2ec4b6; } } html, body { height: 100%; } body{ margin: 0; font: 18px/1.7 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; color: var(--fg); background: var(--bg); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .wrap { max-width: 1200px; margin: 0 auto; padding: 20px; } .title { margin: 0 0 12px; font-size: 30px; line-height: 1.2; font-weight: 800; letter-spacing: 0.2px; } .status { margin-top: 8px; font-size: 15px; color: var(--muted); } .card { background: transparent; border: 2px solid var(--border); border-radius: 16px; padding: 20px; } /* Reels layout — bigger & touch-friendly */ .reels { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; } @media (max-width: 1024px){ .reels{ grid-template-columns: repeat(2, 1fr); } } @media (max-width: 640px){ .reels{ grid-template-columns: 1fr; } } .reel { border: 2px solid var(--border); border-radius: 14px; overflow: hidden; background: transparent; perspective: 800px; /* depth */ box-shadow: 0 10px 24px rgba(0,0,0,0.08); } .header { display: grid; grid-template-columns: auto auto; align-items: center; gap: 12px; padding: 12px 14px; border-bottom: 2px solid var(--border); } .header strong { font-size: 18px; font-weight: 800; } .hold { font-size: 14px; color: var(--muted); user-select: none; } .hold input { width: 18px; height: 18px; vertical-align: middle; margin-left: 6px; } /* Window shows 5 rows while spinning, 1 row when compact */ .window { position: relative; overflow: hidden; height: calc(var(--item-h) * 5); mask-image: linear-gradient(to bottom, transparent 0, black 12px, black calc(100% - 12px), transparent 100%); -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 12px, black calc(100% - 12px), transparent 100%); transition: height 220ms ease; transform: translateZ(0); } .window.compact { height: var(--item-h); } /* single row when stopped */ .list { position: absolute; left: 0; top: 0; width: 100%; will-change: transform; transform-style: preserve-3d; } .item { height: var(--item-h); display: flex; align-items: center; padding: 0 14px; font-size: 18px; font-weight: 600; color: inherit; background: linear-gradient(180deg, rgba(255,255,255,0.65), transparent); border-bottom: 1px dashed var(--border); } .item.alt { background: transparent; } .controls { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 16px; } .btn { padding: 12px 16px; border-radius: 14px; border: 2px solid var(--border); background: transparent; color: inherit; cursor: pointer; font-weight: 700; font-size: 16px; transition: background 140ms ease, transform 140ms ease, box-shadow 140ms ease; touch-action: manipulation; } .btn:hover { background: rgba(0,0,0,0.06); } .btn.primary { background: var(--accent); color: #08110f; border-color: rgba(15,118,110,0.9); } .btn.primary:hover { transform: translateY(-1px); box-shadow: 0 8px 18px rgba(15,118,110,0.25); } @media (prefers-color-scheme: dark) { .btn.primary { color: #06100e; } } .winning-header { margin: 0 0 8px; font-size: 24px; font-weight: 900; } .grid { display: grid; gap: 20px; grid-template-columns: 1fr 1fr; } @media (max-width: 1024px){ .grid{ grid-template-columns: 1fr; } } article.work { border: 2px solid var(--border); border-radius: 14px; padding: 16px; } article.work h3 { margin: 0 0 8px; font-size: 22px; font-weight: 800; } .meta { font-size: 15px; color: var(--muted); display: flex; gap: 10px; flex-wrap: wrap; } .cta { margin-top: 10px; } .motion-blur { filter: blur(0.6px); } /* Landing bounce (brief overshoot) */ @keyframes landBounce { 0% { transform: translateY(var(--land)); } 60% { transform: translateY(calc(var(--land) + 6px)); } 100% { transform: translateY(var(--land)); } } .bounce { animation: landBounce 180ms ease-out forwards; } .reel.glow .header { box-shadow: 0 0 0 2px var(--accent) inset; }

/* Story Slots — full JS for WordPress page, using your backend payout endpoint */ /* 1) Reels pools */ const POOLS = { Character: ['Taxi driver','Mime','Cartographer','Night‑shift nurse','Alien','Assassin','Time traveler','Bohemian'], Situation: ['Gets a cryptic letter','Runs into an old rival','Invents a wildly popular food','Is trapped','Returns to hometown','Witnesses a crime','Goes on a journey'], Object: ['Lucky penny','Silver quill','Hourglass','Old photograph','Key','Tarot cards','Hidden note','Journal'], Setting: ['Outskirts farm','Cow shed','Town square','Creek at dawn','Dock at night','Library at noon','Porch in summer'] }; /* 2) Helpers to call your payout endpoint and format text */ async function fetchPayout(tags, mood){ const url = `${window.location.origin}/wp-json/reactink/v1/payout`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tags, mood: mood || null }) }); if(!res.ok) throw new Error('Payout fetch failed'); return res.json(); } function snippetFromExcerpt(text, maxWords = 22){ if(!text) return ''; const words = text.replace(/\s+/g,' ').trim().split(' '); const cut = words.slice(0, maxWords).join(' '); const endPunct = /[.?!…]$/.test(cut); return endPunct ? cut : cut + '…'; } /* 3) Build reels UI */ const reelsEl = document.getElementById('reels'); const reels = []; Object.keys(POOLS).forEach((name) => { const div = document.createElement('div'); div.className = 'reel'; div.innerHTML = `
${name}
${POOLS[name].map(v => `
${v}
`).join('')}
`; reelsEl.appendChild(div); reels.push({ name, container: div, list: div.querySelector(`#list-${name}`), windowEl: div.querySelector('.window'), items: POOLS[name].slice(), hold: false, current: POOLS[name][0], itemHeight: parseFloat(getComputedStyle(div.querySelector('.item')).height) || 64 }); }); reelsEl.querySelectorAll('input[type="checkbox"]').forEach(cb => { cb.addEventListener('change', e => { const r = reels.find(x => x.name === e.target.dataset.reel); r.hold = e.target.checked; }); }); /* 4) Mood + picking */ const status = document.getElementById('status'); const selectionEl = document.getElementById('selection'); const resultsEl = document.getElementById('results'); let moodPref = null; // 'quieter' | 'brighter' | null function pick(arr){ return arr[Math.floor(Math.random()*arr.length)]; } function weightedPick(items, pref, reelName){ if(!pref) return pick(items); const weights = items.map(v => { let w = 1; const s = v.toLowerCase(); if(pref==='quieter'){ if(reelName==='Setting' && (s.includes('dawn')||s.includes('porch')||s.includes('cow shed')||s.includes('creek'))) w+=1.2; if(reelName==='Situation' && (s.includes('letter')||s.includes('journey'))) w+=0.5; } else if(pref==='brighter'){ if(reelName==='Setting' && (s.includes('square')||s.includes('library')||s.includes('summer'))) w+=1.2; if(reelName==='Situation' && (s.includes('invent')||s.includes('returns'))) w+=0.5; } return w; }); const total = weights.reduce((a,b)=>a+b,0); let r = Math.random()*total; for(let i=0;i { const html = r.items.map((v,i) => `
${v}
`).join(''); r.list.innerHTML = html + html + html + html; // 4× repeat r.itemHeight = parseFloat(getComputedStyle(r.list.querySelector('.item')).height) || r.itemHeight; r.cycleHeight = r.items.length * r.itemHeight; r.list.style.transform = 'translateY(0px)'; }); } upgradeListsForSpin(); /* 6) Spin orchestration */ const SPEED_MULT = [0.9, 0.98, 1.03, 1.08]; const START_DELAY = [0, 120, 240, 360]; const BASE_PX_MS = 0.6; const BLUR_ON = 1.3; const STOP_JITTER = 280; const LAND_FLICKER = true; const SPIN = { baseLoops: [1.5, 2.1, 2.7, 3.3], extraRange: [0.15, 0.6], decelMs: [900, 1150, 1400, 1650], }; function smoothstep(t){ return t*t*(3 - 2*t); } function getCurrentOffsetPx(el){ const m = /translateY\((-?\d+(\.\d+)?)px\)/.exec(el.style.transform || ''); return m ? parseFloat(m[1]) : 0; } function spinReelSmooth(reel, index, moodPref){ return new Promise(resolve => { if (reel.hold){ resolve({ reel, choice: reel.current }); return; } reel.windowEl.classList.remove('compact'); const choice = weightedPick(reel.items, moodPref, reel.name); const targetIndex = reel.items.indexOf(choice); reel.current = choice; const loopsMin = SPIN.baseLoops[index % SPIN.baseLoops.length]; const loopsExtra = SPIN.extraRange[0] + Math.random() * (SPIN.extraRange[1] - SPIN.extraRange[0]); const constantLoops = loopsMin + loopsExtra; const decelMs = SPIN.decelMs[index % SPIN.decelMs.length] + Math.floor(Math.random()*STOP_JITTER); const pxPerMs = BASE_PX_MS * SPEED_MULT[index % SPEED_MULT.length]; const startDelay = START_DELAY[index % START_DELAY.length]; let startOffset = getCurrentOffsetPx(reel.list); const cycle = reel.cycleHeight; startOffset = ((startOffset % cycle) + cycle) % cycle; const phaseAAdvance = constantLoops * cycle; const phaseBEndOffset = - (targetIndex * reel.itemHeight); const startTime = performance.now() + startDelay; let lastNow = startTime; let phase = 'A'; let phaseBStartOffset = startOffset; let phaseBStartTime = startTime; const READABLE_MS = 160; const READABLE_PX_MS = 0.25; reel.container.classList.add('glow'); function step(now){ if(now < startTime){ requestAnimationFrame(step); return; } const dt = Math.max(1, now - lastNow); lastNow = now; if(phase === 'A'){ const jitter = (Math.random() - 0.5) * 0.5; const advance = (pxPerMs + jitter) * dt; startOffset -= advance; const wrapped = ((startOffset % (cycle*4)) + (cycle*4)) % (cycle*4); reel.list.style.transform = `translateY(${wrapped}px)`; if(pxPerMs > BLUR_ON) reel.list.classList.add('motion-blur'); else reel.list.classList.remove('motion-blur'); if (Math.abs(startOffset) >= phaseAAdvance){ phase = 'B-readable'; phaseBStartOffset = startOffset; phaseBStartTime = now; } requestAnimationFrame(step); return; } if(phase === 'B-readable'){ const tR = (now - phaseBStartTime); const advance = READABLE_PX_MS * dt; phaseBStartOffset -= advance; const wrapped = ((phaseBStartOffset % (cycle*4)) + (cycle*4)) % (cycle*4); reel.list.style.transform = `translateY(${wrapped}px)`; reel.list.classList.remove('motion-blur'); if(tR >= READABLE_MS){ phase = 'B-ease'; phaseBStartTime = now; } requestAnimationFrame(step); return; } const t = Math.min(1, (now - phaseBStartTime) / decelMs); const eased = smoothstep(t); const current = phaseBStartOffset + (phaseBEndOffset - phaseBStartOffset) * eased; const wrapped = ((current % (cycle*4)) + (cycle*4)) % (cycle*4); reel.list.style.transform = `translateY(${wrapped}px)`; if(t > 0.25) reel.list.classList.remove('motion-blur'); if(t < 1){ requestAnimationFrame(step); } else { const finalSnap = - (targetIndex * reel.itemHeight); reel.list.style.setProperty('--land', finalSnap + 'px'); reel.list.style.transform = `translateY(${finalSnap}px)`; reel.container.classList.remove('glow'); if(LAND_FLICKER){ reel.container.classList.add('glow'); setTimeout(() => reel.container.classList.remove('glow'), 160); } reel.list.classList.add('bounce'); setTimeout(() => reel.list.classList.remove('bounce'), 200); reel.windowEl.classList.add('compact'); resolve({ reel, choice }); } } requestAnimationFrame(step); }); } async function spinAllSmooth(){ status.textContent = 'Spinning…'; selectionEl.textContent = ''; reels.forEach(r => r.windowEl.classList.remove('compact')); const spins = reels.map((reel, i) => spinReelSmooth(reel, i, moodPref)); await Promise.all(spins); finalizeSelection(); } /* 7) Fingerprint tags for payout */ function fingerprintTags(fp){ const tags = []; if(fp.Character) tags.push(fp.Character.toLowerCase().split(' ')[0]); if(fp.Object) tags.push(fp.Object.toLowerCase().split(' ')[0]); if(fp.Situation){ const s = fp.Situation.toLowerCase(); if(s.includes('letter')) tags.push('letter'); if(s.includes('rival')) tags.push('rival'); if(s.includes('trapped')) tags.push('confinement'); if(s.includes('journey')) tags.push('journey'); if(s.includes('returns')) tags.push('return'); if(s.includes('crime')) tags.push('crime'); } if(fp.Setting){ const map = { 'Outskirts farm':'farm','Cow shed':'cow-shed','Town square':'square','Creek at dawn':'creek','Dock at night':'dock','Library at noon':'library','Porch in summer':'porch' }; tags.push(map[fp.Setting]); if(fp.Setting.includes('dawn')) tags.push('dawn'); if(fp.Setting.includes('night')) tags.push('night'); if(fp.Setting.includes('summer')) tags.push('summer'); } return tags.filter(Boolean); } /* 8) Render payout using backend */ async function renderPayout(fp, moodPref){ const tags = fingerprintTags(fp); try { const data = await fetchPayout(tags, moodPref); const two = [data.short, data.long].filter(Boolean); const html = two.map(w => `

${w.title}

${w.form} ~${w.readMin} min