THE GOOD STUFF
Find your feline frequency.
Shuffle the cats
-
+ How the Arena works Two cats enter. Tap the funnier one — or press ← / → .Build a streak. Every crown adds to your judging streak.Skips reset the streak. Cowardice has consequences. Stats stay in this browser.
Skip this matchupReset my stats ← vote left → vote right · swipe on phone
+
Rounding up the cats… Sort by Chimp’s picks A–Z Unseen first
@@ -48,7 +50,7 @@
A little less doom. A little more meow.The archive & its sources ↗
- Discover Saved Collections Creations
+ Discover Arena Saved Collections Creations
THE NONSENSE LAB
Make a little mischief.
diff --git a/public/library.json b/public/library.json
index ea77c12..65b274b 100644
--- a/public/library.json
+++ b/public/library.json
@@ -959,5 +959,1445 @@
"source": "https://imgflip.com/meme/488919824/Huh-Cat",
"kind": "remix",
"added": "2026-09-08"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
+ },
+ {
+ "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"
}
-]
\ No newline at end of file
+]
diff --git a/public/style.css b/public/style.css
index 23b7852..62c9035 100644
--- a/public/style.css
+++ b/public/style.css
@@ -5,3 +5,4 @@
@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}@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}}
From 3ae59350f3ccef1fbff7bba4c0a1461cf28812f4 Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:33:56 +0200
Subject: [PATCH 02/37] Add smoke test and CI check
---
.github/workflows/check.yml | 13 ++++++++++++
package.json | 2 +-
scripts/smoke.mjs | 41 +++++++++++++++++++++++++++++++++++++
3 files changed, 55 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/check.yml
create mode 100644 scripts/smoke.mjs
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/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/scripts/smoke.mjs b/scripts/smoke.mjs
new file mode 100644
index 0000000..42b533e
--- /dev/null
+++ b/scripts/smoke.mjs
@@ -0,0 +1,41 @@
+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 (m.source !== null && m.source !== undefined && !String(m.source).startsWith('https://')) fail(`${where} bad source`);
+}
+
+const html = readFileSync(join(pub, 'index.html'), 'utf8');
+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', 'meme-grid', 'viewer', 'editor']) {
+ if (!html.includes(`id="${id}"`)) fail(`index.html missing #${id}`);
+}
+
+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`);
From 4784440ff045dd46f8ef370ecb94e93e09132dc9 Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:34:22 +0200
Subject: [PATCH 03/37] Add offline service worker
---
public/app.js | 2 +-
public/sw.js | 31 +++++++++++++++++++++++++++++++
scripts/smoke.mjs | 3 +++
3 files changed, 35 insertions(+), 1 deletion(-)
create mode 100644 public/sw.js
diff --git a/public/app.js b/public/app.js
index 2a94072..26ead0e 100644
--- a/public/app.js
+++ b/public/app.js
@@ -153,7 +153,7 @@ document.addEventListener('keydown',e=>{if(e.ctrlKey||e.altKey||e.metaKey||/INPU
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});
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(){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();pickBattle();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.')}
+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();pickBattle();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.')}
}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/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
index 42b533e..6612257 100644
--- a/scripts/smoke.mjs
+++ b/scripts/smoke.mjs
@@ -33,6 +33,9 @@ else if (Number(count[1]) !== lib.length) fail(`#all-count is ${count[1]} but li
for (const id of ['arena', 'battle-grid', 'arena-stats', 'leaderboard', 'last-crowned', 'arena-skip', 'arena-reset', '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']) {
+ if (!existsSync(join(pub, f))) fail(`public/${f} missing`);
+}
if (failures.length) {
console.error(`smoke: ${failures.length} problem(s)\n- ${failures.join('\n- ')}`);
From 3332d07a096eb97b1e48bae44d8040a786cf27f2 Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:35:28 +0200
Subject: [PATCH 04/37] Add dark mode with system default
---
public/app.js | 5 ++++-
public/index.html | 2 +-
public/style.css | 21 +++++++++++++++++++++
3 files changed, 26 insertions(+), 2 deletions(-)
diff --git a/public/app.js b/public/app.js
index 26ead0e..fd23563 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:' ',trophy:' ',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:' ',heart:' ',folder:' ',sparkles:' ',shuffle:' ',play:' ',pause:' ',keyboard:' ',search:' ',plus:' ','arrow-right':' ','arrow-left':' ','arrow-down':' ',x:' ',download:' ',upload:' ',edit:' ',link:' ',check:' '};
const icon=n=>`${paths[n]||paths.sparkles} `;
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));}
@@ -138,6 +138,9 @@ $('#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;
$('#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-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.')}};
diff --git a/public/index.html b/public/index.html
index 5c308a1..0148512 100644
--- a/public/index.html
+++ b/public/index.html
@@ -30,7 +30,7 @@
-
+
✳ 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.
Find me a good cat
diff --git a/public/style.css b/public/style.css
index 62c9035..a05f704 100644
--- a/public/style.css
+++ b/public/style.css
@@ -6,3 +6,24 @@
@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}@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"] .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{border:1px solid var(--line);background:var(--paper);border-radius:8px;padding:9px}
From 062206e2979e661f551c2d240e2036dc1e0cfcf6 Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:36:46 +0200
Subject: [PATCH 05/37] Add Frankenmeme Lab and Arena streak milestones
---
public/app.js | 43 ++++++++++++++++++++++++++++---------------
public/index.html | 4 +++-
public/style.css | 11 +++++++++++
scripts/smoke.mjs | 2 +-
4 files changed, 43 insertions(+), 17 deletions(-)
diff --git a/public/app.js b/public/app.js
index fd23563..f4ccd94 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:' ',moon:' ',sun:' ',trophy:' ',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:' ',heart:' ',folder:' ',sparkles:' ',shuffle:' ',play:' ',pause:' ',keyboard:' ',search:' ',plus:' ','arrow-right':' ','arrow-left':' ','arrow-down':' ',x:' ',download:' ',upload:' ',edit:' ',link:' ',check:' '};
const icon=n=>`${paths[n]||paths.sparkles} `;
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));}
@@ -27,11 +27,23 @@ function voteBattle(winnerId){if(!battleLeft||!battleRight)return;
battleStats.wins[winnerId]=battleWins(winnerId)+1;battleStats.battles++;battleStats.streak++;battleStats.best=Math.max(battleStats.best,battleStats.streak);battleStats.lastWinner=winnerId;
setLocal('battles',battleStats);
const w=allMemes().find(m=>m.id===winnerId);
- toast(`“${w?.title||'Cat'}” takes the crown. ${battleTaunts[Math.floor(Math.random()*battleTaunts.length)]}`);
+ toast(streakMilestones[battleStats.streak]||`“${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.');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};setLocal('battles',battleStats);pickBattle();renderArena();toast('Fresh slate. The cats forgive you.')}
+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;
+const randMeme=()=>allMemes()[Math.floor(Math.random()*allMemes().length)];
+function rollLab(part){if(!allMemes().length)return;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:labTop?.top||'',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: ${esc(labTop.title)} Below the cat: ${esc(labBottom.title)} The cat: ${esc(labCat.title)} ${stars} ${names[curseRating()-1]} `}
+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);navigate('studio');toast('Abomination preserved for future generations.')}catch{toast('Could not save in this browser. Use PNG to keep your creation.')}}
function battleCard(m,side){const wins=battleWins(m.id),plays=battlePlays(m.id),rate=battleRate(m.id),label=moodInfo(m.mood)[2];
return `${side==='left'?'← CONTENDER':'CONTENDER →'} ${visual(m,'eager')}${moodTag(m.mood)}${esc(m.title)} ${plays?`${wins}W · ${plays-wins}L · ${rate}%`:'No battles yet — be its first fan'} ${rate!==null?` `:''}👑 Crown this cat `}
function renderArena(){if(!battleLeft||!battleRight)pickBattle();if(!battleLeft||!battleRight)return;
@@ -65,15 +77,16 @@ function card(m,i){return `The cats haven’t arrived yet.'}else renderArena();return}
+ if(inLab){renderLab();return}
if(page==='collections')$('#collection-grid').innerHTML=collections.map(c=>`${library.filter(c.filter).length} CATS · CURATED COLLECTION ${c.title} ${c.description}
`).join('');
$('#mood-filters').innerHTML=moods.map(([id,symbol,label])=>`${symbol} ${label} `).join('');
const list=filterMemes(),display=list.slice(0,limit);$('#meme-grid').innerHTML=display.map(card).join('');
@@ -84,7 +97,7 @@ function render(){updateCounts();$$('[data-page]').forEach(b=>{b.classList.toggl
$('#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-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='';if(p==='arena'&&(!battleLeft||!battleRight))pickBattle();render();window.scrollTo({top:0,behavior:'smooth'})}
+function navigate(p){page=p;mood='all';query='';collection='';limit=18;$('#search').value='';if(p==='arena'&&(!battleLeft||!battleRight))pickBattle();if(p==='lab'&&(!labTop||!labBottom||!labCat))rollLab();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()}
if($('#viewer').open)updateViewerSave();toast(saved.has(id)?'A good cat, safely tucked away.':'Released back into the wild.');
@@ -128,11 +141,11 @@ async function download(m,button){if(button)button.disabled=true;try{const blob=
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 R A random cat from this view ← / → Previous / next cat in the viewer, or vote in the Arena S Save the cat in the viewer Space Pause / resume autoplay in the viewer Esc Close 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.
`)}
+function shortcuts(){info('Less clicking. More cats.',`/ Jump to search R A random cat from this view ← / → Previous / next cat in the viewer, or vote in the Arena T / B / C / F Reroll Frankenmeme top / bottom / cat / full chaos S Save the cat in the viewer Space Pause / resume autoplay in the viewer Esc Close 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()}});
$('.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;$('#arena-skip').onclick=skipBattle;$('#arena-reset').onclick=resetArena;$('#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');$('#lab-bottom').onclick=()=>rollLab('bottom');$('#lab-cat').onclick=()=>rollLab('cat');$('#lab-chaos').onclick=()=>rollLab('all');$('#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')};
@@ -152,11 +165,11 @@ $('#about-open').onclick=about;$('#shortcuts-open').onclick=shortcuts;$('#source
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()}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)}});
+ 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')}else if(page==='lab'&&e.key.toLowerCase()==='b'){e.preventDefault();rollLab('bottom')}else if(page==='lab'&&e.key.toLowerCase()==='c'){e.preventDefault();rollLab('cat')}else if(page==='lab'&&e.key.toLowerCase()==='f'){e.preventDefault();rollLab('all')}});
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});
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();pickBattle();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.')}
+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();pickBattle();rollLab();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.')}
}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/index.html b/public/index.html
index 0148512..1380b92 100644
--- a/public/index.html
+++ b/public/index.html
@@ -16,6 +16,7 @@
Discover150
Battle Arena0
+ Frankenmeme Lab
Saved cats0
Collections
My creations0
@@ -39,6 +40,7 @@
THE GOOD STUFF
Find your feline frequency.
Shuffle the cats
How the Arena works Two cats enter. Tap the funnier one — or press ← / → .Build a streak. Every crown adds to your judging streak.Skips reset the streak. Cowardice has consequences. Stats stay in this browser.
Skip this matchupReset my stats ← vote left → vote right · swipe on phone
+ The Frankenmeme Lab Stitch a top from one cat, a bottom from another, and a third cat entirely. No ethics committee was consulted.
🎲 Top 🎲 Bottom 🎲 Cat ⚡ Full chaos
Save creation PNG
T top B bottom C cat F full chaos
Rounding up the cats… Sort by Chimp’s picks A–Z Unseen first
@@ -50,7 +52,7 @@
A little less doom. A little more meow.The archive & its sources ↗
- Discover Arena Saved Collections Creations
+ Discover Arena Lab Saved Collections Creations
THE NONSENSE LAB
Make a little mischief.
diff --git a/public/style.css b/public/style.css
index a05f704..26b0e28 100644
--- a/public/style.css
+++ b/public/style.css
@@ -27,3 +27,14 @@
[data-theme="dark"] .skip-link{background:#ece2cb!important;color:#221d15!important}
[data-theme="dark"] footer{color:var(--muted)}
#theme-toggle{border:1px solid var(--line);background:var(--paper);border-radius:8px;padding:9px}
+.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}}
diff --git a/scripts/smoke.mjs b/scripts/smoke.mjs
index 6612257..f2c5852 100644
--- a/scripts/smoke.mjs
+++ b/scripts/smoke.mjs
@@ -30,7 +30,7 @@ const html = readFileSync(join(pub, 'index.html'), 'utf8');
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', 'meme-grid', 'viewer', 'editor']) {
+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', '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']) {
From 58e6120c7096117aec6fef0f7ba751db423ba295 Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:37:06 +0200
Subject: [PATCH 06/37] Add first-run welcome and smarter empty states
---
public/app.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/public/app.js b/public/app.js
index f4ccd94..36ceeee 100644
--- a/public/app.js
+++ b/public/app.js
@@ -94,7 +94,7 @@ function render(){updateCounts();$$('[data-page]').forEach(b=>{b.classList.toggl
$('#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='';if(p==='arena'&&(!battleLeft||!battleRight))pickBattle();if(p==='lab'&&(!labTop||!labBottom||!labCat))rollLab();render();window.scrollTo({top:0,behavior:'smooth'})}
@@ -170,6 +170,7 @@ document.addEventListener('visibilitychange',()=>{if(document.hidden&&autoTimer)
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});
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();pickBattle();rollLab();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.')}
+ if(!deepId&&!readState('welcomed',false)){setLocal('welcomed',true);setTimeout(()=>info('Welcome to the cat archive.',`${library.length} memes. Zero mercy. Here’s the tour:
🏟️ Battle Arena — crown the funnier cat, build a streak. 🧪 Frankenmeme Lab — stitch three cats into one abomination. ✨ Studio — caption your own and export PNGs.
Press / to search, R for a random cat, ? for all shortcuts. Everything stays in this browser.
`),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();
From e37931a8c6f5f1d96c1ab16c0f1e306cc26ccf98 Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:49:04 +0200
Subject: [PATCH 07/37] Give the app one red thread: Find Crown Stitch Keep
---
public/app.js | 2 +-
public/index.html | 2 +-
public/style.css | 3 +++
3 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/public/app.js b/public/app.js
index 36ceeee..79ef0a9 100644
--- a/public/app.js
+++ b/public/app.js
@@ -170,7 +170,7 @@ document.addEventListener('visibilitychange',()=>{if(document.hidden&&autoTimer)
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});
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();pickBattle();rollLab();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.')}
- if(!deepId&&!readState('welcomed',false)){setLocal('welcomed',true);setTimeout(()=>info('Welcome to the cat archive.',`${library.length} memes. Zero mercy. Here’s the tour:
🏟️ Battle Arena — crown the funnier cat, build a streak. 🧪 Frankenmeme Lab — stitch three cats into one abomination. ✨ Studio — caption your own and export PNGs.
Press / to search, R for a random cat, ? for all shortcuts. Everything stays in this browser.
`),450)}
+ if(!deepId&&!readState('welcomed',false)){setLocal('welcomed',true);setTimeout(()=>info('Welcome to the cat archive.',`Follow the thread: 🔍 Find → 🏟 Crown → 🧪 Stitch → 📦 Keep.
${library.length} memes. Battle them in the Arena, stitch abominations in the Lab, make your own in the Studio, and hoard everything in Collections.
Press / to search, R for a random cat, ? for all shortcuts. Everything stays in this browser.
`),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/index.html b/public/index.html
index 1380b92..c91d802 100644
--- a/public/index.html
+++ b/public/index.html
@@ -34,7 +34,7 @@
- ✳ 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.
Find me a good cat
+ ✳ 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.
Find me a good cat or follow the thread: 🏟 Crown 🧪 Stitch ✨ Make 📦 Keep
✳ no thoughts. only potassium.
me: “i’m fine” also me:
100%GOOD CATS ✦
a very serious collection ↗
diff --git a/public/style.css b/public/style.css
index 26b0e28..f46403f 100644
--- a/public/style.css
+++ b/public/style.css
@@ -38,3 +38,6 @@
.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}}
+.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}
From 60afc853add67be7b7f37c62035425bd9a2752cb Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:50:08 +0200
Subject: [PATCH 08/37] Add custom collections with viewer stash dialog
---
public/app.js | 25 ++++++++++++++++++++-----
public/index.html | 4 +++-
public/style.css | 19 +++++++++++++++++++
scripts/smoke.mjs | 2 +-
4 files changed, 43 insertions(+), 7 deletions(-)
diff --git a/public/app.js b/public/app.js
index 79ef0a9..fa035f9 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:' ',moon:' ',sun:' ',trophy:' ',flask:' ',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:' ',heart:' ',folder:' ',sparkles:' ',shuffle:' ',play:' ',pause:' ',keyboard:' ',search:' ',plus:' ','arrow-right':' ','arrow-left':' ','arrow-down':' ',x:' ',download:' ',upload:' ',edit:' ',link:' ',check:' '};
const icon=n=>`${paths[n]||paths.sparkles} `;
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));}
@@ -44,6 +44,17 @@ function renderLab(){if(!labTop||!labBottom||!labCat){rollLab();return}const m=l
const names=['blessed','mildly cursed','cursed','deeply cursed','FORBIDDEN'];
$('#lab-provenance').innerHTML=`Above the cat: ${esc(labTop.title)} Below the cat: ${esc(labBottom.title)} The cat: ${esc(labCat.title)} ${stars} ${names[curseRating()-1]} `}
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);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.
Create set ${userSets.length?userSets.map(s=>`${esc(s.name)} ${s.ids.length} ${s.ids.length===1?'cat':'cats'} Delete
`).join(''):'No sets yet. Name one above — future you says thanks.
'}`}
+function renderCollect(){const m=currentMeme();const box=$('#collect-list');if(!box||!m)return;
+ box.innerHTML=userSets.length?userSets.map(s=>`${s.ids.includes(m.id)?'✓':'+'} ${esc(s.name)} ${s.ids.length} `).join(''):'No sets yet — create one below.
'}
function battleCard(m,side){const wins=battleWins(m.id),plays=battlePlays(m.id),rate=battleRate(m.id),label=moodInfo(m.mood)[2];
return `${side==='left'?'← CONTENDER':'CONTENDER →'} ${visual(m,'eager')}${moodTag(m.mood)}${esc(m.title)} ${plays?`${wins}W · ${plays-wins}L · ${rate}%`:'No battles yet — be its first fan'} ${rate!==null?` `:''}👑 Crown this cat `}
function renderArena(){if(!battleLeft||!battleRight)pickBattle();if(!battleLeft||!battleRight)return;
@@ -65,7 +76,7 @@ async function dbRead(){const db=await database();return new Promise((res,rej)=>
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];
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(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;
@@ -87,9 +98,11 @@ function render(){updateCounts();$$('[data-page]').forEach(b=>{b.classList.toggl
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(page==='collections')$('#collection-grid').innerHTML=collections.map(c=>`${library.filter(c.filter).length} CATS · CURATED COLLECTION ${c.title} ${c.description}
`).join('');
+ if(page==='collections'){$('#collection-grid').innerHTML=collections.map(c=>`${library.filter(c.filter).length} CATS · CURATED COLLECTION ${c.title} ${c.description}
`).join('');renderSets()}
+ $('#stash').hidden=page!=='collections';
$('#mood-filters').innerHTML=moods.map(([id,symbol,label])=>`${symbol} ${label} `).join('');
- const list=filterMemes(),display=list.slice(0,limit);$('#meme-grid').innerHTML=display.map(card).join('');
+ const list=filterMemes(),display=list.slice(0,limit);$('#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} `:'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;
@@ -142,7 +155,7 @@ function info(title,content){$('#info-title').textContent=title;$('#info-content
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 R A random cat from this view ← / → Previous / next cat in the viewer, or vote in the Arena T / B / C / F Reroll Frankenmeme top / bottom / cat / full chaos S Save the cat in the viewer Space Pause / resume autoplay in the viewer Esc Close 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()}});
+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()}});
$('.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;$('#arena-skip').onclick=skipBattle;$('#arena-reset').onclick=resetArena;$('#lab-top').onclick=()=>rollLab('top');$('#lab-bottom').onclick=()=>rollLab('bottom');$('#lab-cat').onclick=()=>rollLab('cat');$('#lab-chaos').onclick=()=>rollLab('all');$('#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.')};
@@ -157,6 +170,8 @@ applyTheme(readState('theme',matchMedia('(prefers-color-scheme: dark)').matches?
$('#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-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-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.`)}});
$('#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'));
diff --git a/public/index.html b/public/index.html
index c91d802..7f6af2f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -42,6 +42,7 @@
How the Arena works Two cats enter. Tap the funnier one — or press ← / → .Build a streak. Every crown adds to your judging streak.Skips reset the streak. Cowardice has consequences. Stats stay in this browser.
Skip this matchupReset my stats ← vote left → vote right · swipe on phone
The Frankenmeme Lab Stitch a top from one cat, a bottom from another, and a third cat entirely. No ethics committee was consulted.
🎲 Top 🎲 Bottom 🎲 Cat ⚡ Full chaos
Save creation PNG
T top B bottom C cat F full chaos
+
Rounding up the cats… Sort by Chimp’s picks A–Z Unseen first
@@ -53,8 +54,9 @@
Discover Arena Lab Saved Collections Creations
-
+
THE NONSENSE LAB
Make a little mischief.
+
diff --git a/public/style.css b/public/style.css
index f46403f..6776338 100644
--- a/public/style.css
+++ b/public/style.css
@@ -38,6 +38,25 @@
.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}}
+#stash{display:grid;gap:16px;margin-top:18px}
+.sets-panel,.trophies{background:var(--paper);border:1px solid var(--line);border-radius:13px;padding:16px}
+.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}
+.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/scripts/smoke.mjs b/scripts/smoke.mjs
index f2c5852..5e1353e 100644
--- a/scripts/smoke.mjs
+++ b/scripts/smoke.mjs
@@ -30,7 +30,7 @@ const html = readFileSync(join(pub, 'index.html'), 'utf8');
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', 'meme-grid', 'viewer', 'editor']) {
+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', 'stash', 'sets-panel', 'trophies', 'viewer-collect', 'collect-dialog', 'collect-list', 'collect-new', 'collect-new-name', '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']) {
From 8a0d1caa7b77f63f2df7f96296ccd4c7dd6f50c8 Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:50:23 +0200
Subject: [PATCH 09/37] Smoke test now cross-checks every element id
---
scripts/smoke.mjs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/scripts/smoke.mjs b/scripts/smoke.mjs
index 5e1353e..8fba880 100644
--- a/scripts/smoke.mjs
+++ b/scripts/smoke.mjs
@@ -27,6 +27,14 @@ for (const [i, m] of lib.entries()) {
}
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}`);
From 3005d4ce3e70c510210cfedcc34f608a451de1db Mon Sep 17 00:00:00 2001
From: SaltKing0 <318362007+SaltKing0@users.noreply.github.com>
Date: Mon, 14 Sep 2026 18:51:52 +0200
Subject: [PATCH 10/37] Add synthesized sound effects and milestone confetti
---
public/app.js | 31 +++++++++++++++++++++----------
public/index.html | 5 +++--
public/style.css | 3 ++-
scripts/smoke.mjs | 2 +-
4 files changed, 27 insertions(+), 14 deletions(-)
diff --git a/public/app.js b/public/app.js
index fa035f9..4c1d783 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:' ',moon:' ',sun:' ',trophy:' ',flask:' ',folderplus:' ',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:' ',heart:' ',folder:' ',sparkles:' ',shuffle:' ',play:' ',pause:' ',keyboard:' ',search:' ',plus:' ','arrow-right':' ','arrow-left':' ','arrow-down':' ',x:' ',download:' ',upload:' ',edit:' ',link:' ',check:' '};
const icon=n=>`${paths[n]||paths.sparkles} `;
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));}
@@ -27,10 +27,12 @@ function voteBattle(winnerId){if(!battleLeft||!battleRight)return;
battleStats.wins[winnerId]=battleWins(winnerId)+1;battleStats.battles++;battleStats.streak++;battleStats.best=Math.max(battleStats.best,battleStats.streak);battleStats.lastWinner=winnerId;
setLocal('battles',battleStats);
const w=allMemes().find(m=>m.id===winnerId);
- toast(streakMilestones[battleStats.streak]||`“${w?.title||'Cat'}” takes the crown. ${battleTaunts[Math.floor(Math.random()*battleTaunts.length)]}`);
+ const ms=streakMilestones[battleStats.streak];
+ 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.');battleStats.streak=0;setLocal('battles',battleStats);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};setLocal('battles',battleStats);pickBattle();renderArena();toast('Fresh slate. The cats forgive you.')}
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;
@@ -43,7 +45,7 @@ function renderLab(){if(!labTop||!labBottom||!labCat){rollLab();return}const m=l
const stars='★'.repeat(curseRating())+'☆'.repeat(5-curseRating());
const names=['blessed','mildly cursed','cursed','deeply cursed','FORBIDDEN'];
$('#lab-provenance').innerHTML=`Above the cat: ${esc(labTop.title)} Below the cat: ${esc(labBottom.title)} The cat: ${esc(labCat.title)} ${stars} ${names[curseRating()-1]} `}
-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);navigate('studio');toast('Abomination preserved for future generations.')}catch{toast('Could not save in this browser. Use PNG to keep your creation.')}}
+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);sfx('fanfare');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);
@@ -111,12 +113,12 @@ function render(){updateCounts();$$('[data-page]').forEach(b=>{b.classList.toggl
$('#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='';if(p==='arena'&&(!battleLeft||!battleRight))pickBattle();if(p==='lab'&&(!labTop||!labBottom||!labCat))rollLab();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;
+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');
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()}
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`;
@@ -154,11 +156,11 @@ async function download(m,button){if(button)button.disabled=true;try{const blob=
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 R A random cat from this view ← / → Previous / next cat in the viewer, or vote in the Arena T / B / C / F Reroll Frankenmeme top / bottom / cat / full chaos S Save the cat in the viewer Space Pause / resume autoplay in the viewer Esc Close 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.
`)}
+function shortcuts(){info('Less clicking. More cats.',`/ Jump to search R A random cat from this view ← / → Previous / next cat in the viewer, or vote in the Arena T / B / C / F Reroll Frankenmeme top / bottom / cat / full chaos M Mute / unmute sounds S Save the cat in the viewer Space Pause / resume autoplay in the viewer Esc Close 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()}});
$('.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;$('#arena-skip').onclick=skipBattle;$('#arena-reset').onclick=resetArena;$('#lab-top').onclick=()=>rollLab('top');$('#lab-bottom').onclick=()=>rollLab('bottom');$('#lab-cat').onclick=()=>rollLab('cat');$('#lab-chaos').onclick=()=>rollLab('all');$('#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.')};
+$('#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-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')};
@@ -167,6 +169,15 @@ setDensity(readState('dense',false)===true);$('#view-comfy').onclick=()=>{setDen
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;
+for(const ev of ['pointerdown','keydown'])document.addEventListener(ev,()=>{audioReady=true},{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(vol,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==='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;b.innerHTML=icon(soundOn?'volume':'mute');b.setAttribute('aria-label',soundOn?'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-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.')}};
@@ -175,12 +186,12 @@ document.addEventListener('submit',e=>{if(e.target.id==='new-set-form'){e.preven
$('#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}};
+$('#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();sfx('fanfare');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()}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')}else if(page==='lab'&&e.key.toLowerCase()==='b'){e.preventDefault();rollLab('bottom')}else if(page==='lab'&&e.key.toLowerCase()==='c'){e.preventDefault();rollLab('cat')}else if(page==='lab'&&e.key.toLowerCase()==='f'){e.preventDefault();rollLab('all')}});
+ 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(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});
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});
diff --git a/public/index.html b/public/index.html
index 7f6af2f..5363172 100644
--- a/public/index.html
+++ b/public/index.html
@@ -31,7 +31,7 @@
-
+
✳ 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.
Find me a good cat or follow the thread: 🏟 Crown 🧪 Stitch ✨ Make 📦 Keep
@@ -58,5 +58,6 @@
THE NONSENSE LAB
Make a little mischief.
-
+
+