diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..86d8bd92f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# Documentation and project management folders
+docs/
+.github/
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# IDE/Editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
\ No newline at end of file
diff --git a/README.md b/README.md
index 58f1a8a66..f8b01f939 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,83 @@
-# js-project-recipe-library
+# Recipe Library
+
+A progressive recipe discovery app built over Weeks 5-7 of the Web Development Bootcamp. Features Spoonacular API integration, intelligent caching, search functionality, favorites system, and pagination.
+
+## Live Demo & Screenshot
+
+- **Live App:** https://recipelibrarya.netlify.app
+- **Screenshot:**
+
+
+
+## Features
+
+### Core Functionality
+- **Responsive Design** - Mobile-first layout (320px to 1600px+)
+- **Dynamic Recipe Display** - Template literal-based card generation
+- **Multi-Filter System** - Diet, cuisine, and sorting filters work together
+- **Search** - Real-time search by title and ingredients with 300ms debouncing
+- **Random Recipe** - Discover new recipes with dice button
+- **Empty States** - Helpful guidance when no results found
+
+### Advanced Features
+- **Spoonacular API Integration** - Fetches up to 100 fresh recipes
+- **Smart Caching** - 24-hour localStorage cache with backup fallback
+- **Favorites System** - Save/unsave recipes with localStorage persistence
+- **Pagination** - 15 recipes per page with navigation controls
+- **Loading States** - Clear feedback during async operations
+- **Progressive Enhancement** - Works offline with backup data (19 recipes)
+
+## Technical Implementation
+
+### Data Flow
+```
+Spoonacular API → localStorage Cache → Backup Data
+ ↓
+Data Normalization (mapSpoonacularToLocal)
+ ↓
+Filter Chain: Search → Filter → Sort → Display
+```
+
+### Key Technologies
+- **Vanilla JavaScript** - ES6+ with async/await, template literals, array methods
+- **CSS Grid + Flexbox** - Modern responsive layout
+- **CSS Custom Properties** - Centralized theming system
+- **Fetch API** - External data integration with error handling
+
+### Performance Optimizations
+- Debounced search input (300ms)
+- Pagination for large datasets
+- Lazy image loading
+- Intelligent caching strategy
+- Loading messages with 2-second minimum display time to showcase
+
+## Getting Started
+1. Clone the repository and open `index.html` in your browser.
+2. The app functions fully with live API data, cached recipes, and backup recipes for offline or quota-exceeded scenarios.
+
+## File Structure
+
+```
+├── index.html # Main application
+├── script.js # Core logic (~800 lines)
+├── styles.css # Responsive styling (~531 lines)
+└── backupdata.js # Fallback recipes (19 items)
+```
+
+## Requirements Met
+
+**Week 5-7 Core Requirements**: ✅ All completed
+- Responsive HTML/CSS structure
+- Filter and sorting functionality
+- Array manipulation and recipe display
+- API integration with error handling
+- localStorage caching
+
+**Stretch Goals Achieved**: ✅ 6/6 completed
+- Multiple filters working together
+- Search functionality for specific recipe names or ingredients with debouncing
+- Favorites system with persistence
+- Pagination for performance
+- Loading states and error handling
+- Local storage caching
+
diff --git a/backupdata.js b/backupdata.js
new file mode 100644
index 000000000..72bb7e928
--- /dev/null
+++ b/backupdata.js
@@ -0,0 +1,254 @@
+// Minimal backup recipe data - 19 recipes only
+// Recipe IDs: 658753, 644846, 635345, 982376, 634204, 665403, 661218, 634171, 1062883, 647615, 654504, 649030, 664284, 641145, 644642, 645732, 1043340, 645068, 634854
+
+const backupData = {
+ "recipes": [
+ {
+ "id": 658753,
+ "title": "Roma Tomato Bruschetta",
+ "image": "https://img.spoonacular.com/recipes/658753-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 4,
+ "sourceUrl": "https://www.foodista.com/recipe/M3RH6LBH/roma-tomato-bruschetta",
+ "diets": ["vegetarian", "vegan"],
+ "cuisine": ["mediterranean", "italian"],
+ "ingredients": ["balsamic vinegar", "bread", "basil", "garlic", "tomatoes"],
+ "pricePerServing": 1.89,
+ "popularity": 5
+ },
+ {
+ "id": 644846,
+ "title": "Gluten Free Onion Rings",
+ "image": "https://img.spoonacular.com/recipes/644846-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 4,
+ "sourceUrl": "https://www.foodista.com/recipe/L5CSWWPM/gluten-free-onion-rings",
+ "diets": ["vegetarian"],
+ "cuisine": ["american"],
+ "ingredients": ["egg", "oil", "milk", "onions"],
+ "pricePerServing": 0.86,
+ "popularity": 38
+ },
+ {
+ "id": 635345,
+ "title": "Blue Cheese and Mushroom Turkey Burger",
+ "image": "https://img.spoonacular.com/recipes/635345-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 1,
+ "sourceUrl": "https://www.foodista.com/recipe/3JGSVVX8/blue-cheese-and-mushroom-turkey-burger",
+ "diets": [],
+ "cuisine": ["american"],
+ "ingredients": ["garlic cloves", "ground turkey", "mushrooms", "soy sauce"],
+ "pricePerServing": 2.32,
+ "popularity": 2
+ },
+ {
+ "id": 982376,
+ "title": "Chicken Noodle Casserole Dish",
+ "image": "https://img.spoonacular.com/recipes/982376-556x370.jpg",
+ "readyInMinutes": 55,
+ "servings": 6,
+ "sourceUrl": "https://www.pinkwhen.com/chicken-noodle-casserole-dish/",
+ "diets": [],
+ "cuisine": ["american"],
+ "ingredients": ["bread crumbs", "carrots", "chicken broth", "chicken", "mozzarella"],
+ "pricePerServing": 2.50,
+ "popularity": 5
+ },
+ {
+ "id": 634204,
+ "title": "Banana Walnut Cinnamon Bread",
+ "image": "https://img.spoonacular.com/recipes/634204-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 8,
+ "sourceUrl": "http://www.foodista.com/recipe/CCDRBSRB/banana-walnut-cinnamon-bread",
+ "diets": ["vegetarian"],
+ "cuisine": [],
+ "ingredients": ["baking powder", "eggs", "flour", "salt", "vanilla extract"],
+ "pricePerServing": 0.41,
+ "popularity": 3
+ },
+ {
+ "id": 665403,
+ "title": "Wisconsin Beer Cheese Soup",
+ "image": "https://img.spoonacular.com/recipes/665403-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 4,
+ "sourceUrl": "https://www.foodista.com/recipe/VPR24J2V/wisconsin-beer-cheese-soup",
+ "diets": [],
+ "cuisine": ["american"],
+ "ingredients": ["butter", "flour", "beer", "mustard", "salt"],
+ "pricePerServing": 1.60,
+ "popularity": 2
+ },
+ {
+ "id": 661218,
+ "title": "Spicy Tuna Cakes",
+ "image": "https://img.spoonacular.com/recipes/661218-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 3,
+ "sourceUrl": "https://www.foodista.com/recipe/HWFXR7RN/spicy-tuna-cakes",
+ "diets": [],
+ "cuisine": [],
+ "ingredients": ["breadcrumbs", "tuna", "egg", "hot sauce"],
+ "pricePerServing": 1.84,
+ "popularity": 5
+ },
+ {
+ "id": 634171,
+ "title": "Banana Pudding Cake",
+ "image": "https://img.spoonacular.com/recipes/634171-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 12,
+ "sourceUrl": "https://www.foodista.com/recipe/NM38YSZL/banana-pudding-cake",
+ "diets": [],
+ "cuisine": [],
+ "ingredients": ["baking powder", "cocoa", "eggs", "sugar", "vanilla wafers"],
+ "pricePerServing": 2.36,
+ "popularity": 3
+ },
+ {
+ "id": 1062883,
+ "title": "How to Make Easy Cheesy Garlic Bread",
+ "image": "https://img.spoonacular.com/recipes/1062883-556x370.jpg",
+ "readyInMinutes": 15,
+ "servings": 8,
+ "sourceUrl": "https://www.pinkwhen.com/easy-cheesy-garlic-bread/",
+ "diets": [],
+ "cuisine": [],
+ "ingredients": ["bread", "butter", "cheese", "garlic", "olive oil"],
+ "pricePerServing": 0.75,
+ "popularity": 5
+ },
+ {
+ "id": 647615,
+ "title": "Huli-Huli Chicken",
+ "image": "https://img.spoonacular.com/recipes/647615-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 6,
+ "sourceUrl": "https://www.foodista.com/recipe/BDBHNYGT/huli-huli-chicken",
+ "diets": [],
+ "cuisine": ["hawaiian"],
+ "ingredients": ["chicken drumsticks", "garlic", "ginger", "pineapple"],
+ "pricePerServing": 0.52,
+ "popularity": 2
+ },
+ {
+ "id": 654504,
+ "title": "Pancit Bihon (Filipino Pancit)",
+ "image": "https://img.spoonacular.com/recipes/654504-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 6,
+ "sourceUrl": "https://www.foodista.com/recipe/LDWW4QDB/pancit-bihon-filipino-pancit",
+ "diets": [],
+ "cuisine": ["filipino", "asian"],
+ "ingredients": ["rice vermicelli", "cabbage", "carrots", "chicken", "shrimp"],
+ "pricePerServing": 3.10,
+ "popularity": 3
+ },
+ {
+ "id": 649030,
+ "title": "Korean Style Beef and Vegetables Over Rice",
+ "image": "https://img.spoonacular.com/recipes/649030-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 4,
+ "sourceUrl": "https://www.foodista.com/recipe/5LTQJ3V2/korean-style-beef-and-vegetables-over-rice",
+ "diets": [],
+ "cuisine": ["korean", "asian"],
+ "ingredients": ["beef", "carrots", "green beans", "rice", "sesame oil"],
+ "pricePerServing": 3.46,
+ "popularity": 2
+ },
+ {
+ "id": 664284,
+ "title": "Vanilla and Lime Flan",
+ "image": "https://img.spoonacular.com/recipes/664284-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 8,
+ "sourceUrl": "https://www.foodista.com/recipe/TMCNMVR/vanilla-and-lime-flan",
+ "diets": ["vegetarian"],
+ "cuisine": ["mexican"],
+ "ingredients": ["condensed milk", "eggs", "lime", "vanilla pod", "water"],
+ "pricePerServing": 1.43,
+ "popularity": 2
+ },
+ {
+ "id": 641145,
+ "title": "Curry-Braised Chicken",
+ "image": "https://img.spoonacular.com/recipes/641145-556x370.jpg",
+ "readyInMinutes": 75,
+ "servings": 4,
+ "sourceUrl": "http://www.foodista.com/recipe/YN6PSSKQ/curry-braised-chicken",
+ "diets": [],
+ "cuisine": ["indian", "asian"],
+ "ingredients": ["basmati rice", "chicken breast", "curry paste", "coconut milk"],
+ "pricePerServing": 2.20,
+ "popularity": 2
+ },
+ {
+ "id": 644642,
+ "title": "Ginger Snap and Pumpkin Ice Cream Sandwiches",
+ "image": "https://img.spoonacular.com/recipes/644642-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 20,
+ "sourceUrl": "https://www.foodista.com/recipe/5ZY78MKS/ginger-snap-and-pumpkin-ice-cream-sandwiches",
+ "diets": ["vegetarian"],
+ "cuisine": [],
+ "ingredients": ["flour", "sugar", "ginger", "cinnamon", "pumpkin"],
+ "pricePerServing": 0.76,
+ "popularity": 2
+ },
+ {
+ "id": 645732,
+ "title": "Grilled Ham and Cheese French Toast",
+ "image": "https://img.spoonacular.com/recipes/645732-556x370.jpg",
+ "readyInMinutes": 20,
+ "servings": 4,
+ "sourceUrl": "https://www.foodista.com/recipe/NYVTQ65V/grilled-ham-and-cheese-french-toast",
+ "diets": [],
+ "cuisine": ["american"],
+ "ingredients": ["buttermilk", "egg whites", "mustard", "wheat bread", "ham"],
+ "pricePerServing": 1.34,
+ "popularity": 2
+ },
+ {
+ "id": 1043340,
+ "title": "The BEST Sweet Potato Casserole",
+ "image": "https://img.spoonacular.com/recipes/1043340-556x370.jpg",
+ "readyInMinutes": 40,
+ "servings": 6,
+ "sourceUrl": "https://www.pinkwhen.com/sweet-potato-casserole-gourmet-holiday-baking/",
+ "diets": ["vegetarian"],
+ "cuisine": ["american"],
+ "ingredients": ["sweet potatoes", "butter", "eggs", "vanilla extract", "cinnamon"],
+ "pricePerServing": 1.89,
+ "popularity": 5
+ },
+ {
+ "id": 645068,
+ "title": "Gooey Chocolate Buttermilk Sheet Cake",
+ "image": "https://img.spoonacular.com/recipes/645068-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 12,
+ "sourceUrl": "https://www.foodista.com/recipe/GJ66TZ5G/gooey-chocolate-buttermilk-sheet-cake",
+ "diets": ["vegetarian"],
+ "cuisine": [],
+ "ingredients": ["flour", "baking soda", "butter", "eggs", "sugar"],
+ "pricePerServing": 0.39,
+ "popularity": 3
+ },
+ {
+ "id": 634854,
+ "title": "Berry Fruit Crumble",
+ "image": "https://img.spoonacular.com/recipes/634854-556x370.jpg",
+ "readyInMinutes": 45,
+ "servings": 6,
+ "sourceUrl": "https://www.foodista.com/recipe/67KX7C62/berry-fruit-crumble",
+ "diets": [],
+ "cuisine": [],
+ "ingredients": ["berries", "maple syrup", "oatmeal", "almond meal", "brown sugar"],
+ "pricePerServing": 1.51,
+ "popularity": 18
+ }
+ ]
+}
diff --git a/index.html b/index.html
new file mode 100644
index 000000000..f72e18c17
--- /dev/null
+++ b/index.html
@@ -0,0 +1,134 @@
+
+
+
+
+
+ Recipe Library
+
+
+
+
+
+
+
+
+
+ Recipe Library
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select your preferences above
+
Choose your dietary preferences, cuisine type, cooking time, and sorting options to discover the perfect recipe for you!
+
+ ⏱️ Ready to cook
+ 👨🍳 Easy
+
+
+
+
+
+
+
+
👋 Welcome! Use the filters above to find your perfect recipe.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/screenshot/screenshot.png b/screenshot/screenshot.png
new file mode 100644
index 000000000..c02cee5f4
Binary files /dev/null and b/screenshot/screenshot.png differ
diff --git a/script.js b/script.js
new file mode 100644
index 000000000..c56994576
--- /dev/null
+++ b/script.js
@@ -0,0 +1,964 @@
+// Recipe Library - Spoonacular API with localStorage caching
+// backupData is loaded globally from backupdata.js script tag
+
+// === UI HELPERS ===
+
+/** Update sort button styling based on selection */
+function updateSortSelectedClass() {
+ const sortOptions = document.querySelectorAll('.filter-group.sort-group .filter-option');
+ sortOptions.forEach(option => {
+ const input = option.querySelector('input[type="radio"]');
+ if (input && input.checked) {
+ option.classList.add('selected');
+ } else {
+ option.classList.remove('selected');
+ }
+ });
+}
+
+// === API CONFIGURATION & GLOBAL STATE ===
+
+const API_CONFIG = {
+ BASE_URL: 'https://api.spoonacular.com',
+ API_KEY: '5607f37e52ff4b879d8f5006edbca556',
+ ENDPOINTS: {
+ RANDOM: '/recipes/random'
+ }
+};
+
+// Global recipe storage
+let allRecipes = [];
+let recipes = [];
+let displayedRecipes = [];
+
+// Pagination settings
+let recipesPerPage = 15;
+let currentPage = 1;
+
+// === DOM ELEMENT REFERENCES ===
+
+const clearFiltersBtn = document.getElementById('clear-filters-btn');
+const randomRecipeBtn = document.getElementById('random-recipe-btn');
+const favoritesBtn = document.getElementById('favorites-btn');
+const recipeContainer = document.getElementById('recipe-container');
+const messageDisplay = document.getElementById('message-display');
+const searchInput = document.getElementById('search-input');
+
+
+// === API FUNCTIONS ===
+
+/**
+ * Merge cached and backup recipes, removing duplicates
+ * Pure function - no side effects or UI updates
+ * @returns {Array} - Combined recipes from cache and backup
+ */
+function getMergedRecipes() {
+ const cachedRecipes = getCachedRecipes() || [];
+ const mappedBackup = backupData.recipes.map(mapSpoonacularToLocal);
+
+ // Merge both sources, removing duplicates by ID
+ const combinedRecipes = [...cachedRecipes];
+ const existingIds = new Set(cachedRecipes.map(r => r.id));
+
+ mappedBackup.forEach(recipe => {
+ if (!existingIds.has(recipe.id)) {
+ combinedRecipes.push(recipe);
+ }
+ });
+
+ return combinedRecipes;
+}
+
+/**
+ * Fetch fresh recipes from Spoonacular API
+ * Pure function - only handles API call and data transformation
+ * @param {number} count - Number of recipes to fetch
+ * @returns {Promise} - Array of normalized recipe objects
+ */
+async function fetchFromSpoonacular(count) {
+ const url = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.RANDOM}?number=${count}&apiKey=${API_CONFIG.API_KEY}`;
+ const response = await fetch(url);
+
+ if (!response.ok) {
+ throw new Error(`API Error: ${response.status} ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return data.recipes.map(mapSpoonacularToLocal);
+}
+
+/**
+ * Handle API fallback with appropriate user messaging
+ * @param {Error} error - The error that occurred during API call
+ * @param {number} count - Number of recipes requested
+ * @returns {Array} - Fallback recipes with appropriate count
+ */
+function handleAPIFallback(error, count) {
+ console.error('❌ Failed to fetch recipes:', error);
+
+ const mergedRecipes = getMergedRecipes();
+ const cachedCount = (getCachedRecipes() || []).length;
+
+ // Show appropriate message based on what data is available
+ if (cachedCount > 0) {
+ showMessage(`⚠️ API failed - Using ${mergedRecipes.length} recipes (${cachedCount} cached + ${backupData.recipes.length} backup)`, 'success');
+ } else {
+ showMessage(`📚 Using ${mergedRecipes.length} recipes from backup library`, 'success');
+ }
+
+ setCachedRecipes(mergedRecipes);
+ return mergedRecipes.slice(0, count);
+}
+
+/**
+ * Fetch recipes from Spoonacular API with caching fallback
+ * Main orchestrator function that coordinates API calls and fallbacks
+ * @param {number} count - Number of recipes to fetch (max: 100)
+ * @returns {Promise} - Array of recipe objects
+ */
+async function fetchRecipesFromAPI(count = 100) {
+ // If no API key, use merged cache + backup immediately
+ if (!API_CONFIG.API_KEY) {
+ console.warn('⚠️ No API key found - using merged cache + backup data');
+ const mergedRecipes = getMergedRecipes();
+ showMessage(`📚 Using ${mergedRecipes.length} recipes (cache + backup library)`, 'success');
+ setCachedRecipes(mergedRecipes);
+ return mergedRecipes.slice(0, count);
+ }
+
+ try {
+ // Show loading message and attempt API fetch
+ await showMessage('🔄 Fetching fresh recipes from Spoonacular...', 'loading', 2000);
+ const freshRecipes = await fetchFromSpoonacular(count);
+
+ // Success: cache the fresh recipes and return them
+ setCachedRecipes(freshRecipes);
+ return freshRecipes;
+
+ } catch (error) {
+ // API failed: use fallback strategy
+ return handleAPIFallback(error, count);
+ }
+}
+
+/**
+ * Extract diet information from Spoonacular recipe
+ * @param {object} spoonacularRecipe - Raw recipe from API
+ * @returns {Array} - Array of diet types
+ */
+function extractDiets(spoonacularRecipe) {
+ const diets = [];
+ if (spoonacularRecipe.vegetarian) diets.push('vegetarian');
+ if (spoonacularRecipe.vegan) diets.push('vegan');
+ if (spoonacularRecipe.glutenFree) diets.push('gluten-free');
+ if (spoonacularRecipe.dairyFree) diets.push('dairy-free');
+ return diets;
+}
+
+/**
+ * Extract and normalize cuisine information
+ * @param {object} spoonacularRecipe - Raw recipe from API
+ * @returns {string} - Cuisine type or 'International' as default
+ */
+function extractCuisine(spoonacularRecipe) {
+ return spoonacularRecipe.cuisines && spoonacularRecipe.cuisines.length > 0
+ ? spoonacularRecipe.cuisines[0]
+ : 'International';
+}
+
+/**
+ * Extract ingredient list from recipe
+ * @param {object} spoonacularRecipe - Raw recipe from API
+ * @returns {Array} - Array of ingredient names
+ */
+function extractIngredients(spoonacularRecipe) {
+ return spoonacularRecipe.extendedIngredients
+ ? spoonacularRecipe.extendedIngredients.map(ing => ing.name)
+ : [];
+}
+
+/**
+ * Calculate normalized price per serving
+ * @param {object} spoonacularRecipe - Raw recipe from API
+ * @returns {number} - Price per serving in dollars
+ */
+function calculatePrice(spoonacularRecipe) {
+ const pricePerServing = spoonacularRecipe.pricePerServing
+ ? (spoonacularRecipe.pricePerServing / 100).toFixed(2)
+ : 2.5;
+ return parseFloat(pricePerServing);
+}
+
+/**
+ * Calculate popularity score from various metrics
+ * @param {object} spoonacularRecipe - Raw recipe from API
+ * @returns {number} - Popularity score
+ */
+function calculatePopularity(spoonacularRecipe) {
+ return spoonacularRecipe.aggregateLikes ||
+ Math.round(spoonacularRecipe.spoonacularScore) ||
+ 50;
+}
+
+/**
+ * Transform Spoonacular API data to our local format
+ * @param {object} spoonacularRecipe - Recipe from Spoonacular API
+ * @returns {object} - Normalized recipe object
+ */
+function mapSpoonacularToLocal(spoonacularRecipe) {
+ return {
+ id: spoonacularRecipe.id,
+ title: spoonacularRecipe.title,
+ image: spoonacularRecipe.image,
+ readyInMinutes: spoonacularRecipe.readyInMinutes || 30,
+ servings: spoonacularRecipe.servings || 4,
+ sourceUrl: spoonacularRecipe.sourceUrl || spoonacularRecipe.spoonacularSourceUrl,
+ diets: extractDiets(spoonacularRecipe),
+ cuisine: extractCuisine(spoonacularRecipe),
+ ingredients: extractIngredients(spoonacularRecipe),
+ pricePerServing: calculatePrice(spoonacularRecipe),
+ popularity: calculatePopularity(spoonacularRecipe)
+ };
+}
+
+/**
+ * Save recipes to localStorage with 24-hour expiration
+ * @param {Array} recipesToCache - Recipes to cache
+ */
+function setCachedRecipes(recipesToCache) {
+ try {
+ const cacheData = {
+ recipes: recipesToCache,
+ timestamp: Date.now(),
+ expires: Date.now() + (24 * 60 * 60 * 1000)
+ };
+ localStorage.setItem('spoonacular_recipes', JSON.stringify(cacheData));
+ console.log(`✅ Cached ${recipesToCache.length} recipes`);
+ } catch (error) {
+ console.warn('⚠️ Failed to cache recipes:', error);
+ }
+}
+
+/**
+ * Get cached recipes from localStorage if not expired
+ * @returns {Array|null} - Cached recipes or null
+ */
+function getCachedRecipes() {
+ try {
+ const cached = localStorage.getItem('spoonacular_recipes');
+ if (!cached) return null;
+
+ const cacheData = JSON.parse(cached);
+
+ if (Date.now() > cacheData.expires) {
+ localStorage.removeItem('spoonacular_recipes');
+ return null;
+ }
+
+ return cacheData.recipes;
+ } catch (error) {
+ console.warn('Failed to retrieve cached recipes:', error);
+ return null;
+ }
+}
+
+/** Initialize recipes from API */
+async function initializeRecipes() {
+ try {
+ const allFetchedRecipes = await fetchRecipesFromAPI(100);
+ allRecipes = allFetchedRecipes;
+ recipes = allFetchedRecipes;
+
+ if (allRecipes.length === 0) {
+ showMessage('❌ No recipes available. Please check your API configuration and internet connection.', 'error');
+ }
+
+ return allRecipes;
+ } catch (error) {
+ console.error('Failed to initialize recipes:', error);
+ allRecipes = [];
+ recipes = [];
+ showMessage('❌ Failed to load recipes. Please try refreshing the page.', 'error');
+ return [];
+ }
+}
+
+// === FAVORITES SYSTEM ===
+
+// Initialize favorites from localStorage
+let favoriteRecipes = JSON.parse(localStorage.getItem('favorite_recipes') || '[]');
+let isShowingFavorites = false;
+
+/** Toggle favorite status of a recipe */
+function toggleFavorite(recipeId) {
+ const index = favoriteRecipes.indexOf(recipeId);
+
+ if (index > -1) {
+ favoriteRecipes.splice(index, 1);
+ showMessage('❤️ Removed from favorites', 'success');
+ } else {
+ favoriteRecipes.push(recipeId);
+ showMessage('💖 Added to favorites', 'success');
+ }
+
+ localStorage.setItem('favorite_recipes', JSON.stringify(favoriteRecipes));
+ updateHeartIcons();
+}
+
+window.toggleFavorite = toggleFavorite;
+
+/** Update heart icons for all displayed recipes */
+function updateHeartIcons() {
+ document.querySelectorAll('.heart-btn').forEach(btn => {
+ const recipeId = parseInt(btn.dataset.recipeId);
+ const isFavorited = favoriteRecipes.includes(recipeId);
+
+ btn.querySelector('.heart-icon').textContent = isFavorited ? '💖' : '🤍';
+ btn.classList.toggle('favorited', isFavorited);
+ });
+}
+
+/** Show only favorited recipes */
+function showFavoritesView() {
+ const favRecipes = allRecipes.filter(r => favoriteRecipes.includes(r.id));
+
+ if (favRecipes.length === 0) {
+ recipeContainer.innerHTML = `
+
+
No favorites yet 💔
+
Click the heart icon to save recipes!
+
+ `;
+ showMessage('Start adding favorites!', 'error');
+ return;
+ }
+
+ isShowingFavorites = true;
+ favoritesBtn.classList.add('selected');
+ displayMultipleRecipes(favRecipes);
+ showMessage(`💖 ${favRecipes.length} favorite${favRecipes.length === 1 ? '' : 's'}`, 'success');
+}
+
+/** Show all recipes (exit favorites view) */
+function showAllRecipesView() {
+ isShowingFavorites = false;
+ favoritesBtn.classList.remove('selected');
+ displayMultipleRecipes(allRecipes);
+ showMessage('👋 All recipes', 'success');
+}
+
+// === FILTER & DISPLAY FUNCTIONS ===
+
+/** Get all current filter selections */
+function getAllFilters() {
+ return {
+ diet: getSelectedFilter('diet'),
+ cuisine: getSelectedFilter('cuisine'),
+ sort: getSelectedFilter('sort')
+ };
+}
+
+/**
+ * Search recipes by title or ingredients
+ * @param {string} searchTerm - Search text
+ * @param {array} recipesToSearch - Recipes to search
+ * @returns {array} - Matching recipes
+ */
+function searchRecipes(searchTerm, recipesToSearch = allRecipes) {
+ if (!searchTerm || searchTerm.trim() === '') {
+ return recipesToSearch;
+ }
+
+ const term = searchTerm.toLowerCase().trim();
+
+ return recipesToSearch.filter(recipe => {
+ const titleMatch = recipe.title.toLowerCase().includes(term);
+ const ingredientMatch = recipe.ingredients.some(ingredient =>
+ ingredient.toLowerCase().includes(term)
+ );
+ return titleMatch || ingredientMatch;
+ });
+}
+
+/**
+ * Filter recipes by selected criteria
+ * @param {object} filters - Filter object
+ * @param {array} recipesToFilter - Recipes to filter
+ * @returns {array} - Filtered recipes
+ */
+function filterRecipes(filters, recipesToFilter = recipes) {
+ return recipesToFilter.filter(recipe => {
+ // Diet filter
+ if (filters.diet && !recipe.diets.includes(filters.diet)) {
+ return false;
+ }
+
+ // Cuisine filter
+ if (filters.cuisine && recipe.cuisine.toLowerCase() !== filters.cuisine.toLowerCase()) {
+ return false;
+ }
+
+ return true;
+ });
+}
+
+/**
+ * Sort recipes by specified criteria
+ * @param {array} recipesToSort - Recipes to sort
+ * @param {string} sortBy - Sort field
+ * @param {boolean} ascending - Sort direction
+ * @returns {array} - Sorted recipes
+ */
+function sortRecipes(recipesToSort, sortBy, ascending = true) {
+ const sorted = [...recipesToSort].sort((a, b) => {
+ let valueA, valueB;
+
+ switch(sortBy) {
+ case 'cooking-time':
+ valueA = a.readyInMinutes;
+ valueB = b.readyInMinutes;
+ break;
+ case 'popularity':
+ valueA = a.popularity;
+ valueB = b.popularity;
+ break;
+ default:
+ return 0;
+ }
+
+ return ascending ? valueA - valueB : valueB - valueA;
+ });
+
+ return sorted;
+}
+
+/**
+ * Creates HTML for a recipe card using real recipe data
+ * Uses template literals (backticks) to create HTML string
+ * @param {object} recipe - Recipe object from the recipes array
+ * @returns {string} HTML string for the recipe card
+ */
+function createRecipeCardHTML(recipe) {
+ return `
+
+ ${createRecipeImage(recipe)}
+ ${createRecipeContent(recipe)}
+
+ `;
+}
+
+/**
+ * Creates the image section of a recipe card
+ * Includes the recipe image and favorite heart button
+ * @param {object} recipe - Recipe object
+ * @returns {string} HTML string for the image section
+ */
+function createRecipeImage(recipe) {
+ // Check if recipe is favorited to show correct heart icon
+ const isFavorited = favoriteRecipes.includes(recipe.id);
+ const heartIcon = isFavorited ? '💖' : '🤍'; // Filled or outline heart
+ const heartClass = isFavorited ? 'favorited' : '';
+
+ return `
+
+

+
+
+ `;
+}
+
+/**
+ * Creates the content section of a recipe card
+ * Includes title, meta info, details, and ingredients
+ * @param {object} recipe - Recipe object
+ * @returns {string} HTML string for the content section
+ */
+function createRecipeContent(recipe) {
+ // Create a shortened ingredients list (max 6 ingredients)
+ // .slice(0, 6) gets first 6 items
+ // .join(', ') puts them together with commas
+ const ingredientsList = recipe.ingredients.slice(0, 6).join(', ') +
+ (recipe.ingredients.length > 6 ? '...' : '');
+
+ return `
+
+
${recipe.title}
+
+ ⏱️ ${recipe.readyInMinutes} min
+ 👨🍳 ${recipe.servings} servings
+
+
+
Cuisine: ${recipe.cuisine}
+
Diet: ${recipe.diets.length > 0 ? recipe.diets.join(', ') : 'No restrictions'}
+
Price: $${recipe.pricePerServing}/serving
+
+
+ Ingredients: ${ingredientsList}
+
+
+ `;
+}
+
+/**
+ * Display multiple recipes with pagination
+ * @param {array} recipesToDisplay - Array of recipe objects to display
+ */
+function displayMultipleRecipes(recipesToDisplay) {
+ displayedRecipes = recipesToDisplay;
+ displayPage(recipesToDisplay, 1);
+}
+
+/**
+ * Display a specific page of recipes
+ * @param {array} recipes - All recipes to display
+ * @param {number} pageNum - Page number to show
+ */
+function displayPage(recipes, pageNum = 1) {
+ if (recipes.length === 0) {
+ displayEmptyState();
+ return;
+ }
+
+ const totalPages = Math.ceil(recipes.length / recipesPerPage);
+ currentPage = Math.max(1, Math.min(pageNum, totalPages));
+
+ const startIndex = (currentPage - 1) * recipesPerPage;
+ const endIndex = startIndex + recipesPerPage;
+ const pageRecipes = recipes.slice(startIndex, endIndex);
+
+ const recipeCards = pageRecipes.map(r => createRecipeCardHTML(r)).join('');
+
+ recipeContainer.innerHTML = `
+ ${recipeCards}
+ ${totalPages > 1 ? createPaginationHTML(totalPages) : ''}
+ `;
+
+ const startNum = startIndex + 1;
+ const endNum = startIndex + pageRecipes.length;
+ const message = totalPages > 1
+ ? `📚 ${startNum}-${endNum} of ${recipes.length} (Page ${currentPage}/${totalPages})`
+ : `📚 ${recipes.length} recipe${recipes.length === 1 ? '' : 's'}`;
+ showMessage(message, 'success');
+}
+
+/**
+ * Create pagination HTML
+ * @param {number} totalPages - Total number of pages
+ * @returns {string} HTML for pagination buttons
+ */
+function createPaginationHTML(totalPages) {
+ return `
+
+ `;
+}
+
+/**
+ * Navigate to a specific page
+ * @param {number} pageNum - Page number to go to
+ */
+function goToPage(pageNum) {
+ displayPage(displayedRecipes, pageNum);
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+}
+
+window.goToPage = goToPage;
+
+/**
+ * Display empty state when no recipes match filters
+ * Shows friendly message encouraging user to adjust filters
+ */
+function displayEmptyState() {
+ recipeContainer.innerHTML = `
+
+
No recipes found 😕
+
Try adjusting your filters to find more recipes!
+
+ `;
+ showMessage('No recipes match your current filters. Try different selections!', 'error');
+}
+
+/**
+ * Display a single recipe card
+ * Simpler version for showing just one recipe
+ * @param {object} recipe - Recipe object to display
+ */
+function displayRecipe(recipe) {
+ // Replace container content with single recipe card
+ recipeContainer.innerHTML = createRecipeCardHTML(recipe);
+}
+
+/**
+ * Show a message to the user
+ * Updates the message display area with text and styling
+ * @param {string} message - Message to display
+ * @param {string} type - Type of message ('success', 'error', or 'loading')
+ * @param {number} minDisplayTime - Minimum time to display message in milliseconds (default: 0)
+ * @returns {Promise} - Resolves after minimum display time for loading messages
+ */
+function showMessage(message, type = '', minDisplayTime = 0) {
+ // Step 1: Set the text content
+ messageDisplay.textContent = message;
+
+ // Step 2: Set CSS classes for styling
+ // The type determines the color (green for success, red for error)
+ messageDisplay.className = `message-display ${type}`;
+
+ // Step 3: Return promise for minimum display time (used for loading states)
+ if (minDisplayTime > 0) {
+ return new Promise(resolve => setTimeout(resolve, minDisplayTime));
+ }
+ return Promise.resolve();
+}
+
+/**
+ * Get and display a random recipe
+ * Picks one random recipe from all available recipes
+ */
+function getRandomRecipe() {
+ // Step 1: Generate random index
+ // Math.random() gives number between 0 and 1
+ // Multiply by array length, then floor() rounds down to whole number
+ const randomIndex = Math.floor(Math.random() * allRecipes.length);
+
+ // Step 2: Get the recipe at that random index
+ const randomRecipe = allRecipes[randomIndex];
+
+ // Step 3: Clear any existing filters for clean display
+ clearAllFiltersQuiet();
+
+ // Step 4: Display the single random recipe
+ // Wrap in array because displayMultipleRecipes expects an array
+ displayMultipleRecipes([randomRecipe]);
+
+ // Step 5: Show fun message
+ showMessage(`🎲 Here's a random recipe for you: ${randomRecipe.title}! Feeling adventurous?`, 'success');
+}
+
+// -------------------------------------------
+// State Management Functions
+// -------------------------------------------
+
+/**
+ * SEARCH INPUT HANDLER: Handle search input with debouncing
+ * Delays filtering until user stops typing (300ms delay)
+ * Prevents excessive filtering while user is still typing
+ */
+let searchTimeout; // Store the timeout ID so we can cancel it
+function handleSearchInput() {
+ // Step 1: Cancel any previous timeout
+ // This ensures we only filter once user stops typing
+ clearTimeout(searchTimeout);
+
+ // Step 2: Set new timeout to filter after 300ms
+ searchTimeout = setTimeout(() => {
+ // Step 3: Trigger the filter system
+ findRecipe();
+ }, 300); // 300ms delay
+}
+
+/**
+ * Clear all filters without updating the display
+ * Resets all filter dropdowns and radio buttons to default state
+ */
+function clearAllFiltersQuiet() {
+ // Clear select filters
+ const dietFilter = document.getElementById('diet-filter');
+ const cuisineFilter = document.getElementById('cuisine-filter');
+
+ if (dietFilter) dietFilter.value = '';
+ if (cuisineFilter) cuisineFilter.value = '';
+
+ // Clear all radio buttons
+ const allRadios = document.querySelectorAll('input[type="radio"]');
+ allRadios.forEach(radio => {
+ radio.checked = false;
+ });
+
+ // Clear search input
+ if (searchInput) {
+ searchInput.value = '';
+ }
+
+ // Reset sort order to ascending (default)
+ const orderDesc = document.getElementById('order-desc');
+ const orderAsc = document.getElementById('order-asc');
+ orderDesc.classList.remove('selected');
+ orderAsc.classList.add('selected');
+
+ // Update sort display
+ updateSortSelectedClass();
+}
+
+/**
+ * Clear all filter selections and reset the display
+ * Like clearAllFiltersQuiet but also shows all recipes again
+ */
+function clearAllFilters() {
+ // Clear native select filters
+ const dietFilter = document.getElementById('diet-filter');
+ const cuisineFilter = document.getElementById('cuisine-filter');
+
+ if (dietFilter) dietFilter.value = '';
+ if (cuisineFilter) cuisineFilter.value = '';
+
+ // Clear all radio buttons (sort options)
+ const allRadios = document.querySelectorAll('input[type="radio"]');
+ allRadios.forEach(radio => {
+ radio.checked = false;
+ });
+
+ // Step 3: Clear search input
+ if (searchInput) {
+ searchInput.value = '';
+ }
+
+ // Step 4: Reset sort order to ascending (default)
+ const orderDesc = document.getElementById('order-desc');
+ const orderAsc = document.getElementById('order-asc');
+ orderDesc.classList.remove('selected');
+ orderAsc.classList.add('selected');
+
+ // Step 5: Update sort display
+ updateSortSelectedClass();
+
+ // Step 6: Show all recipes again
+ displayMultipleRecipes(allRecipes);
+ showMessage('👋 Filters cleared! Showing all recipes.');
+}
+
+/**
+ * Get the current sort order (ascending or descending)
+ * @returns {boolean} - True if ascending, false if descending
+ */
+function getSortOrder() {
+ const orderDesc = document.getElementById('order-desc');
+ return !orderDesc.classList.contains('selected');
+}
+
+/**
+ * Create informative message about current recipe results
+ * @param {Array} recipesToShow - Filtered recipes
+ * @param {string} searchTerm - Current search term
+ * @param {object} filters - Current filter selections
+ * @returns {string} - User-friendly status message
+ */
+function createResultMessage(recipesToShow, searchTerm, filters) {
+ const totalCount = recipesToShow.length;
+ const hasAnyFilter = filters.diet || filters.cuisine;
+
+ let message = '';
+
+ if (searchTerm || hasAnyFilter) {
+ // User has applied filters or search
+ message = `Found ${totalCount} recipe${totalCount === 1 ? '' : 's'}`;
+
+ if (searchTerm) message += ` matching "${searchTerm}"`;
+ if (filters.diet) message += ` for ${filters.diet} diet`;
+ if (filters.cuisine) message += ` from ${filters.cuisine} cuisine`;
+ if (filters.sort) {
+ const order = getSortOrder() ? 'ascending' : 'descending';
+ message += `, sorted by ${filters.sort.replace('-', ' ')} (${order})`;
+ }
+ } else {
+ // Showing all recipes (no filters)
+ message = `Showing ${totalCount} recipes`;
+ if (filters.sort) {
+ const order = getSortOrder() ? 'ascending' : 'descending';
+ message += `, sorted by ${filters.sort.replace('-', ' ')} (${order})`;
+ }
+ if (totalCount > recipesPerPage) {
+ message += ` (scroll for more)`;
+ }
+ }
+
+ return message;
+}
+
+/**
+ * Apply the complete filter pipeline to recipes
+ * @param {Array} allRecipes - Complete recipe dataset
+ * @param {string} searchTerm - Search text
+ * @param {object} filters - Filter selections
+ * @returns {Array} - Filtered and sorted recipes
+ */
+function applyFilterPipeline(allRecipes, searchTerm, filters) {
+ let recipesToShow = [...allRecipes];
+
+ // Apply search filter first (if there's a search term)
+ if (searchTerm) {
+ recipesToShow = searchRecipes(searchTerm, recipesToShow);
+ }
+
+ // Apply diet and cuisine filters
+ const hasAnyFilter = filters.diet || filters.cuisine;
+ if (hasAnyFilter) {
+ recipesToShow = filterRecipes(filters, recipesToShow);
+ }
+
+ // Apply sorting if a sort option is selected
+ if (filters.sort && recipesToShow.length > 0) {
+ const ascending = getSortOrder();
+ recipesToShow = sortRecipes(recipesToShow, filters.sort, ascending);
+ }
+
+ return recipesToShow;
+}
+
+/**
+ * Main function that handles filtering and displaying recipes
+ * This is called every time a filter changes
+ */
+function findRecipe() {
+ // Exit favorites view if user is applying filters
+ if (isShowingFavorites) {
+ isShowingFavorites = false;
+ favoritesBtn.classList.remove('selected');
+ }
+
+ // Get current user selections
+ const filters = getAllFilters();
+ const searchTerm = searchInput ? searchInput.value.trim() : '';
+
+ // Apply complete filter pipeline
+ const recipesToShow = applyFilterPipeline(allRecipes, searchTerm, filters);
+
+ // Update global state and display
+ recipes = recipesToShow;
+ displayMultipleRecipes(recipesToShow);
+
+ // Show appropriate status message
+ if (recipesToShow.length === 0) {
+ showMessage('No recipes match your filters. Try adjusting your selections!', 'error');
+ } else {
+ const message = createResultMessage(recipesToShow, searchTerm, filters);
+ showMessage(message, 'success');
+ }
+}
+
+// ===============================================
+// SECTION 7: EVENT HANDLERS & INITIALIZATION
+// ===============================================
+// Event listeners let us respond to user actions like clicks and scrolls
+
+/**
+ * Clear Filters Button Click Handler
+ * When user clicks the broom button, clear all filters and show all recipes
+ */
+clearFiltersBtn.addEventListener('click', function() {
+ clearAllFilters();
+});
+
+/**
+ * Random Recipe Button Click Handler
+ * When user clicks the dice button, show one random recipe
+ */
+randomRecipeBtn.addEventListener('click', function() {
+ getRandomRecipe();
+});
+
+/**
+ * Favorites Button Click Handler
+ * When user clicks heart button, toggle between favorites view and all recipes view
+ */
+favoritesBtn.addEventListener('click', function() {
+ if (isShowingFavorites) {
+ showAllRecipesView(); // Currently showing favorites → switch to all recipes
+ } else {
+ showFavoritesView(); // Currently showing all → switch to favorites only
+ }
+});
+
+// Document ready - runs when the page finishes loading
+document.addEventListener('DOMContentLoaded', async function() {
+ // Initialize recipes from API
+ await showMessage('🔄 Loading recipes...', 'loading', 2000);
+
+ try {
+ await initializeRecipes();
+
+ // Display all recipes on page load with infinite scroll
+ displayMultipleRecipes(allRecipes);
+ showMessage('👋 Welcome! Browse all recipes below or use filters to find your perfect match.');
+
+ } catch (error) {
+ console.error('Failed to load recipes:', error);
+ showMessage('⚠️ Failed to load recipes. Please try refreshing the page.', 'error');
+ }
+
+ // Add event listener to search input
+ if (searchInput) {
+ searchInput.addEventListener('input', handleSearchInput);
+ }
+
+ // Add event listeners to select filters
+ const dietFilter = document.getElementById('diet-filter');
+ const cuisineFilter = document.getElementById('cuisine-filter');
+
+ if (dietFilter) {
+ dietFilter.addEventListener('change', findRecipe);
+ }
+
+ if (cuisineFilter) {
+ cuisineFilter.addEventListener('change', findRecipe);
+ }
+
+ // Add event listeners to radio buttons (sort)
+ const allRadios = document.querySelectorAll('input[type="radio"]');
+ allRadios.forEach(radio => {
+ radio.addEventListener('change', function() {
+ updateSortSelectedClass();
+ findRecipe();
+ });
+ });
+
+ // Add event listeners to order direction buttons
+ const orderDesc = document.getElementById('order-desc');
+ const orderAsc = document.getElementById('order-asc');
+
+ // Set default to ascending
+ orderAsc.classList.add('selected');
+
+ orderDesc.addEventListener('click', function() {
+ orderDesc.classList.add('selected');
+ orderAsc.classList.remove('selected');
+ findRecipe();
+ });
+
+ orderAsc.addEventListener('click', function() {
+ orderAsc.classList.add('selected');
+ orderDesc.classList.remove('selected');
+ findRecipe();
+ });
+
+ // Initial state for sort selected
+ updateSortSelectedClass();
+});
+
+// FILTER HELPER FUNCTIONS
+
+/**
+ * Get selected value from filter (native select) or sort (radio buttons)
+ * @param {string} name - Filter name ('diet', 'cuisine', 'sort')
+ * @returns {string} Selected value or empty string
+ */
+function getSelectedFilter(name) {
+ // For select dropdowns (diet, cuisine)
+ const select = document.getElementById(`${name}-filter`);
+ if (select) {
+ return select.value;
+ }
+
+ // For radio buttons (sort)
+ const selectedRadio = document.querySelector(`input[name="${name}"]:checked`);
+ return selectedRadio ? selectedRadio.value : '';
+}
diff --git a/styles.css b/styles.css
new file mode 100644
index 000000000..110eb286b
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,509 @@
+/* Recipe Library CSS
+ Simple styling for recipe app
+ Responsive design with clean layout */
+
+/* Color scheme and theming */
+
+:root {
+ --color-blue: #0018A4;
+ --color-pink: #FF6589;
+ --color-light-pink: #FFECEA;
+ --color-white: #FFFFFF;
+ --color-mint: #CCFFE2;
+}
+
+/* Reset and base styles */
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ line-height: 1.6;
+ color: var(--color-blue);
+ background-color: var(--color-white);
+}
+
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
+
+/* Main content layout */
+
+.main-content {
+ padding: 2rem 0;
+ min-height: calc(100vh - 200px);
+}
+
+.section-title {
+ text-align: center;
+ font-size: 2rem;
+ color: var(--color-blue);
+ margin-bottom: 2rem;
+ font-weight: 600;
+}
+
+/* Search bar styling */
+
+.search-container {
+ margin-bottom: 2rem;
+}
+
+.search-input {
+ width: 100%;
+ padding: 1rem 1.5rem;
+ font-size: 1.1rem;
+ border: 2.5px solid var(--color-blue);
+ border-radius: 50px;
+ background: var(--color-white);
+ color: var(--color-blue);
+ transition: all 0.3s ease;
+ font-family: inherit;
+}
+
+.search-input::placeholder {
+ opacity: 0.5;
+}
+
+.search-input:focus {
+ outline: none;
+ border-color: var(--color-pink);
+ background: var(--color-light-pink);
+ box-shadow: 0 4px 12px rgba(255, 105, 180, 0.2);
+}
+
+/* Filter controls and buttons */
+
+.filters-section {
+ background: var(--color-white);
+ border-radius: 12px;
+ padding: 2rem;
+ margin-bottom: 3rem;
+ box-shadow: 0 4px 6px rgba(0, 24, 164, 0.08);
+ border: 1.5px solid var(--color-blue);
+}
+
+.filter-group {
+ margin-bottom: 2rem;
+}
+
+.filter-title {
+ font-size: 1.1rem;
+ color: var(--color-blue);
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+}
+
+.dropdown-filters-row {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 2rem;
+ margin-bottom: 2rem;
+}
+
+/* Native select dropdown styling */
+.filter-select {
+ width: 100%;
+ padding: 0.8rem 1.2rem;
+ border: 2.5px solid var(--color-blue);
+ border-radius: 25px;
+ background: var(--color-white);
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ outline: none;
+}
+
+.filter-select:hover {
+ border-color: var(--color-pink);
+}
+
+.filter-select:focus {
+ border-color: var(--color-pink);
+ background: var(--color-light-pink);
+ box-shadow: 0 0 0 3px rgba(255, 107, 197, 0.1);
+}
+
+.sort-group {
+ margin-bottom: 2rem;
+ text-align: center;
+}
+
+.filter-options {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 0.5rem;
+}
+
+.filter-option {
+ padding: 0.5rem 1rem;
+ border: 2px solid var(--color-blue);
+ border-radius: 25px;
+ background: var(--color-white);
+ color: var(--color-blue);
+ cursor: pointer;
+ transition: all 0.2s;
+ font-weight: 500;
+}
+
+.filter-option:hover {
+ background: var(--color-mint);
+ border-color: var(--color-mint);
+}
+
+.filter-option.selected {
+ background: var(--color-blue);
+ color: var(--color-white);
+}
+
+.filter-option input[type="radio"] {
+ margin-right: 0.5rem;
+}
+
+.order-direction-group {
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+ margin: 1rem 0;
+}
+
+.btn-order-direction {
+ background: var(--color-blue);
+ color: var(--color-white);
+ border: 2.5px solid var(--color-blue);
+ border-radius: 25px;
+ padding: 0.6rem 2rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-order-direction:hover {
+ background: var(--color-mint);
+ color: var(--color-blue);
+ border-color: var(--color-mint);
+}
+
+.btn-order-direction.selected {
+ background: var(--color-pink);
+ color: var(--color-white);
+ border-color: var(--color-pink);
+}
+
+/* Button styling */
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.3rem;
+ padding: 0.75rem 2rem;
+ border: none;
+ border-radius: 25px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ text-transform: uppercase;
+}
+
+.btn-primary,
+.btn-secondary {
+ background: var(--color-pink);
+ color: var(--color-white);
+}
+
+.btn-primary:hover,
+.btn-secondary:hover {
+ background: var(--color-mint);
+ color: var(--color-blue);
+ transform: translateY(-2px);
+}
+
+.btn-primary.selected {
+ background: var(--color-blue);
+ color: var(--color-white);
+}
+
+.action-buttons {
+ display: flex;
+ gap: 1rem;
+ justify-content: center;
+ margin-top: 2rem;
+ flex-wrap: wrap;
+}
+
+.btn-icon {
+ font-size: 1.2em;
+}
+
+.btn:hover .btn-icon {
+ transform: scale(1.1);
+}
+
+/* Recipe cards and pagination */
+
+.recipe-display-section {
+ text-align: center;
+}
+
+.recipe-container {
+ margin-bottom: 2rem;
+}
+
+.recipe-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 1.5rem;
+ margin: 2rem 0;
+}
+
+/* Pagination Controls */
+.pagination {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 2rem;
+ margin: 3rem 0;
+ padding: 1.5rem;
+}
+
+.pagination-btn {
+ padding: 0.8rem 1.5rem;
+ border: 2.5px solid var(--color-blue);
+ border-radius: 25px;
+ background: var(--color-white);
+ color: var(--color-blue);
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.pagination-btn:hover:not(:disabled) {
+ background: var(--color-blue);
+ color: var(--color-white);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 165, 251, 0.3);
+}
+
+.pagination-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ border-color: #ccc;
+ color: #ccc;
+}
+
+.pagination-info {
+ font-weight: 600;
+ color: var(--color-blue);
+ font-size: 1rem;
+}
+
+.recipe-card {
+ background: var(--color-white);
+ border-radius: 15px;
+ overflow: hidden;
+ box-shadow: 0 0 0 2px var(--color-blue);
+ max-width: 400px;
+ transition: all 0.3s;
+ justify-self: center;
+}
+
+.recipe-card:hover {
+ box-shadow: 0 4px 12px var(--color-blue);
+ transform: translateY(-3px);
+}
+
+.recipe-image {
+ height: 250px;
+ background: var(--color-light-pink);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+}
+
+.recipe-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.placeholder-image {
+ font-size: 4rem;
+ color: var(--color-blue);
+ opacity: 0.5;
+}
+
+.heart-btn {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ background: rgba(255, 255, 255, 0.9);
+ border: none;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.heart-btn:hover {
+ background: white;
+ transform: scale(1.1);
+}
+
+.heart-btn.favorited {
+ background: var(--color-pink);
+ color: white;
+}
+
+.heart-icon {
+ font-size: 1.2rem;
+}
+
+.recipe-content {
+ padding: 1.5rem;
+}
+
+.recipe-title {
+ font-size: 1.4rem;
+ color: var(--color-blue);
+ margin-bottom: 0.5rem;
+ font-weight: 600;
+}
+
+.recipe-description {
+ color: var(--color-pink);
+ margin-bottom: 1rem;
+}
+
+.recipe-meta {
+ display: flex;
+ justify-content: space-between;
+ color: var(--color-blue);
+ font-size: 0.9rem;
+}
+
+.recipe-details {
+ margin: 1rem 0;
+ font-size: 0.9rem;
+}
+
+/* Messages and utility classes */
+
+.message-display {
+ background: var(--color-mint);
+ border: 1.5px solid var(--color-blue);
+ border-radius: 8px;
+ padding: 1rem;
+ margin: 1rem 0;
+ color: var(--color-blue);
+ font-weight: 500;
+}
+
+.message-display.error {
+ background: var(--color-light-pink);
+ border-color: var(--color-pink);
+ color: var(--color-pink);
+}
+
+.message-display.loading {
+ background: #fff3cd;
+ border-color: #ffc107;
+ color: #856404;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 3rem;
+ color: var(--color-blue);
+ background: var(--color-light-pink);
+ border-radius: 12px;
+ border: 2px solid var(--color-pink);
+}
+
+.empty-state h3 {
+ color: var(--color-pink);
+ margin-bottom: 1rem;
+}
+
+.hidden {
+ display: none;
+}
+
+/* Footer styling */
+
+.footer {
+ background: var(--color-blue);
+ color: var(--color-white);
+ text-align: center;
+ padding: 1.5rem 0;
+ margin-top: 3rem;
+}
+
+/* Responsive design for different screen sizes */
+
+/* Tablet and below */
+@media (max-width: 768px) {
+ .dropdown-filters-row {
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+ }
+
+ .filter-options {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .action-buttons {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .recipe-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Mobile */
+@media (max-width: 576px) {
+ .container {
+ padding: 0 15px;
+ }
+
+ .dropdown-filters-row,
+ .filter-options {
+ grid-template-columns: 1fr;
+ }
+
+ .order-direction-group {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .btn {
+ width: 100%;
+ max-width: 250px;
+ }
+
+ .recipe-image {
+ height: 200px;
+ }
+}
+
+/* Accessibility */
+@media (prefers-reduced-motion: reduce) {
+ * {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}