diff --git a/.DS_Store b/.DS_Store index b3b5687b..b5a07990 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java b/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java index 4180e96e..26be11cb 100644 --- a/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java +++ b/backend/src/main/java/com/rit/canteen/sales/controller/ProductController.java @@ -42,6 +42,13 @@ public class ProductController { return productRepository.findByIsDraftTrue(); } + @GetMapping("/{id}") + public ResponseEntity getProductById(@PathVariable Long id) { + return productRepository.findByIdWithStalls(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + @GetMapping("/category/{categoryName}") public List getProductsByCategory(@PathVariable String categoryName) { return productRepository.findByCategory(categoryName); diff --git a/backend/src/main/java/com/rit/canteen/sales/repository/ProductRepository.java b/backend/src/main/java/com/rit/canteen/sales/repository/ProductRepository.java index 97b71b7e..bc9d55ba 100644 --- a/backend/src/main/java/com/rit/canteen/sales/repository/ProductRepository.java +++ b/backend/src/main/java/com/rit/canteen/sales/repository/ProductRepository.java @@ -30,6 +30,9 @@ public interface ProductRepository extends JpaRepository { @Query("SELECT p FROM Product p WHERE p.productId = :productId AND (p.isDraft = false OR p.isDraft IS NULL)") java.util.Optional findByProductIdRobust(@org.springframework.data.repository.query.Param("productId") String productId); + + @Query("SELECT p FROM Product p LEFT JOIN FETCH p.stalls WHERE p.id = :id") + java.util.Optional findByIdWithStalls(@org.springframework.data.repository.query.Param("id") Long id); boolean existsByNameAndCategory(String name, String category); @org.springframework.data.jpa.repository.Modifying diff --git a/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java b/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java index 6730e26a..97d347fd 100644 --- a/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java +++ b/backend/src/main/java/com/rit/canteen/sales/service/DashboardService.java @@ -8,6 +8,7 @@ import com.rit.canteen.sales.repository.OrderRepository; import com.rit.canteen.sales.repository.PurchaseOrderRepository; import com.rit.canteen.sales.repository.VendorRepository; import com.rit.canteen.sales.repository.TokenTransactionRepository; +import com.rit.canteen.sales.repository.UserRepository; import com.rit.canteen.sales.model.ProcurementDashboardData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -34,6 +35,9 @@ public class DashboardService { @Autowired private TokenTransactionRepository tokenTransactionRepository; + @Autowired + private UserRepository userRepository; + public GeneralDashboardData getGeneralDashboardData(LocalDateTime from, LocalDateTime to) { java.time.ZoneId zone = java.time.ZoneId.of("Asia/Kolkata"); if (from == null) from = java.time.LocalDate.now(zone).atStartOfDay(); @@ -168,6 +172,8 @@ public class DashboardService { BigDecimal periodExpensesRaw = purchaseOrderRepository.getTotalPurchaseAmountInRange(from, to); long periodExpenses = periodExpensesRaw != null ? periodExpensesRaw.longValue() : 0; + long suspendedUserCount = userRepository.countByIsSuspended(true); + double growth = 0; if (yesterdayRevenue != null && yesterdayRevenue.compareTo(BigDecimal.ZERO) > 0) { BigDecimal today = todayRevenue != null ? todayRevenue : BigDecimal.ZERO; @@ -179,7 +185,7 @@ public class DashboardService { growth = 100.0; } - return new DashboardStats(totalSales, periodRevenue, activeOrders, dailyCustomers, growth, totalExpenses, periodExpenses); + return new DashboardStats(totalSales, periodRevenue, activeOrders, dailyCustomers, growth, totalExpenses, periodExpenses, suspendedUserCount); } public ProcurementDashboardData getProcurementDashboardData() { diff --git a/frontend/src/pages/Customers.tsx b/frontend/src/pages/Customers.tsx index 594d4c47..57a7c0af 100644 --- a/frontend/src/pages/Customers.tsx +++ b/frontend/src/pages/Customers.tsx @@ -305,7 +305,7 @@ const Customers: React.FC = () => { - ₹{user.ritzTokenBalance?.toLocaleString() || '0'} + R{user.ritzTokenBalance?.toLocaleString() || '0'} @@ -400,7 +400,7 @@ const Customers: React.FC = () => { )} - ₹{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS + R{user.ritzTokenBalance?.toLocaleString() || '0'} TOKENS diff --git a/frontend/src/pages/Ritz.tsx b/frontend/src/pages/Ritz.tsx index d55ebc39..08006998 100644 --- a/frontend/src/pages/Ritz.tsx +++ b/frontend/src/pages/Ritz.tsx @@ -195,7 +195,7 @@ const RitzPage: React.FC = () => { - {t.type === 'TOPUP' ? '+' : '-'} {t.amount.toLocaleString()} + {t.type === 'TOPUP' ? '+' : '-'} R{t.amount.toLocaleString()} diff --git a/ordering_site/src/App.tsx b/ordering_site/src/App.tsx index 8c308985..2430c5d6 100644 --- a/ordering_site/src/App.tsx +++ b/ordering_site/src/App.tsx @@ -2,6 +2,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { CartProvider } from './contexts/CartContext'; import { AuthProvider } from './contexts/AuthContext'; import { FoodProvider } from './contexts/FoodContext'; +import { ThemeProvider } from './contexts/ThemeContext'; import ProtectedRoute from './components/ProtectedRoute'; import HomeScreen from './pages/HomeScreen'; import CategoryScreen from './pages/CategoryScreen'; @@ -16,14 +17,16 @@ import ChangePinScreen from './pages/ChangePinScreen'; import StallDetailScreen from './pages/StallDetailScreen'; import WalletScreen from './pages/WalletScreen'; import TopUpScreen from './pages/TopUpScreen'; +import PreferencesScreen from './pages/PreferencesScreen'; import StockAlert from './components/StockAlert'; import './App.css'; function App() { return ( - - - + + + + @@ -90,11 +93,17 @@ function App() { } /> + + + + } /> + ); } diff --git a/ordering_site/src/components/BottomNav.css b/ordering_site/src/components/BottomNav.css index 99e96a15..6e785639 100644 --- a/ordering_site/src/components/BottomNav.css +++ b/ordering_site/src/components/BottomNav.css @@ -6,9 +6,9 @@ width: 100%; max-width: 600px; height: 65px; - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + background: var(--surface); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); display: flex; justify-content: space-around; align-items: center; diff --git a/ordering_site/src/components/CartTab.css b/ordering_site/src/components/CartTab.css index e0bc195f..5ac93542 100644 --- a/ordering_site/src/components/CartTab.css +++ b/ordering_site/src/components/CartTab.css @@ -30,12 +30,12 @@ align-items: center; justify-content: space-between; padding: 12px 16px; - background: rgba(255, 255, 255, 0.95); + background: var(--surface); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border-radius: 16px; border: 1px solid var(--border); - box-shadow: 0 10px 30px rgba(35, 22, 81, 0.12); + box-shadow: var(--shadow); color: var(--text-dark); } @@ -62,7 +62,7 @@ flex-shrink: 0; } -.cart-count { +.cart-tab-count { font-size: 0.75rem; font-weight: 700; color: var(--text-mid); diff --git a/ordering_site/src/components/CartTab.tsx b/ordering_site/src/components/CartTab.tsx index cc41c24f..e6d2c771 100644 --- a/ordering_site/src/components/CartTab.tsx +++ b/ordering_site/src/components/CartTab.tsx @@ -19,7 +19,7 @@ const CartTab: React.FC = () => {
navigate('/cart')}>
- + {totalItems} {totalItems === 1 ? 'item' : 'items'}
diff --git a/ordering_site/src/components/Header.css b/ordering_site/src/components/Header.css index 5cd09b51..9ee82da3 100644 --- a/ordering_site/src/components/Header.css +++ b/ordering_site/src/components/Header.css @@ -123,7 +123,7 @@ display: flex; align-items: center; gap: 10px; - background-color: rgba(35, 22, 81, 0.05); /* Light Purple Wash */ + background-color: var(--primary-light); padding: 10px 14px; border-radius: 12px; border: 1px solid var(--border); @@ -144,7 +144,7 @@ .cart-count { background-color: white; - color: #000000; /* Black text as requested earlier */ + color: #000000; border-radius: 50%; width: 18px; height: 18px; @@ -173,3 +173,34 @@ text-overflow: ellipsis; } } + +/* Dark Mode Overrides for Light Header */ +[data-theme='dark'] .app-header { + background-color: #ffffff; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +[data-theme='dark'] .app-header .header-title, +[data-theme='dark'] .app-header .location-label { + color: #231651; +} + +[data-theme='dark'] .app-header .location-sub { + color: #4a3e7a; +} + +[data-theme='dark'] .app-header .back-button, +[data-theme='dark'] .app-header .icon-button:not(.profile) { + background-color: #f0f1ff; + color: #231651; +} + +[data-theme='dark'] .app-header .search-bar { + background-color: #f0f1ff; + border-color: rgba(35, 22, 81, 0.1); +} + +[data-theme='dark'] .app-header .cart-count { + background-color: #231651; + color: white; +} diff --git a/ordering_site/src/components/ItemCard.css b/ordering_site/src/components/ItemCard.css index 0091c8bb..e0284fab 100644 --- a/ordering_site/src/components/ItemCard.css +++ b/ordering_site/src/components/ItemCard.css @@ -151,7 +151,7 @@ display: flex; align-items: center; justify-content: center; - background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%); + background: var(--primary-light); border: 1px solid var(--border); } @@ -165,9 +165,9 @@ bottom: -12px; left: 50%; transform: translateX(-50%); - background-color: white; + background-color: var(--surface); border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.12); + box-shadow: var(--shadow); border: 1px solid var(--border); min-width: 80px; display: flex; diff --git a/ordering_site/src/index.css b/ordering_site/src/index.css index 8bf16358..862d0bf5 100644 --- a/ordering_site/src/index.css +++ b/ordering_site/src/index.css @@ -1,9 +1,11 @@ :root { + /* LIGHT THEME (Default) */ --primary: #231651; + --primary-rgb: 35, 22, 81; --primary-light: #f0f1ff; - --bg: #ffffff; + --bg: #f8f9fe; --surface: #ffffff; - --border: rgba(35, 22, 81, 0.1); + --border: rgba(35, 22, 81, 0.08); --green: #00B894; --green-light: #E6FFFA; --red: #ff4d4f; @@ -12,6 +14,28 @@ --text-light: #7b71af; --star: #f4c640; --shadow: 0 4px 20px rgba(35, 22, 81, 0.05); + --input-bg: #f8f9ff; + --modal-overlay: rgba(35, 22, 81, 0.3); +} + +[data-theme='dark'] { + /* DARK THEME - Visual Poetry Deep Slate & Indigo */ + --primary: #9d87ff; /* Softer, glowing purple for dark mode */ + --primary-rgb: 157, 135, 255; + --primary-light: #251b4d; + --bg: #0b0e14; + --surface: #151921; + --border: rgba(157, 135, 255, 0.12); + --green: #00d2aa; + --green-light: #062d26; + --red: #ff6b6b; + --text-dark: #f0f2f5; + --text-mid: #a0aec0; + --text-light: #718096; + --star: #ffd700; + --shadow: 0 4px 30px rgba(0, 0, 0, 0.4); + --input-bg: #1c212b; + --modal-overlay: rgba(0, 0, 0, 0.7); } * { diff --git a/ordering_site/src/pages/CheckoutScreen.css b/ordering_site/src/pages/CheckoutScreen.css index de0498b0..ec4edc13 100644 --- a/ordering_site/src/pages/CheckoutScreen.css +++ b/ordering_site/src/pages/CheckoutScreen.css @@ -39,8 +39,8 @@ } .ritz-payment-card.insufficient-state { - border: 1px solid rgba(244, 63, 94, 0.3); - background: rgba(244, 63, 94, 0.03); + border: 1px solid rgba(244, 63, 94, 0.4); + background: var(--surface); position: relative; overflow: hidden; } @@ -106,16 +106,16 @@ } .insufficient-val { - color: #f43f5e !important; + color: var(--red) !important; text-decoration: line-through; - opacity: 0.7; + opacity: 0.8; } .add-tokens-checkout-btn { width: 100%; margin-top: 12px; - background: white; - border: 1.5px dashed rgba(99, 102, 241, 0.4); + background: var(--surface); + border: 1.5px dashed var(--primary); padding: 1rem; border-radius: 18px; display: flex; @@ -158,13 +158,13 @@ .insufficient-warning-premium { margin: 1.5rem; padding: 1.25rem; - background: white; - border: 1px solid rgba(244, 63, 94, 0.1); + background: var(--surface); + border: 1px solid var(--border); border-radius: 24px; display: flex; align-items: center; gap: 1.25rem; - box-shadow: 0 12px 30px -10px rgba(244, 63, 94, 0.15); + box-shadow: var(--shadow); } .token-warning-visual { @@ -201,7 +201,7 @@ .shortfall-amount { font-size: 1rem; font-weight: 800; - color: #334155; + color: var(--text-dark); margin-bottom: 0.25rem; } @@ -248,7 +248,7 @@ } .address-card.selected .address-icon, .payment-card.selected .payment-icon { - background-color: white; + background-color: var(--primary-light); color: var(--primary); } @@ -366,7 +366,7 @@ max-width: 600px; background-color: var(--surface); padding: 16px; - box-shadow: 0 -4px 20px rgba(0,0,0,0.05); + box-shadow: var(--shadow); border-top: 1px solid var(--border); z-index: 101; } diff --git a/ordering_site/src/pages/HomeScreen.css b/ordering_site/src/pages/HomeScreen.css index 1d7d88d9..311a3a6e 100644 --- a/ordering_site/src/pages/HomeScreen.css +++ b/ordering_site/src/pages/HomeScreen.css @@ -25,7 +25,7 @@ display: flex; align-items: center; gap: 10px; - background-color: #f8f9ff; + background-color: var(--input-bg); padding: 10px 14px; border-radius: 12px; border: 1px solid var(--border); @@ -147,9 +147,9 @@ height: 200px; border-radius: 24px; overflow: hidden; - background-color: #ffffff; - border: 1px solid rgba(0, 0, 0, 0.04); - box-shadow: 0 10px 25px rgba(35, 22, 81, 0.08); + background-color: var(--surface); + border: 1px solid var(--border); + box-shadow: var(--shadow); cursor: pointer; transition: all 0.4s cubic-bezier(0.165, 0.84, 0.44, 1); position: relative; @@ -280,7 +280,7 @@ bottom: 80px; /* Above bottom nav */ left: 16px; right: 16px; - background: white; + background: var(--surface); border-radius: 20px; padding: 12px 16px; display: flex; diff --git a/ordering_site/src/pages/ItemDetailScreen.css b/ordering_site/src/pages/ItemDetailScreen.css index e3e4a971..1d8a5643 100644 --- a/ordering_site/src/pages/ItemDetailScreen.css +++ b/ordering_site/src/pages/ItemDetailScreen.css @@ -58,10 +58,74 @@ } .placeholder-emoji { - font-size: 4rem; - filter: drop-shadow(0 4px 10px rgba(0,0,0,0.1)); - animation: float 3s ease-in-out infinite; - z-index: 1; + font-size: 3rem; + opacity: 0.6; +} + +/* Loading & Not Found States */ +.item-loading-wrapper, .item-not-found-wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + background: var(--bg); +} + +.loading-spinner-wrapper { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + color: var(--text-mid); + font-weight: 600; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid var(--border); + border-top-color: var(--primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.not-found-content { + padding: 40px 24px; + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.not-found-icon { + color: var(--text-light); + margin-bottom: 8px; +} + +.not-found-content h2 { + font-size: 1.5rem; + font-weight: 800; + color: var(--text-dark); +} + +.not-found-content p { + color: var(--text-mid); + max-width: 250px; +} + +.back-home-btn { + margin-top: 12px; + padding: 12px 24px; + background: var(--primary); + color: white; + border-radius: 12px; + font-weight: 700; + border: none; } @keyframes float { diff --git a/ordering_site/src/pages/ItemDetailScreen.tsx b/ordering_site/src/pages/ItemDetailScreen.tsx index 7e4d5272..49c7668f 100644 --- a/ordering_site/src/pages/ItemDetailScreen.tsx +++ b/ordering_site/src/pages/ItemDetailScreen.tsx @@ -10,18 +10,79 @@ import './ItemDetailScreen.css'; const ItemDetailScreen: React.FC = () => { const { itemId } = useParams<{ itemId: string }>(); const { addToCart, updateQuantity, getItemQuantity } = useCart(); - const { foodItems, isLoading } = useFood(); + const { foodItems, isLoading: isGlobalLoading } = useFood(); + const [fetchedItem, setFetchedItem] = React.useState(null); + const [isFetching, setIsFetching] = React.useState(false); - const item = foodItems.find((i) => i.id === itemId); + const contextItem = foodItems.find((i) => i.id === itemId); + const item = contextItem || fetchedItem; + + React.useEffect(() => { + if (!contextItem && itemId) { + const fetchItem = async () => { + setIsFetching(true); + try { + const response = await fetch(`http://${window.location.hostname}:8080/api/products/${itemId}`); + if (response.ok) { + const data = await response.json(); + + // Map backend product to FoodItem type + const rawImg = data.imageData?.trim(); + const finalImage = rawImg ? (rawImg.startsWith('data:') ? rawImg : `data:image/png;base64,${rawImg}`) : ''; + + const stallInfo = data.stalls && data.stalls.length > 0 ? data.stalls[0] : null; + + const mappedItem: FoodItem = { + id: data.id.toString(), + name: data.name, + description: data.description || 'Quality food prepared with care', + price: data.price || data.basePrice || 0, + category: data.category, + image: finalImage, + isVeg: data.veg, + isPopular: data.active, + stock: data.stock, + stallId: stallInfo?.id?.toString(), + stallName: stallInfo?.name + }; + setFetchedItem(mappedItem); + } + } catch (err) { + console.error('Error fetching individual product:', err); + } finally { + setIsFetching(false); + } + }; + fetchItem(); + } + }, [itemId, contextItem]); const quantity = item ? getItemQuantity(item.id) : 0; const isLimitReached = item && item.stock !== undefined && quantity >= item.stock && item.stock > 0; - if (isLoading && foodItems.length === 0) { - return
Loading...
; + if ((isGlobalLoading && foodItems.length === 0) || isFetching) { + return ( +
+
+
+

Loading Delights...

+
+
+ ); } - if (!item && !isLoading) return
Item not found
; - if (!item) return null; + if (!item && !isGlobalLoading && !isFetching) { + return ( +
+
+
+ +

Item Not Found

+

Oops! The item you're looking for seems to have vanished.

+ +
+
+ ); + } return (
diff --git a/ordering_site/src/pages/MyOrdersScreen.css b/ordering_site/src/pages/MyOrdersScreen.css index acfb5fa7..9321332c 100644 --- a/ordering_site/src/pages/MyOrdersScreen.css +++ b/ordering_site/src/pages/MyOrdersScreen.css @@ -26,10 +26,10 @@ } .latest-order-card { - background: white; + background: var(--surface); border-radius: 24px; padding: 20px; - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05); + box-shadow: var(--shadow); border: 1px solid var(--border); display: flex; flex-direction: column; @@ -129,7 +129,7 @@ } .order-item-list { - background: white; + background: var(--surface); padding: 14px; border-radius: 16px; display: flex; @@ -203,7 +203,7 @@ .order-detail-modal { width: 100%; - background: white; + background: var(--surface); border-radius: 24px 24px 0 0; padding: 24px; display: flex; @@ -263,7 +263,7 @@ display: flex; align-items: center; gap: 8px; - background: white; + background: var(--surface); border: 1px solid var(--border); padding: 8px 16px; border-radius: 20px; @@ -401,7 +401,7 @@ } .stock-alert-modal { - background: white; + background: var(--surface); width: 100%; max-width: 400px; border-radius: 28px; @@ -463,10 +463,10 @@ font-size: 13px; font-weight: 700; color: #e11d48; - background: white; + background: var(--bg); padding: 8px 12px; border-radius: 10px; - border: 1px solid #fecdd3; + border: 1px solid var(--border); } .alert-actions { diff --git a/ordering_site/src/pages/ProfileScreen.tsx b/ordering_site/src/pages/ProfileScreen.tsx index ae04bb35..a0d343da 100644 --- a/ordering_site/src/pages/ProfileScreen.tsx +++ b/ordering_site/src/pages/ProfileScreen.tsx @@ -79,7 +79,7 @@ const ProfileScreen: React.FC = () => { { icon: ShoppingBag, label: 'My Orders', sub: 'View order history', path: '/orders' }, { icon: Wallet, label: 'My Wallet', sub: 'Ritz Tokens & History', path: '/wallet' }, { icon: ShieldCheck, label: 'Account Security', sub: 'Change Security PIN', path: '/change-pin' }, - { icon: Settings, label: 'Preferences', sub: 'Notifications, Language', path: '#' }, + { icon: Settings, label: 'Preferences', sub: 'Dark Mode, Language', path: '/preferences' }, { icon: HelpCircle, label: 'Help & Support', sub: 'FAQs, Contact Us', path: '#' } ]; diff --git a/ordering_site/src/pages/StallDetailScreen.css b/ordering_site/src/pages/StallDetailScreen.css index 60cc208a..6e8aab8e 100644 --- a/ordering_site/src/pages/StallDetailScreen.css +++ b/ordering_site/src/pages/StallDetailScreen.css @@ -1,5 +1,5 @@ .stall-detail-page { - background-color: #f7f8fc; + background-color: var(--bg); min-height: 100vh; } @@ -12,9 +12,25 @@ .detail-header.sticky { height: auto; padding: 12px 16px; - background: white; + background: var(--surface); z-index: 100; - box-shadow: 0 2px 10px rgba(0,0,0,0.05); + box-shadow: var(--shadow); +} + +.stall-detail-page .add-to-cart-container { + position: absolute; + bottom: -12px; + left: 50%; + transform: translateX(-50%); + background-color: var(--surface); + border-radius: 8px; + box-shadow: var(--shadow); + border: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + color: var(--primary); + box-shadow: var(--shadow); } .stall-detail-page .back-button { @@ -22,7 +38,7 @@ top: 16px; left: 16px; z-index: 10; - background: white; + background: var(--surface); width: 40px; height: 40px; border-radius: 50%; @@ -30,7 +46,7 @@ align-items: center; justify-content: center; color: var(--primary); - box-shadow: 0 4px 12px rgba(0,0,0,0.15); + box-shadow: var(--shadow); } .header-image-container { @@ -62,11 +78,11 @@ .stall-main-card { margin: -40px 16px 20px; - background: white; + background: var(--surface); border-radius: 24px; padding: 24px; position: relative; - box-shadow: 0 8px 30px rgba(35, 22, 81, 0.08); + box-shadow: var(--shadow); z-index: 5; } @@ -159,7 +175,7 @@ padding: 40px 24px; text-align: center; color: var(--text-light); - background: white; + background: var(--surface); margin: 0 16px; border-radius: 16px; border: 1px dashed var(--border);