From bd518310da73e72915fc724940e24fc6025ea8c0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 5 Oct 2025 08:18:07 +0000 Subject: [PATCH 1/2] Checkpoint before follow-up message Co-authored-by: cursor001 --- scripts/minify_assets.py | 82 +++++++++++++++++++ src/app/core/application.py | 11 ++- src/app/core/config.py | 4 + src/app/core/static_files.py | 31 +++++++ src/app/web/static/css/auth.min.css | 1 + src/app/web/static/css/base.min.css | 1 + src/app/web/static/css/brand.min.css | 1 + src/app/web/static/css/dashboard.min.css | 1 + .../static/css/moderation-dashboard.min.css | 1 + src/app/web/static/css/nusantarum.min.css | 1 + src/app/web/static/css/profile.min.css | 1 + .../web/static/css/purchase-workflow.min.css | 1 + src/app/web/static/css/ui-ux-tracker.min.css | 1 + .../static/js/dashboard-brand-owner.min.js | 1 + src/app/web/static/js/home-tabs.min.js | 1 + .../web/static/js/moderation-dashboard.min.js | 1 + src/app/web/static/js/navbar.min.js | 1 + src/app/web/static/js/newsletter.js | 57 +++++++++++++ src/app/web/static/js/newsletter.min.js | 1 + src/app/web/static/js/nusantarum-tabs.min.js | 1 + src/app/web/static/js/session.min.js | 1 + src/app/web/templates/__init__.py | 3 +- src/app/web/templates/auth.html | 2 +- src/app/web/templates/base.html | 32 ++++++-- src/app/web/templates/index.html | 62 +------------- src/app/web/templates/pages/brand/detail.html | 2 +- src/app/web/templates/pages/brand/form.html | 2 +- src/app/web/templates/pages/brand/index.html | 2 +- .../pages/dashboard/brand_owner.html | 4 +- .../templates/pages/dashboard/moderation.html | 4 +- .../web/templates/pages/nusantarum/index.html | 4 +- .../web/templates/pages/profile/detail.html | 2 +- src/app/web/templates/pages/profile/edit.html | 2 +- src/app/web/templates/purchase_workflow.html | 2 +- src/app/web/templates/ui_ux_tracker.html | 2 +- 35 files changed, 242 insertions(+), 84 deletions(-) create mode 100755 scripts/minify_assets.py create mode 100644 src/app/core/static_files.py create mode 100644 src/app/web/static/css/auth.min.css create mode 100644 src/app/web/static/css/base.min.css create mode 100644 src/app/web/static/css/brand.min.css create mode 100644 src/app/web/static/css/dashboard.min.css create mode 100644 src/app/web/static/css/moderation-dashboard.min.css create mode 100644 src/app/web/static/css/nusantarum.min.css create mode 100644 src/app/web/static/css/profile.min.css create mode 100644 src/app/web/static/css/purchase-workflow.min.css create mode 100644 src/app/web/static/css/ui-ux-tracker.min.css create mode 100644 src/app/web/static/js/dashboard-brand-owner.min.js create mode 100644 src/app/web/static/js/home-tabs.min.js create mode 100644 src/app/web/static/js/moderation-dashboard.min.js create mode 100644 src/app/web/static/js/navbar.min.js create mode 100644 src/app/web/static/js/newsletter.js create mode 100644 src/app/web/static/js/newsletter.min.js create mode 100644 src/app/web/static/js/nusantarum-tabs.min.js create mode 100644 src/app/web/static/js/session.min.js diff --git a/scripts/minify_assets.py b/scripts/minify_assets.py new file mode 100755 index 0000000..92476ed --- /dev/null +++ b/scripts/minify_assets.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +Minify CSS and JavaScript files for production. +This script reduces file sizes by removing whitespace, comments, and optimizing code. +""" +import re +from pathlib import Path + + +def minify_css(content: str) -> str: + """Minify CSS by removing comments, whitespace, and optimizing.""" + # Remove comments + content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) + # Remove newlines and extra spaces + content = re.sub(r'\s+', ' ', content) + # Remove spaces around special characters + content = re.sub(r'\s*([{}:;,>+~])\s*', r'\1', content) + # Remove trailing semicolons + content = re.sub(r';}', '}', content) + return content.strip() + + +def minify_js(content: str) -> str: + """Minify JavaScript by removing comments and unnecessary whitespace.""" + # Remove single-line comments (but preserve URLs) + content = re.sub(r'(?:=+\-*/%!&|?])\s*', r'\1', content) + # Add back necessary spaces for keywords + content = re.sub(r'(\b(?:return|var|let|const|if|else|for|while|function|class)\b)', r' \1 ', content) + return content.strip() + + +def process_files(source_dir: Path, output_dir: Path, extensions: dict): + """Process files with specified extensions using their minify functions.""" + for ext, minify_func in extensions.items(): + for file_path in source_dir.rglob(f'*.{ext}'): + # Skip already minified files + if '.min.' in file_path.name: + continue + + content = file_path.read_text(encoding='utf-8') + minified = minify_func(content) + + # Create output path + relative = file_path.relative_to(source_dir) + output_file = output_dir / relative.parent / f"{relative.stem}.min.{ext}" + output_file.parent.mkdir(parents=True, exist_ok=True) + + # Write minified content + output_file.write_text(minified, encoding='utf-8') + + # Calculate size reduction + original_size = len(content) + minified_size = len(minified) + reduction = ((original_size - minified_size) / original_size) * 100 + + print(f"✓ {relative}: {original_size:,} → {minified_size:,} bytes ({reduction:.1f}% reduction)") + + +if __name__ == '__main__': + project_root = Path(__file__).parent.parent + static_dir = project_root / 'src' / 'app' / 'web' / 'static' + + print("🚀 Minifying assets...") + print() + + print("📦 CSS Files:") + css_dir = static_dir / 'css' + process_files(css_dir, css_dir, {'css': minify_css}) + + print() + print("📦 JavaScript Files:") + js_dir = static_dir / 'js' + process_files(js_dir, js_dir, {'js': minify_js}) + + print() + print("✨ Minification complete!") diff --git a/src/app/core/application.py b/src/app/core/application.py index 93a9b2c..6f953d5 100644 --- a/src/app/core/application.py +++ b/src/app/core/application.py @@ -4,10 +4,11 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from starlette.staticfiles import StaticFiles +from fastapi.middleware.gzip import GZipMiddleware from app.core.config import get_settings from app.core.session import InMemorySessionMiddleware +from app.core.static_files import CachedStaticFiles from app.web.templates import template_engine from app.api.routes import onboarding as onboarding_routes from app.api.routes import profile as profile_routes @@ -27,6 +28,10 @@ def create_app() -> FastAPI: app = FastAPI(title=settings.app_name) + # Add GZip compression for better performance + if settings.enable_compression: + app.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=6) + app.add_middleware( InMemorySessionMiddleware, max_age=60 * 60 * 24 * 30, @@ -42,8 +47,8 @@ def create_app() -> FastAPI: allow_headers=["*"], ) - # Mount static assets (CSS, JS, images) served by the Jinja2 templates. - app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + # Mount static assets (CSS, JS, images) with optimized caching. + app.mount("/static", CachedStaticFiles(directory=str(STATIC_DIR)), name="static") # Register routers for server-rendered pages and API endpoints. app.include_router(root_routes.router) diff --git a/src/app/core/config.py b/src/app/core/config.py index a91d1bf..4502b9d 100644 --- a/src/app/core/config.py +++ b/src/app/core/config.py @@ -28,6 +28,10 @@ class Settings(BaseSettings): # Session management session_secret: str = "development-session-secret-placeholder" static_asset_version: str = "2024051901" + + # Performance optimizations + use_minified_assets: bool = False + enable_compression: bool = True @field_validator("session_secret") @classmethod diff --git a/src/app/core/static_files.py b/src/app/core/static_files.py new file mode 100644 index 0000000..a669959 --- /dev/null +++ b/src/app/core/static_files.py @@ -0,0 +1,31 @@ +"""Optimized static file serving with caching headers.""" + +from starlette.staticfiles import StaticFiles +from starlette.responses import Response +from starlette.types import Scope, Receive, Send + + +class CachedStaticFiles(StaticFiles): + """Static files middleware with aggressive caching for immutable assets.""" + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + """Add cache headers to static file responses.""" + + async def send_wrapper(message): + if message["type"] == "http.response.start": + headers = dict(message.get("headers", [])) + + # Add cache headers for static assets + # Assets with version query params are immutable + if b"Cache-Control" not in headers: + # Cache for 1 year for versioned assets + headers[b"Cache-Control"] = b"public, max-age=31536000, immutable" + + # Add compression hint + headers[b"Vary"] = b"Accept-Encoding" + + message["headers"] = list(headers.items()) + + await send(message) + + await super().__call__(scope, receive, send_wrapper) diff --git a/src/app/web/static/css/auth.min.css b/src/app/web/static/css/auth.min.css new file mode 100644 index 0000000..1e4c97b --- /dev/null +++ b/src/app/web/static/css/auth.min.css @@ -0,0 +1 @@ +.auth-page{min-height:calc(100vh - 6rem);display:flex;align-items:center;justify-content:center;padding:clamp(2rem,5vw,4rem);position:relative;overflow:hidden}.auth-page::before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 10% 20%,rgba(59,130,246,0.14),transparent 55%),radial-gradient(circle at 85% 80%,rgba(16,185,129,0.15),transparent 60%),linear-gradient(135deg,rgba(15,23,42,0.6),rgba(30,64,175,0.4));opacity:0.75;pointer-events:none}.auth-login-section{position:relative;width:min(460px,100%);z-index:1}.auth-login-section::before{content:"";position:absolute;inset:-30px;border-radius:var(--radius-xl);background:linear-gradient(160deg,rgba(59,130,246,0.2),transparent 70%);filter:blur(45px);z-index:-1}.login-backdrop{position:relative;padding:clamp(2rem,4vw,2.75rem);border-radius:var(--radius-xl);border:1px solid rgba(148,163,184,0.25);backdrop-filter:blur(14px);background:rgba(15,23,42,0.55);box-shadow:0 18px 36px rgba(15,23,42,0.45)}.auth-feedback{border-radius:var(--radius-md);padding:1rem 1.2rem;margin-bottom:1.25rem;display:none;background:rgba(56,189,248,0.08);border:1px solid rgba(56,189,248,0.25);color:var(--text-primary)}.auth-feedback.visible{display:block}.auth-feedback[data-tone='success']{background:rgba(52,211,153,0.12);border-color:rgba(52,211,153,0.4)}.auth-feedback[data-tone='danger']{background:rgba(248,113,113,0.12);border-color:rgba(248,113,113,0.4);color:#fecaca}.auth-form{display:flex;flex-direction:column;gap:1.15rem}.auth-form label{display:flex;flex-direction:column;gap:0.45rem;font-weight:500;color:var(--text-primary)}.auth-form .input-hint{margin:0;font-size:0.85rem;font-weight:400;color:var(--text-secondary);line-height:1.4}.auth-form input{border-radius:var(--radius-sm);border:1px solid rgba(148,163,184,0.35);background:rgba(15,23,42,0.7);color:var(--text-primary);padding:0.85rem 1rem;font-size:0.95rem;transition:border-color var(--transition-base),box-shadow var(--transition-base),background var(--transition-base)}.auth-form input:focus{outline:none;border-color:rgba(56,189,248,0.85);box-shadow:0 0 0 3px rgba(56,189,248,0.22);background:rgba(15,23,42,0.85)}.login-header{display:flex;flex-direction:column;gap:0.4rem}.login-header h2{margin:0;font-size:clamp(1.8rem,3vw,2.2rem)}.login-header p{margin:0;color:var(--text-secondary)}.login-aux{display:flex;justify-content:flex-end}.forgot-link{font-size:0.85rem;color:rgba(191,219,254,0.9);text-decoration:none;border-bottom:1px solid transparent;transition:border-color var(--transition-base),color var(--transition-base)}.forgot-link:hover{color:rgba(224,242,254,1);border-color:rgba(191,219,254,0.5)}.login-form .btn{width:100%}.login-divider{text-transform:uppercase;letter-spacing:0.2em;font-size:0.75rem;text-align:center;color:rgba(148,163,184,0.75)}.register-button{text-align:center}.form-note{font-size:0.85rem;color:var(--muted);line-height:1.4;margin:0}@media (max-width:640px){.auth-page{padding:1.75rem 1.25rem 2.5rem}.login-backdrop{padding:1.75rem}} \ No newline at end of file diff --git a/src/app/web/static/css/base.min.css b/src/app/web/static/css/base.min.css new file mode 100644 index 0000000..acc2131 --- /dev/null +++ b/src/app/web/static/css/base.min.css @@ -0,0 +1 @@ +:root{--gradient-background:radial-gradient(circle at 15% 20%,rgba(249,115,22,0.15),transparent 55%),radial-gradient(circle at 85% 15%,rgba(192,38,211,0.18),transparent 60%),linear-gradient(135deg,#0f172a,#1e293b 55%,#0b1120);--glass-surface:rgba(15,23,42,0.62);--glass-border:rgba(148,163,184,0.35);--glass-shadow:0 25px 45px rgba(15,23,42,0.45);--text-primary:#f8fafc;--text-secondary:#cbd5f5;--text-tertiary:rgba(203,213,225,0.78);--accent-primary:linear-gradient(135deg,#f97316,#c026d3);--accent-secondary:#38bdf8;--muted:#94a3b8;--success:#34d399;--danger:#f87171;--radius-lg:28px;--radius-md:20px;--radius-sm:14px;--radius-full:999px;--transition-base:180ms ease}body{min-height:100vh;margin:0;background:var(--gradient-background);color:var(--text-primary);font-family:"Inter",system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;line-height:1.6;position:relative}.background-aurora{position:fixed;inset:0;background:inherit;filter:blur(60px) saturate(140%);opacity:0.9;pointer-events:none;z-index:-2}body::before{content:"";position:fixed;inset:0;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='.06'%3E%3Cpath d='M80 0h1v160h-1zM0 80h160v1H0z'/%3E%3C/g%3E%3C/svg%3E");opacity:0.4;pointer-events:none;z-index:-1}.page-wrapper{width:min(1180px,100%);margin:0 auto;padding:2.5rem 1.5rem 4rem;display:flex;flex-direction:column;gap:2.75rem}.content{display:flex;flex-direction:column;gap:3rem}.glass-surface{backdrop-filter:blur(24px) saturate(140%);background:linear-gradient(145deg,rgba(15,23,42,0.65),rgba(15,23,42,0.4));border:1px solid var(--glass-border);border-radius:var(--radius-lg);box-shadow:var(--glass-shadow)}.glass-card{backdrop-filter:blur(22px) saturate(150%);background:rgba(15,23,42,0.68);border:1px solid rgba(148,163,184,0.25);border-radius:var(--radius-md);box-shadow:0 18px 40px rgba(8,15,29,0.35);transition:transform var(--transition-base),box-shadow var(--transition-base)}.glass-card:hover{transform:translateY(-6px);box-shadow:0 28px 60px rgba(8,15,29,0.45)}h1,h2,h3,h4{font-family:"Playfair Display","Times New Roman",serif;font-weight:600;line-height:1.25}p{color:var(--text-secondary)}.btn{display:inline-flex;align-items:center;justify-content:center;gap:0.5rem;border-radius:999px;padding:0.75rem 1.8rem;font-weight:600;text-decoration:none;cursor:pointer;border:none;transition:transform var(--transition-base),box-shadow var(--transition-base),background var(--transition-base)}.btn:hover{transform:translateY(-2px)}.gradient-button{background:var(--accent-primary);color:#0f172a;box-shadow:0 12px 30px rgba(249,115,22,0.3)}.gradient-button:hover{box-shadow:0 18px 40px rgba(192,38,211,0.25)}.btn-outline{background:transparent;border:1px solid rgba(56,189,248,0.35);color:var(--accent-secondary)}.btn-outline:hover{border-color:rgba(56,189,248,0.75);background:rgba(56,189,248,0.08)}.btn-ghost{background:transparent;color:var(--text-secondary);border:1px solid transparent}.btn-ghost:hover{color:var(--text-primary);border-color:rgba(148,163,184,0.3)}.button{display:inline-flex;align-items:center;justify-content:center;gap:0.5rem;border-radius:var(--radius-full);padding:0.75rem 1.9rem;font-weight:600;text-decoration:none;cursor:pointer;border:none;transition:transform var(--transition-base),box-shadow var(--transition-base),background var(--transition-base)}.button:focus-visible{outline:2px solid rgba(56,189,248,0.55);outline-offset:2px}.button:hover{transform:translateY(-2px)}.button-primary{background:var(--accent-primary);color:#0f172a;box-shadow:0 16px 32px rgba(249,115,22,0.24)}.button-primary:hover{box-shadow:0 22px 40px rgba(192,38,211,0.28)}.button-secondary{background:rgba(56,189,248,0.14);color:rgba(241,245,249,0.95);border:1px solid rgba(56,189,248,0.4)}.button-secondary:hover{background:rgba(56,189,248,0.2)}.button-ghost{background:transparent;color:rgba(226,232,240,0.95);border:1px solid rgba(148,163,184,0.25)}.button-ghost:hover{border-color:rgba(148,163,184,0.4);color:rgba(248,250,252,0.95)}.navbar{position:sticky;top:1.5rem;z-index:20;display:flex;align-items:center;justify-content:space-between;padding:1rem 1.5rem;gap:1rem}.navbar-collapsible{display:flex;align-items:center;gap:1.5rem;margin-left:auto}.navbar-brand .logo{font-family:"Playfair Display","Times New Roman",serif;font-size:1.4rem}.navbar-menu{list-style:none;display:flex;align-items:center;gap:1.5rem;padding:0;margin:0}.navbar-menu a{color:var(--text-secondary);text-decoration:none;font-weight:500;position:relative;padding:0.25rem 0}.navbar-menu a::after{content:"";position:absolute;left:0;bottom:-0.35rem;width:100%;height:2px;background:var(--accent-primary);transform:scaleX(0);transform-origin:left;transition:transform var(--transition-base)}.navbar-menu a:hover::after{transform:scaleX(1)}.navbar-menu a.active{color:var(--text-primary)}.navbar-menu a.active::after{transform:scaleX(1)}.navbar-actions{display:flex;align-items:center;gap:0.75rem}.navbar-dropdown{position:relative;display:inline-flex}.navbar-dropdown__toggle{display:inline-flex;align-items:center;gap:0.4rem;padding:0.4rem 0.75rem;border-radius:999px;cursor:pointer;color:var(--text-secondary);font-weight:600;text-decoration:none;user-select:none;margin:0}.navbar-dropdown__toggle:hover{color:var(--text-primary);background:rgba(255,255,255,0.06)}.navbar-dropdown__toggle::-webkit-details-marker{display:none}.navbar-dropdown__toggle:focus-visible{outline:2px solid var(--accent-secondary);outline-offset:3px}.navbar-dropdown[open] .navbar-dropdown__toggle{color:var(--text-primary);background:rgba(255,255,255,0.08)}.navbar-dropdown__icon{font-size:0.75rem;transition:transform var(--transition-fast)}.navbar-dropdown[open] .navbar-dropdown__icon{transform:rotate(180deg)}.navbar-dropdown__menu{position:absolute;top:calc(100%+0.75rem);right:0;min-width:200px;padding:0.75rem;border-radius:1rem;background:rgba(18,18,18,0.7);backdrop-filter:blur(16px);box-shadow:0 18px 40px rgba(15,23,42,0.35);list-style:none;display:grid;gap:0.35rem;z-index:25}.navbar-dropdown:not([open]) .navbar-dropdown__menu{display:none}.navbar-dropdown__menu a,.navbar-dropdown__menu button{display:flex;width:100%;align-items:center;gap:0.4rem;border:none;background:transparent;color:var(--text-secondary);font:inherit;padding:0.45rem 0.6rem;border-radius:0.65rem;cursor:pointer;text-align:left;text-decoration:none;transition:background var(--transition-fast),color var(--transition-fast)}.navbar-dropdown__menu a:hover,.navbar-dropdown__menu a:focus-visible,.navbar-dropdown__menu button:hover,.navbar-dropdown__menu button:focus-visible{color:var(--text-primary);background:rgba(255,255,255,0.08)}.navbar-actions .is-loading{opacity:0.6;pointer-events:none}.navbar-toggle{display:none;flex-direction:column;gap:0.3rem;background:none;border:none;cursor:pointer}.navbar-toggle span{width:20px;height:2px;background:var(--text-primary);border-radius:999px}.hero{padding:3rem 3.5rem;position:relative;overflow:hidden;display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:2.5rem}.hero::after{content:"";position:absolute;inset:0;background:linear-gradient(120deg,rgba(249,115,22,0.08),rgba(192,38,211,0.12));opacity:0.6;pointer-events:none}.hero>*{position:relative;z-index:1}.badge{display:inline-flex;align-items:center;gap:0.4rem;font-size:0.85rem;color:var(--accent-secondary);background:rgba(56,189,248,0.1);border:1px solid rgba(56,189,248,0.35);border-radius:999px;padding:0.35rem 0.85rem;letter-spacing:0.03em;text-transform:uppercase}.badge.brand{color:#f472b6;background:rgba(244,114,182,0.14);border-color:rgba(244,114,182,0.4)}.badge.creator{color:#facc15;background:rgba(250,204,21,0.16);border-color:rgba(250,204,21,0.45)}.badge.accent{color:#f97316;background:rgba(249,115,22,0.16);border-color:rgba(249,115,22,0.45)}.badge.subtle{color:var(--text-secondary);background:rgba(148,163,184,0.12);border-color:rgba(148,163,184,0.35)}.hero-copy h1{font-size:clamp(2.2rem,3vw,3.3rem);margin:1rem 0}.hero-copy p{max-width:540px}.filter-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;align-items:flex-end}.filter-form.single-field{grid-template-columns:1fr}.filter-hint{grid-column:1 / -1;font-size:0.85rem;color:var(--text-tertiary);margin:0}.catalog-controls{display:flex;flex-direction:column;gap:0.75rem}.catalog-tab-bar{display:grid;grid-template-columns:minmax(0,1fr) minmax(220px,320px);gap:0.9rem;align-items:center;background:rgba(15,23,42,0.45);border-radius:var(--radius-lg);padding:0.85rem 1rem;border:1px solid rgba(148,163,184,0.28);box-shadow:0 18px 38px rgba(8,15,29,0.28)}.catalog-search{justify-self:end;width:100%;max-width:360px;display:flex;flex-direction:column;gap:0.35rem}.catalog-search-input{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:0.5rem;background:linear-gradient(135deg,rgba(148,163,184,0.16),rgba(56,189,248,0.12));border:1px solid rgba(148,163,184,0.4);border-radius:var(--radius-full);padding:0.35rem 0.35rem 0.35rem 0.65rem;backdrop-filter:blur(16px) saturate(130%);box-shadow:inset 0 0 0 1px rgba(15,23,42,0.4);transition:border-color var(--transition-base),box-shadow var(--transition-base),background var(--transition-base)}.catalog-search-input:focus-within{border-color:rgba(56,189,248,0.65);box-shadow:0 12px 28px rgba(14,116,144,0.28);background:linear-gradient(135deg,rgba(148,163,184,0.18),rgba(56,189,248,0.18))}.catalog-search-icon{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;color:rgba(148,163,184,0.85);pointer-events:none}.catalog-search-input svg{width:1.1rem;height:1.1rem}.catalog-search-input input{width:100%;background:transparent;border:none;color:var(--text-primary);font-size:0.95rem;padding:0.55rem 0.4rem}.catalog-search-input input::placeholder{color:rgba(226,232,240,0.62)}.catalog-search-input input:focus{outline:none}.catalog-search-button{display:inline-flex;align-items:center;justify-content:center;width:2.6rem;height:2.6rem;border-radius:var(--radius-full);border:none;background:linear-gradient(135deg,rgba(56,189,248,0.9),rgba(192,38,211,0.9));color:#0b1120;cursor:pointer;transition:transform var(--transition-base),box-shadow var(--transition-base),filter var(--transition-base);box-shadow:0 10px 28px rgba(56,189,248,0.32)}.catalog-search-button:hover,.catalog-search-button:focus{transform:translateY(-1px);box-shadow:0 14px 34px rgba(192,38,211,0.28);outline:none}.catalog-search-button svg{width:1.1rem;height:1.1rem}.catalog-search .filter-hint{margin:0;font-size:0.8rem;color:var(--text-tertiary)}.field{display:flex;flex-direction:column;gap:0.35rem;font-size:0.9rem;color:var(--text-secondary)}.field input,.field select{background:rgba(15,23,42,0.65);border:1px solid rgba(148,163,184,0.35);border-radius:var(--radius-md);padding:0.65rem 0.85rem;color:var(--text-primary);font-size:0.95rem;outline:none}.field select{cursor:pointer}.field.range .range-inputs{display:flex;align-items:center;gap:0.5rem}.field.range input[type="number"]{width:100%}.range-inputs .divider{color:var(--text-secondary)}.filter-form button{grid-column:1 / -1;justify-self:flex-start;margin-top:0.5rem}.hero-visual{position:relative;min-height:260px;display:flex;align-items:center;justify-content:center}.hero-aurora{position:absolute;width:320px;height:320px;border-radius:50%;background:radial-gradient(circle,rgba(249,115,22,0.45),rgba(192,38,211,0.1));filter:blur(30px);animation:pulse 6s ease-in-out infinite}.hero-bottle{position:relative;width:160px;display:flex;flex-direction:column;align-items:center;gap:0.4rem}.hero-bottle .cap{width:60px;height:24px;background:rgba(148,163,184,0.55);border-radius:12px}.hero-bottle .body{width:100%;height:180px;background:linear-gradient(145deg,rgba(56,189,248,0.4),rgba(192,38,211,0.4));border-radius:80px 80px 28px 28px;border:1px solid rgba(148,163,184,0.3);box-shadow:inset 0 12px 20px rgba(255,255,255,0.15)}.hero-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem}.stat-card{padding:1.5rem;text-align:center}.stat-value{font-size:2rem;font-weight:700}.stat-label{color:var(--text-secondary);margin-top:0.4rem}.catalog{display:flex;flex-direction:column;gap:1.5rem}.catalog-header{display:flex;justify-content:space-between;gap:1.5rem;padding:2rem 2.5rem;align-items:center;flex-wrap:wrap}.catalog-header h2{margin-bottom:0.5rem}.tab-list{display:inline-flex;gap:0.75rem;background:rgba(148,163,184,0.1);padding:0.4rem;border-radius:999px}.tab{background:transparent;border:none;color:var(--text-secondary);padding:0.55rem 1.4rem;border-radius:999px;cursor:pointer;font-weight:500;transition:background var(--transition-base),color var(--transition-base)}.tab:hover,.tab:focus-visible{background:rgba(249,115,22,0.18);color:var(--text-primary)}.tab:focus-visible{outline:2px solid rgba(56,189,248,0.55);outline-offset:2px}.tab.is-active,.tab[aria-selected="true"]{background:rgba(249,115,22,0.24);color:var(--text-primary)}.tab-panel[hidden]{display:none}.filter-bar{padding:1.2rem 1.5rem;display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;border-radius:var(--radius-md)}.filter-group{display:flex;gap:0.75rem;flex-wrap:wrap}.filter-chip{padding:0.45rem 1.1rem;background:rgba(15,23,42,0.6);border-radius:999px;border:1px solid rgba(148,163,184,0.3);color:var(--text-secondary);font-size:0.9rem}.search-box{display:inline-flex;align-items:center;gap:0.6rem;background:rgba(15,23,42,0.65);border-radius:999px;padding:0.45rem 1.1rem;border:1px solid rgba(148,163,184,0.3);width:min(320px,100%);color:var(--text-secondary)}.search-box input{flex:1;background:transparent;border:none;outline:none;color:var(--text-primary);font-size:0.95rem}.search-box .search-submit{display:inline-flex;align-items:center;justify-content:center;width:2.4rem;height:2.4rem;border-radius:var(--radius-full);border:none;background:linear-gradient(135deg,rgba(56,189,248,0.9),rgba(192,38,211,0.9));color:#0b1120;cursor:pointer;transition:transform var(--transition-base),box-shadow var(--transition-base);box-shadow:0 10px 24px rgba(56,189,248,0.28)}.search-box .search-submit:hover,.search-box .search-submit:focus-visible{transform:translateY(-1px);box-shadow:0 14px 32px rgba(192,38,211,0.24);outline:none}.search-box .search-submit svg{width:1.05rem;height:1.05rem}.product-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.5rem}.product-card{display:flex;flex-direction:column;overflow:hidden;position:relative}a.product-card{color:inherit;text-decoration:none}a.product-card:focus-visible{outline:2px solid rgba(56,189,248,0.55);outline-offset:3px}.product-badge{position:absolute;top:1rem;left:1rem;display:inline-flex;align-items:center;gap:0.35rem;padding:0.4rem 0.85rem;border-radius:999px;font-size:0.75rem;font-weight:600;letter-spacing:0.02em;text-transform:uppercase;background:rgba(15,23,42,0.75);border:1px solid rgba(148,163,184,0.35);box-shadow:0 10px 25px rgba(8,15,29,0.35)}.product-badge .badge-icon{font-size:0.95rem}.product-badge-sambatan{background:linear-gradient(135deg,rgba(249,115,22,0.8),rgba(192,38,211,0.8));color:#0f172a;border:1px solid rgba(248,250,252,0.6)}.product-media{height:180px;border-radius:var(--radius-md) var(--radius-md) 0 0;background:radial-gradient(circle at 30% 20%,rgba(249,115,22,0.4),transparent 60%),radial-gradient(circle at 70% 30%,rgba(56,189,248,0.35),transparent 65%),rgba(148,163,184,0.1)}.parfum-aurora{background:linear-gradient(135deg,rgba(249,115,22,0.45),rgba(56,189,248,0.3))}.parfum-tropis{background:linear-gradient(135deg,rgba(192,38,211,0.4),rgba(56,189,248,0.35))}.parfum-malam{background:linear-gradient(135deg,rgba(59,130,246,0.4),rgba(17,94,89,0.35))}.bahan-cendana{background:linear-gradient(135deg,rgba(180,83,9,0.38),rgba(249,115,22,0.24))}.bahan-jeruk{background:linear-gradient(135deg,rgba(251,191,36,0.34),rgba(234,179,8,0.26))}.alat-scale{background:linear-gradient(135deg,rgba(14,165,233,0.35),rgba(56,189,248,0.28))}.alat-strip{background:linear-gradient(135deg,rgba(147,197,253,0.32),rgba(126,34,206,0.24))}.community-lagoon{background:linear-gradient(135deg,rgba(56,189,248,0.38),rgba(16,185,129,0.32))}.community-solstice{background:linear-gradient(135deg,rgba(251,191,36,0.32),rgba(239,68,68,0.28))}.community-dawn{background:linear-gradient(135deg,rgba(110,231,183,0.32),rgba(96,165,250,0.35))}.material-citronella{background:linear-gradient(135deg,rgba(34,197,94,0.34),rgba(74,222,128,0.26))}.material-benzoin{background:linear-gradient(135deg,rgba(249,115,22,0.32),rgba(217,119,6,0.28))}.equipment-pipette{background:linear-gradient(135deg,rgba(59,130,246,0.3),rgba(125,211,252,0.28))}.equipment-scale{background:linear-gradient(135deg,rgba(30,64,175,0.32),rgba(129,140,248,0.28))}.misc-packaging{background:linear-gradient(135deg,rgba(244,114,182,0.32),rgba(236,72,153,0.24))}.misc-workshop{background:linear-gradient(135deg,rgba(99,102,241,0.3),rgba(168,85,247,0.26))}.product-content{display:flex;flex-direction:column;gap:0.75rem;padding:1.5rem}.marketplace-grid .product-content{padding:0;gap:0}.marketplace-grid .product-media{border-radius:0;width:100%;aspect-ratio:1 / 1}.marketplace-grid .product-info{display:flex;flex-direction:column;gap:0.4rem;padding:1.25rem 1.5rem 1.5rem}.marketplace-grid .product-name{font-size:1.1rem;font-weight:600}.marketplace-grid .product-brand{color:var(--text-secondary);text-transform:none;font-size:0.8rem;letter-spacing:0.02em;display:inline-flex;align-items:center;gap:0.35rem}.marketplace-grid .product-brand[role='link']{cursor:pointer;transition:color var(--transition-base)}.marketplace-grid .product-brand[role='link']:hover{color:var(--text-primary)}.marketplace-grid .product-brand[role='link']:focus-visible{outline:2px solid rgba(56,189,248,0.55);outline-offset:2px}.product-brand-label{display:inline-flex;align-items:center;gap:0.35rem}.brand-verified-badge{display:inline-flex;align-items:center;justify-content:center;width:1rem;height:1rem;border-radius:var(--radius-full);background:linear-gradient(135deg,rgba(56,189,248,0.85),rgba(192,38,211,0.85));color:#0f172a;box-shadow:0 6px 16px rgba(15,23,42,0.35)}.brand-verified-badge svg{width:0.6rem;height:0.6rem}.marketplace-grid .product-price{font-weight:600;font-size:1rem}.product-meta{display:flex;justify-content:space-between;align-items:center;gap:0.5rem;color:var(--text-secondary);font-size:0.85rem;text-transform:uppercase;letter-spacing:0.04em}.product-description{flex:1}.product-footer{display:flex;justify-content:space-between;align-items:flex-end;gap:1rem}.price{font-weight:700;font-size:1.1rem}.product-owner{font-size:0.85rem;color:var(--text-secondary)}.product-tags{display:flex;flex-wrap:wrap;gap:0.5rem;justify-content:flex-end}.product-tags .tag{font-size:0.75rem;padding:0.25rem 0.6rem;border-radius:999px;background:rgba(148,163,184,0.16);border:1px solid rgba(148,163,184,0.25);color:var(--text-secondary);letter-spacing:0.03em;text-transform:uppercase}.progress-indicator{display:flex;flex-direction:column;gap:0.4rem;min-width:160px;margin-top:0.75rem}.progress-label,.progress-deadline{font-size:0.85rem;color:var(--text-secondary)}.progress-track{position:relative;height:6px;background:rgba(148,163,184,0.25);border-radius:999px;overflow:hidden}.progress-fill{position:absolute;left:0;top:0;bottom:0;width:var(--progress);background:var(--accent-primary);border-radius:999px;transition:width 320ms ease}.section-header{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:1.5rem}.section-header p{color:var(--text-secondary);margin-top:0.5rem;max-width:520px}.marketplace-grid{gap:1.75rem}.catalog-tabs{display:flex;gap:0.75rem;flex-wrap:wrap;flex:1 1 auto;overflow-x:auto;scrollbar-width:none}.catalog-tabs::-webkit-scrollbar{display:none}.catalog-tab{border:1px solid transparent;border-radius:var(--radius-full);padding:0.45rem 1.25rem;font-weight:500;font-size:0.95rem;background:transparent;color:var(--text-secondary);transition:all 180ms ease;cursor:pointer}.catalog-tab:hover,.catalog-tab:focus{color:var(--text-primary);border-color:rgba(148,163,184,0.35);outline:none}.catalog-tab.is-active{background:linear-gradient(135deg,rgba(56,189,248,0.28),rgba(147,197,253,0.24));color:var(--text-primary);border-color:rgba(148,163,184,0.4);box-shadow:0 16px 32px rgba(30,64,175,0.18)}@media (max-width:960px){.catalog-tab-bar{grid-template-columns:minmax(0,1fr);padding:0.85rem}.catalog-search{justify-self:stretch;max-width:none}.catalog-tabs{row-gap:0.6rem}}@media (max-width:640px){.catalog-tab-bar{gap:0.75rem}.catalog-search-input{grid-template-columns:auto 1fr auto;padding:0.3rem 0.3rem 0.3rem 0.55rem}.catalog-search-button{width:2.4rem;height:2.4rem}}.catalog-panel{display:flex;flex-direction:column;gap:1.4rem;margin-top:1rem}.catalog-panel.is-hidden{display:none}.panel-description{color:var(--text-secondary);max-width:640px}.empty-state{font-size:0.9rem;color:var(--text-tertiary);margin:0}.product-availability{margin-top:0.75rem;font-size:0.85rem;color:var(--text-secondary)}.product-perfumer{font-size:0.85rem;color:var(--accent-primary);margin:0.35rem 0 0}.product-card.is-filtered-out{display:none}.sambatan{color:#facc15;background:rgba(250,204,21,0.16);border:1px solid rgba(250,204,21,0.4)}.sambatan-highlight{display:flex;flex-direction:column;gap:1.25rem}.highlight-header{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:1rem}.highlight-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1.5rem}.sambatan-card{padding:1.75rem;display:flex;flex-direction:column;gap:1rem}.card-header{display:flex;justify-content:space-between;align-items:center;font-size:0.85rem;color:var(--text-secondary)}.deadline{color:var(--accent-secondary)}.card-footer{margin-top:auto}.nusantarum{display:flex;flex-direction:column;gap:1.5rem}.nusantarum-header h2{margin-bottom:0.35rem}.story-carousel{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1.5rem}.story-card{padding:1.6rem;display:flex;flex-direction:column;gap:0.75rem}.story-meta{display:flex;justify-content:space-between;align-items:center;color:var(--text-secondary);font-size:0.85rem}.story-link{color:var(--accent-secondary);text-decoration:none;font-weight:600}.story-link:hover{text-decoration:underline}.community-cta{padding:2.5rem 2.75rem;display:flex;justify-content:space-between;align-items:center;gap:2rem;flex-wrap:wrap}.community-cta p{max-width:460px}.newsletter-form{display:flex;gap:0.75rem;flex-wrap:wrap}.newsletter-form input{background:rgba(15,23,42,0.7);border:1px solid rgba(148,163,184,0.4);border-radius:999px;padding:0.75rem 1.4rem;min-width:240px;color:var(--text-primary)}.newsletter-form input::placeholder{color:var(--muted)}.footer{padding:1.8rem 2.2rem;display:flex;flex-direction:column;gap:1.5rem}.footer .logo{font-family:"Playfair Display","Times New Roman",serif;font-size:1.25rem}.footer-top{display:flex;gap:2.25rem;align-items:flex-start;justify-content:space-between;flex-wrap:wrap}.footer-brand{max-width:260px;display:flex;flex-direction:column;gap:0.45rem}.footer-brand p{margin:0;font-size:0.9rem;line-height:1.5}.footer-nav{display:grid;gap:1.25rem 2.5rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));flex:1 1 320px}.footer-group{display:flex;flex-direction:column;gap:0.6rem}.footer-group h4{font-size:0.95rem;margin:0}.footer-group ul{list-style:none;padding:0;margin:0;display:grid;gap:0.35rem}.footer-group a{color:var(--text-secondary);text-decoration:none}.footer-group a:hover{color:var(--accent-secondary)}.footer-bottom{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.75rem 1.5rem;padding-top:0.85rem;border-top:1px solid rgba(148,163,184,0.2)}.footer-social{list-style:none;padding:0;margin:0;display:flex;gap:1.1rem}.footer-social a{color:var(--text-secondary);text-decoration:none;font-size:0.9rem}.footer-social a:hover{color:var(--accent-secondary)}.footer-note{color:var(--muted);font-size:0.85rem}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}@keyframes pulse{0%,100%{transform:scale(1);opacity:0.9}50%{transform:scale(1.05);opacity:0.6}}@media (max-width:1024px){.navbar{flex-wrap:wrap;align-items:flex-start;gap:0.75rem}.navbar-toggle{display:flex;margin-left:auto}.navbar-collapsible{display:none;position:relative;width:100%;margin-left:0;padding:1.5rem 1.25rem;background:rgba(15,23,42,0.92);border:1px solid rgba(148,163,184,0.25);border-radius:var(--radius-md);box-shadow:0 18px 40px rgba(8,15,29,0.4);flex-direction:column;gap:1.5rem}.navbar[data-open="true"] .navbar-collapsible{display:flex}.navbar-menu{width:100%;flex-direction:column;align-items:flex-start;gap:1.1rem}.navbar-menu li{width:100%}.navbar-menu a{width:100%;display:block;padding:0.35rem 0}.navbar-actions{width:100%;flex-direction:column;align-items:stretch;gap:0.75rem}.navbar-actions .navbar-dropdown{width:100%}.hero{padding:2.5rem}.marketplace-hero{grid-template-columns:1fr;padding:2.25rem}.hero-filters{order:2}.filter-form{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.section-header{align-items:stretch}.filter-bar{flex-direction:column;align-items:stretch}.product-footer{flex-direction:column;align-items:stretch}.progress-indicator{width:100%}.community-cta{padding:2rem}}@media (max-width:640px){.page-wrapper{padding:1.75rem 1rem 3rem}.hero{padding:2rem}.marketplace-hero{padding:1.75rem}.hero-filters{padding:1.5rem}.filter-form{grid-template-columns:1fr}.product-tags{justify-content:flex-start}.hero-stats{grid-template-columns:1fr}.catalog-header{padding:1.8rem}.tab-list{width:100%;justify-content:space-between}.search-box{width:100%}.newsletter-form{width:100%}.newsletter-form input,.newsletter-form .btn{width:100%}.footer{padding:1.6rem;gap:1.25rem}.footer-top{flex-direction:column;gap:1.5rem}.footer-bottom{flex-direction:column;align-items:flex-start;gap:0.65rem}.footer-social{gap:0.85rem}}.onboarding-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:2rem;padding:2.5rem;align-items:center}.hero-progress{display:flex;flex-direction:column;gap:1.4rem}.progress-bar{width:100%;height:10px;background:rgba(148,163,184,0.22);border-radius:999px;overflow:hidden}.progress-fill{width:0%;height:100%;background:var(--accent-primary);transition:width var(--transition-base)}.progress-steps{list-style:none;margin:0;padding:0;display:grid;gap:1rem}.progress-step{display:grid;grid-template-columns:auto 1fr;gap:1rem;align-items:center;padding:0.75rem 1rem;border-radius:var(--radius-md);background:rgba(15,23,42,0.45);border:1px solid transparent}.progress-step .step-index{display:grid;place-items:center;width:32px;height:32px;border-radius:999px;border:2px solid rgba(148,163,184,0.45);font-weight:600}.progress-step.active{border-color:rgba(56,189,248,0.5);background:rgba(56,189,248,0.08)}.progress-step.active .step-index{border-color:var(--accent-secondary);color:var(--accent-secondary)}.progress-step.completed{border-color:rgba(52,211,153,0.4);background:rgba(16,185,129,0.12)}.progress-step.completed .step-index{border-color:var(--success);color:var(--success)}.onboarding-layout{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:2.5rem}.onboarding-main{padding:2.25rem;display:flex;flex-direction:column;gap:1.75rem}.section-header{display:flex;align-items:center;justify-content:space-between;gap:1.5rem}.section-header h2{margin-bottom:0.35rem}.status-chip{display:inline-flex;align-items:center;padding:0.35rem 0.9rem;border-radius:999px;border:1px solid rgba(148,163,184,0.35);font-size:0.9rem;color:var(--text-secondary)}.feedback{min-height:1.75rem;border-radius:var(--radius-sm);padding:0.5rem 0.85rem;background:rgba(15,23,42,0.65);border:1px solid rgba(148,163,184,0.2);font-size:0.95rem;opacity:0;transition:opacity var(--transition-base)}.feedback.visible{opacity:1}.feedback[data-tone="success"]{border-color:rgba(52,211,153,0.45);color:var(--success)}.feedback[data-tone="danger"]{border-color:rgba(248,113,113,0.45);color:var(--danger)}.onboarding-forms{display:grid;gap:1.5rem}.onboarding-form{border:1px solid rgba(148,163,184,0.25);border-radius:var(--radius-md);padding:1.75rem;display:grid;gap:1.25rem;background:rgba(15,23,42,0.55)}.onboarding-form fieldset{border:none;padding:0;margin:0;display:grid;gap:1rem}.onboarding-form legend{font-weight:600;margin-bottom:0.5rem}.onboarding-form label{display:grid;gap:0.45rem;font-size:0.95rem}.onboarding-form input,.onboarding-form select{border-radius:var(--radius-sm);border:1px solid rgba(148,163,184,0.28);padding:0.65rem 0.85rem;background:rgba(15,23,42,0.75);color:var(--text-primary);font-size:1rem}.onboarding-form input:focus,.onboarding-form select:focus{outline:none;border-color:rgba(56,189,248,0.65);box-shadow:0 0 0 3px rgba(56,189,248,0.2)}.checkbox{display:flex !important;align-items:center;gap:0.6rem}.checkbox input[type="checkbox"]{width:18px;height:18px;accent-color:#f97316}.form-actions{display:flex;gap:0.75rem;flex-wrap:wrap}.form-help{font-size:0.9rem;color:rgba(203,213,225,0.8)}.debug-token{font-family:"Fira Code","Consolas",monospace;font-size:0.85rem;color:rgba(125,211,252,0.9)}.onboarding-sidebar{padding:2.25rem;display:grid;gap:1.5rem}.event-log{list-style:none;padding:0;margin:0;display:grid;gap:0.75rem}.event-log li{display:flex;align-items:center;justify-content:space-between;background:rgba(15,23,42,0.55);border-radius:var(--radius-sm);border:1px solid rgba(148,163,184,0.25);padding:0.6rem 0.9rem;font-size:0.9rem}.event-time{font-family:"Fira Code",monospace;color:rgba(148,163,184,0.85)}.event-name{font-weight:600;color:var(--text-primary)}.qa-notes{border-radius:var(--radius-md);border:1px dashed rgba(148,163,184,0.35);padding:1.25rem;background:rgba(15,23,42,0.45)}.qa-notes h3{margin-bottom:0.75rem}.qa-notes ul{margin:0;padding-left:1.1rem;color:rgba(203,213,225,0.85);display:grid;gap:0.35rem}@media (max-width:960px){.onboarding-layout{grid-template-columns:1fr}.section-header{flex-direction:column;align-items:flex-start}}@media (max-width:720px){.onboarding-hero{padding:2rem}.onboarding-form{padding:1.5rem}.sambatan-progress__meta{flex-direction:column;align-items:flex-start;gap:0.35rem}.slot-selector{grid-template-columns:repeat(3,minmax(2.4rem,auto))}}.marketplace-section{display:flex;flex-direction:column;gap:1.25rem;margin-top:1.75rem}.product-detail-page{padding:3rem 0 4rem;background:radial-gradient(circle at 10% 20%,rgba(56,189,248,0.18),transparent 55%),radial-gradient(circle at 80% 0%,rgba(99,102,241,0.28),transparent 52%),linear-gradient(180deg,rgba(15,23,42,0.95),rgba(15,23,42,0.9))}.product-detail-container{max-width:1100px;margin:0 auto;padding:0 1.5rem;display:flex;flex-direction:column;gap:2.5rem}.product-breadcrumb ol{list-style:none;display:flex;flex-wrap:wrap;gap:0.4rem;padding:0;margin:0;font-size:0.95rem;color:rgba(226,232,240,0.85)}.product-breadcrumb li{display:flex;align-items:center;gap:0.4rem}.product-breadcrumb li::after{content:"/";opacity:0.45}.product-breadcrumb li:last-child::after{content:none}.product-breadcrumb a{color:rgba(56,189,248,0.9);text-decoration:none}.product-breadcrumb a:hover{text-decoration:underline}.product-detail-hero{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,0.8fr);gap:2rem;padding:2.5rem;border-radius:var(--radius-xl,28px);background:rgba(15,23,42,0.55);backdrop-filter:blur(22px);border:1px solid rgba(148,163,184,0.25);position:relative}.product-hero-copy{display:grid;gap:1.1rem}.hero-kicker{display:inline-flex;align-self:flex-start;padding:0.35rem 0.85rem;border-radius:999px;background:rgba(56,189,248,0.12);border:1px solid rgba(56,189,248,0.35);font-size:0.9rem;letter-spacing:0.04em;text-transform:uppercase;color:rgba(125,211,252,0.95)}.product-detail-hero h1{margin:0;font-size:clamp(2.5rem,5vw,3.25rem);letter-spacing:-0.02em}.product-tagline{margin:0;font-size:1.05rem;color:rgba(203,213,225,0.8)}.product-summary{font-size:1.15rem;line-height:1.55;color:rgba(226,232,240,0.95)}.product-description{color:rgba(203,213,225,0.85);line-height:1.6}.product-pricing{display:flex;gap:0.85rem;align-items:baseline;font-weight:600}.product-pricing .price-primary{font-size:1.9rem;color:rgba(250,250,250,0.98)}.product-pricing .price-secondary{font-size:1rem;color:rgba(148,163,184,0.85)}.product-volume-options{display:flex;flex-wrap:wrap;gap:0.75rem}.volume-option{padding:0.75rem 1.1rem;border-radius:var(--radius-md);background:rgba(255,255,255,0.05);border:1px solid rgba(148,163,184,0.3);display:grid;gap:0.25rem}.volume-size{font-weight:600;color:rgba(248,250,252,0.95)}.volume-price{font-size:0.95rem;color:rgba(148,163,184,0.85)}.product-cta{width:fit-content;padding-inline:1.8rem;font-size:1rem}.origin-info{margin:0;font-size:0.95rem;color:rgba(148,163,184,0.85)}.product-hero-visual{position:relative;border-radius:var(--radius-lg);background:linear-gradient(160deg,rgba(56,189,248,0.35),rgba(99,102,241,0.35));border:1px solid rgba(148,163,184,0.2);overflow:hidden;display:grid;place-items:center}.product-visual-glow{width:240px;height:240px;border-radius:50%;background:radial-gradient(circle,rgba(191,219,254,0.65),rgba(59,130,246,0.15));filter:blur(0.5px)}.purchase-mode{display:grid;gap:1rem;padding:1.4rem 1.6rem;border-radius:var(--radius-md);border:1px solid rgba(148,163,184,0.25);background:rgba(15,23,42,0.5)}.purchase-mode--regular{background:linear-gradient(145deg,rgba(15,23,42,0.55),rgba(30,41,59,0.5))}.purchase-mode--sambatan{border-color:rgba(56,189,248,0.35);background:linear-gradient(150deg,rgba(30,64,175,0.55),rgba(56,189,248,0.3));box-shadow:0 18px 36px rgba(14,116,144,0.25)}.purchase-mode__header{display:grid;gap:0.6rem}.purchase-mode__header p{margin:0;color:rgba(226,232,240,0.9)}.purchase-mode__price{font-size:1.1rem;color:rgba(241,245,249,0.95)}.purchase-benefits{list-style:none;margin:0;padding:0;display:grid;gap:0.55rem;color:rgba(226,232,240,0.9)}.purchase-benefits li{display:flex;gap:0.55rem}.purchase-benefits li::before{content:"✶";color:rgba(56,189,248,0.8);margin-top:0.1rem}.purchase-footnote{margin:0;font-size:0.9rem;color:rgba(191,219,254,0.85)}.sambatan-progress{display:grid;gap:0.6rem}.sambatan-progress__bar{height:0.6rem;border-radius:var(--radius-full);background:rgba(15,23,42,0.6);border:1px solid rgba(148,163,184,0.25);overflow:hidden}.sambatan-progress__fill{display:block;height:100%;width:var(--progress,0%);border-radius:inherit;background:linear-gradient(90deg,rgba(56,189,248,0.9),rgba(129,140,248,0.9));transition:width 220ms ease}.sambatan-progress__meta{display:flex;justify-content:space-between;font-size:0.9rem;color:rgba(226,232,240,0.85)}.slot-selector{display:inline-grid;grid-template-columns:repeat(3,auto);gap:0.75rem;align-items:center}.slot-stepper{width:2.75rem;height:2.75rem;border-radius:var(--radius-full);border:1px solid rgba(148,163,184,0.35);background:rgba(15,23,42,0.6);color:rgba(241,245,249,0.9);font-size:1.4rem;line-height:1;display:grid;place-items:center;cursor:pointer;transition:transform var(--transition-base),border-color var(--transition-base)}.slot-stepper:hover,.slot-stepper:focus-visible{border-color:rgba(56,189,248,0.6);transform:translateY(-2px)}.slot-stepper:focus-visible{outline:2px solid rgba(56,189,248,0.45);outline-offset:2px}.slot-stepper:disabled{opacity:0.45;cursor:not-allowed;transform:none}.slot-input{width:4rem;text-align:center;padding:0.55rem 0.75rem;border-radius:var(--radius-sm);border:1px solid rgba(148,163,184,0.35);background:rgba(15,23,42,0.55);color:rgba(241,245,249,0.95);font-weight:600;font-size:1.05rem}.slot-input:focus{outline:none;border-color:rgba(56,189,248,0.65);box-shadow:0 0 0 2px rgba(56,189,248,0.2)}.slot-hint{margin:0;font-size:0.9rem;color:rgba(191,219,254,0.85)}.product-visual-rimba{background:linear-gradient(155deg,rgba(12,74,110,0.75),rgba(30,64,175,0.55))}.product-visual-sunset{background:linear-gradient(160deg,rgba(244,114,182,0.75),rgba(251,191,36,0.45))}.product-visual-lagoon{background:linear-gradient(160deg,rgba(56,189,248,0.75),rgba(14,116,144,0.55))}.product-visual-scale{background:linear-gradient(160deg,rgba(56,189,248,0.6),rgba(148,163,184,0.45))}.product-detail-grid{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:1.75rem}.scent-profile header h2,.experience-section header h2,.recommendations header h2,.featured-categories h2{margin-top:0;margin-bottom:0.75rem}.scent-columns{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1.5rem}.scent-column h3{font-size:1.05rem;color:rgba(191,219,254,0.95)}.scent-column ul{list-style:none;margin:0;padding:0;display:grid;gap:0.35rem;color:rgba(226,232,240,0.85)}.experience-list{list-style:none;margin:0 0 1.5rem;padding:0;display:grid;gap:0.75rem;color:rgba(226,232,240,0.9)}.experience-list li{display:flex;gap:0.5rem;align-items:flex-start}.experience-list li::before{content:"✶";color:rgba(56,189,248,0.85);margin-top:0.1rem}.production-meta{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;padding-top:1rem;border-top:1px solid rgba(148,163,184,0.25)}.production-meta .meta-label{font-size:0.85rem;text-transform:uppercase;letter-spacing:0.08em;color:rgba(148,163,184,0.7)}.production-meta strong{font-size:1.1rem;color:rgba(250,250,250,0.95)}.featured-categories{display:grid;gap:1.2rem}ul.category-chips{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:0.6rem}.category-chip{padding:0.5rem 1rem;border-radius:999px;border:1px solid rgba(148,163,184,0.35);background:rgba(255,255,255,0.05);font-size:0.9rem;color:rgba(203,213,225,0.85)}.featured-summary p{margin:0;color:rgba(203,213,225,0.85);line-height:1.55}.recommendations{display:grid;gap:1.5rem}.recommendation-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1.25rem}.recommendation-card{display:grid;gap:1rem;padding:1.25rem;border-radius:var(--radius-md);background:rgba(15,23,42,0.55);border:1px solid rgba(148,163,184,0.25)}.recommendation-visual{height:140px;border-radius:var(--radius-md);background:rgba(51,65,85,0.65);border:1px solid rgba(148,163,184,0.2)}.recommendation-body{display:grid;gap:0.45rem}.recommendation-body h3{margin:0}.recommendation-price{font-weight:600;color:rgba(248,250,252,0.95)}.product-detail-link{margin-left:auto}@media (max-width:1024px){.product-detail-hero{grid-template-columns:1fr}.product-detail-grid{grid-template-columns:1fr}}@media (max-width:720px){.product-detail-page{padding:2.5rem 0 3rem}.product-detail-hero{padding:2rem}.product-volume-options{flex-direction:column;align-items:stretch}.product-breadcrumb ol{font-size:0.85rem}} \ No newline at end of file diff --git a/src/app/web/static/css/brand.min.css b/src/app/web/static/css/brand.min.css new file mode 100644 index 0000000..c8e8ca4 --- /dev/null +++ b/src/app/web/static/css/brand.min.css @@ -0,0 +1 @@ +.brand-page{display:grid;gap:3rem;padding-bottom:4rem}.button{display:inline-flex;align-items:center;justify-content:center;padding:0.75rem 1.5rem;border-radius:999px;border:1px solid rgba(255,255,255,0.35);background:rgba(255,255,255,0.12);color:inherit;font-weight:600;text-decoration:none;transition:background 0.2s ease,transform 0.2s ease}.button:hover{background:rgba(255,255,255,0.2);transform:translateY(-1px)}.button-secondary{background:linear-gradient(135deg,rgba(255,255,255,0.15),rgba(255,255,255,0.05))}.button-ghost{background:transparent;border-style:dashed}.button-small{padding:0.5rem 1rem;font-size:0.9rem}.brand-hero{display:grid;grid-template-columns:minmax(240px,1fr) 1.4fr;gap:2rem;padding:2.5rem;align-items:center}.brand-hero__branding{display:flex;align-items:center;gap:1.25rem;flex-wrap:wrap;margin-bottom:1.5rem}.brand-logo{width:104px;height:104px;border-radius:28px;object-fit:cover;background:rgba(15,23,42,0.6);border:1px solid rgba(255,255,255,0.2);box-shadow:0 20px 35px rgba(15,23,42,0.35)}.brand-logo--placeholder{display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:2.1rem;letter-spacing:0.08em;width:104px;height:104px;border-radius:28px;background:linear-gradient(135deg,rgba(59,130,246,0.32),rgba(14,165,233,0.42));color:#e0f2fe;border:1px solid rgba(14,165,233,0.45);text-transform:uppercase}.brand-logo-upload{display:grid;gap:0.4rem}.brand-logo-upload__trigger{display:inline-flex;align-items:center;gap:0.5rem;padding:0.6rem 1.2rem;border-radius:999px;border:1px dashed rgba(148,163,184,0.55);background:rgba(15,23,42,0.45);font-weight:600;cursor:pointer}.brand-logo-upload__trigger:hover{background:rgba(30,41,59,0.7);border-color:rgba(148,163,184,0.8)}.brand-logo-upload__trigger input{display:none}.brand-logo-upload__hint{margin:0;font-size:0.75rem;color:rgba(226,232,240,0.75)}.brand-hero__media{width:100%;padding-top:75%;background-size:cover;background-position:center;border-radius:1.5rem;position:relative}.brand-hero__content h1{font-family:"Playfair Display",serif;font-size:clamp(2.4rem,3vw,3.6rem);margin:0}.brand-title-group{display:flex;align-items:center;flex-wrap:wrap;gap:0.75rem;margin-bottom:0.75rem}.brand-tagline{font-size:1.15rem;font-weight:500;margin-bottom:0.75rem}.brand-description{display:grid;gap:0.35rem;margin-bottom:1.5rem}.brand-description h2{font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;color:rgba(255,255,255,0.65);margin:0}.brand-description p{margin:0;line-height:1.6}.brand-meta{display:grid;gap:0.5rem;margin:1.5rem 0}.brand-meta dt{font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;color:rgba(255,255,255,0.65)}.brand-meta dd{margin:0.25rem 0 0;font-weight:500;display:flex;flex-wrap:wrap;gap:0.5rem}.badge.verified{color:#4ade80;background:rgba(74,222,128,0.18);border-color:rgba(34,197,94,0.45)}.chip{display:inline-flex;align-items:center;gap:0.35rem;padding:0.35rem 0.75rem;border-radius:999px;background:rgba(148,163,184,0.16);border:1px solid rgba(148,163,184,0.35);font-size:0.85rem;font-weight:500}.brand-form-page{padding:3rem 0 5rem;display:flex;justify-content:center}.brand-form-shell{width:min(1120px,100%);padding:2.5rem clamp(1.5rem,4vw,3rem);display:grid;gap:2.5rem}.brand-form-header{display:grid;gap:1rem}.brand-form-header__top{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:1rem}.brand-form-back{color:rgba(148,163,184,0.9);text-decoration:none;font-weight:500;transition:color 0.2s ease}.brand-form-back:hover{color:#f8fafc}.brand-form-chip{padding:0.25rem 0.75rem;border-radius:999px;border:1px solid rgba(148,163,184,0.4);background:rgba(148,163,184,0.12);font-size:0.75rem;letter-spacing:0.08em;text-transform:uppercase}.brand-form-header h1{margin:0;font-size:clamp(2rem,3vw,2.75rem)}.brand-form-header p{margin:0;color:rgba(226,232,240,0.82);line-height:1.7}.brand-form-progress{display:flex;gap:0.5rem;list-style:none;padding:0;margin:0;flex-wrap:wrap}.brand-form-progress__item{padding:0.35rem 0.75rem;border-radius:999px;border:1px dashed rgba(148,163,184,0.4);font-size:0.8rem;text-transform:uppercase;letter-spacing:0.08em;color:rgba(148,163,184,0.8)}.brand-form-progress__item--active{border-style:solid;background:rgba(59,130,246,0.18);border-color:rgba(59,130,246,0.45);color:#bfdbfe}.brand-form-errors{background:rgba(248,113,113,0.08);border:1px solid rgba(248,113,113,0.35);border-radius:1rem;padding:1.5rem;display:grid;gap:0.75rem}.brand-form-errors h2{margin:0;font-size:1.1rem;color:#fecaca}.brand-form-errors ul{margin:0;padding-left:1.25rem;color:#fecaca;display:grid;gap:0.35rem}.brand-form{display:grid;gap:2rem}.brand-form-layout{display:grid;grid-template-columns:minmax(0,2fr) minmax(260px,0.8fr);gap:clamp(2rem,4vw,3rem)}.brand-form-main{display:grid;gap:1.75rem}.brand-form-section{border:1px solid rgba(148,163,184,0.25);border-radius:1.5rem;padding:clamp(1.5rem,3vw,2rem);background:linear-gradient(135deg,rgba(15,23,42,0.65),rgba(15,23,42,0.5));display:grid;gap:1.5rem}.brand-form-section legend{font-size:1.2rem;font-weight:600;letter-spacing:-0.01em}.brand-form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1.25rem 1.5rem}.brand-form-field{display:grid;gap:0.5rem}.brand-form-field span{font-weight:500}.brand-form-field input,.brand-form-field textarea,.brand-form-field select{width:100%;padding:0.75rem 1rem;border-radius:0.85rem;border:1px solid rgba(148,163,184,0.25);background:rgba(15,23,42,0.75);color:inherit;font:inherit;transition:border-color 0.2s ease,background 0.2s ease}.brand-form-field input:focus,.brand-form-field textarea:focus,.brand-form-field select:focus{outline:none;border-color:rgba(59,130,246,0.55);background:rgba(30,41,59,0.85)}.brand-form-full{grid-column:1 / -1}.brand-form-hint{margin:0;font-size:0.8rem;color:rgba(148,163,184,0.75)}.brand-form-checkbox{align-items:center;grid-template-columns:auto 1fr;gap:0.75rem}.brand-form-checkbox input{width:auto;transform:scale(1.1)}.brand-form-intro{margin:0;color:rgba(226,232,240,0.78)}.brand-member-list{display:grid;gap:1.25rem}.brand-member-card{position:relative;border:1px solid rgba(148,163,184,0.25);border-radius:1.25rem;padding:1.5rem;background:rgba(15,23,42,0.6);display:grid;gap:1rem}.brand-member-card legend{font-weight:600;font-size:1rem}.brand-member-remove{position:absolute;top:1rem;right:1rem;opacity:0.8}.brand-member-remove--hidden{visibility:hidden}.brand-member-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem 1.25rem}.brand-member-actions{display:flex;align-items:center;gap:1rem;flex-wrap:wrap}.brand-form-sidebar{display:grid;gap:1.5rem}.brand-form-card{border:1px solid rgba(148,163,184,0.2);border-radius:1.25rem;padding:clamp(1.25rem,3vw,1.75rem);background:rgba(15,23,42,0.55);display:grid;gap:0.75rem}.brand-form-card h3{margin:0;font-size:1.05rem}.brand-form-card p{margin:0;color:rgba(226,232,240,0.75)}.brand-form-card--tips ul{margin:0;padding-left:1.25rem;display:grid;gap:0.45rem;color:rgba(226,232,240,0.8)}.brand-form-sidebar-actions{display:grid;gap:0.5rem}.brand-form-actions{display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap}.brand-form-actions .button{min-width:180px}.input-error{border-color:rgba(248,113,113,0.8) !important;background:rgba(248,113,113,0.12) !important}@media (max-width:1024px){.brand-form-layout{grid-template-columns:1fr}.brand-form-sidebar{order:-1}}@media (max-width:720px){.brand-form-grid,.brand-member-grid{grid-template-columns:1fr}.brand-form-actions{flex-direction:column;align-items:stretch}.brand-form-actions .button{width:100%}}.chip-owner{background:rgba(59,130,246,0.14);border-color:rgba(59,130,246,0.45);color:#bfdbfe}.chip-muted{background:rgba(148,163,184,0.1);color:rgba(226,232,240,0.9)}.brand-cta{display:flex;gap:1rem;flex-wrap:wrap}.brand-section{display:grid;gap:1.5rem}.section-header h2{font-size:2rem;margin-bottom:0.5rem}.brand-story-list{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.brand-story-item{position:relative;padding:1.75rem}.story-index{font-family:"Playfair Display",serif;font-size:1.5rem;color:rgba(255,255,255,0.4);position:absolute;top:1.25rem;right:1.5rem}.brand-product-grid{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.brand-product-card{padding:1.75rem;display:grid;gap:0.75rem}.brand-product-card__header{display:flex;justify-content:space-between;align-items:baseline}.brand-product-card__note{color:rgba(255,255,255,0.8)}.brand-product-card__price{font-weight:600;font-size:1.1rem}.brand-product-card__availability{font-size:0.95rem;color:rgba(255,255,255,0.7)}.brand-product-card__actions{margin-top:0.5rem;display:flex}.brand-product-card__actions .button{flex:1}.brand-product-card__actions .button:focus-visible{outline:2px solid rgba(14,165,233,0.65);outline-offset:3px}.brand-collaboration .team-column{display:grid;gap:1rem}.brand-team-grid{display:grid;gap:2rem;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.team-list{list-style:none;display:grid;gap:1rem;padding:0;margin:0}.team-card{display:grid;grid-template-columns:64px 1fr;gap:1rem;align-items:center;padding:1rem}.team-card img{width:64px;height:64px;object-fit:cover;border-radius:50%}.team-card--pending{border:1px dashed rgba(255,255,255,0.3)}.team-name{font-weight:600}.team-role{text-transform:capitalize;font-size:0.9rem;color:rgba(255,255,255,0.7)}.team-expertise{font-size:0.9rem;margin-top:0.25rem}.team-invited{font-size:0.8rem;color:rgba(255,255,255,0.6);margin-top:0.25rem}.team-actions{display:flex;gap:0.5rem;grid-column:1 / -1}.team-actions .button-small{font-size:0.85rem;padding:0.35rem 0.75rem}.invite-form{display:grid;gap:1rem;padding:1.5rem}.invite-form label{display:grid;gap:0.5rem;font-size:0.9rem}.invite-form input,.invite-form textarea{padding:0.75rem;border-radius:0.75rem;border:1px solid rgba(255,255,255,0.2);background:rgba(15,18,41,0.35);color:inherit}.brand-highlight-grid{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}.highlight-card{padding:1.75rem;display:grid;gap:0.75rem}.highlight-timestamp{font-size:0.85rem;text-transform:uppercase;letter-spacing:0.08em;color:rgba(255,255,255,0.6)}.brand-directory{display:grid;gap:2.5rem;padding-bottom:4rem}.brand-directory-grid{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.brand-directory-card{padding:2rem;display:grid;gap:1rem}.brand-directory-card__header{display:flex;align-items:center;justify-content:flex-start}.brand-directory-logo{width:72px;height:72px;border-radius:1.25rem;object-fit:cover;background:rgba(15,23,42,0.55);border:1px solid rgba(255,255,255,0.15)}.brand-directory-logo--placeholder{display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:1.5rem;width:72px;height:72px;border-radius:1.25rem;background:linear-gradient(135deg,rgba(59,130,246,0.32),rgba(14,165,233,0.42));color:#e0f2fe;text-transform:uppercase;letter-spacing:0.06em}.brand-directory-card__meta{display:flex;flex-direction:column;gap:0.5rem}.brand-directory-card__meta h2{font-family:"Playfair Display",serif;font-size:1.75rem;margin:0}.brand-directory-meta-extra{display:flex;flex-wrap:wrap;gap:0.75rem;font-size:0.95rem;color:rgba(226,232,240,0.85)}.brand-directory-meta-extra p{margin:0}.brand-directory-tagline{font-weight:500;margin-top:0.5rem}.brand-directory-card .badge.verified{align-self:flex-start}.brand-directory-location{font-size:0.9rem;color:rgba(255,255,255,0.6)}.brand-directory-summary{font-size:0.95rem;line-height:1.6}.brand-directory-focus{display:flex;gap:0.5rem;flex-wrap:wrap}@media (max-width:960px){.brand-hero{grid-template-columns:1fr}.brand-hero__media{padding-top:55%}}@media (max-width:640px){.brand-page{gap:2rem}.brand-hero{padding:1.75rem}.brand-product-grid,.brand-team-grid,.brand-highlight-grid,.brand-directory-grid{grid-template-columns:1fr}.team-card{grid-template-columns:48px 1fr}} \ No newline at end of file diff --git a/src/app/web/static/css/dashboard.min.css b/src/app/web/static/css/dashboard.min.css new file mode 100644 index 0000000..0ff27cd --- /dev/null +++ b/src/app/web/static/css/dashboard.min.css @@ -0,0 +1 @@ +.dashboard-hero{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2.5rem;padding:2.75rem;align-items:center}.hero-badge{display:inline-flex;align-items:center;padding:0.35rem 0.9rem;border-radius:999px;background:rgba(148,163,184,0.18);border:1px solid rgba(148,163,184,0.45);font-size:0.85rem;letter-spacing:0.02em}.hero-main h1{font-size:clamp(2rem,4vw,2.75rem);margin:0.75rem 0 0.5rem}.hero-main p{font-size:1.05rem;max-width:36ch}.hero-meta{display:flex;flex-wrap:wrap;gap:0.75rem;margin:1.35rem 0;font-size:0.95rem;color:var(--text-secondary)}.hero-meta span{display:inline-flex;align-items:center;gap:0.4rem;padding:0.4rem 0.9rem;border-radius:999px;background:rgba(148,163,184,0.12);border:1px solid rgba(148,163,184,0.25)}.hero-actions{display:flex;flex-wrap:wrap;gap:0.85rem}.hero-kpis{align-self:stretch}.kpi-grid{list-style:none;display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1.25rem;padding:0;margin:0}.kpi-card{padding:1.35rem 1.5rem}.kpi-label{font-size:0.95rem;color:var(--text-secondary);margin:0 0 0.5rem}.kpi-value{font-size:1.65rem;font-weight:600;margin:0 0 0.35rem}.kpi-delta{font-size:0.9rem;margin:0}.kpi-delta.positive{color:#4ade80}.kpi-delta.negative{color:#f97316}.dashboard-overview{padding:2.25rem;display:grid;gap:2rem}.section-heading{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:1.5rem}.section-heading h2{margin:0 0 0.35rem;font-size:1.8rem}.section-heading p{margin:0;max-width:48ch}.section-tools{display:flex;gap:0.75rem;flex-wrap:wrap}.overview-content{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,0.9fr);gap:2rem}.status-pills{display:flex;flex-wrap:wrap;gap:1rem}.status-pill{min-width:140px;padding:1rem 1.25rem;border-radius:var(--radius-md);border:1px solid rgba(148,163,184,0.25);background:rgba(15,23,42,0.6);display:grid;gap:0.25rem}.status-count{font-size:1.6rem;font-weight:600}.status-label{font-size:0.95rem;color:var(--text-secondary)}.status-pill.tone-info{border-color:rgba(56,189,248,0.45)}.status-pill.tone-primary{border-color:rgba(129,140,248,0.55)}.status-pill.tone-success{border-color:rgba(74,222,128,0.55)}.status-pill.tone-warning{border-color:rgba(250,204,21,0.55)}.notification-panel{display:grid;gap:1.2rem}.notification-panel h3{margin:0}.notification-list{list-style:none;display:grid;gap:1rem;margin:0;padding:0}.notification-item{padding:1.25rem 1.5rem;display:grid;gap:0.75rem}.notification-empty{font-size:0.9rem;color:var(--text-secondary);margin:0}.notification-header{display:flex;justify-content:space-between;font-size:0.9rem;color:var(--text-secondary)}.notification-item p{margin:0}.dashboard-management{display:grid;gap:2rem}.management-card{padding:2.25rem;display:grid;gap:1.5rem}.management-controls{display:flex;flex-wrap:wrap;gap:1.5rem;justify-content:space-between;align-items:center}.filter-chips{display:flex;flex-wrap:wrap;gap:0.65rem}.chip{display:inline-flex;align-items:center;justify-content:center;padding:0.4rem 0.95rem;border-radius:999px;border:1px solid rgba(148,163,184,0.35);background:rgba(148,163,184,0.12);color:var(--text-secondary);font-size:0.9rem;font-weight:500;cursor:pointer;transition:background var(--transition-base),color var(--transition-base),border-color var(--transition-base),transform var(--transition-base)}.chip:hover,.chip:focus-visible{border-color:rgba(96,165,250,0.7);color:var(--text-primary);transform:translateY(-1px)}.chip.is-active{background:var(--accent-primary);color:#0f172a;border-color:transparent;box-shadow:0 12px 24px rgba(192,38,211,0.25)}.search-field{display:grid;gap:0.35rem;font-size:0.9rem}.search-field input{background:rgba(15,23,42,0.7);border:1px solid rgba(148,163,184,0.35);border-radius:var(--radius-sm);padding:0.65rem 0.9rem;color:var(--text-primary)}.search-field input:focus{outline:2px solid rgba(96,165,250,0.6);outline-offset:2px}.table-wrapper{overflow-x:auto;border-radius:var(--radius-md)}.data-table{width:100%;border-collapse:collapse;min-width:680px}.data-table th,.data-table td{padding:0.9rem 1.1rem;text-align:left;border-bottom:1px solid rgba(148,163,184,0.2)}.data-table thead th{font-size:0.85rem;letter-spacing:0.02em;text-transform:uppercase;color:var(--text-secondary)}.data-table tbody tr:hover{background:rgba(15,23,42,0.55)}.table-primary{display:grid;gap:0.2rem}.primary-text{font-weight:600}.secondary-text{font-size:0.85rem;color:var(--text-secondary)}.status-badge{display:inline-flex;align-items:center;padding:0.35rem 0.75rem;border-radius:999px;font-size:0.8rem;border:1px solid rgba(148,163,184,0.3);background:rgba(148,163,184,0.12);text-transform:capitalize}.status-aktif{color:#4ade80;border-color:rgba(74,222,128,0.5)}.status-baru{color:#facc15;border-color:rgba(250,204,21,0.55)}.status-diproses{color:#38bdf8;border-color:rgba(56,189,248,0.55)}.status-dikirim{color:#a5b4fc;border-color:rgba(129,140,248,0.55)}.status-draft{color:#facc15;border-color:rgba(250,204,21,0.55)}.status-pending{color:#f97316;border-color:rgba(249,115,22,0.55)}.status-arsip{color:#a5b4fc;border-color:rgba(129,140,248,0.55)}.status-perlu-perhatian{color:#f97316;border-color:rgba(249,115,22,0.55)}.status-lengkap{color:#4ade80;border-color:rgba(74,222,128,0.55)}.status-perlu-revisi{color:#f97316;border-color:rgba(249,115,22,0.55)}.status-opsional{color:#a5b4fc;border-color:rgba(129,140,248,0.5)}.analytics-section{padding:2.5rem;display:grid;gap:1.75rem}.analytics-panel{display:grid;gap:1.5rem;padding:1.75rem;border-radius:var(--radius-lg);background:rgba(15,23,42,0.6);border:1px solid rgba(148,163,184,0.2)}.analytics-panel.is-hidden{display:none}.analytics-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem}.summary-label{font-size:0.85rem;color:var(--text-secondary)}.summary-value{font-size:1.4rem;font-weight:600}.analytics-chart{position:relative;height:220px;border-radius:var(--radius-md);overflow:hidden;background:linear-gradient(145deg,rgba(15,23,42,0.65),rgba(15,23,42,0.45));border:1px solid rgba(148,163,184,0.2)}.analytics-chart svg{width:100%;height:100%}.chart-area{stroke:none;opacity:0.55}.chart-line{fill:none;stroke:#38bdf8;stroke-width:2}.chart-label{position:absolute;bottom:6px;transform:translateX(-50%);font-size:0.7rem;color:var(--text-secondary);pointer-events:none}.analytics-details{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1.5rem}.top-products ul,.customer-segments ul{list-style:none;margin:0;padding:0;display:grid;gap:0.75rem}.top-products li,.customer-segments li{display:flex;justify-content:space-between;font-size:0.95rem}.promotion-section{display:grid;gap:1.75rem;padding:2.25rem}.promotion-grid{display:grid;gap:1.5rem;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}.promotion-card{display:grid;gap:1rem;padding:1.5rem}.promotion-meta{display:flex;justify-content:space-between;font-size:0.85rem;color:var(--text-secondary)}.progress-track{position:relative;width:100%;height:10px;border-radius:999px;background:rgba(148,163,184,0.15);overflow:hidden}.progress-fill{position:absolute;inset:0;width:var(--progress);background:var(--accent-primary);border-radius:inherit}.promotion-actions{display:flex;gap:0.5rem;flex-wrap:wrap}.verification-section{display:grid;gap:2rem;grid-template-columns:minmax(0,1.15fr) minmax(0,0.85fr)}.verification-steps{padding:2.5rem;display:grid;gap:1.5rem}.step-list{list-style:none;display:grid;gap:1rem;margin:0;padding:0}.step-item{display:grid;grid-template-columns:auto 1fr;gap:1rem;align-items:start;padding:1.1rem 1.35rem;border-radius:var(--radius-md);border:1px solid rgba(148,163,184,0.2);background:rgba(15,23,42,0.55)}.step-icon{width:14px;height:14px;border-radius:50%;border:3px solid rgba(148,163,184,0.5);margin-top:0.4rem}.step-selesai .step-icon{border-color:#4ade80;background:rgba(74,222,128,0.35)}.step-revisi .step-icon{border-color:#f97316;background:rgba(249,115,22,0.3)}.step-menunggu .step-icon{border-color:rgba(148,163,184,0.4)}.verification-aside{padding:2rem;display:grid;gap:1.5rem}.document-list{list-style:none;margin:0;padding:0;display:grid;gap:1rem}.document-list li{display:flex;justify-content:space-between;align-items:flex-start;gap:1.25rem}.document-name{font-weight:600}.document-note{display:block;font-size:0.85rem;color:var(--text-secondary);margin-top:0.35rem}.timeline-heading{margin:0}.timeline{list-style:none;margin:0;padding:0;display:grid;gap:1.1rem}.timeline li{display:grid;grid-template-columns:auto 1fr;gap:1rem}.timeline-date{font-size:0.85rem;color:var(--text-secondary)}.timeline-actor{font-weight:600}.team-section{display:grid;gap:2rem;padding:2.5rem}.team-layout{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,0.8fr);gap:2rem}.team-members table{min-width:720px}.permission-list{list-style:none;margin:0;padding:0;display:grid;gap:0.35rem}.team-sidebar{display:grid;gap:1.5rem}.invitation-card ul,.activity-card ul{list-style:none;margin:0;padding:0;display:grid;gap:1rem}.invitation-card li,.activity-card li{display:grid;gap:0.5rem;padding-bottom:0.75rem;border-bottom:1px solid rgba(148,163,184,0.15)}.invitation-meta{font-size:0.85rem;color:var(--text-secondary);display:flex;gap:0.75rem}.invitation-actions{display:flex;gap:0.5rem}.activity-time{font-size:0.85rem;color:var(--text-secondary)}.activity-actor{font-weight:600}@media (max-width:1024px){.dashboard-hero{grid-template-columns:1fr}.overview-content,.verification-section,.team-layout{grid-template-columns:1fr}.analytics-chart{height:200px}}@media (max-width:768px){.dashboard-hero,.dashboard-overview,.management-card,.analytics-section,.promotion-section,.team-section{padding:1.75rem}.status-pills{gap:0.75rem}.data-table{min-width:540px}.analytics-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (max-width:600px){.section-heading{flex-direction:column;align-items:flex-start}.hero-actions{flex-direction:column;align-items:stretch}.chip{width:fit-content}} \ No newline at end of file diff --git a/src/app/web/static/css/moderation-dashboard.min.css b/src/app/web/static/css/moderation-dashboard.min.css new file mode 100644 index 0000000..4c74d66 --- /dev/null +++ b/src/app/web/static/css/moderation-dashboard.min.css @@ -0,0 +1 @@ +.mod-hero{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2.5rem;padding:2.75rem;align-items:start}.mod-hero__main h1{font-size:clamp(2rem,4vw,2.75rem);margin:0.5rem 0 0.75rem}.mod-hero__main .mission{font-size:1.05rem;margin:0 0 1rem;max-width:46ch}.role-badge{display:inline-flex;align-items:center;padding:0.4rem 1rem;border-radius:999px;background:rgba(148,163,184,0.2);border:1px solid rgba(148,163,184,0.4);font-size:0.85rem;letter-spacing:0.03em}.hero-meta{display:grid;gap:0.75rem;margin:1.5rem 0 1.25rem;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.hero-meta dt{font-size:0.8rem;text-transform:uppercase;letter-spacing:0.08em;color:rgba(203,213,225,0.8);margin-bottom:0.25rem}.hero-meta dd{margin:0;font-size:0.95rem}.hero-actions{display:flex;flex-wrap:wrap;gap:0.85rem}.acknowledgement{margin-top:1.25rem;font-size:0.95rem;color:var(--text-secondary)}.mod-section-tabs{margin:2.5rem 0 1.5rem;padding:1.25rem 2.5rem 1rem;display:block}.mod-section-tabs .tab-bar{display:flex;align-items:center;gap:1.25rem}.mod-section-tabs .tab-scroller{position:relative;flex:1 1 auto;overflow-x:auto;padding-bottom:0.35rem;scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;mask-image:linear-gradient(90deg,rgba(0,0,0,0) 0%,#000 6%,#000 94%,rgba(0,0,0,0) 100%)}.mod-section-tabs .tab-scroller::-webkit-scrollbar{display:none}.mod-section-tabs .tab-scroller{-ms-overflow-style:none;scrollbar-width:none}.mod-section-tabs .tab-list{list-style:none;margin:0;padding:0;display:flex;gap:0.65rem;min-width:max-content}.mod-section-tabs .tab-list__item{flex:0 0 auto}.mod-section-tabs .tab-link{display:grid;gap:0.15rem;padding:0.7rem 1.3rem;border-radius:var(--radius-md);border:1px solid transparent;background:rgba(15,23,42,0.25);color:var(--text-secondary);cursor:pointer;text-align:left;min-width:130px;transition:background 0.2s ease,color 0.2s ease,border-color 0.2s ease,box-shadow 0.2s ease;scroll-snap-align:center;text-decoration:none}.mod-section-tabs .tab-link .tab-label{font-size:0.95rem;font-weight:600;color:inherit}.mod-section-tabs .tab-link .tab-subtitle{font-size:0.7rem;text-transform:uppercase;letter-spacing:0.12em;color:rgba(148,163,184,0.85)}.mod-section-tabs .tab-link:is(:hover,:focus-visible){background:rgba(59,130,246,0.18);color:var(--text-primary);border-color:rgba(59,130,246,0.25);box-shadow:0 1px 0 rgba(148,163,184,0.2)}.mod-section-tabs .tab-link.is-active{color:var(--text-primary);background:rgba(59,130,246,0.22);border-color:rgba(59,130,246,0.35);box-shadow:0 10px 30px rgba(59,130,246,0.2)}.mod-section-tabs .tab-link.is-active .tab-subtitle{color:rgba(226,232,240,0.9)}.mod-section-tabs .tab-indicator{position:absolute;left:0;bottom:0;height:3px;width:0;border-radius:999px;background:linear-gradient(90deg,rgba(191,219,254,0.65) 0%,rgba(59,130,246,0.95) 100%);opacity:0;transition:transform 0.28s ease,width 0.28s ease,opacity 0.18s ease}.mod-section-tabs .tab-indicator.is-visible{opacity:1}.mod-section-tabs .tab-actions{flex:0 0 auto}.mod-section-tabs .tab-actions .btn{white-space:nowrap}.mod-hero__kpis{align-self:stretch}.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1.25rem;list-style:none;padding:0;margin:0}.kpi-card{padding:1.4rem 1.5rem}.kpi-card .kpi-label{margin:0;color:var(--text-secondary);font-size:0.95rem}.kpi-card .kpi-value{margin:0.5rem 0 0.35rem;font-size:1.65rem;font-weight:600}.kpi-card .kpi-delta{margin:0;font-size:0.9rem}.kpi-card.tone-success{border-left:3px solid rgba(52,211,153,0.8)}.kpi-card.tone-warning{border-left:3px solid rgba(250,204,21,0.75)}.kpi-card.tone-info{border-left:3px solid rgba(56,189,248,0.75)}.kpi-card.tone-positive{border-left:3px solid rgba(129,140,248,0.75)}.mod-overview{padding:2.5rem;display:grid;gap:2.25rem}.overview-grid{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,0.95fr);gap:2.25rem}.summary-pills{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.summary-pill{padding:1.1rem 1.35rem;border-radius:var(--radius-md);border:1px solid rgba(148,163,184,0.25);background:rgba(15,23,42,0.6);display:grid;gap:0.2rem}.summary-pill .summary-value{font-size:1.45rem;font-weight:600}.summary-pill .summary-label{font-size:0.9rem;color:var(--text-secondary)}.summary-pill.tone-danger{border-color:rgba(248,113,113,0.5)}.summary-pill.tone-warning{border-color:rgba(250,204,21,0.5)}.summary-pill.tone-primary{border-color:rgba(129,140,248,0.5)}.summary-pill.tone-info{border-color:rgba(56,189,248,0.5)}.alert-stream{display:grid;gap:1.2rem}.alert-list{list-style:none;margin:0;padding:0;display:grid;gap:1.1rem}.alert-item{padding:1.35rem 1.5rem;display:grid;gap:0.75rem}.alert-header{display:flex;justify-content:space-between;align-items:center;font-size:0.85rem;text-transform:uppercase;letter-spacing:0.05em}.alert-severity{color:var(--text-secondary)}.alert-item.severity-kritis{border-left:3px solid rgba(248,113,113,0.8)}.alert-item.severity-penting{border-left:3px solid rgba(129,140,248,0.75)}.alert-item.severity-pengingat{border-left:3px solid rgba(56,189,248,0.75)}.alert-actions{display:flex;gap:0.75rem;flex-wrap:wrap}.mod-team{display:grid}.team-roster{padding:2.5rem;display:grid;gap:2rem}.team-layout{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,0.8fr);gap:2.25rem}.team-table{display:grid;gap:0.75rem}.team-header,.team-row{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:1rem;padding:0.9rem 1.1rem;border-radius:var(--radius-md);background:rgba(15,23,42,0.55);border:1px solid rgba(148,163,184,0.18)}.team-header{font-size:0.85rem;text-transform:uppercase;letter-spacing:0.05em;background:rgba(15,23,42,0.7)}.team-sidepanel{display:grid;gap:1.75rem}.invite-panel ul,.audit-trail ol{margin:0;padding:0;list-style:none;display:grid;gap:1rem}.invite-panel li,.audit-trail li{background:rgba(15,23,42,0.55);border:1px solid rgba(148,163,184,0.18);border-radius:var(--radius-md);padding:1rem 1.2rem;display:grid;gap:0.5rem}.panel-actions{display:flex;gap:0.75rem;flex-wrap:wrap}.audit-time{font-size:0.8rem;color:var(--text-secondary);text-transform:uppercase;letter-spacing:0.05em}.mod-queues{padding:2.5rem;display:grid;gap:2.25rem}.queue-grid{display:grid;grid-template-columns:minmax(0,0.55fr) minmax(0,1.45fr);gap:2rem}.queue-sidebar ul{list-style:none;margin:0 0 1.5rem;padding:0;display:grid;gap:0.75rem}.queue-sidebar li{display:flex;align-items:center;gap:0.8rem;font-size:0.95rem}.queue-sidebar .indicator{width:12px;height:12px;border-radius:999px;display:inline-block}.indicator.tone-danger{background:rgba(248,113,113,0.85)}.indicator.tone-warning{background:rgba(250,204,21,0.85)}.indicator.tone-primary{background:rgba(129,140,248,0.85)}.indicator.tone-info{background:rgba(56,189,248,0.85)}.queue-note{font-size:0.9rem;color:var(--text-secondary)}.queue-table{width:100%;border-collapse:collapse;background:rgba(15,23,42,0.55);border:1px solid rgba(148,163,184,0.18);border-radius:var(--radius-md);overflow:hidden}.queue-table th,.queue-table td{padding:0.85rem 1rem;text-align:left;border-bottom:1px solid rgba(148,163,184,0.12)}.queue-table thead{background:rgba(15,23,42,0.7);font-size:0.85rem;text-transform:uppercase;letter-spacing:0.05em}.queue-table tbody tr:last-child td{border-bottom:none}.priority-badge{display:inline-flex;align-items:center;padding:0.3rem 0.75rem;border-radius:999px;font-size:0.85rem;border:1px solid transparent}.priority-merah{background:rgba(248,113,113,0.18);border-color:rgba(248,113,113,0.45)}.priority-kuning{background:rgba(250,204,21,0.18);border-color:rgba(250,204,21,0.45)}.priority-hijau{background:rgba(52,211,153,0.18);border-color:rgba(52,211,153,0.45)}.mod-curation{padding:2.5rem;display:grid;gap:2.25rem}.curation-grid{display:grid;grid-template-columns:minmax(0,0.6fr) minmax(0,1.4fr);gap:2rem}.summary-list{list-style:none;margin:0 0 1.5rem;padding:0;display:grid;gap:0.85rem}.summary-list li{display:grid;gap:0.2rem}.summary-list .summary-value{font-size:1.35rem;font-weight:600}.summary-list .summary-label{font-size:0.95rem;color:var(--text-secondary)}.summary-list .tone-success{color:#34d399}.summary-list .tone-danger{color:#f87171}.summary-list .tone-warning{color:#fbbf24}.summary-list .tone-primary{color:#818cf8}.checklist{list-style:none;margin:0;padding:0;display:grid;gap:0.6rem;font-size:0.95rem}.curation-table,.team-performance{width:100%;border-collapse:collapse;background:rgba(15,23,42,0.55);border:1px solid rgba(148,163,184,0.18);border-radius:var(--radius-md);overflow:hidden}.curation-table th,.curation-table td,.team-performance th,.team-performance td{padding:0.85rem 1rem;text-align:left;border-bottom:1px solid rgba(148,163,184,0.12)}.curation-table thead,.team-performance thead{background:rgba(15,23,42,0.7);font-size:0.85rem;text-transform:uppercase;letter-spacing:0.05em}.mod-analytics{padding:2.5rem;display:grid;gap:2.25rem}.analytics-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:2rem}.insight-cards{display:grid;gap:1.25rem}.insight-card{padding:1.4rem 1.6rem;display:grid;gap:0.6rem}.insight-card h3{margin:0}.insight-card .insight-value{font-size:1.8rem;font-weight:600;margin:0}.insight-card.tone-warning{border-left:3px solid rgba(250,204,21,0.75)}.insight-card.tone-info{border-left:3px solid rgba(56,189,248,0.75)}.insight-card.tone-success{border-left:3px solid rgba(52,211,153,0.75)}.analytics-details{display:grid;gap:1.75rem}.heatmap{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem}.heatmap-slot{border:1px solid rgba(148,163,184,0.18);border-radius:var(--radius-md);padding:1rem 1.2rem;background:rgba(15,23,42,0.55);display:grid;gap:0.35rem}.heatmap-slot.state-kritikal{border-color:rgba(248,113,113,0.5)}.heatmap-slot.state-padat{border-color:rgba(250,204,21,0.45)}.heatmap-slot.state-stabil{border-color:rgba(56,189,248,0.45)}.heatmap-slot.state-rendah{border-color:rgba(129,140,248,0.35)}.violation-list{margin:0;padding:0 0 0 1.25rem;display:grid;gap:0.6rem}.mod-policies{padding:2.5rem;display:grid;gap:2.25rem}.policy-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2rem}.policy-list{list-style:none;margin:0;padding:0;display:grid;gap:1rem}.policy-list li{padding:1.3rem 1.5rem;display:grid;gap:0.6rem}.policy-tags{display:flex;flex-wrap:wrap;gap:0.5rem}.policy-tags span{padding:0.35rem 0.8rem;border-radius:999px;background:rgba(148,163,184,0.15);border:1px solid rgba(148,163,184,0.25);font-size:0.85rem}.template-list,.automation-list{list-style:none;margin:0 0 1.25rem;padding:0;display:grid;gap:0.8rem}.automation-list li{padding-left:1.25rem;position:relative}.automation-list li::before{content:"•";position:absolute;left:0;color:rgba(56,189,248,0.75)}.mod-help{padding:2.5rem;display:grid;gap:2.25rem}.help-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2rem}.resource-list,.contact-list{list-style:none;margin:0;padding:0;display:grid;gap:1rem}.resource-list li{padding:1.2rem 1.4rem;display:grid;gap:0.5rem}.contact-list li{background:rgba(15,23,42,0.55);border:1px solid rgba(148,163,184,0.18);border-radius:var(--radius-md);padding:1rem 1.2rem}.feedback-box{margin-top:1.5rem;padding:1.4rem 1.6rem;display:grid;gap:0.75rem}@media (max-width:1024px){.mod-hero,.overview-grid,.team-layout,.queue-grid,.curation-grid,.analytics-grid,.policy-grid,.help-grid{grid-template-columns:1fr}.mod-hero{padding:2.25rem}}@media (max-width:768px){.team-header,.team-row{grid-template-columns:repeat(2,minmax(0,1fr))}.queue-table,.curation-table,.team-performance{display:block;overflow-x:auto;white-space:nowrap}} \ No newline at end of file diff --git a/src/app/web/static/css/nusantarum.min.css b/src/app/web/static/css/nusantarum.min.css new file mode 100644 index 0000000..b4e3eef --- /dev/null +++ b/src/app/web/static/css/nusantarum.min.css @@ -0,0 +1 @@ +.nusantarum-page{display:grid;gap:2.5rem;padding-block:2rem 4rem;color:var(--text-primary)}.nusantarum-hero{display:grid;gap:2rem;padding:clamp(2rem,5vw,3rem);border-radius:28px;background:linear-gradient(135deg,rgba(15,23,42,0.9),rgba(30,41,59,0.65));border:1px solid rgba(255,255,255,0.18);backdrop-filter:blur(18px);box-shadow:0 32px 80px rgba(15,23,42,0.45)}.nusantarum-hero__copy{display:grid;gap:1rem}.nusantarum-hero__eyebrow{font-size:0.85rem;letter-spacing:0.28em;text-transform:uppercase;color:rgba(255,255,255,0.65)}.nusantarum-hero h1{font-family:'Playfair Display',serif;font-size:clamp(2.5rem,4vw,3.5rem);font-weight:700;letter-spacing:-0.02em}.nusantarum-hero p{max-width:60ch;font-size:1rem;line-height:1.7;color:rgba(255,255,255,0.85)}.nusantarum-hero__stats{display:grid;gap:1rem;grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}.nusantarum-hero__stats div{padding:1.2rem 1.4rem;border-radius:18px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.15)}.nusantarum-hero__stats dt{font-size:0.85rem;text-transform:uppercase;letter-spacing:0.14em;color:rgba(255,255,255,0.65);margin-bottom:0.35rem}.nusantarum-hero__stats dd{font-size:clamp(1.8rem,2vw+1rem,2.4rem);font-weight:600;margin:0}.nusantarum-tabs{display:flex;flex-wrap:wrap;gap:0.75rem}.nusantarum-tab{display:inline-flex;align-items:center;gap:0.65rem;padding:0.6rem 1.3rem;border-radius:999px;border:1px solid transparent;background:rgba(255,255,255,0.15);color:inherit;cursor:pointer;transition:transform 0.2s ease,box-shadow 0.2s ease,border 0.2s ease}.nusantarum-tab[aria-selected='true']{background:linear-gradient(135deg,rgba(251,191,36,0.9),rgba(236,72,153,0.85));border-color:rgba(255,255,255,0.45);color:#0f172a;box-shadow:0 12px 32px rgba(236,72,153,0.35)}.nusantarum-tab:hover,.nusantarum-tab:focus-visible{border-color:rgba(255,255,255,0.5);transform:translateY(-2px)}.nusantarum-tab__count{display:inline-flex;align-items:center;justify-content:center;min-width:2rem;padding:0.15rem 0.5rem;border-radius:999px;background:rgba(15,23,42,0.4);color:inherit;font-size:0.85rem;font-weight:600}.nusantarum-controls{display:grid;gap:1rem}.nusantarum-search{display:flex;flex-wrap:wrap;gap:1rem;align-items:center}.nusantarum-search input[type='search']{flex:1;min-width:min(360px,100%);padding:0.85rem 1.2rem;border-radius:18px;border:1px solid rgba(255,255,255,0.25);background:rgba(15,23,42,0.55);color:inherit;font-size:1rem;transition:border 0.2s ease,box-shadow 0.2s ease}.nusantarum-search input[type='search']:focus{outline:none;border-color:rgba(251,191,36,0.7);box-shadow:0 0 0 2px rgba(251,191,36,0.35)}.nusantarum-search input[type='search']::placeholder{color:rgba(255,255,255,0.6)}.nusantarum-search-results{display:grid;gap:1.2rem;padding:1.3rem;border-radius:20px;background:rgba(15,23,42,0.65);border:1px solid rgba(255,255,255,0.18);box-shadow:inset 0 1px 0 rgba(255,255,255,0.08)}.nusantarum-search-results__group{display:grid;gap:0.6rem}.nusantarum-search-results__group h3{font-size:0.9rem;text-transform:uppercase;letter-spacing:0.18em;color:rgba(255,255,255,0.6)}.nusantarum-search-results ul{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:0.5rem}.nusantarum-search-results__pill{display:inline-flex;align-items:center;gap:0.4rem;padding:0.35rem 0.8rem;border-radius:999px;background:rgba(255,255,255,0.14);font-size:0.85rem}.nusantarum-search-results__message{font-size:0.9rem;color:rgba(255,255,255,0.7)}.nusantarum-search-results__placeholder{display:grid;gap:0.75rem}.nusantarum-body{display:grid;gap:1.5rem}.nusantarum-content{display:grid;gap:1.5rem}@media (min-width:1024px){.nusantarum-hero{grid-template-columns:minmax(0,1fr) 320px;align-items:center}.nusantarum-body{grid-template-columns:minmax(0,320px) minmax(0,1fr);align-items:start}}.nusantarum-sidebar{position:relative}.nusantarum-filter-panel{position:sticky;top:1.5rem;display:grid;gap:1.25rem;padding:1.75rem;border-radius:24px;background:rgba(15,23,42,0.72);border:1px solid rgba(255,255,255,0.16)}.nusantarum-filter-panel h2{font-size:1.15rem;font-weight:600;margin:0}.nusantarum-filter-panel h3{font-size:0.95rem;text-transform:uppercase;letter-spacing:0.16em;color:rgba(255,255,255,0.6);margin-bottom:0.5rem}.filter-actions{display:flex;justify-content:flex-end;margin-top:0.5rem}.filter-submit{display:inline-flex;align-items:center;gap:0.5rem;padding:0.7rem 1.35rem;border-radius:999px;border:1px solid rgba(255,255,255,0.28);background:linear-gradient(135deg,rgba(251,191,36,0.88),rgba(236,72,153,0.82));color:#0f172a;font-weight:600;letter-spacing:0.02em;box-shadow:0 12px 32px rgba(236,72,153,0.25);cursor:pointer;transition:transform 0.2s ease,box-shadow 0.2s ease,opacity 0.2s ease}.filter-submit:hover,.filter-submit:focus-visible{transform:translateY(-1px);box-shadow:0 18px 40px rgba(236,72,153,0.35);outline:none}.filter-submit:active{transform:translateY(0);box-shadow:0 8px 22px rgba(236,72,153,0.28)}.text-muted{color:rgba(255,255,255,0.7);font-size:0.95rem;line-height:1.6}.filter-chip-group{display:flex;flex-wrap:wrap;gap:0.5rem}.filter-chip{position:relative;display:inline-flex;align-items:center;justify-content:center;padding:0.45rem 0.9rem;border-radius:999px;border:1px solid rgba(255,255,255,0.25);background:transparent;color:inherit;cursor:pointer;overflow:hidden}.filter-chip input[type='checkbox']{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}.filter-chip span{pointer-events:none;transition:transform 0.2s ease}.filter-chip input[type='checkbox']:checked+span{transform:translateY(-1px);font-weight:600}.toggle{display:inline-flex;gap:0.75rem;align-items:center;font-size:0.95rem}.toggle input[type='checkbox']{width:1.15rem;height:1.15rem;accent-color:#fbbf24}.nusantarum-card-grid{display:grid;gap:1.25rem;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.nusantarum-card{display:grid;gap:1.2rem;padding:1.6rem;border-radius:22px;background:rgba(15,23,42,0.68);border:1px solid rgba(255,255,255,0.16);transition:transform 0.2s ease,box-shadow 0.2s ease}.nusantarum-card:hover,.nusantarum-card:focus-within{transform:translateY(-4px);box-shadow:0 18px 50px rgba(15,23,42,0.5)}.nusantarum-card__header{display:flex;gap:1rem;align-items:center}.nusantarum-card__avatar{width:3rem;height:3rem;border-radius:50%;background:linear-gradient(135deg,rgba(251,191,36,0.9),rgba(236,72,153,0.9));display:inline-flex;align-items:center;justify-content:center;font-weight:600;color:#0f172a}.nusantarum-card__title{display:flex;flex-wrap:wrap;gap:0.5rem;font-size:1.2rem;font-weight:600}.nusantarum-card__badge{display:inline-flex;align-items:center;gap:0.3rem;padding:0.25rem 0.6rem;border-radius:999px;background:rgba(34,197,94,0.16);color:rgba(134,239,172,1);font-size:0.75rem;text-transform:uppercase;letter-spacing:0.12em}.nusantarum-card__meta{margin:0;font-size:0.9rem;color:rgba(255,255,255,0.7)}.nusantarum-card__description{margin:0;font-size:0.95rem;color:rgba(255,255,255,0.78)}.nusantarum-card__stats{display:flex;flex-wrap:wrap;gap:0.5rem;align-items:center}.nusantarum-stat-pill{display:inline-flex;align-items:center;gap:0.3rem;padding:0.35rem 0.8rem;border-radius:999px;background:rgba(255,255,255,0.12);font-size:0.85rem;color:rgba(255,255,255,0.85)}.nusantarum-card__footer{display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap}.nusantarum-link{color:#fbbf24;text-decoration:none;font-weight:600;transition:color 0.2s ease}.nusantarum-link:hover,.nusantarum-link:focus{color:#f59e0b;text-decoration:underline}.nusantarum-table-wrapper{overflow-x:auto;border-radius:20px;border:1px solid rgba(255,255,255,0.16);background:rgba(15,23,42,0.72)}.nusantarum-table{width:100%;border-collapse:collapse;min-width:640px}.nusantarum-table th,.nusantarum-table td{padding:1rem 1.25rem;border-bottom:1px solid rgba(255,255,255,0.08);text-align:left;vertical-align:top}.nusantarum-table thead th{font-size:0.85rem;text-transform:uppercase;letter-spacing:0.18em;color:rgba(255,255,255,0.65)}.nusantarum-table__sort{display:inline-flex;align-items:center;gap:0.35rem;padding:0;border:none;background:none;color:inherit;font:inherit;text-transform:inherit;letter-spacing:inherit;cursor:pointer}.nusantarum-table__sort:hover,.nusantarum-table__sort:focus-visible{color:rgba(255,255,255,0.9);outline:none}.nusantarum-table__sort-indicator{font-size:0.85rem;opacity:0.75}.nusantarum-table tbody tr:last-child th,.nusantarum-table tbody tr:last-child td{border-bottom:none}.nusantarum-table__description{margin:0.45rem 0 0;font-size:0.9rem;color:rgba(255,255,255,0.7)}.nusantarum-table__meta{font-size:0.85rem;color:rgba(255,255,255,0.6);margin:0.25rem 0 0}.nusantarum-aroma-group{display:flex;flex-wrap:wrap;gap:0.4rem}.nusantarum-badge{display:inline-flex;align-items:center;padding:0.3rem 0.7rem;border-radius:999px;background:rgba(251,191,36,0.18);color:rgba(255,255,255,0.85);font-size:0.8rem;letter-spacing:0.03em}.nusantarum-empty{padding:2.2rem;border-radius:20px;background:rgba(15,23,42,0.68);border:1px solid rgba(255,255,255,0.16);display:grid;gap:0.75rem;justify-items:center;text-align:center}.nusantarum-empty h3{font-size:1.3rem;margin:0}.nusantarum-empty__suggestions{list-style:disc;text-align:left;padding-left:1.25rem;display:grid;gap:0.35rem;font-size:0.95rem;color:rgba(255,255,255,0.75)}.nusantarum-pagination{display:flex;align-items:center;justify-content:space-between;gap:1rem;flex-wrap:wrap}.nusantarum-pagination__button{padding:0.6rem 1.3rem;border-radius:999px;border:1px solid rgba(255,255,255,0.25);background:rgba(15,23,42,0.65);color:inherit;cursor:pointer;transition:transform 0.2s ease,box-shadow 0.2s ease,border 0.2s ease}.nusantarum-pagination__button:hover,.nusantarum-pagination__button:focus-visible{border-color:rgba(251,191,36,0.7);transform:translateY(-2px);box-shadow:0 12px 24px rgba(251,191,36,0.25)}.nusantarum-pagination__button[disabled]{opacity:0.5;cursor:not-allowed;transform:none;box-shadow:none}.nusantarum-pagination__status{font-size:0.9rem;color:rgba(255,255,255,0.7)}.sync-status-list{display:grid;gap:0.5rem}.sync-status-item{display:flex;justify-content:space-between;gap:0.5rem;font-size:0.85rem;color:rgba(255,255,255,0.7)}@media (max-width:768px){.nusantarum-table{min-width:100%}.nusantarum-table th,.nusantarum-table td{padding-inline:1rem}.nusantarum-tabs{gap:0.5rem}.nusantarum-tab{width:100%;justify-content:center}} \ No newline at end of file diff --git a/src/app/web/static/css/profile.min.css b/src/app/web/static/css/profile.min.css new file mode 100644 index 0000000..113ec8c --- /dev/null +++ b/src/app/web/static/css/profile.min.css @@ -0,0 +1 @@ +.profile-page{display:flex;flex-direction:column;gap:2.5rem}.glass-panel{backdrop-filter:blur(24px) saturate(160%);background:linear-gradient(145deg,rgba(15,23,42,0.75),rgba(30,41,59,0.55));border:1px solid rgba(148,163,184,0.25);border-radius:var(--radius-lg);box-shadow:0 25px 45px rgba(8,15,29,0.45)}.profile-hero{display:grid;grid-template-columns:160px 1fr;gap:2rem;padding:2.5rem;align-items:center}.profile-hero__avatar{width:160px;height:160px;border-radius:50%;overflow:hidden;border:3px solid rgba(148,163,184,0.35);box-shadow:0 14px 28px rgba(8,15,29,0.4)}.profile-hero__avatar img{width:100%;height:100%;object-fit:cover}.profile-hero__content{display:flex;flex-direction:column;gap:1rem}.profile-hero__headline{display:flex;justify-content:space-between;gap:1rem;align-items:start}.profile-hero__headline h1{margin:0;font-size:clamp(2.1rem,2vw+1.2rem,2.6rem)}.profile-hero__username{margin:0.25rem 0 0;color:var(--muted);font-weight:500}.profile-hero__bio{margin:0;font-size:1.05rem;color:var(--text-secondary)}.profile-hero__meta{list-style:none;display:flex;flex-wrap:wrap;gap:0.75rem 1.5rem;padding:0;margin:0;color:var(--muted)}.profile-badges{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:0.75rem}.profile-badge{display:inline-flex;align-items:center;gap:0.65rem;padding:0.45rem 1rem;border-radius:999px;background:rgba(148,163,184,0.18);border:1px solid rgba(148,163,184,0.25)}.profile-badge__label{font-weight:600;letter-spacing:0.02em}.profile-badge__meta{font-size:0.9rem;color:var(--accent-secondary)}.profile-badge--perfumer{background:linear-gradient(135deg,rgba(192,38,211,0.25),rgba(14,116,144,0.15));border-color:rgba(192,38,211,0.35)}.profile-badge--brand-owner{background:linear-gradient(135deg,rgba(249,115,22,0.25),rgba(244,114,182,0.18));border-color:rgba(249,115,22,0.35)}.profile-follow{display:flex;align-items:center;gap:0.75rem}.profile-follow__button,.profile-follow__edit{display:inline-flex;align-items:center;justify-content:center;border-radius:999px;padding:0.65rem 1.8rem;font-weight:600;background:var(--accent-primary);color:#0f172a;border:none;cursor:pointer;text-decoration:none;transition:transform var(--transition-base),box-shadow var(--transition-base);box-shadow:0 16px 30px rgba(192,38,211,0.35)}.profile-follow__button:hover,.profile-follow__edit:hover{transform:translateY(-2px)}.profile-follow__button--muted{background:rgba(148,163,184,0.2);color:var(--text-primary);box-shadow:none;border:1px solid rgba(148,163,184,0.35)}.profile-follow__button--ghost{background:transparent;border:1px solid rgba(148,163,184,0.35);color:var(--text-secondary);box-shadow:none}.profile-follow__button--ghost:hover{color:var(--text-primary)}.profile-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;padding:1.5rem 2rem}.profile-stat{display:flex;flex-direction:column;gap:0.35rem;text-align:center}.profile-stat__label{color:var(--muted);font-size:0.95rem}.profile-stat__value{font-size:1.6rem;font-weight:600;color:var(--text-primary);background:none;border:none;cursor:pointer;padding:0.25rem;transition:color var(--transition-base),transform var(--transition-base)}.profile-stat__value:hover{color:var(--accent-secondary);transform:translateY(-2px)}.profile-tabs{padding:2rem;display:flex;flex-direction:column;gap:1.5rem}.profile-edit-page{display:flex;justify-content:center;padding:2rem 0 4rem}.profile-edit-page__shell{display:flex;flex-direction:column;gap:1.5rem;width:min(720px,100%)}.profile-edit-page__back{color:var(--accent-secondary);text-decoration:none;font-weight:600;display:inline-flex;align-items:center;gap:0.35rem}.profile-edit-page__back:focus,.profile-edit-page__back:hover{text-decoration:underline}.profile-edit{padding:2.25rem;display:flex;flex-direction:column;gap:1.75rem}.profile-edit__header{display:flex;gap:1.25rem;align-items:center}.profile-edit__avatar{width:80px;height:80px;border-radius:50%;overflow:hidden;border:2px solid rgba(148,163,184,0.35);flex-shrink:0}.profile-edit__avatar img{width:100%;height:100%;object-fit:cover}.profile-edit__intro h1{margin:0;font-size:1.6rem}.profile-edit__intro p{margin:0.35rem 0 0;color:var(--muted)}.profile-edit__feedback{padding:0.75rem 1rem;border-radius:var(--radius-md);font-weight:500}.profile-edit__feedback--success{background:rgba(34,197,94,0.12);color:rgb(134,239,172);border:1px solid rgba(34,197,94,0.35)}.profile-edit__feedback--info{background:rgba(148,163,184,0.12);color:rgba(226,232,240,0.9);border:1px solid rgba(148,163,184,0.3)}.profile-edit__form{display:flex;flex-direction:column;gap:1.25rem}.profile-edit__field{display:flex;flex-direction:column;gap:0.5rem}.profile-edit__field label{font-weight:600}.profile-edit__field input,.profile-edit__field textarea{background:rgba(15,23,42,0.35);border:1px solid rgba(148,163,184,0.25);border-radius:var(--radius-md);padding:0.75rem 1rem;color:var(--text-primary);font:inherit;transition:border-color var(--transition-base),box-shadow var(--transition-base)}.profile-edit__field input:focus,.profile-edit__field textarea:focus{border-color:var(--accent-secondary);box-shadow:0 0 0 3px rgba(192,38,211,0.25);outline:none}.profile-edit__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem}.profile-edit__counter{color:var(--muted);display:block}.profile-edit__actions{display:flex;flex-wrap:wrap;gap:1rem;align-items:center}.profile-edit__submit{background:var(--accent-primary);color:#0f172a;border:none;border-radius:999px;padding:0.75rem 1.75rem;font-weight:600;cursor:pointer;transition:transform var(--transition-base),box-shadow var(--transition-base);box-shadow:0 16px 30px rgba(192,38,211,0.35)}.profile-edit__submit:hover{transform:translateY(-1px)}.profile-edit__submit:focus-visible{outline:3px solid rgba(192,38,211,0.6);outline-offset:2px}.profile-edit__cancel{color:var(--muted);text-decoration:none;font-weight:500}.profile-edit__cancel:focus,.profile-edit__cancel:hover{text-decoration:underline}@media (max-width:640px){.profile-edit{padding:1.75rem}.profile-edit__header{flex-direction:column;align-items:flex-start}.profile-edit__avatar{width:96px;height:96px}.profile-edit__actions{flex-direction:column;align-items:stretch}.profile-edit__cancel{text-align:center}}.profile-tabs__nav{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:0.75rem}.profile-tabs__button{padding:0.75rem 1rem;border-radius:var(--radius-sm);background:rgba(148,163,184,0.14);border:1px solid transparent;color:var(--text-secondary);font-weight:600;cursor:pointer;transition:background var(--transition-base),border var(--transition-base)}.profile-tabs__button--active,.profile-tabs__button:hover{background:rgba(56,189,248,0.18);border-color:rgba(56,189,248,0.35);color:var(--text-primary)}.profile-tabs__content{display:flex;flex-direction:column;gap:1.25rem}.profile-timeline{list-style:none;display:flex;flex-direction:column;gap:1rem;padding:0;margin:0}.profile-timeline__item{padding:1.5rem;border-radius:var(--radius-md);border:1px solid rgba(148,163,184,0.25)}.profile-timeline__title{margin:0;font-size:1.2rem}.profile-timeline__time{color:var(--muted);font-size:0.95rem}.profile-list{list-style:none;padding:1.25rem 1.5rem;margin:0;border-radius:var(--radius-md);border:1px solid rgba(148,163,184,0.25);display:flex;flex-direction:column;gap:0.85rem}.profile-list__item{color:var(--text-secondary)}.profile-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:1.25rem}.profile-card{padding:1.5rem;border-radius:var(--radius-md);border:1px solid rgba(148,163,184,0.25);display:flex;flex-direction:column;gap:0.75rem;transition:transform var(--transition-base),box-shadow var(--transition-base)}.profile-card:hover{transform:translateY(-4px);box-shadow:0 20px 40px rgba(8,15,29,0.45)}.profile-card__header{display:flex;justify-content:space-between;align-items:center;gap:0.75rem}.profile-card__title{margin:0}.profile-card__brand{color:var(--muted);font-weight:500}.profile-card__notes{margin:0;color:var(--text-secondary);font-weight:500}.profile-card__highlight{margin:0;color:var(--muted);font-size:0.95rem}.profile-card__cta{align-self:flex-start;padding:0.55rem 1.2rem;border-radius:999px;border:1px solid rgba(56,189,248,0.45);color:var(--accent-secondary);text-decoration:none;font-weight:600;transition:background var(--transition-base),color var(--transition-base)}.profile-card__cta:hover{background:rgba(56,189,248,0.15);color:var(--text-primary)}.profile-card__badge{padding:0.3rem 0.8rem;border-radius:999px;font-size:0.85rem;background:rgba(148,163,184,0.18);border:1px solid rgba(148,163,184,0.25);text-transform:capitalize}.profile-card__badge--active{background:rgba(56,189,248,0.2);border-color:rgba(56,189,248,0.45)}.profile-empty{padding:2rem;text-align:center;border-radius:var(--radius-md);border:1px dashed rgba(148,163,184,0.35);color:var(--text-secondary)}.profile-empty--subtle{padding:1rem 1.25rem;border-style:solid}.profile-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none}.profile-modal__overlay{position:fixed;inset:0;backdrop-filter:blur(6px);background:rgba(8,15,29,0.45);display:flex;align-items:center;justify-content:center;pointer-events:auto;padding:2rem}.profile-modal__content{width:min(420px,100%);padding:1.5rem;border-radius:var(--radius-md);display:flex;flex-direction:column;gap:1rem}.profile-modal__header{display:flex;justify-content:space-between;align-items:center}.profile-modal__header h2{margin:0}.profile-modal__close{background:transparent;border:none;color:var(--text-secondary);font-size:1.25rem;cursor:pointer}.profile-modal__list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:0.85rem}.profile-modal__item{display:flex;align-items:center;gap:0.85rem}.profile-modal__avatar{width:40px;height:40px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(148,163,184,0.2);font-weight:600}.profile-modal__name{font-weight:600}.profile-modal__username{color:var(--muted);font-size:0.9rem}.profile-modal__empty{color:var(--text-secondary);text-align:center}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media (max-width:768px){.profile-hero{grid-template-columns:1fr;text-align:center}.profile-hero__headline{flex-direction:column;align-items:center}.profile-follow{justify-content:center}.profile-stats{grid-template-columns:repeat(2,minmax(120px,1fr));padding:1.25rem}.profile-tabs{padding:1.5rem}} \ No newline at end of file diff --git a/src/app/web/static/css/purchase-workflow.min.css b/src/app/web/static/css/purchase-workflow.min.css new file mode 100644 index 0000000..b63118c --- /dev/null +++ b/src/app/web/static/css/purchase-workflow.min.css @@ -0,0 +1 @@ +.purchase-hero{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,320px);gap:2rem;padding:2.5rem;align-items:start}.purchase-hero .hero-copy h1{font-size:clamp(2.2rem,3vw,2.8rem);margin-bottom:1rem}.purchase-hero .hero-copy p{margin:0}.purchase-hero .badge{display:inline-flex;align-items:center;gap:0.5rem;padding:0.35rem 1rem;border-radius:999px;background:rgba(56,189,248,0.12);color:var(--accent-secondary);font-weight:600;letter-spacing:0.02em;margin-bottom:1.5rem}.status-legend h2{margin-top:0;margin-bottom:1rem;font-size:1.35rem}.status-legend ul{list-style:none;padding:0;margin:0;display:grid;gap:1rem}.status-legend li{display:grid;grid-template-columns:auto 1fr;gap:0.75rem;align-items:start}.status-icon{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;border-radius:999px;background:rgba(148,163,184,0.15);font-size:1.25rem}.status-label{margin:0;color:var(--text-primary);font-weight:600}.status-description{margin:0.35rem 0 0;font-size:0.95rem}.flow-nav{padding:1.75rem;display:grid;gap:1rem}.flow-nav h2{margin:0;font-size:1.4rem}.flow-nav ol{list-style:none;padding:0;margin:0;display:flex;gap:1rem;flex-wrap:wrap}.flow-nav a{color:var(--text-primary);text-decoration:none;font-weight:600;padding:0.5rem 1.25rem;border-radius:999px;border:1px solid rgba(148,163,184,0.35);background:rgba(15,23,42,0.45);transition:transform var(--transition-base),border-color var(--transition-base)}.flow-nav a:hover{transform:translateY(-2px);border-color:rgba(56,189,248,0.6)}.flow-section{padding:2.25rem;display:grid;gap:2rem}.flow-header{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,0.8fr);gap:2.5rem;align-items:start}.flow-summary{color:var(--text-secondary);margin-bottom:0.75rem}.flow-persona{margin:0;color:var(--text-secondary)}.flow-meta{display:grid;gap:1.5rem;justify-items:start}.status-chip{display:inline-flex;align-items:center;gap:0.5rem;padding:0.5rem 1.2rem;border-radius:999px;font-weight:600;border:1px solid rgba(148,163,184,0.4);background:rgba(15,23,42,0.5)}.metric-list{background:rgba(15,23,42,0.45);border:1px solid rgba(148,163,184,0.25);border-radius:var(--radius-md);padding:1.25rem 1.5rem;box-shadow:inset 0 0 0 1px rgba(56,189,248,0.08)}.metric-list h3{margin-top:0;margin-bottom:0.75rem;font-size:1.1rem}.metric-list ul{margin:0;padding-left:1.1rem;display:grid;gap:0.5rem}.flow-steps{list-style:none;margin:0;padding:0;display:grid;gap:1.5rem}.flow-step{background:rgba(15,23,42,0.55);border-radius:var(--radius-md);border:1px solid rgba(148,163,184,0.25);padding:1.75rem;box-shadow:0 16px 45px rgba(8,15,29,0.35);backdrop-filter:blur(18px);transition:transform var(--transition-base),box-shadow var(--transition-base)}.flow-step:hover{transform:translateY(-4px);box-shadow:0 24px 55px rgba(8,15,29,0.45)}.step-header{display:grid;grid-template-columns:auto 1fr auto;gap:1.25rem;align-items:center;margin-bottom:1.25rem}.step-index{width:2.75rem;height:2.75rem;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;background:var(--accent-primary);color:#0f172a;font-weight:700;font-size:1.2rem;box-shadow:0 12px 30px rgba(249,115,22,0.25)}.step-stage{margin:0;text-transform:uppercase;letter-spacing:0.08em;font-size:0.85rem;color:var(--accent-secondary)}.step-header h3{margin:0.25rem 0 0;font-size:1.35rem}.step-status{display:inline-flex;align-items:center;gap:0.5rem;padding:0.35rem 0.9rem;border-radius:999px;font-weight:600;border:1px solid rgba(148,163,184,0.35);background:rgba(148,163,184,0.12)}.step-body{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1.5rem}.step-body h4{margin-top:0;font-size:1.05rem}.step-body ul{margin:0;padding-left:1.1rem;display:grid;gap:0.6rem}.foundation-notes{padding:2rem 2.25rem;display:grid;gap:1rem}.foundation-notes ul{margin:0;padding-left:1.2rem;display:grid;gap:0.5rem}.step-status.status-live,.status-chip.status-live,.status-icon.status-live{border-color:rgba(52,211,153,0.4);background:rgba(52,211,153,0.18)}.step-status.status-ready,.status-chip.status-ready,.status-icon.status-ready{border-color:rgba(56,189,248,0.45);background:rgba(56,189,248,0.16)}.step-status.status-in-design,.status-chip.status-in-design,.status-icon.status-in-design{border-color:rgba(192,132,252,0.4);background:rgba(192,132,252,0.16)}.step-status.status-in-discovery,.status-chip.status-in-discovery,.status-icon.status-in-discovery{border-color:rgba(248,180,0,0.4);background:rgba(248,180,0,0.2)}@media (max-width:1024px){.purchase-hero{grid-template-columns:minmax(0,1fr)}.status-legend{order:2}.flow-header{grid-template-columns:1fr}.flow-meta{justify-items:stretch}.step-body{grid-template-columns:1fr}}@media (max-width:640px){.purchase-hero,.flow-section,.foundation-notes{padding:1.75rem}.flow-nav ol{flex-direction:column}.step-header{grid-template-columns:1fr;justify-items:start}.step-status{margin-top:0.75rem}} \ No newline at end of file diff --git a/src/app/web/static/css/ui-ux-tracker.min.css b/src/app/web/static/css/ui-ux-tracker.min.css new file mode 100644 index 0000000..a550ef0 --- /dev/null +++ b/src/app/web/static/css/ui-ux-tracker.min.css @@ -0,0 +1 @@ +.top-anchor{position:relative;top:-120px}.tracker-hero{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:2rem;padding:2.5rem;align-items:start}.tracker-hero .badge{display:inline-flex;align-items:center;gap:0.5rem;padding:0.35rem 1rem;border-radius:999px;background:rgba(148,163,184,0.18);color:var(--text-secondary);font-size:0.9rem;letter-spacing:0.02em;text-transform:uppercase}.tracker-hero h1{margin-top:1.2rem;margin-bottom:1rem;font-size:clamp(1.8rem,4vw,2.6rem)}.tracker-hero p{margin:0.75rem 0;max-width:36ch}.tracker-hero .hero-subcopy{color:var(--text-primary);font-weight:500}.status-legend{padding:1.8rem;display:flex;flex-direction:column;gap:1rem}.status-legend h2{margin:0;font-size:1.15rem}.status-legend ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.75rem}.status-legend li{display:flex;align-items:center;gap:0.65rem;font-weight:500}.status-icon{display:inline-flex;align-items:center;justify-content:center;width:1.8rem;height:1.8rem;border-radius:0.75rem;background:rgba(148,163,184,0.12);font-size:1rem}.status-todo{color:var(--muted)}.status-progress{color:var(--accent-secondary)}.status-done{color:var(--success)}.section-nav{display:grid;gap:1rem;padding:1.8rem 2rem}.section-nav h2{margin:0;font-size:1.25rem}.section-nav ol{margin:0;padding-left:1.2rem;display:grid;gap:0.6rem}.section-nav a{color:var(--text-secondary);text-decoration:none;position:relative}.section-nav a::after{content:"";position:absolute;left:0;bottom:-0.2rem;width:100%;height:2px;background:var(--accent-primary);transform:scaleX(0);transform-origin:left;transition:transform var(--transition-base)}.section-nav a:hover::after,.section-nav a:focus-visible::after{transform:scaleX(1)}.tracker-section{padding:2.25rem 2.4rem;display:flex;flex-direction:column;gap:1.75rem}.section-header{display:flex;align-items:start;justify-content:space-between;gap:1.5rem}.section-eyebrow{text-transform:uppercase;letter-spacing:0.12em;font-size:0.8rem;color:var(--muted);margin:0}.section-header h2{margin:0.25rem 0 0;font-size:clamp(1.5rem,3vw,2.1rem)}.section-top-link{text-decoration:none;color:var(--text-secondary);font-size:1.4rem;transition:color var(--transition-base),transform var(--transition-base)}.section-top-link:hover,.section-top-link:focus-visible{color:var(--accent-secondary);transform:translateY(-2px)}.task-grid{display:grid;gap:1.4rem;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}.task-card{display:flex;flex-direction:column;gap:1rem;padding:1.6rem;border-radius:var(--radius-md);background:rgba(15,23,42,0.72);border:1px solid rgba(148,163,184,0.25);box-shadow:0 20px 45px rgba(8,15,29,0.35);transition:transform var(--transition-base),box-shadow var(--transition-base)}.task-card:hover{transform:translateY(-4px);box-shadow:0 28px 60px rgba(8,15,29,0.4)}.task-card header{display:flex;align-items:center;gap:0.9rem}.task-card h3,.task-card h4{margin:0;font-size:1.2rem}.task-card p{margin:0;color:var(--text-secondary)}.task-card footer{display:flex;flex-direction:column;gap:0.3rem;margin-top:auto;font-size:0.9rem}.task-card .label{text-transform:uppercase;letter-spacing:0.08em;color:var(--muted);font-weight:600}.task-card .deliverable{word-break:break-word;color:var(--text-primary)}.task-group{display:flex;flex-direction:column;gap:1.25rem;padding:1.6rem 1.8rem;border-radius:var(--radius-md);background:rgba(15,23,42,0.55);border:1px solid rgba(148,163,184,0.2)}.task-group h3{margin:0;font-size:1.3rem}.grouped-section{display:grid;gap:1.6rem}.tracker-note{padding:2rem 2.3rem;display:grid;gap:0.75rem}.tracker-note h2{margin:0}@media (max-width:720px){.tracker-hero{padding:2rem}.section-header{flex-direction:column;align-items:flex-start}.section-top-link{align-self:flex-end}.task-group{padding:1.4rem}} \ No newline at end of file diff --git a/src/app/web/static/js/dashboard-brand-owner.min.js b/src/app/web/static/js/dashboard-brand-owner.min.js new file mode 100644 index 0000000..c047b1c --- /dev/null +++ b/src/app/web/static/js/dashboard-brand-owner.min.js @@ -0,0 +1 @@ +( function (){ function normalise(value){ return (value||"").toString().toLowerCase().normalize("NFD").replace(/\p{Diacritic}/gu,"");} function activateTabGroup(buttons,targetKey,{buttonDatasetKey,panels,panelDatasetKey,shouldFocus=true}={}){ let activeButton=null;buttons.forEach((button)=>{ const key=buttonDatasetKey?button.dataset[buttonDatasetKey]:undefined; const isActive=key===targetKey;button.classList.toggle("is-active",isActive);button.setAttribute("aria-selected",String(isActive));button.setAttribute("tabindex",isActive?"0":"-1"); if (isActive){activeButton=button;}}); if (panels&&panelDatasetKey){panels.forEach((panel)=>{ const panelKey=panel.dataset[panelDatasetKey]; const isMatch=panelKey===targetKey;panel.classList.toggle("is-hidden",!isMatch); if (isMatch){panel.removeAttribute("hidden");panel.setAttribute("aria-hidden","false");} else {panel.setAttribute("hidden","hidden");panel.setAttribute("aria-hidden","true");}});} if (activeButton&&shouldFocus){activeButton.focus();} return activeButton;} function bindProductFilters(){ const buttons=Array.from(document.querySelectorAll("[data-product-filter]")); const rows=document.querySelectorAll("[data-product-row]"); const searchInput=document.querySelector("[data-product-search]"); if (!buttons.length) return ; let activeFilter=buttons.find((button)=>button.classList.contains("is-active"))?.dataset.productFilter||"semua";activateTabGroup(buttons,activeFilter,{buttonDatasetKey:"productFilter",shouldFocus:false,}); function apply(){ const query=normalise(searchInput?searchInput.value.trim():"");rows.forEach((row)=>{ const status=row.dataset.status||""; const name=normalise(row.dataset.name||""); const sku=normalise(row.dataset.sku||""); const matchesFilter=activeFilter==="semua"||status===activeFilter; const matchesQuery=!query||name.includes(query)||sku.includes(query); const shouldShow=matchesFilter&&matchesQuery;row.hidden=!shouldShow;});}buttons.forEach((button,index)=>{button.addEventListener("click",()=>{ const target=button.dataset.productFilter; if (!target) return ;activeFilter=target;activateTabGroup(buttons,target,{buttonDatasetKey:"productFilter"});apply();});button.addEventListener("keydown",(event)=>{ const {key}=event; const isNext=key==="ArrowRight"||key==="ArrowDown"; const isPrev=key==="ArrowLeft"||key==="ArrowUp"; if (!isNext&&!isPrev) return ;event.preventDefault(); const offset=isNext?1:-1; const nextIndex=(index+offset+buttons.length)%buttons.length; const nextButton=buttons[nextIndex]; const target=nextButton?.dataset.productFilter; if (!target) return ;activeFilter=target;activateTabGroup(buttons,target,{buttonDatasetKey:"productFilter"});apply();});}); if (searchInput){["input","change"].forEach((eventName)=>{searchInput.addEventListener(eventName,apply);});}} function bindOrderFilters(){ const buttons=Array.from(document.querySelectorAll("[data-order-filter]")); const rows=document.querySelectorAll("[data-order-row]"); const searchInput=document.querySelector("[data-order-search]"); if (!buttons.length) return ; let activeFilter=buttons.find((button)=>button.classList.contains("is-active"))?.dataset.orderFilter||"semua";activateTabGroup(buttons,activeFilter,{buttonDatasetKey:"orderFilter",shouldFocus:false,}); function apply(){ const query=normalise(searchInput?searchInput.value.trim():"");rows.forEach((row)=>{ const status=normalise(row.dataset.status||""); const invoice=normalise(row.dataset.invoice||""); const customer=normalise(row.dataset.customer||""); const matchesFilter=activeFilter==="semua"||status===normalise(activeFilter); const matchesQuery=!query||invoice.includes(query)||customer.includes(query); const shouldShow=matchesFilter&&matchesQuery;row.hidden=!shouldShow;});}buttons.forEach((button,index)=>{button.addEventListener("click",()=>{ const target=button.dataset.orderFilter; if (!target) return ;activeFilter=target;activateTabGroup(buttons,target,{buttonDatasetKey:"orderFilter"});apply();});button.addEventListener("keydown",(event)=>{ const {key}=event; const isNext=key==="ArrowRight"||key==="ArrowDown"; const isPrev=key==="ArrowLeft"||key==="ArrowUp"; if (!isNext&&!isPrev) return ;event.preventDefault(); const offset=isNext?1:-1; const nextIndex=(index+offset+buttons.length)%buttons.length; const nextButton=buttons[nextIndex]; const target=nextButton?.dataset.orderFilter; if (!target) return ;activeFilter=target;activateTabGroup(buttons,target,{buttonDatasetKey:"orderFilter"});apply();});}); if (searchInput){["input","change"].forEach((eventName)=>{searchInput.addEventListener(eventName,apply);});}} function renderChart(chartEl){ const pointsAttr=chartEl.dataset.chartPoints; if (!pointsAttr) return ; const points=pointsAttr .split(",").map((value)=>Number.parseFloat(value.trim())).filter((value)=>Number.isFinite(value)); if (points.length<2) return ; const labelsAttr=chartEl.dataset.chartLabels||""; const labels=labelsAttr.split(",").map((label)=>label.trim()); const maxValue=Math.max(...points); const minValue=Math.min(...points); const range=maxValue-minValue||1; const svgWidth=100; const svgHeight=40; const verticalPadding=6; const usableHeight=svgHeight-verticalPadding*2; const stepX=svgWidth/(points.length-1); const line=chartEl.querySelector("[data-chart-line]"); const area=chartEl.querySelector("[data-chart-area]"); if (!line||!area) return ; const coordinates=points.map((value,index)=>{ const normalised=(value-minValue)/range; const y=svgHeight-verticalPadding-normalised*usableHeight; const x=index*stepX; return [x,y];}); const polylinePoints=coordinates.map(([x,y])=>`${x},${y}`).join(" ");line.setAttribute("points",polylinePoints); const areaPath=[`M0 ${svgHeight}`,`L${coordinates[0][0]}${coordinates[0][1]}`,...coordinates.slice(1).map(([x,y])=>`L${x}${y}`),`L${coordinates[coordinates.length-1][0]}${svgHeight}`,"Z",].join(" ");area.setAttribute("d",areaPath); if (labels.length===points.length){ const existingLabels=chartEl.querySelectorAll(".chart-label");existingLabels.forEach((label)=>label.remove());labels.forEach((label,index)=>{ const marker=document.createElement("span");marker.className="chart-label";marker.textContent=label;marker.style.left=`${(index/(labels.length-1||1))*100}%`;chartEl.appendChild(marker);});}} function initialiseAnalytics(){ const buttons=Array.from(document.querySelectorAll("[data-analytics-range]")); const panels=Array.from(document.querySelectorAll("[data-analytics-panel]")); if (!buttons.length) return ; let activeRange=buttons.find((button)=>button.classList.contains("is-active"))?.dataset.analyticsRange||buttons[0]?.dataset.analyticsRange;activateTabGroup(buttons,activeRange,{buttonDatasetKey:"analyticsRange",panels,panelDatasetKey:"analyticsPanel",shouldFocus:false,});buttons.forEach((button,index)=>{button.addEventListener("click",()=>{ const target=button.dataset.analyticsRange; if (!target) return ;activeRange=target;activateTabGroup(buttons,target,{buttonDatasetKey:"analyticsRange",panels,panelDatasetKey:"analyticsPanel",});});button.addEventListener("keydown",(event)=>{ const {key}=event; const isNext=key==="ArrowRight"||key==="ArrowDown"; const isPrev=key==="ArrowLeft"||key==="ArrowUp"; if (!isNext&&!isPrev) return ;event.preventDefault(); const offset=isNext?1:-1; const nextIndex=(index+offset+buttons.length)%buttons.length; const nextButton=buttons[nextIndex]; const target=nextButton?.dataset.analyticsRange; if (!target) return ;activeRange=target;activateTabGroup(buttons,target,{buttonDatasetKey:"analyticsRange",panels,panelDatasetKey:"analyticsPanel",});});});document.querySelectorAll("[data-chart]").forEach((chartEl)=>{renderChart(chartEl);});} function bindNotificationActions(){ const list=document.querySelector("[data-notification-list]"); if (!list) return ; const emptyState=document.querySelector("[data-notification-empty]");list.addEventListener("click",(event)=>{ const target=event.target; if (!(target instanceof HTMLElement)) return ; if (!target.matches("[data-dismiss-notification]")) return ; const item=target.closest(".notification-item"); if (!item) return ;item.remove(); if (!list.children.length&&emptyState){emptyState.hidden=false;}});}document.addEventListener("DOMContentLoaded",()=>{bindProductFilters();bindOrderFilters();initialiseAnalytics();bindNotificationActions();});})(); \ No newline at end of file diff --git a/src/app/web/static/js/home-tabs.min.js b/src/app/web/static/js/home-tabs.min.js new file mode 100644 index 0000000..a11e063 --- /dev/null +++ b/src/app/web/static/js/home-tabs.min.js @@ -0,0 +1 @@ +( function (){ const tabLists=document.querySelectorAll('[role="tablist"]'); if (!tabLists.length){ return ;} const activateTab=(tabs,panels,tab,{focus=true}={})=>{ const panelId=tab.getAttribute('aria-controls'); const nextPanel=panelId?document.getElementById(panelId):null;tabs.forEach((candidate)=>{ const isSelected=candidate===tab;candidate.setAttribute('aria-selected',String(isSelected));candidate.tabIndex=isSelected?0:-1;candidate.classList.toggle('is-active',isSelected);});panels.forEach((panel)=>{ if (!panel){ return ;} const shouldShow=panel===nextPanel; if (shouldShow){panel.removeAttribute('hidden');} else {panel.setAttribute('hidden','');}}); if (focus){tab.focus();}};tabLists.forEach((tabList)=>{ const tabs=Array.from(tabList.querySelectorAll('[role="tab"]')); const panels=tabs.map((tab)=>{ const panelId=tab.getAttribute('aria-controls'); return panelId?document.getElementById(panelId):null;}); if (!tabs.length){ return ;} const initiallySelected=tabs.find((tab)=>tab.getAttribute('aria-selected')==='true')||tabs[0];activateTab(tabs,panels,initiallySelected,{focus:false});tabList.addEventListener('click',(event)=>{ const target=event.target.closest('[role="tab"]'); if (!target||!tabList.contains(target)){ return ;}event.preventDefault();activateTab(tabs,panels,target);});tabList.addEventListener('keydown',(event)=>{ const {key}=event; const currentIndex=tabs.indexOf(document.activeElement); let nextIndex=null; if (key==='ArrowRight'||key==='ArrowDown'){nextIndex=(currentIndex+1)%tabs.length;} else if (key==='ArrowLeft'||key==='ArrowUp'){nextIndex=(currentIndex-1+tabs.length)%tabs.length;} else if (key==='Home'){nextIndex=0;} else if (key==='End'){nextIndex=tabs.length-1;} if (nextIndex===null){ return ;}event.preventDefault(); const nextTab=tabs[nextIndex];activateTab(tabs,panels,nextTab);});});})(); \ No newline at end of file diff --git a/src/app/web/static/js/moderation-dashboard.min.js b/src/app/web/static/js/moderation-dashboard.min.js new file mode 100644 index 0000000..74c1a0f --- /dev/null +++ b/src/app/web/static/js/moderation-dashboard.min.js @@ -0,0 +1 @@ +( function (){ function copyToClipboard(text){ if (!text) return Promise.resolve(false); if (navigator.clipboard&&typeof navigator.clipboard.writeText===" function "){ return navigator.clipboard .writeText(text).then( function (){ return true;}, function (){ return legacyCopy(text);});} return Promise.resolve(legacyCopy(text));} function legacyCopy(text){ var textarea=document.createElement("textarea");textarea.value=text;textarea.setAttribute("readonly","readonly");textarea.style.position="fixed";textarea.style.top="-1000px";document.body.appendChild(textarea);textarea.select(); var succeeded=false;try{succeeded=document.execCommand("copy");}catch(err){succeeded=false;}document.body.removeChild(textarea); return succeeded;} function toggleState(element,attribute,state){ if (!element) return ;element.setAttribute(attribute,String(Boolean(state))); if (state){element.classList.add("is-active");} else {element.classList.remove("is-active");}} function activateTab(trigger,options){ if (!trigger) return ; var opts=options||{}; var container=trigger.closest("[data-tab-group]"); if (!container) return ; var tabs=container.querySelectorAll('[data-action="jump-section"]');Array.prototype.forEach.call(tabs, function (tab){tab.classList.remove("is-active");});trigger.classList.add("is-active"); if (!opts.silent){ensureTabVisible(container,trigger);}requestFrame( function (){updateTabIndicator(container,trigger);});} function scrollToTarget(targetSelector){ if (!targetSelector) return false; var target=document.querySelector(targetSelector); if (!target) return false; if (typeof target.scrollIntoView===" function "){target.scrollIntoView({behavior:"smooth",block:"start"}); return true;} return false;} function handleAction(trigger,action,event){switch(action){case "share-report":{ var shareUrl=trigger.dataset.shareUrl||window.location.href;copyToClipboard(shareUrl).then( function (success){ var message=success?trigger.dataset.feedbackSuccess||"Tautan laporan disalin":trigger.dataset.feedbackError||"Tidak dapat menyalin tautan";trigger.setAttribute("data-feedback",message);trigger.classList.add("has-feedback");window.setTimeout( function (){trigger.classList.remove("has-feedback");trigger.removeAttribute("data-feedback");},2000);});break;}case "refresh-snapshot":{window.location.reload();break;}case "focus-mode":{ var focusTarget=trigger.dataset.focusTarget; if (focusTarget){window.location.href=focusTarget; return ;}document.body.classList.toggle("is-focus-mode");toggleState(trigger,"aria-pressed",document.body.classList.contains("is-focus-mode"));break;}case "bulk-action":{ var targetSelector=trigger.dataset.bulkTarget; var bulkContainer=targetSelector?document.querySelector(targetSelector):null; var isActive=trigger.getAttribute("aria-pressed")==="true";isActive=!isActive;toggleState(trigger,"aria-pressed",isActive); if (bulkContainer){bulkContainer.classList.toggle("is-bulk-select",isActive);}break;}case "invite-resend":{ var email=trigger.dataset.inviteEmail||"pengguna";dispatchToast("Undangan dikirim ulang ke "+email+".");break;}case "invite-cancel":{ var cancelEmail=trigger.dataset.inviteEmail||"pengguna";dispatchToast("Undangan untuk "+cancelEmail+" dibatalkan.");break;}case "invite-open":{dispatchToast("Panel undangan dibuka.");break;}case "jump-section":{ var targetSelector=trigger.dataset.target||trigger.getAttribute("href"); var scrolled=scrollToTarget(targetSelector);activateTab(trigger); if (scrolled&&event&&typeof event.preventDefault===" function "){event.preventDefault();} return scrolled;}case "scroll-top":{ if (event&&typeof event.preventDefault===" function "){event.preventDefault();} if (typeof window.scrollTo===" function "){window.scrollTo({top:0,behavior:"smooth"});} else {window.scrollTo(0,0);} return true;}case "view-sop":case "add-moderator":default:{break;}}} function dispatchToast(message){ if (!message) return ; var event;try{event=new CustomEvent("mod:toast",{detail:{message:message}});}catch(error){event=document.createEvent("CustomEvent");event.initCustomEvent("mod:toast",false,false,{message:message});}document.dispatchEvent(event);} function bindActions(){document.addEventListener("click", function (event){ var target=event.target; if (!(target instanceof Element)){ return ;} var trigger=target.closest("[data-action]"); if (!trigger){ return ;} var action=trigger.dataset.action; if (!action){ return ;} var handled=handleAction(trigger,action,event); if (handled===true&&typeof event.preventDefault===" function "){event.preventDefault();}});}document.addEventListener("DOMContentLoaded", function (){bindActions();setupTabGroups();updateAllTabIndicators();}); if (typeof window!=="undefined"){window.addEventListener("resize", function (){updateAllTabIndicators();});window.addEventListener("orientationchange", function (){updateAllTabIndicators();});} function ensureTabVisible(container,tab){ if (!container||!tab) return ; var scroller=container.querySelector(".tab-scroller"); if (!scroller){ return ;} var left=tab.offsetLeft; var right=left+tab.offsetWidth; var viewStart=scroller.scrollLeft; var viewEnd=viewStart+scroller.clientWidth; var targetLeft=null; if (leftviewEnd){targetLeft=right-scroller.clientWidth;} if (targetLeft===null){ return ;} if (typeof scroller.scrollTo===" function "){scroller.scrollTo({left:targetLeft,behavior:"smooth"});} else {scroller.scrollLeft=targetLeft;}} function requestFrame(callback){ if (typeof window!=="undefined"&&typeof window.requestAnimationFrame===" function "){window.requestAnimationFrame(callback); return ;}callback();} function updateTabIndicator(container,activeTab){ if (!container||!activeTab) return ; var indicator=container.querySelector(".tab-indicator"); if (!indicator) return ; var scroller=container.querySelector(".tab-scroller")||container; var activeRect=activeTab.getBoundingClientRect(); var scrollerRect=scroller.getBoundingClientRect(); var scrollLeft=typeof scroller.scrollLeft==="number"?scroller.scrollLeft:0; var offset=activeRect.left-scrollerRect.left+scrollLeft;indicator.style.transform="translateX("+offset+"px)";indicator.style.width=activeRect.width+"px";indicator.classList.add("is-visible");} function updateAllTabIndicators(){ var groups=document.querySelectorAll("[data-tab-group]"); if (!groups.length){ return ;}requestFrame( function (){Array.prototype.forEach.call(groups, function (group){ var active=group.querySelector(".tab-link.is-active"); if (active){updateTabIndicator(group,active);}});});} function setupTabGroups(){ var groups=document.querySelectorAll("[data-tab-group]"); if (!groups.length){ return ;}Array.prototype.forEach.call(groups, function (group){ var tabs=group.querySelectorAll('[data-action="jump-section"]'); if (!tabs.length){ return ;} var initialActive=group.querySelector(".tab-link.is-active")||tabs[0]; if (initialActive){activateTab(initialActive,{silent:true});} if (typeof IntersectionObserver!==" function "){ return ;} var tabSections=[];Array.prototype.forEach.call(tabs, function (tab){ var targetSelector=tab.dataset.target||tab.getAttribute("href"); if (!targetSelector){ return ;} var section=document.querySelector(targetSelector); if (!section){ return ;}tabSections.push({tab:tab,section:section});}); if (!tabSections.length){ return ;} var observer=new IntersectionObserver( function (entries){ var candidate=null; for ( var i=0;i{ const navbars=Array.from(document.querySelectorAll(".navbar")); const configs=[]; const mobileQuery=window.matchMedia("(max-width:1024px)");navbars.forEach((nav)=>{ const toggle=nav.querySelector(".navbar-toggle"); const collapsible=nav.querySelector(".navbar-collapsible"); if (!toggle||!collapsible){ return ;} const setOpenState=(isOpen)=>{nav.dataset.open=isOpen?"true":"false";toggle.setAttribute("aria-expanded",isOpen?"true":"false");toggle.setAttribute("aria-label",isOpen?"Tutup navigasi":"Buka navigasi"); if (mobileQuery.matches){collapsible.setAttribute("aria-hidden",isOpen?"false":"true");} else {collapsible.setAttribute("aria-hidden","false");} if (isOpen){nav.classList.add("navbar--open");} else {nav.classList.remove("navbar--open");}};setOpenState(false); const handleBreakpointChange=()=>{ const isOpen=nav.dataset.open==="true";setOpenState(isOpen);}; if (typeof mobileQuery.addEventListener===" function "){mobileQuery.addEventListener("change",handleBreakpointChange);} else if (typeof mobileQuery.addListener===" function "){mobileQuery.addListener(handleBreakpointChange);} const toggleOpen=()=>{ const isOpen=nav.dataset.open==="true";setOpenState(!isOpen);}; const close=()=>setOpenState(false);toggle.addEventListener("click",(event)=>{event.preventDefault();toggleOpen();});collapsible.addEventListener("click",(event)=>{ const interactive=event.target.closest("a,button"); if (!interactive||interactive===toggle){ return ;}close();});configs.push({toggle,close,nav});}); if (configs.length===0){ return ;}document.addEventListener("keydown",(event)=>{ if (event.key!=="Escape"){ return ;}configs.forEach(({nav,toggle,close})=>{ if (nav.dataset.open==="true"){close();toggle.focus();}});});}); \ No newline at end of file diff --git a/src/app/web/static/js/newsletter.js b/src/app/web/static/js/newsletter.js new file mode 100644 index 0000000..3a0e594 --- /dev/null +++ b/src/app/web/static/js/newsletter.js @@ -0,0 +1,57 @@ +(function () { + document.addEventListener("DOMContentLoaded", () => { + const form = document.querySelector(".newsletter-form"); + if (!form) { + return; + } + + const statusRegion = form.querySelector("#newsletter-status"); + const submitButton = form.querySelector("button[type='submit']"); + + const setStatus = (message, tone) => { + if (!statusRegion) { + return; + } + statusRegion.textContent = message; + statusRegion.dataset.tone = tone; + }; + + form.addEventListener("submit", async (event) => { + event.preventDefault(); + event.stopPropagation(); + + if (!submitButton) { + return; + } + + const formData = new FormData(form); + submitButton.disabled = true; + setStatus("Mengirim...", "pending"); + + try { + const response = await fetch(form.action, { + method: "POST", + headers: { + Accept: "application/json", + }, + body: formData, + }); + + const payload = await response.json().catch(() => null); + + if (response.ok) { + const successMessage = payload?.message ?? "Terima kasih! Email kamu sudah terdaftar."; + setStatus(successMessage, "success"); + form.reset(); + } else { + const errorMessage = payload?.message ?? "Alamat email tidak valid. Coba lagi."; + setStatus(errorMessage, "error"); + } + } catch (error) { + setStatus("Terjadi kendala jaringan. Silakan coba lagi.", "error"); + } finally { + submitButton.disabled = false; + } + }); + }); +})(); diff --git a/src/app/web/static/js/newsletter.min.js b/src/app/web/static/js/newsletter.min.js new file mode 100644 index 0000000..3e9d1db --- /dev/null +++ b/src/app/web/static/js/newsletter.min.js @@ -0,0 +1 @@ +( function (){document.addEventListener("DOMContentLoaded",()=>{ const form=document.querySelector(".newsletter-form"); if (!form){ return ;} const statusRegion=form.querySelector("#newsletter-status"); const submitButton=form.querySelector("button[type='submit']"); const setStatus=(message,tone)=>{ if (!statusRegion){ return ;}statusRegion.textContent=message;statusRegion.dataset.tone=tone;};form.addEventListener("submit",async(event)=>{event.preventDefault();event.stopPropagation(); if (!submitButton){ return ;} const formData=new FormData(form);submitButton.disabled=true;setStatus("Mengirim...","pending");try{ const response=await fetch(form.action,{method:"POST",headers:{Accept:"application/json",},body:formData,}); const payload=await response.json().catch(()=>null); if (response.ok){ const successMessage=payload?.message??"Terima kasih!Email kamu sudah terdaftar.";setStatus(successMessage,"success");form.reset();} else { const errorMessage=payload?.message??"Alamat email tidak valid. Coba lagi.";setStatus(errorMessage,"error");}}catch(error){setStatus("Terjadi kendala jaringan. Silakan coba lagi.","error");}finally{submitButton.disabled=false;}});});})(); \ No newline at end of file diff --git a/src/app/web/static/js/nusantarum-tabs.min.js b/src/app/web/static/js/nusantarum-tabs.min.js new file mode 100644 index 0000000..5e9b362 --- /dev/null +++ b/src/app/web/static/js/nusantarum-tabs.min.js @@ -0,0 +1 @@ +( function (){ function setupTabs(){ const tablist=document.querySelector('.nusantarum-tabs'); const tabPanel=document.getElementById('nusantarum-tab-content'); if (!tablist){ return ;} const tabs=Array.from(tablist.querySelectorAll('[role="tab"]')); if (!tabs.length){ return ;} function updateTabState(selectedTab,{shouldFocus=true}={}){ if (!selectedTab){ return ;}tabs.forEach((tab)=>{ const isActive=tab===selectedTab;tab.setAttribute('aria-selected',isActive?'true':'false');tab.setAttribute('tabindex',isActive?'0':'-1');tab.classList.toggle('is-active',isActive); if (isActive&&shouldFocus){tab.focus();}}); if (tabPanel){tabPanel.setAttribute('aria-busy','true');}}tabs.forEach((tab)=>{ const isActive=tab.getAttribute('aria-selected')==='true';tab.setAttribute('tabindex',isActive?'0':'-1');tab.classList.toggle('is-active',isActive); if (tab.dataset.nusantarumTabsBound==='true'){ return ;}tab.dataset.nusantarumTabsBound='true';tab.addEventListener('click',(event)=>{updateTabState(event.currentTarget);});tab.addEventListener('keydown',(event)=>{ const currentIndex=tabs.indexOf(event.currentTarget); if (currentIndex<0){ return ;} let nextTab=null;switch(event.key){case 'ArrowRight':case 'ArrowDown':nextTab=tabs[(currentIndex+1)%tabs.length];break;case 'ArrowLeft':case 'ArrowUp':nextTab=tabs[(currentIndex-1+tabs.length)%tabs.length];break;case 'Home':nextTab=tabs[0];break;case 'End':nextTab=tabs[tabs.length-1];break;default: return ;} if (nextTab){event.preventDefault();nextTab.focus();nextTab.click();}});});} function handleBeforeRequest(event){ const tabPanel=document.getElementById('nusantarum-tab-content'); if (!tabPanel){ return ;} const requestElement=event.detail?event.detail.elt:event.target; if (!requestElement){ return ;} const hxTarget=requestElement.getAttribute('hx-target'); if (hxTarget==='#nusantarum-tab-content'){tabPanel.setAttribute('aria-busy','true');}} function handleAfterSwap(event){ const tabPanel=document.getElementById('nusantarum-tab-content'); if (!tabPanel){ return ;} const swapTarget=event.detail?event.detail.target:null; if (event.target===tabPanel||swapTarget===tabPanel){tabPanel.setAttribute('aria-busy','false');setupTabs();}}document.addEventListener('htmx:beforeRequest',handleBeforeRequest);document.addEventListener('htmx:afterSwap',handleAfterSwap); if (document.readyState==='loading'){document.addEventListener('DOMContentLoaded',setupTabs);} else {setupTabs();}})(); \ No newline at end of file diff --git a/src/app/web/static/js/session.min.js b/src/app/web/static/js/session.min.js new file mode 100644 index 0000000..01a8f8d --- /dev/null +++ b/src/app/web/static/js/session.min.js @@ -0,0 +1 @@ +( function (){ function handleLogout(event){ const trigger=event.target.closest('[data-action="logout"]'); if (!trigger){ return ;}event.preventDefault(); if (trigger.dataset.loading==='true'){ return ;}trigger.dataset.loading='true';trigger.classList.add('is-loading');fetch('/api/auth/logout',{method:'POST'}).catch(()=>{}).finally(()=>{ const redirect=trigger.dataset.redirect; if (redirect){window.location.href=redirect;} else {window.location.reload();}});}document.addEventListener('click',handleLogout);})(); \ No newline at end of file diff --git a/src/app/web/templates/__init__.py b/src/app/web/templates/__init__.py index 1700ef3..b8df4ce 100644 --- a/src/app/web/templates/__init__.py +++ b/src/app/web/templates/__init__.py @@ -15,5 +15,6 @@ template_engine.env.globals.update( app_name=settings.app_name, static_asset_version=settings.static_asset_version, - static_asset_query=f"v={settings.static_asset_version}", + static_asset_query=settings.static_asset_version, + use_minified=getattr(settings, 'use_minified_assets', False), ) diff --git a/src/app/web/templates/auth.html b/src/app/web/templates/auth.html index e67fcad..68aa58b 100644 --- a/src/app/web/templates/auth.html +++ b/src/app/web/templates/auth.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% block head_extra %} - + {% endblock %} {% block content %} diff --git a/src/app/web/templates/base.html b/src/app/web/templates/base.html index 01baee4..8bb496a 100644 --- a/src/app/web/templates/base.html +++ b/src/app/web/templates/base.html @@ -4,16 +4,36 @@ {{ title or app_name }} – Sensasiwangi.id + + + + + + + - - - {% block head_extra %}{% endblock %} - + + + + + + {% block head_extra %}{% endblock %} + + + +
@@ -25,8 +45,8 @@ {% include 'partials/footer.html' %} {% block body_scripts %} - - + + {% endblock %} diff --git a/src/app/web/templates/index.html b/src/app/web/templates/index.html index 4c25f7d..325f324 100644 --- a/src/app/web/templates/index.html +++ b/src/app/web/templates/index.html @@ -434,64 +434,6 @@

Siap ikut meracik wewangian Nusantara?

{% block body_scripts %} {{ super() }} - - + + {% endblock %} diff --git a/src/app/web/templates/pages/brand/detail.html b/src/app/web/templates/pages/brand/detail.html index b7c9152..a85ca5d 100644 --- a/src/app/web/templates/pages/brand/detail.html +++ b/src/app/web/templates/pages/brand/detail.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% block head_extra %} - + {% endblock %} {% block content %} diff --git a/src/app/web/templates/pages/brand/form.html b/src/app/web/templates/pages/brand/form.html index 6fc87c0..9385281 100644 --- a/src/app/web/templates/pages/brand/form.html +++ b/src/app/web/templates/pages/brand/form.html @@ -2,7 +2,7 @@ {% block head_extra %} {{ super() }} - + {% endblock %} {% block content %} diff --git a/src/app/web/templates/pages/brand/index.html b/src/app/web/templates/pages/brand/index.html index a15cb80..2aaea55 100644 --- a/src/app/web/templates/pages/brand/index.html +++ b/src/app/web/templates/pages/brand/index.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% block head_extra %} - + {% endblock %} {% block content %} diff --git a/src/app/web/templates/pages/dashboard/brand_owner.html b/src/app/web/templates/pages/dashboard/brand_owner.html index b95c109..9f47b40 100644 --- a/src/app/web/templates/pages/dashboard/brand_owner.html +++ b/src/app/web/templates/pages/dashboard/brand_owner.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% block head_extra %} - + {% endblock %} {% block content %} @@ -520,5 +520,5 @@

Log Aktivitas Tim

{% block body_scripts %} {{ super() }} - + {% endblock %} diff --git a/src/app/web/templates/pages/dashboard/moderation.html b/src/app/web/templates/pages/dashboard/moderation.html index de2b912..e87390f 100644 --- a/src/app/web/templates/pages/dashboard/moderation.html +++ b/src/app/web/templates/pages/dashboard/moderation.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% block head_extra %} - + {% endblock %} {% block content %} @@ -484,5 +484,5 @@

Form Masukan Cepat

{% block body_scripts %} {{ super() }} - + {% endblock %} diff --git a/src/app/web/templates/pages/nusantarum/index.html b/src/app/web/templates/pages/nusantarum/index.html index 9dfb818..04181eb 100644 --- a/src/app/web/templates/pages/nusantarum/index.html +++ b/src/app/web/templates/pages/nusantarum/index.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% block head_extra %} - + {% endblock %} {% block content %} @@ -123,7 +123,7 @@

Nusantarum

{% block body_scripts %} {{ super() }} - + +``` + +**Benefits**: +- Scripts don't block HTML parsing +- Scripts execute in order after DOM is ready +- Better perceived page load time + +### 7. Conditional Minified Assets + +**Location**: `src/app/core/config.py`, `src/app/web/templates/__init__.py` + +Added configuration to toggle between development and production assets: + +```python +# In config.py +use_minified_assets: bool = False + +# In templates +{{ url_for('static', path='css/base.min.css') if use_minified else url_for('static', path='css/base.css') }} +``` + +**Benefits**: +- Easy switching between dev and prod builds +- Maintains readable code in development +- Optimized delivery in production + +### 8. Extracted Inline Scripts + +**Location**: `src/app/web/static/js/newsletter.js` + +Moved inline JavaScript to separate cacheable files: + +- Newsletter form handling → `newsletter.js` +- Other inline scripts can be extracted similarly + +**Benefits**: +- Better caching (inline scripts can't be cached) +- Reduced HTML size +- Easier maintenance and testing + +### 9. HTMX Optimization + +**Location**: `src/app/web/templates/base.html` + +Added module preload for HTMX: + +```html + + +``` + +**Benefits**: +- Browser preloads HTMX while parsing HTML +- HTMX executes after DOM is ready (defer) +- Maintains full HTMX functionality + +## Performance Metrics + +### Before Optimizations +- Total CSS: ~128KB uncompressed +- Total JS: ~30KB uncompressed +- No caching headers +- No compression +- Blocking font loading +- Inline scripts + +### After Optimizations +- Total CSS: ~105KB minified (~60KB gzipped) +- Total JS: ~24KB minified (~14KB gzipped) +- 1-year cache for static assets +- GZip compression enabled +- Non-blocking font loading +- Extracted, cached scripts + +### Estimated Improvements +- **Initial Load**: 30-40% faster (reduced payload + compression) +- **Repeat Visits**: 70-80% faster (aggressive caching) +- **Time to Interactive**: 20-30% improvement (deferred scripts) +- **Largest Contentful Paint**: 15-25% improvement (font optimization) + +## Configuration + +### Environment Variables + +```bash +# Enable minified assets (production) +USE_MINIFIED_ASSETS=true + +# Enable GZip compression (default: true) +ENABLE_COMPRESSION=true + +# Static asset version (for cache busting) +STATIC_ASSET_VERSION=2024051901 +``` + +### Production Deployment + +1. Minify assets: + ```bash + python3 scripts/minify_assets.py + ``` + +2. Set environment variables: + ```bash + export USE_MINIFIED_ASSETS=true + export ENABLE_COMPRESSION=true + ``` + +3. Deploy application + +### Development Mode + +Keep `USE_MINIFIED_ASSETS=false` for easier debugging with readable source files. + +## Future Optimizations + +### Potential Improvements + +1. **Critical CSS Extraction**: Extract above-the-fold CSS and inline it +2. **Image Optimization**: Add WebP support, lazy loading, responsive images +3. **HTTP/2 Server Push**: Push critical assets before they're requested +4. **Service Worker**: Implement offline-first caching strategy +5. **Brotli Compression**: Add Brotli for even better compression than GZip +6. **CSS Purging**: Remove unused CSS rules in production +7. **Code Splitting**: Split CSS/JS by route for smaller initial bundles +8. **Preload Key Requests**: Preload critical API calls +9. **Resource Prioritization**: Use `fetchpriority` for critical resources +10. **CDN Integration**: Serve static assets from CDN + +### Monitoring + +Consider adding: +- Web Vitals tracking (Core Web Vitals) +- Real User Monitoring (RUM) +- Lighthouse CI for automated performance testing +- Bundle size tracking in CI/CD + +## Testing + +### Verify Optimizations + +1. **Check minified files exist**: + ```bash + ls -lh src/app/web/static/css/*.min.css + ls -lh src/app/web/static/js/*.min.js + ``` + +2. **Test compression**: + ```bash + curl -H "Accept-Encoding: gzip" -I http://localhost:8000/static/css/base.min.css + # Should include: Content-Encoding: gzip + ``` + +3. **Verify cache headers**: + ```bash + curl -I http://localhost:8000/static/css/base.min.css?v=2024051901 + # Should include: Cache-Control: public, max-age=31536000, immutable + ``` + +4. **Lighthouse audit**: + ```bash + lighthouse http://localhost:8000 --view + ``` + +## Troubleshooting + +### Minification Issues + +If minified assets cause issues: +1. Set `USE_MINIFIED_ASSETS=false` +2. Check browser console for errors +3. Verify minified files exist and are valid +4. Re-run minification script + +### Cache Issues + +If changes aren't reflected: +1. Update `STATIC_ASSET_VERSION` in config +2. Clear browser cache +3. Use incognito/private browsing +4. Check Network tab for cache hits + +### Compression Issues + +If compression causes problems: +1. Set `ENABLE_COMPRESSION=false` +2. Check client Accept-Encoding headers +3. Verify middleware order in application.py + +## Maintainers + +When modifying CSS or JavaScript: +1. Edit source files (non-minified versions) +2. Run minification script: `python3 scripts/minify_assets.py` +3. Test both minified and non-minified versions +4. Update `STATIC_ASSET_VERSION` if deploying + +## References + +- [Web.dev Performance Guide](https://web.dev/performance/) +- [MDN Web Performance](https://developer.mozilla.org/en-US/docs/Web/Performance) +- [HTMX Documentation](https://htmx.org/docs/) +- [FastAPI Performance](https://fastapi.tiangolo.com/advanced/middleware/)