feat: Overlay-Look für ALLE Unterseiten — Milchglas + zentrierte Card (Impressum, Login, Register, Profil, Beitrag)
Quality Gate / 🛡️ Lint + Type Check + Tests (push) Failing after 12m30s

This commit is contained in:
Hermes Agent
2026-06-25 10:03:08 +02:00
parent d108618965
commit ff7d96a3a8
+70 -186
View File
@@ -2433,23 +2433,7 @@ def _get_user_from_session(db) -> dict | None:
# ── HTML-Templates (plain strings, no f-string needed) ──────────────
_REGISTER_TPL = """<!DOCTYPE html>
<html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, user-scalable=no">
<title>Registrieren BSN Community</title>
<style>
body{font-family:-apple-system,sans-serif;background:#1a1a2e;color:#eee;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0}
.card{background:#16213e;border-radius:16px;padding:2rem;width:100%;max-width:400px;box-shadow:0 8px 32px #00000040}
h1{color:#20228a;margin:0 0 1rem;font-size:1.4rem;text-align:center}
input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px solid #333;background:#0f3460;color:#fff;box-sizing:border-box;font-size:1rem}
.btn{width:100%;padding:0.8rem;background:#20228a;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;margin-top:0.5rem}
.btn:hover{background:#2a2caa}
.error{color:#ff453a;font-size:0.85rem;margin:0.5rem 0}
.hint{color:#888;font-size:0.75rem;margin:0.2rem 0 0.5rem}
.link{text-align:center;margin-top:1rem;font-size:0.85rem}
.link a{color:#5e5ce6}
</style></head><body>
<div class="card">
<h1>📋 Registrieren</h1>
_REGISTER_CONTENT = """<h1>📋 Registrieren</h1>
{error_html}
<form method="post">
<input name="email" placeholder="E-Mail (oder Telefon)" value="{email_val}" required>
@@ -2458,67 +2442,21 @@ input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px sol
<input name="password" type="password" placeholder="Passwort (min. 4 Zeichen)" required>
<button class="btn" type="submit">Registrieren</button>
</form>
<div class="link">Bereits registriert? <a href="/login">Einloggen</a></div>
</div></body></html>"""
<div class="link">Bereits registriert? <a href="/login">Einloggen</a></div>"""
_LOGIN_TPL = """<!DOCTYPE html>
<html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, user-scalable=no">
<title>Login BSN Community</title>
<style>
body{font-family:-apple-system,sans-serif;background:#1a1a2e;color:#eee;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0}
.card{background:#16213e;border-radius:16px;padding:2rem;width:100%;max-width:400px;box-shadow:0 8px 32px #00000040}
h1{color:#20228a;margin:0 0 1rem;font-size:1.4rem;text-align:center}
input{width:100%;padding:0.8rem;margin:0.4rem 0;border-radius:8px;border:1px solid #333;background:#0f3460;color:#fff;box-sizing:border-box;font-size:1rem}
.btn{width:100%;padding:0.8rem;background:#20228a;color:#fff;border:none;border-radius:8px;font-size:1rem;cursor:pointer;margin-top:0.5rem}
.btn:hover{background:#2a2caa}
.error{color:#ff453a;font-size:0.85rem;margin:0.5rem 0}
.hint{color:#888;font-size:0.75rem;margin:0.2rem 0 0.5rem}
.link{text-align:center;margin-top:1rem;font-size:0.85rem}
.link a{color:#5e5ce6}
</style></head><body>
<div class="card">
<h1>🔐 Login</h1>
_LOGIN_CONTENT = """<h1>🔐 Login</h1>
{error_html}
<form method="post">
<input name="login_id" placeholder="E-Mail oder Telefon" value="{login_val}" required>
<div class="hint">E-Mail oder Telefonnummer</div>
<input name="login_id" placeholder="E-Mail (oder Telefon)" value="{login_val}" required>
<input name="password" type="password" placeholder="Passwort" required>
<button class="btn" type="submit">Einloggen</button>
</form>
<div class="link">Neu hier? <a href="/register">Registrieren</a></div>
</div></body></html>"""
<div class="link">Noch nicht registriert? <a href="/register">Registrieren</a></div>"""
_PROFILE_TPL = """<!DOCTYPE html>
<html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, user-scalable=no">
<title>Profil BSN Community</title>
<style>
body{font-family:-apple-system,sans-serif;background:#1a1a2e;color:#eee;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0}
.card{background:#16213e;border-radius:16px;padding:2rem;width:100%;max-width:400px;box-shadow:0 8px 32px #00000040}
h1{color:#20228a;margin:0 0 1rem;font-size:1.4rem}
.field{margin:0.8rem 0}
.label{color:#888;font-size:0.8rem;text-transform:uppercase}
.value{font-size:1.1rem}
input{width:100%;padding:0.6rem;margin:0.3rem 0;border-radius:6px;border:1px solid #333;background:#0f3460;color:#fff;box-sizing:border-box}
.btn{padding:0.6rem 1.2rem;background:#20228a;color:#fff;border:none;border-radius:6px;cursor:pointer}
.btn:hover{background:#2a2caa}
.msg{color:#30d158;margin:0.5rem 0}
.nav{margin-top:1rem;display:flex;gap:0.5rem}
</style></head><body>
<div class="card">
<h1>👤 Mein Profil</h1>
{msg_html}
<div class="field"><div class="label">E-Mail</div><div class="value">{email}</div></div>
<div class="field"><div class="label">Telefon</div><div class="value">{phone_display}</div></div>
<form method="post">
<div class="field"><div class="label">Name</div>
<input name="name" value="{name}" placeholder="Dein Name"></div>
<button class="btn" type="submit">Speichern</button>
</form>
<div class="nav">
<a href="/" style="color:#5e5ce6"> Startseite</a>
<a href="/logout" style="color:#ff453a">Abmelden</a>
</div>
</div></body></html>"""
_PROFILE_CONTENT = """<h1>👤 Profil</h1>
<p>Telefon: {phone}</p>
<p style="color:#9CA3AF;font-size:0.8rem;">(Letzte 4 Ziffern: {phone_display})</p>
<a href="/logout" class="btn btn-secondary" style="margin-top:1rem;">Abmelden</a>"""
@app.route("/register", methods=["GET", "POST"])
@@ -2538,14 +2476,14 @@ def user_register():
if is_email:
if db.execute("SELECT id FROM users WHERE email=?", (login_id,)).fetchone():
error = "Diese E-Mail ist bereits registriert."
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}",
"<div class='error'>" + error + "</div>")
return _overlay_page("Registrieren", _REGISTER_CONTENT.replace("{email_val}", login_id).replace("{error_html}",
"<div class='error'>" + error + "</div>"), "400px")
else:
existing = _lookup_user_by_phone(db, login_id)
if existing:
error = "Diese Telefonnummer ist bereits registriert."
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}",
"<div class='error'>" + error + "</div>")
return _overlay_page("Registrieren", _REGISTER_CONTENT.replace("{email_val}", login_id).replace("{error_html}",
"<div class='error'>" + error + "</div>"), "400px")
# User anlegen
pw_hash = _hash_password(password)
phone_hash_val = None
@@ -2570,7 +2508,7 @@ def user_register():
resp.set_cookie("bsn_session", token, max_age=30*86400, httponly=True)
return resp
login_id = request.form.get("email", "")
return _REGISTER_TPL.replace("{email_val}", login_id).replace("{error_html}", "")
return _overlay_page("Registrieren", _REGISTER_CONTENT.replace("{email_val}", login_id).replace("{error_html}", ""), "400px")
@app.route("/login", methods=["GET", "POST"])
@@ -2596,7 +2534,7 @@ def user_login():
return resp
login_val = request.form.get("login_id", "")
error_html = "<div class='error'>" + error + "</div>" if error else ""
return _LOGIN_TPL.replace("{login_val}", login_val).replace("{error_html}", error_html)
return _overlay_page("Login", _LOGIN_CONTENT.replace("{login_val}", login_val).replace("{error_html}", error_html), "400px")
@app.route("/logout")
@@ -2629,9 +2567,9 @@ def user_profile():
msg_html = "<div class='msg'>" + msg + "</div>" if msg else ""
phone_display = "✱✱✱✱" + (user.get("phone_last4") or "") if user.get("phone_last4") else "nicht hinterlegt"
email_display = user.get("email") or "nicht hinterlegt"
return _PROFILE_TPL.replace("{phone}", phone_display).replace("{phone_display}", phone_display)\
.replace("{email}", email_display).replace("{name}", user["name"] or "")\
.replace("{msg_html}", msg_html)
phone_display_str = "✱✱✱✱" + (user.get("phone_last4") or "") if user.get("phone_last4") else "nicht hinterlegt"
content = _PROFILE_CONTENT.replace("{phone}", phone_display_str).replace("{phone_display}", phone_display_str)
return _overlay_page("Profil", content, "400px")
@app.route("/")
@@ -3826,8 +3764,8 @@ def generate_image_thumbnail(image_path: str, width: int = 300) -> str | None:
# ── Legal pages ─────────────────────────────────────────────────────
def _legal_page(title, content_html):
"""Render a legal page with BSN header/footer styling."""
def _overlay_page(title, content_html, max_width="640px"):
"""Overlay-Seite: Milchglas-Backdrop + zentrierte weiße Card. Gleicher Look wie Submit-Overlay."""
return f"""<!DOCTYPE html>
<html lang="de">
<head>
@@ -3835,106 +3773,52 @@ def _legal_page(title, content_html):
<title>{title} BSN Community</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700;14..32,800&display=swap" rel="stylesheet">
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#E8E4DC; color:#1a1a2e; min-height:100vh; }}
.site-header {{ background:#20228a; padding:1rem 2rem; display:flex; align-items:center; gap:1rem; position:sticky; top:0; z-index:100; box-shadow:0 2px 12px rgba(0,0,0,0.15); }}
.site-header a {{ color:#fff; text-decoration:none; font-weight:800; font-size:1.15rem; }}
.legal-container {{ max-width:760px; margin:2rem auto; padding:2rem; background:#fff; border-radius:14px; box-shadow:0 4px 18px rgba(0,0,0,0.10); }}
.legal-container h1 {{ font-size:1.8rem; font-weight:800; color:#20228a; margin-bottom:0.3rem; }}
.legal-container .stand {{ font-size:0.78rem; color:#888; margin-bottom:2rem; }}
.legal-container h2 {{ font-size:1.2rem; font-weight:700; color:#20228a; margin:2rem 0 0.5rem; }}
.legal-container h3 {{ font-size:1rem; font-weight:600; margin:1.2rem 0 0.3rem; }}
.legal-container p {{ line-height:1.7; margin-bottom:0.8rem; font-size:0.92rem; }}
.legal-container ul, .legal-container ol {{ margin:0.5rem 0 0.8rem 1.5rem; font-size:0.92rem; line-height:1.7; }}
.legal-container blockquote {{ border-left:3px solid #20228a; padding:0.5rem 1rem; margin:1rem 0; background:#f8f6f1; border-radius:0 8px 8px 0; font-size:0.85rem; color:#555; }}
.legal-container hr {{ border:none; border-top:1px solid #e0ddd6; margin:1.5rem 0; }}
.legal-container a {{ color:#20228a; text-decoration:underline; }}
.back-link {{ display:inline-block; margin-top:2rem; color:#20228a; text-decoration:none; font-weight:600; font-size:0.9rem; }}
.back-link:hover {{ text-decoration:underline; }}
.nav-toggle {{ background:none; border:none; color:#fff; font-size:1.3rem; cursor:pointer; padding:0.3rem 0.4rem; line-height:1; margin-right:0.5rem; transition:opacity 0.2s; }}
.nav-toggle:hover {{ opacity:0.7; }}
.offcanvas-backdrop {{ position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:200; opacity:0; pointer-events:none; transition:opacity 0.3s ease; }}
.offcanvas-backdrop.open {{ opacity:1; pointer-events:auto; }}
.offcanvas-nav {{ position:fixed; top:0; left:0; width:280px; height:100%; height:100dvh; background:#fff; z-index:201; transform:translateX(-100%); transition:transform 0.3s cubic-bezier(0.4,0,0.2,1); box-shadow:4px 0 24px rgba(0,0,0,0.15); display:flex; flex-direction:column; }}
.offcanvas-nav.open {{ transform:translateX(0); }}
.offcanvas-nav-header {{ background:#20228a; color:#fff; padding:1.2rem 1.5rem; display:flex; align-items:center; justify-content:space-between; }}
.offcanvas-nav-header .nav-logo {{ font-weight:800; font-size:1rem; }}
.offcanvas-nav-header .nav-close {{ background:none; border:none; color:#fff; font-size:1.4rem; cursor:pointer; line-height:1; }}
.offcanvas-nav-links {{ list-style:none; padding:1rem 0; flex:1; }}
.offcanvas-nav-links a {{ display:block; padding:0.75rem 1.5rem; color:#1a1a2e; text-decoration:none; font-size:0.95rem; font-weight:500; transition:background 0.15s; }}
.offcanvas-nav-links a:hover {{ background:#f0ede6; }}
.offcanvas-nav-links .nav-section {{ padding:0.5rem 1.5rem 0.2rem; font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:#888; }}
.offcanvas-nav-footer {{ padding:1rem 1.5rem; border-top:1px solid #e0ddd6; font-size:0.75rem; color:#888; }}
.site-header {{ display:flex; align-items:center; gap:0.5rem; }}
@media(max-width:600px) {{ .legal-container {{ margin:1rem; padding:1.2rem; }} }}
body {{ font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#F9FAFB; color:#1F2937; min-height:100vh; display:flex; align-items:flex-start; justify-content:center; padding:2rem 1rem; -webkit-font-smoothing:antialiased; }}
/* Milchglas-Backdrop */
body::before {{ content:''; position:fixed; inset:0; background:rgba(0,0,0,0.08); backdrop-filter:blur(12px); -webkit-backdrop-filter:blur(12px); z-index:0; }}
.overlay-card {{ position:relative; z-index:1; background:#fff; border-radius:18px; box-shadow:0 16px 64px rgba(0,0,0,0.12); width:100%; max-width:{max_width}; padding:2rem; animation:fadeUp 0.3s ease; }}
@keyframes fadeUp {{ from{{opacity:0;transform:translateY(16px);}} to{{opacity:1;transform:translateY(0);}} }}
.overlay-card h1 {{ font-size:1.5rem; font-weight:800; color:#1F2937; margin-bottom:0.3rem; }}
.overlay-card .stand {{ font-size:0.78rem; color:#9CA3AF; margin-bottom:1.5rem; }}
.overlay-card h2 {{ font-size:1.1rem; font-weight:700; color:#374151; margin:1.5rem 0 0.4rem; }}
.overlay-card h3 {{ font-size:0.95rem; font-weight:600; color:#374151; margin:1rem 0 0.3rem; }}
.overlay-card p {{ line-height:1.65; margin-bottom:0.7rem; font-size:0.9rem; color:#4B5563; }}
.overlay-card ul, .overlay-card ol {{ margin:0.5rem 0 0.8rem 1.5rem; font-size:0.9rem; line-height:1.65; color:#4B5563; }}
.overlay-card blockquote {{ border-left:3px solid #E5E7EB; padding:0.5rem 1rem; margin:1rem 0; background:#F9FAFB; border-radius:0 8px 8px 0; font-size:0.85rem; color:#6B7280; }}
.overlay-card hr {{ border:none; border-top:1px solid #E5E7EB; margin:1.5rem 0; }}
.overlay-card a {{ color:#1F2937; text-decoration:underline; text-underline-offset:2px; }}
.overlay-card input, .overlay-card textarea, .overlay-card select {{ width:100%; padding:0.65rem 0.8rem; margin:0.3rem 0; border:1.5px solid #E5E7EB; border-radius:10px; font-size:0.9rem; font-family:inherit; outline:none; transition:border-color 0.15s; background:#fff; }}
.overlay-card input:focus, .overlay-card textarea:focus {{ border-color:#6B7280; }}
.overlay-card .btn {{ display:inline-block; width:100%; padding:0.7rem; background:#1F2937; color:#F9FAFB; border:none; border-radius:10px; font-size:0.9rem; font-weight:600; cursor:pointer; font-family:inherit; transition:background 0.15s; text-align:center; text-decoration:none; margin-top:0.5rem; }}
.overlay-card .btn:hover {{ background:#374151; }}
.overlay-card .btn-secondary {{ background:#F3F4F6; color:#374151; }}
.overlay-card .btn-secondary:hover {{ background:#E5E7EB; }}
.overlay-card .error {{ color:#EF4444; font-size:0.85rem; margin:0.5rem 0; }}
.overlay-card .hint {{ color:#9CA3AF; font-size:0.75rem; margin:0.2rem 0 0.5rem; }}
.overlay-card .link {{ text-align:center; margin-top:1rem; font-size:0.85rem; color:#6B7280; }}
.overlay-card .link a {{ color:#1F2937; font-weight:500; }}
.back-link {{ display:inline-flex; align-items:center; gap:0.3rem; margin-top:1.5rem; color:#9CA3AF; text-decoration:none; font-weight:500; font-size:0.85rem; transition:color 0.15s; }}
.back-link:hover {{ color:#6B7280; }}
@media(max-width:600px) {{ body {{ padding:0.5rem; align-items:flex-start; }} .overlay-card {{ padding:1.2rem; border-radius:14px; }} }}
</style>
</head>
<body>
<header class="site-header">
<button id="navToggle" class="nav-toggle" aria-label="Menü öffnen"></button>
<a href="/">BSN Community</a>
</header>
<main class="legal-container">
<div class="overlay-card">
{content_html}
<a href="/" class="back-link"> Zurück zur Startseite</a>
</main>
<div class="offcanvas-backdrop" id="offcanvasBackdrop"></div>
<nav class="offcanvas-nav" id="offcanvasNav">
<div class="offcanvas-nav-header">
<span class="nav-logo">BSN Community</span>
<button class="nav-close" id="navClose" aria-label="Menü schließen"></button>
</div>
<ul class="offcanvas-nav-links">
<li class="nav-section">Navigation</li>
<li><a href="/">Startseite</a></li>
<li class="nav-section">Community</li>
<li><a href="/register">📋 Registrieren</a></li>
<li><a href="/login">🔐 Login</a></li>
<li class="nav-section">Rechtliches</li>
<li><a href="/impressum">Impressum</a></li>
<li><a href="/datenschutz">Datenschutzerklärung</a></li>
<li><a href="/agb">AGB</a></li>
</ul>
<div class="offcanvas-nav-footer">
© 2026 Brettspiel-News.de
</div>
</nav>
<script>
(function() {{
var toggle = document.getElementById('navToggle');
var close = document.getElementById('navClose');
var nav = document.getElementById('offcanvasNav');
var backdrop = document.getElementById('offcanvasBackdrop');
var main = document.querySelector('main');
var header = document.querySelector('.site-header');
function open() {{
nav.classList.add('open');
backdrop.classList.add('open');
if (main) main.style.filter = 'blur(4px)';
if (header) header.style.filter = 'blur(4px)';
}}
function closeNav() {{
nav.classList.remove('open');
backdrop.classList.remove('open');
if (main) main.style.filter = '';
if (header) header.style.filter = '';
}}
toggle.addEventListener('click', open);
close.addEventListener('click', closeNav);
backdrop.addEventListener('click', closeNav);
}})();
</script>
</div>
</body>
</html>"""
def _legal_page(title, content_html):
"""Legacy-Wrapper — nutzt jetzt _overlay_page."""
return _overlay_page(title, content_html)
@app.route("/impressum")
def legal_impressum():
return _legal_page("Impressum", """<h1>Impressum</h1>
@@ -4709,26 +4593,26 @@ def public_detail(sub_id: int):
<link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700;14..32,800&display=swap" rel="stylesheet">
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#F3EDE4; color:#1a1a2e; min-height:100vh; }}
.site-header {{ background:#20228a; padding:1rem 2rem; display:flex; align-items:center; gap:0.5rem; position:sticky; top:0; z-index:100; }}
.nav-toggle {{ background:none; border:none; color:#fff; font-size:1.3rem; cursor:pointer; padding:0.3rem 0.4rem; line-height:1; margin-right:0.5rem; transition:opacity 0.2s; }}
body {{ font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; background:#F9FAFB; color:#1F2937; min-height:100vh; -webkit-font-smoothing:antialiased; }}
.site-header {{ background:#1F2937; padding:1rem 2rem; display:flex; align-items:center; gap:0.5rem; position:sticky; top:0; z-index:100; }}
.nav-toggle {{ background:none; border:none; color:#D1D5DB; font-size:1.3rem; cursor:pointer; padding:0.3rem 0.4rem; line-height:1; margin-right:0.5rem; transition:opacity 0.2s; }}
.nav-toggle:hover {{ opacity:0.7; }}
.offcanvas-backdrop {{ position:fixed; inset:0; background:rgba(0,0,0,0.4); z-index:200; opacity:0; pointer-events:none; transition:opacity 0.3s ease; }}
.offcanvas-backdrop {{ position:fixed; inset:0; background:rgba(0,0,0,0.3); z-index:200; opacity:0; pointer-events:none; transition:opacity 0.3s ease; }}
.offcanvas-backdrop.open {{ opacity:1; pointer-events:auto; }}
.offcanvas-nav {{ position:fixed; top:0; left:0; width:280px; height:100%; height:100dvh; background:#fff; z-index:201; transform:translateX(-100%); transition:transform 0.3s cubic-bezier(0.4,0,0.2,1); box-shadow:4px 0 24px rgba(0,0,0,0.15); display:flex; flex-direction:column; }}
.offcanvas-nav {{ position:fixed; top:0; left:0; width:280px; height:100%; height:100dvh; background:#fff; z-index:201; transform:translateX(-100%); transition:transform 0.3s cubic-bezier(0.4,0,0.2,1); box-shadow:4px 0 24px rgba(0,0,0,0.1); display:flex; flex-direction:column; }}
.offcanvas-nav.open {{ transform:translateX(0); }}
.offcanvas-nav-header {{ background:#20228a; color:#fff; padding:1.2rem 1.5rem; display:flex; align-items:center; justify-content:space-between; }}
.offcanvas-nav-header {{ background:#1F2937; color:#F9FAFB; padding:1.2rem 1.5rem; display:flex; align-items:center; justify-content:space-between; }}
.offcanvas-nav-header .nav-logo {{ font-weight:800; font-size:1rem; }}
.offcanvas-nav-header .nav-close {{ background:none; border:none; color:#fff; font-size:1.4rem; cursor:pointer; line-height:1; }}
.offcanvas-nav-header .nav-close {{ background:none; border:none; color:#D1D5DB; font-size:1.4rem; cursor:pointer; line-height:1; }}
.offcanvas-nav-links {{ list-style:none; padding:1rem 0; flex:1; }}
.offcanvas-nav-links a {{ display:block; padding:0.75rem 1.5rem; color:#1a1a2e; text-decoration:none; font-size:0.95rem; font-weight:500; transition:background 0.15s; }}
.offcanvas-nav-links a:hover {{ background:#f0ede6; }}
.offcanvas-nav-links .nav-section {{ padding:0.5rem 1.5rem 0.2rem; font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:#888; }}
.offcanvas-nav-footer {{ padding:1rem 1.5rem; border-top:1px solid #e0ddd6; font-size:0.75rem; color:#888; }}
.site-logo {{ display:flex; align-items:center; gap:0.6rem; text-decoration:none; color:#fff; }}
.site-logo .logo-icon {{ width:40px; height:40px; background:#20228a; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:1.3rem; }}
.site-logo .logo-text {{ font-weight:800; font-size:1.15rem; color:#fff; }}
.site-logo-sub {{ font-size:0.7rem; font-weight:500; color:rgba(255,255,255,0.7); letter-spacing:0.03em; margin-left:0.5rem; white-space:nowrap; }}
.offcanvas-nav-links a {{ display:block; padding:0.75rem 1.5rem; color:#1F2937; text-decoration:none; font-size:0.95rem; font-weight:500; transition:background 0.15s; }}
.offcanvas-nav-links a:hover {{ background:#F3F4F6; }}
.offcanvas-nav-links .nav-section {{ padding:0.5rem 1.5rem 0.2rem; font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:#9CA3AF; }}
.offcanvas-nav-footer {{ padding:1rem 1.5rem; border-top:1px solid #E5E7EB; font-size:0.75rem; color:#9CA3AF; }}
.site-logo {{ display:flex; align-items:center; gap:0.6rem; text-decoration:none; color:#F9FAFB; }}
.site-logo .logo-icon {{ width:40px; height:40px; background:#374151; border-radius:8px; display:flex; align-items:center; justify-content:center; font-size:1.3rem; }}
.site-logo .logo-text {{ font-weight:800; font-size:1.15rem; color:#F9FAFB; }}
.site-logo-sub {{ font-size:0.7rem; font-weight:500; color:#9CA3AF; letter-spacing:0.03em; margin-left:0.5rem; white-space:nowrap; }}
.detail-container {{ max-width:720px; margin:2rem auto; padding:0 1.5rem; }}
.back-link {{ display:inline-block; color:#20228a; text-decoration:none; font-weight:600; margin-bottom:1.5rem; font-size:0.9rem; }}
.back-link:hover {{ text-decoration:underline; }}