const { useState, useEffect, useCallback, useRef, useMemo } = React;

// ─── Constants ───────────────────────────────────────────────────────────────

// Theme-aware colors via CSS variables
const BG = 'var(--bg)';
const TILE_COLOR = 'var(--tile-color)';
const TILE_BG = 'var(--tile-bg)';
const GREEN = '#4ADE80';
const AMBER = '#FBBF24';
const RED = '#EF6461';
const GREEN_BG = 'var(--green-bg)';
const AMBER_BG = 'var(--amber-bg)';
const RED_BG = 'var(--red-bg)';
const GREEN_BG_MUTED = 'var(--green-bg-muted)';
const AMBER_BG_MUTED = 'var(--amber-bg-muted)';
const RED_BG_MUTED = 'var(--red-bg-muted)';
const LOCKED_BORDER = 'var(--locked-border)';
const SPLASH_BG = '#E8725C';
const MUTED = 'var(--muted)';
const FADED_DARK = 'rgba(26,26,30,0.55)';
const OVERLAY_BG = 'var(--overlay-bg)';
const SUBTLE_BORDER = 'var(--subtle-border)';

const COUNT = 6;
const TILE_H = 62;
const GAP = 10;
const STRIDE = TILE_H + GAP;
const DEAD_ZONE = 7;
const EPOCH = new Date('2026-01-01T00:00:00').getTime();
const MAX_ATTEMPTS = 3;

const DIFFICULTY_LABELS = { 1: 'Easy', 2: 'Medium', 3: 'Tricky', 4: 'Hard', 5: 'Devious' };
const DIFFICULTY_COLORS = { 1: '#4ADE80', 2: '#60A5FA', 3: '#FBBF24', 4: '#F97316', 5: '#EF4444' };
const SCORE_LABELS = { 3: 'Flawless', 2: 'Sharp', 1: 'Got it', 0: '' };
const APP_VERSION = '__VERSION__';

// ─── Puzzle Bank ─────────────────────────────────────────────────────────────

const PUZZLES = [
  {
    id: 1,
    difficulty: 3,
    category: 'Science',
    items: ['Venus', 'Mercury', 'Earth', 'Mars', 'Jupiter', 'Neptune'],
    itemValues: ["462°C", "167°C", "15°C", "-63°C", "-145°C", "-214°C"],
    dimension: 'Average surface temperature (hottest → coldest)',
    trapDimension: 'Distance from the Sun',
    funFact: 'Venus is hotter than Mercury despite being farther from the Sun, thanks to its thick CO₂ atmosphere creating a runaway greenhouse effect (462°C).'
  },
  {
    id: 2,
    difficulty: 3,
    category: 'Geography',
    items: ['Canada', 'Indonesia', 'Norway', 'Philippines', 'Japan', 'Australia'],
    itemValues: ["202,080 km", "99,083 km", "58,133 km", "36,289 km", "29,751 km", "25,760 km"],
    dimension: 'Length of coastline (longest → shortest)',
    trapDimension: 'Land area',
    funFact: 'Canada has the world\'s longest coastline at 202,080 km — more than the next 5 countries combined. Indonesia\'s 17,000+ islands give it the #2 spot.'
  },
  {
    id: 3,
    difficulty: 2,
    category: 'Animals',
    items: ['Bowhead Whale', 'Tortoise', 'Macaw', 'Elephant', 'Horse', 'Hamster'],
    itemValues: ["200+ yrs", "190 yrs", "80 yrs", "70 yrs", "30 yrs", "3 yrs"],
    dimension: 'Maximum lifespan (longest → shortest)',
    trapDimension: 'Body size',
    funFact: 'Bowhead whales can live over 200 years. Some tortoises reach 190+. Macaws can outlive most mammals at 60-80 years.'
  },
  {
    id: 4,
    difficulty: 4,
    category: 'Food & Drink',
    items: ['Drip Coffee', 'Energy Drink', 'Espresso', 'Black Tea', 'Green Tea', 'Cola'],
    itemValues: ["95 mg", "80 mg", "63 mg", "47 mg", "28 mg", "22 mg"],
    dimension: 'Caffeine per serving (most → least)',
    trapDimension: 'Perceived "strength" or intensity',
    funFact: 'A standard cup of drip coffee (~95mg) has more caffeine than a shot of espresso (~63mg). Espresso is more concentrated, but the serving is much smaller.'
  },
  {
    id: 5,
    difficulty: 4,
    category: 'Geography',
    items: ['New Jersey', 'Rhode Island', 'Massachusetts', 'Connecticut', 'Maryland', 'Ohio'],
    itemValues: ["1,263/sq mi", "1,018/sq mi", "901/sq mi", "743/sq mi", "636/sq mi", "290/sq mi"],
    dimension: 'Population density (most → least dense)',
    trapDimension: 'Total population',
    funFact: 'New Jersey is the most densely populated US state at ~1,200 people/sq mi. Rhode Island, despite being the smallest state by area, is #2 in density.'
  },
  {
    id: 6,
    difficulty: 3,
    category: 'Science',
    items: ['Osmium', 'Gold', 'Lead', 'Iron', 'Diamond', 'Aluminum'],
    itemValues: ["22.59 g/cm³", "19.32 g/cm³", "11.34 g/cm³", "7.87 g/cm³", "3.51 g/cm³", "2.70 g/cm³"],
    dimension: 'Density (heaviest → lightest per unit volume)',
    trapDimension: 'Atomic number or perceived "heaviness"',
    funFact: 'Osmium is the densest naturally occurring element at 22.59 g/cm³ — nearly twice as dense as lead (11.34 g/cm³). Diamond, despite being precious, is only 3.51 g/cm³.'
  },
  {
    id: 7,
    difficulty: 2,
    category: 'Geography',
    items: ['La Paz', 'Bogotá', 'Addis Ababa', 'Mexico City', 'Nairobi', 'Denver'],
    itemValues: ["3,640 m", "2,640 m", "2,355 m", "2,240 m", "1,795 m", "1,609 m"],
    dimension: 'Elevation above sea level (highest → lowest)',
    trapDimension: 'Latitude (distance from equator)',
    funFact: 'La Paz, Bolivia sits at ~3,640m (11,942 ft) — the highest administrative capital in the world. Denver\'s "Mile High" (1,609m) is modest by comparison.'
  },
  {
    id: 8,
    difficulty: 3,
    category: 'Food & Drink',
    items: ['Lychee', 'Mango', 'Grape', 'Banana', 'Strawberry', 'Avocado'],
    itemValues: ["15 g", "14 g", "13 g", "12 g", "5 g", "0.7 g"],
    dimension: 'Sugar content per 100g (most → least)',
    trapDimension: 'Perceived sweetness',
    funFact: 'Lychee contains ~15g sugar per 100g, topping most common fruits. Avocado has less than 1g — it\'s technically a fruit but tastes nothing like one.'
  },
  {
    id: 9,
    difficulty: 1,
    category: 'Geography',
    items: ['Nile', 'Amazon', 'Yangtze', 'Mississippi', 'Danube', 'Thames'],
    itemValues: ["6,650 km", "6,400 km", "6,300 km", "3,730 km", "2,850 km", "346 km"],
    dimension: 'River length (longest → shortest)',
    trapDimension: 'Water volume or fame',
    funFact: 'The Nile (~6,650 km) edges out the Amazon (~6,400 km) for length, but the Amazon carries more water than the next 7 largest rivers combined.'
  },
  {
    id: 10,
    difficulty: 2,
    category: 'Culture',
    items: ['University of Bologna', 'Oxford', 'Cambridge', 'Harvard', 'MIT', 'Stanford'],
    itemValues: ["1088", "1096", "1209", "1636", "1861", "1885"],
    dimension: 'Year founded (oldest → newest)',
    trapDimension: 'Prestige ranking or fame',
    funFact: 'The University of Bologna was founded in 1088, making it nearly 300 years older than Oxford (~1096–1167). MIT (1861) and Stanford (1885) are relative newcomers.'
  },
  {
    id: 11,
    difficulty: 4,
    category: 'Animals',
    items: ['Elephant', 'Giraffe', 'Rhino', 'Horse', 'Dog', 'Cat'],
    itemValues: ["22 mo", "15 mo", "15 mo", "11 mo", "63 days", "63 days"],
    dimension: 'Gestation period (longest → shortest)',
    trapDimension: 'Body weight',
    funFact: 'Elephants have the longest gestation of any land animal at ~22 months. Giraffes (15 months) carry longer than rhinos (15-16 months) despite being lighter.'
  },
  {
    id: 12,
    difficulty: 3,
    category: 'Science',
    items: ['Bamboo', 'Kudzu', 'Eucalyptus', 'Oak', 'Cactus', 'Bonsai'],
    itemValues: ["91 cm/day", "30 cm/day", "3 m/yr", "60 cm/yr", "3 cm/yr", "~0 cm/yr"],
    dimension: 'Growth rate (fastest → slowest)',
    trapDimension: 'Final size',
    funFact: 'Bamboo can grow up to 91 cm (35 inches) per day — the fastest-growing plant on Earth. Bonsai trees are deliberately kept small but are normal species; their slow growth is enforced by pruning.'
  },
  {
    id: 13,
    difficulty: 5,
    category: 'Culture',
    items: ['Minecraft', 'GTA V', 'Tetris', 'Wii Sports', 'Pac-Man', 'Fortnite'],
    itemValues: ["300 M+", "200 M+", "100 M+", "82.9 M", "43 M", "F2P"],
    dimension: 'Copies sold, all-time (most → least)',
    trapDimension: 'Cultural impact or recency',
    funFact: 'Minecraft has sold over 300 million copies, making it the best-selling video game ever. Tetris and GTA V follow. Fortnite, despite massive popularity, is free-to-play with fewer "sales."'
  },
  {
    id: 14,
    difficulty: 4,
    category: 'Geography',
    items: ['Pacific', 'Atlantic', 'Indian', 'Southern', 'Arctic', 'Mediterranean'],
    itemValues: ["4,280 m", "3,646 m", "3,741 m", "3,270 m", "1,500 m", "1,205 m"],
    dimension: 'Average depth (deepest → shallowest)',
    trapDimension: 'Surface area',
    funFact: 'The Pacific Ocean\'s average depth is ~4,280m. The Mediterranean Sea (~1,500m avg) is surprisingly deep for an enclosed sea, but shallow compared to oceans.'
  },
  {
    id: 15,
    difficulty: 2,
    category: 'Animals',
    items: ['Peregrine Falcon', 'Golden Eagle', 'Cheetah', 'Sailfish', 'Horse', 'Human'],
    itemValues: ["389 km/h", "320 km/h", "112 km/h", "110 km/h", "88 km/h", "45 km/h"],
    dimension: 'Top speed (fastest → slowest)',
    trapDimension: 'Size or perceived danger',
    funFact: 'The Peregrine Falcon reaches 389 km/h (242 mph) in a hunting dive — the fastest animal on Earth. The cheetah\'s famous 112 km/h is only #3 overall.'
  },
  {
    id: 16,
    difficulty: 1,
    category: 'Food & Drink',
    items: ['Carolina Reaper', 'Habanero', 'Cayenne', 'Jalape\u00f1o', 'Poblano', 'Bell Pepper'],
    itemValues: ["1.64 M SHU", "200 K SHU", "40 K SHU", "5 K SHU", "1.5 K SHU", "0 SHU"],
    dimension: 'Scoville heat units (hottest \u2192 mildest)',
    trapDimension: 'Size of the pepper',
    funFact: 'The Carolina Reaper averages 1.64 million Scoville Heat Units \u2014 over 200x hotter than a jalape\u00f1o. It was bred specifically to break heat records.'
  },
  {
    id: 17,
    difficulty: 2,
    category: 'Space',
    items: ['Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Earth', 'Mars'],
    itemValues: ["139,820 km", "116,460 km", "50,724 km", "49,244 km", "12,742 km", "6,779 km"],
    dimension: 'Planet diameter (largest \u2192 smallest)',
    trapDimension: 'Distance from the Sun',
    funFact: 'Jupiter\'s diameter of 139,820 km is 11x Earth\'s. You could fit all other planets inside Jupiter with room to spare.'
  },
  {
    id: 18,
    difficulty: 2,
    category: 'Science',
    items: ['Tungsten', 'Iron', 'Copper', 'Silver', 'Lead', 'Mercury'],
    itemValues: ["3,422°C", "1,538°C", "1,085°C", "962°C", "327°C", "-38.8°C"],
    dimension: 'Melting point (highest \u2192 lowest)',
    trapDimension: 'Hardness or perceived toughness',
    funFact: 'Tungsten has the highest melting point of any element at 3,422\u00b0C \u2014 so extreme that it\'s used in rocket nozzles and light bulb filaments.'
  },
  {
    id: 19,
    difficulty: 2,
    category: 'Animals',
    items: ['Hummingbird', 'Hamster', 'Rabbit', 'Cat', 'Human', 'Elephant'],
    itemValues: ["600 bpm", "500 bpm", "205 bpm", "140 bpm", "70 bpm", "28 bpm"],
    dimension: 'Heart rate (fastest \u2192 slowest)',
    trapDimension: 'Body speed or energy level',
    funFact: 'A hummingbird\'s heart beats around 600 times per minute \u2014 10 beats every second. Smaller body almost always means faster heartbeat across all mammals.'
  },
  {
    id: 20,
    difficulty: 3,
    category: 'Science',
    items: ['Diamond', 'Steel', 'Glass', 'Water', 'Air', 'Rubber'],
    itemValues: ["12,000 m/s", "5,120 m/s", "4,540 m/s", "1,480 m/s", "343 m/s", "60 m/s"],
    dimension: 'Speed of sound (fastest \u2192 slowest)',
    trapDimension: 'Density or weight of material',
    funFact: 'Sound travels through diamond at 12,000 m/s \u2014 35x faster than through air. Counter-intuitively, sound moves faster through stiffer materials, not denser ones.'
  },
  {
    id: 21,
    difficulty: 2,
    category: 'Architecture',
    items: ['Burj Khalifa', 'Shanghai Tower', 'Makkah Clock Tower', 'One World Trade', 'Taipei 101', 'Empire State'],
    itemValues: ["828 m", "632 m", "601 m", "541 m", "508 m", "381 m"],
    dimension: 'Building height (tallest \u2192 shortest)',
    trapDimension: 'Fame or iconic status',
    funFact: 'The Burj Khalifa at 828 meters is so tall that temperatures at its peak can be 10\u00b0C cooler than at ground level. The Empire State Building isn\'t even in the top 40 anymore.'
  },
  {
    id: 22,
    difficulty: 1,
    category: 'Food & Drink',
    items: ['Olive Oil', 'Peanut Butter', 'Cheddar', 'Bread', 'Banana', 'Cucumber'],
    itemValues: ["884 kcal", "588 kcal", "402 kcal", "265 kcal", "89 kcal", "15 kcal"],
    dimension: 'Calories per 100g (most \u2192 least)',
    trapDimension: 'How filling or heavy the food feels',
    funFact: 'Olive oil packs 884 calories per 100g \u2014 making it one of the most calorie-dense foods in any kitchen. A cucumber, at just 15 calories per 100g, is 98% water.'
  },
  {
    id: 23,
    difficulty: 2,
    category: 'History',
    items: ['Great Pyramid', 'Hanging Gardens', 'Temple of Artemis', 'Statue of Zeus', 'Mausoleum', 'Colossus of Rhodes'],
    itemValues: ["~2560 BC", "~600 BC", "~550 BC", "~435 BC", "~350 BC", "~280 BC"],
    dimension: 'Estimated construction date (oldest \u2192 newest)',
    trapDimension: 'Grandeur or fame',
    funFact: 'The Great Pyramid of Giza (~2560 BC) is the oldest of the Seven Wonders by over 1,900 years \u2014 and the only one still standing today.'
  },
  {
    id: 24,
    difficulty: 3,
    category: 'Geography',
    items: ['Canada\u2013USA', 'Russia\u2013Kazakhstan', 'Argentina\u2013Chile', 'China\u2013Mongolia', 'India\u2013Bangladesh', 'USA\u2013Mexico'],
    itemValues: ["8,893 km", "7,644 km", "6,691 km", "4,630 km", "4,142 km", "3,145 km"],
    dimension: 'Shared border length (longest \u2192 shortest)',
    trapDimension: 'Country size',
    funFact: 'The Canada\u2013USA border stretches 8,893 km \u2014 the longest international border in the world. The Alaska\u2013Canada section alone is longer than the entire USA\u2013Mexico border.'
  },
  {
    id: 25,
    difficulty: 3,
    category: 'Science',
    items: ['Diamond', 'Sapphire', 'Quartz', 'Marble', 'Gold', 'Talc'],
    itemValues: ["10", "9", "7", "3", "2.5", "1"],
    dimension: 'Mohs hardness (hardest \u2192 softest)',
    trapDimension: 'Value or preciousness',
    funFact: 'Pure gold is so soft (2.5 Mohs) that it can be dented with a fingernail. That\'s why jewelry uses gold alloys \u2014 pure gold would lose its shape from everyday wear.'
  },
  {
    id: 26,
    difficulty: 3,
    category: 'Geography',
    items: ['Caspian Sea', 'Lake Superior', 'Lake Victoria', 'Lake Huron', 'Lake Michigan', 'Lake Baikal'],
    itemValues: ["371,000 km²", "82,100 km²", "68,800 km²", "59,600 km²", "58,000 km²", "31,722 km²"],
    dimension: 'Surface area (largest \u2192 smallest)',
    trapDimension: 'Depth or water volume',
    funFact: 'Lake Baikal holds 20% of the world\'s surface freshwater \u2014 more than all the Great Lakes combined \u2014 yet by surface area it\'s the smallest lake in this list.'
  },
  {
    id: 27,
    difficulty: 1,
    category: 'Food & Drink',
    items: ['Port Wine', 'Sake', 'Champagne', 'Hard Seltzer', 'Guinness', 'Kombucha'],
    itemValues: ["20% ABV", "15% ABV", "12% ABV", "5% ABV", "4.2% ABV", "0.5% ABV"],
    dimension: 'Alcohol by volume (highest \u2192 lowest)',
    trapDimension: 'Perceived strength or heaviness',
    funFact: 'Guinness at just 4.2% ABV is lighter than most IPAs and even some hard seltzers (~5%). Its dark color and creamy head create a powerful illusion of strength.'
  },
  {
    id: 28,
    difficulty: 4,
    category: 'Animals',
    items: ['Snail', 'Dolphin', 'Dog', 'Human', 'Cat', 'Hen'],
    itemValues: ["~20,000", "~250", "42", "32", "30", "0"],
    dimension: 'Number of teeth (most \u2192 fewest)',
    trapDimension: 'Mouth size or jaw strength',
    funFact: 'Garden snails have around 20,000 microscopic teeth arranged on a ribbon-like radula. Hens have zero \u2014 they use a gizzard filled with small stones to grind their food.'
  },
  {
    id: 29,
    difficulty: 3,
    category: 'Technology',
    items: ['Email', 'Pong', 'Apple Mac', 'World Wide Web', 'Google', 'iPhone'],
    itemValues: ["1971", "1972", "1984", "1991", "1998", "2007"],
    dimension: 'Year of first public release (earliest \u2192 latest)',
    trapDimension: 'Perceived modernity',
    funFact: 'The first email was sent by Ray Tomlinson in 1971, using the now-ubiquitous @ symbol \u2014 a year before Pong launched and over a decade before the personal computer revolution.'
  },
  {
    id: 30,
    difficulty: 2,
    category: 'Geography',
    items: ['Angel Falls', 'Tugela Falls', 'Yosemite Falls', 'Victoria Falls', 'Niagara Falls', 'Gullfoss'],
    itemValues: ["979 m", "948 m", "739 m", "108 m", "51 m", "32 m"],
    dimension: 'Waterfall height (tallest \u2192 shortest)',
    trapDimension: 'Water volume or fame',
    funFact: 'Angel Falls in Venezuela drops 979 meters \u2014 nearly 20x the height of Niagara Falls. The water free-falls so far that much of it evaporates into mist before reaching the bottom.'
  },
  {
    id: 31,
    difficulty: 3,
    category: 'History',
    items: ['Colosseum', 'Notre-Dame', 'Machu Picchu', 'Taj Mahal', 'Big Ben', 'Eiffel Tower'],
    itemValues: ["80 AD", "1260", "1450", "1653", "1859", "1889"],
    dimension: 'Year of completion (earliest \u2192 latest)',
    trapDimension: 'Architectural style or perceived age',
    funFact: 'Machu Picchu (~1450) was built over a century after Notre-Dame (~1260). Despite its ancient mystique, the Inca citadel was abandoned just 100 years after being built.'
  },
  {
    id: 32,
    difficulty: 3,
    category: 'Animals',
    items: ['Arctic Tern', 'Humpback Whale', 'Caribou', 'Monarch Butterfly', 'Salmon', 'Wildebeest'],
    itemValues: ["70,000 km", "8,000 km", "5,000 km", "4,800 km", "3,000 km", "1,800 km"],
    dimension: 'Annual migration distance (longest \u2192 shortest)',
    trapDimension: 'Body size',
    funFact: 'The Arctic Tern migrates ~70,000 km annually \u2014 pole to pole and back. Over its 30-year lifespan, it travels roughly three round trips to the Moon.'
  },
  {
    id: 33,
    difficulty: 3,
    category: 'Food & Drink',
    items: ['Sugarcane', 'Corn', 'Wheat', 'Rice', 'Potato', 'Coffee'],
    itemValues: ["1.9 B t", "1.2 B t", "780 M t", "755 M t", "388 M t", "10 M t"],
    dimension: 'Global production in tonnes (most \u2192 least)',
    trapDimension: 'Cultural importance or price',
    funFact: 'Sugarcane is the world\'s most-produced crop at nearly 2 billion tonnes annually \u2014 mostly processed into sugar and ethanol. Coffee, despite its global ubiquity, produces only ~10 million tonnes.'
  },
  {
    id: 34,
    difficulty: 2,
    category: 'Geography',
    items: ['Greenland', 'New Guinea', 'Borneo', 'Madagascar', 'Sumatra', 'Honshu'],
    itemValues: ["2.17 M km²", "786 K km²", "748 K km²", "587 K km²", "473 K km²", "228 K km²"],
    dimension: 'Island area (largest \u2192 smallest)',
    trapDimension: 'Population or fame',
    funFact: 'Greenland is the world\'s largest island at 2.17 million km\u00b2 \u2014 nearly 3x the size of second-place New Guinea. Australia is bigger but classified as a continent, not an island.'
  },
  {
    id: 35,
    difficulty: 4,
    category: 'Science',
    items: ['Uranium-238', 'Carbon-14', 'Cobalt-60', 'Iodine-131', 'Radon-222', 'Polonium-214'],
    itemValues: ["4.5 B yrs", "5,730 yrs", "5.27 yrs", "8.02 days", "3.82 days", "0.16 ms"],
    dimension: 'Radioactive half-life (longest \u2192 shortest)',
    trapDimension: 'How dangerous they sound',
    funFact: 'Uranium-238 has a half-life of 4.5 billion years \u2014 roughly the age of the Earth itself. Polonium-214 decays in just 0.16 milliseconds, a ratio spanning 26 orders of magnitude.'
  },
  {
    id: 36,
    difficulty: 3,
    category: 'Culture',
    items: ['Mandarin', 'Spanish', 'English', 'Hindi', 'Arabic', 'Portuguese'],
    itemValues: ["920 M", "485 M", "370 M", "345 M", "310 M", "260 M"],
    dimension: 'Number of native speakers (most \u2192 fewest)',
    trapDimension: 'Global influence or number of countries',
    funFact: 'Mandarin Chinese has ~920 million native speakers \u2014 nearly 2.5x more than Spanish. English, despite being the global lingua franca, ranks third with ~370 million native speakers.'
  },
  {
    id: 37,
    difficulty: 3,
    category: 'Science',
    items: ['Lightning Bolt', 'Sun\'s Surface', 'Lava', 'Oven', 'Boiling Water', 'Human Body'],
    itemValues: ["30,000°C", "5,500°C", "1,200°C", "230°C", "100°C", "37°C"],
    dimension: 'Temperature (hottest \u2192 coldest)',
    trapDimension: 'Size or destructive power',
    funFact: 'A lightning bolt reaches ~30,000\u00b0C \u2014 about 5x hotter than the surface of the Sun (5,500\u00b0C). The bolt is so brief that its total energy is only enough to power a light bulb for a few months.'
  },
  {
    id: 38,
    difficulty: 3,
    category: 'Geography',
    items: ['Atacama Desert', 'Sahara Desert', 'Death Valley', 'Dubai', 'Los Angeles', 'London'],
    itemValues: ["~0 mm", "25 mm", "60 mm", "94 mm", "380 mm", "600 mm"],
    dimension: 'Annual rainfall (least \u2192 most)',
    trapDimension: 'Temperature or heat',
    funFact: 'Parts of the Atacama Desert in Chile have never recorded rainfall. It\'s so dry that NASA uses it as a Mars analog for testing rover equipment.'
  },
  {
    id: 39,
    difficulty: 2,
    category: 'Animals',
    items: ['Koala', 'Brown Bat', 'Python', 'Cat', 'Dog', 'Giraffe'],
    itemValues: ["22 hrs", "20 hrs", "18 hrs", "13 hrs", "12 hrs", "0.5 hrs"],
    dimension: 'Hours of sleep per day (most \u2192 least)',
    trapDimension: 'Perceived laziness or activity level',
    funFact: 'Koalas sleep up to 22 hours a day \u2014 their eucalyptus diet is so low in nutrition that they conserve energy by barely moving. Giraffes survive on just 30 minutes of deep sleep.'
  },
  {
    id: 40,
    difficulty: 2,
    category: 'Food & Drink',
    items: ['Parmesan', 'Chicken Breast', 'Salmon', 'Egg', 'Tofu', 'Rice'],
    itemValues: ["36 g", "31 g", "22 g", "13 g", "8 g", "2.7 g"],
    dimension: 'Protein per 100g (most \u2192 least)',
    trapDimension: 'Health food reputation',
    funFact: 'Parmesan cheese contains ~36g of protein per 100g \u2014 more than chicken breast. Its 36-month aging process concentrates the protein as moisture evaporates.'
  },
  {
    id: 41,
    difficulty: 3,
    category: 'History',
    items: ['Holy Roman Empire', 'Ottoman Empire', 'Spanish Empire', 'Mughal Empire', 'Mongol Empire', 'Third Reich'],
    itemValues: ["844 yrs", "623 yrs", "369 yrs", "331 yrs", "162 yrs", "12 yrs"],
    dimension: 'Duration in years (longest \u2192 shortest)',
    trapDimension: 'Territory size or military fame',
    funFact: 'The Holy Roman Empire lasted 844 years (962\u20131806) \u2014 despite Voltaire\'s famous quip that it was "neither holy, nor Roman, nor an empire." The Mongol Empire, the largest contiguous land empire ever, lasted only 162 years.'
  },
  {
    id: 42,
    difficulty: 4,
    category: 'Science',
    items: ['Silver', 'Copper', 'Gold', 'Aluminum', 'Iron', 'Glass'],
    itemValues: ["63 MS/m", "59 MS/m", "45 MS/m", "37 MS/m", "10 MS/m", "~0"],
    dimension: 'Electrical conductivity (most \u2192 least conductive)',
    trapDimension: 'Everyday use in wiring',
    funFact: 'Silver is the most electrically conductive element \u2014 beating copper by about 5%. We use copper for wiring only because it\'s roughly 100x cheaper.'
  },
  {
    id: 43,
    difficulty: 2,
    category: 'Geography',
    items: ['Everest', 'Aconcagua', 'Denali', 'Kilimanjaro', 'Elbrus', 'Vinson Massif'],
    itemValues: ["8,849 m", "6,961 m", "6,190 m", "5,895 m", "5,642 m", "4,892 m"],
    dimension: 'Peak elevation (tallest \u2192 shortest)',
    trapDimension: 'Continent size or fame',
    funFact: 'Kilimanjaro (5,895m) in Tanzania is taller than Europe\'s highest peak, Mt. Elbrus (5,642m). Being near the equator, Kilimanjaro has glaciers despite being in tropical Africa.'
  },
  {
    id: 44,
    difficulty: 3,
    category: 'Technology',
    items: ['IBM', 'HP', 'Sony', 'Microsoft', 'Apple', 'Google'],
    itemValues: ["1911", "1939", "1946", "1975", "1976", "1998"],
    dimension: 'Company founding year (earliest \u2192 latest)',
    trapDimension: 'Market cap or perceived innovation',
    funFact: 'Microsoft was founded in 1975, a year before Apple (1976). Despite Apple\'s reputation as the original tech innovator, Bill Gates and Paul Allen got there first.'
  },
  {
    id: 45,
    difficulty: 1,
    category: 'Animals',
    items: ['Wandering Albatross', 'Andean Condor', 'Bald Eagle', 'Great Horned Owl', 'Crow', 'Hummingbird'],
    itemValues: ["3.5 m", "3.3 m", "2.1 m", "1.4 m", "1.0 m", "0.11 m"],
    dimension: 'Wingspan (largest \u2192 smallest)',
    trapDimension: 'Body weight or aggressiveness',
    funFact: 'The Wandering Albatross has the largest wingspan of any living bird at 3.5 meters (11.5 feet). They can glide for hours without a single wingbeat.'
  },
  {
    id: 46,
    difficulty: 4,
    category: 'Economics',
    items: ['Switzerland', 'Norway', 'USA', 'Japan', 'China', 'Egypt'],
    itemValues: ["$7.73", "$6.26", "$5.58", "$3.17", "$3.10", "$2.16"],
    dimension: 'Big Mac price (most \u2192 least expensive)',
    trapDimension: 'GDP or perceived wealth',
    funFact: 'A Big Mac costs ~$7.73 in Switzerland vs ~$2.16 in Egypt. The Big Mac Index, created by The Economist, uses this to measure purchasing power parity between currencies.'
  },
  {
    id: 47,
    difficulty: 5,
    category: 'Science',
    items: ['Oxygen', 'Silicon', 'Aluminum', 'Iron', 'Calcium', 'Gold'],
    itemValues: ["46.1%", "28.2%", "8.2%", "5.6%", "4.1%", "0.0004%"],
    dimension: 'Abundance in Earth\'s crust (most \u2192 least)',
    trapDimension: 'Perceived value or everyday visibility',
    funFact: 'Oxygen makes up 46% of Earth\'s crust by mass \u2014 it\'s bound up in rocks and minerals, not just the air. Silicon (28%) is second, which is why sand (silicon dioxide) is everywhere.'
  },
  {
    id: 48,
    difficulty: 3,
    category: 'Literature',
    items: ['War and Peace', 'Les Mis\u00e9rables', 'Don Quixote', 'Moby-Dick', 'Harry Potter PS', 'The Great Gatsby'],
    itemValues: ["580 K", "530 K", "430 K", "209 K", "77 K", "47 K"],
    dimension: 'Novel word count (longest \u2192 shortest)',
    trapDimension: 'Cultural weight or physical book size',
    funFact: 'War and Peace contains ~580,000 words. You could read The Great Gatsby (~47,000 words) twelve times over in the same word count.'
  },
  {
    id: 49,
    difficulty: 4,
    category: 'Science',
    items: ['Vacuum', 'Air', 'Water', 'Glass', 'Diamond', 'Silicon'],
    itemValues: ["300,000 km/s", "299,700 km/s", "225,000 km/s", "200,000 km/s", "124,000 km/s", "75,000 km/s"],
    dimension: 'Speed of light through material (fastest \u2192 slowest)',
    trapDimension: 'Transparency',
    funFact: 'Light slows to just 41% of its vacuum speed when passing through diamond \u2014 the same property that creates a diamond\'s famous sparkle and fire.'
  },
  {
    id: 50,
    difficulty: 4,
    category: 'Food & Drink',
    items: ['Guava', 'Bell Pepper', 'Kiwi', 'Strawberry', 'Orange', 'Apple'],
    itemValues: ["228 mg", "128 mg", "93 mg", "59 mg", "53 mg", "4.6 mg"],
    dimension: 'Vitamin C per 100g (most \u2192 least)',
    trapDimension: 'Association with Vitamin C',
    funFact: 'Guava contains ~228mg of Vitamin C per 100g \u2014 over 4x more than oranges. The orange\'s Vitamin C reputation is mostly marketing from the early 20th century.'
  },
  {
    id: 51,
    difficulty: 2,
    category: 'Geography',
    items: ['Reykjavik', 'Helsinki', 'Moscow', 'London', 'Washington DC', 'Canberra'],
    itemValues: ["64°N", "60°N", "55°N", "51°N", "38°N", "35°S"],
    dimension: 'Capital city latitude (northernmost \u2192 southernmost)',
    trapDimension: 'How cold or northern the country feels',
    funFact: 'Reykjavik, Iceland is the world\'s northernmost capital at 64\u00b0N. Moscow, despite its harsh winters, is further south at 55\u00b0N \u2014 roughly the same latitude as Edinburgh.'
  },
  {
    id: 52,
    difficulty: 3,
    category: 'Animals',
    items: ['Sperm Whale', 'Elephant Seal', 'Leatherback Turtle', 'Emperor Penguin', 'Dolphin', 'Sea Otter'],
    itemValues: ["2,250 m", "1,800 m", "1,280 m", "565 m", "300 m", "100 m"],
    dimension: 'Diving depth (deepest \u2192 shallowest)',
    trapDimension: 'Body size or swimming speed',
    funFact: 'Sperm whales can dive to 2,250 meters \u2014 deeper than most military submarines. They hold their breath for up to 90 minutes on a single dive.'
  },
  {
    id: 53,
    difficulty: 4,
    category: 'Science',
    items: ['Oxygen', 'Carbon', 'Hydrogen', 'Nitrogen', 'Calcium', 'Iron'],
    itemValues: ["65%", "18%", "10%", "3%", "1.5%", "0.006%"],
    dimension: 'Abundance in the human body by mass (most \u2192 least)',
    trapDimension: 'Dietary importance or health focus',
    funFact: 'Your body is 65% oxygen by mass \u2014 mostly locked in water and organic molecules. Iron, despite its vital role in blood, makes up only 0.006% of your body weight.'
  },
  {
    id: 54,
    difficulty: 3,
    category: 'Architecture',
    items: ['Pantheon', 'Hagia Sophia', 'Angkor Wat', 'Forbidden City', 'St. Peter\'s Basilica', 'Sydney Opera House'],
    itemValues: ["126 AD", "537", "1150", "1420", "1626", "1973"],
    dimension: 'Year completed (earliest \u2192 latest)',
    trapDimension: 'Architectural style or perceived era',
    funFact: 'The Pantheon in Rome (126 AD) has the world\'s largest unreinforced concrete dome \u2014 nearly 1,900 years old. Its design was unsurpassed for over 1,300 years.'
  },
  {
    id: 55,
    difficulty: 4,
    category: 'Food & Drink',
    items: ['Coffee', 'Chocolate', 'Beef', 'Rice', 'Banana', 'Potato'],
    itemValues: ["18,900 L", "17,000 L", "15,400 L", "2,500 L", "790 L", "290 L"],
    dimension: 'Water needed to produce 1kg (most \u2192 least)',
    trapDimension: 'Physical water content of the food',
    funFact: 'Producing 1kg of coffee requires ~18,900 liters of water \u2014 more than beef (15,400L). Your morning cup represents about 140 liters of water from farm to mug.'
  },
  {
    id: 56,
    difficulty: 3,
    category: 'Sports',
    items: ['Modi Stadium', 'Camp Nou', 'Wembley', 'San Siro', 'Yankee Stadium', 'Wimbledon'],
    itemValues: ["132,000", "99,354", "90,000", "75,923", "54,251", "14,979"],
    dimension: 'Stadium capacity (largest \u2192 smallest)',
    trapDimension: 'Team fame or tournament prestige',
    funFact: 'India\'s Modi Stadium seats 132,000 \u2014 built for cricket, the world\'s second most popular sport. Wimbledon\'s Centre Court holds just 14,979, proving prestige and size don\'t always match.'
  },
  {
    id: 57,
    difficulty: 1,
    category: 'Science',
    items: ['Battery Acid', 'Lemon Juice', 'Coffee', 'Pure Water', 'Baking Soda', 'Bleach'],
    itemValues: ["pH 1", "pH 2", "pH 5", "pH 7", "pH 9", "pH 13"],
    dimension: 'pH level (most acidic \u2192 most basic)',
    trapDimension: 'How harsh or chemical something seems',
    funFact: 'The pH scale is logarithmic \u2014 lemon juice (pH 2) is 100,000 times more acidic than pure water (pH 7). Battery acid at pH 1 is 10x more acidic than lemon juice.'
  },
  {
    id: 58,
    difficulty: 2,
    category: 'Animals',
    items: ['Chihuahua', 'Dachshund', 'Beagle', 'Labrador', 'Bulldog', 'Great Dane'],
    itemValues: ["17 yrs", "14 yrs", "13 yrs", "11 yrs", "9 yrs", "8 yrs"],
    dimension: 'Average lifespan (longest \u2192 shortest)',
    trapDimension: 'Size equals health or longevity',
    funFact: 'Chihuahuas can live up to 17 years, while Great Danes rarely make it past 10. In dogs, smaller breeds almost always outlive larger ones \u2014 the opposite of what most people expect.'
  },
  {
    id: 59,
    difficulty: 2,
    category: 'Technology',
    items: ['Pong', 'Space Invaders', 'Pac-Man', 'Tetris', 'Super Mario Bros', 'Sonic'],
    itemValues: ["1972", "1978", "1980", "1984", "1985", "1991"],
    dimension: 'Year of first release (earliest \u2192 latest)',
    trapDimension: 'Console or brand association',
    funFact: 'Tetris was created in 1984 by Soviet programmer Alexey Pajitnov on an Elektronika 60 computer \u2014 behind the Iron Curtain. It reached the West years before the Berlin Wall fell.'
  },
  {
    id: 60,
    difficulty: 2,
    category: 'Space',
    items: ['Moon', 'Venus', 'Mars', 'Jupiter', 'Neptune', 'Proxima Centauri'],
    itemValues: ["384,400 km", "38 M km", "55 M km", "588 M km", "4.3 B km", "4.0 ly"],
    dimension: 'Distance from Earth (nearest \u2192 farthest)',
    trapDimension: 'Size or brightness in the sky',
    funFact: 'Venus is our closest planetary neighbor at ~38 million km, not Mars (~55 million km). Venus is also the brightest planet in our sky, sometimes mistaken for a UFO.'
  },
  {
    id: 61,
    difficulty: 4,
    category: 'Culture',
    items: ['Taming of the Shrew', 'Richard III', 'Romeo and Juliet', 'Hamlet', 'Macbeth', 'The Tempest'],
    itemValues: ["~1590", "~1593", "~1595", "~1601", "~1606", "~1611"],
    dimension: 'Shakespeare first performance date (earliest \u2192 latest)',
    trapDimension: 'Familiarity or perceived maturity of the work',
    funFact: 'Romeo and Juliet (~1595) was written when Shakespeare was about 31. The Tempest (~1611) is believed to be his final solo play, written just five years before his death.'
  },
  {
    id: 62,
    difficulty: 5,
    category: 'Food & Drink',
    items: ['Finland', 'Norway', 'Iceland', 'Denmark', 'USA', 'China'],
    itemValues: ["12 kg", "9.9 kg", "9 kg", "8.7 kg", "4.2 kg", "0.1 kg"],
    dimension: 'Coffee consumption per capita (most \u2192 least)',
    trapDimension: 'Total production or perceived coffee culture',
    funFact: 'Finland consumes ~12 kg of coffee per person per year \u2014 nearly 3x the US rate. Nordic countries dominate coffee consumption, while Italy (famous for espresso) doesn\'t crack the top 10.'
  },
  {
    id: 63,
    difficulty: 1,
    category: 'Science',
    items: ['Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Violet'],
    itemValues: ["~700 nm", "~620 nm", "~580 nm", "~530 nm", "~470 nm", "~410 nm"],
    dimension: 'Light wavelength (longest \u2192 shortest)',
    trapDimension: 'Perceived energy or brightness',
    funFact: 'Red light has the longest wavelength (~700nm) and lowest energy, which is why it\'s used in darkrooms \u2014 it doesn\'t affect night-adapted vision as much as blue or violet light.'
  },
  {
    id: 64,
    difficulty: 4,
    category: 'Animals',
    items: ['Kiwi', 'Hummingbird', 'Chicken', 'Duck', 'Eagle', 'Ostrich'],
    itemValues: ["20%", "10%", "3%", "2%", "1.5%", "1%"],
    dimension: 'Egg-to-body-weight ratio (largest \u2192 smallest)',
    trapDimension: 'Body size or egg size',
    funFact: 'The kiwi lays an egg that\'s 20% of its body weight \u2014 proportionally the largest of any bird. It\'s like a human giving birth to a 4-year-old. The ostrich has the smallest ratio despite laying the largest egg.'
  },
  {
    id: 65,
    difficulty: 2,
    category: 'Geography',
    items: ['Sahara', 'Arabian', 'Gobi', 'Kalahari', 'Patagonian', 'Mojave'],
    itemValues: ["9.2 M km²", "2.3 M km²", "1.3 M km²", "930 K km²", "673 K km²", "124 K km²"],
    dimension: 'Desert area (largest \u2192 smallest)',
    trapDimension: 'How hot or sandy they look',
    funFact: 'The Sahara at 9.2 million km\u00b2 is roughly the size of the entire United States. The Gobi Desert gets freezing cold, with winter temperatures dropping to -40\u00b0C.'
  },
  {
    id: 66,
    difficulty: 4,
    category: 'Science',
    items: ['Mercury', 'Honey', 'Milk', 'Water', 'Olive Oil', 'Ethanol'],
    itemValues: ["13.5 g/cm³", "1.42 g/cm³", "1.03 g/cm³", "1.00 g/cm³", "0.92 g/cm³", "0.79 g/cm³"],
    dimension: 'Density of liquids (most \u2192 least dense)',
    trapDimension: 'Thickness or viscosity',
    funFact: 'Mercury is so dense (13.5 g/cm\u00b3) that a cannonball would float on a pool of it. Honey, despite being extremely viscous, is only slightly denser than milk.'
  },
  {
    id: 67,
    difficulty: 2,
    category: 'Food & Drink',
    items: ['Honey', 'White Rice', 'Canned Goods', 'Dark Chocolate', 'Eggs', 'Fresh Bread'],
    itemValues: ["Forever", "10+ yrs", "3–5 yrs", "2 yrs", "3–5 wks", "5–7 days"],
    dimension: 'Shelf life (longest \u2192 shortest)',
    trapDimension: 'Level of processing',
    funFact: 'Honey never spoils \u2014 edible honey has been found in Egyptian tombs over 3,000 years old. Its low moisture and high acidity make it inhospitable to bacteria.'
  },
  {
    id: 68,
    difficulty: 1,
    category: 'Animals',
    items: ['Grizzly Bear', 'Domestic Cat', 'Hippopotamus', 'Squirrel', 'Pig', 'Chicken'],
    itemValues: ["56 km/h", "48 km/h", "30 km/h", "20 km/h", "17 km/h", "14 km/h"],
    dimension: 'Top running speed (fastest \u2192 slowest)',
    trapDimension: 'Size equals speed',
    funFact: 'Grizzly bears can sprint at 56 km/h (35 mph) \u2014 fast enough to keep up with a horse. A domestic cat at 48 km/h outpaces most dogs, and hippos at 30 km/h can easily outrun most humans.'
  },
  {
    id: 69,
    difficulty: 3,
    category: 'Space',
    items: ['Saturn', 'Jupiter', 'Uranus', 'Neptune', 'Mars', 'Earth'],
    itemValues: ["146", "95", "27", "16", "2", "1"],
    dimension: 'Number of known moons (most \u2192 fewest)',
    trapDimension: 'Planet size',
    funFact: 'Saturn overtook Jupiter as the planet with the most known moons in 2023, with 146 confirmed. Jupiter has 95 \u2014 proving that size isn\'t everything in our solar system.'
  },
  {
    id: 70,
    difficulty: 4,
    category: 'Economics',
    items: ['USA', 'Germany', 'Italy', 'France', 'Russia', 'China'],
    itemValues: ["8,133 t", "3,355 t", "2,452 t", "2,437 t", "2,333 t", "2,235 t"],
    dimension: 'Gold reserves in tonnes (most \u2192 least)',
    trapDimension: 'GDP or economic power',
    funFact: 'Italy holds more gold reserves than both Russia and China. The US leads with 8,133 tonnes \u2014 stored primarily at Fort Knox and the New York Fed.'
  },
  {
    id: 71,
    difficulty: 1,
    category: 'Science',
    items: ['Radio Waves', 'Infrared', 'Visible Light', 'Ultraviolet', 'X-Rays', 'Gamma Rays'],
    itemValues: [">1 m", "~1 mm", "~500 nm", "~100 nm", "~1 nm", "<0.01 nm"],
    dimension: 'Electromagnetic wavelength (longest \u2192 shortest)',
    trapDimension: 'Perceived power or danger',
    funFact: 'Radio waves can be kilometers long, while gamma ray wavelengths are smaller than an atom. Despite their tiny size, gamma rays carry enough energy to damage DNA.'
  },
  {
    id: 72,
    difficulty: 2,
    category: 'Culture',
    items: ['LinkedIn', 'Facebook', 'YouTube', 'Twitter', 'Instagram', 'TikTok'],
    itemValues: ["2003", "2004", "2005", "2006", "2010", "2016"],
    dimension: 'Platform launch year (earliest \u2192 latest)',
    trapDimension: 'Current popularity or user count',
    funFact: 'LinkedIn launched in 2003, a year before Facebook. Despite being the oldest major social platform still active, it\'s often forgotten in discussions of social media history.'
  },
  {
    id: 73,
    difficulty: 5,
    category: 'Geography',
    items: ['Manila', 'Paris', 'Mumbai', 'Tokyo', 'New York', 'Los Angeles'],
    itemValues: ["43,000/km²", "20,700/km²", "20,500/km²", "11,000/km²", "10,900/km²", "3,200/km²"],
    dimension: 'Population density (most \u2192 least dense)',
    trapDimension: 'Total population or skyline height',
    funFact: 'Manila is the world\'s most densely populated city at ~43,000 people per km\u00b2 \u2014 roughly 4x denser than New York City. Despite its famous skyline, LA is surprisingly spread out at just ~3,200/km\u00b2.'
  },
  {
    id: 74,
    difficulty: 2,
    category: 'History',
    items: ['Printing Press', 'Telescope', 'Steam Engine', 'Telephone', 'Light Bulb', 'Internet'],
    itemValues: ["1440", "1608", "1712", "1876", "1879", "1983"],
    dimension: 'Year of invention (earliest \u2192 latest)',
    trapDimension: 'Perceived modernity',
    funFact: 'The telephone (1876) was invented just three years before the practical light bulb (1879). Alexander Graham Bell and Thomas Edison were contemporaries working in the same era of innovation.'
  },
  {
    id: 75,
    difficulty: 3,
    category: 'Science',
    items: ['Cucumber', 'Watermelon', 'Strawberry', 'Orange', 'Banana', 'Bread'],
    itemValues: ["96%", "92%", "91%", "86%", "75%", "37%"],
    dimension: 'Water percentage (most \u2192 least)',
    trapDimension: 'Name or perceived juiciness',
    funFact: 'Cucumbers are 96% water \u2014 wetter than watermelon (92%). Despite its name, watermelon isn\'t even the most water-rich fruit; that honor goes to the humble cucumber.'
  },
  {
    id: 76,
    difficulty: 3,
    category: 'Animals',
    items: ['Beetles', 'Fish', 'Reptiles', 'Birds', 'Amphibians', 'Mammals'],
    itemValues: ["~400,000", "~34,000", "~11,700", "~11,000", "~8,400", "~6,500"],
    dimension: 'Number of known species (most \u2192 fewest)',
    trapDimension: 'Familiarity or perceived diversity',
    funFact: 'There are roughly 400,000 known beetle species \u2014 more than all mammals, birds, reptiles, and amphibians combined. J.B.S. Haldane reportedly noted God\'s "inordinate fondness for beetles."'
  },
  {
    id: 77,
    difficulty: 2,
    category: 'Science',
    items: ['Glass Bottle', 'Plastic Bottle', 'Aluminum Can', 'Leather', 'Cigarette Butt', 'Banana Peel'],
    itemValues: ["1 M+ yrs", "450 yrs", "200 yrs", "50 yrs", "10 yrs", "2 yrs"],
    dimension: 'Decomposition time (longest \u2192 shortest)',
    trapDimension: 'Perceived environmental harm',
    funFact: 'A glass bottle takes over 1 million years to decompose \u2014 far longer than plastic (~450 years). Aluminum cans, often seen as recyclable and harmless, take 200 years in a landfill.'
  },
  {
    id: 78,
    difficulty: 2,
    category: 'Space',
    items: ['Neptune', 'Uranus', 'Saturn', 'Jupiter', 'Mars', 'Earth'],
    itemValues: ["165 yrs", "84 yrs", "29.5 yrs", "11.9 yrs", "687 days", "365 days"],
    dimension: 'Orbital period (longest \u2192 shortest)',
    trapDimension: 'Planet size',
    funFact: 'Neptune takes 165 Earth years to orbit the Sun. Since its discovery in 1846, it completed its first observed orbit in 2011 \u2014 just one Neptunian year after being found.'
  },
  {
    id: 79,
    difficulty: 3,
    category: 'Architecture',
    items: ['Ponte Vecchio', 'Rialto Bridge', 'Brooklyn Bridge', 'Tower Bridge', 'Golden Gate', 'Millau Viaduct'],
    itemValues: ["1345", "1591", "1883", "1894", "1937", "2004"],
    dimension: 'Year opened (earliest \u2192 latest)',
    trapDimension: 'Architectural style or country',
    funFact: 'Florence\'s Ponte Vecchio (1345) is one of the few medieval bridges still lined with shops. It was the only bridge in Florence spared from destruction by retreating German forces in WWII.'
  },
  {
    id: 80,
    difficulty: 3,
    category: 'Animals',
    items: ['Saltwater Croc', 'Hippopotamus', 'Gorilla', 'Grizzly Bear', 'Lion', 'Wolf'],
    itemValues: ["3,700 PSI", "1,821 PSI", "1,300 PSI", "975 PSI", "650 PSI", "406 PSI"],
    dimension: 'Bite force in PSI (strongest \u2192 weakest)',
    trapDimension: 'Size or predator reputation',
    funFact: 'The saltwater crocodile\'s bite force of 3,700 PSI is over 5x stronger than a lion\'s (650 PSI). Lions are often called king of the jungle, but their bite is surprisingly modest.'
  },
  {
    id: 81,
    difficulty: 5,
    category: 'Food & Drink',
    items: ['USA', 'Germany', 'France', 'Italy', 'Netherlands', 'Switzerland'],
    itemValues: ["6.1 M t", "2.2 M t", "1.9 M t", "1.3 M t", "0.9 M t", "0.2 M t"],
    dimension: 'Cheese production by country (most \u2192 least)',
    trapDimension: 'Cheese reputation or tradition',
    funFact: 'The USA produces more cheese than any other country \u2014 over 6 million tonnes annually, more than France, Italy, and Switzerland combined.'
  },
  {
    id: 82,
    difficulty: 5,
    category: 'Culture',
    items: ['English', 'Cebuano', 'German', 'Swedish', 'French', 'Dutch'],
    itemValues: ["6.8 M", "6.1 M", "2.9 M", "2.6 M", "2.6 M", "2.1 M"],
    dimension: 'Wikipedia articles by language (most \u2192 fewest)',
    trapDimension: 'Number of speakers of the language',
    funFact: 'Cebuano Wikipedia has over 6 million articles \u2014 nearly as many as English \u2014 thanks to Lsjbot, a Swedish-made bot that auto-generated millions of articles about species and places.'
  },
  {
    id: 83,
    difficulty: 2,
    category: 'Science',
    items: ['Honey', 'Maple Syrup', 'Olive Oil', 'Milk', 'Water', 'Acetone'],
    itemValues: ["10,000 mPa·s", "180 mPa·s", "84 mPa·s", "2.0 mPa·s", "1.0 mPa·s", "0.3 mPa·s"],
    dimension: 'Viscosity at room temperature (most \u2192 least viscous)',
    trapDimension: 'Perceived thickness or weight',
    funFact: 'Honey is about 10,000 times more viscous than water. Acetone (nail polish remover) is even thinner than water \u2014 about 3x less viscous.'
  },
  {
    id: 84,
    difficulty: 3,
    category: 'History',
    items: ['Great Wall of China', 'Cologne Cathedral', 'Sagrada Familia', 'St. Peter\'s Basilica', 'Taj Mahal', 'Empire State'],
    itemValues: ["~2,000 yrs", "632 yrs", "140+ yrs", "120 yrs", "22 yrs", "410 days"],
    dimension: 'Construction duration (longest \u2192 shortest)',
    trapDimension: 'Building size or complexity',
    funFact: 'Cologne Cathedral took 632 years to complete (1248\u20131880), with a 300-year construction pause. The Empire State Building was built in just 410 days.'
  },
  {
    id: 85,
    difficulty: 3,
    category: 'Space',
    items: ['Jupiter', 'Neptune', 'Saturn', 'Earth', 'Mars', 'Pluto'],
    itemValues: ["2.53 g", "1.14 g", "1.07 g", "1.00 g", "0.38 g", "0.06 g"],
    dimension: 'Surface gravity (strongest \u2192 weakest)',
    trapDimension: 'Planet size or mass',
    funFact: 'Despite being smaller than Saturn, Neptune has stronger surface gravity (1.14g vs 1.07g) because it\'s much denser. Saturn is so light for its size that it would float in water.'
  }
];

// ─── Utility Functions ───────────────────────────────────────────────────────

function getDayOffset(date) {
  const d = date || new Date();
  const start = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
  return Math.floor((start - EPOCH) / 86400000);
}

function getTodaysPuzzle() {
  const day = getDayOffset();
  const idx = ((day % PUZZLES.length) + PUZZLES.length) % PUZZLES.length;
  return { ...PUZZLES[idx], number: day + 1 };
}

function getPuzzleForDate(date) {
  const day = getDayOffset(date);
  const idx = ((day % PUZZLES.length) + PUZZLES.length) % PUZZLES.length;
  return { ...PUZZLES[idx], number: day + 1 };
}

function shuffle(arr) {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

function shuffleNotCorrect(items) {
  let s;
  let tries = 0;
  do {
    s = shuffle(items);
    tries++;
  } while (s.every((item, i) => item === items[i]) && tries < 100);
  return s;
}

function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }

function calcFeedback(current, correct) {
  return current.map((item, i) => {
    const ci = correct.indexOf(item);
    const diff = Math.abs(i - ci);
    if (diff === 0) return 'perfect';
    if (diff === 1) return 'close';
    return 'far';
  });
}

function reorder(arr, fromIdx, toIdx, locked) {
  if (fromIdx === toIdx) return arr;
  if (locked.has(fromIdx) || locked.has(toIdx)) return arr;

  // Reorder only among unlocked positions so locked items never move
  const result = [...arr];
  const unlockedPositions = [];
  const unlockedItems = [];
  for (let i = 0; i < result.length; i++) {
    if (!locked.has(i)) {
      unlockedPositions.push(i);
      unlockedItems.push(result[i]);
    }
  }
  const fromUL = unlockedPositions.indexOf(fromIdx);
  const toUL = unlockedPositions.indexOf(toIdx);
  if (fromUL === -1 || toUL === -1) return arr;

  const item = unlockedItems.splice(fromUL, 1)[0];
  unlockedItems.splice(toUL, 0, item);

  for (let i = 0; i < unlockedPositions.length; i++) {
    result[unlockedPositions[i]] = unlockedItems[i];
  }
  return result;
}

function feedbackColor(fb) {
  if (fb === 'perfect') return GREEN;
  if (fb === 'close') return AMBER;
  if (fb === 'far') return RED;
  return TILE_COLOR;
}

const HINT_GREEN = 'var(--hint-green)';
const HINT_AMBER = 'var(--hint-amber)';
const HINT_RED   = 'var(--hint-red)';

function feedbackTextColor(fb) {
  if (fb === 'perfect') return HINT_GREEN;
  if (fb === 'close') return HINT_AMBER;
  if (fb === 'far') return HINT_RED;
  return TILE_COLOR;
}

function feedbackBg(fb) {
  if (fb === 'perfect') return GREEN_BG;
  if (fb === 'close') return AMBER_BG;
  if (fb === 'far') return RED_BG;
  return TILE_BG;
}

function feedbackBgMuted(fb) {
  if (fb === 'perfect') return GREEN_BG_MUTED;
  if (fb === 'close') return AMBER_BG_MUTED;
  if (fb === 'far') return RED_BG_MUTED;
  return TILE_BG;
}

function feedbackEmoji(fb) {
  if (fb === 'perfect') return '🟩';
  if (fb === 'close') return '🟨';
  return '🟥';
}

// ─── Storage ─────────────────────────────────────────────────────────────────

function loadStats() {
  const defaults = { played: 0, won: 0, streak: 0, maxStreak: 0, distribution: [0, 0, 0, 0] };
  try {
    const s = JSON.parse(localStorage.getItem('sorted-stats'));
    if (!s || typeof s !== 'object') return defaults;
    return {
      played: Math.max(0, s.played | 0),
      won: Math.max(0, s.won | 0),
      streak: Math.max(0, s.streak | 0),
      maxStreak: Math.max(0, s.maxStreak | 0),
      distribution: Array.isArray(s.distribution) && s.distribution.length === 4
        ? s.distribution.map(n => Math.max(0, n | 0))
        : [0, 0, 0, 0]
    };
  } catch { return defaults; }
}

function saveStats(puzzleNum, stars, attempts, attemptHistory) {
  try {
    const stats = loadStats();
    stats.played++;
    if (stars > 0) { stats.won++; stats.streak++; } else { stats.streak = 0; }
    stats.maxStreak = Math.max(stats.maxStreak, stats.streak);
    stats.distribution[Math.max(0, Math.min(3, stars))]++;
    localStorage.setItem('sorted-stats', JSON.stringify(stats));
    localStorage.setItem(`sorted-${puzzleNum}`, JSON.stringify({
      stars, attempts, attemptHistory: attemptHistory || [], date: new Date().toISOString()
    }));
  } catch {}
}

function loadPuzzleState(puzzleNum) {
  try {
    return JSON.parse(localStorage.getItem(`sorted-${puzzleNum}`));
  } catch { return null; }
}

function loadFavorites() {
  try { return JSON.parse(localStorage.getItem('sorted-favorites')) || []; }
  catch { return []; }
}

function saveFavorites(arr) {
  try { localStorage.setItem('sorted-favorites', JSON.stringify(arr)); } catch {}
}

function isFavorite(n) {
  return loadFavorites().includes(n);
}

function toggleFavorite(n) {
  const cur = loadFavorites();
  const next = cur.includes(n) ? cur.filter(x => x !== n) : [...cur, n];
  saveFavorites(next);
  return next;
}

function dateForPuzzleNumber(n) {
  return new Date(EPOCH + (n - 1) * 86400000);
}

function generateShareText(puzzle, stars, attemptHistory) {
  const header = `Sorted #${puzzle.number} — ${puzzle.category}`;
  const grid = attemptHistory.map((fb, i) => {
    const row = fb.map(feedbackEmoji).join('');
    const isLast = i === attemptHistory.length - 1;
    return isLast && stars > 0 ? row + ' Sorted!' : row;
  }).join('\n');
  return `${header}\n${grid}\n\nhttps://sorted-alpha.vercel.app/`;
}

// ─── Styles (injected) ──────────────────────────────────────────────────────

const STYLES = `
:root {
  --bg: #0E0F10;
  --tile-color: #E4E0D5;
  --tile-bg: #1A1A1E;
  --green-bg: #0E2E1A;
  --amber-bg: #2C2510;
  --red-bg: #2E1414;
  --locked-border: #2D6B45;
  --green-bg-muted: #0C1F14;
  --amber-bg-muted: #1F1C12;
  --red-bg-muted: #1F1212;
  --muted: #8A8A8A;
  --overlay-bg: #18181B;
  --subtle-border: #2A2A2E;
  --hint-green: #4ADE80;
  --hint-amber: #FBBF24;
  --hint-red: #EF6461;
}
:root[data-theme="light"] {
  --bg: #FAF9F6;
  --tile-color: #1A1A1E;
  --tile-bg: #EFECE4;
  --green-bg: #D6F5E0;
  --amber-bg: #FFF5D6;
  --red-bg: #FDE2E2;
  --locked-border: #68C98A;
  --green-bg-muted: #E8F8ED;
  --amber-bg-muted: #FDF8E8;
  --red-bg-muted: #FCEAEA;
  --muted: #6B6B6B;
  --overlay-bg: #FFFFFF;
  --subtle-border: #D4D0C8;
  --hint-green: #166534;
  --hint-amber: #92400E;
  --hint-red: #9B1C1C;
}
html, body { overflow: hidden; height: 100%; }
#root { overflow: hidden; height: 100%; }
@keyframes lockPulse {
  0% { transform: scale(1); }
  50% { transform: scale(1.04); }
  100% { transform: scale(1); }
}
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes slideUp {
  from { opacity: 0; transform: translateY(40px); }
  to { opacity: 1; transform: translateY(0); }
}
@keyframes bounceIn {
  0% { transform: scale(0.3); opacity: 0; }
  50% { transform: scale(1.08); }
  70% { transform: scale(0.95); }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes starPop {
  0% { transform: scale(0); opacity: 0; }
  60% { transform: scale(1.3); }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}
`;

// ─── Components ──────────────────────────────────────────────────────────────

function StyleInjector() {
  useEffect(() => {
    const el = document.createElement('style');
    el.textContent = STYLES;
    document.head.appendChild(el);
    return () => el.remove();
  }, []);
  return null;
}

// ─── Splash Icon ────────────────────────────────────────────────────────────

function SortedIcon({ size = 56 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 56 56" fill="none">
      <rect x="4" y="4" width="48" height="48" rx="12" fill="#1A1A1E" />
      <rect x="12" y="14" width="32" height="6" rx="3" fill={GREEN} />
      <rect x="12" y="25" width="32" height="6" rx="3" fill={AMBER} />
      <rect x="12" y="36" width="32" height="6" rx="3" fill={RED} />
    </svg>
  );
}

function HeartIcon({ filled, size = 20, color }) {
  const stroke = color || (filled ? RED : MUTED);
  return (
    <svg width={size} height={size} viewBox="0 0 24 24"
         fill={filled ? (color || RED) : 'none'} stroke={stroke}
         strokeWidth="1.6" strokeLinejoin="round" strokeLinecap="round">
      <path d="M12 20.5s-7.2-4.3-9.3-9.1a5 5 0 0 1 8.6-5 1 1 0 0 0 1.4 0 5 5 0 0 1 8.6 5C19.2 16.2 12 20.5 12 20.5z"/>
    </svg>
  );
}

// ─── Splash Screen ───────────────────────────────────────────────────────────

function SplashScreen({ puzzle, onPlay, onHowTo, onStats, onArchive, alreadyPlayed, isArchive, archiveDate }) {
  const dateStr = useMemo(() => {
    const d = isArchive && archiveDate ? archiveDate : new Date();
    return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
  }, [isArchive, archiveDate]);

  return (
    <div style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
      height: '100%', background: SPLASH_BG, padding: '2rem',
      animation: 'fadeIn 0.5s ease', overflow: 'hidden'
    }}>
      <SortedIcon size={64} />

      <div style={{
        fontSize: '2.6rem', fontFamily: "'DM Serif Display', serif",
        fontWeight: 700, color: '#1A1A1E', marginTop: '1rem', letterSpacing: '-0.02em'
      }}>
        Sorted
      </div>

      <div style={{
        fontSize: '1.05rem', color: FADED_DARK, textAlign: 'center',
        marginTop: '0.6rem', marginBottom: '2.25rem', maxWidth: '260px', lineHeight: 1.5
      }}>
        Things aren't always what they seem.
      </div>

      {alreadyPlayed ? (
        <div style={{ textAlign: 'center' }}>
          <div style={{ fontSize: '0.95rem', color: '#1A1A1E', marginBottom: '0.4rem', fontWeight: 600 }}>
            {isArchive ? 'You\'ve already played this one!' : 'You\'ve already played today!'}
          </div>
          <div style={{ fontSize: '0.85rem', color: FADED_DARK, marginBottom: '1.25rem' }}>
            {isArchive ? 'Try another day from the archive.' : 'Come back tomorrow for a new one.'}
          </div>
          <button onClick={onArchive} style={{
            background: '#1A1A1E', color: SPLASH_BG, border: 'none', borderRadius: '28px',
            padding: '0.9rem 2.5rem', fontSize: '1.1rem', fontWeight: 700,
            fontFamily: "'DM Sans', sans-serif", cursor: 'pointer',
            transition: 'transform 0.15s ease, opacity 0.15s ease',
            display: 'flex', alignItems: 'center', gap: '0.6rem', margin: '0 auto'
          }}>
            <svg width="18" height="18" viewBox="0 0 20 20" fill="none">
              <rect x="2.5" y="4" width="15" height="13" rx="2" stroke={SPLASH_BG} strokeWidth="1.5"/>
              <line x1="2.5" y1="8" x2="17.5" y2="8" stroke={SPLASH_BG} strokeWidth="1.5"/>
              <line x1="6" y1="2" x2="6" y2="5.5" stroke={SPLASH_BG} strokeWidth="1.5" strokeLinecap="round"/>
              <line x1="14" y1="2" x2="14" y2="5.5" stroke={SPLASH_BG} strokeWidth="1.5" strokeLinecap="round"/>
              <rect x="5" y="10.5" width="2.5" height="2.5" rx="0.5" fill={SPLASH_BG}/>
            </svg>
            Browse Archive
          </button>
        </div>
      ) : (
        <button onClick={onPlay} style={{
          background: '#1A1A1E', color: SPLASH_BG, border: 'none', borderRadius: '28px',
          padding: '0.9rem 3.5rem', fontSize: '1.1rem', fontWeight: 700,
          fontFamily: "'DM Sans', sans-serif", cursor: 'pointer',
          transition: 'transform 0.15s ease, opacity 0.15s ease'
        }}>
          Play
        </button>
      )}

      <div style={{ marginTop: '1.75rem', textAlign: 'center' }}>
        <div style={{ fontSize: '1.08rem', color: '#1A1A1E', fontWeight: 600 }}>{dateStr}</div>
        <div style={{ fontSize: '0.95rem', color: FADED_DARK, marginTop: '0.2rem' }}>
          Puzzle #{puzzle.number} · {puzzle.category} · {DIFFICULTY_LABELS[puzzle.difficulty]}
        </div>
      </div>

      <div style={{ display: 'flex', gap: '2rem', marginTop: '2rem' }}>
        <button onClick={onArchive} style={{...splashLinkStyle, display: 'flex', alignItems: 'center', gap: '0.35rem'}}>
          <svg width="14" height="14" viewBox="0 0 20 20" fill="none">
            <rect x="2.5" y="4" width="15" height="13" rx="2" stroke={FADED_DARK} strokeWidth="1.5"/>
            <line x1="2.5" y1="8" x2="17.5" y2="8" stroke={FADED_DARK} strokeWidth="1.5"/>
            <line x1="6" y1="2" x2="6" y2="5.5" stroke={FADED_DARK} strokeWidth="1.5" strokeLinecap="round"/>
            <line x1="14" y1="2" x2="14" y2="5.5" stroke={FADED_DARK} strokeWidth="1.5" strokeLinecap="round"/>
            <rect x="5" y="10.5" width="2.5" height="2.5" rx="0.5" fill={FADED_DARK}/>
          </svg>
          Archive
        </button>
        <button onClick={onHowTo} style={splashLinkStyle}>How to Play</button>
        <button onClick={onStats} style={splashLinkStyle}>Stats</button>
      </div>
    </div>
  );
}

const splashLinkStyle = {
  background: 'none', border: 'none', color: FADED_DARK, fontSize: '0.95rem',
  cursor: 'pointer', textDecoration: 'underline', fontFamily: "'DM Sans', sans-serif"
};

const iconBtnStyle = {
  background: 'none', border: 'none', padding: '4px', cursor: 'pointer',
  display: 'flex', alignItems: 'center', justifyContent: 'center'
};

const darkLinkStyle = {
  background: 'none', border: 'none', color: MUTED, fontSize: '0.95rem',
  cursor: 'pointer', textDecoration: 'underline', fontFamily: "'DM Sans', sans-serif",
  padding: '0.25rem 0.5rem'
};

// ─── How To Play ─────────────────────────────────────────────────────────────

function HowToPlay({ onClose }) {
  return (
    <Overlay onClose={onClose} title="How to Play">
      <div style={{ fontSize: '0.85rem', lineHeight: 1.7, color: '#B0ADA5' }}>
        <p style={{ marginBottom: '1rem' }}>
          Arrange the <strong style={{ color: TILE_COLOR }}>6 items</strong> in the correct order along a hidden dimension.
        </p>
        <p style={{ marginBottom: '1rem' }}>
          <strong style={{ color: TILE_COLOR }}>Drag</strong> tiles to reorder them, or <strong style={{ color: TILE_COLOR }}>tap</strong> two tiles to swap them.
        </p>
        <p style={{ marginBottom: '1rem' }}>
          You have <strong style={{ color: TILE_COLOR }}>3 attempts</strong>. After each guess, tiles show feedback:
        </p>
        <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginBottom: '1rem', paddingLeft: '0.5rem' }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
            <span style={{ color: GREEN, fontWeight: 700 }}>■</span>
            <span><strong style={{ color: GREEN }}>Green</strong> — Correct position</span>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
            <span style={{ color: AMBER, fontWeight: 700 }}>■</span>
            <span><strong style={{ color: AMBER }}>Amber</strong> — Off by 1 position</span>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
            <span style={{ color: RED, fontWeight: 700 }}>■</span>
            <span><strong style={{ color: RED }}>Red</strong> — Off by 2+ positions</span>
          </div>
        </div>
        <p style={{ marginBottom: '1rem' }}>
          Tiles in the <strong style={{ color: GREEN }}>correct position</strong> lock in place.
        </p>
        <p style={{ marginBottom: '1rem' }}>
          <strong style={{ color: TILE_COLOR }}>Hints</strong> — <em>Sort Rule</em> reveals the sorting dimension. <em>Values</em> shows each tile's metric once it lands in the correct spot.
        </p>
        <p style={{ marginBottom: '1rem' }}>
          <strong style={{ color: TILE_COLOR }}>Beware the trap!</strong> The obvious way to sort is usually wrong.
        </p>
        <p style={{ marginBottom: '1rem' }}>
          After each puzzle, the solution reveals the real <strong style={{ color: TILE_COLOR }}>value of every item</strong> on the hidden dimension, plus a short fact so you learn something new.
        </p>
        <p style={{ marginBottom: '0.5rem' }}>
          Tap the <strong style={{ color: RED }}>heart</strong> to favorite a puzzle, or open the <strong style={{ color: TILE_COLOR }}>archive</strong> (calendar icon) to replay recent days or your favorites.
        </p>
        {APP_VERSION !== '__VERSION__' && (
          <div style={{
            marginTop: '1.5rem', paddingTop: '0.75rem',
            borderTop: '1px solid rgba(255,255,255,0.06)',
            fontSize: '0.65rem', color: '#555', textAlign: 'center'
          }}>
            {APP_VERSION}
          </div>
        )}
      </div>
    </Overlay>
  );
}

// ─── Stats Overlay ───────────────────────────────────────────────────────────

function StatsOverlay({ onClose }) {
  const stats = loadStats();
  const winPct = stats.played > 0 ? Math.round((stats.won / stats.played) * 100) : 0;

  return (
    <Overlay onClose={onClose} title="Statistics">
      <div style={{ display: 'flex', justifyContent: 'space-around', marginBottom: '1.5rem' }}>
        {[
          [stats.played, 'Played'],
          [winPct + '%', 'Win %'],
          [stats.streak, 'Day Streak'],
          [stats.maxStreak, 'Max Streak']
        ].map(([val, label]) => (
          <div key={label} style={{ textAlign: 'center' }}>
            <div style={{ fontSize: '1.5rem', fontWeight: 700, color: TILE_COLOR }}>{val}</div>
            <div style={{ fontSize: '0.65rem', color: MUTED, textTransform: 'uppercase', letterSpacing: '0.05em' }}>{label}</div>
          </div>
        ))}
      </div>
      <div style={{ fontSize: '0.75rem', color: MUTED, marginBottom: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
        Score Distribution
      </div>
      {(() => {
        const max = Math.max(...stats.distribution, 1);
        return ['Flawless', 'Sharp', 'Got it', 'Failed'].map((name, i) => {
          const val = stats.distribution[3 - i];
          const pct = Math.round((val / max) * 100);
          return (
            <div key={i} style={{ padding: '0.5rem 0', borderBottom: `1px solid ${SUBTLE_BORDER}` }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: '0.3rem', fontSize: '0.85rem' }}>
                <span style={{ color: TILE_COLOR }}>{name}</span>
                <span style={{ color: MUTED, fontVariantNumeric: 'tabular-nums' }}>{val}</span>
              </div>
              <div style={{ height: '4px', background: SUBTLE_BORDER, borderRadius: '2px' }}>
                <div style={{ height: '100%', width: `${pct}%`, background: GREEN, opacity: 0.4, borderRadius: '2px', transition: 'width 0.5s ease' }} />
              </div>
            </div>
          );
        });
      })()}
    </Overlay>
  );
}

// ─── Archive Overlay ─────────────────────────────────────────────────────────

function ArchiveRow({ date, puzzle, saved, isFav, onToggleFav, onSelect, formatDate }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'stretch',
      background: TILE_BG, border: `1px solid ${SUBTLE_BORDER}`,
      borderRadius: '10px', overflow: 'hidden'
    }}>
      <button onClick={onSelect} style={{
        flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        background: 'transparent', border: 'none',
        padding: '0.7rem 0.9rem', cursor: 'pointer',
        fontFamily: "'DM Sans', sans-serif", textAlign: 'left', color: 'inherit'
      }}>
        <div>
          <div style={{ fontSize: '0.85rem', fontWeight: 600, color: TILE_COLOR }}>{formatDate(date)}</div>
          <div style={{ fontSize: '0.7rem', color: MUTED, marginTop: '2px' }}>
            #{puzzle.number} · {puzzle.category} · <span style={{ color: DIFFICULTY_COLORS[puzzle.difficulty] }}>{DIFFICULTY_LABELS[puzzle.difficulty]}</span>
          </div>
        </div>
        <div style={{ textAlign: 'right', minWidth: '50px' }}>
          {saved ? (
            <span style={{ fontSize: '0.9rem' }}>
              {saved.stars > 0 ? (
                <>
                  <span style={{ color: GREEN }}>{'▆'.repeat(saved.stars)}</span>
                  <span style={{ color: '#333' }}>{'▁'.repeat(3 - saved.stars)}</span>
                </>
              ) : '—'}
            </span>
          ) : (
            <span style={{ fontSize: '0.75rem', color: GREEN, fontWeight: 600 }}>Play</span>
          )}
        </div>
      </button>
      <button
        onClick={(e) => { e.stopPropagation(); onToggleFav(); }}
        aria-label={isFav ? 'Unfavorite puzzle' : 'Favorite puzzle'}
        style={{
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          background: 'transparent', border: 'none', borderLeft: `1px solid ${SUBTLE_BORDER}`,
          padding: '0 0.8rem', cursor: 'pointer'
        }}>
        <HeartIcon filled={isFav} size={18} />
      </button>
    </div>
  );
}

function ArchiveOverlay({ onClose, onSelect }) {
  const today = new Date();
  const [tab, setTab] = useState('recent');
  const [favorites, setFavorites] = useState(() => loadFavorites());

  const days = useMemo(() => {
    const result = [];
    for (let i = 1; i <= 14; i++) {
      const d = new Date(today);
      d.setDate(d.getDate() - i);
      const puzzle = getPuzzleForDate(d);
      const saved = loadPuzzleState(puzzle.number);
      result.push({ date: d, puzzle, saved, daysAgo: i });
    }
    return result;
  }, []);

  const favoriteRows = useMemo(() => {
    return favorites
      .slice()
      .sort((a, b) => b - a)
      .map((n) => {
        const idx = (((n - 1) % PUZZLES.length) + PUZZLES.length) % PUZZLES.length;
        const puzzle = { ...PUZZLES[idx], number: n };
        const date = dateForPuzzleNumber(n);
        const saved = loadPuzzleState(n);
        return { date, puzzle, saved, num: n };
      });
  }, [favorites]);

  const formatDate = (d) => d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });

  const handleToggleFav = (n) => setFavorites(toggleFavorite(n));
  const handleSelect = (date) => onSelect(date);

  const tabBtn = (id, label) => {
    const active = tab === id;
    return (
      <button onClick={() => setTab(id)} style={{
        flex: 1, padding: '0.5rem 0.75rem', borderRadius: '999px',
        border: `1px solid ${active ? TILE_COLOR : SUBTLE_BORDER}`,
        background: active ? TILE_COLOR : 'transparent',
        color: active ? BG : MUTED,
        fontFamily: "'DM Sans', sans-serif", fontSize: '0.8rem', fontWeight: 600,
        cursor: 'pointer'
      }}>{label}</button>
    );
  };

  return (
    <Overlay onClose={onClose} title="Archive">
      <div style={{ display: 'flex', gap: '6px', marginBottom: '1rem' }}>
        {tabBtn('recent', 'Recent')}
        {tabBtn('favorites', `Favorites${favorites.length ? ` (${favorites.length})` : ''}`)}
      </div>

      {tab === 'recent' && (
        <>
          <div style={{ fontSize: '0.8rem', color: MUTED, marginBottom: '0.75rem' }}>
            Play puzzles from the last 14 days.
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
            {days.map(({ date, puzzle, saved, daysAgo }) => (
              <ArchiveRow
                key={daysAgo}
                date={date}
                puzzle={puzzle}
                saved={saved}
                isFav={favorites.includes(puzzle.number)}
                onToggleFav={() => handleToggleFav(puzzle.number)}
                onSelect={() => handleSelect(date)}
                formatDate={formatDate}
              />
            ))}
          </div>
        </>
      )}

      {tab === 'favorites' && (
        <>
          {favoriteRows.length === 0 ? (
            <div style={{
              fontSize: '0.85rem', color: MUTED, textAlign: 'center',
              padding: '2rem 1rem', lineHeight: 1.6
            }}>
              No favorites yet.<br/>
              Tap the star on any puzzle to save it.
            </div>
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
              {favoriteRows.map(({ date, puzzle, saved, num }) => (
                <ArchiveRow
                  key={num}
                  date={date}
                  puzzle={puzzle}
                  saved={saved}
                  isFav={true}
                  onToggleFav={() => handleToggleFav(num)}
                  onSelect={() => handleSelect(date)}
                  formatDate={formatDate}
                />
              ))}
            </div>
          )}
        </>
      )}
    </Overlay>
  );
}

// ─── Overlay Container ───────────────────────────────────────────────────────

function Overlay({ onClose, title, children }) {
  return (
    <div style={{
      position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 100,
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '1rem'
    }} onClick={onClose}>
      <div style={{
        background: OVERLAY_BG, borderRadius: '16px', padding: '1.5rem',
        maxWidth: '360px', width: '100%', maxHeight: '85vh', overflowY: 'auto',
        animation: 'slideUp 0.3s ease'
      }} onClick={e => e.stopPropagation()}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.25rem' }}>
          <div style={{ fontSize: '1.2rem', fontFamily: "'DM Serif Display', serif" }}>{title}</div>
          <button onClick={onClose} aria-label="Close" style={{
            background: 'none', border: 'none', color: MUTED, fontSize: '1.5rem',
            cursor: 'pointer', lineHeight: 1, padding: '4px'
          }}>×</button>
        </div>
        {children}
      </div>
    </div>
  );
}

// ─── Tile ────────────────────────────────────────────────────────────────────

const Tile = React.memo(function Tile({ item, index, isSelected, isLocked, isDragging, feedback, hintColor, bgHintColor, y, onPointerDown, zIndex, itemValue, showValues }) {
  const bg = isLocked ? GREEN_BG : feedback ? feedbackBg(feedback) : bgHintColor || TILE_BG;
  const borderColor = isSelected ? AMBER : isLocked ? LOCKED_BORDER : feedback ? feedbackColor(feedback) + '44' : SUBTLE_BORDER;
  const textColor = hintColor || (feedback ? feedbackTextColor(feedback) : TILE_COLOR);
  const lockAnim = isLocked ? 'lockPulse 0.35s ease' : 'none';

  return (
    <div
      onPointerDown={(e) => onPointerDown(index, e)}
      style={{
        position: 'absolute',
        left: '0', right: '0',
        top: 0,
        transform: `translateY(${y}px)${isDragging ? ' scale(1.04)' : ''}`,
        height: `${TILE_H}px`,
        background: bg,
        border: `2px solid ${borderColor}`,
        borderRadius: '12px',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'space-between',
        padding: '0 1.1rem',
        cursor: isLocked ? 'default' : 'grab',
        transition: isDragging ? 'none' : 'transform 0.2s ease, background 0.3s ease, border-color 0.3s ease',
        boxShadow: isDragging ? '0 8px 24px rgba(0,0,0,0.5)' : '0 1px 3px rgba(0,0,0,0.2)',
        zIndex: zIndex,
        touchAction: 'none',
        userSelect: 'none',
        animation: lockAnim,
        willChange: isDragging ? 'transform' : 'auto'
      }}
    >
      <span style={{
        fontSize: '0.95rem', fontWeight: 600, color: textColor,
        transition: 'color 0.3s ease',
        fontFamily: "'DM Sans', sans-serif"
      }}>
        {item}
      </span>
      {isLocked && showValues && itemValue && (
        <span style={{
          fontSize: '0.78rem',
          color: GREEN,
          fontVariantNumeric: 'tabular-nums',
          whiteSpace: 'nowrap',
          opacity: 0.7,
          fontFamily: "'DM Sans', sans-serif",
          letterSpacing: '0.01em'
        }}>{itemValue}</span>
      )}
    </div>
  );
});

// ─── Result Screen ───────────────────────────────────────────────────────────

function ResultScreen({ puzzle, won, stars, attemptHistory, onStats, onArchive, onBack, isArchive }) {
  const [shareStatus, setShareStatus] = useState(null);
  const [fav, setFav] = useState(() => isFavorite(puzzle.number));
  const handleToggleFav = () => {
    toggleFavorite(puzzle.number);
    setFav(f => !f);
  };

  const handleShare = () => {
    const text = generateShareText(puzzle, stars, attemptHistory);
    navigator.clipboard.writeText(text).then(() => {
      setShareStatus('copied');
      setTimeout(() => setShareStatus(null), 2000);
    }).catch(() => {
      setShareStatus('failed');
      setTimeout(() => setShareStatus(null), 2000);
    });
  };

  return (
    <div style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      height: '100%', padding: '1.75rem 1.5rem 1.25rem',
      animation: 'fadeIn 0.5s ease'
    }}>
      <div style={{
        fontSize: '1.65rem', fontFamily: "'DM Serif Display', serif",
        marginBottom: '0.4rem', marginTop: '0.25rem', lineHeight: 1.1
      }}>
        {won ? 'Sorted!' : 'Not quite...'}
      </div>

      {won && (
        <div style={{ textAlign: 'center', marginBottom: '0.85rem' }}>
          <div style={{ fontSize: '1.85rem', letterSpacing: '0.2em', marginBottom: '0.3rem', lineHeight: 1 }}>
            {Array.from({ length: 3 }, (_, i) => (
              <span key={i} style={{
                display: 'inline-block',
                color: i < stars ? GREEN : '#333',
                animation: `starPop 0.4s ease ${i * 0.15}s both`
              }}>{i < stars ? '▆' : '▁'}</span>
            ))}
          </div>
          {SCORE_LABELS[stars] && (
            <div style={{ fontSize: '0.95rem', color: MUTED, letterSpacing: '0.05em' }}>
              {SCORE_LABELS[stars]}
            </div>
          )}
        </div>
      )}

      <div style={{ fontSize: '0.9rem', color: MUTED, marginBottom: '1.25rem' }}>
        Puzzle #{puzzle.number} · {puzzle.category} · <span style={{ color: DIFFICULTY_COLORS[puzzle.difficulty] }}>{DIFFICULTY_LABELS[puzzle.difficulty]}</span>
      </div>

      {/* Correct order reveal */}
      <div style={{
        background: TILE_BG, borderRadius: '12px', padding: '0.85rem 1.15rem',
        width: '100%', maxWidth: '320px', marginBottom: '1rem'
      }}>
        <div style={{ fontSize: '0.72rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: MUTED, marginBottom: '0.5rem' }}>
          {puzzle.dimension}
        </div>
        {puzzle.items.map((item, i) => (
          <div key={item} style={{
            display: 'flex', alignItems: 'center', gap: '0.6rem',
            padding: '0.3rem 0', fontSize: '1rem',
            animation: `fadeIn 0.3s ease ${i * 0.08}s both`
          }}>
            <span style={{ color: '#555', fontSize: '0.82rem', width: '1.1rem', textAlign: 'right' }}>{i + 1}</span>
            <span style={{ color: TILE_COLOR }}>{item}</span>
            {puzzle.itemValues && puzzle.itemValues[i] && (
              <span style={{
                marginLeft: 'auto',
                color: MUTED,
                fontSize: '0.9rem',
                fontVariantNumeric: 'tabular-nums',
                whiteSpace: 'nowrap'
              }}>
                {puzzle.itemValues[i]}
              </span>
            )}
          </div>
        ))}
      </div>

      {/* Fun fact */}
      <div style={{
        background: TILE_BG, borderRadius: '12px', padding: '0.85rem 1.15rem',
        width: '100%', maxWidth: '320px', marginBottom: '1rem',
        fontSize: '0.9rem', color: MUTED, lineHeight: 1.5
      }}>
        <div style={{ fontSize: '0.72rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#555', marginBottom: '0.35rem' }}>
          Did you know?
        </div>
        {puzzle.funFact}
      </div>

      {/* Footer: Share centered with heart flanking right, then one-line links */}
      <div style={{
        display: 'flex', flexDirection: 'column', alignItems: 'center',
        gap: '0.85rem', width: '100%', marginTop: 'auto'
      }}>
        <div style={{
          display: 'grid', gridTemplateColumns: '1fr auto 1fr',
          alignItems: 'center', width: '100%', maxWidth: '360px'
        }}>
          <div />
          <button onClick={handleShare} style={{
            background: GREEN, color: '#0E0F10', border: 'none', borderRadius: '28px',
            padding: '0.8rem 2.1rem', fontSize: '1.05rem', fontWeight: 700,
            fontFamily: "'DM Sans', sans-serif", cursor: 'pointer',
            minWidth: '190px', justifySelf: 'center'
          }}>
            {shareStatus === 'copied' ? 'Copied!' : shareStatus === 'failed' ? 'Copy failed' : 'Share Result'}
          </button>
          <button onClick={handleToggleFav}
            aria-label={fav ? 'Unfavorite puzzle' : 'Favorite puzzle'}
            style={{
              background: 'transparent', border: `1px solid ${SUBTLE_BORDER}`,
              borderRadius: '999px', width: '44px', height: '44px',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              cursor: 'pointer', padding: 0, justifySelf: 'start', marginLeft: '0.6rem'
            }}>
            <HeartIcon filled={fav} size={20} />
          </button>
        </div>

        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          gap: '0.5rem', flexWrap: 'nowrap'
        }}>
          <button onClick={onArchive} style={{
            ...darkLinkStyle, fontSize: '0.82rem', padding: '0.25rem 0.3rem',
            display: 'inline-flex', alignItems: 'center', gap: '0.3rem'
          }}>
            <svg width="13" height="13" viewBox="0 0 20 20" fill="none">
              <rect x="2.5" y="4" width="15" height="13" rx="2" stroke={MUTED} strokeWidth="1.5"/>
              <line x1="2.5" y1="8" x2="17.5" y2="8" stroke={MUTED} strokeWidth="1.5"/>
              <line x1="6" y1="2" x2="6" y2="5.5" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
              <line x1="14" y1="2" x2="14" y2="5.5" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
              <rect x="5" y="10.5" width="2.5" height="2.5" rx="0.5" fill={MUTED}/>
            </svg>
            {isArchive ? 'Back to Archive' : 'Play More Puzzles'}
          </button>
          <span style={{ color: MUTED, fontSize: '0.82rem' }}>·</span>
          <button onClick={onStats} style={{ ...darkLinkStyle, fontSize: '0.82rem', padding: '0.25rem 0.3rem' }}>View Stats</button>
          <span style={{ color: MUTED, fontSize: '0.82rem' }}>·</span>
          <button onClick={onBack} style={{ ...darkLinkStyle, fontSize: '0.82rem', padding: '0.25rem 0.3rem' }}>Back to Home</button>
        </div>
      </div>
    </div>
  );
}

// ─── Game Board ──────────────────────────────────────────────────────────────

function GameBoard({ puzzle, onWin, onLose, onHowTo, onStats, onArchive, onToggleTheme, isDark }) {
  const [order, setOrder] = useState(() => shuffleNotCorrect(puzzle.items));
  const [attempt, setAttempt] = useState(0);
  const [feedback, setFeedback] = useState(Array(COUNT).fill(null));
  const [locked, setLocked] = useState(new Set());
  const [itemHints, setItemHints] = useState({});
  const [itemBgHints, setItemBgHints] = useState({});
  const [sel, setSel] = useState(null);
  const [drag, setDrag] = useState(null);
  const dragRef = useRef(null);
  dragRef.current = drag;
  const [showReveal, setShowReveal] = useState(false);
  const [showValues, setShowValues] = useState(false);
  const [showHintsModal, setShowHintsModal] = useState(false);
  const [showFeedback, setShowFeedback] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [attemptHistory, setAttemptHistory] = useState([]);
  const containerRef = useRef(null);
  const selRef = useRef(null);
  selRef.current = sel;
  const lockedRef = useRef(locked);
  lockedRef.current = locked;

  const containerH = COUNT * STRIDE - GAP;

  // ─── Drag handlers ──────────────────────
  const longPressTimer = useRef(null);
  const dragActivated = useRef(false);

  useEffect(() => () => clearTimeout(longPressTimer.current), []);

  const handlePointerDown = useCallback((idx, e) => {
    if (lockedRef.current.has(idx)) return;
    e.preventDefault();
    if (e.target) e.target.setPointerCapture(e.pointerId);
    dragActivated.current = false;
    setDrag({ idx, startY: e.clientY, dy: 0, moved: false, pointerId: e.pointerId });

    // Start long-press timer (150ms before drag activates)
    clearTimeout(longPressTimer.current);
    longPressTimer.current = setTimeout(() => {
      dragActivated.current = true;
      // Haptic feedback when drag activates
      if (navigator.vibrate) navigator.vibrate(10);
    }, 150);
  }, []);

  const handlePointerMove = useCallback((e) => {
    const d = dragRef.current;
    if (!d) return;
    const dy = e.clientY - d.startY;
    // Require both long-press activation AND dead zone clearance
    if (!dragActivated.current) return;
    if (!d.moved && Math.abs(dy) < DEAD_ZONE) return;
    setDrag(prev => prev ? { ...prev, dy, moved: true } : null);
  }, []);

  const handlePointerUp = useCallback(() => {
    const d = dragRef.current;
    if (!d) return;
    clearTimeout(longPressTimer.current);
    const lk = lockedRef.current;
    if (d.moved) {
      const shift = Math.round(d.dy / STRIDE);
      let toIdx = clamp(d.idx + shift, 0, COUNT - 1);
      // Skip over locked tiles (with bounds check to prevent infinite loop)
      while (lk.has(toIdx) && toIdx !== d.idx) {
        if ((shift > 0 && toIdx >= COUNT - 1) || (shift < 0 && toIdx <= 0)) break;
        toIdx += (shift > 0 ? 1 : -1);
        toIdx = clamp(toIdx, 0, COUNT - 1);
      }
      if (!lk.has(toIdx)) {
        setOrder(prev => reorder(prev, d.idx, toIdx, lk));
      }
    } else {
      // Tap (released before drag activated, or no movement)
      const idx = d.idx;
      const s = selRef.current;
      if (s === null) {
        setSel(idx);
      } else if (s === idx) {
        setSel(null);
      } else {
        // Swap
        if (!lk.has(s) && !lk.has(idx)) {
          setOrder(prev => {
            const next = [...prev];
            [next[s], next[idx]] = [next[idx], next[s]];
            return next;
          });
        }
        setSel(null);
      }
    }
    dragActivated.current = false;
    setDrag(null);
  }, []);

  // Get displacement offset for each tile during drag
  const getOffset = useCallback((idx) => {
    const d = dragRef.current;
    if (!d || !d.moved) return 0;
    if (idx === d.idx) return 0;
    if (lockedRef.current.has(idx)) return 0;
    const insertIdx = clamp(d.idx + Math.round(d.dy / STRIDE), 0, COUNT - 1);
    if (d.idx < insertIdx && idx > d.idx && idx <= insertIdx) return -STRIDE;
    if (d.idx > insertIdx && idx < d.idx && idx >= insertIdx) return STRIDE;
    return 0;
  }, []);

  // ─── Submit ──────────────────────────────
  const handleSubmit = useCallback(() => {
    if (submitting) return;
    setSubmitting(true);
    setSel(null);
    const fb = calcFeedback(order, puzzle.items);
    setFeedback(fb);
    setShowFeedback(true);

    // Update persistent hints
    const newHints = { ...itemHints };
    order.forEach((item, i) => { newHints[item] = fb[i]; });
    setItemHints(newHints);

    // Update persistent background hints
    const newBgHints = { ...itemBgHints };
    order.forEach((item, i) => { newBgHints[item] = fb[i]; });
    setItemBgHints(newBgHints);

    // Lock perfect tiles
    const newLocked = new Set(locked);
    fb.forEach((f, i) => { if (f === 'perfect') newLocked.add(i); });
    setLocked(newLocked);

    const newHistory = [...attemptHistory, fb];
    setAttemptHistory(newHistory);

    const allPerfect = fb.every(f => f === 'perfect');
    const nextAttempt = attempt + 1;

    setTimeout(() => {
      if (allPerfect) {
        const s = MAX_ATTEMPTS - attempt;
        saveStats(puzzle.number, s, nextAttempt, newHistory);
        onWin(s, newHistory);
      } else if (nextAttempt >= MAX_ATTEMPTS) {
        saveStats(puzzle.number, 0, MAX_ATTEMPTS, newHistory);
        onLose(newHistory);
      } else {
        setAttempt(nextAttempt);
        setShowFeedback(false);
        setFeedback(Array(COUNT).fill(null));
        setSubmitting(false);
      }
    }, 1400);
  }, [order, puzzle, attempt, locked, itemHints, itemBgHints, attemptHistory, submitting, onWin, onLose]);

  return (
    <div style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center',
      height: '100%', padding: '1rem 1.5rem', position: 'relative',
      overflow: 'hidden'
    }}>
      {/* Top bar */}
      <div style={{
        width: '100%', maxWidth: '360px', display: 'flex', alignItems: 'center',
        justifyContent: 'space-between', marginBottom: '0.75rem'
      }}>
        <div style={{ fontSize: '1.1rem', fontFamily: "'DM Serif Display', serif" }}>
          Sorted
        </div>
        <div style={{ display: 'flex', gap: '0.6rem', alignItems: 'center' }}>
          <button onClick={onArchive} aria-label="Archive" style={iconBtnStyle}>
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
              <rect x="2.5" y="4" width="15" height="13" rx="2" stroke={MUTED} strokeWidth="1.5"/>
              <line x1="2.5" y1="8" x2="17.5" y2="8" stroke={MUTED} strokeWidth="1.5"/>
              <line x1="6" y1="2" x2="6" y2="5.5" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
              <line x1="14" y1="2" x2="14" y2="5.5" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
              <rect x="5" y="10.5" width="2.5" height="2.5" rx="0.5" fill={MUTED}/>
            </svg>
          </button>
          <button onClick={onToggleTheme} aria-label="Toggle theme" style={iconBtnStyle}>
            {isDark ? (
              <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
                <circle cx="10" cy="10" r="4" fill={MUTED}/>
                <line x1="10" y1="2" x2="10" y2="4.5" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
                <line x1="10" y1="15.5" x2="10" y2="18" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
                <line x1="2" y1="10" x2="4.5" y2="10" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
                <line x1="15.5" y1="10" x2="18" y2="10" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
                <line x1="4.34" y1="4.34" x2="6.11" y2="6.11" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
                <line x1="13.89" y1="13.89" x2="15.66" y2="15.66" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
                <line x1="4.34" y1="15.66" x2="6.11" y2="13.89" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
                <line x1="13.89" y1="6.11" x2="15.66" y2="4.34" stroke={MUTED} strokeWidth="1.5" strokeLinecap="round"/>
              </svg>
            ) : (
              <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
                <path d="M17 10.5C16.3 13.8 13.3 16.2 9.8 16.2C5.6 16.2 2.2 12.8 2.2 8.6C2.2 5.1 4.6 2.1 7.9 1.4C6.6 2.9 5.8 4.8 5.8 6.9C5.8 11 9.2 14.4 13.3 14.4C14.7 14.4 16 13.9 17 10.5Z" fill={MUTED}/>
              </svg>
            )}
          </button>
          <button onClick={onHowTo} aria-label="How to play" style={iconBtnStyle}>
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
              <circle cx="10" cy="10" r="8.5" stroke={MUTED} strokeWidth="1.5"/>
              <text x="10" y="14.5" textAnchor="middle" fill={MUTED} fontSize="12" fontFamily="DM Serif Display, serif">?</text>
            </svg>
          </button>
          <button onClick={onStats} aria-label="Stats" style={iconBtnStyle}>
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
              <rect x="3" y="11" width="3.5" height="6" rx="1" fill={MUTED}/>
              <rect x="8.25" y="7" width="3.5" height="10" rx="1" fill={MUTED}/>
              <rect x="13.5" y="3" width="3.5" height="14" rx="1" fill={MUTED}/>
            </svg>
          </button>
        </div>
      </div>

      {/* Puzzle info + attempt dots */}
      <div style={{ width: '100%', maxWidth: '360px', marginBottom: '1rem' }}>
        <div style={{ fontSize: '0.7rem', color: MUTED, marginBottom: '0.5rem' }}>
          #{puzzle.number} · {puzzle.category} · <span style={{ color: DIFFICULTY_COLORS[puzzle.difficulty] }}>{DIFFICULTY_LABELS[puzzle.difficulty]}</span>
        </div>
        {/* Attempt dots + hint link */}
        <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
          <div style={{ display: 'flex', gap: '6px' }}>
            {Array.from({ length: MAX_ATTEMPTS }, (_, i) => (
              <div key={i} style={{
                width: '8px', height: '8px', borderRadius: '50%',
                background: i < attempt ? MUTED : i === attempt ? GREEN : SUBTLE_BORDER,
                transition: 'background 0.3s ease'
              }} />
            ))}
          </div>
          <div style={{ fontSize: '0.7rem', color: MUTED }}>
            Attempt {attempt + 1} of {MAX_ATTEMPTS}
          </div>
          <button
            onClick={() => setShowHintsModal(true)}
            style={{
              marginLeft: 'auto', background: 'none', border: 'none', padding: 0,
              cursor: 'pointer', fontFamily: "'DM Sans', sans-serif",
              fontSize: '0.65rem', color: (showReveal || showValues) ? AMBER : MUTED,
              transition: 'color 0.2s ease',
            }}
          >
            Need a hint?
          </button>
        </div>
      </div>

      {/* Dimension strip */}
      {showReveal && (
        <div style={{
          width: '100%', maxWidth: '360px', marginBottom: '0.65rem',
          animation: 'fadeIn 0.2s ease',
        }}>
          <span style={{ fontSize: '0.62rem', color: MUTED, fontFamily: "'DM Sans', sans-serif", lineHeight: 1.4 }}>
            Sort by: {puzzle.dimension}
          </span>
        </div>
      )}

      {/* Hints modal */}
      {showHintsModal && (
        <Overlay onClose={() => setShowHintsModal(false)} title="Hints">
          <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
            {/* Sort Rule toggle */}
            <button
              onClick={() => setShowReveal(s => !s)}
              style={{
                display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
                width: '100%', padding: '0.85rem 1rem',
                background: showReveal ? AMBER_BG : 'rgba(255,255,255,0.05)',
                border: `1px solid ${showReveal ? 'rgba(251,191,36,0.35)' : SUBTLE_BORDER}`,
                borderRadius: '12px', cursor: 'pointer',
                fontFamily: "'DM Sans', sans-serif",
                transition: 'background 0.2s ease, border-color 0.2s ease',
                textAlign: 'left',
              }}
            >
              <div style={{ flex: 1, marginRight: '0.75rem' }}>
                <div style={{ fontSize: '0.9rem', fontWeight: 600, color: showReveal ? AMBER : TILE_COLOR, marginBottom: showReveal ? '0.4rem' : '0.2rem' }}>
                  Sort Rule
                </div>
                <div style={{ fontSize: '0.75rem', color: showReveal ? AMBER : MUTED, lineHeight: 1.4 }}>
                  {showReveal ? puzzle.dimension : 'Reveals the hidden sorting dimension.'}
                </div>
              </div>
              <div style={{
                width: '20px', height: '20px', borderRadius: '50%', flexShrink: 0, marginTop: '1px',
                background: showReveal ? AMBER : 'transparent',
                border: `2px solid ${showReveal ? AMBER : MUTED}`,
                transition: 'all 0.2s ease',
              }} />
            </button>
            {/* Values toggle */}
            <button
              onClick={() => setShowValues(s => !s)}
              style={{
                display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
                width: '100%', padding: '0.85rem 1rem',
                background: showValues ? AMBER_BG : 'rgba(255,255,255,0.05)',
                border: `1px solid ${showValues ? 'rgba(251,191,36,0.35)' : SUBTLE_BORDER}`,
                borderRadius: '12px', cursor: 'pointer',
                fontFamily: "'DM Sans', sans-serif",
                transition: 'background 0.2s ease, border-color 0.2s ease',
                textAlign: 'left',
              }}
            >
              <div style={{ flex: 1, marginRight: '0.75rem' }}>
                <div style={{ fontSize: '0.9rem', fontWeight: 600, color: showValues ? AMBER : TILE_COLOR, marginBottom: '0.2rem' }}>
                  Values
                </div>
                <div style={{ fontSize: '0.75rem', color: MUTED, lineHeight: 1.4 }}>
                  Shows each tile's metric once it lands in the correct position.
                </div>
              </div>
              <div style={{
                width: '20px', height: '20px', borderRadius: '50%', flexShrink: 0, marginTop: '1px',
                background: showValues ? AMBER : 'transparent',
                border: `2px solid ${showValues ? AMBER : MUTED}`,
                transition: 'all 0.2s ease',
              }} />
            </button>
          </div>
        </Overlay>
      )}

      {/* Tile container */}
      <div
        ref={containerRef}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        onPointerCancel={handlePointerUp}
        style={{
          position: 'relative',
          width: '100%', maxWidth: '360px',
          height: `${containerH}px`,
          marginBottom: '1.5rem',
          touchAction: 'none'
        }}
      >
        {order.map((item, idx) => {
          const isDragging = drag && drag.idx === idx && drag.moved;
          const baseY = idx * STRIDE;
          const dragY = isDragging ? baseY + drag.dy : baseY + getOffset(idx);

          return (
            <Tile
              key={item}
              item={item}
              index={idx}
              isSelected={sel === idx}
              isLocked={locked.has(idx)}
              isDragging={isDragging}
              feedback={showFeedback ? feedback[idx] : null}
              hintColor={itemHints[item] ? feedbackTextColor(itemHints[item]) : null}
              bgHintColor={itemBgHints[item] ? feedbackBgMuted(itemBgHints[item]) : null}
              y={dragY}
              zIndex={isDragging ? 10 : 1}
              onPointerDown={handlePointerDown}
              itemValue={puzzle.itemValues ? puzzle.itemValues[idx] : null}
              showValues={showValues}
            />
          );
        })}
      </div>

      {/* Submit button */}
      <button
        onClick={handleSubmit}
        disabled={submitting}
        style={{
          background: submitting ? '#333' : GREEN,
          color: submitting ? '#666' : '#0E0F10',
          border: 'none', borderRadius: '28px',
          padding: '0.8rem 2.5rem', fontSize: '0.95rem', fontWeight: 700,
          fontFamily: "'DM Sans', sans-serif", cursor: submitting ? 'default' : 'pointer',
          transition: 'background 0.2s ease, color 0.2s ease',
          maxWidth: '360px', width: '100%'
        }}
      >
        {submitting ? 'Checking...' : 'Submit'}
      </button>

      {/* Tap-to-swap hint */}
      {sel !== null && (
        <div style={{
          fontSize: '0.7rem', color: AMBER, marginTop: '0.75rem',
          animation: 'fadeIn 0.2s ease'
        }}>
          Tap another tile to swap
        </div>
      )}
    </div>
  );
}

// ─── App ─────────────────────────────────────────────────────────────────────

function App() {
  const [archiveDate, setArchiveDate] = useState(null);
  const puzzle = useMemo(() => archiveDate ? getPuzzleForDate(archiveDate) : getTodaysPuzzle(), [archiveDate]);
  const existing = useMemo(() => loadPuzzleState(puzzle.number), [puzzle.number]);

  const [phase, setPhase] = useState('splash');
  const [showHowTo, setShowHowTo] = useState(false);
  const [showStats, setShowStats] = useState(false);
  const [showArchive, setShowArchive] = useState(false);
  const [stars, setStars] = useState(existing ? existing.stars : 0);
  const [attemptHistory, setAttemptHistory] = useState([]);

  // ── Back-navigation (swipe-back / hardware back) ───────────────────────────
  // Each forward in-app transition pushes an undo function + a history entry.
  // On popstate (edge-swipe or back button) we run the top undo.
  const backStack = useRef([]);

  useEffect(() => {
    const onPop = () => {
      const undo = backStack.current.pop();
      if (undo) undo();
      // If the stack is empty we let the browser navigate away normally.
    };
    window.addEventListener('popstate', onPop);
    return () => window.removeEventListener('popstate', onPop);
  }, []);

  const pushBack = (undo) => {
    backStack.current.push(undo);
    try { window.history.pushState({ sorted: true }, ''); } catch {}
  };

  const goBack = () => {
    if (backStack.current.length > 0) window.history.back();
  };

  // Theme
  const [isDark, setIsDark] = useState(() => {
    try { return localStorage.getItem('sorted-theme') !== 'light'; } catch { return true; }
  });
  useEffect(() => {
    document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
    try { localStorage.setItem('sorted-theme', isDark ? 'dark' : 'light'); } catch {}
  }, [isDark]);

  const toggleTheme = () => setIsDark(d => !d);

  const handlePlay = () => {
    pushBack(() => setPhase('splash'));
    setPhase('play');
  };

  const handleWin = (s, history) => {
    setStars(s);
    setAttemptHistory(history);
    setPhase('won');
  };

  const handleLose = (history) => {
    setStars(0);
    setAttemptHistory(history);
    setPhase('lost');
  };

  const openHowTo = () => {
    pushBack(() => setShowHowTo(false));
    setShowHowTo(true);
  };
  const openStats = () => {
    pushBack(() => setShowStats(false));
    setShowStats(true);
  };
  const openArchive = () => {
    pushBack(() => setShowArchive(false));
    setShowArchive(true);
  };

  const handleArchiveSelect = (date) => {
    // Swiping back from an archive-selected puzzle returns to the archive
    // overlay on splash — a safe, stateless return point regardless of where
    // the user originally opened the archive from.
    pushBack(() => {
      setArchiveDate(null);
      setStars(0);
      setAttemptHistory([]);
      setPhase('splash');
      setShowArchive(true);
    });

    const p = getPuzzleForDate(date);
    const saved = loadPuzzleState(p.number);
    setShowArchive(false);
    setArchiveDate(date);
    if (saved) {
      setStars(saved.stars);
      setAttemptHistory(saved.attemptHistory || []);
      setPhase(saved.stars > 0 ? 'won' : 'lost');
    } else {
      setStars(0);
      setAttemptHistory([]);
      setPhase('play');
    }
  };

  return (
    <div style={{
      height: '100%', width: '100%', maxWidth: '430px', margin: '0 auto',
      display: 'flex', flexDirection: 'column', position: 'relative',
      overflow: 'hidden'
    }}>
      <StyleInjector />

      {phase === 'splash' && (
        <SplashScreen
          puzzle={puzzle}
          onPlay={handlePlay}
          onHowTo={openHowTo}
          onStats={openStats}
          onArchive={openArchive}
          alreadyPlayed={!!existing}
          isArchive={!!archiveDate}
          archiveDate={archiveDate}
        />
      )}

      {phase === 'play' && (
        <GameBoard
          key={puzzle.number}
          puzzle={puzzle}
          onWin={handleWin}
          onLose={handleLose}
          onHowTo={openHowTo}
          onStats={openStats}
          onArchive={openArchive}
          onToggleTheme={toggleTheme}
          isDark={isDark}
        />
      )}

      {(phase === 'won' || phase === 'lost') && (
        <ResultScreen
          puzzle={puzzle}
          won={phase === 'won'}
          stars={stars}
          attemptHistory={attemptHistory}
          onStats={openStats}
          onArchive={openArchive}
          onBack={goBack}
          isArchive={!!archiveDate}
        />
      )}

      {showHowTo && <HowToPlay onClose={goBack} />}
      {showStats && <StatsOverlay onClose={goBack} />}
      {showArchive && <ArchiveOverlay onClose={goBack} onSelect={handleArchiveSelect} />}
    </div>
  );
}

// ─── Mount ───────────────────────────────────────────────────────────────────

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
