Implemented main home design and cart viewer

This commit is contained in:
Khwezi Mngoma
2026-05-23 15:37:44 +02:00
parent d8cb68e8bf
commit 5e839dca03
8 changed files with 1000 additions and 139 deletions
+3 -2
View File
@@ -11,13 +11,14 @@
<link rel="stylesheet" href="@Assets["MidrandBookshop.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes />
<Routes @rendermode="InteractiveServer" />
<ReconnectModal />
<script src="@Assets["_framework/blazor.web.js"]"></script>
<script src="https://unpkg.com/lucide@latest"></script>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
<div class="col-12 col-md-6 col-lg-4 mb-4">
<div class="card border-0 h-100 p-4 position-relative" style="background-color: #F1F1F1; border-radius: var(--mb-radius);">
<div class="d-flex justify-content-between align-items-center mb-4">
<span class="badge rounded-pill px-3 py-2" style="background-color: var(--mb-accent-red); font-weight: 500;">New</span>
<button class="btn bg-white rounded-circle d-flex align-items-center justify-content-center p-2 shadow-sm border-0" style="width: 32px; height: 32px;">
<i data-lucide="arrow-up-right" style="width: 16px; height: 16px; color: var(--mb-text-dark);"></i>
</button>
</div>
<div class="d-flex justify-content-center align-items-center flex-grow-1 my-3" style="min-height: 260px;">
<img src="@BookImageUrl" class="img-fluid book-shadow" style="max-height: 240px; object-fit: contain;" alt="@Title" />
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3 px-2">
<span class="fw-medium text-dark" style="font-size: 0.9rem;">@Title</span>
<span class="text-muted fw-semibold" style="font-size: 0.9rem;">$@Price</span>
</div>
</div>
@code {
[Parameter] public string Title { get; set; } = "";
[Parameter] public decimal Price { get; set; }
[Parameter] public string BookImageUrl { get; set; } = "";
}
@@ -0,0 +1,9 @@
.book-shadow {
filter: drop-shadow(5px 10px 15px rgba(0, 0, 0, 0.15)) drop-shadow(1px 2px 4px rgba(0, 0, 0, 0.1));
}
.sm-icon {
width: 14px;
height: 14px;
vertical-align: middle;
}
@@ -1,13 +1,318 @@
@inherits LayoutComponentBase
@inject NavigationManager Navigation
<div class="page">
<main>
<div class="top-row px-4">
<h2>Midrand Books</h2>
<div class="position-relative min-vh-100 d-flex flex-column justify-content-between overflow-hidden" style="background-color: #F9F9F9;">
<div class="cart-overlay @(IsCartOpen ? "is-visible" : "")" @onclick="ToggleCart"></div>
<div class="cart-drawer @(IsCartOpen ? "is-open" : "") d-flex flex-column bg-white shadow-lg">
<div class="cart-header d-flex align-items-center justify-content-between p-4 border-bottom">
<h5 class="fw-bold m-0 text-dark tracking-tight" style="font-family: 'Inter', sans-serif; font-size: 1.1rem;">
YOUR CART (@CartItems.Sum(i => i.Quantity))
</h5>
<button class="btn btn-sm text-dark p-1 border-0" @onclick="ToggleCart" type="button">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<article class="content px-4">
<div class="cart-body flex-grow-1 overflow-y-auto p-4">
@if (!CartItems.Any())
{
<div class="h-100 d-flex flex-column align-items-center justify-content-center text-muted py-5">
<svg class="mb-3 opacity-50" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z M3 6h18 M16 10a4 4 0 0 1-8 0" />
</svg>
<span class="small tracking-wide">Your collection is empty.</span>
</div>
}
else
{
<div class="d-flex flex-column gap-4">
@foreach (var item in CartItems)
{
<div class="cart-item d-flex gap-3 align-items-start pb-3 border-bottom-dashed">
<div class="cart-item-thumb bg-dark text-white-50 d-flex align-items-center justify-content-center px-2 text-center" style="width: 54px; height: 74px; font-size: 0.45rem; letter-spacing: 0.5px;">
[ COVER ]
</div>
<div class="flex-grow-1">
<h6 class="text-dark small fw-bold mb-0 text-truncate" style="max-width: 180px;">@item.Title</h6>
<p class="text-muted xx-small mb-2">by @item.Author</p>
<div class="d-flex align-items-center justify-content-between">
<div class="quantity-picker d-flex align-items-center border rounded-pill bg-light">
<button class="btn btn-sm py-0 px-2 text-dark border-0" @onclick="() => ChangeQuantity(item, -1)" type="button">-</button>
<span class="px-1 text-dark fw-medium" style="font-size: 0.75rem;">@item.Quantity</span>
<button class="btn btn-sm py-0 px-2 text-dark border-0" @onclick="() => ChangeQuantity(item, 1)" type="button">+</button>
</div>
<span class="small fw-semibold text-dark">R @(item.Price * item.Quantity)</span>
</div>
</div>
<button class="btn text-muted p-0 border-0 mt-1 align-self-start" style="background: none;" @onclick="() => RemoveFromCart(item)" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
</button>
</div>
}
</div>
}
</div>
@if (CartItems.Any())
{
<div class="cart-footer p-4 bg-light border-top mt-auto">
<div class="d-flex align-items-center justify-content-between mb-4">
<span class="text-secondary small fw-medium tracking-wider">TOTAL DUE</span>
<span class="text-dark h5 fw-bold mb-0">R @GetCartTotal()</span>
</div>
<div class="d-flex flex-column gap-2">
<button class="btn btn-dark w-100 rounded-pill py-2.5 fw-medium shadow-sm small tracking-wide" @onclick="RedirectToCheckout" type="button">
Proceed to Checkout
</button>
<button class="btn btn-outline-dark w-100 rounded-pill py-2.5 fw-medium bg-white small tracking-wide" @onclick="RedirectToCart" type="button">
Go to Cart Page
</button>
</div>
</div>
}
</div>
<div class="w-100 flex-grow-1 position-relative">
<div class="position-absolute top-0 start-0 overflow-hidden d-none d-md-block" style="z-index: 0; pointer-events: none; opacity: 0.08; transform: translate(-10%, -10%);">
<svg width="480" height="480" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="98" stroke="#1A1A1A" stroke-width="0.25" stroke-dasharray="0.5 1.5" />
<circle cx="100" cy="100" r="92" stroke="#1A1A1A" stroke-width="0.4" />
<circle cx="100" cy="100" r="86" stroke="#1A1A1A" stroke-width="0.2" stroke-dasharray="2 1" />
<circle cx="100" cy="100" r="80" stroke="#1A1A1A" stroke-width="0.3" />
<path d="M100 20 A80 80 0 0 1 180 100 A80 80 0 0 1 100 180 A80 80 0 0 1 20 100 A80 80 0 0 1 100 20" stroke="#1A1A1A" stroke-width="0.15" />
<path d="M100 20 A80 80 0 0 0 180 100 A80 80 0 0 0 100 180 A80 80 0 0 0 20 100 A80 80 0 0 0 100 20" stroke="#1A1A1A" stroke-width="0.15" />
<line x1="40" y1="100" x2="160" y2="100" stroke="#1A1A1A" stroke-width="0.2" stroke-dasharray="1 4" />
<line x1="100" y1="40" x2="100" y2="160" stroke="#1A1A1A" stroke-width="0.2" stroke-dasharray="1 4" />
<g transform="translate(65, 82)" stroke="#1A1A1A" stroke-width="0.6" stroke-linecap="round" stroke-linejoin="round" fill="none">
<path d="M16 18 C 16 10, 24 10, 24 18" stroke-width="0.5" />
<line x1="20" y1="10" x2="20" y2="4" />
<path d="M28 18 C 28 14, 34 14, 34 18" stroke-width="0.5" />
<line x1="31" y1="14" x2="31" y2="7" />
<rect x="3" y="8" width="5" height="10" stroke-width="0.5" />
<rect x="11" y="5" width="5" height="13" stroke-width="0.5" />
<rect x="38" y="3" width="7" height="15" stroke-width="0.5" />
<polygon points="38,3 41.5,0 45,3" stroke-width="0.4" />
<rect x="49" y="9" width="6" height="9" stroke-width="0.5" />
<line x1="0" y1="18" x2="70" y2="18" stroke-width="0.75" />
</g>
</svg>
</div>
<nav class="navbar navbar-expand-lg py-4 bg-transparent position-relative" style="z-index: 100; pointer-events: auto;">
<div class="container-fluid px-md-5">
<a class="navbar-brand d-flex align-items-center" href="/" style="transform: scale(1.6); transform-origin: left center; margin-right: 4rem;">
<svg class="me-2" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#1A1A1A" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 17c-2.5 0-4.5-1.8-4.5-4s2-4 4.5-4 4.5 1.8 4.5 4-2 4-4.5 4Z" />
<path d="M12 9c2.2 0 4-1.6 4-3.5S14.2 2 12 2 8 3.6 8 5.5 9.8 9 12 9Z" />
<path d="M2 12h20" stroke-width="1" stroke-dasharray="1 2" opacity="0.5" />
</svg>
<span class="fw-bold tracking-tight text-dark" style="font-size: 0.65rem; letter-spacing: 0.8px; font-family: 'Inter', sans-serif;">MIDRAND BOOKS</span>
</a>
<div class="collapse navbar-collapse justify-content-center" id="navbarNav">
<div class="nav-pill-wrapper d-flex align-items-center">
<a class="nav-pill-link active" href="/">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" /><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" /></svg>
Books
</a>
<a class="nav-pill-link" href="/audiobooks">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6" /><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z" /></svg>
Audiobooks
</a>
<a class="nav-pill-link" href="/ebooks">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2" /><line x1="12" y1="18" x2="12.01" y2="18" /></svg>
E-Books
</a>
</div>
</div>
<div class="d-flex align-items-center gap-2 position-relative">
<div class="search-input-container d-flex align-items-center @(IsSearchActive ? "is-active" : "")">
<input type="text"
class="form-control form-control-sm rounded-pill border-light-subtle px-3 text-dark bg-light custom-search-field"
placeholder="Search by ISBN, Author, Title..."
value="@GlobalSearchQuery"
@oninput="OnSearchInput" />
</div>
<button class="btn btn-sm rounded-circle d-flex align-items-center justify-content-center border-0 p-0 transition-smooth @(IsSearchActive ? "bg-dark text-white" : "bg-light text-dark")"
style="width: 32px; height: 32px;"
@onclick="ToggleGlobalSearch"
type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</button>
<button class="btn d-flex align-items-center justify-content-center p-2 position-relative border-0"
style="background: transparent; color: #1A1A1A; width: 36px; height: 36px;"
@onclick="ToggleCart"
type="button">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75">
<path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z M3 6h18 M16 10a4 4 0 0 1-8 0" />
</svg>
@if (CartItems.Any())
{
<span class="cart-badge">@CartItems.Sum(i => i.Quantity)</span>
}
</button>
<a href="/login" class="btn btn-dark rounded-pill px-3 py-1 d-inline-flex align-items-center gap-2 btn-sm fw-medium shadow-sm">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
Log In
</a>
</div>
</div>
</nav>
<main class="position-relative" style="z-index: 5;">
<CascadingValue Value="GlobalSearchQuery">
@Body
</article>
</CascadingValue>
</main>
</div>
<footer class="w-100 position-relative overflow-hidden"
style="z-index: 10; padding: 5.5rem 0 3.5rem 0; background-color: #F2F4F7 !important; background-image: linear-gradient(90deg, #FFF 0%, rgba(243,245,249,0.98) 22%, #FFF 48%, rgba(239,242,247,0.98) 73%, #FFF 100%), repeating-linear-gradient(15deg, rgba(0,0,0,0.005) 0px, rgba(0,0,0,0.005) 1px, transparent 1px, transparent 41px), repeating-linear-gradient(-25deg, rgba(255,255,255,0.6) 0px, rgba(255,255,255,0.6) 2px, transparent 2px, transparent 73px), repeating-linear-gradient(65deg, rgba(0,0,0,0.003) 0px, rgba(0,0,0,0.003) 1px, transparent 1px, transparent 127px) !important;">
<div class="position-absolute top-0 start-0 w-100 h-100" style="background: radial-gradient(circle at 50% 30%, rgba(255,255,255,0.45) 0%, rgba(242,244,248,0.05) 100%); pointer-events: none; z-index: 1; backdrop-filter: blur(1.5px); -webkit-backdrop-filter: blur(1.5px);"></div>
<div class="position-absolute top-0 start-0 w-100 h-100" style="z-index: 2; opacity: 0.95; pointer-events: none; mix-blend-mode: multiply;">
<svg width="100%" height="100%" viewBox="0 0 1440 400" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="15%" cy="35%" rx="750" ry="400" fill="url(#randomMetal_1)" />
<ellipse cx="60%" cy="80%" rx="950" ry="350" fill="url(#randomMetal_2)" />
<ellipse cx="85%" cy="20%" rx="600" ry="250" fill="url(#randomMetal_3)" />
<defs>
<radialGradient id="randomMetal_1" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(216 140) rotate(15) scale(350 750)">
<stop offset="0%" stop-color="#FFFFFF" />
<stop offset="60%" stop-color="#EBF0F7" />
<stop offset="100%" stop-color="#FFFFFF" stop-opacity="0" />
</radialGradient>
<radialGradient id="randomMetal_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(864 320) rotate(-15) scale(300 950)">
<stop offset="0%" stop-color="#FFFFFF" />
<stop offset="70%" stop-color="#E2E7F2" />
<stop offset="100%" stop-color="#FFFFFF" stop-opacity="0" />
</radialGradient>
<radialGradient id="randomMetal_3" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1224 80) rotate(45) scale(200 600)">
<stop offset="0%" stop-color="#FFFFFF" stop-opacity="0.8" />
<stop offset="50%" stop-color="#E9EDF5" stop-opacity="0.3" />
<stop offset="100%" stop-color="#FFFFFF" stop-opacity="0" />
</radialGradient>
</defs>
</svg>
</div>
<div class="position-absolute top-0 start-0 w-100" style="height: 2px; z-index: 5;">
<div class="w-100" style="height: 1px; background: rgba(0, 0, 0, 0.055);"></div>
<div class="w-100" style="height: 1px; background: rgba(255, 255, 255, 0.95);"></div>
</div>
<div class="container-fluid px-md-5 position-relative" style="z-index: 4;">
<div class="row g-4 pb-5 justify-content-between" style="border-bottom: 1px solid rgba(0, 0, 0, 0.05);">
<div class="col-12 col-md-5">
<span class="fw-bold tracking-tight text-dark d-block mb-3" style="font-size: 0.75rem; letter-spacing: 1px; font-family: 'Inter', sans-serif;">MIDRAND BOOKS</span>
<p class="text-muted small mb-4" style="line-height: 1.6; max-width: 360px;">An architectural destination for curated print, thoughtful reading culture, and global design perspectives.</p>
<div class="d-flex flex-column gap-2.5">
<a href="mailto:info@midrandshop.co.za" class="text-decoration-none text-muted d-inline-flex align-items-center gap-2 small">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /></svg>
info@midrandshop.co.za
</a>
<a href="tel:+27872650198" class="text-decoration-none text-muted d-inline-flex align-items-center gap-2 small">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 .2.81.7A2 2 0 0 1 22 16.92z" /></svg>
+27 87 265 0198
</a>
</div>
</div>
<div class="col-12 col-md-6 col-lg-5 text-start">
<div class="row row-cols-2 justify-content-md-end g-3">
<div style="max-width: 160px;">
<span class="text-dark fw-bold tracking-wider d-block mb-3" style="font-size: 0.65rem; letter-spacing: 1.2px;">PLATFORM</span>
<ul class="list-unstyled d-flex flex-column gap-2 m-0">
<li><a href="/about" class="text-decoration-none text-muted small">About Us</a></li>
<li><a href="/" class="text-decoration-none text-muted small">Browse Catalog</a></li>
<li><a href="/contact" class="text-decoration-none text-muted small">Contact Us</a></li>
</ul>
</div>
<div style="max-width: 210px;">
<span class="text-dark fw-bold tracking-wider d-block mb-3" style="font-size: 0.65rem; letter-spacing: 1.2px;">ENTERPRISE</span>
<ul class="list-unstyled d-flex flex-column gap-2 m-0">
<li class="small text-muted d-flex align-items-center gap-1.5 mb-1" style="font-weight: 500;">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" /></svg>
Lite Charms (PTY) Ltd
</li>
<li><a href="/terms" class="text-decoration-none text-muted small">Terms & Conditions</a></li>
<li><a href="/privacy" class="text-decoration-none text-muted small">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="row align-items-center pt-4">
<div class="col-12 text-center text-md-start">
<span class="text-muted tracking-wide" style="font-size: 0.75rem; font-weight: 500;">
&copy; @DateTime.Now.Year Midrand Books. All rights reserved.
</span>
</div>
</div>
</div>
</footer>
</div>
@code {
private string GlobalSearchQuery { get; set; } = string.Empty;
private bool IsSearchActive { get; set; } = false;
private bool IsCartOpen { get; set; } = false;
private List<CartItem> CartItems = new()
{
new CartItem { Id = 1, Title = "Letters from M/M (Paris)", Author = "M/M Paris", Price = 720, Quantity = 1 },
new CartItem { Id = 2, Title = "Daan Paans: Floating Signifiers", Author = "Daan Paans", Price = 540, Quantity = 1 },
new CartItem { Id = 3, Title = "Album Architectures, Maputo", Author = "Guedes Archive", Price = 350, Quantity = 1 }
};
private void ToggleGlobalSearch() => IsSearchActive = !IsSearchActive;
private void ToggleCart() => IsCartOpen = !IsCartOpen;
private void OnSearchInput(ChangeEventArgs e)
{
GlobalSearchQuery = e.Value?.ToString() ?? string.Empty;
}
private void ChangeQuantity(CartItem item, int delta)
{
item.Quantity += delta;
if (item.Quantity <= 0)
{
CartItems.Remove(item);
}
}
private void RemoveFromCart(CartItem item) => CartItems.Remove(item);
private int GetCartTotal() => CartItems.Sum(item => item.Price * item.Quantity);
private void RedirectToCart()
{
IsCartOpen = false;
Navigation.NavigateTo("/cart");
}
private void RedirectToCheckout()
{
IsCartOpen = false;
Navigation.NavigateTo("/checkout");
}
public class CartItem
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public int Price { get; set; }
public int Quantity { get; set; }
}
}
@@ -1,98 +1,84 @@
.page {
position: relative;
display: flex;
flex-direction: column;
/* --- Midrand Books Sliding Cart Extensions --- */
/* Dimmed backdrop background blur styling */
.cart-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
z-index: 1040;
opacity: 0;
pointer-events: none;
transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1);
}
main {
flex: 1;
.cart-overlay.is-visible {
opacity: 1;
pointer-events: auto;
}
/* Slide-out Sidebar Panel layout specification */
.cart-drawer {
position: fixed;
top: 0;
right: -420px;
width: 100%;
max-width: 400px;
height: 100vh;
z-index: 1050;
pointer-events: none; /* Block layout actions while hidden off-screen */
transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.cart-drawer.is-open {
transform: translateX(-420px);
pointer-events: auto; /* Allow complete drawer clicks once slid forward */
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
/* FIXED: Prevent badge rendering from stealing button-down mouse highlights */
.cart-badge {
position: absolute;
top: 2px;
right: 2px;
background-color: #1A1A1A;
color: #FFF;
font-size: 0.62rem;
font-weight: 700;
border-radius: 50%;
width: 15px;
height: 15px;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
/* Micro typography utility sizes */
.xx-small {
font-size: 0.68rem;
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
/* Clean dashed divider lines for item items listing styling */
.border-bottom-dashed {
border-bottom: 1px dashed rgba(0, 0, 0, 0.12);
}
#blazor-error-ui {
color-scheme: light only;
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
/* Custom quantity container inline metrics layout structure */
.quantity-picker {
padding: 2px 4px;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
.quantity-picker button {
font-size: 0.85rem;
font-weight: 600;
line-height: 1;
}
.quantity-picker button:hover {
background-color: rgba(0,0,0,0.05);
border-radius: 50%;
}
+321 -3
View File
@@ -1,7 +1,325 @@
@page "/"
@rendermode InteractiveServer
<PageTitle>Midrand Books</PageTitle>
<div class="container text-center text-hero-wrapper">
<h1 class="display-3 text-dark mb-3 px-2 master-headline">
Discover thoughtfully curated<br>books for every reader.
</h1>
<p class="text-muted mx-auto mb-0 sub-headline">
Explorations into books, reading culture, and the art of thoughtful curation from Midrand to the world.
</p>
</div>
<h3>midrandbooks.co.za</h3>
<div class="container mb-5 px-md-5">
<div class="row align-items-center justify-content-between pb-3 g-3">
Welcome to the Midrand Books online bookstore! We are passionate about providing a wide selection of books to readers of all ages and interests. Whether you're looking for the latest bestsellers, classic literature, or educational resources, we have something for everyone.
<div class="col-12 col-md-8">
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-start">
@foreach (var category in MainCategories)
{
var catName = category;
<button class="btn btn-sm rounded-pill px-3 py-1-5 fw-medium transition-smooth
@(ActiveCategory == catName ? "btn-dark" : "btn-outline-secondary text-dark border-light-subtle bg-white")"
@onclick="() => SelectCategory(catName)">
@catName
</button>
}
<button class="btn btn-link text-muted btn-sm text-decoration-none fw-medium transition-smooth d-inline-flex align-items-center gap-1 p-1 ms-1"
@onclick="ToggleExtraCategories">
<span>@(ShowExpandedCategories ? "Show Less" : "See More")</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"
style="transform: @(ShowExpandedCategories ? "rotate(180deg)" : "rotate(0deg)"); transition: transform 0.2s ease;">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
</div>
</div>
<div class="col-12 col-md-4">
<div class="d-flex align-items-center justify-content-start justify-content-md-end gap-2">
<button class="btn btn-sm rounded-pill px-3 py-1-5 fw-medium transition-smooth border border-light-subtle bg-white text-dark d-inline-flex align-items-center gap-2"
style="height: 32px; font-size: 0.8rem;"
@onclick="ToggleFilterMenu">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>
<span>Filter & Sort</span>
</button>
<div class="d-flex align-items-center bg-light p-1 rounded-pill border border-light-subtle" style="height: 32px;">
<button class="btn btn-sm rounded-circle p-0 d-flex align-items-center justify-content-center transition-smooth
@(CurrentViewMode == ViewMode.Grid ? "bg-white text-dark shadow-sm" : "text-muted border-0 bg-transparent")"
style="width: 24px; height: 24px;"
@onclick="() => SetViewMode(ViewMode.Grid)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect width="7" height="7" x="3" y="3" rx="1" /><rect width="7" height="7" x="14" y="3" rx="1" /><rect width="7" height="7" x="14" y="14" rx="1" /><rect width="7" height="7" x="3" y="14" rx="1" /></svg>
</button>
<button class="btn btn-sm rounded-circle p-0 d-flex align-items-center justify-content-center transition-smooth
@(CurrentViewMode == ViewMode.List ? "bg-white text-dark shadow-sm" : "text-muted border-0 bg-transparent")"
style="width: 24px; height: 24px; margin-left: 2px;"
@onclick="() => SetViewMode(ViewMode.List)">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg>
</button>
</div>
</div>
</div>
</div>
@if (ShowExpandedCategories)
{
<div class="d-flex flex-wrap gap-2 align-items-center justify-content-start pt-3 pb-2 border-top border-light-subtle mt-2 animate-fade-in">
@foreach (var category in DynamicExtendedCategories)
{
var catName = category;
<button class="btn btn-sm rounded-pill px-3 py-1-5 fw-medium transition-smooth
@(ActiveCategory == catName ? "btn-dark" : "btn-outline-secondary text-dark border-light-subtle bg-white")"
@onclick="() => SelectCategory(catName)">
@catName
</button>
}
</div>
}
@if (ShowFilterMenu)
{
<div class="p-4 bg-light border border-light-subtle mt-3 animate-fade-in filter-dropdown-panel">
<div class="row g-4">
<div class="col-12 col-sm-4">
<p class="text-dark font-monospace small fw-bold mb-2 opacity-50 panel-section-heading">SORT ORDER</p>
<div class="d-flex flex-column gap-1">
<button type="button" class="btn btn-sm text-start py-1 px-2 rounded @(SelectedSortOption == "default" ? "fw-bold text-dark bg-white shadow-sm" : "text-muted bg-transparent border-0")" @onclick='() => ChangeSort("default")'>Curated Default</button>
<button type="button" class="btn btn-sm text-start py-1 px-2 rounded @(SelectedSortOption == "price-low" ? "fw-bold text-dark bg-white shadow-sm" : "text-muted bg-transparent border-0")" @onclick='() => ChangeSort("price-low")'>Price: Low to High</button>
<button type="button" class="btn btn-sm text-start py-1 px-2 rounded @(SelectedSortOption == "price-high" ? "fw-bold text-dark bg-white shadow-sm" : "text-muted bg-transparent border-0")" @onclick='() => ChangeSort("price-high")'>Price: High to Low</button>
<button type="button" class="btn btn-sm text-start py-1 px-2 rounded @(SelectedSortOption == "title-asc" ? "fw-bold text-dark bg-white shadow-sm" : "text-muted bg-transparent border-0")" @onclick='() => ChangeSort("title-asc")'>Title: A-Z</button>
</div>
</div>
<div class="col-12 col-sm-4">
<p class="text-dark font-monospace small fw-bold mb-2 opacity-50 panel-section-heading">PRICE RANGE</p>
<div class="d-flex flex-column gap-1">
<button type="button" class="btn btn-sm text-start py-1 px-2 rounded @(ActivePriceFilter == "all" ? "fw-bold text-dark bg-white shadow-sm" : "text-muted bg-transparent border-0")" @onclick='() => ChangePriceFilter("all")'>All Prices</button>
<button type="button" class="btn btn-sm text-start py-1 px-2 rounded @(ActivePriceFilter == "under-500" ? "fw-bold text-dark bg-white shadow-sm" : "text-muted bg-transparent border-0")" @onclick='() => ChangePriceFilter("under-500")'>Under R 500</button>
<button type="button" class="btn btn-sm text-start py-1 px-2 rounded @(ActivePriceFilter == "500-1000" ? "fw-bold text-dark bg-white shadow-sm" : "text-muted bg-transparent border-0")" @onclick='() => ChangePriceFilter("500-1000")'>R 500 R 1,000</button>
<button type="button" class="btn btn-sm text-start py-1 px-2 rounded @(ActivePriceFilter == "over-1000" ? "fw-bold text-dark bg-white shadow-sm" : "text-muted bg-transparent border-0")" @onclick='() => ChangePriceFilter("over-1000")'>Over R 1,000</button>
</div>
</div>
<div class="col-12 col-sm-4">
<p class="text-dark font-monospace small fw-bold mb-2 opacity-50 panel-section-heading">RELEASE AVAILABILITY</p>
<div class="form-check form-switch pt-1 ms-1">
<input class="form-check-input style-track-switch" type="checkbox" id="newArrivalsToggle" checked="@OnlyShowNew" @onchange="ToggleNewArrivalsOnly" style="cursor: pointer;">
<label class="form-check-label small text-dark fw-medium ps-1" for="newArrivalsToggle" style="cursor: pointer;">Only New Acquisitions</label>
</div>
<button type="button" class="btn btn-sm btn-link text-danger text-decoration-none mt-3 p-0 font-monospace reset-link-btn" @onclick="ResetFilters">RESET ALL FILTERS</button>
</div>
</div>
</div>
}
<div class="position-relative w-100 custom-milled-line">
<div class="position-absolute start-50 translate-middle-x center-bloom-shadow"></div>
<div class="position-absolute w-100 core-horizontal-rule"></div>
</div>
</div>
<div class="container px-md-5 pb-5">
@if (!PaginatedBooks.Any())
{
<div class="text-center text-muted py-5">
<p class="mb-0 small style-track" style="letter-spacing: 1px;">NO PRODUCTS MATCH YOUR TARGET SELECTION SPECIFICATIONS</p>
</div>
}
else if (CurrentViewMode == ViewMode.Grid)
{
<div class="row g-4 animate-fade-in">
@foreach (var book in PaginatedBooks)
{
<div class="col-12 col-md-6 col-lg-4">
<div class="card border-0 p-4 d-flex flex-column position-relative justify-content-between book-grid-card">
<div class="d-flex justify-content-between align-items-center">
@if (book.IsNew)
{
<span class="badge rounded-pill px-3 py-2 badge-new-arrival">New</span>
}
else
{
<div></div>
}
<button class="btn bg-white rounded-circle d-flex align-items-center justify-content-center p-0 shadow-sm border-0" style="width: 32px; height: 32px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#1A1A1A" stroke-width="2.5"><line x1="7" y1="17" x2="17" y2="7" /><polyline points="7,7 17,7 17,17" /></svg>
</button>
</div>
<div class="d-flex justify-content-center align-items-center my-auto py-3">
<div class="book-spine-fallback bg-dark d-flex align-items-center justify-content-center text-center p-4 text-white-50">
@book.Category.ToUpper()<br><span class="opacity-50" style="font-size:0.55rem;">EDITION</span>
</div>
</div>
</div>
<div class="d-flex justify-content-between align-items-start mt-3 px-2">
<div>
<h3 class="text-dark m-0 lh-sm product-card-title">@book.Title</h3>
<p class="text-muted m-0 mt-1 small" style="font-size: 0.8rem;">by @book.Author</p>
</div>
<span class="text-muted fw-semibold" style="font-size: 0.95rem;">R @book.Price.ToString("N0")</span>
</div>
</div>
}
</div>
}
else
{
<div class="d-flex flex-column border-top border-light-subtle animate-fade-in">
@foreach (var book in PaginatedBooks)
{
<div class="d-flex align-items-center justify-content-between py-3 px-2 list-row-item">
<div class="d-flex align-items-center gap-4 structural-list-left">
<span class="text-dark fw-medium list-item-title">@book.Title</span>
<span class="text-muted small list-item-author">by @book.Author</span>
<span class="badge bg-light text-secondary border rounded-pill px-2.5 py-1 font-monospace list-item-tag">@book.Category.ToUpper()</span>
</div>
<div class="d-flex align-items-center gap-4">
@if (book.IsNew)
{
<span class="badge rounded-pill bg-danger-subtle text-danger px-2.5 py-1 list-new-badge">NEW</span>
}
<span class="text-dark font-monospace fw-medium list-item-price">R @book.Price.ToString("N0")</span>
<button class="btn btn-link text-dark p-1">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="7" y1="17" x2="17" y2="7" /><polyline points="7,7 17,7 17,17" /></svg>
</button>
</div>
</div>
}
</div>
}
@if (HasMoreItems)
{
<div class="d-flex flex-column align-items-center justify-content-center mt-5 pt-4">
<button class="btn btn-outline-dark rounded-pill px-5 py-2.5 fw-medium shadow-sm" style="font-size: 0.85rem;" @onclick="LoadNextPage">
<span>Show More Artifacts</span>
</button>
</div>
}
</div>
@code {
public enum ViewMode { Grid, List }
private ViewMode CurrentViewMode = ViewMode.Grid;
[CascadingParameter]
public string SharedSearchQuery { get; set; } = string.Empty;
private string ActiveCategory = "All";
private bool ShowExpandedCategories = false;
private bool ShowFilterMenu = false;
private string SelectedSortOption = "default";
private string ActivePriceFilter = "all";
private bool OnlyShowNew = false;
private List<string> MainCategories = new() { "All", "Graphic Design", "Product Design", "Architecture" };
private List<string> DynamicExtendedCategories = new();
private int ItemsPerPage = 12;
private int VisibleCount = 12;
public class BookItem
{
public string Title { get; set; } = string.Empty;
public string Author { get; set; } = string.Empty;
public decimal Price { get; set; }
public string Category { get; set; } = string.Empty;
public bool IsNew { get; set; }
public string Isbn { get; set; } = string.Empty;
}
private List<BookItem> BooksCollection = new();
private IEnumerable<BookItem> FilteredData
{
get
{
var data = BooksCollection.AsEnumerable();
if (!string.IsNullOrWhiteSpace(SharedSearchQuery))
{
var q = SharedSearchQuery.Trim();
data = data.Where(b =>
b.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
b.Author.Contains(q, StringComparison.OrdinalIgnoreCase) ||
b.Isbn.Contains(q, StringComparison.OrdinalIgnoreCase)
);
}
if (ActiveCategory != "All")
{
data = data.Where(b => b.Category.Equals(ActiveCategory, StringComparison.OrdinalIgnoreCase));
}
if (OnlyShowNew) { data = data.Where(b => b.IsNew); }
data = ActivePriceFilter switch
{
"under-500" => data.Where(b => b.Price < 500),
"500-1000" => data.Where(b => b.Price >= 500 && b.Price <= 1000),
"over-1000" => data.Where(b => b.Price > 1000),
_ => data
};
return data;
}
}
private IEnumerable<BookItem> SortedAndFilteredBooks => SelectedSortOption switch
{
"price-low" => FilteredData.OrderBy(b => b.Price),
"price-high" => FilteredData.OrderByDescending(b => b.Price),
"title-asc" => FilteredData.OrderBy(b => b.Title),
_ => FilteredData
};
private IEnumerable<BookItem> PaginatedBooks => SortedAndFilteredBooks.Take(VisibleCount);
private int TotalFilteredCount => FilteredData.Count();
private bool HasMoreItems => VisibleCount < TotalFilteredCount;
protected override void OnInitialized()
{
var extraSourceCategories = new[] { "Fine Arts", "Science", "Photography", "Typography", "Interior Design", "Industrialism", "Fashion", "Curation Studies" };
DynamicExtendedCategories.AddRange(extraSourceCategories);
BooksCollection.Add(new BookItem { Title = "Letters from M/M (Paris)", Author = "M/M Paris", Price = 720, Category = "Graphic Design", IsNew = true, Isbn = "9782915173" });
BooksCollection.Add(new BookItem { Title = "Daan Paans: Floating Signifiers", Author = "Daan Paans", Price = 540, Category = "Product Design", IsNew = true, Isbn = "9789492051" });
BooksCollection.Add(new BookItem { Title = "Album Architectures, Maputo", Author = "Guedes Archive", Price = 350, Category = "Architecture", IsNew = true, Isbn = "9780620751" });
var designPrefixes = new[] { "Minimalist", "Monolithic", "Architectural", "Japanese", "Scandinavian" };
var designNouns = new[] { "Structures", "Typologies", "Forms & Spaces", "Systems Matrix", "Graphic Ephemera" };
var designers = new[] { "J. Morrison", "K. Fujita", "Studio Bouroullec", "Es Devlin", "Kenya Hara" };
var entireCategoryPool = MainCategories.Concat(DynamicExtendedCategories).Where(c => c != "All").ToArray();
var random = new Random(42);
for (int i = 4; i <= 60; i++)
{
BooksCollection.Add(new BookItem
{
Title = $"{designPrefixes[random.Next(designPrefixes.Length)]} {designNouns[random.Next(designNouns.Length)]} (Vol. {random.Next(1, 4)})",
Author = designers[random.Next(designers.Length)],
Price = random.Next(25, 135) * 10,
Category = entireCategoryPool[random.Next(entireCategoryPool.Length)],
IsNew = random.NextDouble() > 0.7,
Isbn = $"978000000{i}"
});
}
}
private void SetViewMode(ViewMode targetMode) => CurrentViewMode = targetMode;
private void SelectCategory(string categoryName) { ActiveCategory = categoryName; VisibleCount = ItemsPerPage; }
private void ToggleExtraCategories() => ShowExpandedCategories = !ShowExpandedCategories;
private void ToggleFilterMenu() => ShowFilterMenu = !ShowFilterMenu;
private void ChangeSort(string sortOption) => SelectedSortOption = sortOption;
private void ChangePriceFilter(string priceBracket) { ActivePriceFilter = priceBracket; VisibleCount = ItemsPerPage; }
private void ToggleNewArrivalsOnly(ChangeEventArgs e) { OnlyShowNew = e.Value is bool b && b; VisibleCount = ItemsPerPage; }
private void ResetFilters() { SelectedSortOption = "default"; ActivePriceFilter = "all"; OnlyShowNew = false; VisibleCount = ItemsPerPage; }
private void LoadNextPage() { if (HasMoreItems) VisibleCount += ItemsPerPage; }
}
@@ -0,0 +1,141 @@
/* Expanding Search Input Controls */
.search-input-container {
max-width: 0;
opacity: 0;
overflow: hidden;
transition: max-width 0.35s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.25s ease;
}
.search-input-container.is-active {
max-width: 280px;
opacity: 1;
}
.custom-search-field {
font-size: 0.8rem;
height: 32px;
box-shadow: none !important;
}
/* Structural Layout Typography */
.branding-logo {
font-size: 1.1rem;
letter-spacing: -0.5px;
}
.text-hero-wrapper {
margin-top: 5rem;
margin-bottom: 3.5rem;
}
.master-headline {
letter-spacing: -1.5px;
font-weight: 400;
line-height: 1.15;
}
.sub-headline {
max-width: 520px;
font-size: 0.95rem;
font-weight: 300;
line-height: 1.6;
}
/* Filtering Dropdown Drawer Settings */
.filter-dropdown-panel {
border-radius: 12px;
}
.panel-section-heading {
letter-spacing: 0.5px;
font-size: 0.7rem;
}
.reset-link-btn {
font-size: 0.7rem;
}
/* Custom Minimal Separator Rule Layout */
.custom-milled-line {
height: 35px;
margin-top: 5px;
margin-bottom: 25px;
z-index: 0;
pointer-events: none;
}
.center-bloom-shadow {
bottom: 15px;
width: 55%;
height: 50px;
background: radial-gradient(ellipse at bottom, rgba(20, 20, 20, 0.07) 0%, rgba(40, 40, 40, 0.02) 60%, rgba(255, 255, 255, 0) 100%);
filter: blur(10px);
}
.core-horizontal-rule {
bottom: 15px;
height: 1px;
background: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.15) 50%, rgba(0,0,0,0) 100%);
}
/* View Engine Layout Presentation Variants: Grid Cards */
.book-grid-card {
background-color: #F1F1F1;
border-radius: 12px;
min-height: 380px;
}
.badge-new-arrival {
background-color: #E63946;
font-size: 0.75rem;
}
.book-spine-fallback {
width: 130px;
height: 185px;
font-size: 0.65rem;
letter-spacing: 1px;
border-radius: 2px;
}
.product-card-title {
font-size: 0.95rem;
font-weight: 500;
}
/* View Engine Layout Presentation Variants: List Rows */
.list-row-item {
border-bottom: 1px solid rgba(0,0,0,0.06);
transition: background-color 0.2s ease;
}
.list-row-item:hover {
background-color: rgba(0,0,0,0.01);
}
.structural-list-left {
flex: 2;
}
.list-item-title {
font-size: 0.95rem;
min-width: 260px;
}
.list-item-author {
min-width: 160px;
}
.list-item-tag {
font-size: 0.65rem;
}
.list-new-badge {
font-size: 0.65rem;
}
.list-item-price {
font-size: 0.9rem;
min-width: 80px;
text-align: right;
}
+118 -43
View File
@@ -1,60 +1,135 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@300;400;500;600&display=swap');
:root {
/* Fonts */
--font-heading: 'Instrument Serif', serif;
--font-ui: 'Inter', sans-serif;
/* Color Palette */
--mb-bg: #F9F9F9;
--mb-card-bg: #FFFFFF;
--mb-text-dark: #1A1A1A;
--mb-text-muted: #666666;
--mb-accent-red: #E63946;
--mb-radius: 12px;
/* High-Visibility Machined White Metal Core Surface Definition */
/* Sharp linear reflection channels paired with micro-milled density grids */
--brushed-metal-bg: linear-gradient(90deg, rgba(255, 255, 255, 1) 0%, rgba(240, 241, 245, 0.95) 25%, rgba(255, 255, 255, 1) 50%, rgba(238, 240, 244, 0.95) 75%, rgba(255, 255, 255, 1) 100% ), repeating-linear-gradient( 0deg, rgba(0, 0, 0, 0.012) 0px, rgba(0, 0, 0, 0.012) 1px, transparent 1px, transparent 2px ),
repeating-linear-gradient( 90deg, rgba(255, 255, 255, 0.9) 0px, rgba(255, 255, 255, 0.9) 1px, transparent 1px, transparent 3px ), #EDEFF4; /* Definitive satin platinum breakout base fallback */
}
a, .btn-link {
color: #006bb7;
/* Global Reset & Core Variables Mapping */
body {
background-color: var(--mb-bg);
color: var(--mb-text-dark);
font-family: var(--font-ui);
-webkit-font-smoothing: antialiased;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
h1, h2, h3, .display-font {
font-family: var(--font-heading);
font-weight: 400;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
/* Custom Navigation Pill Styling */
.nav-pill-wrapper {
background-color: #FFFFFF;
border: 1px solid rgba(0, 0, 0, 0.06);
padding: 6px;
border-radius: 50px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.02);
}
.content {
padding-top: 1.1rem;
.nav-pill-link {
font-size: 0.9rem;
font-weight: 500;
color: var(--mb-text-muted);
padding: 8px 20px;
border-radius: 50px;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
.nav-pill-link i {
width: 16px;
height: 16px;
stroke-width: 1.5;
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
/* Navigation Pill States */
.nav-pill-link:hover {
color: var(--mb-text-dark);
background-color: #F1F1F1;
}
.nav-pill-link.active {
color: #FFFFFF !important;
background-color: var(--mb-text-dark);
}
.nav-pill-link.active svg {
stroke: #FFFFFF;
}
/* Utilities & Component Support Layouts */
.cart-badge {
position: absolute;
top: -5px;
right: -5px;
background-color: var(--mb-text-dark);
color: #FFFFFF;
font-size: 0.7rem;
font-weight: 600;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--mb-bg);
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
.transition-smooth {
transition: color 0.2s ease;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
.transition-smooth:hover {
color: var(--mb-text-dark) !important;
}
.py-1-5 {
padding-top: 0.35rem !important;
padding-bottom: 0.35rem !important;
}
/* Global Focus Ring Corrections */
h1:focus, h2:focus, h3:focus, h4:focus, p:focus, div:focus, span:focus {
outline: none !important;
box-shadow: none !important;
}
[tabindex="-1"]:focus {
outline: none !important;
}
/* Polished White Metal Footer Enhancements */
footer .text-muted {
color: #555555 !important;
}
footer .text-dark {
color: #1A1A1A !important;
font-weight: 600;
}
footer span.text-muted,
footer a.text-muted {
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6);
transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
footer a.text-muted:hover {
color: #1A1A1A !important;
}