diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..9afaf4a --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,13 @@ +name: check +on: + push: + pull_request: +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm run check diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index a12ab71..ee5ff31 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -4,7 +4,7 @@ CATMEMECHIMP is open-source software. The MIT license in this repository covers ## Meme template images -The 24 files in `public/assets/memes/` are community meme templates obtained from Imgflip. Each item in `public/library.json` records the template name and its individual Imgflip source page. The application also exposes that source link in the focused viewer. +The 31 files in `public/assets/memes/` are community meme templates obtained from Imgflip. Each item in `public/library.json` records the template name and its individual Imgflip source page. The application also exposes that source link in the focused viewer. Imgflip is a user-generated-content platform and may not own every image uploaded to it. The inclusion of a source link is attribution and provenance, not a claim that Imgflip or this project can relicense the underlying image. These images are bundled to support the project's caption remixes and offline experience. They are expressly excluded from the repository's MIT license. @@ -13,6 +13,14 @@ Before using or redistributing an image separately, determine whether your use i - Imgflip terms: https://imgflip.com/terms - Imgflip cat-template search: https://imgflip.com/memesearch?q=cat +## TV bumper clips + +The MP4 files in `public/assets/clips/` are short, muted excerpts transcoded locally with ffmpeg for size. They are bundled under their original Creative Commons licenses, which are not replaced by this repository's MIT license: + +- `calico.mp4` — excerpt of "Calico kitten playing" by باسم, CC BY-SA 4.0. Source: https://commons.wikimedia.org/wiki/File:Calico_kitten_playing.webm +- `tokyo.mp4` — excerpt of "Kitten playing - Tokyo - Jan 7 2020" by Nesnad, CC BY 4.0. Source: https://commons.wikimedia.org/wiki/File:Kitten_playing_-_Tokyo_-_Jan_7_2020.webm +- `gatos.mp4` — excerpt of "Gatos hmnos" by EEIM, CC BY-SA 4.0. Source: https://commons.wikimedia.org/wiki/File:Gatos_hmnos.webm + ## Fonts The WOFF2 files in `public/assets/fonts/` are DM Sans and Fraunces files downloaded through Google Fonts. Both families are distributed under the SIL Open Font License 1.1. Their license applies to the font files; the application's MIT license does not replace it. diff --git a/package.json b/package.json index ca0d91a..fbe13c7 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,5 @@ "bugs": {"url": "https://github.com/burhama/CatMemeChimp/issues"}, "homepage": "https://github.com/burhama/CatMemeChimp#readme", "engines": {"node": ">=22"}, - "scripts": {"start": "node server.mjs", "check": "node --check server.mjs && node --check public/app.js"} + "scripts": {"start": "node server.mjs", "smoke": "node scripts/smoke.mjs", "check": "node --check server.mjs && node --check public/app.js && node scripts/smoke.mjs"} } diff --git a/public/app.js b/public/app.js index da1ec00..9ada7d5 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,5 @@ const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)]; -const paths={grid:'',dense:'',heart:'',folder:'',sparkles:'',shuffle:'',play:'',pause:'',keyboard:'',search:'',plus:'','arrow-right':'','arrow-left':'','arrow-down':'',x:'',download:'',upload:'',edit:'',link:'',check:''}; +const paths={grid:'',dense:'',moon:'',sun:'',trophy:'',flask:'',folderplus:'',volume:'',mute:'',book:'',package:'',tv:'',heart:'',folder:'',sparkles:'',shuffle:'',play:'',pause:'',keyboard:'',search:'',plus:'','arrow-right':'','arrow-left':'','arrow-down':'',x:'',download:'',upload:'',edit:'',link:'',check:''}; const icon=n=>``; const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); function hydrateIcons(root=document){root.querySelectorAll('[data-icon]').forEach(e=>e.innerHTML=icon(e.dataset.icon));} @@ -10,111 +10,430 @@ const moodTag=m=>`${moodInfo(m)[1 const readState=(k,d)=>{try{return JSON.parse(localStorage.getItem('cmc-'+k))??d}catch{return d}}; const asIds=v=>new Set(Array.isArray(v)?v.filter(x=>typeof x==='string'):[]); let saved=asIds(readState('saved',[])),seen=asIds(readState('seen',[])),created=[],library=[],page='discover',mood='all',query='',collection='',limit=18,order=[]; -let viewerQueue=[],viewerIndex=0,autoTimer=null,toastTimer=null,editing=null,uploadedTemplate=null,dbPromise=null; +function currentSeason(){const d=new Date();return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`} +function seasonLabel(s){const[y,m]=s.split('-').map(Number);return new Date(y,m-1,1).toLocaleDateString([],{month:'long',year:'numeric'})} +function daysLeftInSeason(){const d=new Date();return new Date(d.getFullYear(),d.getMonth()+1,0).getDate()-d.getDate()} +function readBattles(){const d=readState('battles',{wins:{},plays:{},battles:0,streak:0,best:0,lastWinner:null,lastChamp:null,season:currentSeason(),hall:[]});return {wins:{...d.wins||{}},plays:{...d.plays||{}},battles:d.battles|0,streak:d.streak|0,best:d.best|0,lastWinner:typeof d.lastWinner==='string'?d.lastWinner:null,lastChamp:typeof d.lastChamp==='string'?d.lastChamp:null,season:typeof d.season==='string'?d.season:currentSeason(),hall:Array.isArray(d.hall)?d.hall:[]}} +function rollSeason(){const cur=currentSeason();if(battleStats.season===cur)return;const prev=champId(); + if(prev&&battleStats.battles>0){const pm=allMemes().find(m=>m.id===prev);battleStats.hall=[{season:battleStats.season,champ:prev,wins:battleWins(prev)},...battleStats.hall].slice(0,12);setTimeout(()=>toast(prev?`📜 New season! “${pm?.title||'Cat'}” enters the hall of fame.`:'📜 New season! Fresh board.'),600)} + battleStats={wins:{},plays:{},battles:0,streak:0,best:0,lastWinner:null,lastChamp:null,season:cur,hall:battleStats.hall};setLocal('battles',battleStats)} +let battleStats=readBattles(),battleLeft=null,battleRight=null; +const battleWins=id=>battleStats.wins[id]|0; +const battlePlays=id=>battleStats.plays[id]|0; +const battleLosses=id=>Math.max(0,battlePlays(id)-battleWins(id)); +const battleRate=id=>{const p=battlePlays(id);return p?Math.round(battleWins(id)/p*100):null}; +function pickBattle(){const pool=allMemes();if(pool.length<2)return;let a=battleLeft,b=Math.random()<0.5?battleLeft:battleRight; + for(let i=0;i<20;i++){const x=pool[Math.floor(Math.random()*pool.length)],y=pool[Math.floor(Math.random()*pool.length)];if(x.id!==y.id&&(x.id!==a?.id||y.id!==b?.id)){a=x;b=y;break}} + if(!a||!b||a.id===b.id){const s=shuffle(pool);a=s[0];b=s[1]} + battleLeft=a;battleRight=b; +} +const battleTaunts=['The crowd goes mild.','A historic moment for cats.','Democracy works. No recount needed.','The chimp nods approvingly.','Questionable behavior, rewarded.','A landslide. Somehow still about naps.','The other cat demands a recount. Denied.','Instant classic. Frame it.','The judges (you) have spoken.','That one had main-character energy.','Science can’t explain this one.','Straight into the hall of fame.']; +function voteBattle(winnerId){if(!battleLeft||!battleRight)return; + battleStats.plays[battleLeft.id]=battlePlays(battleLeft.id)+1;battleStats.plays[battleRight.id]=battlePlays(battleRight.id)+1; + battleStats.wins[winnerId]=battleWins(winnerId)+1;battleStats.battles++;battleStats.streak++;battleStats.best=Math.max(battleStats.best,battleStats.streak);battleStats.lastWinner=winnerId; + setLocal('battles',battleStats);checkAwards(); + const w=allMemes().find(m=>m.id===winnerId); + const ms=streakMilestones[battleStats.streak]; + const champ=champId(),dethroned=champ&&battleStats.lastChamp&&champ!==battleStats.lastChamp&&battleStats.battles>3; + battleStats.lastChamp=champ;setLocal('battles',battleStats); + if(dethroned){const c=allMemes().find(m=>m.id===champ);sfx('fanfare');confetti();toast(`👑 DETHRONED! “${c?.title||'Cat'}” seizes the crown!`)} + else{sfx(ms?'fanfare':'pop');if(ms)confetti();toast(ms||`“${w?.title||'Cat'}” takes the crown. ${battleTaunts[Math.floor(Math.random()*battleTaunts.length)]}`)} + pickBattle();renderArena(); +} +function skipBattle(){if(battleStats.streak>0){toast('Streak reset. The cats judge your indecision.');sfx('chaos')}battleStats.streak=0;setLocal('battles',battleStats);pickBattle();renderArena()} +function resetArena(){if(!battleStats.battles){toast('No battles yet — crown a cat first.');return}if(!window.confirm('Reset your Arena record? Wins, streaks and the leaderboard go back to zero.'))return;battleStats={wins:{},plays:{},battles:0,streak:0,best:0,lastWinner:null,lastChamp:null,season:battleStats.season,hall:battleStats.hall};setLocal('battles',battleStats);pickBattle();renderArena();toast('Fresh slate. The cats forgive you.')} +function rankBoard(){return [...allMemes()].map(m=>({m,w:battleWins(m.id),p:battlePlays(m.id)})).filter(x=>x.p>0).sort((a,b)=>b.w-a.w||(b.w/b.p)-(a.w/a.p)||a.m.title.localeCompare(b.m.title))} +const champId=()=>rankBoard()[0]?.m.id||null; +const streakMilestones={5:'Five crowns in a row. The cats whisper your name.',10:'TEN STREAK. Certified cat judge.',25:'25 straight crowns. Unhinged. Legendary.',50:'50?! Touch grass. Then crown more cats.'}; +let labTop=null,labBottom=null,labCat=null,labEngine=null; +const randMeme=()=>allMemes()[Math.floor(Math.random()*allMemes().length)]; +function rollLab(part){if(!allMemes().length)return;labEngine=null;const withTop=()=>{let m=randMeme();for(let i=0;i<10&&!m.top;i++)m=randMeme();return m};const withBottom=()=>{let m=randMeme();for(let i=0;i<10&&!m.bottom;i++)m=randMeme();return m}; + if(!labTop||part==='top'||part==='all')labTop=withTop();if(!labBottom||part==='bottom'||part==='all')labBottom=withBottom();if(!labCat||part==='cat'||part==='all')labCat=randMeme();renderLab()} +function labMeme(){return {title:`${labTop?.title||'Top'} × ${labBottom?.title||'Bottom'}`,top:labEngine?.top||labTop?.top||'',bottom:labEngine?.bottom||labBottom?.bottom||'',image:labCat?.image||'',template:labCat?`Frankenmeme (${labCat.template})`:'Frankenmeme',mood:labCat?.mood||'chaos'}} +function curseRating(){const n=(labTop?.top.length||0)+(labBottom?.bottom.length||0)*3+(labCat?.id.length||0)*7;return (n%5)+1} +function renderLab(){if(!labTop||!labBottom||!labCat){rollLab();return}const m=labMeme();$('#lab-preview').innerHTML=visual(m,'eager'); + const stars='★'.repeat(curseRating())+'☆'.repeat(5-curseRating()); + const names=['blessed','mildly cursed','cursed','deeply cursed','FORBIDDEN']; + $('#lab-provenance').innerHTML=`Above the cat: ${labEngine?'🤖 The Engine':esc(labTop.title)}Below the cat: ${labEngine?'🤖 The Engine':esc(labBottom.title)}The cat: ${esc(labCat.title)}${stars} ${names[curseRating()-1]}`} +function inspireLab(){if(!labTop||!labBottom||!labCat)rollLab();labEngine=inspireCaption();renderLab();sfx('blip');toast('The machine dreamed this. No refunds.')} +async function saveLab(){const m={...labMeme(),id:'own-'+crypto.randomUUID(),kind:'creation',added:new Date().toISOString(),tags:['frankenmeme','my creation']};try{await dbPut(m);created.push(m);labSaves++;setLocal('stats',{labSaves});sfx('fanfare');checkAwards();navigate('studio');toast('Abomination preserved for future generations.')}catch{toast('Could not save in this browser. Use PNG to keep your creation.')}} +function readSets(){const d=readState('sets',[]);return Array.isArray(d)?d.filter(s=>s&&typeof s.id==='string'&&typeof s.name==='string'&&Array.isArray(s.ids)):[]} +let userSets=readSets(); +const saveSets=()=>setLocal('sets',userSets); +const setById=id=>userSets.find(s=>s.id===id); +function createSet(name){const s={id:'set-'+crypto.randomUUID(),name:String(name||'').trim().slice(0,40)||'Untitled set',ids:[]};userSets.push(s);saveSets();return s} +function deleteSet(id){userSets=userSets.filter(s=>s.id!==id);if(collection===id)collection='';saveSets()} +function toggleInSet(setId,memeId){const s=setById(setId);if(!s)return false;const has=s.ids.includes(memeId);s.ids=has?s.ids.filter(x=>x!==memeId):[...s.ids,memeId];saveSets();return !has} +function renderSets(){const p=$('#sets-panel');if(!p)return; + p.innerHTML=`

Your sets

Hoard with purpose. Sets live in this browser — or in a file, if you export them.

${userSets.length?userSets.map(s=>`
`).join(''):'

No sets yet. Name one above — future you says thanks.

'}`} +function exportSets(){if(!userSets.length){toast('No sets to export. Hoard first.');return}const blob=new Blob([JSON.stringify({app:'CATMEMECHIMP-sets',version:1,exported:new Date().toISOString(),sets:userSets.map(s=>({name:s.name,ids:s.ids}))},null,2)],{type:'application/json'});const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='catmeme-sets.json';document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),5000);toast(`Exported ${userSets.length} ${userSets.length===1?'set':'sets'}. Send it to a friend.`)} +async function importSets(file){if(!file)return;try{const data=JSON.parse(await file.text());const arr=Array.isArray(data)?data:data.sets;if(!Array.isArray(arr))throw new Error('bad file');const known=new Set(allMemes().map(m=>m.id));let added=0; + for(const s of arr){if(!s||typeof s.name!=='string'||!Array.isArray(s.ids))continue;const ids=[...new Set(s.ids.filter(id=>typeof id==='string'&&known.has(id)))];let name=s.name.trim().slice(0,40)||'Untitled set';if(userSets.some(u=>u.name===name))name=`${name} (imported)`;userSets.push({id:'set-'+crypto.randomUUID(),name,ids});added++} + if(!added)throw new Error('bad file');saveSets();if(page==='collections')render();toast(`Imported ${added} ${added===1?'set':'sets'}. Sharing is caring.`)}catch{toast('That file isn’t a CatMemeChimp set export.')}} +function renderCollect(){const m=currentMeme();const box=$('#collect-list');if(!box||!m)return; + box.innerHTML=userSets.length?userSets.map(s=>``).join(''):'

No sets yet — create one below.

'} +const PACK_MS=24*60*60*1000; +function readPack(){const d=readState('pack',{lastOpened:0,streak:0,best:0,haul:{},lastPull:null,scrap:0});return {lastOpened:d.lastOpened|0,streak:d.streak|0,best:d.best|0,haul:{...d.haul||{}},lastPull:d.lastPull||null,scrap:d.scrap|0}} +let pack=readPack(),packStage='sealed',packReveal=null,keepTab='sets'; +const savePack=()=>setLocal('pack',pack); +const haulSize=()=>Object.values(pack.haul).reduce((a,b)=>a+(b|0),0); +const canOpenPack=()=>Date.now()-(pack.lastOpened||0)>=PACK_MS; +function packCountdown(){const left=PACK_MS-(Date.now()-(pack.lastOpened||0));if(left<=0)return 'Ready!';const h=Math.floor(left/3600000),m=Math.floor(left%3600000/60000),s=Math.floor(left%60000/1000);return `${h}h ${String(m).padStart(2,'0')}m ${String(s).padStart(2,'0')}s`} +function rollRarity(){const r=Math.random();return r<0.08?'legendary':r<0.3?'rare':'common'} +function openPack(){if(!canOpenPack()){toast(`Next pack in ${packCountdown()}. Patience, goblin.`);return}doPull(false)} +function forgePack(){if((pack.scrap|0)<3){toast('Need 3 scrap. Melt spare copies below.');return}pack.scrap-=3;doPull(true)} +function meltDupe(id){if((pack.haul[id]|0)<2){toast('Only spare copies can be melted. The original stays.');return}pack.haul[id]--;pack.scrap=(pack.scrap|0)+1;savePack();renderPacks();sfx('blip');toast('+1 scrap. The furnace hungers.')} +function doPull(bonus){ + const pool=allMemes();if(!pool.length)return; + const unseen=pool.filter(m=>!pack.haul[m.id]&&!seen.has(m.id)); + const lucky=Math.random()<0.7&&unseen.length?unseen[Math.floor(Math.random()*unseen.length)]:pool[Math.floor(Math.random()*pool.length)]; + const rarity=rollRarity(),wasOwned=(pack.haul[lucky.id]|0)>0; + pack.haul[lucky.id]=(pack.haul[lucky.id]|0)+1; + if(!bonus){const gap=Date.now()-(pack.lastOpened||0);pack.streak=pack.lastOpened&&gap<2*PACK_MS?pack.streak+1:1;pack.best=Math.max(pack.best,pack.streak);pack.lastOpened=Date.now()} + pack.lastPull={id:lucky.id,rarity,shiny:wasOwned,at:Date.now(),bonus:!!bonus}; + savePack();packStage='opening';packReveal=pack.lastPull;renderPacks();sfx('chaos'); + setTimeout(()=>{if(page!=='packs')return;packStage='revealed';renderPacks(); + if(rarity==='legendary'){sfx('fanfare');confetti()}else sfx('save'); + checkAwards();if(!bonus&&pack.streak>=2)buddySay('streak'); + toast(wasOwned?`✨ SHINY ${lucky.title}! A duplicate, but make it fashion.`:`${bonus?'Forged pull! ':rarity==='legendary'?'🌟 LEGENDARY PULL! ':rarity==='rare'?'💎 Rare pull! ':''}“${lucky.title}” joins your haul.`)} + ,1100)} +function renderPacks(){const panel=$('#pack-panel');if(!panel)return; + const ready=canOpenPack(); + panel.innerHTML=packStage==='opening' + ?`
📦

Ripping it open…

` + :ready + ?`` + :`
Next drop in
${packCountdown()}
one pack per human per 24h. rules are rules.
${pack.lastPull?packMini(allMemes().find(m=>m.id===pack.lastPull.id),pack.lastPull):''}
`; + if(packStage==='revealed'&&packReveal){const m=allMemes().find(m=>m.id===packReveal.id);if(m){panel.insertAdjacentHTML('beforeend',packMini(m,packReveal,true));panel.insertAdjacentHTML('beforeend',`
`)}} + $('#pack-stats').innerHTML=`
0 🔥day streak
0 🏆best streak
0cats hauled
`;countUp($('#pack-stats')); + renderForge(); + const bc=$('#daily-count');if(bc)bc.textContent=ready?'🎁':haulSize()} +function renderKeep(){const tabs=$('#keep-tabs');if(!tabs)return;const loved=allMemes().filter(m=>saved.has(m.id)).length; + tabs.innerHTML=[['sets','📁 Sets'],['haul',`📦 Haul · ${haulSize()}`],['loved',`♥ Loved · ${loved}`],['trophies',`🏆 Trophies · ${awards.length}/${ACH.length}`]].map(([id,label])=>``).join(''); + const show={sets:['collection-grid','sets-panel'],haul:['keep-haul'],loved:['keep-loved'],trophies:['trophies']}[keepTab]||[]; + for(const id of ['collection-grid','sets-panel','keep-haul','keep-loved','trophies'])document.getElementById(id).hidden=!show.includes(id); + if(keepTab==='sets'){$('#collection-grid').innerHTML=collections.map(c=>``).join('');renderSets()} + if(keepTab==='haul'){const list=allMemes().filter(m=>pack.haul[m.id]).slice(0,60);$('#keep-haul').innerHTML=list.length?list.map((m,i)=>packCard(m,i)).join(''):'

No pulls yet. Rip a pack on the Daily Drop page.

'} + if(keepTab==='loved'){const list=allMemes().filter(m=>saved.has(m.id)).slice(0,60);$('#keep-loved').innerHTML=list.length?list.map(card).join(''):'

No loved cats yet — tap ♥ on any meme.

'} + if(keepTab==='trophies')renderTrophies()} +function renderForge(){const f=$('#forge');if(!f)return;const dupes=allMemes().filter(m=>(pack.haul[m.id]|0)>1); + f.innerHTML=`

The Forge

Melt spare copies into scrap. 3 scrap = 1 bonus pack, no waiting.

🪙 ${pack.scrap|0} scrap
${dupes.length?dupes.map(m=>`
${esc(m.title)}×${pack.haul[m.id]} owned
`).join(''):'

No spares yet. Duplicates you pull become meltable here.

'}`} +function packMini(m,pull,fresh){if(!m)return '';return ``} +async function sharePull(id){const m=allMemes().find(m=>m.id===id);if(!m)return; + await document.fonts.ready;let img;try{img=await loadImage(m.image)}catch{toast('That cat refuses to travel.');return} + const width=1000,pad=48,fontSize=44,lineHeight=56,headH=150,footH=120; + const canvas=document.createElement('canvas');canvas.width=width; + let ctx=canvas.getContext('2d');const sht=memeTheme(m);ctx.font=`800 ${fontSize}px ${sht.font}`; + const top=m.top?wrapText(ctx,m.top.toUpperCase(),width-pad*2):[],bottom=m.bottom?wrapText(ctx,m.bottom.toUpperCase(),width-pad*2):[]; + const imageH=Math.round(Math.min(img.height*(width/img.width),1100)),th=top.length?top.length*lineHeight+40:0,bh=bottom.length?bottom.length*lineHeight+40:0; + canvas.height=headH+th+imageH+bh+footH;ctx=canvas.getContext('2d');ctx.textBaseline='top'; + ctx.fillStyle='#4A412A';ctx.fillRect(0,0,width,headH); + ctx.fillStyle='#F7F5EE';ctx.font='800 34px "DM Sans", sans-serif';ctx.fillText('CATMEMECHIMP · DAILY DROP',pad,40); + const pull=pack.lastPull&&pack.lastPull.id===id?pack.lastPull:null,tag=pull?`${pull.rarity.toUpperCase()}${pull.shiny?' · SHINY':''}`:'PACK PULL'; + ctx.fillStyle='#B9C5F3';ctx.font='700 26px "DM Sans", sans-serif';ctx.fillText(tag,pad,86); + ctx.fillStyle=sht.bg;ctx.fillRect(0,headH,width,th+imageH+bh); + ctx.fillStyle=sht.fg;ctx.font=`800 ${fontSize}px ${sht.font}`; + top.forEach((s,i)=>ctx.fillText(s,pad,headH+20+i*lineHeight)); + const ratio=Math.min(width/img.width,imageH/img.height),iw=img.width*ratio,ih=img.height*ratio; + ctx.drawImage(img,(width-iw)/2,headH+th+(imageH-ih)/2,iw,ih); + bottom.forEach((s,i)=>ctx.fillText(s,pad,headH+th+imageH+20+i*lineHeight)); + ctx.fillStyle='#4A412A';ctx.fillRect(0,headH+th+imageH+bh,width,footH); + ctx.fillStyle='#F7F5EE';ctx.font='700 28px "DM Sans", sans-serif';ctx.fillText(m.title.slice(0,48),pad,headH+th+imageH+bh+28); + ctx.fillStyle='#B9C5F3';ctx.font='500 22px "DM Sans", sans-serif';ctx.fillText('ripped from a daily pack · good cats, questionable behavior',pad,headH+th+imageH+bh+66); + const blob=await new Promise(res=>canvas.toBlob(res,'image/png'));if(!blob){toast('The card couldn’t be made.');return} + const file=new File([blob],'catmeme-drop.png',{type:'image/png'}); + try{if(navigator.canShare&&navigator.canShare({files:[file]})){await navigator.share({files:[file],title:'My daily cat'});toast('Shared! Go forth and convert friends.');return}}catch(e){if(e&&e.name==='AbortError')return} + const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=`catmeme-drop-${m.id}.png`;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),5000);toast('Card downloaded. Spread the gospel.')} +function packCard(m,i){const n=pack.haul[m.id]|0;return `
${moodTag(m.mood)}${n>1?`PULLED ×${n}`:'PACK PULL'}

${esc(m.title)}

`} +setInterval(()=>{const el=$('#pack-timer');if(el&&page==='packs'&&!canOpenPack())el.textContent=packCountdown();if(el&&page==='packs'&&canOpenPack()&&packStage!=='opening')renderPacks()},1000); +const ACH=[ + {id:'first-crown',icon:'👑',name:'First blood (but cats)',desc:'Crown your first Arena champion'}, + {id:'streak-5',icon:'🔥',name:'Warming up',desc:'Reach a 5-crown streak'}, + {id:'streak-10',icon:'🔥',name:'Certified judge',desc:'Reach a 10-crown streak'}, + {id:'streak-25',icon:'🏆',name:'Unhinged',desc:'Reach a 25-crown streak'}, + {id:'brawler-100',icon:'⚔️',name:'Centurion',desc:'Judge 100 battles'}, + {id:'collector',icon:'💾',name:'Hoarder',desc:'Save 10 cats'}, + {id:'creator',icon:'✨',name:'Meme parent',desc:'Create your first meme'}, + {id:'prolific',icon:'🏭',name:'Meme factory',desc:'Create 5 memes'}, + {id:'frankenstein',icon:'🧪',name:'It’s alive',desc:'Save a Frankenmeme'}, + {id:'explorer',icon:'🧭',name:'Completionist-ish',desc:'View 75 different cats'}, + {id:'first-pull',icon:'📦',name:'Unboxer',desc:'Open your first daily pack'}, + {id:'week-streak',icon:'📅',name:'Regular',desc:'Open packs 7 days in a row'}, + {id:'dex-half',icon:'📖',name:'Halfway there',desc:'Discover half the archive'}, + {id:'dex-full',icon:'🌟',name:'Living dex',desc:'Discover all 290 cats'}]; +let awards=readState('awards',[]),labSaves=readState('stats',{labSaves:0}).labSaves|0; +function checkAwards(){const earned=[]; + const need=(id,ok)=>{if(ok&&!awards.includes(id)&&!earned.includes(id))earned.push(id)}; + need('first-crown',battleStats.battles>0);need('streak-5',battleStats.best>=5);need('streak-10',battleStats.best>=10);need('streak-25',battleStats.best>=25);need('brawler-100',battleStats.battles>=100); + need('collector',allMemes().filter(m=>saved.has(m.id)).length>=10);need('creator',created.length>=1);need('prolific',created.length>=5); + need('frankenstein',labSaves>=1);need('explorer',seen.size>=75); + need('first-pull',haulSize()>=1);need('week-streak',pack.best>=7); + need('dex-half',discoveredCount()>=Math.ceil(library.length/2));need('dex-full',library.length>0&&discoveredCount()>=library.length); + if(!earned.length)return;awards=[...awards,...earned];setLocal('awards',awards);sfx('fanfare'); + toast(earned.length===1?`🏆 Achievement: ${ACH.find(a=>a.id===earned[0]).name}!`:`🏆 ${earned.length} achievements: ${earned.map(id=>ACH.find(a=>a.id===id).name).join(' · ')}!`); + renderTrophies()} +function renderTrophies(){const t=$('#trophies');if(!t)return; + t.innerHTML=`

Trophies

${awards.length} of ${ACH.length} unlocked · stored in this browser

${ACH.map(a=>{const has=awards.includes(a.id);return `
${esc(a.name)}${esc(a.desc)}
`}).join('')}
`} +function battleCard(m,side){const wins=battleWins(m.id),plays=battlePlays(m.id),rate=battleRate(m.id),label=moodInfo(m.mood)[2]; + return ``} +function renderArena(){if(!battleLeft||!battleRight)pickBattle();if(!battleLeft||!battleRight)return; + $('#arena-stats').innerHTML=`
0battles judged
0 🔥current streak
0 🏆best streak
${seasonLabel(battleStats.season).split(' ')[0]}${daysLeftInSeason()}d left
`;countUp($('#arena-stats')); + const bc=$('#battle-count');if(bc)bc.textContent=battleStats.battles; + const last=battleStats.lastWinner?allMemes().find(m=>m.id===battleStats.lastWinner):null; + const lc=$('#last-crowned');if(last){lc.hidden=false;lc.innerHTML=`Last crowned: · ${battleWins(last.id)}W · ${battleRate(last.id)}%`}else{lc.hidden=true;lc.innerHTML=''} + $('#battle-grid').innerHTML=`${battleCard(battleLeft,'left')}${battleCard(battleRight,'right')}`; + const ranked=rankBoard().slice(0,5),top3=ranked.slice(0,3),rest=ranked.slice(3); + const medalFor=i=>['🥇','🥈','🥉'][i]; + const podiumOrder=[1,0,2].filter(i=>top3[i]); + const podiumCard=x=>{const i=top3.indexOf(x),r=Math.round(x.w/x.p*100);return ``}; + const hall=battleStats.hall.filter(h=>allMemes().some(m=>m.id===h.champ)).slice(0,6); + $('#leaderboard').innerHTML=`

Local legends

${battleStats.battles?`From ${battleStats.battles} judged ${battleStats.battles===1?'battle':'battles'} · stored only in this browser`:'No champions yet — your votes build this board. Stored only in this browser.'}

${ranked.length?`${top3.length?`
${podiumOrder.map(i=>podiumCard(top3[i])).join('')}
`:''}${rest.map((x,i)=>{const r=Math.round(x.w/x.p*100);return `
${esc(x.m.title)}${x.w}W · ${x.p-x.w}L · ${r}%
`}).join('')}`:'

Crown your first cat above and it will show up here.

'}${hall.length?`

📜 Hall of fame

${hall.map(h=>{const m=allMemes().find(m=>m.id===h.champ);return `
${esc(m.title)}${esc(seasonLabel(h.season))} · ${h.wins}W
`}).join('')}`:''}`; +} +let viewerQueue=[],viewerIndex=0,autoTimer=null,tvMode=false,toastTimer=null,editing=null,inspired=false,uploadedTemplate=null,dbPromise=null; const collections=[{id:'office',title:'The office survival kit',description:'For meetings that could have been naps.',image:'1bh7.jpg',filter:m=>m.tags.some(t=>['work','email','meeting','career','deadline'].includes(t))},{id:'battery',title:'The low battery club',description:'A safe space for professional nappers.',image:'11wis1.jpg',filter:m=>m.mood==='sleepy'},{id:'serotonin',title:'A little serotonin',description:'Small cats. Unreasonably big feelings.',image:'amuvy.jpg',filter:m=>m.mood==='wholesome'}]; function toast(message){clearTimeout(toastTimer);$('#toast').textContent=message;$('#toast').classList.add('visible');toastTimer=setTimeout(()=>$('#toast').classList.remove('visible'),3300);} function setLocal(k,value){try{localStorage.setItem('cmc-'+k,JSON.stringify(value));return true}catch{toast('Browser storage is unavailable. Download to keep a copy.');return false}} -function database(){if(!dbPromise)dbPromise=new Promise((res,rej)=>{const req=indexedDB.open('CATMEMECHIMP',1);req.onupgradeneeded=()=>req.result.createObjectStore('creations',{keyPath:'id'});req.onsuccess=()=>res(req.result);req.onerror=()=>rej(req.error)});return dbPromise} +function database(){if(!dbPromise)dbPromise=new Promise((res,rej)=>{const req=indexedDB.open('CATMEMECHIMP',2);req.onupgradeneeded=()=>{const db=req.result;if(!db.objectStoreNames.contains('creations'))db.createObjectStore('creations',{keyPath:'id'});if(!db.objectStoreNames.contains('clips'))db.createObjectStore('clips',{keyPath:'id'})};req.onsuccess=()=>res(req.result);req.onerror=()=>rej(req.error)});return dbPromise} +async function dbReadClips(){try{const db=await database();return await new Promise((res,rej)=>{const r=db.transaction('clips').objectStore('clips').getAll();r.onsuccess=()=>res(r.result||[]);r.onerror=()=>rej(r.error)})}catch{return[]}} +async function dbPutClip(value){const db=await database();return new Promise((res,rej)=>{const tx=db.transaction('clips','readwrite');tx.objectStore('clips').put(value);tx.oncomplete=res;tx.onerror=()=>rej(tx.error);tx.onabort=()=>rej(tx.error)})} async function dbRead(){const db=await database();return new Promise((res,rej)=>{const r=db.transaction('creations').objectStore('creations').getAll();r.onsuccess=()=>res(r.result);r.onerror=()=>rej(r.error)})} async function dbPut(value){const db=await database();return new Promise((res,rej)=>{const tx=db.transaction('creations','readwrite');tx.objectStore('creations').put(value);tx.oncomplete=res;tx.onerror=()=>rej(tx.error);tx.onabort=()=>rej(tx.error)})} const allMemes=()=>[...library,...created]; +const hotScore=id=>battlePlays(id)+2*battleWins(id); +function hotIds(){return new Set([...allMemes()].filter(m=>battlePlays(m.id)>=3).sort((a,b)=>hotScore(b.id)-hotScore(a.id)).slice(0,12).map(m=>m.id))} +let templateFilter=null,hotCache=new Set(); +function showTemplate(image){navigate('discover');templateFilter=image;limit=18;render();const m=library.find(m=>m.image===image);toast(m?`Every “${m.template}” in the house.`:'Template view.')} function filterMemes(){let list=page==='studio'?[...created].reverse():page==='saved'?allMemes().filter(m=>saved.has(m.id)):[...library]; - if(collection){const c=collections.find(c=>c.id===collection);if(c)list=list.filter(c.filter)} + if(templateFilter)list=list.filter(m=>m.image===templateFilter); + if(collection){const c=collections.find(c=>c.id===collection);if(c)list=list.filter(c.filter);else{const u=setById(collection);if(u){const keep=new Set(u.ids);list=allMemes().filter(m=>keep.has(m.id))}}} if(mood!=='all')list=list.filter(m=>m.mood===mood); if(query){const terms=query.toLowerCase().trim().split(/\s+/);list=list.filter(m=>{const haystack=[m.title,m.top,m.bottom,m.template,m.mood,moodInfo(m.mood)[2],...m.tags].join(' ').toLowerCase();return terms.every(t=>haystack.includes(t))})} - const sort=$('#sort').value; - if(sort==='az')list.sort((a,b)=>a.title.localeCompare(b.title));else if(sort==='unseen')list.sort((a,b)=>Number(seen.has(a.id))-Number(seen.has(b.id)));else if(order.length){const map=new Map(order.map((x,i)=>[x,i]));list.sort((a,b)=>(map.get(a.id)??999)-(map.get(b.id)??999))} + const sort=$('#sort').value; + if(sort==='az')list.sort((a,b)=>a.title.localeCompare(b.title));else if(sort==='hot')list.sort((a,b)=>hotScore(b.id)-hotScore(a.id)||b.title.localeCompare(a.title));else if(sort==='unseen')list.sort((a,b)=>Number(seen.has(a.id))-Number(seen.has(b.id)));else if(order.length){const map=new Map(order.map((x,i)=>[x,i]));list.sort((a,b)=>(map.get(a.id)??999)-(map.get(b.id)??999))} return list; } -function visual(m,loading='lazy'){return `
${m.top?`
${esc(m.top)}
`:''}${esc(m.template)}${m.bottom?`
${esc(m.bottom)}
`:''}
`} -function card(m,i){return `
${moodTag(m.mood)}${m.kind==='creation'?'YOUR CREATION':'CHIMP REMIX'}

${esc(m.title)}

`} -function updateCounts(){$('#all-count').textContent=library.length;$('#saved-count').textContent=allMemes().filter(m=>saved.has(m.id)).length;$('#created-count').textContent=created.length} +function memeTheme(m){const fonts={dm:'"DM Sans", sans-serif',fraunces:'Fraunces, Georgia, serif',mono:'ui-monospace, SFMono-Regular, Menlo, monospace'};const styles={light:{bg:'#fff',fg:'#24231e'},dark:{bg:'#171410',fg:'#F7F5EE'},banana:{bg:'#FFD75E',fg:'#3A2F10'}};return {font:fonts[m?.font]||fonts.dm,...(styles[m?.style]||styles.light)}} +function visual(m,loading='lazy'){const st=m.sticker&&m.sticker.emoji?``:'';return `
${m.top?`
${esc(m.top)}
`:''}${esc(m.template)}${m.bottom?`
${esc(m.bottom)}
`:''}${st}
`} +function card(m,i){const hot=hotCache.has(m.id);return `
${moodTag(m.mood)}${hot?'🔥 HOT':''}${m.kind==='creation'?'YOUR CREATION':'CHIMP REMIX'}

${esc(m.title)}

`} +function updateCounts(){$('#all-count').textContent=library.length;const dc=$('#daily-count');if(dc)dc.textContent=canOpenPack()?'🎁':haulSize();const dx=$('#dex-count');if(dx)dx.textContent=discoveredCount();$('#created-count').textContent=created.length;const bc=$('#battle-count');if(bc)bc.textContent=battleStats.battles} function render(){updateCounts();$$('[data-page]').forEach(b=>{b.classList.toggle('active',b.dataset.page===page);if(b.dataset.page===page)b.setAttribute('aria-current','page');else b.removeAttribute('aria-current')}); - $('#hero').hidden=page!=='discover'||!!query; - $('#breadcrumb').textContent=({discover:'THE CAT ARCHIVE',saved:'SAVED FOR A RAINY DAY',collections:'HANDPICKED CAT COLLECTIONS',studio:'THE NONSENSE LAB'})[page]; - $('#section-eyebrow').textContent=({discover:'THE GOOD STUFF',saved:'YOUR PERSONAL SEROTONIN STASH',collections:'A CAT FOR EVERY OCCASION',studio:'MADE BY YOU. APPROVED BY YOU.'})[page]; - $('#library-title').textContent=query?`Cats matching “${query}”`:({discover:'Find your feline frequency.',saved:'The keepers.',collections:'Good things come in collections.',studio:'Your little masterpieces.'})[page]; - const desc=({discover:'',saved:'Every cat you’ve hearted, right here when you need one.',collections:'Small, handpicked corners of the archive. Pick one and settle in.',studio:'Your captions. Your cats. Your very questionable sense of humor.'})[page];$('#page-description').textContent=desc;$('#page-description').hidden=!desc; - $('#collection-grid').hidden=page!=='collections'; - if(page==='collections')$('#collection-grid').innerHTML=collections.map(c=>``).join(''); + $('#hero').hidden=page!=='discover'||!!query;$('#ticker').hidden=$('#hero').hidden; + $('#breadcrumb').textContent=({discover:'THE CAT ARCHIVE',arena:'THE BATTLE ARENA',lab:'THE FRANKENMEME LAB',packs:'THE DAILY DROP',album:'THE COMPLETE DEX',saved:'SAVED FOR A RAINY DAY',collections:'HANDPICKED CAT COLLECTIONS',studio:'THE NONSENSE LAB'})[page]||'THE CAT ARCHIVE'; + $('#section-eyebrow').textContent=({discover:'THE GOOD STUFF',arena:'TWO CATS ENTER. ONE CAT LEAVES.',lab:'MAD SCIENCE, BUT CATS.',packs:'A LITTLE GAMBLE.',album:'GOTTA PET ’EM ALL.',saved:'YOUR PERSONAL SEROTONIN STASH',collections:'A CAT FOR EVERY OCCASION',studio:'MADE BY YOU. APPROVED BY YOU.'})[page]; + const tmplName=templateFilter?(library.find(m=>m.image===templateFilter)?.template||'Template'):''; + $('#library-title').textContent=page==='arena'?'Crown the funniest cat.':page==='lab'?'Build an abomination.':page==='packs'?"Today's drop.":page==='album'?'Every cat. Yours to find.':templateFilter?`More ${tmplName}.`:query?`Cats matching “${query}”`:({discover:'Find your feline frequency.',saved:'The keepers.',collections:'Good things come in collections.',studio:'Your little masterpieces.'})[page]; + const desc=({discover:'',arena:'290 memes. Zero mercy. Pick a winner, build a streak — skips reset the streak.',lab:'Three cats walk in. One meme walks out. Reroll parts until it feels illegal.',packs:'One sealed pack per human per 24 hours. Rip it, keep the cat, chase the streak.',album:'Open, pull, or battle the whole archive. Silhouettes are personal failures.',saved:'Every cat you’ve hearted, right here when you need one.',collections:'Small, handpicked corners of the archive. Pick one and settle in.',studio:'Your captions. Your cats. Your very questionable sense of humor.'})[page];$('#page-description').textContent=desc;$('#page-description').hidden=!desc; + const inArena=page==='arena',inLab=page==='lab',inPacks=page==='packs',inAlbum=page==='album',hideGrid=inArena||inLab||inPacks||inAlbum; + $('#arena').hidden=!inArena;$('#lab').hidden=!inLab;$('#packs').hidden=!inPacks;$('#album').hidden=!inAlbum;$('#shuffle').hidden=hideGrid; + $('#filter-row').hidden=hideGrid;$('#collection-grid').hidden=page!=='collections'; + document.querySelector('.results-bar').hidden=hideGrid;$('#meme-grid').hidden=hideGrid;$('#empty').hidden=hideGrid?true:!!0;$('#load-more').hidden=hideGrid;$('#end-note').hidden=hideGrid; + if(inArena){if(!allMemes().length){$('#arena-stats').innerHTML='';$('#battle-grid').innerHTML='';$('#leaderboard').innerHTML='

The cats haven’t arrived yet.

'}else renderArena();return} + if(inLab){renderLab();return} + if(inPacks){renderPacks();return} + if(inAlbum){renderDex();return} + if(page==='collections'){renderKeep()} + $('#stash').hidden=page!=='collections'; $('#mood-filters').innerHTML=moods.map(([id,symbol,label])=>``).join(''); - const list=filterMemes(),display=list.slice(0,limit);$('#meme-grid').innerHTML=display.map(card).join(''); - $('#result-count').innerHTML=list.length?`${list.length} ${list.length===1?'cat':'cats'} ${page==='saved'?'worth keeping':mood==='all'&&!query&&!collection?'and not a single thought':collection?'in this collection':'in this corner'} · showing ${display.length}`:'0 cats found'; + const list=filterMemes(),display=list.slice(0,limit);hotCache=hotIds();$('#meme-grid').innerHTML=display.map(card).join(''); + if(setById(collection)){$$('#meme-grid .meme-card').forEach(c=>{const id=c.querySelector('[data-open]')?.dataset.open;if(!id)return;const b=document.createElement('button');b.className='remove-from-set';b.dataset.removeFromSet=id;b.textContent='× Remove from set';b.setAttribute('aria-label','Remove from set');c.append(b)})} + $('#result-count').innerHTML=list.length?`${list.length} ${list.length===1?'cat':'cats'} ${page==='saved'?'worth keeping':mood==='all'&&!query&&!collection?'and not a single thought':collection?'in this collection':'in this corner'} · showing ${display.length}${templateFilter?' · ':''}`:'0 cats found'; $('#empty').hidden=!!list.length;$('#load-more').hidden=list.length<=limit;$('#end-note').hidden=!list.length||list.length>limit; const unfiltered=!query&&mood==='all'&&!collection; $('#empty-title').textContent=page==='saved'&&unfiltered?'Your favorites are waiting to happen.':page==='studio'&&unfiltered?'Every masterpiece starts with a cat.':'No cats in this corner.'; - $('#empty-copy').textContent=page==='saved'&&unfiltered?'Tap the heart on any meme. We’ll keep it warm for you.':page==='studio'&&unfiltered?'Pick a cat, add a caption, make someone’s day.':'Try a different search or let all the cats back in.'; + $('#empty-copy').textContent=page==='saved'&&unfiltered?'Tap the heart on any meme. We’ll keep it warm for you.':page==='studio'&&unfiltered?'Pick a cat, add a caption, make someone’s day.':`Try a different search — ${library.length} cats are hiding somewhere.`; $('#empty-action').textContent=page==='studio'&&unfiltered?'Make your first meme':page==='saved'&&unfiltered?'Find some favorites':'Show all cats'; } -function navigate(p){page=p;mood='all';query='';collection='';limit=18;$('#search').value='';render();window.scrollTo({top:0,behavior:'smooth'})} -function toggleSave(id){const next=new Set(saved);next.has(id)?next.delete(id):next.add(id);if(!setLocal('saved',[...next]))return;saved=next; - if(page==='saved')render();else{$$(`[data-save="${id}"]`).forEach(b=>{b.classList.toggle('saved',saved.has(id));b.setAttribute('aria-pressed',saved.has(id));const m=allMemes().find(m=>m.id===id);b.setAttribute('aria-label',`${saved.has(id)?'Unsave':'Save'} ${m?.title||'meme'}`)});updateCounts()} +const BUDDY_QUIPS={ + hello:['Psst. I saved you the good cats. You’re welcome.','New here? Rule one: trust the banana cat. Rule two: see rule one.','I curate. The cats approve. Mostly.'], + idle:['Still here? Elite behavior.','You’ve been staring for a while. Hydrate, then resume cats.','I counted your blinks. Rookie numbers.','The cats noticed you stayed. They’re pretending not to care.'], + click:['Boop received. Filing it under “morale”.','Yes, I’m real. No, I won’t share my snacks.','Poke me again and I’ll tell the cats.','I’m the reason the vibes are like this.'], + night:['Past midnight, huh? The forbidden browsing hours. Respect.','Shh. The day-people must never know about this.'], + packReady:['Your daily pack is RIPE. Go rip it.','Psst — fresh pack upstairs. Don’t let it age.'], + streak:['Streak looking tasty. Don’t fumble it.','One more day keeps the streak dream alive.']}; +let buddyMuted=readState('buddy',{muted:false}).muted===true,buddyLast=0,buddyHideT=null,buddyPressT=null; +function buddySay(kind){if(buddyMuted&&kind!=='force')return;const now=Date.now();if(kind!=='force'&&now-buddyLast<25000)return;buddyLast=now; + const lines=BUDDY_QUIPS[kind]||BUDDY_QUIPS.idle;const b=$('#buddy-bubble');if(!b)return; + b.textContent=lines[Math.floor(Math.random()*lines.length)];b.hidden=false; + clearTimeout(buddyHideT);buddyHideT=setTimeout(()=>{b.hidden=true},4500)} +function buddyInit(){const w=$('#chimp-buddy');if(!w)return;w.hidden=false;w.classList.toggle('muted',buddyMuted); + setTimeout(()=>{if(page==='discover'&&!location.hash.slice(1)){buddySay('hello');sfx('meow')}},2500); + $('#buddy-btn').onclick=()=>{sfx('meow');buddySay('force')}; + $('#buddy-btn').addEventListener('pointerdown',()=>{clearTimeout(buddyPressT);buddyPressT=setTimeout(()=>{buddyMuted=!buddyMuted;setLocal('buddy',{muted:buddyMuted});w.classList.toggle('muted',buddyMuted);$('#buddy-bubble').hidden=true;toast(buddyMuted?'Biscuit muted. She’ll pretend it doesn’t hurt.':'Biscuit is back. She missed you terribly.')},650)}); + $('#buddy-btn').addEventListener('pointerup',()=>clearTimeout(buddyPressT)); + $('#buddy-btn').addEventListener('pointerleave',()=>clearTimeout(buddyPressT)); + let idleT=null;const poke=()=>{clearTimeout(idleT);idleT=setTimeout(()=>buddySay('idle'),90000);const h=new Date().getHours();if((h>=23||h<5)&&Math.random()<0.3)buddySay('night')}; + for(const ev of ['pointerdown','keydown'])document.addEventListener(ev,poke,{passive:true,capture:true});poke();buddy3D()} +function buddy3D(){const btn=$('#buddy-btn');if(!btn||!matchMedia('(pointer:fine)').matches||matchMedia('(prefers-reduced-motion: reduce)').matches)return;let raf=null;addEventListener('pointermove',e=>{if(raf)return;raf=requestAnimationFrame(()=>{raf=null;const r=btn.getBoundingClientRect(),cx=r.left+r.width/2,cy=r.top+r.height/2,dx=Math.max(-1,Math.min(1,(e.clientX-cx)/220)),dy=Math.max(-1,Math.min(1,(e.clientY-cy)/220));btn.style.transform=`rotateY(${(dx*22).toFixed(1)}deg) rotateX(${(-dy*22).toFixed(1)}deg)`;btn.style.setProperty('--gx',`${50+dx*40}%`);btn.style.setProperty('--gy',`${50+dy*40}%`)})},{passive:true})} +function countUp(root){if(!root)return;if(matchMedia('(prefers-reduced-motion: reduce)').matches){root.querySelectorAll('[data-count]').forEach(s=>{s.textContent=s.dataset.count});return}root.querySelectorAll('[data-count]').forEach(s=>{const target=+s.dataset.count||0,t0=performance.now();const step=t=>{const p=Math.min(1,(t-t0)/600);s.textContent=Math.round(target*(1-Math.pow(1-p,3)));if(p<1)requestAnimationFrame(step)};requestAnimationFrame(step)})} +function fillTicker(){const el=$('#ticker-inner');if(!el||!library.length)return;const picks=Array.from({length:12},()=>library[Math.floor(Math.random()*library.length)].title);const half=picks.map(t=>`BREAKING: ${esc(t)}`).join('  ✳  ');el.innerHTML=`${half}  ✳  `} +function heroParallax(){const hero=document.querySelector('.hero'),art=document.querySelector('.hero-art');if(!hero||!art||!matchMedia('(pointer:fine)').matches||matchMedia('(prefers-reduced-motion: reduce)').matches)return;let raf=null;hero.addEventListener('pointermove',e=>{const r=e.currentTarget.getBoundingClientRect(),dx=(e.clientX-r.left)/r.width-0.5,dy=(e.clientY-r.top)/r.height-0.5;if(raf)return;raf=requestAnimationFrame(()=>{raf=null;art.style.transform=`translate(${dx*14}px,${dy*10}px)`})});hero.addEventListener('pointerleave',()=>{art.style.transform=''})} +function discoveredCount(){return library.filter(m=>seen.has(m.id)||pack.haul[m.id]).length} +function renderDex(){const total=library.length,done=discoveredCount(),pct=total?Math.round(done/total*100):0; + $('#album').innerHTML=`
${done} / ${total}${pct}% discovered
`+moods.slice(1).map(([id,sym,label])=>{const ms=library.map((m,i)=>({m,idx:i})).filter(x=>x.m.mood===id),got=ms.filter(x=>seen.has(x.m.id)||pack.haul[x.m.id]).length; + return `

${sym} ${label} · ${got}/${ms.length}

${ms.map(x=>{const known=seen.has(x.m.id)||pack.haul[x.m.id];return known?``:``}).join('')}
`}).join(''); + const bc=$('#dex-count');if(bc)bc.textContent=done} +function navigate(p){page=p;mood='all';query='';collection='';templateFilter=null;limit=18;$('#search').value='';if(p==='arena'&&(!battleLeft||!battleRight))pickBattle();if(p==='lab'&&(!labTop||!labBottom||!labCat))rollLab();if(p==='packs'&&canOpenPack())setTimeout(()=>buddySay('packReady'),800);render();window.scrollTo({top:0,behavior:'smooth'})} +function toggleSave(id){const added=!saved.has(id);const next=new Set(saved);next.has(id)?next.delete(id):next.add(id);if(!setLocal('saved',[...next]))return;saved=next;if(added)sfx('save');checkAwards(); + if(page==='packs')render();else{$$(`[data-save="${id}"]`).forEach(b=>{b.classList.toggle('saved',saved.has(id));b.setAttribute('aria-pressed',saved.has(id));const m=allMemes().find(m=>m.id===id);b.setAttribute('aria-label',`${saved.has(id)?'Unsave':'Save'} ${m?.title||'meme'}`)});updateCounts()} if($('#viewer').open)updateViewerSave();toast(saved.has(id)?'A good cat, safely tucked away.':'Released back into the wild.'); } function shuffle(list){const out=[...list];for(let i=out.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[out[i],out[j]]=[out[j],out[i]]}return out} -function shuffledGrid(){order=shuffle(allMemes()).map(m=>m.id);$('#sort').value='curated';limit=18;render();toast('Same good cats. A little new chaos.')} +function shuffledGrid(){order=shuffle(allMemes()).map(m=>m.id);$('#sort').value='curated';limit=18;sfx('chaos');render();toast('Same good cats. A little new chaos.')} function currentMeme(){return viewerQueue[viewerIndex]} function updateViewerSave(){const on=saved.has(currentMeme()?.id);$('#viewer-save').classList.toggle('saved',on);$('#viewer-save').setAttribute('aria-pressed',on);$('#viewer-save').innerHTML=icon(on?'check':'heart')+`${on?'Saved':'Save'}`} -function showViewer(){const m=currentMeme();if(!m)return;$('#viewer-image-wrap').innerHTML=visual(m,'eager');$('#viewer-title').textContent=m.title;$('#viewer-mood').outerHTML=moodTag(m.mood).replace('class="mood-label"','id="viewer-mood" class="mood-label"');$('#viewer-template').textContent=m.kind==='creation'?'Your original creation':`${m.template} · A CATMEMECHIMP remix`; - $('#viewer-progress').textContent=`${String(viewerIndex+1).padStart(2,'0')} / ${String(viewerQueue.length).padStart(2,'0')}`; +function showViewer(){const m=currentMeme();if(!m)return;tvClip=false;clearTimeout(clipT);$('#viewer-image-wrap').innerHTML=visual(m,'eager');$('#viewer-title').textContent=m.title;$('#viewer-mood').outerHTML=moodTag(m.mood).replace('class="mood-label"','id="viewer-mood" class="mood-label"');$('#viewer-template').textContent=m.kind==='creation'?'Your original creation':`${m.template} · ${m.status||'remix'} · est. ${m.origin||'unknown'}`;$('#viewer-template').title=m.lore||'';const vm=$('#viewer-more');if(vm){vm.hidden=m.kind==='creation';if(m.kind!=='creation')vm.textContent=`More ${m.template}`} + $('#viewer-progress').textContent=`${String(viewerIndex+1).padStart(2,'0')} / ${String(viewerQueue.length).padStart(2,'0')}`; + $('#viewer').classList.toggle('tv',tvMode);const ov=$('#tv-overlay');ov.hidden=!tvMode; + if(tvMode)updateTVOverlay(m) $('#viewer-source').hidden=!m.source;if(m.source)$('#viewer-source').href=m.source;updateViewerSave(); - seen.add(m.id);try{localStorage.setItem('cmc-seen',JSON.stringify([...seen]))}catch{} + seen.add(m.id);try{localStorage.setItem('cmc-seen',JSON.stringify([...seen]))}catch{}checkAwards(); history.replaceState(null,'',m.kind==='creation'?location.pathname:location.pathname+'#meme='+encodeURIComponent(m.id)); if(autoTimer)restartAutoplay(); } function openViewer(id,list=filterMemes()){stopAutoplay();viewerQueue=list.length?[...list]:[...library];viewerIndex=Math.max(0,viewerQueue.findIndex(m=>m.id===id));if(!viewerQueue.length)return;showViewer();$('#viewer').showModal()} -function browse(delta){if(!viewerQueue.length)return;viewerIndex=(viewerIndex+delta+viewerQueue.length)%viewerQueue.length;showViewer()} +function browse(delta){if(!viewerQueue.length)return;viewerIndex=(viewerIndex+delta+viewerQueue.length)%viewerQueue.length;if(tvMode){tvVJ++;if(tvVJ>=4){tvVJ=0;vjSlot()}}showViewer()} function randomCat(){const list=filterMemes();if(!list.length){toast('No cats here yet. Discover a few first.');return}const unseen=list.filter(m=>!seen.has(m.id));const pool=unseen.length?unseen:list;const m=pool[Math.floor(Math.random()*pool.length)];openViewer(m.id,shuffle(list))} function closeViewer(){$('#viewer').close()} +function enterTV(){if(!viewerQueue.length)return;tvReturn={queue:[...viewerQueue],index:viewerIndex};tvMode=true;const blk=tvBlock();tvChannel=blk.mood==='prime'?'all':blk.mood;tvPrime=blk.mood==='prime';tvBlockName=blk.name; + if(tvPrime){viewerQueue=[...allMemes()].sort((a,b)=>hotScore(b.id)-hotScore(a.id)||a.title.localeCompare(b.title))}else{const pool=tvChannel==='all'?[...allMemes()]:allMemes().filter(m=>m.mood===tvChannel);viewerQueue=pool.length?shuffle(pool):shuffle([...allMemes()])} + viewerIndex=0;tvPlays=0;tvVJ=0;const vj0=$('#tv-vj');if(vj0)vj0.textContent='';showViewer();restartAutoplay();zap();ident('📺 CAT TV',`${blk.name.toUpperCase()}${tvPrime?' · HOTTEST FIRST':''}`)} +function exitTV(){tvMode=false;tvSleep=0;tvClip=false;clearTimeout(clipT);$('#viewer').classList.remove('tv');$('#tv-overlay').hidden=true;$('#screen').hidden=true;stopAutoplay();if(tvReturn){viewerQueue=tvReturn.queue;viewerIndex=Math.min(tvReturn.index,Math.max(0,viewerQueue.length-1));tvReturn=null;if($('#viewer').open)showViewer()}} +const TV_SPEEDS=[3,6,10],TV_SLEEPS=[0,10,20,30]; +let tvChannel='all',tvSpeedIdx=1,tvSleepIdx=0,tvSleep=0,tvReturn=null,tvPlays=0,tvVJ=0,tvPrime=false,tvBlockName='',identT=null; +function tvBlock(h=new Date().getHours()){const order=[[2,'snacks','Night Snacks'],[5,'wholesome','Morning Cuddles'],[10,'relatable','Midday Relatables'],[13,'chaos','Afternoon Chaos'],[17,'prime','Prime Time'],[22,'sleepy','After Dark']];let b=order[order.length-1];for(const x of order)if(h>=x[0])b=x;return {mood:b[1],name:b[2]}} +const VJ_QUIPS=['That last one broke me. In a good way.','I would like the record to show: I laughed.','My whiskers are tingling. Good sign.','Note to self: steal that one for the group chat.','Ten out of ten. No notes. Next.','I pay zero rent and judge this highly.','Somewhere, a dog is furious about this broadcast.','This channel is my Roman empire.','Shh. The good part is coming. Probably.','I approved this message. I approve most messages.']; +function vjSlot(){const vj=$('#tv-vj');if(!vj)return;vj.textContent='🐱 Biscuit: '+VJ_QUIPS[Math.floor(Math.random()*VJ_QUIPS.length)];sfx('meow')} +const reducedMotion=()=>matchMedia('(prefers-reduced-motion: reduce)').matches; +function ident(big,small){if(reducedMotion()){toast(`📺 ${big} — ${small}`);return}const s=$('#screen');if(!s)return;s.innerHTML=`
${esc(big)}
${esc(small)}
`;s.hidden=false;s.classList.remove('show');void s.offsetWidth;s.classList.add('show');clearTimeout(identT);identT=setTimeout(()=>{s.hidden=true},1600)} +function signOff(){const s=$('#screen');if(!reducedMotion()&&s){s.innerHTML=`
PLEASE STAND BYNAPPING IN PROGRESS…
`;s.hidden=false;s.classList.remove('show');void s.offsetWidth;s.classList.add('show');setTimeout(()=>{closeViewer();toast('💤 TV off. Dream of potassium.')},2600)}else{closeViewer();toast('💤 TV off. Dream of potassium.')}} +const ADS=[ + {brand:'Potassium™',line:'For cats with places to be.',img:'/assets/memes/7s8bhl.jpg',cta:'ASK YOUR HUMAN'}, + {brand:'THE BOX',line:'Now with 20% more box.',img:'/assets/memes/tau4.jpg',cta:'FITS (ALLEGEDLY)'}, + {brand:'NapCo™',line:'Horizontal is a lifestyle.',img:'/assets/memes/lp2ba.jpg',cta:'DO NOT PERCEIVE'}, + {brand:'Sunbeam Reserve',line:'Moves one inch. So do you.',img:'/assets/memes/11wis1.jpg',cta:'CHASE IT'}, + {brand:'Crünch™',line:'Louder means hungrier.',img:'/assets/memes/4p3h78.jpg',cta:'SECOND BREAKFAST'}, + {brand:'Grump™',line:'Judgment you can trust.',img:'/assets/memes/8p0a.jpg',cta:'NO REFUNDS'}, + {brand:'Zoomies Energy',line:'3AM. No destination.',img:'/assets/memes/24uu.jpg',cta:'FEEL THE RUSH'}, + {brand:'Smug™',line:'The absence of vegetables.',img:'/assets/memes/39binw.jpg',cta:'ORDER ANYWAY'}]; +function showAd(){const ad=ADS[Math.floor(Math.random()*ADS.length)];$('#viewer-image-wrap').innerHTML=`
AD
${esc(ad.brand)}
${esc(ad.brand)}
${esc(ad.line)}
${esc(ad.cta)}
`;$('#tv-name').textContent='📢 '+ad.brand;$('#tv-next').textContent='Your show resumes shortly · click to skip';sfx('save')} +function tvTick(){if(tvSleep>0){tvSleep--;const m=currentMeme();if(m)updateTVOverlay(m);if(tvSleep<=0){signOff();return}}tvPlays++;const clips=[...BUNDLED_CLIPS,...userClips];if(clips.length&&tvPlays%8===0){showClip(clips[Math.floor(Math.random()*clips.length)])}else if(tvPlays%5===0){showAd()}else{browse(1)}} +const BUNDLED_CLIPS=[ + {src:'/assets/clips/calico.mp4',title:'Calico Cam',credit:'CC BY-SA · Wikimedia Commons'}, + {src:'/assets/clips/tokyo.mp4',title:'Tokyo Kitten',credit:'CC BY · Wikimedia Commons'}, + {src:'/assets/clips/gatos.mp4',title:'Gatos Playing',credit:'CC BY-SA · Wikimedia Commons'}]; +let userClips=[],tvClip=false,clipT=null; +function showClip(clip){if(!clip)return;tvClip=true;stopAutoplay();$('#viewer-image-wrap').innerHTML=``;$('#tv-name').textContent='🎥 '+clip.title;$('#tv-next').textContent='Back to the show shortly · click to skip';sfx('blip');clearTimeout(clipT);clipT=setTimeout(()=>{if(tvMode&&$('#viewer').open){tvClip=false;browse(1);restartAutoplay()}},5500)} +function skipClip(){clearTimeout(clipT);tvClip=false;browse(1);restartAutoplay()} +async function importClip(file){if(!file)return;if(!['video/mp4','video/webm'].includes(file.type)){toast('MP4 or WebM only, please.');return}if(file.size>15*1024*1024){toast('That’s a big clip. Under 15 MB, please.');return}if(userClips.length>=10){toast('Ten personal clips max. The schedule is full.');return} + try{const rec={id:'clip-'+crypto.randomUUID(),name:file.name,blob:file,added:new Date().toISOString()};await dbPutClip(rec);userClips.push({id:rec.id,title:file.name.replace(/\.[^.]+$/,''),src:URL.createObjectURL(file)});toast('Your clip joins the rotation. Stardom awaits.')}catch{toast('Could not store that clip in this browser.')}} +function cycleVol(){volIdx=(volIdx+1)%VOLS.length;masterVol=VOLS[volIdx];setLocal('vol',masterVol);applySoundIcon();updateTVOverlay(currentMeme());sfx('blip')} +function zap(){if(matchMedia('(prefers-reduced-motion: reduce)').matches)return;const z=$('#zap');if(!z)return;z.classList.remove('on');void z.offsetWidth;z.classList.add('on')} +function setTVChannel(d){const i=(moods.findIndex(m=>m[0]===tvChannel)+d+moods.length)%moods.length;tvChannel=moods[i][0];tvPrime=false;const pool=tvChannel==='all'?[...allMemes()]:allMemes().filter(m=>m.mood===tvChannel);if(!pool.length)return;viewerQueue=shuffle(pool);viewerIndex=0;showViewer();zap();sfx('blip');ident(`CH${i+1} · ${moods[i][1]} ${moods[i][2]}`,tvChannel==='all'?`${library.length} CATS · NO MERCY`:`${pool.length} CATS · ALL ${moods[i][2].toUpperCase()}`)} +function cycleTVSpeed(){tvSpeedIdx=(tvSpeedIdx+1)%TV_SPEEDS.length;if(autoTimer)restartAutoplay();updateTVOverlay(currentMeme());sfx('blip')} +function cycleTVSleep(){tvSleepIdx=(tvSleepIdx+1)%TV_SLEEPS.length;tvSleep=TV_SLEEPS[tvSleepIdx];updateTVOverlay(currentMeme());toast(tvSleep?`💤 Sleep timer: TV off in ${tvSleep} cats.`:'💤 Sleep timer off. Eternal broadcast.')} +function updateTVOverlay(m){if(!m)return;const i=Math.max(0,moods.findIndex(x=>x[0]===tvChannel));$('#tv-chan').textContent=tvPrime?`⭐ PRIME · Trending`:`CH${i+1} ${moods[i][1]} ${moods[i][2]}`;$('#tv-clock').textContent=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'});$('#tv-count').textContent=`${viewerIndex+1} / ${viewerQueue.length}`;$('#tv-name').textContent=m.title;const nx=viewerQueue[(viewerIndex+1)%viewerQueue.length];$('#tv-next').textContent=nx&&viewerQueue.length>1?`Up next: ${nx.title}`:'';$('#tv-speed').textContent=TV_SPEEDS[tvSpeedIdx]+'s';$('#tv-sleep').textContent=tvSleep?`💤 ${tvSleep}`:'💤 Off';$('#tv-vol').textContent=`🔊 ${Math.round(masterVol*100)}%`} function stopAutoplay(){clearInterval(autoTimer);autoTimer=null;$('#autoplay').innerHTML=icon('play')+'Autoplay';$('#autoplay').setAttribute('aria-pressed','false');$('#autoplay-fill').classList.remove('running')} -function restartAutoplay(){clearInterval(autoTimer);autoTimer=setInterval(()=>browse(1),6000);$('#autoplay').innerHTML=icon('pause')+'Pause';$('#autoplay').setAttribute('aria-pressed','true');$('#autoplay-fill').classList.remove('running');void $('#autoplay-fill').offsetWidth;$('#autoplay-fill').classList.add('running')} +function restartAutoplay(){clearInterval(autoTimer);const secs=tvMode?TV_SPEEDS[tvSpeedIdx]:6;autoTimer=setInterval(()=>{if(tvMode)tvTick();else browse(1)},secs*1000);$('#autoplay').innerHTML=icon('pause')+'Pause';$('#autoplay').setAttribute('aria-pressed','true');const fill=$('#autoplay-fill');fill.style.animationDuration=secs+'s';fill.classList.remove('running');void fill.offsetWidth;fill.classList.add('running')} function templates(){return [...new Map(library.map(m=>[m.image,m])).values()]} -function editorMeme(){return {...editing,title:$('#meme-name').value.trim()||'My little masterpiece',top:$('#top-caption').value.trim(),bottom:$('#bottom-caption').value.trim(),mood:$('#editor-mood').value}} +function editorMeme(){const sv=$('#editor-sticker').value;return {...editing,title:$('#meme-name').value.trim()||'My little masterpiece',top:$('#top-caption').value.trim(),bottom:$('#bottom-caption').value.trim(),mood:$('#editor-mood').value,font:$('#editor-font').value,style:$('#editor-style').value,sticker:sv==='none'?null:{emoji:sv,corner:$('#editor-corner').value}}} function preview(){$('#editor-preview').innerHTML=visual(editorMeme(),'eager')} -function openEditor(m){if($('#viewer').open)closeViewer();editing={...(m||library[0])};uploadedTemplate=editing.image.startsWith('data:')?{image:editing.image,template:editing.template,source:editing.source}:null;$('#template-select').innerHTML=templates().map(t=>``).join(''); +function openEditor(m){if($('#viewer').open)closeViewer();inspired=false;editing={...(m||library[0])};uploadedTemplate=editing.image.startsWith('data:')?{image:editing.image,template:editing.template,source:editing.source}:null;$('#template-select').innerHTML=templates().map(t=>``).join(''); if(editing.image.startsWith('data:')){$('#template-select').insertAdjacentHTML('afterbegin','');$('#template-select').value='custom'}else $('#template-select').value=editing.image; - $('#top-caption').value=m?.top||'';$('#bottom-caption').value=m?.bottom||'';$('#meme-name').value=m?m.title+' (my version)':'';$('#editor-mood').innerHTML=moods.slice(1).map(([id,s,label])=>``).join('');$('#editor-mood').value=m?.mood||'relatable';$('#upload').value='';preview();$('#editor').showModal() + $('#top-caption').value=m?.top||'';$('#bottom-caption').value=m?.bottom||'';$('#meme-name').value=m?m.title+' (my version)':'';$('#editor-mood').innerHTML=moods.slice(1).map(([id,s,label])=>``).join('');$('#editor-mood').value=m?.mood||'relatable';$('#editor-font').value=m?.font||'dm';$('#editor-style').value=m?.style||'light';$('#editor-sticker').value=m?.sticker?.emoji||'none';$('#editor-corner').value=m?.sticker?.corner||'tr';$('#upload').value='';preview();$('#editor').showModal() } const loadImage=src=>new Promise((resolve,reject)=>{const img=new Image();img.onload=()=>resolve(img);img.onerror=()=>reject(new Error('This image could not be loaded.'));img.src=src}); async function uploadCat(file){if(!file)return;if(!['image/jpeg','image/png','image/webp','image/gif'].includes(file.type)){toast('Please choose a JPG, PNG, WebP, or GIF.');return}if(file.size>12*1024*1024){toast('That’s a big cat. Choose an image under 12 MB.');return} const url=URL.createObjectURL(file);try{const img=await loadImage(url);const scale=Math.min(1,1400/Math.max(img.width,img.height));const c=document.createElement('canvas');c.width=Math.round(img.width*scale);c.height=Math.round(img.height*scale);const ctx=c.getContext('2d');ctx.fillStyle='#fff';ctx.fillRect(0,0,c.width,c.height);ctx.drawImage(img,0,0,c.width,c.height);uploadedTemplate={image:c.toDataURL('image/jpeg',.9),template:file.name,source:null};editing={...editing,...uploadedTemplate};if(!$('#template-select option[value="custom"]'))$('#template-select').insertAdjacentHTML('afterbegin','');$('#template-select').value='custom';preview();toast(file.type==='image/gif'?'Your cat is in. GIFs use a still frame.':'Your cat has entered the chat.')}catch(e){toast(e.message)}finally{URL.revokeObjectURL(url)} } function wrapText(ctx,text,width){const lines=[];for(const paragraph of text.split('\n')){let line='';for(const word of paragraph.split(/\s+/)){const next=line?line+' '+word:word;if(ctx.measureText(next).width<=width){line=next;continue}if(line)lines.push(line);line='';for(const char of word){if(ctx.measureText(line+char).width>width){lines.push(line);line=char}else line+=char}}lines.push(line)}return lines} -async function exportPNG(m){await document.fonts.ready;const img=await loadImage(m.image);const canvas=document.createElement('canvas'),width=1000,fontSize=42,lineHeight=55,pad=42;canvas.width=width;let ctx=canvas.getContext('2d');ctx.font=`700 ${fontSize}px "DM Sans", sans-serif`; +function chainFrom(lines){const starts=[],map=new Map(),known=new Set(); + for(const raw of lines){const line=String(raw||'').trim();if(!line)continue;known.add(line.toLowerCase());const words=line.split(/\s+/).filter(Boolean);if(words.length<2)continue;starts.push(words[0]); + for(let i=0;i3)break} + let s=out.join(' ').replace(/\s+([,.!?…])/g,'$1');if(s.length<8||s.length>110)continue;const low=s.toLowerCase(); + if(ch.known.has(low))continue;if(out.length<3)continue; + return s.charAt(0).toUpperCase()+s.slice(1)} + return null} +function inspireCaption(){const pool=allMemes();const tops=pool.map(m=>m.top).filter(Boolean),bottoms=pool.map(m=>m.bottom).filter(Boolean); + const top=walkChain(chainFrom(tops))||tops[Math.floor(Math.random()*tops.length)]||''; + const bottom=walkChain(chainFrom(bottoms))||bottoms[Math.floor(Math.random()*bottoms.length)]||''; + return {top,bottom}} +async function exportPNG(m){await document.fonts.ready;const img=await loadImage(m.image);const th_=memeTheme(m);const canvas=document.createElement('canvas'),width=1000,fontSize=42,lineHeight=55,pad=42;canvas.width=width;let ctx=canvas.getContext('2d');ctx.font=`700 ${fontSize}px ${th_.font}`; const top=m.top?wrapText(ctx,m.top,width-pad*2):[],bottom=m.bottom?wrapText(ctx,m.bottom,width-pad*2):[]; - const imageH=Math.round(Math.min(img.height*(width/img.width),1200));const th=top.length?top.length*lineHeight+pad:0,bh=bottom.length?bottom.length*lineHeight+pad:0;canvas.height=th+imageH+bh;ctx=canvas.getContext('2d');ctx.fillStyle='#fff';ctx.fillRect(0,0,width,canvas.height);ctx.fillStyle='#24231e';ctx.font=`700 ${fontSize}px "DM Sans", sans-serif`;ctx.textBaseline='top'; + const imageH=Math.round(Math.min(img.height*(width/img.width),1200));const th=top.length?top.length*lineHeight+pad:0,bh=bottom.length?bottom.length*lineHeight+pad:0;canvas.height=th+imageH+bh;ctx=canvas.getContext('2d');ctx.fillStyle=th_.bg;ctx.fillRect(0,0,width,canvas.height);ctx.fillStyle=th_.fg;ctx.font=`700 ${fontSize}px ${th_.font}`;ctx.textBaseline='top'; top.forEach((s,i)=>ctx.fillText(s,pad,pad/2+i*lineHeight));const ratio=Math.min(width/img.width,imageH/img.height),iw=img.width*ratio,ih=img.height*ratio;ctx.drawImage(img,(width-iw)/2,th+(imageH-ih)/2,iw,ih);bottom.forEach((s,i)=>ctx.fillText(s,pad,th+imageH+pad/2+i*lineHeight)); + if(m.sticker&&m.sticker.emoji){ctx.font='130px serif';const sx=m.sticker.corner==='tl'||m.sticker.corner==='bl'?pad:width-pad-130,sy=m.sticker.corner==='tl'||m.sticker.corner==='tr'?th+10:th+imageH-140;ctx.fillText(m.sticker.emoji,sx,sy)} const blob=await new Promise(res=>canvas.toBlob(res,'image/png'));if(!blob)throw new Error('The PNG couldn’t be created. Please try again.');return blob; } async function download(m,button){if(button)button.disabled=true;try{const blob=await exportPNG(m);const url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download='catmemechimp-'+m.title.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')+'.png';document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),5000);toast('One portable cat. PNG downloaded.')}catch(e){toast(e.message||'Download failed. Please try again.')}finally{if(button)button.disabled=false}} +async function exportMotion(m,button){if(button)button.disabled=true;try{ + if(!('MediaRecorder' in window)||!HTMLCanvasElement.prototype.captureStream)throw new Error('Motion export needs a modern browser. PNG still works.'); + const still=await exportPNG(m);const url=URL.createObjectURL(still);let frame;try{frame=await loadImage(url)}finally{setTimeout(()=>URL.revokeObjectURL(url),5000)} + const width=640,Hs=Math.round(frame.height*(width/frame.width)); + const canvas=document.createElement('canvas');canvas.width=width;canvas.height=Hs;const ctx=canvas.getContext('2d'); + const stream=canvas.captureStream(30);const mime=MediaRecorder.isTypeSupported('video/webm;codecs=vp9')?'video/webm;codecs=vp9':'video/webm'; + const rec=new MediaRecorder(stream,{mimeType:mime,videoBitsPerSecond:2500000});const chunks=[];rec.ondataavailable=e=>{if(e.data&&e.data.size)chunks.push(e.data)}; + const done=new Promise(res=>{rec.onstop=res});rec.start();toast('Rolling… three seconds of cinema.'); + const DUR=3000,t0=performance.now(); + await new Promise(res=>{const step=()=>{const p=Math.min(1,(performance.now()-t0)/DUR),s=1+p*0.12,dw=width*s,dh=Hs*s;ctx.drawImage(frame,(width-dw)/2,(Hs-dh)/2-p*24,dw,dh);if(p<1)requestAnimationFrame(step);else res()};requestAnimationFrame(step)}); + rec.stop();await done; + const blob=new Blob(chunks,{type:'video/webm'});if(!blob.size)throw new Error('The video came out empty. PNG still works.'); + const vurl=URL.createObjectURL(blob),a=document.createElement('a');a.href=vurl;a.download='catmemechimp-'+(m.title||'motion').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')+'.webm';document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(vurl),5000);toast('Motion meme exported. Hollywood calls.')}catch(e){toast(e.message||'Motion export failed. PNG still works.')}finally{if(button)button.disabled=false}} function info(title,content){$('#info-title').textContent=title;$('#info-content').innerHTML=content;$('#info').showModal()} function about(){info('An unserious archive. A serious color.',`

CATMEMECHIMP is a small, lovingly assembled corner of the internet for taking a cat break.

448 C · #4A412A
PERIWINKLE
WARM CREAM

The pixel chimp wears #4A412A, a screen approximation of Pantone 448 C. Deep olive-brown anchors the app; complementary blue and periwinkle bring a little lightness. Its silhouette comes from the canonical Chimp facekit.

Made with creative direction and code by GPT-6 Astra. No accounts, no subscription, no paid API, no tracking. Your favorites and creations stay in this browser.

Color reference ↗

`)} function sources(){info('A little credit for the cats.',`

The starter archive contains ${library.length} original CATMEMECHIMP caption remixes using ${templates().length} community cat templates from Imgflip. Every meme’s viewer links to its individual image source.

The captions were written for this app. Source photographs and templates belong to their respective creators; this app does not claim their ownership or grant reuse rights. Files are stored locally so the collection keeps working when you’re offline.

This is a curated collection, not a live trending feed. The starter collection was assembled on September 8, 2026.

Your favorites use browser storage and your creations use IndexedDB. Clearing this site’s browser data clears them; download your creations if you want a separate copy.

Explore the source templates on Imgflip ↗

`)} -function shortcuts(){info('Less clicking. More cats.',`
/Jump to search
RA random cat from this view
← / →Previous / next cat in the viewer
SSave the cat in the viewer
SpacePause / resume autoplay in the viewer
EscClose the current panel
?This little cheat sheet

On your phone, swipe left or right on a meme in the viewer. Autoplay changes cats every six seconds and pauses when you leave the tab.

`)} -document.addEventListener('click',e=>{const nav=e.target.closest('[data-page]');if(nav){navigate(nav.dataset.page);return}const open=e.target.closest('[data-open]');if(open){openViewer(open.dataset.open);return}const save=e.target.closest('[data-save]');if(save){toggleSave(save.dataset.save);return}const mb=e.target.closest('[data-mood]');if(mb?.classList.contains('mood-filter')){mood=mb.dataset.mood;limit=18;render();return}const cb=e.target.closest('[data-collection]');if(cb){collection=collection===cb.dataset.collection?'':cb.dataset.collection;mood='all';limit=18;render()}}); +function shortcuts(){info('Less clicking. More cats.',`
/Jump to search
RA random cat from this view
← / →Previous / next cat in the viewer, or vote in the Arena
T / B / C / FReroll Frankenmeme top / bottom / cat / full chaos
↑ / ↓Change TV channel in Cat TV mode
OOpen today's pack on the Daily Drop page
MMute / unmute sounds
SSave the cat in the viewer
SpacePause / resume autoplay in the viewer
EscClose the current panel
?This little cheat sheet

On your phone, swipe left or right on a meme in the viewer. Tap a cat to crown it in the Arena. Autoplay changes cats every six seconds and pauses when you leave the tab.

`)} +document.addEventListener('click',e=>{const nav=e.target.closest('[data-page]');if(nav){navigate(nav.dataset.page);return}const vt=e.target.closest('[data-vote]');if(vt){voteBattle(vt.dataset.vote);return}const open=e.target.closest('[data-open]');if(open){openViewer(open.dataset.open,page==='arena'?allMemes():filterMemes());return}const save=e.target.closest('[data-save]');if(save){toggleSave(save.dataset.save);return}const mb=e.target.closest('[data-mood]');if(mb?.classList.contains('mood-filter')){mood=mb.dataset.mood;limit=18;render();return}const cb=e.target.closest('[data-collection]');if(cb){collection=collection===cb.dataset.collection?'':cb.dataset.collection;mood='all';limit=18;render();return}const ds=e.target.closest('[data-delset]');if(ds){const s=setById(ds.dataset.delset);deleteSet(ds.dataset.delset);render();toast(s?`“${s.name}” released into the wild.`:'Set deleted.');return}const cr=e.target.closest('[data-collect]');if(cr){const added=toggleInSet(cr.dataset.collect,currentMeme()?.id);renderCollect();updateCounts();toast(added?'Stashed. A fine addition.':'Removed from that set.');return}const rm=e.target.closest('[data-remove-from-set]');if(rm){toggleInSet(collection,rm.dataset.removeFromSet);render();return}const po=e.target.closest('#pack-open');if(po){openPack();return}const pt=e.target.closest('[data-keeptab]');if(pt){keepTab=pt.dataset.keeptab;renderKeep();return}const tc=e.target.closest('[data-tclear]');if(tc){templateFilter=null;render();return}const lk=e.target.closest('[data-locked]');if(lk){toast('Undiscovered. Prowl Discover, packs, and the Arena…');return}const sh=e.target.closest('[data-share]');if(sh){sharePull(sh.dataset.share);return}const mz=e.target.closest('[data-melt]');if(mz){meltDupe(mz.dataset.melt);return}if(e.target.closest('[data-forge]')){forgePack();return}const xs=e.target.closest('[data-exportsets]');if(xs){exportSets();return}}); $('.brand').addEventListener('click',e=>{e.preventDefault();navigate('discover')}); $('#search').addEventListener('input',()=>{query=$('#search').value.trim();limit=18;render()});$('#sort').addEventListener('change',()=>{limit=18;render()}); -$('#shuffle').onclick=shuffledGrid;$('#hero-random').onclick=randomCat;$('#side-random').onclick=randomCat;$('#side-focus').onclick=()=>{const list=filterMemes();if(list.length)openViewer((list.find(m=>!seen.has(m.id))||list[0]).id);else toast('This corner needs a few cats first.')}; +$('#shuffle').onclick=shuffledGrid;$('#arena-skip').onclick=skipBattle;$('#arena-reset').onclick=resetArena;$('#lab-top').onclick=()=>{rollLab('top');sfx('blip')};$('#lab-bottom').onclick=()=>{rollLab('bottom');sfx('blip')};$('#lab-cat').onclick=()=>{rollLab('cat');sfx('blip')};$('#lab-chaos').onclick=()=>{rollLab('all');sfx('chaos')};$('#lab-inspire').onclick=inspireLab;$('#lab-save').onclick=saveLab;$('#lab-download').onclick=()=>download(labMeme(),$('#lab-download'));$('#hero-random').onclick=randomCat;$('#side-random').onclick=randomCat;$('#side-focus').onclick=()=>{const list=filterMemes();if(list.length)openViewer((list.find(m=>!seen.has(m.id))||list[0]).id);else toast('This corner needs a few cats first.')}; $('#load-more').onclick=()=>{const before=$$('#meme-grid .meme-card').length;limit+=18;render();const first=$$('#meme-grid .meme-open')[before];first?.focus({preventScroll:true})}; $('#back-top').onclick=()=>window.scrollTo({top:0,behavior:'smooth'}); $('#empty-action').onclick=()=>{if(page==='studio'&&!query&&mood==='all')openEditor();else navigate('discover')}; function setDensity(dense){$('#meme-grid').classList.toggle('dense',dense);$('#view-comfy').classList.toggle('selected',!dense);$('#view-dense').classList.toggle('selected',dense);$('#view-comfy').setAttribute('aria-pressed',!dense);$('#view-dense').setAttribute('aria-pressed',dense)} setDensity(readState('dense',false)===true);$('#view-comfy').onclick=()=>{setDensity(false);setLocal('dense',false)};$('#view-dense').onclick=()=>{setDensity(true);setLocal('dense',true)}; +function applyTheme(t){document.documentElement.dataset.theme=t;const b=$('#theme-toggle');if(!b)return;b.innerHTML=icon(t==='dark'?'sun':'moon');b.setAttribute('aria-label',t==='dark'?'Switch to light mode':'Switch to dark mode')} +function toggleTheme(){const next=document.documentElement.dataset.theme==='dark'?'light':'dark';setLocal('theme',next);applyTheme(next);toast(next==='dark'?'Lights out. The cats can see in the dark.':'Lights on. The cats judge you clearly again.')} +applyTheme(readState('theme',matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'));$('#theme-toggle').onclick=toggleTheme; +let soundOn=readState('sound',true),audioCtx=null,audioReady=false; +const _vol=readState('vol',1);let masterVol=typeof _vol==='number'&&_vol>=0&&_vol<=1?_vol:1; +const VOLS=[1,0.6,0.3,0];let volIdx=Math.max(0,VOLS.indexOf(masterVol));if(VOLS[volIdx]!==masterVol)volIdx=0; +for(const ev of ['pointerdown','keydown'])document.addEventListener(ev,()=>{audioReady=true;try{if(!audioCtx)audioCtx=new (window.AudioContext||window.webkitAudioContext)();if(audioCtx.state==='suspended')audioCtx.resume()}catch{}},{once:true,capture:true}); +function tone(freq,dur,{type='sine',vol=0.12,at=0,slide=null}={}){const t0=audioCtx.currentTime+at;const o=audioCtx.createOscillator(),g=audioCtx.createGain();o.type=type;o.frequency.setValueAtTime(freq,t0);if(slide)o.frequency.exponentialRampToValueAtTime(slide,t0+dur);g.gain.setValueAtTime(0.0001,t0);g.gain.exponentialRampToValueAtTime(Math.max(0.0001,vol*masterVol),t0+0.015);g.gain.exponentialRampToValueAtTime(0.0001,t0+dur);o.connect(g);g.connect(audioCtx.destination);o.start(t0);o.stop(t0+dur+0.05)} +function sfx(name){if(!soundOn||!audioReady)return;try{if(!audioCtx)audioCtx=new (window.AudioContext||window.webkitAudioContext)();if(audioCtx.state==='suspended')audioCtx.resume(); + if(name==='pop')tone(520,0.12,{slide:880});else if(name==='meow'){tone(500,0.15,{slide:760,type:'triangle',vol:0.1});tone(760,0.28,{at:0.14,slide:420,type:'triangle',vol:0.1})}else if(name==='save'){tone(660,0.1);tone(880,0.14,{at:0.09})}else if(name==='fanfare')[523,659,784,1047].forEach((f,i)=>tone(f,0.16,{at:i*0.09,type:'triangle'}));else if(name==='blip')tone(440,0.07,{type:'square',vol:0.05});else if(name==='chaos')tone(300,0.2,{slide:150,type:'sawtooth',vol:0.06})}catch{}} +function applySoundIcon(){const b=$('#sound-toggle');if(!b)return;const on=soundOn&&masterVol>0;b.innerHTML=icon(on?'volume':'mute');b.setAttribute('aria-label',on?'Mute sounds':'Unmute sounds')} +function toggleSound(){soundOn=!soundOn;setLocal('sound',soundOn);applySoundIcon();if(soundOn)sfx('save');toast(soundOn?'Sound on. The cats are audible.':'Sound off. Silent judgment continues.')} +applySoundIcon();$('#sound-toggle').onclick=toggleSound; +function confetti(){if(matchMedia('(prefers-reduced-motion: reduce)').matches)return;const c=$('#confetti');if(!c||!c.getContext)return;const x=c.getContext('2d');c.width=innerWidth;c.height=innerHeight;const cols=['#B9C5F3','#eee2b7','#f4e0db','#e3ebd7','#ffffff'];const ps=Array.from({length:90},()=>({x:innerWidth/2+(Math.random()-0.5)*220,y:innerHeight*0.35,vx:(Math.random()-0.5)*9,vy:Math.random()*-7-2,g:0.35,s:Math.random()*7+4,r:Math.random()*Math.PI,vr:(Math.random()-0.5)*0.3,c:cols[Math.floor(Math.random()*cols.length)],l:1}));let f=0;const iv=setInterval(()=>{x.clearRect(0,0,c.width,c.height);f++;ps.forEach(p=>{p.x+=p.vx;p.y+=p.vy;p.vy+=p.g;p.r+=p.vr;p.l-=0.008;x.save();x.globalAlpha=Math.max(0,p.l);x.translate(p.x,p.y);x.rotate(p.r);x.fillStyle=p.c;x.fillRect(-p.s/2,-p.s/2,p.s,p.s*0.6);x.restore()});if(f>90){clearInterval(iv);x.clearRect(0,0,c.width,c.height)}},16)} $('#viewer-close').onclick=closeViewer;$('#viewer-prev').onclick=()=>browse(-1);$('#viewer-next').onclick=()=>browse(1);$('#viewer-save').onclick=()=>toggleSave(currentMeme().id);$('#viewer-edit').onclick=()=>openEditor(currentMeme());$('#viewer-download').onclick=()=>download(currentMeme(),$('#viewer-download'));$('#autoplay').onclick=()=>autoTimer?stopAutoplay():restartAutoplay(); -$('#viewer').addEventListener('close',()=>{stopAutoplay();history.replaceState(null,'',location.pathname);if($('#sort').value==='unseen')render()}); +$('#viewer').addEventListener('close',()=>{tvMode=false;$('#viewer').classList.remove('tv');stopAutoplay();history.replaceState(null,'',location.pathname);if($('#sort').value==='unseen')render()}); +$('#viewer').addEventListener('cancel',e=>{if(tvMode){e.preventDefault();exitTV()}}); +$('#viewer-image-wrap').addEventListener('click',()=>{if(tvMode){if(tvClip)skipClip();else browse(1)}}); +$('#tv-toggle').onclick=enterTV;$('#tv-exit').onclick=exitTV;$('#tv-ch-up').onclick=()=>setTVChannel(-1);$('#tv-ch-down').onclick=()=>setTVChannel(1);$('#tv-speed').onclick=cycleTVSpeed;$('#tv-sleep').onclick=cycleTVSleep;$('#tv-vol').onclick=cycleVol; $('#viewer-share').onclick=async()=>{if(currentMeme().kind==='creation'){toast('This creation lives here. Download its PNG to share it.');return}try{await navigator.clipboard.writeText(location.href);toast('Link copied. It opens on this computer while the app is running.')}catch{toast('Copy the meme link from your address bar.')}}; +$('#viewer-more').onclick=()=>{const m=currentMeme();if(!m||m.kind==='creation')return;const img=m.image;closeViewer();showTemplate(img)}; +$('#viewer-collect').onclick=()=>{renderCollect();$('#collect-new-name').value='';$('#collect-dialog').showModal()};$('#collect-close').onclick=()=>$('#collect-dialog').close(); +document.addEventListener('submit',e=>{if(e.target.id==='new-set-form'){e.preventDefault();const s=createSet($('#new-set-name').value);render();toast(`Set “${s.name}” created. Start hoarding.`)}if(e.target.id==='collect-new'){e.preventDefault();const s=createSet($('#collect-new-name').value);$('#collect-new-name').value='';renderCollect();toast(`Set “${s.name}” created. Tap it to stash this cat.`)}}); +document.addEventListener('change',e=>{if(e.target.id==='import-sets'){importSets(e.target.files[0]);e.target.value=''}if(e.target.id==='tv-clip-upload'){importClip(e.target.files[0]);e.target.value=''}}); $('#create-open').onclick=()=>openEditor();$('#editor-close').onclick=()=>$('#editor').close();$('#top-caption').oninput=preview;$('#bottom-caption').oninput=preview; $('#template-select').onchange=()=>{const m=$('#template-select').value==='custom'?uploadedTemplate:library.find(m=>m.image===$('#template-select').value);if(m){editing={...editing,image:m.image,template:m.template,source:m.source};preview()}}; -$('#upload').onchange=()=>uploadCat($('#upload').files[0]);$('#editor-download').onclick=()=>download(editorMeme(),$('#editor-download')); -$('#editor-form').onsubmit=async e=>{e.preventDefault();const button=e.submitter;button.disabled=true;const m={...editorMeme(),id:'own-'+crypto.randomUUID(),kind:'creation',added:new Date().toISOString(),tags:['my creation']};try{await dbPut(m);created.push(m);$('#editor').close();navigate('studio');toast('A masterpiece has been born. Saved in My creations.')}catch{toast('Could not save in this browser. Use PNG to keep your creation.')}finally{button.disabled=false}}; +for(const id of ['editor-font','editor-style','editor-sticker','editor-corner'])document.getElementById(id).addEventListener('change',preview); +$('#upload').onchange=()=>uploadCat($('#upload').files[0]);$('#editor-download').onclick=()=>download(editorMeme(),$('#editor-download'));$('#editor-motion').onclick=()=>exportMotion(editorMeme(),$('#editor-motion'));$('#lab-motion').onclick=()=>exportMotion(labMeme(),$('#lab-motion')); +$('#editor-inspire').onclick=()=>{const g=inspireCaption();$('#top-caption').value=g.top;$('#bottom-caption').value=g.bottom;inspired=true;preview();sfx('blip');toast('The machine dreamed this. Edit freely, take credit.')}; +$('#editor-form').onsubmit=async e=>{e.preventDefault();const button=e.submitter;button.disabled=true;const m={...editorMeme(),id:'own-'+crypto.randomUUID(),kind:'creation',added:new Date().toISOString(),tags:inspired?['my creation','robot-assisted']:['my creation']};try{await dbPut(m);created.push(m);$('#editor').close();sfx('fanfare');checkAwards();navigate('studio');toast('A masterpiece has been born. Saved in My creations.')}catch{toast('Could not save in this browser. Use PNG to keep your creation.')}finally{button.disabled=false}}; $('#about-open').onclick=about;$('#shortcuts-open').onclick=shortcuts;$('#sources-open').onclick=sources;$('#info-close').onclick=()=>$('#info').close(); for(const d of $$('dialog'))d.addEventListener('click',e=>{if(e.target===d){const r=d.getBoundingClientRect();if(e.clientXr.right||e.clientYr.bottom)d.close()}}); document.addEventListener('keydown',e=>{if(e.ctrlKey||e.altKey||e.metaKey||/INPUT|TEXTAREA|SELECT/.test(e.target.tagName)||e.target.isContentEditable)return; - if($('#viewer').open){if(e.key==='ArrowRight'){e.preventDefault();browse(1)}else if(e.key==='ArrowLeft'){e.preventDefault();browse(-1)}else if(e.key.toLowerCase()==='s'){e.preventDefault();toggleSave(currentMeme().id)}else if(e.code==='Space'){e.preventDefault();autoTimer?stopAutoplay():restartAutoplay()}return} - if($('dialog[open]'))return;if(e.key==='/'){e.preventDefault();$('#search').focus()}else if(e.key.toLowerCase()==='r'){e.preventDefault();randomCat()}else if(e.key==='?'){e.preventDefault();shortcuts()}}); + if($('#viewer').open){if(tvMode&&e.key==='ArrowUp'){e.preventDefault();setTVChannel(-1)}else if(tvMode&&e.key==='ArrowDown'){e.preventDefault();setTVChannel(1)}else if(e.key==='ArrowRight'){e.preventDefault();browse(1)}else if(e.key==='ArrowLeft'){e.preventDefault();browse(-1)}else if(e.key.toLowerCase()==='s'){e.preventDefault();toggleSave(currentMeme().id)}else if(e.code==='Space'){e.preventDefault();autoTimer?stopAutoplay():restartAutoplay()}return} + if($('dialog[open]'))return;if(e.key==='/'){e.preventDefault();$('#search').focus()}else if(e.key.toLowerCase()==='r'){e.preventDefault();randomCat()}else if(e.key==='?'){e.preventDefault();shortcuts()}else if(page==='arena'&&e.key==='ArrowLeft'&&battleLeft){e.preventDefault();voteBattle(battleLeft.id)}else if(page==='arena'&&e.key==='ArrowRight'&&battleRight){e.preventDefault();voteBattle(battleRight.id)}else if(page==='lab'&&e.key.toLowerCase()==='t'){e.preventDefault();rollLab('top');sfx('blip')}else if(page==='lab'&&e.key.toLowerCase()==='b'){e.preventDefault();rollLab('bottom');sfx('blip')}else if(page==='lab'&&e.key.toLowerCase()==='c'){e.preventDefault();rollLab('cat');sfx('blip')}else if(page==='lab'&&e.key.toLowerCase()==='f'){e.preventDefault();rollLab('all');sfx('chaos')}else if(page==='packs'&&e.key.toLowerCase()==='o'){e.preventDefault();openPack()}else if(e.key.toLowerCase()==='m'){e.preventDefault();toggleSound()}}); document.addEventListener('visibilitychange',()=>{if(document.hidden&&autoTimer)stopAutoplay()}); let swipe=null;$('#viewer-image-wrap').addEventListener('touchstart',e=>{swipe={x:e.changedTouches[0].clientX,y:e.changedTouches[0].clientY}},{passive:true});$('#viewer-image-wrap').addEventListener('touchend',e=>{if(!swipe)return;const dx=e.changedTouches[0].clientX-swipe.x,dy=e.changedTouches[0].clientY-swipe.y;if(Math.abs(dx)>55&&Math.abs(dx)>Math.abs(dy)*1.5)browse(dx<0?1:-1);swipe=null},{passive:true}); -async function init(){try{const response=await fetch('/library.json');if(!response.ok)throw new Error('The archive could not load.');library=await response.json();try{created=await dbRead()}catch{toast('Creations storage is unavailable; PNG downloads still work.')}render();const deepId=new URLSearchParams(location.hash.slice(1)).get('meme');if(deepId){const m=library.find(m=>m.id===deepId);if(m)openViewer(m.id,library);else toast('That cat link is not in this archive.')} +let battleSwipe=null;$('#battle-grid').addEventListener('touchstart',e=>{battleSwipe={x:e.changedTouches[0].clientX,y:e.changedTouches[0].clientY}},{passive:true});$('#battle-grid').addEventListener('touchend',e=>{if(!battleSwipe||page!=='arena')return;const dx=e.changedTouches[0].clientX-battleSwipe.x,dy=e.changedTouches[0].clientY-battleSwipe.y;battleSwipe=null;if(Math.abs(dx)>55&&Math.abs(dx)>Math.abs(dy)*1.5){if(dx<0&&battleRight)voteBattle(battleRight.id);else if(dx>0&&battleLeft)voteBattle(battleLeft.id)}},{passive:true}); +async function init(){if('serviceWorker' in navigator){try{await navigator.serviceWorker.register('/sw.js')}catch{}}try{const response=await fetch('/library.json');if(!response.ok)throw new Error('The archive could not load.');library=await response.json();try{created=await dbRead()}catch{toast('Creations storage is unavailable; PNG downloads still work.')}battleStats=readBattles();rollSeason();pickBattle();rollLab();try{for(const c of await dbReadClips())userClips.push({id:c.id,title:(c.name||'clip').replace(/\.[^.]+$/,''),src:URL.createObjectURL(c.blob)})}catch{}render();buddyInit();fillTicker();heroParallax();const deepId=new URLSearchParams(location.hash.slice(1)).get('meme');if(deepId){const m=library.find(m=>m.id===deepId);if(m)openViewer(m.id,library);else toast('That cat link is not in this archive.')} + if(!deepId&&!readState('welcomed',false)){setLocal('welcomed',true);setTimeout(()=>info('Welcome to the cat archive.',`

Follow the thread: 🔍 Find → 🏟 Crown → 🧪 Stitch → 📦 Keep.

290 memes. Rip a daily pack, battle the arena, stitch lab abominations, fill the album dex.

/ search · R random cat · O open pack · ? everything else. All local, all yours.

`),450)} }catch(e){$('#result-count').textContent='The cats couldn’t arrive.';$('#empty').hidden=false;$('#empty-title').textContent='A small cat-astrophe.';$('#empty-copy').textContent='The local library could not load. Reload to try again.';$('#empty-action').textContent='Try again';$('#empty-action').onclick=()=>location.reload();console.error(e)}} window.addEventListener('hashchange',()=>{const id=new URLSearchParams(location.hash.slice(1)).get('meme');if(!id){if($('#viewer').open)closeViewer();return}const m=library.find(m=>m.id===id);if(!m){toast('That cat link is not in this archive.');return}if($('#viewer').open){viewerQueue=[...library];viewerIndex=viewerQueue.findIndex(m=>m.id===id);showViewer()}else openViewer(id,library)}); await init(); diff --git a/public/assets/clips/calico.mp4 b/public/assets/clips/calico.mp4 new file mode 100644 index 0000000..636051b Binary files /dev/null and b/public/assets/clips/calico.mp4 differ diff --git a/public/assets/clips/gatos.mp4 b/public/assets/clips/gatos.mp4 new file mode 100644 index 0000000..67a679c Binary files /dev/null and b/public/assets/clips/gatos.mp4 differ diff --git a/public/assets/clips/tokyo.mp4 b/public/assets/clips/tokyo.mp4 new file mode 100644 index 0000000..1de9d8a Binary files /dev/null and b/public/assets/clips/tokyo.mp4 differ diff --git a/public/assets/memes/22vtk.jpg b/public/assets/memes/22vtk.jpg new file mode 100644 index 0000000..a0ccd3c Binary files /dev/null and b/public/assets/memes/22vtk.jpg differ diff --git a/public/assets/memes/345v97.jpg b/public/assets/memes/345v97.jpg new file mode 100644 index 0000000..c096d89 Binary files /dev/null and b/public/assets/memes/345v97.jpg differ diff --git a/public/assets/memes/3m5mp.jpg b/public/assets/memes/3m5mp.jpg new file mode 100644 index 0000000..97b0212 Binary files /dev/null and b/public/assets/memes/3m5mp.jpg differ diff --git a/public/assets/memes/7rf5z.jpg b/public/assets/memes/7rf5z.jpg new file mode 100644 index 0000000..ad21305 Binary files /dev/null and b/public/assets/memes/7rf5z.jpg differ diff --git a/public/assets/memes/8tlh.jpg b/public/assets/memes/8tlh.jpg new file mode 100644 index 0000000..e57f8e0 Binary files /dev/null and b/public/assets/memes/8tlh.jpg differ diff --git a/public/assets/memes/jru7x.jpg b/public/assets/memes/jru7x.jpg new file mode 100644 index 0000000..c27a381 Binary files /dev/null and b/public/assets/memes/jru7x.jpg differ diff --git a/public/assets/memes/zl7we.jpg b/public/assets/memes/zl7we.jpg new file mode 100644 index 0000000..6ae2955 Binary files /dev/null and b/public/assets/memes/zl7we.jpg differ diff --git a/public/index.html b/public/index.html index bff6543..dd8c9b8 100644 --- a/public/index.html +++ b/public/index.html @@ -2,7 +2,7 @@ - + CATMEMECHIMP — Good cats. Questionable behavior. @@ -12,13 +12,22 @@
-
+
-
A SMALL BREAK FROM BEING A PERSON

Serious archive.
Unserious cats.

The finest feline nonsense, all in one happy place.
Find your mood. Save your favorites. Lose five minutes.

No algorithm. Just cats.
+
A SMALL BREAK FROM BEING A PERSON

Serious archive.
Unserious cats.

The finest feline nonsense, all in one happy place.
Find your mood. Save your favorites. Lose five minutes.

or follow the thread:
No algorithm. Just cats.
-
-
+
+ +
THE GOOD STUFF

Find your feline frequency.

- + + + + +
-
Rounding up the cats…
+
Rounding up the cats…
@@ -48,9 +62,12 @@
A little less doom. A little more meow.
- -
THE CAT BREAK

Image source ↗ browse S save Esc close
-
THE NONSENSE LAB

Make a little mischief.

Saved in this browser. Yours to keep, download, and share.

+ +
THE CAT BREAK

Image source ↗ browse S save Esc close
+
THE NONSENSE LAB

Make a little mischief.

Saved in this browser. Yours to keep, download, and share.

-
+
STASH IT

Add to a set

+
+ + diff --git a/public/library.json b/public/library.json index ea77c12..886021d 100644 --- a/public/library.json +++ b/public/library.json @@ -13,7 +13,10 @@ "template": "sad thumbs up cat", "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." }, { "id": "cat-002", @@ -29,7 +32,10 @@ "template": "Huh Cat", "source": "https://imgflip.com/meme/488919824/Huh-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." }, { "id": "cat-003", @@ -46,7 +52,10 @@ "template": "I Should Buy A Boat Cat", "source": "https://imgflip.com/meme/I-Should-Buy-A-Boat-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "immortal", + "lore": "Boat questionable. Box undeniable." }, { "id": "cat-004", @@ -62,7 +71,10 @@ "template": "Grumpy Cat", "source": "https://imgflip.com/meme/Grumpy-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." }, { "id": "cat-005", @@ -78,7 +90,10 @@ "template": "Banana cat", "source": "https://imgflip.com/meme/470680617/Banana-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." }, { "id": "cat-006", @@ -94,7 +109,10 @@ "template": "Sleepy Kitten", "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." }, { "id": "cat-007", @@ -110,7 +128,10 @@ "template": "Smiling Cat", "source": "https://imgflip.com/meme/Smiling-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." }, { "id": "cat-008", @@ -126,7 +147,10 @@ "template": "Business Cat", "source": "https://imgflip.com/meme/Business-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." }, { "id": "cat-009", @@ -142,7 +166,10 @@ "template": "Smudge the cat", "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." }, { "id": "cat-010", @@ -158,7 +185,10 @@ "template": "vibing cat", "source": "https://imgflip.com/meme/277365922/vibing-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2020, + "status": "classic", + "lore": "Headphones on. World off." }, { "id": "cat-011", @@ -174,7 +204,10 @@ "template": "Crying cat", "source": "https://imgflip.com/meme/42003405/Crying-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2014, + "status": "classic", + "lore": "Crying professionally for a decade." }, { "id": "cat-012", @@ -190,7 +223,10 @@ "template": "Pop Cat", "source": "https://imgflip.com/meme/284017364/Pop-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." }, { "id": "cat-013", @@ -206,7 +242,10 @@ "template": "Cool Cat Stroll", "source": "https://imgflip.com/meme/Cool-Cat-Stroll", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." }, { "id": "cat-014", @@ -222,7 +261,10 @@ "template": "I'm awake, but at what cost?", "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." }, { "id": "cat-015", @@ -238,7 +280,10 @@ "template": "Cute Cat", "source": "https://imgflip.com/meme/Cute-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." }, { "id": "cat-016", @@ -254,7 +299,10 @@ "template": "Scared Cat", "source": "https://imgflip.com/meme/Scared-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2019, + "status": "classic", + "lore": "Startled since 2019." }, { "id": "cat-017", @@ -270,7 +318,10 @@ "template": "Cat newspaper", "source": "https://imgflip.com/meme/19259689/Cat-newspaper", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2015, + "status": "classic", + "lore": "Reports the news. Is the news." }, { "id": "cat-018", @@ -286,7 +337,10 @@ "template": "Grumpy Cat Not Amused", "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." }, { "id": "cat-019", @@ -302,7 +356,10 @@ "template": "Sad Cat Holding Dog", "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." }, { "id": "cat-020", @@ -318,7 +375,10 @@ "template": "Crying cat on phone", "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." }, { "id": "cat-021", @@ -334,7 +394,10 @@ "template": "Sleepy Cat", "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." }, { "id": "cat-022", @@ -350,7 +413,10 @@ "template": "Chemistry Cat", "source": "https://imgflip.com/meme/Chemistry-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2011, + "status": "classic", + "lore": "Peer reviews your snacks." }, { "id": "cat-023", @@ -366,7 +432,10 @@ "template": "white cat table", "source": "https://imgflip.com/meme/191245022/white-cat-table", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." }, { "id": "cat-024", @@ -382,7 +451,10 @@ "template": "Excited Cat", "source": "https://imgflip.com/meme/Excited-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." }, { "id": "cat-025", @@ -398,7 +470,10 @@ "template": "Huh Cat", "source": "https://imgflip.com/meme/488919824/Huh-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." }, { "id": "cat-026", @@ -414,7 +489,10 @@ "template": "Banana cat", "source": "https://imgflip.com/meme/470680617/Banana-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." }, { "id": "cat-027", @@ -430,7 +508,10 @@ "template": "Grumpy Cat", "source": "https://imgflip.com/meme/Grumpy-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." }, { "id": "cat-028", @@ -446,7 +527,10 @@ "template": "Sleepy Kitten", "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." }, { "id": "cat-029", @@ -462,7 +546,10 @@ "template": "vibing cat", "source": "https://imgflip.com/meme/277365922/vibing-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2020, + "status": "classic", + "lore": "Headphones on. World off." }, { "id": "cat-030", @@ -478,7 +565,10 @@ "template": "Smiling Cat", "source": "https://imgflip.com/meme/Smiling-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." }, { "id": "cat-031", @@ -494,7 +584,10 @@ "template": "I Should Buy A Boat Cat", "source": "https://imgflip.com/meme/I-Should-Buy-A-Boat-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "immortal", + "lore": "Boat questionable. Box undeniable." }, { "id": "cat-032", @@ -510,7 +603,10 @@ "template": "Pop Cat", "source": "https://imgflip.com/meme/284017364/Pop-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." }, { "id": "cat-033", @@ -526,7 +622,10 @@ "template": "sad thumbs up cat", "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." }, { "id": "cat-034", @@ -542,7 +641,10 @@ "template": "Cool Cat Stroll", "source": "https://imgflip.com/meme/Cool-Cat-Stroll", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." }, { "id": "cat-035", @@ -558,7 +660,10 @@ "template": "Smudge the cat", "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." }, { "id": "cat-036", @@ -574,7 +679,10 @@ "template": "I'm awake, but at what cost?", "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." }, { "id": "cat-037", @@ -590,7 +698,10 @@ "template": "Cute Cat", "source": "https://imgflip.com/meme/Cute-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." }, { "id": "cat-038", @@ -606,7 +717,10 @@ "template": "Cat newspaper", "source": "https://imgflip.com/meme/19259689/Cat-newspaper", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2015, + "status": "classic", + "lore": "Reports the news. Is the news." }, { "id": "cat-039", @@ -622,7 +736,10 @@ "template": "Scared Cat", "source": "https://imgflip.com/meme/Scared-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2019, + "status": "classic", + "lore": "Startled since 2019." }, { "id": "cat-040", @@ -638,7 +755,10 @@ "template": "Business Cat", "source": "https://imgflip.com/meme/Business-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." }, { "id": "cat-041", @@ -654,7 +774,10 @@ "template": "Sleepy Cat", "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." }, { "id": "cat-042", @@ -670,7 +793,10 @@ "template": "Grumpy Cat Not Amused", "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." }, { "id": "cat-043", @@ -686,7 +812,10 @@ "template": "Sad Cat Holding Dog", "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." }, { "id": "cat-044", @@ -702,7 +831,10 @@ "template": "Chemistry Cat", "source": "https://imgflip.com/meme/Chemistry-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2011, + "status": "classic", + "lore": "Peer reviews your snacks." }, { "id": "cat-045", @@ -718,7 +850,10 @@ "template": "white cat table", "source": "https://imgflip.com/meme/191245022/white-cat-table", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." }, { "id": "cat-046", @@ -734,7 +869,10 @@ "template": "Crying cat", "source": "https://imgflip.com/meme/42003405/Crying-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2014, + "status": "classic", + "lore": "Crying professionally for a decade." }, { "id": "cat-047", @@ -750,7 +888,10 @@ "template": "Excited Cat", "source": "https://imgflip.com/meme/Excited-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." }, { "id": "cat-048", @@ -766,7 +907,10 @@ "template": "Crying cat on phone", "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." }, { "id": "cat-049", @@ -782,7 +926,10 @@ "template": "Grumpy Cat", "source": "https://imgflip.com/meme/Grumpy-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." }, { "id": "cat-050", @@ -798,7 +945,10 @@ "template": "Banana cat", "source": "https://imgflip.com/meme/470680617/Banana-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." }, { "id": "cat-051", @@ -814,7 +964,10 @@ "template": "Sleepy Kitten", "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." }, { "id": "cat-052", @@ -830,7 +983,10 @@ "template": "Smiling Cat", "source": "https://imgflip.com/meme/Smiling-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." }, { "id": "cat-053", @@ -846,7 +1002,10 @@ "template": "I Should Buy A Boat Cat", "source": "https://imgflip.com/meme/I-Should-Buy-A-Boat-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "immortal", + "lore": "Boat questionable. Box undeniable." }, { "id": "cat-054", @@ -862,7 +1021,10 @@ "template": "Smudge the cat", "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." }, { "id": "cat-055", @@ -878,7 +1040,10 @@ "template": "Grumpy Cat Not Amused", "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." }, { "id": "cat-056", @@ -894,7 +1059,10 @@ "template": "Pop Cat", "source": "https://imgflip.com/meme/284017364/Pop-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." }, { "id": "cat-057", @@ -910,7 +1078,10 @@ "template": "Sleepy Cat", "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." }, { "id": "cat-058", @@ -926,7 +1097,10 @@ "template": "white cat table", "source": "https://imgflip.com/meme/191245022/white-cat-table", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." }, { "id": "cat-059", @@ -942,7 +1116,10 @@ "template": "I'm awake, but at what cost?", "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." }, { "id": "cat-060", @@ -958,6 +1135,4379 @@ "template": "Huh Cat", "source": "https://imgflip.com/meme/488919824/Huh-Cat", "kind": "remix", - "added": "2026-09-08" + "added": "2026-09-08", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." + }, + { + "id": "cat-061", + "title": "Read at 9:03 PM", + "top": "leaves me on read", + "bottom": "posts a story 4 minutes later", + "mood": "relatable", + "tags": [ + "texting", + "friends" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-062", + "title": "The group chat lore", + "top": "everyone laughing at messages", + "bottom": "from while I was asleep", + "mood": "relatable", + "tags": [ + "friends", + "confused" + ], + "image": "/assets/memes/8338y8.jpg", + "template": "Huh Cat", + "source": "https://imgflip.com/meme/488919824/Huh-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." + }, + { + "id": "cat-063", + "title": "Inbox zero speedrun", + "top": "0 unread emails", + "bottom": "147 unread life decisions", + "mood": "relatable", + "tags": [ + "work", + "email" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-064", + "title": "The Sunday reset", + "top": "new week, new me", + "bottom": "same me, new snacks", + "mood": "relatable", + "tags": [ + "weekend", + "snacks" + ], + "image": "/assets/memes/p0a19.jpg", + "template": "Crying cat", + "source": "https://imgflip.com/meme/42003405/Crying-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2014, + "status": "classic", + "lore": "Crying professionally for a decade." + }, + { + "id": "cat-065", + "title": "Performance review face", + "top": "“we value work-life balance”", + "bottom": "sends 11pm slack anyway", + "mood": "relatable", + "tags": [ + "work", + "slack" + ], + "image": "/assets/memes/3u04h5.jpg", + "template": "sad thumbs up cat", + "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." + }, + { + "id": "cat-066", + "title": "The rent conversation", + "top": "landlord: just a small increase", + "bottom": "my salary: absolutely not", + "mood": "relatable", + "tags": [ + "money", + "home" + ], + "image": "/assets/memes/35v1lq.jpg", + "template": "white cat table", + "source": "https://imgflip.com/meme/191245022/white-cat-table", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." + }, + { + "id": "cat-067", + "title": "Trust the science", + "top": "studies show naps improve", + "bottom": "literally everything", + "mood": "relatable", + "tags": [ + "science", + "nap" + ], + "image": "/assets/memes/1bif.jpg", + "template": "Chemistry Cat", + "source": "https://imgflip.com/meme/Chemistry-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "classic", + "lore": "Peer reviews your snacks." + }, + { + "id": "cat-068", + "title": "HR has been notified", + "top": "me leaving the meeting", + "bottom": "I was never invited to", + "mood": "chaos", + "tags": [ + "work", + "trouble" + ], + "image": "/assets/memes/24uu.jpg", + "template": "Cool Cat Stroll", + "source": "https://imgflip.com/meme/Cool-Cat-Stroll", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." + }, + { + "id": "cat-069", + "title": "The doorbell cinematic universe", + "top": "package for you", + "bottom": "fight or flight: activated", + "mood": "chaos", + "tags": [ + "surprised", + "door" + ], + "image": "/assets/memes/2hgfw.jpg", + "template": "Scared Cat", + "source": "https://imgflip.com/meme/Scared-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Startled since 2019." + }, + { + "id": "cat-070", + "title": "Emotional support fruit", + "top": "peeling good today", + "bottom": "potassium levels: unhinged", + "mood": "chaos", + "tags": [ + "banana", + "feelings" + ], + "image": "/assets/memes/7s8bhl.jpg", + "template": "Banana cat", + "source": "https://imgflip.com/meme/470680617/Banana-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." + }, + { + "id": "cat-071", + "title": "The 2am duet", + "top": "smoke detector: chirp", + "bottom": "me: chirp back, but louder", + "mood": "chaos", + "tags": [ + "night", + "drama" + ], + "image": "/assets/memes/4p3h78.jpg", + "template": "Pop Cat", + "source": "https://imgflip.com/meme/284017364/Pop-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." + }, + { + "id": "cat-072", + "title": "Math again?", + "top": "quick mafs", + "bottom": "slow regrets", + "mood": "chaos", + "tags": [ + "math", + "confused" + ], + "image": "/assets/memes/8338y8.jpg", + "template": "Huh Cat", + "source": "https://imgflip.com/meme/488919824/Huh-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." + }, + { + "id": "cat-073", + "title": "DJ Fridge Hum", + "top": "headlining my kitchen", + "bottom": "one night only (every night)", + "mood": "chaos", + "tags": [ + "music", + "night" + ], + "image": "/assets/memes/4l4wwy.jpg", + "template": "vibing cat", + "source": "https://imgflip.com/meme/277365922/vibing-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Headphones on. World off." + }, + { + "id": "cat-074", + "title": "The five-year plan", + "top": "step 1: get box", + "bottom": "step 2: sit in box. done.", + "mood": "chaos", + "tags": [ + "box", + "classic" + ], + "image": "/assets/memes/tau4.jpg", + "template": "I Should Buy A Boat Cat", + "source": "https://imgflip.com/meme/I-Should-Buy-A-Boat-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Boat questionable. Box undeniable." + }, + { + "id": "cat-075", + "title": "The dinner audit", + "top": "you made pasta", + "bottom": "where is MY pasta", + "mood": "snacks", + "tags": [ + "dinner", + "drama" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-076", + "title": "Crunch-based communication", + "top": "if I crunch louder", + "bottom": "second breakfast appears", + "mood": "snacks", + "tags": [ + "hungry", + "breakfast" + ], + "image": "/assets/memes/4p3h78.jpg", + "template": "Pop Cat", + "source": "https://imgflip.com/meme/284017364/Pop-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." + }, + { + "id": "cat-077", + "title": "Peer-reviewed snackology", + "top": "hypothesis: fridge snacks", + "bottom": "taste better at midnight", + "mood": "snacks", + "tags": [ + "science", + "night" + ], + "image": "/assets/memes/1bif.jpg", + "template": "Chemistry Cat", + "source": "https://imgflip.com/meme/Chemistry-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "classic", + "lore": "Peer reviews your snacks." + }, + { + "id": "cat-078", + "title": "Snack market update", + "top": "treat stocks rising", + "bottom": "analysts recommend screaming", + "mood": "snacks", + "tags": [ + "news", + "treat" + ], + "image": "/assets/memes/bgsvd.jpg", + "template": "Cat newspaper", + "source": "https://imgflip.com/meme/19259689/Cat-newspaper", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Reports the news. Is the news." + }, + { + "id": "cat-079", + "title": "Lunch and learn", + "top": "today’s lesson:", + "bottom": "your sandwich is communal", + "mood": "snacks", + "tags": [ + "work", + "lunch" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-080", + "title": "The dessert defense", + "top": "I’m full", + "bottom": "(of dessert-adjacent room)", + "mood": "snacks", + "tags": [ + "dinner", + "dessert" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-081", + "title": "Smoothie era", + "top": "blended my feelings", + "bottom": "still lumpy", + "mood": "snacks", + "tags": [ + "banana", + "feelings" + ], + "image": "/assets/memes/7s8bhl.jpg", + "template": "Banana cat", + "source": "https://imgflip.com/meme/470680617/Banana-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." + }, + { + "id": "cat-082", + "title": "The gym membership", + "top": "paying monthly", + "bottom": "to be judged by a cat", + "mood": "judgment", + "tags": [ + "gym", + "grumpy" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-083", + "title": "The code review", + "top": "“just one small comment”", + "bottom": "47 comments later", + "mood": "judgment", + "tags": [ + "work", + "code" + ], + "image": "/assets/memes/35v1lq.jpg", + "template": "white cat table", + "source": "https://imgflip.com/meme/191245022/white-cat-table", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." + }, + { + "id": "cat-084", + "title": "The salad verdict", + "top": "you ordered salad", + "bottom": "in THIS economy of flavor?", + "mood": "judgment", + "tags": [ + "salad", + "dinner" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-085", + "title": "The morning people", + "top": "“rise and grind”", + "bottom": "I rise and judge", + "mood": "judgment", + "tags": [ + "morning", + "grumpy" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-086", + "title": "The neighbor report", + "top": "mows lawn at 7am", + "bottom": "noted. forever.", + "mood": "judgment", + "tags": [ + "neighbors", + "grudge" + ], + "image": "/assets/memes/24uu.jpg", + "template": "Cool Cat Stroll", + "source": "https://imgflip.com/meme/Cool-Cat-Stroll", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." + }, + { + "id": "cat-087", + "title": "The software update", + "top": "“a quick restart”", + "bottom": "45 minutes of betrayal", + "mood": "judgment", + "tags": [ + "tech", + "phone" + ], + "image": "/assets/memes/3u04h5.jpg", + "template": "sad thumbs up cat", + "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." + }, + { + "id": "cat-088", + "title": "The photo shoot", + "top": "say cheese", + "bottom": "I said what I said", + "mood": "judgment", + "tags": [ + "photo", + "attitude" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-089", + "title": "The elevator nap", + "top": "blinked once", + "bottom": "missed my floor", + "mood": "sleepy", + "tags": [ + "nap", + "tired" + ], + "image": "/assets/memes/11wis1.jpg", + "template": "Sleepy Kitten", + "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." + }, + { + "id": "cat-090", + "title": "The ambitious weekend", + "top": "so many plans", + "bottom": "so horizontal", + "mood": "sleepy", + "tags": [ + "weekend", + "nap" + ], + "image": "/assets/memes/lp2ba.jpg", + "template": "Sleepy Cat", + "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." + }, + { + "id": "cat-091", + "title": "The 6am club", + "top": "member since never", + "bottom": "meetings attended: against will", + "mood": "sleepy", + "tags": [ + "morning", + "work" + ], + "image": "/assets/memes/3e5sun.jpg", + "template": "I'm awake, but at what cost?", + "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." + }, + { + "id": "cat-092", + "title": "Do not perceive", + "top": "bedtime: 10pm", + "bottom": "doomscroll: until 3am", + "mood": "sleepy", + "tags": [ + "phone", + "night" + ], + "image": "/assets/memes/3vnurc.jpg", + "template": "Crying cat on phone", + "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." + }, + { + "id": "cat-093", + "title": "The nap negotiation", + "top": "just 20 minutes", + "bottom": "wakes up employed-ish", + "mood": "sleepy", + "tags": [ + "nap", + "work" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-094", + "title": "Low power mode", + "top": "battery: 2%", + "bottom": "vibe: airplane mode", + "mood": "sleepy", + "tags": [ + "battery", + "tired" + ], + "image": "/assets/memes/11wis1.jpg", + "template": "Sleepy Kitten", + "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." + }, + { + "id": "cat-095", + "title": "The good morning text", + "top": "no reason", + "bottom": "just thought you’d like this cat", + "mood": "wholesome", + "tags": [ + "friends", + "love" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-096", + "title": "The buddy system", + "top": "bad day?", + "bottom": "hug deployment initiated", + "mood": "wholesome", + "tags": [ + "support", + "hug" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-097", + "title": "The airport pickup", + "top": "flight landed", + "bottom": "emotional support human en route", + "mood": "wholesome", + "tags": [ + "friends", + "excited" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-098", + "title": "The compliment heist", + "top": "stealing this moment", + "bottom": "to say you’re doing great", + "mood": "wholesome", + "tags": [ + "kindness", + "win" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-099", + "title": "The soup protocol", + "top": "you sound sick", + "bottom": "soup is already simmering", + "mood": "wholesome", + "tags": [ + "care", + "love" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-100", + "title": "The Friday feeling", + "top": "you survived the week", + "bottom": "gold star. nap next.", + "mood": "wholesome", + "tags": [ + "weekend", + "win" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-101", + "title": "The read receipt spiral", + "top": "seen 2:14pm", + "bottom": "reply drafted. deleted. drafted.", + "mood": "relatable", + "tags": [ + "texting", + "anxiety" + ], + "image": "/assets/memes/3vnurc.jpg", + "template": "Crying cat on phone", + "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." + }, + { + "id": "cat-102", + "title": "The tutorial skipper", + "top": "skips tutorial", + "bottom": "immediately needs tutorial", + "mood": "relatable", + "tags": [ + "gaming", + "confused" + ], + "image": "/assets/memes/8338y8.jpg", + "template": "Huh Cat", + "source": "https://imgflip.com/meme/488919824/Huh-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." + }, + { + "id": "cat-103", + "title": "The calendar invite", + "top": "“quick sync?”", + "bottom": "90 minutes. 14 people. no agenda.", + "mood": "relatable", + "tags": [ + "work", + "meeting" + ], + "image": "/assets/memes/3u04h5.jpg", + "template": "sad thumbs up cat", + "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." + }, + { + "id": "cat-104", + "title": "The subscription audit", + "top": "$4.99 here, $9.99 there", + "bottom": "net worth: streaming services", + "mood": "relatable", + "tags": [ + "money", + "tech" + ], + "image": "/assets/memes/35v1lq.jpg", + "template": "white cat table", + "source": "https://imgflip.com/meme/191245022/white-cat-table", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." + }, + { + "id": "cat-105", + "title": "The adulting formula", + "top": "taxes + laundry +", + "bottom": "existential dread = tuesday", + "mood": "relatable", + "tags": [ + "science", + "adulting" + ], + "image": "/assets/memes/1bif.jpg", + "template": "Chemistry Cat", + "source": "https://imgflip.com/meme/Chemistry-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "classic", + "lore": "Peer reviews your snacks." + }, + { + "id": "cat-106", + "title": "The haircut gamble", + "top": "“just a trim”", + "bottom": "new identity unlocked", + "mood": "relatable", + "tags": [ + "hair", + "regret" + ], + "image": "/assets/memes/p0a19.jpg", + "template": "Crying cat", + "source": "https://imgflip.com/meme/42003405/Crying-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2014, + "status": "classic", + "lore": "Crying professionally for a decade." + }, + { + "id": "cat-107", + "title": "The out-of-office", + "top": "set OOO. feel peace.", + "bottom": "phone buzzes anyway", + "mood": "relatable", + "tags": [ + "work", + "vacation" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-108", + "title": "The alarm snooze treaty", + "top": "5 more minutes", + "bottom": "signed every single morning", + "mood": "relatable", + "tags": [ + "morning", + "sleep" + ], + "image": "/assets/memes/3e5sun.jpg", + "template": "I'm awake, but at what cost?", + "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." + }, + { + "id": "cat-109", + "title": "The hallway strut", + "top": "heard my song", + "bottom": "in the grocery store", + "mood": "chaos", + "tags": [ + "music", + "strut" + ], + "image": "/assets/memes/24uu.jpg", + "template": "Cool Cat Stroll", + "source": "https://imgflip.com/meme/Cool-Cat-Stroll", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." + }, + { + "id": "cat-110", + "title": "The microwave beep", + "top": "3… 2… 1…", + "bottom": "JUMPSCARE AT MIDNIGHT", + "mood": "chaos", + "tags": [ + "night", + "surprised" + ], + "image": "/assets/memes/2hgfw.jpg", + "template": "Scared Cat", + "source": "https://imgflip.com/meme/Scared-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Startled since 2019." + }, + { + "id": "cat-111", + "title": "The potassium prophecy", + "top": "dreams in yellow", + "bottom": "wakes up curved", + "mood": "chaos", + "tags": [ + "banana", + "dreams" + ], + "image": "/assets/memes/7s8bhl.jpg", + "template": "Banana cat", + "source": "https://imgflip.com/meme/470680617/Banana-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." + }, + { + "id": "cat-112", + "title": "The keyboard warrior", + "top": "types one email", + "bottom": "mouth open the whole time", + "mood": "chaos", + "tags": [ + "work", + "drama" + ], + "image": "/assets/memes/4p3h78.jpg", + "template": "Pop Cat", + "source": "https://imgflip.com/meme/284017364/Pop-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." + }, + { + "id": "cat-113", + "title": "The elevator DJ", + "top": "no aux. no problem.", + "bottom": "humming the lobby anthem", + "mood": "chaos", + "tags": [ + "music", + "vibe" + ], + "image": "/assets/memes/4l4wwy.jpg", + "template": "vibing cat", + "source": "https://imgflip.com/meme/277365922/vibing-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Headphones on. World off." + }, + { + "id": "cat-114", + "title": "The laundry mountain", + "top": "will fold tomorrow", + "bottom": "tomorrow is a myth", + "mood": "chaos", + "tags": [ + "laundry", + "classic" + ], + "image": "/assets/memes/tau4.jpg", + "template": "I Should Buy A Boat Cat", + "source": "https://imgflip.com/meme/I-Should-Buy-A-Boat-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Boat questionable. Box undeniable." + }, + { + "id": "cat-115", + "title": "The plot twist", + "top": "read the same page", + "bottom": "three times. still lost.", + "mood": "chaos", + "tags": [ + "reading", + "confused" + ], + "image": "/assets/memes/8338y8.jpg", + "template": "Huh Cat", + "source": "https://imgflip.com/meme/488919824/Huh-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." + }, + { + "id": "cat-116", + "title": "The vet receipt", + "top": "“just a checkup”", + "bottom": "$400 and one betrayed stare", + "mood": "chaos", + "tags": [ + "vet", + "drama" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-117", + "title": "The fry tax", + "top": "you ordered fries", + "bottom": "I collect the toll", + "mood": "snacks", + "tags": [ + "fries", + "dinner" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-118", + "title": "The cereal summit", + "top": "emergency meeting:", + "bottom": "milk first or cereal first?", + "mood": "snacks", + "tags": [ + "breakfast", + "debate" + ], + "image": "/assets/memes/4p3h78.jpg", + "template": "Pop Cat", + "source": "https://imgflip.com/meme/284017364/Pop-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." + }, + { + "id": "cat-119", + "title": "The midnight experiment", + "top": "control group: asleep", + "bottom": "test group: tacos", + "mood": "snacks", + "tags": [ + "science", + "night" + ], + "image": "/assets/memes/1bif.jpg", + "template": "Chemistry Cat", + "source": "https://imgflip.com/meme/Chemistry-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "classic", + "lore": "Peer reviews your snacks." + }, + { + "id": "cat-120", + "title": "The crumb report", + "top": "kitchen floor: 40% snack", + "bottom": "cleanup crew dispatched (me)", + "mood": "snacks", + "tags": [ + "news", + "snack" + ], + "image": "/assets/memes/bgsvd.jpg", + "template": "Cat newspaper", + "source": "https://imgflip.com/meme/19259689/Cat-newspaper", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Reports the news. Is the news." + }, + { + "id": "cat-121", + "title": "The snack drawer memo", + "top": "per my last email:", + "bottom": "these are MY crackers", + "mood": "snacks", + "tags": [ + "work", + "snack" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-122", + "title": "The pizza crust connoisseur", + "top": "saves the best", + "bottom": "for last. then cries it’s over.", + "mood": "snacks", + "tags": [ + "pizza", + "dinner" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-123", + "title": "The banana standoff", + "top": "perfect yellow today", + "bottom": "brown spots tomorrow. pressure’s on.", + "mood": "snacks", + "tags": [ + "banana", + "pressure" + ], + "image": "/assets/memes/7s8bhl.jpg", + "template": "Banana cat", + "source": "https://imgflip.com/meme/470680617/Banana-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." + }, + { + "id": "cat-124", + "title": "The drive-thru regular", + "top": "“the usual?”", + "bottom": "fame tastes like fries", + "mood": "snacks", + "tags": [ + "fastfood", + "fame" + ], + "image": "/assets/memes/24uu.jpg", + "template": "Cool Cat Stroll", + "source": "https://imgflip.com/meme/Cool-Cat-Stroll", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." + }, + { + "id": "cat-125", + "title": "The reply-all", + "top": "replies all", + "bottom": "just to say “thanks”", + "mood": "judgment", + "tags": [ + "work", + "email" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-126", + "title": "The loud chewer", + "top": "crunch. crunch.", + "bottom": "court is now in session", + "mood": "judgment", + "tags": [ + "food", + "grumpy" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-127", + "title": "The group project", + "top": "“we should all contribute”", + "bottom": "— sent from my couch", + "mood": "judgment", + "tags": [ + "school", + "work" + ], + "image": "/assets/memes/35v1lq.jpg", + "template": "white cat table", + "source": "https://imgflip.com/meme/191245022/white-cat-table", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." + }, + { + "id": "cat-128", + "title": "The hotel breakfast", + "top": "“continental breakfast”", + "bottom": "this is toast with confidence", + "mood": "judgment", + "tags": [ + "travel", + "breakfast" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-129", + "title": "The parking spot", + "top": "waits 10 minutes", + "bottom": "for the close spot. walks anyway.", + "mood": "judgment", + "tags": [ + "driving", + "logic" + ], + "image": "/assets/memes/24uu.jpg", + "template": "Cool Cat Stroll", + "source": "https://imgflip.com/meme/Cool-Cat-Stroll", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." + }, + { + "id": "cat-130", + "title": "The terms and conditions", + "top": "read 0 of 47 pages", + "bottom": "agreed to everything", + "mood": "judgment", + "tags": [ + "tech", + "phone" + ], + "image": "/assets/memes/3u04h5.jpg", + "template": "sad thumbs up cat", + "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." + }, + { + "id": "cat-131", + "title": "The influencer morning", + "top": "“my 5am routine”", + "bottom": "posted at noon", + "mood": "judgment", + "tags": [ + "morning", + "social" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-132", + "title": "The spoiler", + "top": "“I won’t spoil it”", + "bottom": "spoils it immediately", + "mood": "judgment", + "tags": [ + "movies", + "grumpy" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-133", + "title": "The couch gravity", + "top": "sat down for a sec", + "bottom": "woke up in 2027", + "mood": "sleepy", + "tags": [ + "nap", + "couch" + ], + "image": "/assets/memes/11wis1.jpg", + "template": "Sleepy Kitten", + "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." + }, + { + "id": "cat-134", + "title": "The blanket burrito", + "top": "rolled tight", + "bottom": "do not unroll", + "mood": "sleepy", + "tags": [ + "nap", + "cozy" + ], + "image": "/assets/memes/lp2ba.jpg", + "template": "Sleepy Cat", + "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." + }, + { + "id": "cat-135", + "title": "The Monday loading screen", + "top": "buffering…", + "bottom": "please wait indefinitely", + "mood": "sleepy", + "tags": [ + "morning", + "monday" + ], + "image": "/assets/memes/3e5sun.jpg", + "template": "I'm awake, but at what cost?", + "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." + }, + { + "id": "cat-136", + "title": "The 1% club", + "top": "phone dying", + "bottom": "me: same, honestly", + "mood": "sleepy", + "tags": [ + "phone", + "battery" + ], + "image": "/assets/memes/3vnurc.jpg", + "template": "Crying cat on phone", + "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." + }, + { + "id": "cat-137", + "title": "The sunbeam schedule", + "top": "sunbeam moves one inch", + "bottom": "I move with it", + "mood": "sleepy", + "tags": [ + "sun", + "nap" + ], + "image": "/assets/memes/11wis1.jpg", + "template": "Sleepy Kitten", + "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." + }, + { + "id": "cat-138", + "title": "The meeting nap", + "top": "camera off", + "bottom": "soul off too", + "mood": "sleepy", + "tags": [ + "work", + "meeting" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-139", + "title": "The yawn contagion", + "top": "one yawn", + "bottom": "whole office infected", + "mood": "sleepy", + "tags": [ + "work", + "tired" + ], + "image": "/assets/memes/lp2ba.jpg", + "template": "Sleepy Cat", + "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." + }, + { + "id": "cat-140", + "title": "The daylight savings", + "top": "lost one hour", + "bottom": "will mourn for weeks", + "mood": "sleepy", + "tags": [ + "time", + "tired" + ], + "image": "/assets/memes/3e5sun.jpg", + "template": "I'm awake, but at what cost?", + "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." + }, + { + "id": "cat-141", + "title": "The bedtime story", + "top": "one more video", + "bottom": "47 videos later", + "mood": "sleepy", + "tags": [ + "phone", + "night" + ], + "image": "/assets/memes/3vnurc.jpg", + "template": "Crying cat on phone", + "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." + }, + { + "id": "cat-142", + "title": "The sidewalk flower", + "top": "picked for you", + "bottom": "slightly crushed. full of love.", + "mood": "wholesome", + "tags": [ + "kindness", + "love" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-143", + "title": "The rain rescue", + "top": "shares umbrella", + "bottom": "gets wet. worth it.", + "mood": "wholesome", + "tags": [ + "kindness", + "rain" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-144", + "title": "The good news sprint", + "top": "heard your news", + "bottom": "already celebrating", + "mood": "wholesome", + "tags": [ + "friends", + "win" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-145", + "title": "The tiny victory", + "top": "made the bed", + "bottom": "unstoppable now", + "mood": "wholesome", + "tags": [ + "win", + "morning" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-146", + "title": "The playlist gift", + "top": "made you 47 songs", + "bottom": "all say “you got this”", + "mood": "wholesome", + "tags": [ + "music", + "friends" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-147", + "title": "The couch counselor", + "top": "no advice. just snacks.", + "bottom": "plus premium listening", + "mood": "wholesome", + "tags": [ + "support", + "snacks" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-148", + "title": "The reunion zoomies", + "top": "haven’t seen you in DAYS", + "bottom": "(it was 3 hours)", + "mood": "wholesome", + "tags": [ + "friends", + "love" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-149", + "title": "The window sunrise", + "top": "saved you a seat", + "bottom": "best view at 7am", + "mood": "wholesome", + "tags": [ + "morning", + "cozy" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-150", + "title": "The thank-you note", + "top": "you helped me once", + "bottom": "now I tell everyone", + "mood": "wholesome", + "tags": [ + "gratitude", + "love" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-151", + "title": "The typing Olympics", + "top": "drafting the perfect reply", + "bottom": "sends “ok”", + "mood": "relatable", + "tags": [ + "texting", + "friends" + ], + "image": "/assets/memes/3vnurc.jpg", + "template": "Crying cat on phone", + "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." + }, + { + "id": "cat-152", + "title": "The meeting math", + "top": "“quick question”", + "bottom": "27 follow-ups later", + "mood": "relatable", + "tags": [ + "work", + "meeting" + ], + "image": "/assets/memes/8338y8.jpg", + "template": "Huh Cat", + "source": "https://imgflip.com/meme/488919824/Huh-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." + }, + { + "id": "cat-153", + "title": "The wifi funeral", + "top": "one bar. one dream.", + "bottom": "buffering my personality", + "mood": "relatable", + "tags": [ + "tech", + "phone" + ], + "image": "/assets/memes/3u04h5.jpg", + "template": "sad thumbs up cat", + "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." + }, + { + "id": "cat-154", + "title": "The plant parent", + "top": "buys a fern", + "bottom": "funeral by friday", + "mood": "relatable", + "tags": [ + "plants", + "fail" + ], + "image": "/assets/memes/35v1lq.jpg", + "template": "white cat table", + "source": "https://imgflip.com/meme/191245022/white-cat-table", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." + }, + { + "id": "cat-155", + "title": "The group chat economist", + "top": "venmo request for $2.14", + "bottom": "plus emotional damages", + "mood": "relatable", + "tags": [ + "money", + "friends" + ], + "image": "/assets/memes/1bif.jpg", + "template": "Chemistry Cat", + "source": "https://imgflip.com/meme/Chemistry-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "classic", + "lore": "Peer reviews your snacks." + }, + { + "id": "cat-156", + "title": "The password saga", + "top": "incorrect password", + "bottom": "it was the same password", + "mood": "relatable", + "tags": [ + "tech", + "fail" + ], + "image": "/assets/memes/p0a19.jpg", + "template": "Crying cat", + "source": "https://imgflip.com/meme/42003405/Crying-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2014, + "status": "classic", + "lore": "Crying professionally for a decade." + }, + { + "id": "cat-157", + "title": "The lunch thief suspect", + "top": "someone ate my yogurt", + "bottom": "trust exercises cancelled", + "mood": "relatable", + "tags": [ + "work", + "lunch" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-158", + "title": "The snooze loop", + "top": "alarm at 7. 7:09. 7:18.", + "bottom": "up at 7:47 somehow", + "mood": "relatable", + "tags": [ + "morning", + "sleep" + ], + "image": "/assets/memes/3e5sun.jpg", + "template": "I'm awake, but at what cost?", + "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." + }, + { + "id": "cat-159", + "title": "The compliment dodge", + "top": "“you look nice today”", + "bottom": "this? old thing. thanks though", + "mood": "relatable", + "tags": [ + "social", + "friends" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-160", + "title": "The friday save", + "top": "email at 4:59pm", + "bottom": "see you never", + "mood": "relatable", + "tags": [ + "work", + "weekend" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-161", + "title": "The self-checkout", + "top": "unexpected item", + "bottom": "it’s me. i’m the item.", + "mood": "relatable", + "tags": [ + "shopping", + "fail" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-162", + "title": "The autocorrect apology", + "top": "meant “love you”", + "bottom": "sent “live you”. standing by it.", + "mood": "relatable", + "tags": [ + "texting", + "fail" + ], + "image": "/assets/memes/4p3h78.jpg", + "template": "Pop Cat", + "source": "https://imgflip.com/meme/284017364/Pop-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." + }, + { + "id": "cat-163", + "title": "The small talk", + "top": "“crazy weather, huh”", + "bottom": "performed flawlessly. retiring.", + "mood": "relatable", + "tags": [ + "social", + "win" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-164", + "title": "The ‘k’ text", + "top": "replies “k”", + "bottom": "war has begun", + "mood": "relatable", + "tags": [ + "texting", + "drama" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-165", + "title": "The horoscope", + "top": "“today you’ll thrive”", + "bottom": "naps aggressively to comply", + "mood": "relatable", + "tags": [ + "nap", + "stars" + ], + "image": "/assets/memes/bgsvd.jpg", + "template": "Cat newspaper", + "source": "https://imgflip.com/meme/19259689/Cat-newspaper", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Reports the news. Is the news." + }, + { + "id": "cat-166", + "title": "The monday outfit", + "top": "dressed for success", + "bottom": "success is sweatpants", + "mood": "relatable", + "tags": [ + "work", + "monday" + ], + "image": "/assets/memes/lp2ba.jpg", + "template": "Sleepy Cat", + "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." + }, + { + "id": "cat-167", + "title": "The pep talk", + "top": "“you got this”", + "bottom": "source: me, crying too", + "mood": "relatable", + "tags": [ + "support", + "friends" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-168", + "title": "The cartwheel", + "top": "nobody: ...", + "bottom": "absolutely nobody: ...", + "mood": "chaos", + "tags": [ + "random", + "classic" + ], + "image": "/assets/memes/24uu.jpg", + "template": "Cool Cat Stroll", + "source": "https://imgflip.com/meme/Cool-Cat-Stroll", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." + }, + { + "id": "cat-169", + "title": "The plot armor", + "top": "trips on nothing", + "bottom": "in front of everyone", + "mood": "chaos", + "tags": [ + "fail", + "public" + ], + "image": "/assets/memes/2hgfw.jpg", + "template": "Scared Cat", + "source": "https://imgflip.com/meme/Scared-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Startled since 2019." + }, + { + "id": "cat-170", + "title": "The banana phone", + "top": "ring ring", + "bottom": "potassium calling", + "mood": "chaos", + "tags": [ + "banana", + "phone" + ], + "image": "/assets/memes/7s8bhl.jpg", + "template": "Banana cat", + "source": "https://imgflip.com/meme/470680617/Banana-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." + }, + { + "id": "cat-171", + "title": "The echo", + "top": "says “hello?” in tunnel", + "bottom": "tunnel says it better", + "mood": "chaos", + "tags": [ + "tunnel", + "drama" + ], + "image": "/assets/memes/4p3h78.jpg", + "template": "Pop Cat", + "source": "https://imgflip.com/meme/284017364/Pop-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." + }, + { + "id": "cat-172", + "title": "The hold music", + "top": "on hold 40 minutes", + "bottom": "vibing unironically now", + "mood": "chaos", + "tags": [ + "music", + "phone" + ], + "image": "/assets/memes/4l4wwy.jpg", + "template": "vibing cat", + "source": "https://imgflip.com/meme/277365922/vibing-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Headphones on. World off." + }, + { + "id": "cat-173", + "title": "The junk drawer", + "top": "contains: 3 batteries", + "bottom": "0 working pens. 1 mystery key.", + "mood": "chaos", + "tags": [ + "home", + "mystery" + ], + "image": "/assets/memes/tau4.jpg", + "template": "I Should Buy A Boat Cat", + "source": "https://imgflip.com/meme/I-Should-Buy-A-Boat-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Boat questionable. Box undeniable." + }, + { + "id": "cat-174", + "title": "The captcha", + "top": "select all traffic lights", + "bottom": "fails. am robot now.", + "mood": "chaos", + "tags": [ + "tech", + "fail" + ], + "image": "/assets/memes/8338y8.jpg", + "template": "Huh Cat", + "source": "https://imgflip.com/meme/488919824/Huh-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2024, + "status": "alive", + "lore": "Confused in high definition." + }, + { + "id": "cat-175", + "title": "The dramatic exit", + "top": "storms out", + "bottom": "forgets keys. returns.", + "mood": "chaos", + "tags": [ + "drama", + "fail" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-176", + "title": "The elevator performance", + "top": "alone in elevator", + "bottom": "grammy-worthy mirror check", + "mood": "chaos", + "tags": [ + "music", + "alone" + ], + "image": "/assets/memes/11wis1.jpg", + "template": "Sleepy Kitten", + "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." + }, + { + "id": "cat-177", + "title": "The reply guy", + "top": "first to reply-all", + "bottom": "“adding myself to this thread”", + "mood": "chaos", + "tags": [ + "work", + "email" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-178", + "title": "The update", + "top": "“update tonight?”", + "bottom": "phone is now a brick", + "mood": "chaos", + "tags": [ + "tech", + "phone" + ], + "image": "/assets/memes/3u04h5.jpg", + "template": "sad thumbs up cat", + "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." + }, + { + "id": "cat-179", + "title": "The group photo", + "top": "“one more!”", + "bottom": "47 photos later", + "mood": "chaos", + "tags": [ + "photo", + "friends" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-180", + "title": "The wave back", + "top": "waves at someone", + "bottom": "behind me. always behind me.", + "mood": "chaos", + "tags": [ + "social", + "fail" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-181", + "title": "The plot hole", + "top": "“it was all a dream”", + "bottom": "worst. trope. ever.", + "mood": "chaos", + "tags": [ + "movies", + "opinion" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-182", + "title": "The jump scare", + "top": "own reflection", + "bottom": "in a dark hallway", + "mood": "chaos", + "tags": [ + "night", + "surprised" + ], + "image": "/assets/memes/2hgfw.jpg", + "template": "Scared Cat", + "source": "https://imgflip.com/meme/Scared-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Startled since 2019." + }, + { + "id": "cat-183", + "title": "The breaking news", + "top": "local cat naps", + "bottom": "through entire eclipse", + "mood": "chaos", + "tags": [ + "news", + "nap" + ], + "image": "/assets/memes/bgsvd.jpg", + "template": "Cat newspaper", + "source": "https://imgflip.com/meme/19259689/Cat-newspaper", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Reports the news. Is the news." + }, + { + "id": "cat-184", + "title": "The hype man", + "top": "friend got a haircut", + "bottom": "screaming like it’s a wedding", + "mood": "chaos", + "tags": [ + "friends", + "hair" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-185", + "title": "The chip hierarchy", + "top": "big chip for me", + "bottom": "broken one for… also me", + "mood": "snacks", + "tags": [ + "chips", + "snack" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-186", + "title": "The popcorn protocol", + "top": "movie starts in 5", + "bottom": "already half the bowl", + "mood": "snacks", + "tags": [ + "movies", + "popcorn" + ], + "image": "/assets/memes/4p3h78.jpg", + "template": "Pop Cat", + "source": "https://imgflip.com/meme/284017364/Pop-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Two frames. Infinite volume." + }, + { + "id": "cat-187", + "title": "The toast theorem", + "top": "butter side always down", + "bottom": "peer reviewed. devastating.", + "mood": "snacks", + "tags": [ + "science", + "breakfast" + ], + "image": "/assets/memes/1bif.jpg", + "template": "Chemistry Cat", + "source": "https://imgflip.com/meme/Chemistry-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "classic", + "lore": "Peer reviews your snacks." + }, + { + "id": "cat-188", + "title": "The menu investigation", + "top": "reads entire menu", + "bottom": "orders the usual", + "mood": "snacks", + "tags": [ + "dinner", + "habit" + ], + "image": "/assets/memes/bgsvd.jpg", + "template": "Cat newspaper", + "source": "https://imgflip.com/meme/19259689/Cat-newspaper", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Reports the news. Is the news." + }, + { + "id": "cat-189", + "title": "The office cake", + "top": "“help yourself!”", + "bottom": "takes the corner piece. power move.", + "mood": "snacks", + "tags": [ + "work", + "cake" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-190", + "title": "The cookie swap", + "top": "one cookie each", + "bottom": "mine had more chips. justice.", + "mood": "snacks", + "tags": [ + "cookies", + "justice" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-191", + "title": "The smoothie regret", + "top": "added spinach", + "bottom": "tastes like lawn. healthy lawn.", + "mood": "snacks", + "tags": [ + "health", + "regret" + ], + "image": "/assets/memes/7s8bhl.jpg", + "template": "Banana cat", + "source": "https://imgflip.com/meme/470680617/Banana-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Potassium with a fanbase." + }, + { + "id": "cat-192", + "title": "The taco balance", + "top": "eating tacos gracefully", + "bottom": "a myth. delicious myth.", + "mood": "snacks", + "tags": [ + "tacos", + "dinner" + ], + "image": "/assets/memes/24uu.jpg", + "template": "Cool Cat Stroll", + "source": "https://imgflip.com/meme/Cool-Cat-Stroll", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." + }, + { + "id": "cat-193", + "title": "The diet starts", + "top": "monday. definitely monday.", + "bottom": "orders fries. it’s sunday.", + "mood": "snacks", + "tags": [ + "diet", + "fries" + ], + "image": "/assets/memes/3u04h5.jpg", + "template": "sad thumbs up cat", + "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." + }, + { + "id": "cat-194", + "title": "The waiter watch", + "top": "sees waiter coming", + "bottom": "suddenly mid-sentence", + "mood": "snacks", + "tags": [ + "dinner", + "social" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-195", + "title": "The delivery tracker", + "top": "driver is 2 stops away", + "bottom": "staring out window like a dog", + "mood": "snacks", + "tags": [ + "delivery", + "hungry" + ], + "image": "/assets/memes/3vnurc.jpg", + "template": "Crying cat on phone", + "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." + }, + { + "id": "cat-196", + "title": "The secret stash", + "top": "hiding chocolate", + "bottom": "from myself. forgot where.", + "mood": "snacks", + "tags": [ + "chocolate", + "fail" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-197", + "title": "The brunch rush", + "top": "table for brunch", + "bottom": "45 min wait. worth it.", + "mood": "snacks", + "tags": [ + "brunch", + "weekend" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-198", + "title": "The recipe review", + "top": "“i swapped everything”", + "bottom": "one star. recipe’s fault.", + "mood": "snacks", + "tags": [ + "cooking", + "review" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-199", + "title": "The fancy dinner", + "top": "tiny portions", + "bottom": "stops for tacos after", + "mood": "snacks", + "tags": [ + "dinner", + "tacos" + ], + "image": "/assets/memes/35v1lq.jpg", + "template": "white cat table", + "source": "https://imgflip.com/meme/191245022/white-cat-table", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." + }, + { + "id": "cat-200", + "title": "The ice cream", + "top": "“just one scoop”", + "bottom": "the tub is gone", + "mood": "snacks", + "tags": [ + "dessert", + "fail" + ], + "image": "/assets/memes/p0a19.jpg", + "template": "Crying cat", + "source": "https://imgflip.com/meme/42003405/Crying-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2014, + "status": "classic", + "lore": "Crying professionally for a decade." + }, + { + "id": "cat-201", + "title": "The midnight cereal", + "top": "bowl at 1am", + "bottom": "hits different. scientifically.", + "mood": "snacks", + "tags": [ + "cereal", + "night" + ], + "image": "/assets/memes/lp2ba.jpg", + "template": "Sleepy Cat", + "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." + }, + { + "id": "cat-202", + "title": "The loud typer", + "top": "mechanical keyboard", + "bottom": "in a quiet café", + "mood": "judgment", + "tags": [ + "noise", + "laptop" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-203", + "title": "The spoiler, part II", + "top": "“you finished it, right?”", + "bottom": "hadn’t. have now. thanks.", + "mood": "judgment", + "tags": [ + "movies", + "spoiler" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-204", + "title": "The meeting monologue", + "top": "“i’ll keep it brief”", + "bottom": "narrates entire quarter", + "mood": "judgment", + "tags": [ + "work", + "meeting" + ], + "image": "/assets/memes/35v1lq.jpg", + "template": "white cat table", + "source": "https://imgflip.com/meme/191245022/white-cat-table", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." + }, + { + "id": "cat-205", + "title": "The restaurant critic", + "top": "“the vibe is off”", + "bottom": "it’s a tuesday, kevin", + "mood": "judgment", + "tags": [ + "dinner", + "attitude" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-206", + "title": "The slow walkers", + "top": "group of five", + "bottom": "entire sidewalk. all of it.", + "mood": "judgment", + "tags": [ + "walking", + "city" + ], + "image": "/assets/memes/24uu.jpg", + "template": "Cool Cat Stroll", + "source": "https://imgflip.com/meme/Cool-Cat-Stroll", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "alive", + "lore": "Struts like rent is due." + }, + { + "id": "cat-207", + "title": "The software license", + "top": "“accept all cookies”", + "bottom": "sells soul for 10% off", + "mood": "judgment", + "tags": [ + "tech", + "cookies" + ], + "image": "/assets/memes/3u04h5.jpg", + "template": "sad thumbs up cat", + "source": "https://imgflip.com/meme/231792809/sad-thumbs-up-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Thumbs up. Spirits down." + }, + { + "id": "cat-208", + "title": "The gym selfie", + "top": "posts leg day", + "bottom": "skipped legs", + "mood": "judgment", + "tags": [ + "gym", + "social" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-209", + "title": "The phone call", + "top": "calls instead of texting", + "bottom": "a bold, hostile choice", + "mood": "judgment", + "tags": [ + "phone", + "social" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-210", + "title": "The desk neighbor", + "top": "eats tuna at desk", + "bottom": "hr has been notified", + "mood": "judgment", + "tags": [ + "work", + "food" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-211", + "title": "The early bird", + "top": "“morning people win”", + "bottom": "win what. exactly.", + "mood": "judgment", + "tags": [ + "morning", + "debate" + ], + "image": "/assets/memes/3e5sun.jpg", + "template": "I'm awake, but at what cost?", + "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." + }, + { + "id": "cat-212", + "title": "The horror movie", + "top": "“don’t go in there!”", + "bottom": "goes in there", + "mood": "judgment", + "tags": [ + "movies", + "logic" + ], + "image": "/assets/memes/2hgfw.jpg", + "template": "Scared Cat", + "source": "https://imgflip.com/meme/Scared-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Startled since 2019." + }, + { + "id": "cat-213", + "title": "The opinion column", + "top": "“unpopular opinion:”", + "bottom": "extremely popular opinion", + "mood": "judgment", + "tags": [ + "opinion", + "social" + ], + "image": "/assets/memes/bgsvd.jpg", + "template": "Cat newspaper", + "source": "https://imgflip.com/meme/19259689/Cat-newspaper", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Reports the news. Is the news." + }, + { + "id": "cat-214", + "title": "The playlist thief", + "top": "steals my playlist", + "bottom": "gets more likes", + "mood": "judgment", + "tags": [ + "music", + "betrayal" + ], + "image": "/assets/memes/4l4wwy.jpg", + "template": "vibing cat", + "source": "https://imgflip.com/meme/277365922/vibing-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2020, + "status": "classic", + "lore": "Headphones on. World off." + }, + { + "id": "cat-215", + "title": "The compliment fish", + "top": "“new haircut, don’t care”", + "bottom": "posted 4 angles", + "mood": "judgment", + "tags": [ + "hair", + "social" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-216", + "title": "The over-packer", + "top": "weekend trip", + "bottom": "3 suitcases. 2 outfits.", + "mood": "judgment", + "tags": [ + "travel", + "packing" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-217", + "title": "The baby voice", + "top": "talks to dog like baby", + "bottom": "in public. no shame.", + "mood": "judgment", + "tags": [ + "dogs", + "public" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-218", + "title": "The advice", + "top": "“just be yourself”", + "bottom": "myself is the problem", + "mood": "judgment", + "tags": [ + "advice", + "truth" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-219", + "title": "The blink", + "top": "rests eyes for a sec", + "bottom": "2 hours. drool.", + "mood": "sleepy", + "tags": [ + "nap", + "oops" + ], + "image": "/assets/memes/11wis1.jpg", + "template": "Sleepy Kitten", + "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." + }, + { + "id": "cat-220", + "title": "The snooze math", + "top": "9 more minutes × 6", + "bottom": "still tired. shocking.", + "mood": "sleepy", + "tags": [ + "morning", + "math" + ], + "image": "/assets/memes/lp2ba.jpg", + "template": "Sleepy Cat", + "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." + }, + { + "id": "cat-221", + "title": "The 3pm wall", + "top": "hits like a truck", + "bottom": "truck full of pillows", + "mood": "sleepy", + "tags": [ + "afternoon", + "tired" + ], + "image": "/assets/memes/3e5sun.jpg", + "template": "I'm awake, but at what cost?", + "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." + }, + { + "id": "cat-222", + "title": "The scroll hole", + "top": "“last video”", + "bottom": "sunrise. birds.", + "mood": "sleepy", + "tags": [ + "phone", + "night" + ], + "image": "/assets/memes/3vnurc.jpg", + "template": "Crying cat on phone", + "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." + }, + { + "id": "cat-223", + "title": "The cozy trap", + "top": "blanket + rain sounds", + "bottom": "plans: cancelled", + "mood": "sleepy", + "tags": [ + "cozy", + "rain" + ], + "image": "/assets/memes/11wis1.jpg", + "template": "Sleepy Kitten", + "source": "https://imgflip.com/meme/63663121/Sleepy-Kitten", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Eepy before eepy was a word." + }, + { + "id": "cat-224", + "title": "The conference coma", + "top": "camera on. brain off.", + "bottom": "nodding professionally", + "mood": "sleepy", + "tags": [ + "work", + "meeting" + ], + "image": "/assets/memes/1bh7.jpg", + "template": "Business Cat", + "source": "https://imgflip.com/meme/Business-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2011, + "status": "immortal", + "lore": "Climbed the corporate ladder. Knocked it over." + }, + { + "id": "cat-225", + "title": "The nap math", + "top": "20 min power nap", + "bottom": "4 hours. oops.", + "mood": "sleepy", + "tags": [ + "nap", + "oops" + ], + "image": "/assets/memes/lp2ba.jpg", + "template": "Sleepy Cat", + "source": "https://imgflip.com/meme/36441334/Sleepy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "classic", + "lore": "Horizontal since forever." + }, + { + "id": "cat-226", + "title": "The timezone", + "top": "flies 3 hours", + "bottom": "jet lagged for a month", + "mood": "sleepy", + "tags": [ + "travel", + "tired" + ], + "image": "/assets/memes/3e5sun.jpg", + "template": "I'm awake, but at what cost?", + "source": "https://imgflip.com/meme/205183823/Im-awake-but-at-what-cost", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Awake. At what cost." + }, + { + "id": "cat-227", + "title": "The low battery", + "top": "phone at 4%", + "bottom": "no charger. this is the end.", + "mood": "sleepy", + "tags": [ + "phone", + "battery" + ], + "image": "/assets/memes/3vnurc.jpg", + "template": "Crying cat on phone", + "source": "https://imgflip.com/meme/234579576/Crying-cat-on-phone", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Doomscrolling before it was cool." + }, + { + "id": "cat-228", + "title": "The good stretch", + "top": "morning stretch", + "bottom": "cracks like glow sticks", + "mood": "sleepy", + "tags": [ + "morning", + "stretch" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-229", + "title": "The warm laundry", + "top": "hugs fresh laundry", + "bottom": "becomes laundry", + "mood": "sleepy", + "tags": [ + "laundry", + "cozy" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-230", + "title": "The friday nap", + "top": "“quick nap before dinner”", + "bottom": "wakes up saturday", + "mood": "sleepy", + "tags": [ + "weekend", + "nap" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-231", + "title": "The snorer", + "top": "“i don’t snore”", + "bottom": "recorded evidence attached", + "mood": "sleepy", + "tags": [ + "sleep", + "evidence" + ], + "image": "/assets/memes/5do96.jpg", + "template": "Grumpy Cat Not Amused", + "source": "https://imgflip.com/meme/Grumpy-Cat-Not-Amused", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "The gold standard of not-amused." + }, + { + "id": "cat-232", + "title": "The monday alarm", + "top": "5 alarms set", + "bottom": "sleeps through all 5", + "mood": "sleepy", + "tags": [ + "morning", + "monday" + ], + "image": "/assets/memes/8p0a.jpg", + "template": "Grumpy Cat", + "source": "https://imgflip.com/meme/Grumpy-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2012, + "status": "immortal", + "lore": "Grumpy since 2012. Forever unimpressed." + }, + { + "id": "cat-233", + "title": "The night owl", + "top": "“so productive at night”", + "bottom": "produces: anxiety", + "mood": "sleepy", + "tags": [ + "night", + "truth" + ], + "image": "/assets/memes/35v1lq.jpg", + "template": "white cat table", + "source": "https://imgflip.com/meme/191245022/white-cat-table", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "Sits at tables. Judges at tables." + }, + { + "id": "cat-234", + "title": "The pillow war", + "top": "steals all pillows", + "bottom": "denies everything", + "mood": "sleepy", + "tags": [ + "sleep", + "denial" + ], + "image": "/assets/memes/39binw.jpg", + "template": "Smudge the cat", + "source": "https://imgflip.com/meme/197052476/Smudge-the-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2018, + "status": "immortal", + "lore": "Yells back at dinner since 2018." + }, + { + "id": "cat-235", + "title": "The proud parent", + "top": "kid drew a potato", + "bottom": "framing it. obviously.", + "mood": "wholesome", + "tags": [ + "kids", + "love" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-236", + "title": "The lost tourist", + "top": "looks lost", + "bottom": "walks them there anyway", + "mood": "wholesome", + "tags": [ + "kindness", + "city" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-237", + "title": "The promotion", + "top": "friend got promoted", + "bottom": "louder than the promotion", + "mood": "wholesome", + "tags": [ + "friends", + "win" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-238", + "title": "The first snow", + "top": "first snowflake", + "bottom": "acts like it’s magic. it is.", + "mood": "wholesome", + "tags": [ + "winter", + "wonder" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-239", + "title": "The grandma call", + "top": "calls grandma", + "bottom": "stays on for an hour", + "mood": "wholesome", + "tags": [ + "family", + "love" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-240", + "title": "The shelter visit", + "top": "“just looking”", + "bottom": "leaves with a cat", + "mood": "wholesome", + "tags": [ + "cats", + "love" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-241", + "title": "The home run", + "top": "kid’s first goal", + "bottom": "entire stands erupt", + "mood": "wholesome", + "tags": [ + "kids", + "sports" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-242", + "title": "The thank-you wave", + "top": "lets car merge", + "bottom": "gets the wave. best feeling.", + "mood": "wholesome", + "tags": [ + "driving", + "kindness" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-243", + "title": "The birthday song", + "top": "off-key singing", + "bottom": "loudest in the room", + "mood": "wholesome", + "tags": [ + "birthday", + "friends" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-244", + "title": "The study buddy", + "top": "brings snacks", + "bottom": "to the library grind", + "mood": "wholesome", + "tags": [ + "friends", + "snacks" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-245", + "title": "The finish line", + "top": "cheers strangers", + "bottom": "at mile 20. all of them.", + "mood": "wholesome", + "tags": [ + "sports", + "kindness" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-246", + "title": "The lucky penny", + "top": "finds a penny", + "bottom": "day = made", + "mood": "wholesome", + "tags": [ + "luck", + "joy" + ], + "image": "/assets/memes/4xgqu.jpg", + "template": "Cute Cat", + "source": "https://imgflip.com/meme/Cute-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2015, + "status": "classic", + "lore": "Certified cute. No notes." + }, + { + "id": "cat-247", + "title": "The neighbor cookies", + "top": "bakes too much", + "bottom": "doorbell ditch with love", + "mood": "wholesome", + "tags": [ + "neighbors", + "baking" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-248", + "title": "The old dog", + "top": "slow on walks", + "bottom": "we go slow. gladly.", + "mood": "wholesome", + "tags": [ + "dogs", + "love" + ], + "image": "/assets/memes/3ex8oy.jpg", + "template": "Sad Cat Holding Dog", + "source": "https://imgflip.com/meme/206464066/Sad-Cat-Holding-Dog", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Emotional support, holding." + }, + { + "id": "cat-249", + "title": "The standing ovation", + "top": "school play", + "bottom": "front row. tears.", + "mood": "wholesome", + "tags": [ + "kids", + "proud" + ], + "image": "/assets/memes/72k5g.jpg", + "template": "Excited Cat", + "source": "https://imgflip.com/meme/Excited-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Excitement levels: unsafe." + }, + { + "id": "cat-250", + "title": "The 250th cat", + "top": "you made it here", + "bottom": "the cats are proud of you", + "mood": "wholesome", + "tags": [ + "milestone", + "love" + ], + "image": "/assets/memes/amuvy.jpg", + "template": "Smiling Cat", + "source": "https://imgflip.com/meme/Smiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2022, + "status": "alive", + "lore": "Smiling through the chaos." + }, + { + "id": "cat-251", + "title": "The deadline approaches", + "top": "heavy breathing intensifies", + "bottom": "it’s due in 20 minutes", + "mood": "relatable", + "tags": [ + "deadline", + "work" + ], + "image": "/assets/memes/7rf5z.jpg", + "template": "Heavy Breathing Cat", + "source": "https://imgflip.com/meme/Heavy-Breathing-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Inhales deadlines. Exhales panic." + }, + { + "id": "cat-252", + "title": "The gossip arrives", + "top": "WAIT. tell me", + "bottom": "EVERYTHING. now.", + "mood": "chaos", + "tags": [ + "gossip", + "drama" + ], + "image": "/assets/memes/7rf5z.jpg", + "template": "Heavy Breathing Cat", + "source": "https://imgflip.com/meme/Heavy-Breathing-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Inhales deadlines. Exhales panic." + }, + { + "id": "cat-253", + "title": "The oven timer", + "top": "2 minutes left", + "bottom": "watching like a hawk", + "mood": "snacks", + "tags": [ + "dinner", + "hungry" + ], + "image": "/assets/memes/7rf5z.jpg", + "template": "Heavy Breathing Cat", + "source": "https://imgflip.com/meme/Heavy-Breathing-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Inhales deadlines. Exhales panic." + }, + { + "id": "cat-254", + "title": "The plot twist episode", + "top": "don’t you dare end here", + "bottom": "TO BE CONTINUED", + "mood": "relatable", + "tags": [ + "tv", + "drama" + ], + "image": "/assets/memes/7rf5z.jpg", + "template": "Heavy Breathing Cat", + "source": "https://imgflip.com/meme/Heavy-Breathing-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Inhales deadlines. Exhales panic." + }, + { + "id": "cat-255", + "title": "The excuse", + "top": "“traffic was crazy”", + "bottom": "you work from home", + "mood": "judgment", + "tags": [ + "work", + "lies" + ], + "image": "/assets/memes/7rf5z.jpg", + "template": "Heavy Breathing Cat", + "source": "https://imgflip.com/meme/Heavy-Breathing-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Inhales deadlines. Exhales panic." + }, + { + "id": "cat-256", + "title": "The bedroom door", + "top": "you shall not nap", + "bottom": "without the tribute (treats)", + "mood": "judgment", + "tags": [ + "door", + "treats" + ], + "image": "/assets/memes/zl7we.jpg", + "template": "Persian Cat Room Guardian", + "source": "https://imgflip.com/meme/Persian-Cat-Room-Guardian", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "You shall not pass. Without snacks." + }, + { + "id": "cat-257", + "title": "The monday guardian", + "top": "none shall pass", + "bottom": "back to bed, all of you", + "mood": "relatable", + "tags": [ + "monday", + "nap" + ], + "image": "/assets/memes/zl7we.jpg", + "template": "Persian Cat Room Guardian", + "source": "https://imgflip.com/meme/Persian-Cat-Room-Guardian", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "You shall not pass. Without snacks." + }, + { + "id": "cat-258", + "title": "The wifi guardian", + "top": "password, mortal", + "bottom": "it’s on the fridge. as always.", + "mood": "chaos", + "tags": [ + "tech", + "wifi" + ], + "image": "/assets/memes/zl7we.jpg", + "template": "Persian Cat Room Guardian", + "source": "https://imgflip.com/meme/Persian-Cat-Room-Guardian", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "You shall not pass. Without snacks." + }, + { + "id": "cat-259", + "title": "The fridge guardian", + "top": "state your business", + "bottom": "it’s 3am. same business.", + "mood": "snacks", + "tags": [ + "fridge", + "night" + ], + "image": "/assets/memes/zl7we.jpg", + "template": "Persian Cat Room Guardian", + "source": "https://imgflip.com/meme/Persian-Cat-Room-Guardian", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "You shall not pass. Without snacks." + }, + { + "id": "cat-260", + "title": "The hallway guardian", + "top": "escorts you at night", + "bottom": "charges one head pat", + "mood": "wholesome", + "tags": [ + "night", + "love" + ], + "image": "/assets/memes/zl7we.jpg", + "template": "Persian Cat Room Guardian", + "source": "https://imgflip.com/meme/Persian-Cat-Room-Guardian", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "You shall not pass. Without snacks." + }, + { + "id": "cat-261", + "title": "The browser history", + "top": "ceiling cat sees", + "bottom": "all 47 tabs", + "mood": "judgment", + "tags": [ + "tech", + "privacy" + ], + "image": "/assets/memes/22vtk.jpg", + "template": "Ceiling Cat", + "source": "https://imgflip.com/meme/Ceiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2006, + "status": "immortal", + "lore": "Watching you since 2006." + }, + { + "id": "cat-262", + "title": "The diet witness", + "top": "saw the midnight snack", + "bottom": "tells no one. judges plenty.", + "mood": "relatable", + "tags": [ + "diet", + "night" + ], + "image": "/assets/memes/22vtk.jpg", + "template": "Ceiling Cat", + "source": "https://imgflip.com/meme/Ceiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2006, + "status": "immortal", + "lore": "Watching you since 2006." + }, + { + "id": "cat-263", + "title": "The ceiling fan", + "top": "staring contest", + "bottom": "loser blinks first. it’s you.", + "mood": "chaos", + "tags": [ + "stare", + "duel" + ], + "image": "/assets/memes/22vtk.jpg", + "template": "Ceiling Cat", + "source": "https://imgflip.com/meme/Ceiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2006, + "status": "immortal", + "lore": "Watching you since 2006." + }, + { + "id": "cat-264", + "title": "The 3am ceiling", + "top": "can’t sleep either, huh", + "bottom": "let’s both pretend", + "mood": "sleepy", + "tags": [ + "night", + "insomnia" + ], + "image": "/assets/memes/22vtk.jpg", + "template": "Ceiling Cat", + "source": "https://imgflip.com/meme/Ceiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2006, + "status": "immortal", + "lore": "Watching you since 2006." + }, + { + "id": "cat-265", + "title": "The guardian above", + "top": "watches over naps", + "bottom": "overtime, unpaid", + "mood": "wholesome", + "tags": [ + "nap", + "care" + ], + "image": "/assets/memes/22vtk.jpg", + "template": "Ceiling Cat", + "source": "https://imgflip.com/meme/Ceiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2006, + "status": "immortal", + "lore": "Watching you since 2006." + }, + { + "id": "cat-266", + "title": "The swamp era", + "top": "out of my swamp", + "bottom": "into my box", + "mood": "chaos", + "tags": [ + "swamp", + "box" + ], + "image": "/assets/memes/3m5mp.jpg", + "template": "Shrek Cat", + "source": "https://imgflip.com/meme/Shrek-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Ogres have layers. Cats have everything." + }, + { + "id": "cat-267", + "title": "The layers", + "top": "ogres have layers", + "bottom": "i have blankets", + "mood": "relatable", + "tags": [ + "cozy", + "layers" + ], + "image": "/assets/memes/3m5mp.jpg", + "template": "Shrek Cat", + "source": "https://imgflip.com/meme/Shrek-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Ogres have layers. Cats have everything." + }, + { + "id": "cat-268", + "title": "The onion verdict", + "top": "tastes like an onion", + "bottom": "crying anyway", + "mood": "snacks", + "tags": [ + "onion", + "dinner" + ], + "image": "/assets/memes/3m5mp.jpg", + "template": "Shrek Cat", + "source": "https://imgflip.com/meme/Shrek-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Ogres have layers. Cats have everything." + }, + { + "id": "cat-269", + "title": "The fairy tale", + "top": "“happily ever after”", + "bottom": "nap first. then ever after.", + "mood": "judgment", + "tags": [ + "tales", + "nap" + ], + "image": "/assets/memes/3m5mp.jpg", + "template": "Shrek Cat", + "source": "https://imgflip.com/meme/Shrek-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Ogres have layers. Cats have everything." + }, + { + "id": "cat-270", + "title": "The donkey energy", + "top": "annoying but loyal", + "bottom": "that’s friendship", + "mood": "wholesome", + "tags": [ + "friends", + "love" + ], + "image": "/assets/memes/3m5mp.jpg", + "template": "Shrek Cat", + "source": "https://imgflip.com/meme/Shrek-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Ogres have layers. Cats have everything." + }, + { + "id": "cat-271", + "title": "The founding father", + "top": "i can has", + "bottom": "a place in history", + "mood": "wholesome", + "tags": [ + "history", + "classic" + ], + "image": "/assets/memes/jru7x.jpg", + "template": "I can has cheezburger cat", + "source": "https://imgflip.com/meme/33211581/I-can-has-cheezburger-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2007, + "status": "immortal", + "lore": "The one that started it all." + }, + { + "id": "cat-272", + "title": "The ancient text", + "top": "2007. simpler times.", + "bottom": "cheezburger was enough", + "mood": "relatable", + "tags": [ + "nostalgia", + "classic" + ], + "image": "/assets/memes/jru7x.jpg", + "template": "I can has cheezburger cat", + "source": "https://imgflip.com/meme/33211581/I-can-has-cheezburger-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2007, + "status": "immortal", + "lore": "The one that started it all." + }, + { + "id": "cat-273", + "title": "The original order", + "top": "i can has", + "bottom": "cheezburger. still correct.", + "mood": "snacks", + "tags": [ + "classic", + "dinner" + ], + "image": "/assets/memes/jru7x.jpg", + "template": "I can has cheezburger cat", + "source": "https://imgflip.com/meme/33211581/I-can-has-cheezburger-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2007, + "status": "immortal", + "lore": "The one that started it all." + }, + { + "id": "cat-274", + "title": "The prophecy", + "top": "it began with burger", + "bottom": "it ends with burger", + "mood": "chaos", + "tags": [ + "destiny", + "classic" + ], + "image": "/assets/memes/jru7x.jpg", + "template": "I can has cheezburger cat", + "source": "https://imgflip.com/meme/33211581/I-can-has-cheezburger-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2007, + "status": "immortal", + "lore": "The one that started it all." + }, + { + "id": "cat-275", + "title": "The grammar", + "top": "“i can has”", + "bottom": "grammatically perfect, actually", + "mood": "judgment", + "tags": [ + "grammar", + "attitude" + ], + "image": "/assets/memes/jru7x.jpg", + "template": "I can has cheezburger cat", + "source": "https://imgflip.com/meme/33211581/I-can-has-cheezburger-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2007, + "status": "immortal", + "lore": "The one that started it all." + }, + { + "id": "cat-276", + "title": "The group chat trial", + "top": "“EXPLAIN YOURSELF”", + "bottom": "seen 9:41pm", + "mood": "relatable", + "tags": [ + "texting", + "drama" + ], + "image": "/assets/memes/345v97.jpg", + "template": "Woman Yelling At Cat", + "source": "https://imgflip.com/meme/Woman-Yelling-At-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "immortal", + "lore": "Dinner theater in two panels." + }, + { + "id": "cat-277", + "title": "The dinner trial", + "top": "“YOU ATE MY FRIES”", + "bottom": "no further questions", + "mood": "snacks", + "tags": [ + "fries", + "drama" + ], + "image": "/assets/memes/345v97.jpg", + "template": "Woman Yelling At Cat", + "source": "https://imgflip.com/meme/Woman-Yelling-At-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "immortal", + "lore": "Dinner theater in two panels." + }, + { + "id": "cat-278", + "title": "The thermostat trial", + "top": "“IT’S 74 IN HERE”", + "bottom": "wearing a sweater. unbothered.", + "mood": "judgment", + "tags": [ + "home", + "thermostat" + ], + "image": "/assets/memes/345v97.jpg", + "template": "Woman Yelling At Cat", + "source": "https://imgflip.com/meme/Woman-Yelling-At-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "immortal", + "lore": "Dinner theater in two panels." + }, + { + "id": "cat-279", + "title": "The 3am trial", + "top": "“WHY ARE YOU AWAKE”", + "bottom": "why are YOU awake", + "mood": "chaos", + "tags": [ + "night", + "comeback" + ], + "image": "/assets/memes/345v97.jpg", + "template": "Woman Yelling At Cat", + "source": "https://imgflip.com/meme/Woman-Yelling-At-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "immortal", + "lore": "Dinner theater in two panels." + }, + { + "id": "cat-280", + "title": "The verdict", + "top": "case dismissed", + "bottom": "cuddle sentence issued", + "mood": "wholesome", + "tags": [ + "love", + "verdict" + ], + "image": "/assets/memes/345v97.jpg", + "template": "Woman Yelling At Cat", + "source": "https://imgflip.com/meme/Woman-Yelling-At-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "immortal", + "lore": "Dinner theater in two panels." + }, + { + "id": "cat-281", + "title": "The yawn approaches", + "top": "inhales deeply", + "bottom": "asleep mid-breath", + "mood": "sleepy", + "tags": [ + "nap", + "yawn" + ], + "image": "/assets/memes/7rf5z.jpg", + "template": "Heavy Breathing Cat", + "source": "https://imgflip.com/meme/Heavy-Breathing-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2013, + "status": "classic", + "lore": "Inhales deadlines. Exhales panic." + }, + { + "id": "cat-282", + "title": "The dream guardian", + "top": "protects your naps", + "bottom": "snores on duty", + "mood": "sleepy", + "tags": [ + "nap", + "duty" + ], + "image": "/assets/memes/zl7we.jpg", + "template": "Persian Cat Room Guardian", + "source": "https://imgflip.com/meme/Persian-Cat-Room-Guardian", + "kind": "remix", + "added": "2026-09-14", + "origin": 2019, + "status": "classic", + "lore": "You shall not pass. Without snacks." + }, + { + "id": "cat-283", + "title": "The lullaby", + "top": "ceiling fan hums", + "bottom": "both asleep now", + "mood": "sleepy", + "tags": [ + "night", + "sleep" + ], + "image": "/assets/memes/22vtk.jpg", + "template": "Ceiling Cat", + "source": "https://imgflip.com/meme/Ceiling-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2006, + "status": "immortal", + "lore": "Watching you since 2006." + }, + { + "id": "cat-284", + "title": "The swamp nap", + "top": "do not disturb", + "bottom": "ogre sleeping", + "mood": "sleepy", + "tags": [ + "nap", + "swamp" + ], + "image": "/assets/memes/3m5mp.jpg", + "template": "Shrek Cat", + "source": "https://imgflip.com/meme/Shrek-Cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2023, + "status": "alive", + "lore": "Ogres have layers. Cats have everything." + }, + { + "id": "cat-285", + "title": "The food coma", + "top": "i can has nap", + "bottom": "after burger. obviously.", + "mood": "sleepy", + "tags": [ + "nap", + "classic" + ], + "image": "/assets/memes/jru7x.jpg", + "template": "I can has cheezburger cat", + "source": "https://imgflip.com/meme/33211581/I-can-has-cheezburger-cat", + "kind": "remix", + "added": "2026-09-14", + "origin": 2007, + "status": "immortal", + "lore": "The one that started it all." + }, + { + "id": "cat-286", + "title": "The group chat fire", + "top": "brought popcorn", + "bottom": "to your drama", + "mood": "chaos", + "tags": [ + "drama", + "popcorn" + ], + "image": "/assets/memes/8tlh.jpg", + "template": "Burn Kitty", + "source": "https://imgflip.com/meme/Burn-Kitty", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Likes the fire. Is the fire." + }, + { + "id": "cat-287", + "title": "The chaos enjoyer", + "top": "“this is fine”", + "bottom": "narrator: it was not fine", + "mood": "judgment", + "tags": [ + "fire", + "denial" + ], + "image": "/assets/memes/8tlh.jpg", + "template": "Burn Kitty", + "source": "https://imgflip.com/meme/Burn-Kitty", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Likes the fire. Is the fire." + }, + { + "id": "cat-288", + "title": "The monday fire", + "top": "everything’s burning", + "bottom": "anyway, coffee?", + "mood": "relatable", + "tags": [ + "monday", + "coffee" + ], + "image": "/assets/memes/8tlh.jpg", + "template": "Burn Kitty", + "source": "https://imgflip.com/meme/Burn-Kitty", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Likes the fire. Is the fire." + }, + { + "id": "cat-289", + "title": "The marshmallow plan", + "top": "everything’s on fire", + "bottom": "s’mores o’clock", + "mood": "snacks", + "tags": [ + "fire", + "smores" + ], + "image": "/assets/memes/8tlh.jpg", + "template": "Burn Kitty", + "source": "https://imgflip.com/meme/Burn-Kitty", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Likes the fire. Is the fire." + }, + { + "id": "cat-290", + "title": "The campfire", + "top": "stories and sparks", + "bottom": "stay for one more", + "mood": "wholesome", + "tags": [ + "campfire", + "friends" + ], + "image": "/assets/memes/8tlh.jpg", + "template": "Burn Kitty", + "source": "https://imgflip.com/meme/Burn-Kitty", + "kind": "remix", + "added": "2026-09-14", + "origin": 2021, + "status": "alive", + "lore": "Likes the fire. Is the fire." } -] \ No newline at end of file +] diff --git a/public/style.css b/public/style.css index 23b7852..90a3ba9 100644 --- a/public/style.css +++ b/public/style.css @@ -1,7 +1,212 @@ -:root{--olive:#4a412a;--olive-dark:#332e20;--olive-soft:#797052;--cream:#f7f5ee;--paper:#fffef9;--line:#e4e0d4;--blue:#b9c5f3;--blue-dark:#424f82;--blue-pale:#e9edf9;--muted:#797665;--green:#e3ebd7;--pink:#f4e0db;--gold:#eee2b7;--radius:16px;font-family:'DM Sans',sans-serif;color:var(--olive);background:var(--cream);font-synthesis:none;scroll-behavior:smooth} -*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button,a,input,select,textarea{-webkit-tap-highlight-color:transparent}button{cursor:pointer}button{color:inherit}a{color:inherit}button:disabled{cursor:wait;opacity:.5}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid var(--blue-dark);outline-offset:4px}button{transition:background .16s,transform .16s,box-shadow .16s}button:active{transform:translateY(1px)}button svg,a svg{pointer-events:none}svg.icon{width:19px;height:19px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;display:block;flex-shrink:0}[data-icon]{display:inline-flex}button{border:0;background:none}h1,h2,h3,p{margin:0}[hidden]{display:none!important}.skip-link{position:fixed;top:-80px;left:20px;background:var(--olive);color:white;padding:12px;z-index:100}.skip-link:focus{top:10px}.sidebar{width:220px;position:fixed;inset:0 auto 0 0;padding:31px 22px 22px;background:#efede3;border-right:1px solid #e0ddcf;display:flex;flex-direction:column;z-index:20}.brand{display:flex;gap:9px;align-items:center;text-decoration:none;font-size:17px;font-weight:800;letter-spacing:-.8px;line-height:1.05}.brand img{width:54px;height:54px;image-rendering:pixelated}.brand-bottom{display:block;font-size:27px;letter-spacing:1.3px}.brand-dot{font-size:11px;vertical-align:top;letter-spacing:0;margin-left:3px}.side-tagline{font-size:12px;color:var(--muted);line-height:1.6;margin:18px 0 34px 8px}.nav-label{font-size:9px;font-weight:700;letter-spacing:1.4px;color:#8b8775;margin:0 8px 12px}.main-nav{display:grid;gap:5px}.nav-item{display:flex;gap:12px;align-items:center;width:100%;padding:12px 11px;border-radius:9px;text-align:left;font-size:12px;font-weight:600;min-height:43px}.nav-item:hover{background:#e5e2d4}.nav-item.active{background:var(--olive);color:#fffdf4}.nav-count{font-size:10px;margin-left:auto;opacity:.8;background:#dfdccd;padding:2px 6px;border-radius:5px;min-width:21px;text-align:center}.active .nav-count{background:#6b6046;color:#fff}.side-divider{height:1px;background:#dcd8c8;margin:26px 8px}.keycap{font-size:9px;border:1px solid #c9c4b2;border-radius:4px;padding:1px 4px;margin-left:auto}.side-bottom{margin-top:auto;padding-top:35px}.chimp-note{position:relative;border:1px solid #d8d3bf;border-radius:13px;padding:19px 12px 28px;background:#e8e4d3;overflow:hidden}.chimp-note p{position:relative;z-index:1;font-size:11px;line-height:1.65}.chimp-note em{font-weight:700;font-style:normal}.chimp-note img{position:absolute;width:66px;right:-7px;bottom:-6px;transform:rotate(-14deg);opacity:.72}.tiny-spark{font-size:20px;color:var(--olive-soft);display:block;margin-bottom:12px}.about-link{display:flex;gap:7px;align-items:center;font-size:9px;padding:16px 0;width:100%;justify-content:center}.swatch{background:var(--olive);width:10px;height:10px;border-radius:2px;display:inline-block}.shortcut-link{display:flex;align-items:center;gap:7px;font-size:10px;color:var(--muted);padding:10px 4px;width:100%;border-top:1px solid #ddd9cb}.shortcut-link svg{width:16px}.shortcut-link>span:last-child{margin-left:auto}.workspace{margin-left:220px}.topbar{height:83px;padding:0 38px;display:flex;align-items:center;justify-content:space-between;gap:20px;border-bottom:1px solid var(--line);background:var(--cream)}.breadcrumb{display:flex;align-items:center;gap:10px;font-size:9px;font-weight:700;letter-spacing:1.5px;white-space:nowrap}.status-dot{width:6px;height:6px;border-radius:100%;background:var(--olive-soft);box-shadow:0 0 0 4px #eae7da}.edition{color:#969181;padding-left:10px;border-left:1px solid #d7d1c2;letter-spacing:1px}.top-actions{display:flex;gap:13px;align-items:center}.search-wrap{display:flex;align-items:center;gap:9px;border:1px solid #dfdbcf;background:#eeece3;border-radius:8px;padding:9px 11px;width:253px}.search-wrap svg{width:16px;height:16px;color:#7b7664}.search-wrap input{width:100%;min-width:0;border:0;background:transparent;outline:0;font-size:11px;color:var(--olive)}.search-wrap:focus-within{border-color:var(--olive-soft);box-shadow:0 0 0 2px #e0dccb}.search-wrap kbd{font-size:10px;border:1px solid #d5d0c0;padding:0 4px;border-radius:3px}.create-button{display:flex;gap:7px;align-items:center;background:var(--olive);color:var(--cream);padding:11px 13px;border-radius:8px;font-size:11px;font-weight:600;white-space:nowrap}.create-button:hover,.primary-button:hover{background:#62573b}.create-button svg{width:16px;height:16px}.mobile-home{display:none}main{max-width:1490px;margin:auto;padding:30px 38px 0}.hero{min-height:315px;border-radius:18px;position:relative;background:var(--olive);color:var(--cream);overflow:hidden;display:flex;align-items:center;padding:35px 34px}.hero-copy{width:60%;position:relative;z-index:2}.eyebrow{font-size:9px;letter-spacing:1.6px;font-weight:700;display:flex;align-items:center;gap:7px}.hero .eyebrow{color:#d3cbb1;font-size:8px;letter-spacing:1.5px}.little-star{font-size:15px;color:var(--blue)}h1{font-family:Fraunces,Georgia,serif;font-size:clamp(38px,4.1vw,63px);letter-spacing:-2px;line-height:1.08;font-weight:600;margin-top:14px}h1 em{color:var(--blue);font-weight:600}.hero-copy p{font-size:11px;line-height:1.8;color:#d7d1c0;margin:15px 0 20px}.hero-button{display:inline-flex;align-items:center;gap:21px;padding:12px 15px;background:var(--blue);color:#303952;font-size:11px;font-weight:700;border-radius:7px}.hero-button:hover{background:#d5dcf7;transform:translateY(-2px)}.hero-button svg{width:16px;height:16px}.hero-footnote{font-size:9px;color:#b2aa92;margin-left:13px}.hero-art{position:absolute;right:21px;width:39%;top:0;height:100%}.hero-ring{position:absolute;width:269px;height:269px;border:1px solid #756a4c;border-radius:100%;top:34px;right:4px;transform:rotate(-20deg)}.hero-ring:after{content:'';position:absolute;inset:22px -30px;border:1px solid #756a4c;border-radius:100%;transform:rotate(60deg)}.hero-sticker{background:var(--paper);padding:9px 9px 7px;width:176px;border-radius:5px;position:absolute;box-shadow:0 10px 26px #211d2340;color:var(--olive)}.sticker-caption{font-size:12px;font-weight:700;line-height:1.35;padding:2px 2px 8px}.hero-sticker img{width:100%;display:block;object-fit:cover;border-radius:2px;max-height:139px}.back-sticker{right:4px;top:30px;transform:rotate(12deg);background:#dfe7ff}.back-sticker img{height:135px;object-fit:contain;background:#fff}.front-sticker{left:1px;top:72px;transform:rotate(-9deg);width:194px;animation:float-cat 7s ease-in-out infinite}.front-sticker img{height:131px;object-position:center}.sticker-footer{font-size:8px;display:flex;justify-content:space-between;align-items:center;padding-top:8px}.sticker-footer span{font-size:18px}.approval-sticker{width:82px;height:82px;position:absolute;background:var(--gold);color:var(--olive);right:4px;bottom:34px;border-radius:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;transform:rotate(13deg);font-family:Fraunces,serif;font-size:26px;box-shadow:0 0 0 4px var(--olive),0 0 0 5px #a69a75}.approval-sticker span{font-family:'DM Sans',sans-serif;font-size:7px;letter-spacing:1px;font-weight:700}.approval-sticker .mini-paw{font-size:17px}.art-star{position:absolute;font-size:40px;left:-21px;top:25px;color:var(--blue)}.hero-doodle{position:absolute;right:58px;bottom:14px;font-size:9px;color:#c8bea0;font-style:italic;transform:rotate(3deg)}.library{margin-top:34px;scroll-margin-top:20px}.library-heading{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:20px}.section-eyebrow{color:var(--muted);font-size:8px;letter-spacing:1.5px;margin-bottom:8px}h2{font-family:Fraunces,Georgia,serif;font-size:27px;font-weight:600;letter-spacing:-.7px}h2>span{color:#8f9dcf}.text-button{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:500;white-space:nowrap;padding:9px 0}.text-button:hover{color:var(--blue-dark)}.text-button svg{width:16px;height:16px}.mood-filters{display:flex;gap:7px;flex-wrap:wrap}.mood-filter{border:1px solid var(--line);display:flex;align-items:center;gap:7px;border-radius:30px;padding:9px 14px;font-size:11px;background:var(--paper);font-weight:500}.mood-filter span{font-size:12px}.mood-filter:hover{border-color:var(--olive-soft);background:#eeeadd}.mood-filter.active{background:var(--olive);border-color:var(--olive);color:var(--cream)}.filter-row{padding-bottom:18px;border-bottom:1px solid var(--line)}.results-bar{display:flex;justify-content:space-between;align-items:center;padding:17px 0;color:var(--muted);font-size:10px;gap:10px}.results-bar strong{color:var(--olive);font-weight:600}.result-controls{display:flex;gap:14px;align-items:center}.sort-label{display:flex;gap:5px;align-items:center;white-space:nowrap}.sort-label select{border:0;background:transparent;color:var(--olive);font-size:10px;font-weight:600;cursor:pointer;max-width:125px}.view-toggle{display:flex;gap:1px;background:#eae7dd;padding:3px;border-radius:6px}.view-toggle button{padding:4px;border-radius:4px;color:#999380}.view-toggle button.selected{background:var(--paper);color:var(--olive);box-shadow:0 1px 3px #42392012}.view-toggle svg{width:14px;height:14px}.meme-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:21px;align-items:start}.meme-grid.dense{grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.meme-card{background:var(--paper);border:1px solid var(--line);border-radius:13px;overflow:hidden;transition:transform .2s,box-shadow .2s,border-color .2s;animation:card-in .35s both}.meme-card:hover{transform:translateY(-4px);box-shadow:0 8px 22px #4a412a10;border-color:#bfb8a1}.meme-open{display:block;width:100%;padding:0;text-align:left}.meme-visual{background:#fff;color:#24231e;display:flex;flex-direction:column;overflow:hidden}.meme-caption{font-size:clamp(13px,1.2vw,19px);font-weight:700;line-height:1.3;padding:15px 17px 11px;min-height:59px;display:flex;align-items:center;white-space:pre-wrap;overflow-wrap:anywhere}.meme-caption.bottom{font-size:clamp(12px,1.08vw,17px);padding:12px 17px 15px;min-height:54px}.meme-image{width:100%;height:215px;object-fit:contain;display:block;background:#f4f2ed}.dense .meme-image{height:168px}.dense .meme-caption{font-size:14px;padding:12px;min-height:56px}.dense .meme-caption.bottom{font-size:13px;min-height:58px}.card-meta{padding:13px 14px 12px;border-top:1px solid #efede5}.card-topline{display:flex;justify-content:space-between;gap:8px;align-items:center}.mood-label{font-size:8px;font-weight:600;padding:4px 7px;border-radius:4px;display:inline-flex;align-items:center;gap:4px;letter-spacing:.2px;background:var(--blue-pale);color:var(--blue-dark)}.mood-label[data-mood=chaos]{background:var(--gold);color:#6f5822}.mood-label[data-mood=wholesome]{background:var(--green);color:#4c653b}.mood-label[data-mood=judgment]{background:var(--pink);color:#82564b}.mood-label[data-mood=sleepy]{background:#eae3f2;color:#6a537f}.mood-label[data-mood=snacks]{background:#f4e7d7;color:#85602f}.card-source{font-size:8px;color:#a09b8d}.card-bottomline{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:8px}.card-title{font-size:11px;font-weight:600;line-height:1.4;margin:0;flex:1;overflow-wrap:anywhere}.save-button{color:#918876;padding:5px;border-radius:6px;position:relative}.save-button:hover{background:var(--pink);color:#9c6055}.save-button.saved{color:#9e5c53}.save-button.saved svg{fill:#eed2cc}.save-button svg{width:18px;height:18px}.load-more{display:flex;align-items:center;justify-content:center;gap:16px;padding:13px 24px;margin:30px auto 0;border:1px solid #ccc5b1;border-radius:7px;font-size:11px;font-weight:600;background:var(--paper)}.load-more:hover{background:#e9e4d5}.load-more svg{width:15px;height:15px}.end-note{display:flex;gap:14px;align-items:center;justify-content:center;padding:38px 0 8px;color:var(--muted);font-size:11px;text-align:left;line-height:1.8}.end-note>span{font-size:29px;color:var(--olive-soft)}.end-note button{padding:0;text-decoration:underline;font-size:10px}footer{padding:30px 0 20px;border-top:1px solid var(--line);margin-top:36px;display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:10px;color:#8a8471}footer>span{display:flex;align-items:center;gap:8px}footer img{width:22px;opacity:.7}footer button{font-size:10px;padding:0}.page-description{font-size:12px;line-height:1.7;color:var(--muted);margin-top:9px}.empty-state{text-align:center;padding:65px 20px}.empty-face{font-size:66px;display:block;color:#958968;margin-bottom:18px}.empty-state h3{font-family:Fraunces,Georgia,serif;font-size:28px;margin-bottom:8px}.empty-state p{font-size:13px;color:var(--muted);margin-bottom:22px}.primary-button,.outline-button{border-radius:7px;padding:11px 14px;font-size:11px;font-weight:600;display:inline-flex;gap:7px;align-items:center;justify-content:center}.primary-button{background:var(--olive);color:var(--paper)}.outline-button{border:1px solid #d6d0bf;background:var(--paper)}.outline-button:hover{background:#eae6d9}.primary-button svg,.outline-button svg{width:16px;height:16px}.collection-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:25px 0}.collection-card{text-align:left;background:var(--blue-pale);border:1px solid #d5dced;border-radius:13px;padding:20px;min-height:155px;position:relative;overflow:hidden}.collection-card:nth-child(2){background:var(--gold);border-color:#e3d4a4}.collection-card:nth-child(3){background:var(--green);border-color:#d3dfc4}.collection-card:hover{transform:translateY(-3px);box-shadow:0 5px 0 #4a412a0d}.collection-card small{font-size:8px;letter-spacing:1px}.collection-card h3{font-family:Fraunces,serif;font-size:25px;max-width:70%;line-height:1.1;margin:14px 0 9px;font-weight:600}.collection-card p{font-size:10px;max-width:68%;line-height:1.5}.collection-card img{position:absolute;width:92px;height:90px;object-fit:cover;right:-8px;bottom:10px;border:5px solid #fff;transform:rotate(10deg);border-radius:4px;box-shadow:0 4px 10px #4a412a18}.collection-card.active{outline:2px solid var(--olive);outline-offset:3px}.mobile-nav{display:none}dialog{border:1px solid #cfc7b3;border-radius:17px;color:var(--olive);background:var(--cream);padding:0;box-shadow:0 25px 120px #18171150;max-height:92dvh;max-width:calc(100vw - 30px)}dialog::backdrop{background:#242117af;backdrop-filter:blur(5px)}body:has(dialog[open]){overflow:hidden}.viewer-dialog{width:890px}.viewer-top{display:flex;align-items:center;justify-content:space-between;padding:15px 22px;border-bottom:1px solid var(--line)}.viewer-top .eyebrow{font-size:9px}.viewer-progress{font-size:10px;color:var(--muted);margin-left:auto;margin-right:22px}.icon-button{padding:7px;border-radius:6px}.icon-button:hover{background:#e3dfd1}.viewer-body{position:relative;padding:22px 85px;min-height:240px;background:#e9e6db}.viewer-image-wrap{max-width:530px;margin:auto;box-shadow:0 5px 18px #4a412a15;border-radius:5px;overflow:hidden}.viewer-image-wrap .meme-image{height:auto;max-height:40dvh;object-fit:contain}.viewer-image-wrap .meme-caption{font-size:22px;padding:17px 22px;min-height:0}.viewer-arrow{position:absolute;top:45%;background:var(--paper);padding:12px;border:1px solid #d8d2c1;border-radius:50%;box-shadow:0 3px 7px #4a412a09}.viewer-arrow:hover{background:var(--blue)}.viewer-arrow.previous{left:20px}.viewer-arrow.next{right:20px}.viewer-details{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px 24px}.viewer-details h2{font-size:23px;line-height:1.2;margin-top:8px}.viewer-details p{font-size:10px;margin-top:7px;color:var(--muted)}.viewer-actions{display:flex;gap:7px;align-items:center;flex-shrink:0}.viewer-actions .saved{background:var(--pink);color:#854e43}.viewer-actions .saved svg{fill:var(--pink)}.viewer-bottom{padding:13px 24px;border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:12px;color:var(--muted);font-size:9px}.viewer-bottom a{text-underline-offset:3px}.viewer-bottom button{display:flex;gap:6px;align-items:center;font-size:10px}.viewer-bottom svg{width:14px;height:14px}.viewer-bottom kbd{display:inline-block;background:#e8e4d8;border:1px solid #d6cfbc;font-size:9px;padding:2px 4px;border-radius:3px;margin:0 2px}.autoplay-track{height:3px;background:transparent}.autoplay-track i{display:block;height:3px;background:var(--blue-dark);width:0}.autoplay-track i.running{animation:progress 6s linear infinite}.editor-dialog{width:870px}.dialog-heading{display:flex;align-items:center;justify-content:space-between;padding:24px 26px;border-bottom:1px solid var(--line);gap:14px}.dialog-heading .eyebrow{margin-bottom:8px;color:var(--muted)}.editor-layout{display:grid;grid-template-columns:1fr 1fr;gap:24px;padding:24px}.editor-preview{align-self:start;border:1px solid var(--line);border-radius:7px;overflow:hidden;position:sticky;top:10px}.editor-preview .meme-image{height:270px}.editor-preview .meme-caption{font-size:19px;padding:18px;min-height:62px}#editor-form{display:grid;gap:13px}#editor-form label{display:grid;gap:7px;font-size:11px;font-weight:600}#editor-form select,#editor-form textarea,#editor-form input{width:100%;border:1px solid #d7d1c1;border-radius:6px;padding:10px;background:var(--paper);color:var(--olive);font-size:12px;resize:vertical}#editor-form .upload-button{display:flex;align-items:center;justify-content:center;gap:6px;padding:10px;border:1px dashed #bcb39a;border-radius:6px;cursor:pointer;font-size:11px}#editor-form .upload-button:hover{background:#e9e4d5}.upload-button svg{width:16px;height:16px}.editor-actions{display:flex;gap:8px;padding-top:3px}.local-note{font-size:10px;color:var(--muted);line-height:1.6}.info-dialog{width:600px}#info-content{padding:25px;line-height:1.8;font-size:13px;color:#736b55}#info-content p+p{margin-top:14px}#info-content strong{color:var(--olive)}#info-content .palette{display:flex;gap:10px;margin:22px 0}#info-content .palette>div{flex:1;height:75px;border-radius:8px;display:flex;align-items:end;justify-content:center;padding:8px;font-size:9px;border:1px solid #cfc8b5}.shortcut-table{width:100%;border-collapse:collapse}.shortcut-table td{border-bottom:1px solid var(--line);padding:10px 4px}.shortcut-table td:first-child{font-family:monospace;font-weight:700;color:var(--olive);width:130px}.toast{position:fixed;bottom:28px;left:calc(50% + 110px);transform:translate(-50%,15px);opacity:0;pointer-events:none;z-index:99;background:var(--olive);color:var(--paper);border:1px solid #7d7359;box-shadow:0 5px 22px #25200d30;border-radius:9px;padding:13px 22px;font-size:12px;transition:transform .2s,opacity .2s;max-width:85vw;text-align:center}.toast.visible{opacity:1;transform:translate(-50%,0)}@keyframes float-cat{0%,100%{transform:rotate(-9deg) translateY(0)}50%{transform:rotate(-7deg) translateY(-6px)}}@keyframes card-in{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:translateY(0)}}@keyframes progress{from{width:0}to{width:100%}} +:root{--olive:#4a412a;--olive-dark:#332e20;--olive-soft:#797052;--cream:#f7f5ee;--paper:#fffef9;--line:#e4e0d4;--blue:#b9c5f3;--blue-dark:#424f82;--blue-pale:#e9edf9;--muted:#645e4c;--green:#e3ebd7;--pink:#f4e0db;--gold:#eee2b7;--radius:16px;font-family:'DM Sans',sans-serif;color:var(--olive);background:var(--cream);font-synthesis:none;scroll-behavior:smooth} +*{box-sizing:border-box}body{margin:0}button,input,select,textarea{font:inherit}button,a,input,select,textarea{-webkit-tap-highlight-color:transparent}button{cursor:pointer}button{color:inherit}a{color:inherit}button:disabled{cursor:wait;opacity:.5}button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid var(--blue-dark);outline-offset:4px}button{transition:background .16s,transform .16s,box-shadow .16s}button:active{transform:translateY(1px)}button svg,a svg{pointer-events:none}svg.icon{width:19px;height:19px;stroke:currentColor;fill:none;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;display:block;flex-shrink:0}[data-icon]{display:inline-flex}button{border:0;background:none}h1,h2,h3,p{margin:0}[hidden]{display:none!important}.skip-link{position:fixed;top:-80px;left:20px;background:var(--olive);color:white;padding:12px;z-index:100}.skip-link:focus{top:10px}.sidebar{width:220px;position:fixed;inset:0 auto 0 0;padding:31px 22px 22px;background:#efede3;border-right:1px solid #e0ddcf;display:flex;flex-direction:column;z-index:20;overflow-y:auto;overscroll-behavior:contain}.brand{display:flex;gap:9px;align-items:center;text-decoration:none;font-size:17px;font-weight:800;letter-spacing:-.8px;line-height:1.05}.brand img{width:54px;height:54px;image-rendering:pixelated}.brand-bottom{display:block;font-size:27px;letter-spacing:1.3px}.brand-dot{font-size:11px;vertical-align:top;letter-spacing:0;margin-left:3px}.side-tagline{font-size:12px;color:var(--muted);line-height:1.6;margin:18px 0 34px 8px}.nav-label{font-size:9px;font-weight:700;letter-spacing:1.4px;color:#8b8775;margin:0 8px 12px}.main-nav{display:grid;gap:5px}.nav-item{display:flex;gap:12px;align-items:center;width:100%;padding:12px 11px;border-radius:9px;text-align:left;font-size:12px;font-weight:600;min-height:43px}.nav-item:hover{background:#e5e2d4}.nav-item.active{background:var(--olive);color:#fffdf4}.nav-count{font-size:10px;margin-left:auto;opacity:.8;background:#dfdccd;padding:2px 6px;border-radius:5px;min-width:21px;text-align:center}.active .nav-count{background:#6b6046;color:#fff}.side-divider{height:1px;background:#dcd8c8;margin:26px 8px}.keycap{font-size:9px;border:1px solid #c9c4b2;border-radius:4px;padding:1px 4px;margin-left:auto}.side-bottom{margin-top:auto;padding-top:35px}.chimp-note{position:relative;border:1px solid #d8d3bf;border-radius:13px;padding:19px 12px 28px;background:#e8e4d3;overflow:hidden}.chimp-note p{position:relative;z-index:1;font-size:11px;line-height:1.65}.chimp-note em{font-weight:700;font-style:normal}.chimp-note img{position:absolute;width:66px;right:-7px;bottom:-6px;transform:rotate(-14deg);opacity:.72}.tiny-spark{font-size:20px;color:var(--olive-soft);display:block;margin-bottom:12px}.about-link{display:flex;gap:7px;align-items:center;font-size:9px;padding:16px 0;width:100%;justify-content:center}.swatch{background:var(--olive);width:10px;height:10px;border-radius:2px;display:inline-block}.shortcut-link{display:flex;align-items:center;gap:7px;font-size:10px;color:var(--muted);padding:10px 4px;width:100%;border-top:1px solid #ddd9cb}.shortcut-link svg{width:16px}.shortcut-link>span:last-child{margin-left:auto}.workspace{margin-left:220px}.topbar{height:83px;padding:0 38px;display:flex;align-items:center;justify-content:space-between;gap:20px;border-bottom:1px solid var(--line);background:var(--cream)}.breadcrumb{display:flex;align-items:center;gap:10px;font-size:9px;font-weight:700;letter-spacing:1.5px;white-space:nowrap}.status-dot{width:6px;height:6px;border-radius:100%;background:var(--olive-soft);box-shadow:0 0 0 4px #eae7da}.edition{color:#969181;padding-left:10px;border-left:1px solid #d7d1c2;letter-spacing:1px}.top-actions{display:flex;gap:13px;align-items:center}.search-wrap{display:flex;align-items:center;gap:9px;border:1px solid #dfdbcf;background:#eeece3;border-radius:8px;padding:9px 11px;width:253px}.search-wrap svg{width:16px;height:16px;color:#7b7664}.search-wrap input{width:100%;min-width:0;border:0;background:transparent;outline:0;font-size:11px;color:var(--olive)}.search-wrap:focus-within{border-color:var(--olive-soft);box-shadow:0 0 0 2px #e0dccb}.search-wrap kbd{font-size:10px;border:1px solid #d5d0c0;padding:0 4px;border-radius:3px}.create-button{display:flex;gap:7px;align-items:center;background:var(--olive);color:var(--cream);padding:11px 13px;border-radius:8px;font-size:11px;font-weight:600;white-space:nowrap}.create-button:hover,.primary-button:hover{background:#62573b}.create-button svg{width:16px;height:16px}.mobile-home{display:none}main{max-width:1490px;margin:auto;padding:30px 38px 0}.hero{min-height:315px;border-radius:18px;position:relative;background:var(--olive);color:var(--cream);overflow:hidden;display:flex;align-items:center;padding:35px 34px}.hero-copy{width:60%;position:relative;z-index:2}.eyebrow{font-size:9px;letter-spacing:1.6px;font-weight:700;display:flex;align-items:center;gap:7px}.hero .eyebrow{color:#d3cbb1;font-size:8px;letter-spacing:1.5px}.little-star{font-size:15px;color:var(--blue)}h1{font-family:Fraunces,Georgia,serif;font-size:clamp(38px,4.1vw,63px);letter-spacing:-2px;line-height:1.08;font-weight:600;margin-top:14px}h1 em{color:var(--blue);font-weight:600}.hero-copy p{font-size:11px;line-height:1.8;color:#d7d1c0;margin:15px 0 20px}.hero-button{display:inline-flex;align-items:center;gap:21px;padding:12px 15px;background:var(--blue);color:#303952;font-size:11px;font-weight:700;border-radius:7px}.hero-button:hover{background:#d5dcf7;transform:translateY(-2px)}.hero-button svg{width:16px;height:16px}.hero-footnote{font-size:9px;color:#b2aa92;margin-left:13px}.hero-art{position:absolute;right:21px;width:39%;top:0;height:100%}.hero-ring{position:absolute;width:269px;height:269px;border:1px solid #756a4c;border-radius:100%;top:34px;right:4px;transform:rotate(-20deg)}.hero-ring:after{content:'';position:absolute;inset:22px -30px;border:1px solid #756a4c;border-radius:100%;transform:rotate(60deg)}.hero-sticker{background:var(--paper);padding:9px 9px 7px;width:176px;border-radius:5px;position:absolute;box-shadow:0 10px 26px #211d2340;color:var(--olive)}.sticker-caption{font-size:12px;font-weight:700;line-height:1.35;padding:2px 2px 8px}.hero-sticker img{width:100%;display:block;object-fit:cover;border-radius:2px;max-height:139px}.back-sticker{right:4px;top:30px;transform:rotate(12deg);background:#dfe7ff}.back-sticker img{height:135px;object-fit:contain;background:#fff}.front-sticker{left:1px;top:72px;transform:rotate(-9deg);width:194px;animation:float-cat 7s ease-in-out infinite}.front-sticker img{height:131px;object-position:center}.sticker-footer{font-size:8px;display:flex;justify-content:space-between;align-items:center;padding-top:8px}.sticker-footer span{font-size:18px}.approval-sticker{width:82px;height:82px;position:absolute;background:var(--gold);color:var(--olive);right:4px;bottom:34px;border-radius:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;transform:rotate(13deg);font-family:Fraunces,serif;font-size:26px;box-shadow:0 0 0 4px var(--olive),0 0 0 5px #a69a75}.approval-sticker span{font-family:'DM Sans',sans-serif;font-size:7px;letter-spacing:1px;font-weight:700}.approval-sticker .mini-paw{font-size:17px}.art-star{position:absolute;font-size:40px;left:-21px;top:25px;color:var(--blue)}.hero-doodle{position:absolute;right:58px;bottom:14px;font-size:9px;color:#c8bea0;font-style:italic;transform:rotate(3deg)}.library{margin-top:34px;scroll-margin-top:20px}.library-heading{display:flex;justify-content:space-between;align-items:center;gap:20px;margin-bottom:20px}.section-eyebrow{color:var(--muted);font-size:8px;letter-spacing:1.5px;margin-bottom:8px}h2{font-family:Fraunces,Georgia,serif;font-size:27px;font-weight:600;letter-spacing:-.7px}h2>span{color:#8f9dcf}.text-button{display:flex;align-items:center;gap:8px;font-size:11px;font-weight:500;white-space:nowrap;padding:9px 0}.text-button:hover{color:var(--blue-dark)}.text-button svg{width:16px;height:16px}.mood-filters{display:flex;gap:7px;flex-wrap:wrap}.mood-filter{border:1px solid var(--line);display:flex;align-items:center;gap:7px;border-radius:30px;padding:9px 14px;font-size:11px;background:var(--paper);font-weight:500}.mood-filter span{font-size:12px}.mood-filter:hover{border-color:var(--olive-soft);background:#eeeadd}.mood-filter.active{background:var(--olive);border-color:var(--olive);color:var(--cream)}.filter-row{padding-bottom:18px;border-bottom:1px solid var(--line)}.results-bar{display:flex;justify-content:space-between;align-items:center;padding:17px 0;color:var(--muted);font-size:10px;gap:10px}.results-bar strong{color:var(--olive);font-weight:600}.result-controls{display:flex;gap:14px;align-items:center}.sort-label{display:flex;gap:5px;align-items:center;white-space:nowrap}.sort-label select{border:0;background:transparent;color:var(--olive);font-size:10px;font-weight:600;cursor:pointer;max-width:125px}.view-toggle{display:flex;gap:1px;background:#eae7dd;padding:3px;border-radius:6px}.view-toggle button{padding:4px;border-radius:4px;color:#999380}.view-toggle button.selected{background:var(--paper);color:var(--olive);box-shadow:0 1px 3px #42392012}.view-toggle svg{width:14px;height:14px}.meme-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:21px;align-items:start}.meme-grid.dense{grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.meme-card{background:var(--paper);border:1px solid var(--line);border-radius:13px;overflow:hidden;transition:transform .2s,box-shadow .2s,border-color .2s;animation:card-in .35s both}.meme-card:hover{transform:translateY(-4px);box-shadow:0 8px 22px #4a412a10;border-color:#bfb8a1}.meme-open{display:block;width:100%;padding:0;text-align:left}.meme-visual{background:#fff;color:#24231e;display:flex;flex-direction:column;overflow:hidden}.meme-caption{font-size:clamp(13px,1.2vw,19px);font-weight:700;line-height:1.3;padding:15px 17px 11px;min-height:59px;display:flex;align-items:center;white-space:pre-wrap;overflow-wrap:anywhere}.meme-caption.bottom{font-size:clamp(12px,1.08vw,17px);padding:12px 17px 15px;min-height:54px}.meme-image{width:100%;height:215px;object-fit:contain;display:block;background:#f4f2ed}.dense .meme-image{height:168px}.dense .meme-caption{font-size:14px;padding:12px;min-height:56px}.dense .meme-caption.bottom{font-size:13px;min-height:58px}.card-meta{padding:13px 14px 12px;border-top:1px solid #efede5}.card-topline{display:flex;justify-content:space-between;gap:8px;align-items:center}.mood-label{font-size:8px;font-weight:600;padding:4px 7px;border-radius:4px;display:inline-flex;align-items:center;gap:4px;letter-spacing:.2px;background:var(--blue-pale);color:var(--blue-dark)}.mood-label[data-mood=chaos]{background:var(--gold);color:#6f5822}.mood-label[data-mood=wholesome]{background:var(--green);color:#4c653b}.mood-label[data-mood=judgment]{background:var(--pink);color:#82564b}.mood-label[data-mood=sleepy]{background:#eae3f2;color:#6a537f}.mood-label[data-mood=snacks]{background:#f4e7d7;color:#85602f}.card-source{font-size:8px;color:#a09b8d}.card-bottomline{display:flex;align-items:center;justify-content:space-between;gap:6px;margin-top:8px}.card-title{font-size:11px;font-weight:600;line-height:1.4;margin:0;flex:1;overflow-wrap:anywhere}.save-button{color:#918876;padding:5px;border-radius:6px;position:relative}.save-button:hover{background:var(--pink);color:#9c6055}.save-button.saved{color:#9e5c53}.save-button.saved svg{fill:#eed2cc}.save-button svg{width:18px;height:18px}.load-more{display:flex;align-items:center;justify-content:center;gap:16px;padding:13px 24px;margin:30px auto 0;border:1px solid #ccc5b1;border-radius:7px;font-size:11px;font-weight:600;background:var(--paper)}.load-more:hover{background:#e9e4d5}.load-more svg{width:15px;height:15px}.end-note{display:flex;gap:14px;align-items:center;justify-content:center;padding:38px 0 8px;color:var(--muted);font-size:11px;text-align:left;line-height:1.8}.end-note>span{font-size:29px;color:var(--olive-soft)}.end-note button{padding:0;text-decoration:underline;font-size:10px}footer{padding:30px 0 20px;border-top:1px solid var(--line);margin-top:36px;display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:10px;color:#8a8471}footer>span{display:flex;align-items:center;gap:8px}footer img{width:22px;opacity:.7}footer button{font-size:10px;padding:0}.page-description{font-size:12px;line-height:1.7;color:var(--muted);margin-top:9px}.empty-state{text-align:center;padding:65px 20px}.empty-face{font-size:66px;display:block;color:#958968;margin-bottom:18px}.empty-state h3{font-family:Fraunces,Georgia,serif;font-size:28px;margin-bottom:8px}.empty-state p{font-size:13px;color:var(--muted);margin-bottom:22px}.primary-button,.outline-button{border-radius:7px;padding:11px 14px;font-size:11px;font-weight:600;display:inline-flex;gap:7px;align-items:center;justify-content:center}.primary-button{background:var(--olive);color:var(--paper)}.outline-button{border:1px solid #d6d0bf;background:var(--paper)}.outline-button:hover{background:#eae6d9}.primary-button svg,.outline-button svg{width:16px;height:16px}.collection-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:25px 0}.collection-card{text-align:left;background:var(--blue-pale);border:1px solid #d5dced;border-radius:13px;padding:20px;min-height:155px;position:relative;overflow:hidden}.collection-card:nth-child(2){background:var(--gold);border-color:#e3d4a4}.collection-card:nth-child(3){background:var(--green);border-color:#d3dfc4}.collection-card:hover{transform:translateY(-3px);box-shadow:0 5px 0 #4a412a0d}.collection-card small{font-size:8px;letter-spacing:1px}.collection-card h3{font-family:Fraunces,serif;font-size:25px;max-width:70%;line-height:1.1;margin:14px 0 9px;font-weight:600}.collection-card p{font-size:10px;max-width:68%;line-height:1.5}.collection-card img{position:absolute;width:92px;height:90px;object-fit:cover;right:-8px;bottom:10px;border:5px solid #fff;transform:rotate(10deg);border-radius:4px;box-shadow:0 4px 10px #4a412a18}.collection-card.active{outline:2px solid var(--olive);outline-offset:3px}.mobile-nav{display:none}dialog{border:1px solid #cfc7b3;border-radius:17px;color:var(--olive);background:var(--cream);padding:0;box-shadow:0 25px 120px #18171150;max-height:92dvh;max-width:calc(100vw - 30px)}dialog::backdrop{background:#242117af;backdrop-filter:blur(5px)}body:has(dialog[open]){overflow:hidden}.viewer-dialog{width:890px}.viewer-top{display:flex;align-items:center;justify-content:space-between;padding:15px 22px;border-bottom:1px solid var(--line)}.viewer-top .eyebrow{font-size:9px}.viewer-progress{font-size:10px;color:var(--muted);margin-left:auto;margin-right:22px}.icon-button{padding:7px;border-radius:6px}.icon-button:hover{background:#e3dfd1}.viewer-body{position:relative;padding:22px 85px;min-height:240px;background:#e9e6db}.viewer-image-wrap{max-width:530px;margin:auto;box-shadow:0 5px 18px #4a412a15;border-radius:5px;overflow:hidden}.viewer-image-wrap .meme-image{height:auto;max-height:40dvh;object-fit:contain}.viewer-image-wrap .meme-caption{font-size:22px;padding:17px 22px;min-height:0}.viewer-arrow{position:absolute;top:45%;background:var(--paper);padding:12px;border:1px solid #d8d2c1;border-radius:50%;box-shadow:0 3px 7px #4a412a09}.viewer-arrow:hover{background:var(--blue)}.viewer-arrow.previous{left:20px}.viewer-arrow.next{right:20px}.viewer-details{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px 24px}.viewer-details h2{font-size:23px;line-height:1.2;margin-top:8px}.viewer-details p{font-size:10px;margin-top:7px;color:var(--muted)}.viewer-actions{display:flex;gap:14px;align-items:center;flex-shrink:0;flex-wrap:wrap}.viewer-actions .saved{background:var(--pink);color:#854e43}.viewer-actions .saved svg{fill:var(--pink)}.viewer-bottom{padding:13px 24px;border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;gap:12px;color:var(--muted);font-size:9px}.viewer-bottom a{text-underline-offset:3px}.viewer-bottom button{display:flex;gap:6px;align-items:center;font-size:10px}.viewer-bottom svg{width:14px;height:14px}.viewer-bottom kbd{display:inline-block;background:#e8e4d8;border:1px solid #d6cfbc;font-size:9px;padding:2px 4px;border-radius:3px;margin:0 2px}.autoplay-track{height:3px;background:transparent}.autoplay-track i{display:block;height:3px;background:var(--blue-dark);width:0}.autoplay-track i.running{animation:progress 6s linear infinite}.editor-dialog{width:870px}.dialog-heading{display:flex;align-items:center;justify-content:space-between;padding:24px 26px;border-bottom:1px solid var(--line);gap:14px}.dialog-heading .eyebrow{margin-bottom:8px;color:var(--muted)}.editor-layout{display:grid;grid-template-columns:1fr 1fr;gap:24px;padding:24px}.editor-preview{align-self:start;border:1px solid var(--line);border-radius:7px;overflow:hidden;position:sticky;top:10px}.editor-preview .meme-image{height:270px}.editor-preview .meme-caption{font-size:19px;padding:18px;min-height:62px}#editor-form{display:grid;gap:13px}#editor-form label{display:grid;gap:7px;font-size:11px;font-weight:600}#editor-form select,#editor-form textarea,#editor-form input{width:100%;border:1px solid #d7d1c1;border-radius:6px;padding:10px;background:var(--paper);color:var(--olive);font-size:12px;resize:vertical}#editor-form .upload-button{display:flex;align-items:center;justify-content:center;gap:6px;padding:10px;border:1px dashed #bcb39a;border-radius:6px;cursor:pointer;font-size:11px}#editor-form .upload-button:hover{background:#e9e4d5}.upload-button svg{width:16px;height:16px}.editor-actions{display:flex;gap:12px;padding-top:3px;flex-wrap:wrap}.local-note{font-size:10px;color:var(--muted);line-height:1.6}.info-dialog{width:600px}#info-content{padding:25px;line-height:1.8;font-size:13px;color:#736b55}#info-content p+p{margin-top:14px}#info-content strong{color:var(--olive)}#info-content .palette{display:flex;gap:10px;margin:22px 0}#info-content .palette>div{flex:1;height:75px;border-radius:8px;display:flex;align-items:end;justify-content:center;padding:8px;font-size:9px;border:1px solid #cfc8b5}.shortcut-table{width:100%;border-collapse:collapse}.shortcut-table td{border-bottom:1px solid var(--line);padding:10px 4px}.shortcut-table td:first-child{font-family:monospace;font-weight:700;color:var(--olive);width:130px}.toast{position:fixed;bottom:28px;left:calc(50% + 110px);transform:translate(-50%,15px);opacity:0;pointer-events:none;z-index:99;background:var(--olive);color:var(--paper);border:1px solid #7d7359;box-shadow:0 5px 22px #25200d30;border-radius:9px;padding:13px 22px;font-size:12px;transition:transform .2s,opacity .2s;max-width:85vw;text-align:center}.toast.visible{opacity:1;transform:translate(-50%,0)}@keyframes float-cat{0%,100%{transform:rotate(-9deg) translateY(0)}50%{transform:rotate(-7deg) translateY(-6px)}}@keyframes card-in{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:translateY(0)}}@keyframes progress{from{width:0}to{width:100%}} @media(min-width:1700px){.meme-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.meme-grid.dense{grid-template-columns:repeat(5,minmax(0,1fr))}.hero-art{width:330px;right:65px}.hero{padding-left:43px}.hero-copy{width:70%}.hero .eyebrow{font-size:10px}.hero-copy p{font-size:13px}.hero-button{font-size:12px}.meme-caption{font-size:17px}.meme-caption.bottom{font-size:16px}} @media(max-width:1150px){.sidebar{width:190px;padding:25px 15px}.workspace{margin-left:190px}.brand{font-size:14px}.brand img{width:48px}.brand-bottom{font-size:24px}.topbar{padding:0 25px}.breadcrumb .edition{display:none}main{padding:25px 25px 0}.hero{padding:28px}.hero-copy{width:61%}.hero-art{width:34%;right:18px}.hero-sticker{width:150px}.front-sticker{width:162px;left:-19px}.hero-footnote{display:block;margin:11px 0 0}.hero-doodle{display:none}.hero-ring{width:210px;height:240px}.approval-sticker{right:-4px;bottom:37px;width:69px;height:69px;font-size:22px}.hero-copy p{font-size:10px}.meme-grid{gap:15px}.meme-image{height:180px}.meme-caption{font-size:14px;padding:12px}.meme-caption.bottom{font-size:13px;padding:12px}.meme-grid.dense{grid-template-columns:repeat(3,minmax(0,1fr))}.search-wrap{width:220px}.toast{left:calc(50% + 95px)}} @media(max-width:900px){.sidebar{width:174px;padding:23px 11px}.workspace{margin-left:174px}.brand img{width:41px}.brand{gap:6px;font-size:13px}.brand-bottom{font-size:22px}.side-tagline{margin-bottom:27px}.nav-item{font-size:11px;gap:8px;padding:11px 9px}.nav-label{font-size:8px;letter-spacing:1px}.breadcrumb{display:none}.top-actions{width:100%;justify-content:space-between}.search-wrap{width:65%;max-width:310px}.hero{min-height:305px}.hero-copy{width:70%}.hero-art{width:28%;opacity:.9}.back-sticker{display:none}.front-sticker{width:135px;left:-15px;top:105px}.sticker-caption{font-size:10px}.sticker-footer{font-size:7px}.hero-sticker img{height:110px}.approval-sticker{bottom:23px;right:-5px;width:57px;height:57px;font-size:19px}.approval-sticker span{font-size:5px}.hero-ring{width:180px;right:-25px}.art-star{left:26px;top:46px}.hero-copy p{max-width:80%}.hero-copy p br{display:none}.hero .eyebrow{font-size:7px;letter-spacing:.9px}.hero-footnote{display:none}h1{font-size:42px}.library-heading h2{font-size:23px}.text-button{font-size:10px}.meme-grid,.meme-grid.dense{grid-template-columns:repeat(2,minmax(0,1fr))}.mood-filter{padding:8px 11px;font-size:10px}.hero-button{gap:10px}.collection-grid{grid-template-columns:1fr}.collection-card{min-height:120px}.collection-card h3{max-width:80%;margin:9px 0}.collection-card img{right:18px}.collection-card p{max-width:80%}.toast{left:calc(50% + 87px)}.viewer-details{align-items:start;flex-direction:column}.viewer-actions{width:100%;justify-content:end}.viewer-dialog{max-height:95dvh}.editor-layout{gap:18px;padding:20px}.viewer-image-wrap .meme-image{max-height:36dvh}} @media(max-width:650px){.sidebar{display:none}.workspace{margin-left:0}.topbar{height:71px;padding:0 17px}.mobile-home{display:block;padding:0}.mobile-home img{width:37px;height:40px}.top-actions{gap:9px}.search-wrap{padding:9px;width:auto;flex:1}.search-wrap input{font-size:10px}.search-wrap kbd{display:none}.create-button{padding:10px}.create-button>span:last-child{display:none}.create-button svg{width:18px}.mobile-nav{display:flex;position:fixed;bottom:0;left:0;right:0;background:#efede3f5;backdrop-filter:blur(10px);padding:8px 10px max(9px,env(safe-area-inset-bottom));border-top:1px solid #d9d4c3;z-index:21;justify-content:space-around}.mobile-nav button{display:flex;flex-direction:column;align-items:center;gap:4px;font-size:9px;padding:5px 17px;border-radius:7px;color:#8a826b}.mobile-nav button.active{color:var(--olive);background:#e0dccb}.mobile-nav svg{width:19px;height:19px}main{padding:18px 17px 80px}.hero{padding:24px 21px;min-height:300px;border-radius:13px}.hero-copy{width:100%}.hero .eyebrow{font-size:7px;letter-spacing:.8px}.hero .eyebrow .little-star{font-size:11px}h1{font-size:39px;letter-spacing:-1.5px;margin-top:13px}.hero-copy p{font-size:10px;max-width:62%;margin-top:13px;margin-bottom:17px;line-height:1.6}.hero-button{font-size:10px;padding:11px 12px;gap:10px}.hero-art{right:0;width:32%;z-index:2;pointer-events:none}.front-sticker{width:111px;left:-5px;top:146px;padding:6px;transform:rotate(-8deg);animation:none}.front-sticker img{height:80px}.sticker-caption{font-size:9px;padding:0 2px 5px}.sticker-footer{font-size:6px;padding-top:4px}.sticker-footer span{font-size:14px}.hero-ring{width:145px;height:220px;top:90px;right:-50px;opacity:.6}.approval-sticker{width:49px;height:49px;right:13px;bottom:15px;font-size:16px}.approval-sticker span{font-size:5px}.approval-sticker .mini-paw{display:none}.art-star{font-size:28px;top:106px;left:40px}.library{margin-top:27px}.library-heading{gap:10px;margin-bottom:17px;align-items:end}.library-heading h2{font-size:23px;line-height:1.16;max-width:230px}.library-heading .text-button{font-size:0;padding:5px;background:#e9e5d9;border:1px solid #d9d2c2;border-radius:6px}.library-heading .text-button svg{width:17px;height:17px}.section-eyebrow{font-size:7px;margin-bottom:6px}.mood-filters{gap:6px;flex-wrap:nowrap;overflow:auto;padding:1px 1px 7px;scrollbar-width:none;margin-right:-17px;padding-right:17px}.mood-filter{white-space:nowrap;font-size:10px;padding:8px 12px}.filter-row{padding-bottom:8px}.results-bar{font-size:9px;padding:14px 0}.sort-label{font-size:0;gap:0}.sort-label select{font-size:9px;width:105px}.result-controls{gap:5px}.view-toggle button{padding:3px}.view-toggle svg{width:12px;height:12px}.meme-grid{gap:12px;grid-template-columns:repeat(2,minmax(0,1fr))}.meme-card{border-radius:9px}.meme-image{height:145px}.meme-caption{font-size:12px;padding:11px 10px;min-height:60px;line-height:1.3}.meme-caption.bottom{font-size:11px;padding:10px;min-height:54px}.card-meta{padding:10px 9px}.card-source{display:none}.card-title{font-size:10px;line-height:1.4}.card-bottomline{align-items:start}.save-button{padding:3px}.save-button svg{width:16px;height:16px}.mood-label{font-size:7px;padding:4px 5px}.meme-grid.dense{grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.dense .meme-image{height:100px}.dense .meme-caption,.dense .meme-caption.bottom{font-size:10px;padding:8px;min-height:57px}.dense .card-title{font-size:9px}.dense .card-meta{padding:8px 6px}.dense .card-bottomline{flex-wrap:wrap}.dense .mood-label{font-size:6px}.dense .save-button{padding:2px}.dense .save-button svg{width:14px}.load-more{margin-top:24px;font-size:11px}.end-note{font-size:10px}.end-note>span{font-size:22px}footer{font-size:8px;gap:10px;padding:22px 0 3px;flex-wrap:wrap}footer button{font-size:9px}.toast{left:50%;bottom:85px;width:max-content;max-width:90vw;font-size:11px}.viewer-dialog{max-width:calc(100vw - 16px);max-height:96dvh;border-radius:12px}.viewer-top{padding:10px 12px}.viewer-top .eyebrow{font-size:8px}.viewer-body{padding:12px 37px}.viewer-arrow{padding:6px}.viewer-arrow svg{width:16px;height:16px}.viewer-arrow.previous{left:4px}.viewer-arrow.next{right:4px}.viewer-image-wrap .meme-caption{font-size:16px;padding:14px}.viewer-image-wrap .meme-image{max-height:33dvh}.viewer-details{padding:15px;gap:13px}.viewer-details h2{font-size:21px}.viewer-details p{font-size:9px}.viewer-actions{justify-content:start;gap:6px;flex-wrap:wrap}.viewer-actions .primary-button,.viewer-actions .outline-button{padding:9px 11px;font-size:10px}.viewer-bottom{padding:11px 15px;font-size:9px;flex-wrap:wrap}.viewer-bottom>span{display:none}.editor-dialog{max-width:calc(100vw - 16px);max-height:95dvh}.dialog-heading{padding:18px}.dialog-heading h2{font-size:24px}.dialog-heading .eyebrow{font-size:8px}.editor-layout{grid-template-columns:1fr;padding:18px}.editor-preview{position:static;max-width:280px;margin:auto;width:100%}.editor-preview .meme-image{height:175px}.editor-preview .meme-caption{font-size:15px;padding:12px;min-height:40px}#editor-form{gap:14px}.info-dialog{max-width:calc(100vw - 24px)}#info-content{padding:20px;font-size:12px}.empty-state{padding:45px 10px}.empty-state h3{font-size:25px}.collection-card p{max-width:70%}.hero-copy .hero-button{max-width:70%}} @media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important;scroll-behavior:auto!important}.meme-card:hover{transform:none}.autoplay-track i.running{width:100%}} +.arena-stats{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:18px}.stat-chip{background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:10px 14px;font-size:11px;font-weight:600;min-width:110px;box-shadow:0 2px 0 #4a412a10}.stat-chip strong{font-size:19px;display:block;font-family:Fraunces,Georgia,serif}.battle-grid{display:grid;grid-template-columns:1fr 64px 1fr;gap:14px;align-items:stretch;margin-bottom:14px}.battle-card{background:var(--paper);border:2px solid var(--line);border-radius:13px;overflow:hidden;text-align:left;padding:0;display:flex;flex-direction:column;animation:card-in .3s both;box-shadow:0 3px 0 #4a412a12}.battle-card:hover{transform:translateY(-4px);border-color:var(--olive);box-shadow:0 8px 22px #4a412a18}.battle-card:active{transform:translateY(1px)}.battle-card:focus-visible{outline:3px solid var(--blue-dark);outline-offset:3px}.battle-side{font-size:10px;font-weight:800;letter-spacing:1px;padding:9px 12px 0;color:var(--muted)}.battle-card .meme-visual{border-top:1px solid #f0ede2;margin-top:8px}.battle-meta{padding:12px 13px 14px;display:grid;gap:4px}.battle-meta .mood-label{justify-self:start}.battle-title{font-size:14px;font-weight:700;line-height:1.25}.battle-record{font-size:11px;font-weight:700;color:var(--olive)}.rate-bar{display:block;height:7px;background:#e9e4d4;border-radius:99px;overflow:hidden;margin-top:6px}.rate-bar i{display:block;height:100%;background:linear-gradient(90deg,var(--blue-dark),#7c8bd4);border-radius:99px}.rate-bar.small{width:70px;margin:0}.battle-cta{margin-top:8px;background:var(--olive);color:var(--paper);border-radius:7px;padding:9px 10px;font-size:11px;font-weight:700;text-align:center}.battle-card:hover .battle-cta{background:#62573b}.battle-vs{align-self:center;justify-self:center;font-family:Fraunces,Georgia,serif;font-weight:700;font-size:18px;background:var(--olive);color:var(--cream);width:54px;height:54px;border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px #4a412a30;animation:vs-pulse 2.2s ease-in-out infinite}@keyframes vs-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.1) rotate(-4deg)}}.arena-actions{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-bottom:20px}.arena-actions .text-button{font-size:11px;text-decoration:underline;color:var(--muted)}.arena-hint{font-size:11px;color:var(--muted)}.arena-hint kbd{background:var(--paper);border:1px solid var(--line);border-bottom-width:2px;border-radius:5px;padding:1px 6px;font-size:10px;font-family:inherit}.arena-intro{background:var(--olive);color:#f7f5ee;border-radius:13px;padding:18px 20px;margin-bottom:18px;box-shadow:0 6px 18px #4a412a22}.arena-intro h3{font-family:Fraunces,Georgia,serif;font-size:19px;margin-bottom:10px}.arena-intro ol{margin:0;padding-left:19px;display:grid;gap:7px;font-size:12px;line-height:1.55}.arena-intro kbd{background:#f7f5ee22;border:1px solid #f7f5ee55;border-radius:5px;padding:1px 7px;font-size:10px;font-family:inherit}.last-crowned{background:var(--gold);border:1px solid #e3d4a4;border-radius:10px;padding:10px 14px;font-size:12px;margin-bottom:14px}.last-crowned button{font-weight:800;text-decoration:underline}.leaderboard{background:var(--paper);border:1px solid var(--line);border-radius:13px;padding:16px 16px 8px}.leaderboard h3{font-family:Fraunces,Georgia,serif;font-size:21px;font-weight:600}.leader-sub{font-size:11px;color:var(--muted);margin:4px 0 8px}.leader-row{display:flex;align-items:center;gap:10px;padding:9px 0;border-top:1px solid var(--line);font-size:12px}.leader-row:hover{background:#faf8f1}.leader-row img{width:44px;height:44px;object-fit:cover;border-radius:8px;background:#f4f2ed}.leader-medal{width:28px;text-align:center;font-weight:800}.leader-name{font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.leader-score{font-size:11px;color:var(--muted);white-space:nowrap}.leader-open{font-size:11px;font-weight:700;text-decoration:underline}.leader-empty{font-size:12px;color:var(--muted);padding:10px 0 14px} +.hall-title{margin-top:14px} +.podium{display:grid;grid-template-columns:1fr 1.25fr 1fr;gap:10px;align-items:end;margin:12px 0 6px} +.podium-card{background:var(--paper);border:2px solid var(--line);border-radius:12px;padding:12px 8px 10px;display:grid;gap:5px;justify-items:center;text-align:center} +.podium-card:hover{transform:translateY(-3px);box-shadow:0 8px 20px #4a412a18} +.podium-card img{width:100%;max-width:110px;aspect-ratio:1;object-fit:cover;border-radius:9px;background:#f4f2ed} +.podium-card.place-1{border-color:#d9c57c;background:#fbf6e3;box-shadow:0 6px 20px #d9c57c44;padding-top:16px} +[data-theme="dark"] .podium-card.place-1{background:#2e2a18;border-color:#6b5f30} +.podium-card.place-1 img{max-width:140px} +.podium-medal{font-size:26px} +.podium-card.place-1 .podium-medal{font-size:34px} +.podium-name{font-size:11px;font-weight:700;line-height:1.3} +.podium-card .leader-score{font-size:10px}@media(max-width:650px){.battle-grid{grid-template-columns:1fr;gap:10px}.battle-vs{width:46px;height:46px;font-size:15px}.arena-stats{display:grid;grid-template-columns:repeat(3,1fr)}.stat-chip{min-width:0;padding:8px 10px}.mobile-nav button{padding:5px 10px;font-size:8px;flex:1}}@media(prefers-reduced-motion:reduce){.battle-card,.battle-card:hover{transform:none;animation:none}.battle-vs{animation:none}} +[data-theme="dark"]{--cream:#171410;--paper:#221d15;--line:#3a3222;--olive:#ece2cb;--olive-dark:#f6efdd;--olive-soft:#8d8266;--muted:#a2967c;--blue:#9fadf0;--blue-dark:#b3bef5;--blue-pale:#252b45;--green:#2b3323;--pink:#3a2b25;--gold:#38311e} +[data-theme="dark"] .sidebar{background:#1f1a13;border-color:#332c1f} +[data-theme="dark"] .hero{background:#4A412A;color:#F7F5EE} +[data-theme="dark"] .hero .eyebrow{color:#d3cbb1} +[data-theme="dark"] .nav-item:hover{background:#2a2417} +[data-theme="dark"] .nav-item.active{background:#ece2cb;color:#221d15} +[data-theme="dark"] .nav-count{background:#38311f} +[data-theme="dark"] .active .nav-count{background:#ece2cb;color:#221d15} +[data-theme="dark"] .side-divider{background:#332c1f} +[data-theme="dark"] .search-wrap{background:#221d15} +[data-theme="dark"] dialog{border-color:#3a3222} +[data-theme="dark"] .outline-button{border-color:#4a4230} +[data-theme="dark"] .outline-button:hover{background:#2c2617} +[data-theme="dark"] .collection-card{background:#282219!important;border-color:#3a3222!important} +[data-theme="dark"] .collection-card:nth-child(2){background:#2e2a18!important;border-color:#453d22!important} +[data-theme="dark"] .collection-card:nth-child(3){background:#222a1e!important;border-color:#33402c!important} +[data-theme="dark"] .arena-intro{background:#2b2517;color:#f0e9d6;border:1px solid #3a3222} +[data-theme="dark"] .mobile-nav{background:#1f1a13f5;border-color:#332c1f} +[data-theme="dark"] .mobile-nav button{color:#8d8266} +[data-theme="dark"] .mobile-nav button.active{color:#ece2cb;background:#2c2618} +[data-theme="dark"] .skip-link{background:#ece2cb!important;color:#221d15!important} +[data-theme="dark"] footer{color:var(--muted)} +#theme-toggle,#sound-toggle{border:1px solid var(--line);background:var(--paper);border-radius:8px;padding:9px} +#confetti{position:fixed;inset:0;pointer-events:none;z-index:200} +.lab-intro{background:var(--blue-pale);border:1px solid #d5dced;border-radius:13px;padding:16px 18px;margin-bottom:16px} +.lab-intro h3{font-family:Fraunces,Georgia,serif;font-size:20px;margin-bottom:6px} +.lab-intro p{font-size:12px;color:var(--muted);line-height:1.6} +.lab-layout{display:grid;grid-template-columns:1.1fr 1fr;gap:18px;align-items:start} +.lab-preview{background:var(--paper);border:1px solid var(--line);border-radius:13px;overflow:hidden} +.lab-controls{display:grid;gap:12px;align-content:start} +.lab-row{display:flex;gap:8px;flex-wrap:wrap} +.lab-provenance{font-size:12px;background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:10px 12px;display:grid;gap:4px;line-height:1.5} +.curse{font-weight:800;letter-spacing:.4px} +[data-theme="dark"] .lab-intro{background:#252b45;border-color:#3a4370} +@media(max-width:650px){.lab-layout{grid-template-columns:1fr}} +.ticker{overflow:hidden;border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin-top:22px;background:var(--paper)} +.ticker-inner{display:inline-block;white-space:nowrap;padding:9px 0;font-size:11px;font-weight:700;letter-spacing:.5px;animation:tickmove 45s linear infinite} +.ticker-inner span{padding-right:24px} +@keyframes tickmove{0%{transform:translateX(0)}100%{transform:translateX(-50%)}} +@media(prefers-reduced-motion:reduce){.ticker-inner{animation:none}} +#library{position:relative} +#library>*{position:relative;z-index:1} +#library::before{content:'MEOW';position:absolute;top:-24px;right:-6px;z-index:0;font-family:Fraunces,Georgia,serif;font-size:clamp(90px,15vw,210px);font-weight:700;line-height:1;color:transparent;-webkit-text-stroke:1px var(--line);opacity:.7;pointer-events:none} +body::after{content:'';position:fixed;inset:0;z-index:1;pointer-events:none;opacity:.05;background-image:url("data:image/svg+xml,")} +.pack-panel{display:grid;gap:12px;background:linear-gradient(135deg,var(--olive) 0%,#5c5136 100%);color:#f7f5ee;border-radius:13px;padding:22px;margin-bottom:16px;text-align:center} +.pack-sealed{background:none;border:2px dashed #f7f5ee66;border-radius:12px;padding:26px;display:grid;gap:6px;justify-items:center;color:#f7f5ee;width:100%} +.pack-sealed>span{font-size:64px} +.pack-sealed strong{font-size:18px;font-family:Fraunces,Georgia,serif} +.pack-sealed small{opacity:.75} +.pack-sealed:hover{border-color:#f7f5ee;background:#f7f5ee11;transform:translateY(-2px)} +.pack-sealed.opening>span{animation:packshake .5s ease-in-out infinite} +@keyframes packshake{0%,100%{transform:rotate(-8deg) scale(1)}50%{transform:rotate(8deg) scale(1.12)}} +.pack-wait{display:flex;gap:18px;align-items:center;justify-content:center;flex-wrap:wrap;text-align:left} +.pack-wait .arena-hint{color:#d7d1c0} +.pack-timer{font-family:Fraunces,Georgia,serif;font-size:30px} +.pack-mini{display:flex;gap:10px;align-items:center;background:#f7f5ee14;border:1px solid #f7f5ee33;border-radius:10px;padding:8px;text-align:left;color:#f7f5ee;max-width:340px} +.pack-mini img{width:64px;height:64px;object-fit:cover;border-radius:8px;flex-shrink:0} +.pack-mini small{opacity:.75;display:block} +.pack-mini.rare{border-color:#9fadf0;box-shadow:0 0 18px #9fadf066} +.pack-mini.legendary{border-color:#ffd75e;box-shadow:0 0 26px #ffd75e88} +.pack-mini.shiny{background:#ffd75e22} +.pack-mini.fresh{animation:packpop .45s ease-out} +@keyframes packpop{0%{transform:scale(.6);opacity:0}100%{transform:scale(1);opacity:1}} +.pack-tabs{display:flex;gap:8px;margin-bottom:14px} +.pack-tabs button{border:1px solid var(--line);background:var(--paper);border-radius:20px;padding:8px 14px;font-size:11px;font-weight:700} +.pack-tabs button.active{background:var(--olive);border-color:var(--olive);color:var(--paper)} +.meme-card{position:relative} +.shiny-badge{position:absolute;top:8px;left:8px;z-index:2;background:#ffd75e;color:#3a2f10;font-size:10px;font-weight:800;border-radius:6px;padding:3px 8px} +.meme-card.is-shiny{border-color:#d9c57c} +.va-group{display:inline-flex;gap:5px;align-items:center} +.dex-progress{background:var(--paper);border:1px solid var(--line);border-radius:13px;padding:16px 18px;margin-bottom:18px;display:flex;gap:14px;align-items:center;flex-wrap:wrap} +.dex-progress strong{font-family:Fraunces,Georgia,serif;font-size:24px;white-space:nowrap} +.dex-progress .rate-bar{flex:1;min-width:140px} +.dex-mood{font-family:Fraunces,Georgia,serif;font-size:20px;margin:20px 0 10px} +.dex-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(108px,1fr));gap:10px} +.dex-card{background:var(--paper);border:1px solid var(--line);border-radius:10px;padding:8px;display:grid;gap:5px;justify-items:center;text-align:center} +button.dex-card:hover{transform:translateY(-3px);box-shadow:0 8px 20px #4a412a18} +.dex-card img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:7px;background:#f4f2ed} +.dex-num{font-family:ui-monospace,monospace;font-size:9px;color:var(--muted)} +.dex-name{font-size:10px;font-weight:700;line-height:1.3} +.dex-card.locked{background:#171410;color:#8f8468;border-style:dashed;cursor:help} +.dex-card.locked .dex-num{color:#5c5545} +.dex-q{font-family:Fraunces,Georgia,serif;font-size:34px;line-height:1.4;color:#d9c57c} +.mobile-nav{overflow-x:auto;scrollbar-width:none} +.mobile-nav button{flex:1 0 auto} +@media(max-width:650px){.mobile-nav button{padding:5px 8px}} +.meme-visual{position:relative} +.font-fraunces .meme-caption{font-family:Fraunces,Georgia,serif} +.font-mono .meme-caption{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.92em} +.style-dark{background:#171410;color:#F7F5EE} +.style-banana{background:#FFD75E;color:#3A2F10} +.sticker{position:absolute;z-index:3;font-size:58px;line-height:1;filter:drop-shadow(0 3px 6px #00000055);pointer-events:none} +.corner-tl{top:6px;left:10px}.corner-tr{top:6px;right:10px}.corner-bl{bottom:6px;left:10px}.corner-br{bottom:6px;right:10px} +.editor-row{display:grid;grid-template-columns:1fr 1fr;gap:10px} +.hot-badge{font-size:9px;font-weight:800;letter-spacing:.6px;background:#ffe3d6;border:1px solid #f0b49a;color:#a03d1c;border-radius:20px;padding:3px 8px;white-space:nowrap} +/* contrast pass: microcopy on ecru cards was washing out */ +.card-source{color:#6f6a58} +.save-button{color:#6f6a58} +.nav-label{color:#6e6957} +.nav-label.nav-gap{margin-top:20px} +footer{color:#6e6957} +.view-toggle button{color:#6e6957} +.mobile-nav button{color:#6e6957} +.search-wrap svg{color:#6e6957} +.hero-footnote{color:#c6bfa9} +#chimp-buddy{position:fixed;right:18px;bottom:18px;z-index:150;display:flex;flex-direction:column;align-items:end;gap:8px;perspective:320px;animation:buddybob 3s ease-in-out infinite} +#buddy-btn{position:relative;overflow:hidden;background:var(--paper);border:2px solid var(--line);border-radius:50%;width:68px;height:68px;padding:0;box-shadow:0 10px 26px #4a412a40;transform-style:preserve-3d;will-change:transform} +#buddy-btn img{width:100%;height:100%;object-fit:cover;display:block;border-radius:50%} +#buddy-btn::after{content:'';position:absolute;inset:0;border-radius:50%;background:radial-gradient(circle at var(--gx,30%) var(--gy,25%),#ffffff66,transparent 55%);pointer-events:none} +#chimp-buddy.muted #buddy-btn{opacity:.4;filter:grayscale(1)} +.buddy-bubble{max-width:230px;background:var(--paper);border:1px solid var(--line);border-radius:12px 12px 4px 12px;padding:10px 12px;font-size:12px;line-height:1.5;box-shadow:0 8px 24px #4a412a25;animation:packpop .25s ease-out} +@keyframes buddybob{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}} +@media(max-width:650px){#chimp-buddy{bottom:76px;right:12px}#buddy-btn{width:54px;height:54px}} +@media(prefers-reduced-motion:reduce){#buddy-btn{animation:none}} +.reveal-actions{margin-top:10px;display:flex;justify-content:center} +.forge{background:var(--paper);border:1px solid var(--line);border-radius:13px;padding:16px;margin-bottom:16px} +.forge h3{font-family:Fraunces,Georgia,serif;font-size:20px} +.forge-top{display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;margin:10px 0 4px} +.scrap-count{font-size:15px;font-weight:800} +.forge .primary-button:disabled{cursor:not-allowed;opacity:.45} +[data-theme="dark"] .pack-panel{background:linear-gradient(135deg,#2b2517,#3a3120);color:#f0e9d6} +#stash{display:grid;gap:16px;margin-top:18px} +.sets-panel,.trophies{background:var(--paper);border:1px solid var(--line);border-radius:13px;padding:16px} +.trophy-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px;margin-top:10px} +.trophy{border:1px solid var(--line);border-radius:10px;padding:12px 10px;text-align:center;display:grid;gap:4px;justify-items:center;opacity:.5} +.trophy.won{opacity:1;border-color:#d9c57c;background:#fbf6e3} +[data-theme="dark"] .trophy.won{background:#2e2a18;border-color:#6b5f30} +.trophy span{font-size:26px} +.trophy strong{font-size:11px} +.trophy small{font-size:10px;color:var(--muted);line-height:1.4} +.viewer-dialog.tv{width:1020px} +.viewer-dialog.tv .viewer-body{position:relative} +.viewer-dialog.tv .viewer-details,.viewer-dialog.tv .viewer-bottom{display:none} +.viewer-dialog.tv .viewer-image-wrap .meme-image{max-height:68dvh} +.viewer-dialog.tv .viewer-image-wrap{cursor:pointer} +.tv-overlay{position:absolute;inset:auto 0 0 0;display:flex;flex-direction:column;gap:3px;background:linear-gradient(transparent,#181410ee);color:#f7f5ee;padding:40px 18px 14px;font-size:12px;border-radius:0 0 16px 16px} +.tv-meta{display:flex;gap:10px;align-items:baseline;min-width:0} +#tv-chan{font-weight:800;letter-spacing:.6px;white-space:nowrap;color:#B9C5F3} +#tv-name{font-weight:800;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.tv-sub{display:flex;gap:10px;opacity:.75;font-size:11px;min-width:0} +.tv-sub.vj-row{min-height:1.4em} +#tv-vj{color:#ffd75e;font-style:italic;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +#tv-next{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.tv-controls{display:flex;gap:6px;margin-top:5px;flex-wrap:wrap} +.tv-controls button{border:1px solid #f7f5ee55;border-radius:7px;padding:6px 10px;font-size:11px;font-weight:700;color:#f7f5ee;white-space:nowrap} +.tv-controls button:hover{background:#f7f5ee22} +.tv-up{border:1px solid #f7f5ee55;border-radius:7px;padding:6px 10px;font-size:11px;font-weight:700;color:#f7f5ee;cursor:pointer} +.tv-up:hover{background:#f7f5ee22} +.clip-player{width:100%;max-height:60dvh;background:#000;display:block} +.viewer-dialog.tv .viewer-image-wrap{position:relative} +.viewer-dialog.tv .viewer-image-wrap::after{content:'';position:absolute;inset:0;background:repeating-linear-gradient(0deg,#00000014 0 1px,transparent 1px 3px);box-shadow:inset 0 0 90px #00000055;pointer-events:none} +#zap{position:absolute;inset:0;background:#fff;opacity:0;pointer-events:none;z-index:30} +#zap.on{animation:zapflash .25s ease-out} +@keyframes zapflash{0%{opacity:.5}100%{opacity:0}} +#screen{position:absolute;inset:0;z-index:40;display:flex;align-items:center;justify-content:center;background:#14110c;color:#f7f5ee;opacity:0;pointer-events:none;text-align:center;padding:20px} +#screen.show{animation:identshow 1.6s ease-out} +@keyframes identshow{0%{opacity:0}12%{opacity:1}80%{opacity:1}100%{opacity:0}} +.ident-big{font-family:Fraunces,Georgia,serif;font-size:clamp(28px,6vw,54px);font-weight:700} +.ident-small{font-size:12px;letter-spacing:2px;opacity:.7;margin-top:8px} +.testbars{height:70px;border-radius:8px;background:linear-gradient(to right,#c0c0c0 0%,#c0c0c0 14%,#c0c000 14%,#c0c000 28%,#00c0c0 28%,#00c0c0 42%,#00c000 42%,#00c000 57%,#c000c0 57%,#c000c0 71%,#c00000 71%,#c00000 85%,#0000c0 85%,#0000c0 100%)} +.testcard{width:min(420px,90%)} +.testcap{font-family:Fraunces,Georgia,serif;font-size:24px;margin-top:14px} +.testcap small{display:block;font-family:'DM Sans',sans-serif;font-size:11px;letter-spacing:2px;opacity:.7;margin-top:6px} +#tv-live{color:#ff6b5e;font-weight:800;white-space:nowrap} +#tv-clock{opacity:.75;white-space:nowrap} +.ad-break{background:#0d0d0f;color:#ffe900;display:flex;flex-direction:column;overflow:hidden;text-align:center} +.ad-bug{align-self:start;background:#ffe900;color:#0d0d0f;font-size:10px;font-weight:800;letter-spacing:2px;padding:4px 10px;margin:12px 0 0 12px;border-radius:4px} +.ad-brand{font-family:Fraunces,Georgia,serif;font-size:clamp(26px,5vw,44px);font-weight:700;padding:14px 16px 0} +.ad-break img{width:100%;max-height:46dvh;object-fit:cover} +.ad-line{font-size:15px;font-weight:600;padding:12px 16px 0} +.ad-cta{display:inline-block;margin:12px auto 16px;border:2px solid #ffe900;border-radius:8px;padding:8px 16px;font-size:11px;font-weight:800;letter-spacing:1px} +.sets-panel h3,.trophies h3{font-family:Fraunces,Georgia,serif;font-size:20px} +#new-set-form{display:flex;gap:8px;margin:10px 0 6px} +#new-set-form input{flex:1;border:1px solid var(--line);background:var(--paper);color:var(--olive);border-radius:7px;padding:10px 12px;font-size:12px;min-width:0} +.set-row{display:flex;gap:8px;align-items:center;padding:8px 0;border-top:1px solid var(--line)} +.set-open{flex:1;display:flex;justify-content:space-between;align-items:center;gap:10px;text-align:left;border-radius:8px;padding:8px 10px;font-size:12px} +.set-open:hover{background:#eeeadd} +[data-theme="dark"] .set-open:hover{background:#2c2618} +.set-open.active{outline:2px solid var(--olive);outline-offset:2px} +.set-open span{color:var(--muted);font-size:11px;white-space:nowrap} +.set-delete{font-size:11px;color:var(--muted);text-decoration:underline;white-space:nowrap} +.upload-label{display:inline-flex;align-items:center;cursor:pointer} +.remove-from-set{margin:0 12px 12px;font-size:11px;font-weight:700;color:#9c6055;text-decoration:underline;align-self:start} +.collect-body{padding:20px 22px;display:grid;gap:12px} +#collect-list{display:grid;gap:6px} +.collect-row{display:flex;justify-content:space-between;align-items:center;border:1px solid var(--line);border-radius:8px;padding:10px 12px;font-size:12px;font-weight:600} +.collect-row.in{background:var(--olive);border-color:var(--olive);color:var(--paper)} +#collect-new{display:flex;gap:8px} +#collect-new input{flex:1;border:1px solid var(--line);background:var(--paper);color:var(--olive);border-radius:7px;padding:10px 12px;font-size:12px;min-width:0} +.hero-paths{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:14px;font-size:10px;color:#d7d1c0} +.hero-paths button{border:1px solid #f7f5ee44;border-radius:20px;padding:7px 12px;font-size:10px;font-weight:700;color:#f7f5ee} +.hero-paths button:hover{background:#f7f5ee22} diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..597e163 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,31 @@ +const VERSION = 'catmemechimp-v1'; +const CORE = ['/', '/index.html', '/app.js', '/style.css', '/fonts.css', '/library.json', '/manifest.webmanifest', '/assets/chimp.svg']; +self.addEventListener('install', (e) => { + e.waitUntil(caches.open(VERSION).then((c) => c.addAll(CORE)).then(() => self.skipWaiting())); +}); +self.addEventListener('activate', (e) => { + e.waitUntil( + caches + .keys() + .then((keys) => Promise.all(keys.filter((k) => k !== VERSION).map((k) => caches.delete(k)))) + .then(() => self.clients.claim()) + ); +}); +self.addEventListener('fetch', (e) => { + const u = new URL(e.request.url); + if (e.request.method !== 'GET' || u.origin !== self.location.origin) return; + e.respondWith( + caches.match(e.request).then((hit) => { + const net = fetch(e.request) + .then((res) => { + if (res.ok) { + const copy = res.clone(); + caches.open(VERSION).then((c) => c.put(e.request, copy)); + } + return res; + }) + .catch(() => hit); + return hit || net; + }) + ); +}); diff --git a/scripts/smoke.mjs b/scripts/smoke.mjs new file mode 100644 index 0000000..aaa976f --- /dev/null +++ b/scripts/smoke.mjs @@ -0,0 +1,55 @@ +import { readFileSync, existsSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const pub = join(root, 'public'); +const failures = []; +const fail = (msg) => failures.push(msg); + +const lib = JSON.parse(readFileSync(join(pub, 'library.json'), 'utf8')); +if (!Array.isArray(lib) || !lib.length) fail('library.json must be a non-empty array'); + +const moods = new Set(['relatable', 'chaos', 'wholesome', 'judgment', 'sleepy', 'snacks']); +const seen = new Set(); +for (const [i, m] of lib.entries()) { + const where = `library[${i}] (${m.id || 'no-id'})`; + for (const f of ['id', 'title', 'mood', 'tags', 'image', 'template', 'kind', 'added']) { + if (m[f] === undefined || m[f] === null || m[f] === '') fail(`${where} missing ${f}`); + } + if (seen.has(m.id)) fail(`duplicate id ${m.id}`); + seen.add(m.id); + if (!moods.has(m.mood)) fail(`${where} bad mood ${m.mood}`); + if (!Array.isArray(m.tags) || !m.tags.length) fail(`${where} tags must be non-empty`); + if (typeof m.image !== 'string' || !m.image.startsWith('/assets/memes/')) fail(`${where} bad image path`); + else if (!existsSync(join(pub, m.image.slice(1)))) fail(`${where} missing file ${m.image}`); + if (typeof m.origin !== 'number') fail(`${where} missing origin year`); + if (!['immortal', 'classic', 'alive'].includes(m.status)) fail(`${where} bad status ${m.status}`); + if (typeof m.lore !== 'string' || !m.lore) fail(`${where} missing lore`); + if (m.source !== null && m.source !== undefined && !String(m.source).startsWith('https://')) fail(`${where} bad source`); +} + +const html = readFileSync(join(pub, 'index.html'), 'utf8'); +const js = readFileSync(join(pub, 'app.js'), 'utf8'); +const defined = new Set([ + ...[...html.matchAll(/id="([A-Za-z0-9-]+)"/g)].map((m) => m[1]), + ...[...js.matchAll(/id="([A-Za-z0-9-]+)"/g)].map((m) => m[1]), +]); +for (const m of js.matchAll(/\$\('#([A-Za-z0-9-]+)'\)/g)) { + if (!defined.has(m[1])) fail(`app.js uses #${m[1]} but no such id is rendered`); +} +const count = html.match(/id="all-count">(\d+)/); +if (!count) fail('index.html missing #all-count'); +else if (Number(count[1]) !== lib.length) fail(`#all-count is ${count[1]} but library has ${lib.length}`); +for (const id of ['arena', 'battle-grid', 'arena-stats', 'leaderboard', 'last-crowned', 'arena-skip', 'arena-reset', 'lab', 'lab-preview', 'lab-provenance', 'lab-top', 'lab-bottom', 'lab-cat', 'lab-chaos', 'lab-save', 'lab-download', 'theme-toggle', 'sound-toggle', 'confetti', 'stash', 'sets-panel', 'trophies', 'viewer-collect', 'collect-dialog', 'collect-list', 'collect-new', 'collect-new-name', 'tv-toggle', 'tv-overlay', 'tv-chan', 'tv-clock', 'tv-live', 'tv-count', 'tv-name', 'tv-next', 'tv-ch-up', 'tv-ch-down', 'tv-vol', 'tv-speed', 'tv-sleep', 'tv-exit', 'tv-vj', 'tv-clip-upload', 'screen', 'zap', 'packs', 'pack-panel', 'pack-stats', 'keep-tabs', 'keep-haul', 'keep-loved', 'daily-count', 'chimp-buddy', 'buddy-bubble', 'buddy-btn', 'ticker', 'ticker-inner', 'viewer-more', 'editor-font', 'editor-style', 'editor-sticker', 'editor-corner', 'editor-motion', 'lab-motion', 'lab-inspire', 'meme-grid', 'viewer', 'editor']) { + if (!html.includes(`id="${id}"`)) fail(`index.html missing #${id}`); +} +for (const f of ['sw.js', 'app.js', 'style.css', 'library.json', 'manifest.webmanifest', 'assets/clips/calico.mp4', 'assets/clips/tokyo.mp4', 'assets/clips/gatos.mp4']) { + if (!existsSync(join(pub, f))) fail(`public/${f} missing`); +} + +if (failures.length) { + console.error(`smoke: ${failures.length} problem(s)\n- ${failures.join('\n- ')}`); + process.exit(1); +} +console.log(`smoke: OK — ${lib.length} memes, ${new Set(lib.map((m) => m.image)).size} templates, all assets present`); diff --git a/server.mjs b/server.mjs index 429145a..448ff80 100644 --- a/server.mjs +++ b/server.mjs @@ -3,7 +3,7 @@ import {readFile,stat} from 'node:fs/promises'; import {fileURLToPath} from 'node:url'; import {resolve,extname,sep} from 'node:path'; const root=fileURLToPath(new URL('./public/',import.meta.url)); -const types={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.json':'application/json; charset=utf-8','.webmanifest':'application/manifest+json','.svg':'image/svg+xml','.jpg':'image/jpeg','.png':'image/png','.ico':'image/x-icon','.woff2':'font/woff2'}; +const types={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.json':'application/json; charset=utf-8','.webmanifest':'application/manifest+json','.svg':'image/svg+xml','.jpg':'image/jpeg','.png':'image/png','.mp4':'video/mp4','.ico':'image/x-icon','.woff2':'font/woff2'}; const server=http.createServer(async(req,res)=>{ try{ if(!['GET','HEAD'].includes(req.method)){res.writeHead(405,{Allow:'GET, HEAD'});return res.end();} @@ -13,7 +13,7 @@ const server=http.createServer(async(req,res)=>{ if(!file.startsWith(root.endsWith(sep)?root:root+sep)){res.writeHead(403);return res.end('Forbidden');} if(!(await stat(file)).isFile())throw new Error('not-file'); const data=await readFile(file); - res.writeHead(200,{'Content-Type':types[extname(file)]||'application/octet-stream','Content-Length':data.length,'X-Content-Type-Options':'nosniff','Referrer-Policy':'strict-origin-when-cross-origin','Cache-Control':p.startsWith('/assets/')?'public,max-age=86400':'no-cache','Content-Security-Policy':"default-src 'self'; img-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"}); + res.writeHead(200,{'Content-Type':types[extname(file)]||'application/octet-stream','Content-Length':data.length,'X-Content-Type-Options':'nosniff','Referrer-Policy':'strict-origin-when-cross-origin','Cache-Control':p.startsWith('/assets/')?'public,max-age=86400':'no-cache','Content-Security-Policy':"default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"}); res.end(req.method==='HEAD'?undefined:data); }catch{res.writeHead(404,{'Content-Type':'text/plain'});res.end('This cat wandered off. Page not found.');} });