"""Statische Assets für Jami – Verlieben & neue Leute treffen."""

from __future__ import annotations

import shutil
import urllib.request
from pathlib import Path

LOGO_SRC = Path(__file__).resolve().parent.parent / "data" / "jami-dating-logo.png"
LOGO_FILE = "images/jami-dating-logo.png"
LOGO_MARKUP = (
    f'<a href="#" class="logo">'
    f'<img src="{LOGO_FILE}" alt="" class="logo-img" width="42" height="42" decoding="async"/>'
    f'<span class="logo-text">Jami-Dating</span></a>'
)
LOGO_MARKUP_HOME = (
    f'<a href="/" class="logo">'
    f'<img src="{LOGO_FILE}" alt="" class="logo-img logo-img-sm" width="36" height="36" decoding="async"/>'
    f'<span class="logo-text">Jami-Dating</span></a>'
)

# Pexels – royalty-free Portraits (Referer nötig)
PROFILE_PHOTOS = (
    ("lena.jpg", "https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=600&h=800&fit=crop"),
    ("marco.jpg", "https://images.pexels.com/photos/2379004/pexels-photo-2379004.jpeg?auto=compress&cs=tinysrgb&w=600&h=800&fit=crop"),
    ("amira.jpg", "https://images.pexels.com/photos/1181686/pexels-photo-1181686.jpeg?auto=compress&cs=tinysrgb&w=600&h=800&fit=crop"),
    ("sofia.jpg", "https://images.pexels.com/photos/1239291/pexels-photo-1239291.jpeg?auto=compress&cs=tinysrgb&w=600&h=800&fit=crop"),
    ("yuki.jpg", "https://images.pexels.com/photos/3756679/pexels-photo-3756679.jpeg?auto=compress&cs=tinysrgb&w=600&h=800&fit=crop"),
)
HERO_COUPLE = (
    "hero-couple.jpg",
    "https://images.pexels.com/photos/3601457/pexels-photo-3601457.jpeg?auto=compress&cs=tinysrgb&w=1920&h=1080&fit=crop",
)

CSS = """
:root {
  --bg: #0a0610;
  --card: rgba(255,255,255,0.07);
  --border: rgba(255,255,255,0.14);
  --accent: #ff4d8d;
  --accent2: #b84dff;
  --warm: #ff9a6c;
  --mint: #6ee7d7;
  --text: #fff9fc;
  --muted: #c4b0d4;
  --radius: 22px;
  --shadow: 0 28px 80px rgba(0,0,0,0.5);
  --glass: rgba(18,10,28,0.72);
}
* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: 'DM Sans', system-ui, -apple-system, Segoe UI, sans-serif;
  background: var(--bg);
  background-image:
    radial-gradient(ellipse 80% 50% at 20% -10%, rgba(255,107,157,0.35), transparent),
    radial-gradient(ellipse 60% 40% at 90% 10%, rgba(196,77,255,0.25), transparent),
    radial-gradient(ellipse 50% 30% at 50% 100%, rgba(94,234,212,0.12), transparent);
  color: var(--text);
  min-height: 100vh;
}
a { color: var(--mint); }
.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--border);
  backdrop-filter: blur(12px);
  position: sticky;
  top: 0;
  z-index: 50;
  background: rgba(15,10,20,0.85);
}
.logo {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  text-decoration: none;
  line-height: 1;
}
.logo-img {
  width: clamp(2rem, 7vw, 2.65rem);
  height: clamp(2rem, 7vw, 2.65rem);
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  animation: logo-glow-pulse 2.8s ease-in-out infinite;
  filter: drop-shadow(0 0 10px rgba(255, 77, 141, 0.42));
}
.logo-img-sm {
  width: 2.15rem;
  height: 2.15rem;
}
.logo:hover .logo-img {
  animation: logo-glow-pulse 1.15s ease-in-out infinite, logo-bob 0.65s ease-in-out;
}
.logo-text {
  font-weight: 700;
  font-size: clamp(1.05rem, 3.5vw, 1.45rem);
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
@keyframes logo-glow-pulse {
  0%, 100% {
    transform: scale(1);
    filter: drop-shadow(0 0 8px rgba(255, 77, 141, 0.38));
  }
  50% {
    transform: scale(1.07);
    filter: drop-shadow(0 0 20px rgba(184, 77, 255, 0.62));
  }
}
@keyframes logo-bob {
  0%, 100% { transform: translateY(0) scale(1.07); }
  50% { transform: translateY(-3px) scale(1.09); }
}
@media (prefers-reduced-motion: reduce) {
  .logo-img, .jami-loader-logo { animation: none !important; }
}
.coin-pill {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  background: linear-gradient(135deg, rgba(255,107,157,0.25), rgba(196,77,255,0.2));
  border: 1px solid var(--border);
  padding: 0.45rem 0.9rem;
  border-radius: 999px;
  font-weight: 600;
  transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.coin-pill.coin-pill-pulse {
  animation: coin-pill-pop 0.85s ease;
}
@keyframes coin-pill-pop {
  0% { transform: scale(1); box-shadow: 0 0 0 rgba(255, 200, 80, 0); }
  35% { transform: scale(1.12); box-shadow: 0 0 22px rgba(255, 200, 80, 0.55); }
  100% { transform: scale(1); box-shadow: 0 0 0 rgba(255, 200, 80, 0); }
}
.coin-fly-particle {
  position: fixed;
  z-index: 200;
  font-size: 1.35rem;
  pointer-events: none;
  transition: transform 0.65s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.55s ease;
  will-change: transform, opacity;
  filter: drop-shadow(0 2px 6px rgba(255, 200, 80, 0.6));
}
.coin-success-popup {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(0.85);
  z-index: 150;
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.85rem 1.35rem;
  border-radius: 16px;
  background: linear-gradient(135deg, rgba(30, 18, 40, 0.97), rgba(50, 25, 70, 0.95));
  border: 1px solid rgba(255, 200, 80, 0.45);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45), 0 0 28px rgba(255, 107, 157, 0.25);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.35s cubic-bezier(0.34, 1.4, 0.64, 1);
}
.coin-success-popup.show {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
.coin-success-popup .coin-success-icon {
  font-size: 1.6rem;
  animation: coin-success-spin 0.6s ease;
}
.coin-success-popup .coin-success-text {
  font-weight: 700;
  font-size: 1.05rem;
  color: #fde68a;
}
@keyframes coin-success-spin {
  0% { transform: scale(0.4) rotate(-20deg); }
  60% { transform: scale(1.15) rotate(8deg); }
  100% { transform: scale(1) rotate(0); }
}
.jami-loader {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 2.5rem 1rem;
  min-height: 200px;
  width: 100%;
}
.jami-loader-compact {
  min-height: 100px;
  padding: 1.25rem 0.5rem;
}
.jami-loader-logo {
  width: 4.25rem;
  height: 4.25rem;
  border-radius: 50%;
  object-fit: cover;
  animation: logo-glow-pulse 1.35s ease-in-out infinite;
  filter: drop-shadow(0 0 16px rgba(255, 107, 157, 0.55));
}
.jami-loader-heart {
  font-size: 2.85rem;
  line-height: 1;
  animation: jami-heart-pulse 1.05s ease-in-out infinite;
  filter: drop-shadow(0 0 14px rgba(255, 107, 157, 0.5));
}
.jami-loader-brand {
  font-weight: 700;
  font-size: 1.08rem;
  letter-spacing: 0.04em;
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  opacity: 0.92;
}
.jami-loader-label {
  font-size: 0.82rem;
  color: var(--muted);
  margin: 0;
  text-align: center;
}
@keyframes jami-heart-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.2); }
}
.swipe-stack .jami-loader { min-height: 280px; }
.nav-tabs {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  overflow-x: auto;
}
.nav-tabs button {
  flex: 1;
  min-width: 5rem;
  padding: 0.65rem;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--card);
  color: var(--text);
  cursor: pointer;
  font-weight: 600;
}
.nav-tabs button.active {
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  border-color: transparent;
}
.screen { display: none; padding: 1rem 1.25rem 5rem; max-width: 520px; margin: 0 auto; }
.screen.active { display: block; }
.hero {
  text-align: center;
  padding: 1.5rem 0 2rem;
}
.hero h1 { font-size: 1.75rem; margin: 0 0 0.5rem; }
.hero p { color: var(--muted); margin: 0; line-height: 1.5; }
.badge-row { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; margin-top: 1rem; }
.badge {
  font-size: 0.8rem;
  padding: 0.35rem 0.75rem;
  border-radius: 999px;
  background: var(--card);
  border: 1px solid var(--border);
}
.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.1rem;
  margin-bottom: 1rem;
  box-shadow: var(--shadow);
}
.profile-card {
  display: grid;
  grid-template-columns: 72px 1fr;
  gap: 1rem;
  align-items: start;
}
.avatar {
  width: 72px;
  height: 72px;
  border-radius: 18px;
  background: linear-gradient(145deg, var(--accent), var(--accent2));
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
  overflow: hidden;
  flex-shrink: 0;
}
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.avatar-with-presence,
.photo-gallery-main-with-presence,
.session-profile-photo-with-presence {
  position: relative;
}
.avatar-presence-dot {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: #22c55e;
  border: 2px solid rgba(12, 8, 18, 0.95);
  box-shadow: 0 0 10px rgba(34, 197, 94, 0.65);
  z-index: 4;
  pointer-events: none;
}
.avatar-live-label {
  position: absolute;
  top: 6px;
  left: 6px;
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  padding: 0.18rem 0.5rem;
  border-radius: 999px;
  background: linear-gradient(135deg, #22c55e, #16a34a);
  color: #fff;
  z-index: 5;
  pointer-events: none;
  text-transform: uppercase;
  box-shadow: 0 2px 10px rgba(34, 197, 94, 0.45);
  animation: avatar-live-blink 1.15s ease-in-out infinite;
}
@keyframes avatar-live-blink {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.65; transform: scale(0.97); }
}
.photo-gallery-main-with-presence .avatar-presence-dot {
  top: 10px;
  right: 10px;
  width: 14px;
  height: 14px;
}
.photo-gallery-main-with-presence .avatar-live-label {
  top: 10px;
  left: 10px;
  font-size: 0.68rem;
}
.profile-card h3 { margin: 0 0 0.25rem; font-size: 1.1rem; }
.meta { color: var(--muted); font-size: 0.85rem; margin: 0.15rem 0; }
.origin {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.5rem;
  padding: 0.35rem 0.65rem;
  background: rgba(94,234,212,0.12);
  border-radius: 8px;
  font-size: 0.82rem;
  color: var(--mint);
}
.dist { color: var(--accent); font-weight: 600; }
label { display: block; font-size: 0.85rem; color: var(--muted); margin-bottom: 0.35rem; }
input, textarea, select {
  width: 100%;
  padding: 0.75rem 1rem;
  margin-bottom: 0.85rem;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: rgba(0,0,0,0.25);
  color: var(--text);
  font: inherit;
}
.btn {
  display: inline-block;
  width: 100%;
  padding: 0.9rem 1.2rem;
  border: none;
  border-radius: 14px;
  font-weight: 700;
  font-size: 1rem;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
}
.btn-primary {
  background: linear-gradient(135deg, #ff2d95 0%, #ff4d8d 48%, #e91e8c 100%);
  color: #fff;
  border-radius: 16px;
  box-shadow:
    0 0 20px rgba(255, 45, 149, 0.45),
    0 10px 28px rgba(255, 77, 141, 0.35);
  transition: transform 0.28s ease, box-shadow 0.28s ease, filter 0.28s ease;
}
.btn-primary:hover {
  transform: translateY(-3px) scale(1.02);
  box-shadow:
    0 0 32px rgba(255, 45, 149, 0.65),
    0 14px 36px rgba(255, 77, 141, 0.5);
  filter: brightness(1.08);
}
.btn-primary:active { transform: translateY(0); }
.btn-ghost {
  background: var(--card);
  color: var(--text);
  border: 1px solid var(--border);
  margin-top: 0.5rem;
}
.btn-paypal {
  background: #0070ba;
  color: #fff;
}
.btn-stripe {
  background: #635bff;
  color: #fff;
  margin-top: 0.5rem;
}
.pay-methods-banner {
  padding: 0.75rem 1rem;
  margin-bottom: 1rem;
  border-radius: 12px;
  background: rgba(99, 91, 255, 0.12);
  border: 1px solid rgba(99, 91, 255, 0.35);
  font-size: 0.88rem;
  line-height: 1.5;
}
.pay-methods-banner strong { color: #a5b4fc; }
.pay-method-picker {
  margin: 0 0 1rem;
  padding: 0.85rem;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.1);
}
.pay-method-picker-label {
  display: block;
  margin: 0 0 0.55rem;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--muted);
}
.pay-method-picker-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
}
.pay-method-picker-grid button {
  flex: 1 1 calc(33% - 0.45rem);
  min-width: 5.5rem;
  padding: 0.55rem 0.45rem;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  background: rgba(0, 0, 0, 0.2);
  color: var(--text);
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s, transform 0.15s;
}
.pay-method-picker-grid button.is-active {
  border-color: rgba(99, 91, 255, 0.75);
  background: rgba(99, 91, 255, 0.22);
  color: #fff;
  box-shadow: 0 0 0 1px rgba(99, 91, 255, 0.35);
}
.pay-method-picker-grid button:hover { transform: translateY(-1px); }
.shop-welcome-banner {
  margin-bottom: 1.1rem;
  padding: 1rem 1.1rem;
  border-radius: 14px;
  background: linear-gradient(135deg, rgba(255, 209, 102, 0.18), rgba(255, 77, 141, 0.14));
  border: 1px solid rgba(255, 209, 102, 0.55);
  box-shadow: 0 8px 28px rgba(255, 77, 141, 0.15);
}
.shop-welcome-banner.hidden { display: none !important; }
.shop-welcome-banner p { margin: 0.35rem 0 0; font-size: 0.88rem; line-height: 1.45; color: #f5e6c8; }
.shop-welcome-timer {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 0.55rem;
  padding: 0.35rem 0.65rem;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.35);
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  font-size: 1.05rem;
  color: #ffd166;
}
.pack-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  align-items: stretch;
  margin-top: 0.5rem;
}
@media (min-width: 960px) {
  .pack-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (max-width: 520px) {
  .pack-grid { grid-template-columns: 1fr; max-width: 22rem; margin-left: auto; margin-right: auto; }
}
.pack-card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: 1.15rem 1rem 1rem;
  border-radius: 18px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.04);
  text-align: center;
  min-height: 100%;
}
.pack-card--featured {
  border: 2px solid #ffd166;
  background: linear-gradient(165deg, rgba(255, 209, 102, 0.12), rgba(255, 77, 141, 0.1));
  box-shadow: 0 0 28px rgba(255, 209, 102, 0.28), 0 12px 32px rgba(255, 77, 141, 0.2);
  transform: scale(1.05);
  z-index: 1;
}
@media (max-width: 820px) {
  .pack-card--featured { transform: none; }
}
.pack-badge {
  position: absolute;
  top: -0.55rem;
  left: 50%;
  transform: translateX(-50%);
  padding: 0.2rem 0.65rem;
  border-radius: 999px;
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  white-space: nowrap;
}
.pack-badge--bestseller {
  background: linear-gradient(135deg, #ffd166, #ff9f43);
  color: #1a1228;
}
.pack-badge--save {
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  color: #fff;
}
.pack-badge--vip-gold {
  background: linear-gradient(135deg, #ffd166, #ffb703);
  color: #1a1228;
  box-shadow: 0 0 12px rgba(255, 209, 102, 0.45);
}
.pack-card-vip-gold {
  margin: -0.15rem 0 0.45rem;
  font-size: 0.72rem;
  font-weight: 700;
  color: #ffd166;
  line-height: 1.35;
}
.pack-card-title {
  margin: 0.65rem 0 0.25rem;
  font-size: 1rem;
  font-weight: 700;
}
.pack-card-coins {
  margin: 0;
  font-size: 1.75rem;
  font-weight: 800;
  color: var(--mint);
  line-height: 1.1;
}
.pack-card-coins span { font-size: 0.95rem; font-weight: 600; color: var(--muted); }
.pack-card-bonus {
  margin: 0.35rem 0 0.75rem;
  font-size: 0.78rem;
  color: var(--muted);
  line-height: 1.35;
  flex: 1;
}
.pack-card-welcome-extra {
  margin: -0.35rem 0 0.5rem;
  font-size: 0.72rem;
  font-weight: 700;
  color: #ffd166;
}
.pack-card-price {
  margin: 0 0 0.85rem;
  font-size: 1.35rem;
  font-weight: 700;
}
.pack-actions {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  width: 100%;
  margin-top: auto;
}
.pack-actions .btn { width: 100%; font-size: 0.88rem; padding: 0.7rem 0.5rem; }
.btn-unlock-pulse {
  animation: shop-btn-pulse 2.2s ease-in-out infinite;
}
@keyframes shop-btn-pulse {
  0%, 100% {
    transform: scale(1);
    box-shadow: 0 4px 16px rgba(255, 77, 141, 0.35);
  }
  50% {
    transform: scale(1.03);
    box-shadow: 0 6px 22px rgba(255, 77, 141, 0.55);
  }
}
.pack-actions .btn-unlock-pulse:active { transform: scale(0.98); }
.chat-box {
  max-height: 280px;
  overflow-y: auto;
  margin-bottom: 1rem;
  padding: 0.75rem;
  background: rgba(0,0,0,0.2);
  border-radius: 14px;
}
.msg { margin: 0.5rem 0; padding: 0.6rem 0.85rem; border-radius: 12px; max-width: 85%; }
.msg.me { background: linear-gradient(135deg, var(--accent), var(--accent2)); margin-left: auto; }
.msg.them { background: var(--card); border: 1px solid var(--border); }
.tier-table { width: 100%; font-size: 0.85rem; border-collapse: collapse; }
.tier-table td, .tier-table th { padding: 0.5rem; border-bottom: 1px solid var(--border); text-align: left; }
.real-banner {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  padding: 0.85rem 1rem;
  margin-bottom: 1rem;
  border-radius: 14px;
  background: linear-gradient(135deg, rgba(94,234,212,0.15), rgba(255,77,141,0.12));
  border: 1px solid rgba(94,234,212,0.35);
  font-size: 0.88rem;
  line-height: 1.45;
}
.real-banner strong { color: var(--mint); display: block; margin-bottom: 0.2rem; }
.badge-verified {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: 0.72rem;
  font-weight: 700;
  padding: 0.2rem 0.5rem;
  border-radius: 6px;
  background: rgba(94,234,212,0.2);
  color: var(--mint);
  margin-left: 0.35rem;
}
.badge-vip-gold {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 0.95rem;
  line-height: 1;
  margin-left: 0.2rem;
  filter: drop-shadow(0 0 6px rgba(255, 209, 102, 0.75));
  vertical-align: middle;
}
.swipe-card-name .badge-vip-gold {
  font-size: 1.05rem;
  margin-left: 0.15rem;
}
.filter-bar {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-bottom: 1rem;
}
.filter-bar label { margin: 0; font-size: 0.75rem; }
.filter-bar input, .filter-bar select {
  margin-bottom: 0;
  padding: 0.5rem 0.65rem;
  font-size: 0.85rem;
}
.filter-km-wrap { grid-column: 1 / -1; }
.filter-km-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 0.35rem;
}
.filter-km-head strong { color: var(--accent); font-size: 0.92rem; font-weight: 700; }
#filterKm {
  width: 100%;
  margin: 0;
  padding: 0;
  height: 28px;
  accent-color: var(--accent);
  cursor: pointer;
}
#filterKm:disabled { opacity: 0.45; cursor: not-allowed; }
.filter-everywhere-toggle {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-top: 0.45rem;
  font-size: 0.82rem;
  cursor: pointer;
  user-select: none;
}
.filter-everywhere-toggle input {
  width: auto;
  margin: 0;
  padding: 0;
  accent-color: var(--accent);
}
.wm-hero {
  margin-bottom: 1rem;
  padding: 1rem 1.1rem;
  border-radius: 16px;
  background: linear-gradient(135deg, rgba(0, 120, 60, 0.25), rgba(255, 77, 141, 0.12));
  border: 1px solid rgba(255, 255, 255, 0.12);
}
.wm-hero h2 { margin: 0 0 0.35rem; font-size: 1.15rem; }
.wm-balance { font-size: 0.9rem; color: var(--accent); font-weight: 700; }
.wm-disclaimer { font-size: 0.72rem; opacity: 0.85; margin: 0.5rem 0 0; }
.wm-match-list { display: flex; flex-direction: column; gap: 0.85rem; }
.wm-match-card {
  padding: 0.85rem 0.95rem;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.04);
}
.wm-match-card.wm-match-closed { opacity: 0.92; }
.wm-match-card.wm-match-today {
  border-color: rgba(255, 215, 0, 0.55);
  background: linear-gradient(135deg, rgba(255, 215, 0, 0.1), rgba(255, 77, 141, 0.06));
  box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.18);
}
.wm-today-badge {
  display: inline-block;
  margin-right: 0.35rem;
  padding: 0.08rem 0.4rem;
  border-radius: 999px;
  background: rgba(255, 215, 0, 0.22);
  color: #ffd700;
  font-size: 0.65rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.wm-today-banner {
  margin: 0 0 0.85rem;
  padding: 0.85rem 1rem;
  border-radius: 14px;
  border: 1px solid rgba(255, 215, 0, 0.45);
  background: linear-gradient(135deg, rgba(255, 215, 0, 0.16), rgba(255, 77, 141, 0.1));
  font-size: 0.92rem;
  line-height: 1.45;
}
.wm-today-banner strong { color: #ffd700; }
.wm-match-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.55rem;
  font-size: 0.72rem;
  color: var(--muted);
}
.wm-match-stage { font-weight: 600; }
.wm-status-open { color: #4ade80; }
.wm-status-locked { color: #fbbf24; }
.wm-status-settled { color: #93c5fd; }
.wm-teams {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 0.5rem;
  align-items: center;
  text-align: center;
  margin-bottom: 0.65rem;
}
.wm-team { font-weight: 700; font-size: 0.92rem; }
.wm-team-flag { font-size: 1.6rem; display: block; margin-bottom: 0.15rem; }
.wm-vs { font-size: 0.8rem; color: var(--muted); font-weight: 700; }
.wm-pick-row, .wm-stake-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.35rem;
  margin-bottom: 0.45rem;
}
.wm-stake-row { grid-template-columns: repeat(4, 1fr); }
.wm-pick-btn, .wm-stake-btn {
  padding: 0.45rem 0.35rem;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(0, 0, 0, 0.2);
  color: inherit;
  font-size: 0.78rem;
  cursor: pointer;
}
.wm-pick-btn strong { display: block; font-size: 0.95rem; }
.wm-pick-btn.is-selected, .wm-stake-btn.is-selected {
  border-color: var(--accent);
  background: rgba(255, 77, 141, 0.2);
}
.wm-pick-btn:disabled, .wm-stake-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.wm-place-btn {
  width: 100%;
  margin-top: 0.25rem;
  padding: 0.55rem;
  border-radius: 10px;
  border: none;
  background: linear-gradient(135deg, #059669, #047857);
  color: #fff;
  font-weight: 700;
  cursor: pointer;
}
.wm-place-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.wm-my-tip {
  margin-top: 0.5rem;
  padding: 0.45rem 0.55rem;
  border-radius: 10px;
  background: rgba(255, 209, 102, 0.12);
  border: 1px solid rgba(255, 209, 102, 0.35);
  font-size: 0.8rem;
}
.wm-my-bets { margin-top: 1rem; }
.wm-my-bets h3 { margin: 0 0 0.5rem; font-size: 0.95rem; }
.wm-bet-history {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  font-size: 0.78rem;
}
.wm-bet-row-h {
  padding: 0.45rem 0.55rem;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
}
.wm-bet-won { border-color: rgba(74, 222, 128, 0.45); }
.wm-bet-lost { border-color: rgba(248, 113, 113, 0.35); opacity: 0.85; }
.wm-screen { padding: 0.5rem 0.65rem 5.5rem; max-width: 560px; }
.wm-fan-lounge {
  position: relative;
  margin-bottom: 1.25rem;
  border-radius: 20px;
  overflow: hidden;
  border: 3px solid #111;
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.08);
  background:
    radial-gradient(ellipse 120% 80% at 50% 100%, rgba(255, 206, 0, 0.1), transparent 55%),
    linear-gradient(180deg, #0b4d22 0%, #1a7a3a 35%, #145c2c 70%, #0a2814 100%);
  min-height: min(78vh, 780px);
  display: flex;
  flex-direction: column;
}
.wm-fan-lounge::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 28px,
    rgba(255, 255, 255, 0.03) 28px,
    rgba(255, 255, 255, 0.03) 29px
  );
  pointer-events: none;
}
.wm-fan-jersey-strip {
  height: 12px;
  background: linear-gradient(90deg, #000 33.33%, #dd0000 33.33%, #dd0000 66.66%, #ffce00 66.66%);
}
.wm-fan-header {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  padding: 1.1rem 1.15rem 0.55rem;
  position: relative;
  z-index: 1;
}
.wm-fan-head-text { flex: 1; min-width: 0; }
.wm-fan-head-text h2 {
  margin: 0;
  font-size: 1.35rem;
  text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
}
.wm-fan-head-text p {
  margin: 0.25rem 0 0;
  font-size: 0.88rem;
  opacity: 0.92;
}
.wm-fan-ball {
  font-size: 2.75rem;
  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.4));
  animation: wm-ball-bounce 2.2s ease-in-out infinite;
}
.wm-fan-vuvuzela {
  font-size: 2.35rem;
  animation: wm-vuvuzela-shake 0.35s ease-in-out infinite alternate;
  filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.35));
}
@keyframes wm-ball-bounce {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50% { transform: translateY(-10px) rotate(180deg); }
}
@keyframes wm-vuvuzela-shake {
  from { transform: rotate(-12deg) scale(1); }
  to { transform: rotate(14deg) scale(1.08); }
}
.wm-fan-stadium-deco {
  display: flex;
  justify-content: center;
  gap: 1.1rem;
  padding: 0 0 0.65rem;
  font-size: 1.45rem;
  opacity: 0.9;
  position: relative;
  z-index: 1;
}
.wm-fan-stadium-deco span { animation: wm-crowd-pulse 1.8s ease-in-out infinite; }
.wm-fan-stadium-deco span:nth-child(2) { animation-delay: 0.2s; }
.wm-fan-stadium-deco span:nth-child(3) { animation-delay: 0.4s; }
.wm-fan-stadium-deco span:nth-child(4) { animation-delay: 0.6s; }
.wm-fan-stadium-deco span:nth-child(5) { animation-delay: 0.8s; }
@keyframes wm-crowd-pulse {
  0%, 100% { transform: scale(1); opacity: 0.7; }
  50% { transform: scale(1.15); opacity: 1; }
}
.wm-fan-chat-box {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  margin: 0 0.85rem;
  min-height: 360px;
  height: min(52vh, 540px);
  max-height: 540px;
  overflow-y: auto;
  padding: 0.85rem 0.75rem;
  border-radius: 16px;
  background: rgba(0, 0, 0, 0.38);
  border: 2px solid rgba(255, 206, 0, 0.3);
  -webkit-overflow-scrolling: touch;
}
.wm-fan-empty {
  text-align: center;
  margin: 3rem 0.75rem;
  font-size: 0.95rem;
  opacity: 0.88;
}
.wm-fan-msg {
  display: flex;
  gap: 0.55rem;
  margin-bottom: 0.7rem;
  align-items: flex-start;
}
.wm-fan-msg.mine { flex-direction: row-reverse; }
.wm-fan-avatar {
  width: 42px;
  height: 42px;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  border: 2px solid rgba(255, 206, 0, 0.55);
  background: #222;
  font-size: 0.9rem;
  display: flex;
  align-items: center;
  justify-content: center;
}
.wm-fan-bubble-wrap { max-width: 84%; min-width: 0; }
.wm-fan-name {
  font-size: 0.76rem;
  font-weight: 700;
  margin-bottom: 0.15rem;
  opacity: 0.92;
}
.wm-fan-msg.mine .wm-fan-name { text-align: right; color: #ffce00; }
.wm-fan-bubble {
  padding: 0.55rem 0.8rem;
  border-radius: 14px;
  font-size: 0.96rem;
  line-height: 1.4;
  word-break: break-word;
  background: rgba(255, 255, 255, 0.12);
  border: 1px solid rgba(255, 255, 255, 0.12);
}
.wm-fan-msg.mine .wm-fan-bubble {
  background: linear-gradient(135deg, rgba(221, 0, 0, 0.45), rgba(0, 0, 0, 0.35));
  border-color: rgba(255, 206, 0, 0.35);
}
.wm-fan-time {
  font-size: 0.62rem;
  opacity: 0.55;
  margin-top: 0.15rem;
}
.wm-fan-msg.mine .wm-fan-time { text-align: right; }
.wm-fan-compose {
  display: flex;
  gap: 0.55rem;
  padding: 0.85rem;
  position: relative;
  z-index: 1;
}
.wm-fan-compose input {
  flex: 1;
  margin: 0;
  padding: 0.75rem 1rem;
  border-radius: 999px;
  border: 2px solid rgba(255, 206, 0, 0.5);
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  font-size: 1rem;
  min-height: 48px;
}
.wm-fan-compose input::placeholder { color: rgba(255, 255, 255, 0.45); }
.wm-fan-send {
  flex-shrink: 0;
  padding: 0.65rem 1.1rem;
  border-radius: 999px;
  border: none;
  font-weight: 800;
  font-size: 0.92rem;
  cursor: pointer;
  color: #111;
  background: linear-gradient(135deg, #ffce00, #f59e0b);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
  min-height: 48px;
}
.wm-fan-emoji-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  padding: 0 0.85rem 0.55rem;
  position: relative;
  z-index: 1;
}
.wm-fan-emoji-bar button {
  margin: 0;
  padding: 0.45rem 0.75rem;
  border-radius: 999px;
  border: 1px solid rgba(255, 206, 0, 0.4);
  background: rgba(0, 0, 0, 0.35);
  font-size: 1.2rem;
  cursor: pointer;
  line-height: 1;
  min-height: 40px;
}
.wm-fan-emoji-bar button:active { transform: scale(1.12); }
.wm-fan-reactions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-top: 0.35rem;
}
.wm-fan-react-btn {
  margin: 0;
  padding: 0.15rem 0.45rem;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(0, 0, 0, 0.25);
  font-size: 0.78rem;
  cursor: pointer;
  color: inherit;
  line-height: 1.3;
}
.wm-fan-react-btn span {
  margin-left: 0.2rem;
  font-size: 0.68rem;
  opacity: 0.85;
}
.wm-fan-react-btn.is-active {
  border-color: #ffce00;
  background: rgba(255, 206, 0, 0.2);
  box-shadow: 0 0 8px rgba(255, 206, 0, 0.35);
}
.wm-goal-flash {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 100040;
  background: radial-gradient(circle at 50% 40%, rgba(255, 206, 0, 0.45), transparent 65%);
  animation: wm-goal-flash 0.65s ease-out forwards;
}
.wm-goal-banner {
  position: fixed;
  top: 18%;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100041;
  pointer-events: none;
  padding: 0.65rem 1.2rem;
  border-radius: 999px;
  font-weight: 800;
  font-size: 1.15rem;
  color: #111;
  background: linear-gradient(135deg, #ffce00, #f59e0b);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
  animation: wm-goal-banner 0.85s ease-out forwards;
}
@keyframes wm-goal-flash {
  0% { opacity: 0; }
  15% { opacity: 1; }
  100% { opacity: 0; }
}
@keyframes wm-goal-banner {
  0% { opacity: 0; transform: translateX(-50%) scale(0.7); }
  20% { opacity: 1; transform: translateX(-50%) scale(1.05); }
  100% { opacity: 0; transform: translateX(-50%) scale(1); }
}
.wm-section-divider {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  margin: 0.25rem 0 1rem;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--muted);
}
.wm-section-divider::before,
.wm-section-divider::after {
  content: '';
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 77, 141, 0.35), transparent);
}
.geo-map-card {
  margin-bottom: 0.75rem;
  padding: 0.65rem 0.75rem;
  border-radius: 14px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.04);
}
.geo-map-card.hidden { display: none !important; }
.geo-map-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 0.5rem;
  margin-bottom: 0.45rem;
  flex-wrap: wrap;
}
.geo-map-head strong { font-size: 0.88rem; }
.geo-map-head .meta { font-size: 0.72rem; margin: 0; }
.geo-map {
  height: 190px;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: #1a1222;
  z-index: 1;
}
.geo-map .leaflet-container {
  height: 100%;
  width: 100%;
  background: #1a1222;
  font-family: inherit;
}
.geo-map .leaflet-control-attribution {
  font-size: 0.62rem;
  background: rgba(12, 8, 18, 0.82);
  color: var(--muted);
}
.geo-map-note { margin: 0.45rem 0 0; font-size: 0.72rem; }
.geo-nearby-strip {
  margin-top: 0.55rem;
  padding-top: 0.5rem;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.geo-nearby-strip.hidden { display: none !important; }
.geo-nearby-strip-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.45rem;
}
.geo-nearby-strip-head strong { font-size: 0.84rem; }
.geo-nearby-list {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  padding-bottom: 0.15rem;
  -webkit-overflow-scrolling: touch;
}
.geo-nearby-chip {
  flex: 0 0 auto;
  width: 72px;
  padding: 0.35rem 0.25rem 0.45rem;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.05);
  color: inherit;
  cursor: pointer;
  text-align: center;
  -webkit-tap-highlight-color: transparent;
}
.geo-nearby-chip img {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
  margin: 0 auto 0.25rem;
  border: 2px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
}
.geo-nearby-chip .geo-nearby-ph {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 0.25rem;
  background: rgba(255, 255, 255, 0.06);
  font-size: 1.25rem;
  border: 2px solid rgba(255, 255, 255, 0.35);
}
.geo-nearby-chip-name {
  display: block;
  font-size: 0.68rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.geo-nearby-chip-km {
  display: block;
  font-size: 0.62rem;
  color: var(--muted);
}
.geo-nearby-chip:hover,
.geo-nearby-chip:focus-visible {
  border-color: rgba(255, 107, 157, 0.55);
  background: rgba(255, 77, 141, 0.12);
}
.geo-nearby-marker-wrap {
  background: transparent !important;
  border: none !important;
}
.geo-nearby-marker {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
  overflow: hidden;
  background: #1a1222;
}
.geo-nearby-marker img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.geo-nearby-marker .geo-nearby-ph {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  font-size: 1.15rem;
}
@media (max-width: 768px) {
  .geo-map { height: 165px; }
}
.swipe-stack {
  position: relative;
  height: min(52vh, 400px);
  margin: 0 auto 0.85rem;
  max-width: 300px;
  width: min(300px, 86vw);
}
.swipe-card {
  position: absolute;
  inset: 0;
  border-radius: 18px;
  overflow: hidden;
  background: #141018;
  border: 1px solid rgba(255, 255, 255, 0.1);
  box-shadow: 0 10px 32px rgba(0, 0, 0, 0.35), 0 3px 12px rgba(0, 0, 0, 0.2);
  display: flex;
  flex-direction: column;
  transition: transform 0.35s ease, opacity 0.35s ease, box-shadow 0.25s ease;
  touch-action: none;
  -webkit-touch-callout: none;
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
}
.swipe-card:active { cursor: grabbing; }
.swipe-card.swipe-drag-pass { box-shadow: 0 10px 32px rgba(255, 60, 90, 0.35); }
.swipe-card.swipe-drag-like { box-shadow: 0 10px 32px rgba(94, 234, 212, 0.28); }
.swipe-hint {
  text-align: center;
  margin: -0.35rem 0 0.75rem;
  font-size: 0.78rem;
  color: var(--muted);
}
.swipe-card:hover { box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45), 0 6px 18px rgba(255, 77, 109, 0.12); }
.swipe-card.leaving-left { transform: translateX(-120%) rotate(-12deg); opacity: 0; }
.swipe-card.leaving-right { transform: translateX(120%) rotate(12deg); opacity: 0; }
.swipe-card-photo {
  flex: 1;
  min-height: 70%;
  background: linear-gradient(145deg, #2a1520, #1a1028);
  position: relative;
  overflow: hidden;
}
.swipe-card-photo .photo-gallery {
  height: 100%;
  margin: 0;
  display: flex;
  flex-direction: column;
}
.swipe-card-photo .photo-gallery-main {
  flex: 1;
  min-height: 0;
  aspect-ratio: auto;
  border-radius: 0;
  position: relative;
}
.swipe-card-photo .photo-gallery-row,
.swipe-card-photo .photo-gallery-locked,
.swipe-card-photo .photo-gallery > .meta { display: none; }
.swipe-card-photo img { width: 100%; height: 100%; object-fit: cover; }
.swipe-card-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  padding: 2.5rem 1.1rem 1rem;
  background: linear-gradient(180deg, transparent 0%, rgba(0, 0, 0, 0.35) 35%, rgba(0, 0, 0, 0.78) 100%);
  color: #fff;
  pointer-events: none;
}
.swipe-card-name {
  margin: 0;
  font-size: 1.42rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
}
.swipe-card-name .profile-name-emoji { font-size: 1.2rem; filter: drop-shadow(0 2px 8px rgba(0,0,0,0.4)); }
.swipe-card-verified {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.35rem;
  height: 1.35rem;
  border-radius: 50%;
  background: linear-gradient(135deg, #3b9eff, #2dd4bf);
  color: #fff;
  font-size: 0.72rem;
  font-weight: 800;
  line-height: 1;
  flex-shrink: 0;
  box-shadow: 0 2px 8px rgba(45, 212, 191, 0.45);
}
.swipe-card-meta {
  margin: 0.35rem 0 0;
  font-size: 0.95rem;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.92);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.45);
}
.photo-upload-row { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.65rem; flex-wrap: wrap; position: relative; }
.photo-upload-row input[type="text"], .photo-upload-row input:not([type="file"]) { flex: 1 1 12rem; min-width: 0; }
.photo-file-input-hidden,
.photo-upload-row .photo-file-input-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
.photo-field-label {
  display: block;
  font-size: 0.8rem;
  margin-top: 0.35rem;
  margin-bottom: 0.15rem;
}
.btn-photo-pick {
  flex-shrink: 0;
  min-height: 44px;
  padding: 0.55rem 0.85rem !important;
  font-size: 0.88rem !important;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}
.reg-photo-pick-label {
  display: block;
  cursor: pointer;
  margin: 0.75rem 0 1rem;
}
.reg-photo-pick-label .btn-photo-pick {
  display: block;
  width: 100%;
  margin-top: 0.35rem;
  text-align: center;
}
@media (max-width: 768px) {
  .photo-upload-row { flex-direction: column; align-items: stretch; }
  .photo-upload-row .btn-photo-pick { width: 100%; }
  .reg-photo-upload .btn-photo-pick { width: 100%; margin-top: 0.35rem; }
}
.photo-upload-row .photo-file-input { max-width: 11rem; font-size: 0.82rem; }
.photo-gallery { margin-bottom: 0.5rem; }
.photo-gallery-main {
  width: 100%;
  aspect-ratio: 4/5;
  border-radius: 14px 14px 0 0;
  overflow: hidden;
  background: #1a1222;
}
.photo-gallery-main img { width: 100%; height: 100%; object-fit: cover; }
.photo-gallery-main-empty {
  width: 100%;
  height: 100%;
  min-height: 120px;
  background: rgba(255,255,255,0.05);
  border: 1px dashed rgba(255,255,255,0.16);
}
.photo-gallery-main-open { cursor: zoom-in; }
.photo-gallery-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 3px;
  margin-top: 3px;
}
.photo-gallery-locked {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
  gap: 3px;
  margin-top: 3px;
}
.photo-slots-summary { margin: 0 0 0.75rem; }
.photo-slot-buy-row { margin: 0.35rem 0 0.85rem; }
.btn-photo-slot-buy {
  width: 100%;
  font-size: 0.85rem;
  border-style: dashed;
}
.photo-slot {
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 6px;
  background: #1a1222;
  position: relative;
}
.photo-slot img { width: 100%; height: 100%; object-fit: cover; }
.photo-slot.locked-blur img,
.session-photo-slot.locked-blur img {
  filter: blur(18px) brightness(0.32) saturate(0.35);
  transform: scale(1.35);
}
.photo-slot.locked-blur::after,
.session-photo-slot.locked-blur::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(90deg, rgba(255,255,255,0.04) 0 3px, transparent 3px 6px),
    repeating-linear-gradient(0deg, rgba(255,255,255,0.04) 0 3px, transparent 3px 6px);
  pointer-events: none;
  mix-blend-mode: overlay;
}
.photo-slot--open,
.session-photo-slot--open { cursor: zoom-in; }
.photo-slot--open:hover,
.session-photo-slot--open:hover { box-shadow: 0 0 0 2px rgba(255, 107, 157, 0.55); }
.photo-slot .lock-badge {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.1rem;
  background: rgba(0,0,0,0.35);
}
.photo-slot.empty { border: 1px dashed var(--border); }
.photo-edit-grid { display: grid; gap: 0.5rem; margin-top: 0.5rem; }
.photo-edit-grid label { font-size: 0.8rem; margin-top: 0.35rem; }
.photo-edit-grid input { width: 100%; font-size: 0.85rem; }
.reg-photo-upload { margin: 0.75rem 0 1rem; }
.reg-photo-preview {
  width: 120px;
  height: 120px;
  border-radius: 18px;
  overflow: hidden;
  background: rgba(255,255,255,0.06);
  border: 1px dashed rgba(255,255,255,0.22);
  display: flex;
  align-items: center;
  justify-content: center;
  margin-bottom: 0.65rem;
}
.reg-photo-preview img { width: 100%; height: 100%; object-fit: cover; }
.reg-photo-placeholder { font-size: 0.82rem; color: var(--muted); text-align: center; padding: 0.5rem; }
.friend-request-card {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.65rem 0;
  border-bottom: 1px solid var(--border);
}
.friend-request-card:last-child { border-bottom: none; }
.friend-request-actions { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.friend-request-actions .btn { font-size: 0.75rem; padding: 0.4rem 0.65rem; }
.btn-friend { font-size: 0.8rem; margin-top: 0.35rem; }
.profile-photo-modal {
  position: fixed;
  inset: 0;
  z-index: 210;
  background: rgba(8,4,12,0.92);
  overflow-y: auto;
  padding: 1rem;
}
body.chat-session-open #profilePhotoModal,
body.chat-profile-sheet-open #profilePhotoModal {
  z-index: 100020;
}
.profile-photo-modal.hidden { display: none; }
.profile-photo-modal-inner { max-width: 480px; margin: 0 auto; }
.chat-session-overlay {
  position: fixed;
  inset: 0;
  z-index: 99999;
  pointer-events: auto;
  background: linear-gradient(165deg, rgba(14,6,20,0.98), rgba(6,3,10,0.99));
  display: flex;
  flex-direction: column;
  padding: 0.2rem 0.45rem 0.35rem;
  overflow: hidden;
}
.chat-session-overlay.hidden { display: none !important; }
body.chat-session-open { overflow: hidden; }
body.chat-session-open #appShell { pointer-events: none; }
.chat-session-overlay:not(.hidden) {
  display: flex !important;
}
.chat-session-overlay:not(.hidden) .chat-session-side,
.chat-session-overlay:not(.hidden) .chat-session-center {
  opacity: 1 !important;
  transform: none !important;
  visibility: visible !important;
}
.chat-session-topbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-shrink: 0;
  padding: 0.1rem 0.2rem 0.15rem;
  min-height: 2rem;
}
.chat-session-topbar-actions {
  display: flex;
  align-items: center;
  gap: 0.25rem;
  flex-shrink: 0;
  min-width: 4.5rem;
  justify-content: flex-end;
}
.btn-block-user {
  padding: 0.35rem 0.55rem;
  font-size: 0.72rem;
  border-radius: 8px;
  border: 1px solid rgba(255, 107, 120, 0.45);
  background: rgba(80, 20, 35, 0.55);
  color: #ffb4c0;
  cursor: pointer;
  white-space: nowrap;
}
.btn-block-user:hover { background: rgba(120, 30, 50, 0.75); color: #fff; }
.btn-view-chat-profile {
  padding: 0.35rem 0.55rem;
  font-size: 1rem;
  line-height: 1;
  border-radius: 8px;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.06);
  color: #fff;
  cursor: pointer;
  flex-shrink: 0;
}
.btn-view-chat-profile.hidden { display: none; }
.chat-session-profile-bar {
  display: none;
  align-items: center;
  gap: 0.65rem;
  width: 100%;
  padding: 0.45rem 0.55rem;
  margin-bottom: 0.35rem;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.05);
  color: inherit;
  cursor: pointer;
  text-align: left;
  flex-shrink: 0;
}
.chat-session-profile-bar.hidden { display: none !important; }
.chat-session-profile-bar-avatar {
  width: 44px;
  height: 44px;
  border-radius: 10px;
  overflow: hidden;
  flex-shrink: 0;
  background: rgba(255, 255, 255, 0.06);
}
.chat-session-profile-bar-avatar img,
.chat-session-profile-bar-avatar .avatar-ph {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.chat-session-profile-bar-text { flex: 1; min-width: 0; }
.chat-session-profile-bar-text strong {
  display: block;
  font-size: 0.92rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-session-profile-bar-text .meta {
  display: block;
  font-size: 0.72rem;
  margin-top: 0.1rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.chat-session-profile-bar-chevron {
  font-size: 0.78rem;
  color: var(--accent);
  flex-shrink: 0;
  font-weight: 600;
}
.chat-profile-sheet {
  position: absolute;
  inset: 0;
  z-index: 100030;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  pointer-events: none;
}
.chat-profile-sheet:not(.hidden) { pointer-events: auto; }
.chat-profile-sheet.hidden { display: none !important; }
.chat-profile-sheet-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.58);
}
.chat-profile-sheet-panel {
  position: relative;
  max-height: min(88dvh, 88vh, 720px);
  background: #120a18;
  border-radius: 18px 18px 0 0;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-bottom: none;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.45);
}
.chat-profile-sheet-head {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.5rem 0.65rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  flex-shrink: 0;
  flex-wrap: wrap;
}
.chat-profile-sheet-head .btn { font-size: 0.82rem; padding: 0.4rem 0.55rem; }
.chat-profile-tabs {
  display: flex;
  gap: 0.35rem;
  margin-left: auto;
}
.chat-profile-tabs button {
  padding: 0.35rem 0.65rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  font-size: 0.78rem;
  cursor: pointer;
}
.chat-profile-tabs button.active {
  background: rgba(255, 77, 141, 0.2);
  color: #fff;
  border-color: rgba(255, 107, 157, 0.45);
}
.chat-profile-sheet-body {
  overflow-y: auto;
  padding: 0.65rem 0.75rem calc(1rem + env(safe-area-inset-bottom, 0px));
  -webkit-overflow-scrolling: touch;
}
.chat-profile-sheet-body .session-profile-card {
  border: none;
  background: transparent;
  padding: 0;
}
.chat-profile-sheet-body .session-panel-tag { display: none; }
.session-actions-row {
  display: flex;
  gap: 0.35rem;
  margin-top: 0.35rem;
  flex-wrap: wrap;
}
.session-actions-row button {
  flex: 1;
  min-width: 4.5rem;
  padding: 0.35rem 0.5rem;
  font-size: 0.62rem;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.session-actions-row button.btn-block-user { flex: 1; }
.chat-session-title {
  margin: 0;
  flex: 1;
  text-align: center;
  font-size: 1.1rem;
  font-weight: 700;
}
.chat-session-layout {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(138px, 168px) minmax(260px, 1fr) minmax(138px, 168px);
  grid-template-areas: "partner chat self";
  gap: 0.35rem;
  padding: 0;
  align-items: start;
  align-content: start;
}
#chatSessionOther { grid-area: partner; }
.chat-session-center { grid-area: chat; }
#chatSessionMe { grid-area: self; }
.chat-session-side {
  overflow-y: auto;
  border-radius: 12px;
  background: rgba(255,255,255,0.04);
  border: 1px solid var(--border);
  padding: 0.4rem 0.45rem;
  min-height: 80px;
  max-height: calc(100vh - 2.85rem);
  min-width: 0;
}
.chat-session-left { transform-origin: right center; }
.chat-session-right { transform-origin: left center; }
.chat-session-center {
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  align-self: start;
  width: 100%;
  max-height: calc(100vh - 2.75rem);
}
.chat-session-center-inner {
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 2.75rem);
  margin: 0;
  padding: 0.55rem 0.65rem 0.5rem;
  border: 2px solid rgba(255, 107, 157, 0.45);
  box-shadow: 0 12px 40px rgba(255, 77, 141, 0.2);
  overflow: hidden;
}
.chat-session-center-head {
  margin: 0 0 0.25rem;
  font-size: 0.92rem;
  font-weight: 700;
  text-align: center;
  flex-shrink: 0;
  cursor: pointer;
}
.chat-session-side .session-profile-card { cursor: pointer; }
.chat-session-center .chat-box {
  flex: 1 1 auto;
  min-height: 80px;
  max-height: min(42vh, 360px);
  overflow-y: auto;
  overflow-x: hidden;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin-bottom: 0;
  padding: 0.35rem 0.15rem 0.5rem;
  -webkit-overflow-scrolling: touch;
}
.chat-session-center .msg.me {
  align-self: flex-end;
  margin-left: auto;
  margin-right: 0;
}
.chat-session-center .msg.them {
  align-self: flex-start;
  margin-right: auto;
  margin-left: 0;
}
.chat-session-center .msg.me .msg-bubble {
  background: linear-gradient(145deg, rgba(255, 140, 175, 0.95), rgba(255, 77, 141, 0.92));
  color: #fff;
  border: 1px solid rgba(255, 180, 210, 0.35);
  box-shadow: 0 2px 10px rgba(255, 77, 141, 0.22);
}
.chat-session-center .msg.them .msg-bubble {
  background: #ececf1;
  color: #1a1228;
  border: 1px solid rgba(255, 255, 255, 0.65);
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
}
.chat-session-status {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.75rem;
  font-size: 0.72rem;
  color: var(--muted);
  margin-bottom: 0.35rem;
  padding-bottom: 0.35rem;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.chat-session-center #chatSessionCostHint {
  flex-shrink: 0;
  margin: 0 0 0.35rem !important;
  font-size: 0.72rem !important;
}
.chat-session-status span { white-space: nowrap; }
.chat-typing-indicator {
  font-size: 0.82rem;
  font-weight: 600;
  color: #53bdeb;
  margin: 0 0 0.35rem;
  min-height: 1.15em;
  flex-shrink: 0;
}
.chat-typing-indicator.is-active {
  animation: typing-fade 1.2s ease-in-out infinite alternate;
}
@keyframes typing-fade {
  from { opacity: 0.65; }
  to { opacity: 1; }
}
.chat-session-center .msg {
  display: flex;
  flex-direction: column;
  max-width: min(88%, 20rem);
  margin: 0.2rem 0;
  padding: 0;
  background: none;
  border: none;
}
.chat-session-center .msg.me { align-items: flex-end; }
.chat-session-center .msg.them { align-items: flex-start; }
.msg-bubble {
  display: inline-block;
  max-width: 100%;
  padding: 0.55rem 0.75rem;
  border-radius: 16px;
  line-height: 1.45;
  word-break: break-word;
}
.chat-session-center .msg.me .msg-bubble { border-bottom-right-radius: 4px; }
.chat-session-center .msg.them .msg-bubble { border-bottom-left-radius: 4px; }
.msg-meta {
  display: flex;
  align-items: center;
  gap: 0.28rem;
  margin-top: 0.14rem;
  padding: 0 0.2rem;
  font-size: 0.62rem;
  line-height: 1;
  color: rgba(255, 255, 255, 0.42);
  user-select: none;
}
.chat-session-center .msg.them .msg-meta { color: rgba(255, 255, 255, 0.38); }
.msg-time { font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
.msg-foot { display: contents; }
.msg-meta .msg-ticks { margin-left: 0.1rem; }
.msg-ticks {
  display: inline-flex;
  gap: 0;
  font-size: 0.72rem;
  line-height: 1;
  letter-spacing: -0.2em;
  user-select: none;
}
.msg-ticks .tick { display: inline-block; }
.msg-ticks-sent .tick { color: rgba(255, 255, 255, 0.5); }
.msg-ticks-read .tick { color: #53bdeb; }

.presence-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  font-weight: 600;
  font-size: 0.78rem;
}
.presence-dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  flex-shrink: 0;
  background: #6b7280;
}
.presence-online .presence-dot {
  background: #22c55e;
  box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.65);
  animation: presence-pulse 1.6s ease-in-out infinite;
}
.presence-online { color: #4ade80; }
.presence-recent { color: #fbbf24; }
.presence-recent .presence-dot { background: #fbbf24; }
.presence-today { color: #93c5fd; }
.presence-today .presence-dot { background: #60a5fa; }
.presence-week { color: #a78bfa; }
.presence-week .presence-dot { background: #8b5cf6; }
.presence-stale { color: #94a3b8; opacity: 0.85; }
.presence-stale .presence-dot { background: #64748b; }
.presence-unknown { color: var(--muted); opacity: 0.75; }
.presence-unknown .presence-dot { background: #475569; }
.presence-offline { color: var(--muted); }
@keyframes presence-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.55); transform: scale(1); }
  50% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0); transform: scale(1.08); }
}
.session-profile-header .presence-badge { margin-top: 0.2rem; }
.swipe-card-body .presence-badge { margin: 0.15rem 0 0.35rem; }
.match-card .presence-badge,
.like-card .presence-badge { font-size: 0.72rem; margin-top: 0.15rem; }
.session-profile-header {
  display: flex;
  align-items: flex-start;
  gap: 0.35rem;
  margin-bottom: 0.3rem;
}
.session-profile-header .avatar { width: 34px; height: 34px; border-radius: 8px; flex-shrink: 0; }
.avatar-compact .avatar-live-label { display: none; }
.avatar-compact .avatar-presence-dot {
  top: auto;
  left: auto;
  bottom: 1px;
  right: 1px;
  width: 9px;
  height: 9px;
  border-width: 1.5px;
}
.session-profile-header h3 { font-size: 0.78rem !important; line-height: 1.25; }
.session-profile-header .meta { font-size: 0.62rem !important; }
.session-profile-header .presence-badge { font-size: 0.6rem; }
.session-info-section {
  margin-top: 0.3rem;
  padding: 0.35rem 0.4rem;
  border-radius: 8px;
  background: rgba(120, 60, 180, 0.15);
  border: 1px solid rgba(160, 90, 220, 0.25);
}
.session-info-section h4 {
  margin: 0 0 0.25rem;
  font-size: 0.6rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent2);
}
.session-photo-strip {
  display: flex;
  gap: 0.25rem;
  overflow-x: auto;
  margin: 0.35rem 0 0.5rem;
  padding-bottom: 0.15rem;
}
.session-photo-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 3px;
  margin: 0.2rem 0 0.35rem;
}
.session-photo-slot {
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 6px;
  background: #1a1222;
  position: relative;
  border: none;
  padding: 0;
}
.session-photo-slot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.session-photo-slot.empty {
  border: 1px dashed rgba(255,255,255,0.12);
  min-height: 36px;
}
.session-photo-hint {
  font-size: 0.58rem !important;
  margin: 0.15rem 0 0.25rem !important;
  line-height: 1.3;
  opacity: 0.85;
}
#chatPhotoLightbox { z-index: 100005; }
#chatPhotoLightbox .profile-photo-modal-inner {
  max-width: min(540px, 94vw);
  text-align: center;
}
#chatPhotoLightboxImg {
  display: block;
  width: 100%;
  max-height: min(78dvh, 78vh);
  object-fit: contain;
  border-radius: 14px;
  margin: 0.65rem auto 0;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
}
.session-photo-strip img {
  width: 42px;
  height: 42px;
  object-fit: cover;
  border-radius: 8px;
  flex-shrink: 0;
}
.session-profile-card .profile-details {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.66rem;
  line-height: 1.35;
}
.session-profile-card .profile-details li { margin: 0.15rem 0; word-break: break-word; }
.session-profile-card .profile-details strong { font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.03em; color: var(--accent2); display: block; margin-bottom: 0.05rem; }
.session-panel-tag {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent2);
  margin: 0 0 0.4rem;
}
.session-panel-tag-me { color: var(--mint); }
.chat-compose-dock {
  flex-shrink: 0;
  z-index: 100002;
  background: rgba(12, 6, 18, 0.97);
  border-top: 1px solid rgba(255, 107, 157, 0.28);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  padding: 0.35rem 0.45rem calc(0.35rem + env(safe-area-inset-bottom, 0px));
  will-change: transform;
}
.chat-session-compose {
  display: flex;
  flex-direction: row;
  gap: 0.4rem;
  margin-top: 0;
  padding-top: 0;
  align-items: center;
  flex-shrink: 0;
}
.chat-send-wrap {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.12rem;
  flex-shrink: 0;
}
.chat-send-cost {
  font-size: 0.58rem;
  font-weight: 700;
  color: rgba(255, 200, 120, 0.95);
  letter-spacing: 0.02em;
  white-space: nowrap;
  line-height: 1;
}
.chat-send-cost-free {
  color: #5eead4;
  font-size: 0.56rem;
}
.chat-session-compose input {
  flex: 1 1 auto;
  min-width: 0;
  width: auto;
  height: 2.65rem;
  margin: 0;
  padding: 0.5rem 0.75rem;
  font-size: 1rem;
  line-height: 1.3;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(0, 0, 0, 0.45);
  color: #f5f0f8;
  box-sizing: border-box;
}
.chat-session-compose input::placeholder { color: rgba(255, 255, 255, 0.45); }
#btnSessionSend {
  flex: 0 0 auto !important;
  width: auto !important;
  min-width: 0 !important;
  max-width: 5.5rem;
  height: 2.65rem !important;
  margin: 0 !important;
  padding: 0.45rem 0.7rem !important;
  font-size: 0.8rem !important;
  line-height: 1.2 !important;
  animation: none !important;
  transform: none !important;
  box-shadow: 0 4px 14px rgba(255, 77, 141, 0.35) !important;
  white-space: nowrap;
}
.chat-compose-emoji { position: relative; flex-shrink: 0; }
.btn-emoji-toggle {
  width: 2.65rem;
  height: 2.65rem;
  margin: 0;
  padding: 0;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(120, 60, 180, 0.25);
  font-size: 1.35rem;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
}
.btn-emoji-toggle:hover { background: rgba(160, 90, 220, 0.35); }
.btn-photo-toggle {
  width: 2.65rem;
  height: 2.65rem;
  margin: 0;
  padding: 0;
  border-radius: 10px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(94, 234, 212, 0.12);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  flex-shrink: 0;
}
.btn-photo-toggle:hover { background: rgba(94, 234, 212, 0.22); }
.msg.msg-photo {
  max-width: 92%;
  padding: 0.4rem;
  background: rgba(0, 0, 0, 0.25);
}
.msg.msg-photo img {
  display: block;
  width: auto;
  height: auto;
  max-width: min(340px, 100%);
  max-height: min(420px, 55vh);
  border-radius: 12px;
  cursor: zoom-in;
  object-fit: contain;
}
.chat-session-center .msg.msg-photo {
  max-width: 96%;
  padding: 0.45rem;
}
.chat-session-center .msg.msg-photo img {
  max-width: min(420px, 100%);
  max-height: min(480px, 58vh);
  border-radius: 14px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
}
.msg.me.msg-photo { background: transparent; }
.chat-session-center .msg.me.msg-photo .msg-bubble {
  background: rgba(255, 77, 141, 0.18);
  border: 1px solid rgba(255, 107, 157, 0.35);
  padding: 0.35rem;
}
.chat-session-center .msg.them.msg-photo .msg-bubble {
  background: #ececf1;
  padding: 0.35rem;
}
.chat-photo-input { position: absolute; width: 0; height: 0; opacity: 0; pointer-events: none; }
.emoji-picker-popover {
  position: absolute;
  bottom: calc(100% + 0.35rem);
  left: 0;
  z-index: 100010;
  width: min(18rem, calc(100vw - 2rem));
  max-height: 14rem;
  overflow-y: auto;
  padding: 0.5rem 0.55rem;
  border-radius: 12px;
  background: #1a1228;
  border: 1px solid rgba(255, 107, 157, 0.45);
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
}
.emoji-picker-section { margin-bottom: 0.45rem; }
.emoji-picker-section:last-child { margin-bottom: 0; }
.emoji-picker-label {
  margin: 0 0 0.3rem;
  font-size: 0.65rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--accent2);
}
.emoji-picker-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}
.emoji-pick-btn {
  width: 2.35rem;
  height: 2.35rem;
  padding: 0;
  border: none;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.08);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
}
.emoji-pick-btn:hover { background: rgba(255, 77, 141, 0.25); }
.emoji-pick-btn .emoji-pick-icon {
  font-size: 1.65rem;
  display: block;
  line-height: 1.1;
}
.emoji-pick-btn.emoji-pick-premium .emoji-pick-icon,
.emoji-pick-locked .emoji-pick-icon { font-size: 1.85rem; }
.chat-session-center .msg .chat-emoji-anim,
.msg .chat-emoji-anim {
  font-size: 3.25rem;
  line-height: 1.15;
  display: inline-block;
  vertical-align: middle;
  margin: 0 0.08rem;
  transform-origin: center center;
  will-change: transform, filter;
  animation-play-state: running !important;
  animation-iteration-count: infinite !important;
}
.msg .chat-emoji-wrap {
  display: inline-block;
  vertical-align: middle;
  line-height: 1;
}
.msg.msg-emoji-only {
  padding: 0.35rem 0.55rem !important;
  max-width: 92%;
  text-align: center;
}
.msg.msg-emoji-only .chat-emoji-anim,
.msg.msg-emoji-only .chat-emoji-wrap { font-size: 4.25rem; }
.msg.me.msg-emoji-only .msg-bubble {
  background: linear-gradient(145deg, rgba(255, 140, 175, 0.55), rgba(255, 77, 141, 0.45)) !important;
  border: 1px solid rgba(255, 107, 157, 0.35);
}
.msg.them.msg-emoji-only .msg-bubble {
  background: #ececf1 !important;
}
.msg.msg-mixed-emoji { font-size: 1rem; }
.msg.msg-mixed-emoji .chat-emoji-anim { font-size: 2.5rem; }
.emoji-pick-locked {
  position: relative;
  opacity: 0.85;
  border: 1px dashed rgba(255, 200, 100, 0.45);
}
.emoji-pick-cost {
  position: absolute;
  right: 1px;
  bottom: 0;
  font-size: 0.45rem;
  font-weight: 700;
  color: var(--mint);
  line-height: 1;
  pointer-events: none;
}
.chat-panel-compose {
  display: flex;
  flex-direction: row;
  gap: 0.4rem;
  align-items: center;
  margin-top: 0.5rem;
}
.chat-panel-compose input { flex: 1; min-width: 0; margin: 0; }
.chat-panel-compose #btnSend {
  flex: 0 0 auto;
  width: auto !important;
  max-width: 8rem;
  margin: 0 !important;
  padding: 0.5rem 0.75rem !important;
  font-size: 0.8rem !important;
  animation: none !important;
}
.session-profile-card h3 { margin: 0.5rem 0 0.25rem; font-size: 1.05rem; }
.session-profile-card--compact { font-size: 0.72rem; }
.session-profile-card--compact .session-panel-tag { font-size: 0.58rem; margin-bottom: 0.2rem; }
.session-profile-card--compact .session-profile-bio { font-size: 0.66rem; margin-top: 0.25rem; line-height: 1.35; }
.session-profile-card--compact .btn-friend { font-size: 0.65rem; padding: 0.3rem 0.45rem; margin-top: 0.25rem; }
.session-profile-photo {
  border-radius: 12px;
  overflow: hidden;
  background: rgba(0,0,0,0.25);
}
.session-profile-photo img {
  width: 100%;
  display: block;
  aspect-ratio: 4/5;
  max-height: 120px;
  object-fit: cover;
}
.session-profile-photo .avatar-ph {
  aspect-ratio: 3/4;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2.5rem;
}
.session-profile-bio {
  font-size: 0.82rem;
  margin: 0.35rem 0 0;
  line-height: 1.4;
  opacity: 0.9;
}
@media (max-width: 640px) {
  .chat-session-overlay {
    padding: 0;
    padding-top: env(safe-area-inset-top, 0px);
  }
  .chat-session-topbar-actions { min-width: 0; gap: 0.2rem; }
  .btn-view-chat-profile { padding: 0.35rem 0.45rem; font-size: 0.82rem; }
  .btn-block-user { padding: 0.35rem 0.45rem; font-size: 0.68rem; }
  .chat-session-layout {
    grid-template-columns: 1fr;
    grid-template-areas: "chat";
    grid-template-rows: minmax(0, 1fr);
    flex: 1;
    min-height: 0;
    padding: 0;
  }
  #chatSessionOther,
  #chatSessionMe {
    display: none !important;
  }
  .chat-session-center {
    max-height: none;
    align-self: stretch;
    height: 100%;
  }
  .chat-session-center-inner {
    max-height: none;
    height: 100%;
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0.35rem 0.5rem 0;
    display: flex;
    flex-direction: column;
  }
  .chat-session-center-head { display: none; }
  .chat-session-profile-bar:not(.hidden) { display: flex; }
  .chat-session-center #chatSessionCostHint { display: none; }
  .chat-session-center .chat-box {
    max-height: none;
    flex: 1;
    min-height: 0;
    padding-bottom: 0.25rem;
    background: rgba(0, 0, 0, 0.18);
    border-radius: 12px 12px 0 0;
  }
  .chat-compose-dock {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
  }
  .chat-session-overlay {
    display: flex;
    flex-direction: column;
  }
  .chat-session-layout {
    padding-bottom: var(--chat-dock-h, 4.25rem);
  }
  .emoji-picker-popover {
    bottom: calc(100% + 0.5rem);
    z-index: 100020;
  }
  .chat-profile-sheet-panel {
    max-height: min(92dvh, 92vh);
    width: 100%;
    border-radius: 18px 18px 0 0;
  }
  .chat-profile-sheet-head .btn {
    min-height: 44px;
    padding: 0.45rem 0.65rem;
  }
  .chat-profile-tabs button {
    min-height: 44px;
    padding: 0.45rem 0.75rem;
  }
  .chat-session-profile-bar {
    min-height: 48px;
    padding: 0.55rem 0.65rem;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
  }
  .btn-view-chat-profile {
    min-height: 44px;
    min-width: 44px;
    padding: 0.45rem 0.65rem;
  }
}
@media (min-width: 641px) {
  .chat-session-layout {
    grid-template-areas: "partner chat self";
    grid-template-columns: minmax(200px, 26vw) minmax(280px, 1fr) minmax(200px, 26vw);
    padding-bottom: 0;
  }
  .chat-session-profile-bar { display: none !important; }
  .chat-profile-sheet-panel {
    max-width: 440px;
    margin: 0 auto;
    width: calc(100% - 1rem);
    border-radius: 18px 18px 0 0;
  }
  .chat-compose-dock {
    position: static;
    margin: 0 0.45rem 0.35rem;
    border-radius: 14px;
  }
}
.swipe-card-body { padding: 0.65rem 0.85rem 0.8rem; background: rgba(12, 8, 18, 0.92); }
.swipe-card-body h3 { margin: 0 0 0.35rem; font-size: 1.35rem; }
.swipe-card--compact .swipe-card-body > p,
.swipe-card--compact .swipe-card-body .profile-details,
.swipe-card--compact .swipe-card-body .swipe-prompt,
.swipe-card--compact .swipe-card-body .interest-tags,
.swipe-card--compact .swipe-card-body .card-actions-row { display: none; }
.swipe-card--compact .swipe-card-body .btn-friend,
.swipe-card--compact .swipe-card-body .friend-request-actions {
  display: inline-flex;
  width: 100%;
  justify-content: center;
  margin-top: 0.35rem;
  font-size: 0.82rem;
  padding: 0.55rem 0.75rem;
}
.swipe-card--compact .swipe-card-body .btn-chat-on-card {
  margin-top: 0.35rem;
  padding: 0.65rem 0.85rem;
  font-size: 0.88rem;
}
.swipe-prompt {
  margin: 0.5rem 0 0;
  padding: 0.65rem 0.85rem;
  border-radius: 12px;
  background: rgba(0,0,0,0.25);
  border-left: 3px solid var(--accent);
  font-size: 0.9rem;
  font-style: italic;
}
.interest-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.5rem; }
.interest-tags span {
  font-size: 0.75rem;
  padding: 0.25rem 0.55rem;
  border-radius: 999px;
  background: rgba(255,255,255,0.08);
  border: 1px solid var(--border);
}
.swipe-actions {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 1.25rem;
  padding: 0.5rem 0 1rem;
  max-width: 300px;
  margin: 0 auto;
}
.swipe-btn {
  width: 58px;
  height: 58px;
  border-radius: 50%;
  border: none;
  font-size: 1.5rem;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
  transition: transform 0.15s;
}
.swipe-btn:active { transform: scale(0.92); }
.swipe-btn.nope { background: #2a1520; color: #ff6b8a; border: 2px solid rgba(255,107,157,0.4); }
.swipe-btn.like { background: linear-gradient(135deg, var(--accent), var(--accent2)); color: #fff; width: 68px; height: 68px; font-size: 1.75rem; }
.swipe-btn.super { background: linear-gradient(135deg, #5eead4, #3b82f6); color: #0a0610; font-size: 1.25rem; }
@keyframes chatPulse {
  0%, 100% {
    box-shadow: 0 0 0 0 rgba(255, 77, 141, 0.55), 0 8px 28px rgba(255, 77, 141, 0.35);
    transform: scale(1);
  }
  50% {
    box-shadow: 0 0 0 16px rgba(255, 77, 141, 0), 0 12px 36px rgba(184, 77, 255, 0.45);
    transform: scale(1.04);
  }
}
.btn-chat-pulse {
  animation: chatPulse 1.75s ease-in-out infinite;
  font-weight: 700 !important;
  letter-spacing: 0.02em;
  border: none !important;
  background: linear-gradient(135deg, var(--accent), var(--accent2)) !important;
  color: #fff !important;
}
.btn-chat-pulse:hover { filter: brightness(1.08); }
.btn-chat-on-card {
  width: 100%;
  margin-top: 0.75rem;
  padding: 0.85rem 1rem !important;
  font-size: 1.05rem !important;
}
.chat-cta-bar {
  text-align: center;
  padding: 0.5rem 1rem 0.25rem;
}
.chat-cta-bar .meta { margin-bottom: 0.5rem; }
.nav-tabs button[data-tab="matches"] {
  position: relative;
}
.nav-tabs button[data-tab="matches"].nav-chat-pulse::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 14px;
  border: 2px solid rgba(255, 77, 141, 0.65);
  animation: chatPulse 2s ease-in-out infinite;
  pointer-events: none;
}
#btnSend.btn-chat-pulse { width: 100%; margin-top: 0.5rem; }
.match-chat-list .btn-chat-pulse { width: 100%; margin-bottom: 0.5rem; text-align: left; }
.swipe-empty {
  text-align: center;
  padding: 3rem 1rem;
  color: var(--muted);
}
.card-actions-row { display: flex; gap: 0.5rem; margin-top: 0.5rem; flex-wrap: wrap; }
.card-actions-row button {
  flex: 1;
  min-width: 5rem;
  padding: 0.45rem 0.6rem;
  font-size: 0.78rem;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.match-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0,0,0,0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.match-modal.hidden { display: none; }
.match-modal-inner {
  text-align: center;
  max-width: 340px;
  animation: matchPop 0.5s ease;
}
@keyframes matchPop {
  0% { transform: scale(0.7); opacity: 0; }
  100% { transform: scale(1); opacity: 1; }
}
.match-modal-inner h2 {
  font-family: 'Playfair Display', serif;
  font-size: 2.2rem;
  background: linear-gradient(120deg, var(--accent), var(--warm));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
.match-modal.match-modal-celebrate .match-modal-inner {
  animation: matchCelebrate 0.65s cubic-bezier(0.34, 1.35, 0.64, 1);
  box-shadow: 0 0 60px rgba(255, 107, 157, 0.35), 0 20px 50px rgba(0, 0, 0, 0.4);
}
@keyframes matchCelebrate {
  0% { transform: scale(0.55) rotate(-4deg); opacity: 0; }
  55% { transform: scale(1.06) rotate(1deg); opacity: 1; }
  100% { transform: scale(1) rotate(0); opacity: 1; }
}
.match-modal-lead {
  font-size: 1.05rem;
  font-weight: 700;
  color: #fde68a;
  margin: 0.65rem 0 0.35rem;
  line-height: 1.4;
}
.match-modal-bonus {
  display: inline-block;
  margin-top: 0.5rem;
  padding: 0.35rem 0.75rem;
  border-radius: 999px;
  font-size: 0.82rem;
  font-weight: 700;
  color: #86efac;
  background: rgba(34, 197, 94, 0.15);
  border: 1px solid rgba(34, 197, 94, 0.35);
}
.match-avatars {
  display: flex;
  justify-content: center;
  gap: 1rem;
  margin: 1.5rem 0;
}
.match-avatars .avatar { width: 88px; height: 88px; border-radius: 50%; border: 3px solid var(--accent); }
.match-list-heading {
  font-size: 0.95rem;
  margin: 0 0 0.5rem;
  color: var(--accent2);
}
.match-card {
  display: flex;
  gap: 0.75rem;
  align-items: flex-start;
  padding: 0.75rem;
  margin-bottom: 0.5rem;
  border-radius: 14px;
  background: linear-gradient(135deg, rgba(255,77,141,0.12), rgba(184,77,255,0.08));
  border: 1px solid rgba(255, 107, 157, 0.45);
  cursor: pointer;
  transition: border-color 0.15s;
}
.match-card:hover { border-color: var(--accent); }
.match-card .avatar { width: 56px; height: 56px; border-radius: 14px; }
.match-card-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-top: 0.5rem; width: 100%; }
.match-card-actions .btn { flex: 1; min-width: 7rem; font-size: 0.78rem; padding: 0.45rem 0.5rem; }
.match-badge {
  display: inline-block;
  font-size: 0.72rem;
  font-weight: 700;
  color: #fff;
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  margin-top: 0.25rem;
}
.like-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.85rem;
  margin-bottom: 0.65rem;
  border-radius: 16px;
  border: 1px solid var(--border);
  background: var(--card);
  cursor: pointer;
}
.like-card .avatar { width: 56px; height: 56px; border-radius: 14px; }
.like-card:hover { border-color: var(--accent); }
.like-card-match {
  border-color: rgba(255, 107, 157, 0.55) !important;
  background: linear-gradient(135deg, rgba(255, 77, 141, 0.12), rgba(184, 77, 255, 0.06));
}
.nav-tabs .nav-badge {
  font-size: 0.65rem;
  background: var(--accent);
  color: #fff;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  margin-left: 0.25rem;
  vertical-align: super;
  font-weight: 800;
  min-width: 1.1rem;
  text-align: center;
}
.nav-tabs button.nav-has-alert .nav-badge {
  box-shadow: 0 0 0 2px rgba(255, 77, 141, 0.35);
}
.inbox-alert-dot {
  display: inline-block;
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: #ff4d8d;
  margin-left: 0.35rem;
  vertical-align: middle;
  box-shadow: 0 0 8px rgba(255, 77, 141, 0.65);
}
.match-card.has-unread,
.like-card.has-unread {
  border-color: rgba(255, 77, 141, 0.55) !important;
}
.chat-unread-label {
  display: inline-block;
  margin-top: 0.25rem;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  background: rgba(255, 77, 141, 0.18);
  color: #ffb8d4;
  font-size: 0.72rem;
  font-weight: 700;
}
.safety-card {
  margin-top: 1rem;
  padding: 1rem;
  border-radius: 16px;
  border: 1px solid rgba(94,234,212,0.3);
  background: rgba(94,234,212,0.06);
}
.safety-card h3 { margin: 0 0 0.5rem; font-size: 1rem; color: var(--mint); }
.safety-card ul { margin: 0; padding-left: 1.1rem; font-size: 0.85rem; color: var(--muted); }
.safety-card li { margin: 0.35rem 0; }
.compare-grid {
  display: grid;
  gap: 1.25rem;
  max-width: 920px;
  margin: 1.5rem auto 0;
  grid-template-columns: 1fr;
}
@media (min-width: 640px) {
  .compare-grid { grid-template-columns: 1fr 1fr; align-items: stretch; }
}
.compare-card {
  padding: 1.35rem 1.4rem;
  border-radius: 16px;
  border: 1px solid var(--border);
  background: var(--card);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.compare-card h3 {
  margin: 0 0 0.85rem;
  font-size: 1.12rem;
  font-weight: 800;
  letter-spacing: 0.01em;
}
.compare-card ul {
  margin: 0;
  padding-left: 1.15rem;
  line-height: 1.75;
  color: var(--muted);
}
.compare-card.good {
  border: 2px solid rgba(255, 120, 130, 0.55);
  background: linear-gradient(155deg, rgba(255, 107, 120, 0.12) 0%, rgba(18, 12, 22, 0.92) 55%);
  box-shadow: 0 10px 36px rgba(255, 107, 120, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.compare-card.good h3 { color: #ffb4c0; }
.compare-card.good li strong { color: var(--text); }
.compare-card.good:hover {
  transform: translateY(-2px);
  box-shadow: 0 14px 42px rgba(255, 107, 120, 0.2);
}
.compare-card.bad {
  opacity: 0.62;
  filter: grayscale(0.35) saturate(0.7);
  border: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(0, 0, 0, 0.28);
  box-shadow: none;
}
.compare-card.bad h3 { color: rgba(255, 255, 255, 0.45); }
.compare-card.bad ul { color: rgba(196, 176, 212, 0.55); }
.real-promise {
  text-align: center;
  padding: clamp(2rem, 5vw, 3rem) clamp(1.25rem, 5vw, 3rem);
  background: linear-gradient(180deg, rgba(94, 234, 212, 0.07), rgba(94, 234, 212, 0.02), transparent);
}
.real-promise h2 { font-family: 'Playfair Display', serif; font-size: clamp(1.6rem, 4vw, 2.2rem); }
.profile-complete-card {
  border: 1px solid rgba(94, 234, 212, 0.4);
  background: linear-gradient(135deg, rgba(94, 234, 212, 0.1), rgba(255, 77, 141, 0.08));
}
.profile-complete-card h3 { margin: 0 0 0.5rem; font-size: 1.05rem; color: var(--mint); }
.profile-complete-bar {
  height: 10px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.35);
  overflow: hidden;
  margin: 0.5rem 0 0.65rem;
}
.profile-complete-bar > div {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--mint), var(--accent2));
  transition: width 0.35s ease;
}
.profile-complete-checks {
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.8rem;
}
.profile-complete-checks li { padding: 0.2rem 0; color: var(--muted); }
.profile-complete-checks li.done { color: var(--mint); }
.profile-complete-checks li.done::before { content: '✓ '; }
.profile-complete-checks li.todo::before { content: '○ '; }
.profile-complete-reward {
  margin: 0.65rem 0 0;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--accent2);
}
.profile-boost-card {
  border: 1px solid rgba(255, 200, 80, 0.35);
  background: linear-gradient(145deg, rgba(255, 180, 50, 0.12), rgba(20, 24, 32, 0.95));
}
.profile-boost-card h3 { margin: 0 0 0.45rem; font-size: 1.05rem; color: #ffc857; }
.profile-boost-card.is-active {
  border-color: rgba(255, 200, 80, 0.55);
  box-shadow: 0 0 24px rgba(255, 180, 50, 0.15);
}
.profile-boost-card .boost-timer {
  font-size: 1.35rem;
  font-weight: 700;
  color: #ffc857;
  margin: 0.35rem 0;
}
.profile-boost-card .btn-boost {
  width: 100%;
  margin-top: 0.65rem;
  font-size: 0.92rem;
  line-height: 1.35;
  padding: 0.75rem 1rem;
  background: linear-gradient(135deg, #ffb347, #ff8c00);
  color: #1a1208;
  border: none;
  font-weight: 700;
}
.profile-boost-card .btn-boost:disabled { opacity: 0.55; cursor: not-allowed; }
.profile-referral-card {
  border: 1px solid rgba(110, 231, 215, 0.35);
  background: linear-gradient(145deg, rgba(110, 231, 215, 0.1), rgba(20, 24, 32, 0.95));
}
.profile-referral-card h3 { margin: 0 0 0.45rem; font-size: 1.05rem; color: var(--mint); }
.profile-referral-card .referral-lead { margin: 0 0 0.75rem; line-height: 1.45; }
.profile-referral-card .referral-link-row {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}
.profile-referral-card .referral-link-input {
  flex: 1 1 12rem;
  min-width: 0;
  padding: 0.55rem 0.75rem;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: rgba(0,0,0,0.25);
  color: var(--text);
  font-size: 0.82rem;
}
.profile-referral-card .btn-referral-copy {
  flex: 0 0 auto;
  background: linear-gradient(135deg, var(--mint), #3dd9c4);
  color: #0a1614;
  border: none;
  font-weight: 700;
}
.swipe-card-body .boost-badge {
  display: inline-block;
  font-size: 0.72rem;
  font-weight: 700;
  padding: 0.2rem 0.55rem;
  border-radius: 999px;
  background: linear-gradient(135deg, rgba(255, 180, 50, 0.35), rgba(255, 120, 0, 0.25));
  color: #ffc857;
  margin: 0.15rem 0 0.35rem;
}
.activity-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.2rem 0 0.4rem;
}
.activity-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  font-size: 0.68rem;
  font-weight: 700;
  padding: 0.18rem 0.5rem;
  border-radius: 999px;
  line-height: 1.25;
  border: 1px solid transparent;
}
.activity-badge-very_active {
  background: rgba(34, 197, 94, 0.18);
  border-color: rgba(34, 197, 94, 0.35);
  color: #86efac;
}
.activity-badge-fast_replier {
  background: rgba(96, 165, 250, 0.18);
  border-color: rgba(96, 165, 250, 0.4);
  color: #93c5fd;
}
.profile-badges-hint {
  margin: 0.35rem 0 0;
  font-size: 0.78rem;
  color: var(--muted);
}

.profile-complete-card.is-done {
  border-color: rgba(94, 234, 212, 0.65);
}
.emoji-shop-card {
  margin-top: 1rem;
  padding: 1rem 1.05rem 1.1rem;
  border-radius: 18px;
  border: 2px solid rgba(255, 107, 157, 0.55);
  background: linear-gradient(145deg, rgba(255, 77, 141, 0.14), rgba(120, 60, 180, 0.2));
  box-shadow: 0 12px 36px rgba(255, 77, 141, 0.22);
}
.emoji-shop-hero {
  text-align: center;
  margin-bottom: 0.85rem;
  padding-bottom: 0.75rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
}
.emoji-shop-hero h3 {
  margin: 0 0 0.35rem;
  font-size: 1.35rem;
  background: linear-gradient(90deg, #ff6b9d, #c084fc, #5eead4);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}
.emoji-shop-hero p { margin: 0; font-size: 0.82rem; color: var(--muted); }
.profile-name-emoji {
  display: inline-block;
  margin-left: 0.3rem;
  font-size: 1.12em;
  vertical-align: middle;
  line-height: 1;
  filter: drop-shadow(0 0 6px rgba(255, 180, 80, 0.35));
}
.swipe-card-body h3 .profile-name-emoji {
  font-size: 1.25rem;
}
.profile-emoji-picker {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.65rem;
}
.profile-emoji-pick {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.75rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--card);
  cursor: pointer;
  font-size: 0.88rem;
  font-weight: 600;
  transition: border-color 0.2s, background 0.2s, transform 0.15s;
}
.profile-emoji-pick:hover { border-color: var(--accent); transform: translateY(-1px); }
.profile-emoji-pick.is-active {
  border-color: var(--accent);
  background: rgba(255, 107, 157, 0.18);
  box-shadow: 0 0 12px rgba(255, 107, 157, 0.25);
}
.profile-emoji-pick .pick-icon { font-size: 1.25rem; line-height: 1; }

.emoji-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));
  gap: 0.55rem;
}
.emoji-item {
  text-align: center;
  padding: 0.75rem 0.4rem 0.65rem;
  border-radius: 14px;
  border: 1px solid rgba(255, 107, 157, 0.35);
  cursor: pointer;
  background: rgba(0, 0, 0, 0.28);
  transition: transform 0.15s, box-shadow 0.15s, border-color 0.15s;
}
.emoji-item:hover {
  transform: translateY(-2px);
  border-color: rgba(255, 107, 157, 0.7);
  box-shadow: 0 8px 20px rgba(255, 77, 141, 0.25);
}
.emoji-item.emoji-owned {
  border-color: rgba(94, 234, 212, 0.55);
  background: rgba(94, 234, 212, 0.08);
}
.emoji-shop-icon {
  font-size: 2.35rem;
  display: block;
  line-height: 1.1;
  margin-bottom: 0.25rem;
  will-change: transform;
}
.emoji-item strong {
  display: block;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--text);
  margin-bottom: 0.15rem;
}
.emoji-item small {
  font-size: 0.68rem;
  color: var(--accent2);
  font-weight: 600;
}
.emoji-item.emoji-owned small { color: var(--mint); }
@keyframes emoji-pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.28); }
}
@keyframes emoji-shop-heart-pulse {
  0%, 100% {
    transform: scale(1);
    filter: drop-shadow(0 0 0 rgba(255, 77, 109, 0));
  }
  50% {
    transform: scale(1.18);
    filter: drop-shadow(0 0 14px rgba(255, 77, 109, 0.75));
  }
}
.emoji-item.emoji-shop-heart-feature .emoji-shop-icon {
  animation: emoji-shop-heart-pulse 1.15s ease-in-out infinite;
}
@keyframes emoji-heartbeat {
  0%, 100% { transform: scale(1); }
  15% { transform: scale(1.2); }
  30% { transform: scale(1); }
  45% { transform: scale(1.26); }
}
@keyframes emoji-fire {
  0%, 100% { transform: scale(1) rotate(-4deg); filter: brightness(1); }
  50% { transform: scale(1.18) rotate(4deg); filter: brightness(1.35); }
}
@keyframes emoji-sparkle {
  0%, 100% { transform: scale(1) rotate(0deg); opacity: 1; }
  50% { transform: scale(1.2) rotate(18deg); opacity: 0.85; }
}
@keyframes emoji-sway {
  0%, 100% { transform: rotate(-8deg); }
  50% { transform: rotate(8deg); }
}
@keyframes emoji-spin {
  0% { transform: rotate(0deg) scale(1); }
  100% { transform: rotate(360deg) scale(1); }
}
@keyframes emoji-glow {
  0%, 100% { filter: drop-shadow(0 0 4px rgba(94, 234, 212, 0.4)); transform: scale(1); }
  50% { filter: drop-shadow(0 0 14px rgba(255, 77, 141, 0.9)); transform: scale(1.12); }
}
@keyframes emoji-bounce {
  0%, 100% { transform: translateY(0); }
  40% { transform: translateY(-10px); }
  60% { transform: translateY(-4px); }
}
@keyframes emoji-wiggle {
  0%, 100% { transform: rotate(0deg); }
  25% { transform: rotate(-12deg); }
  75% { transform: rotate(12deg); }
}
@keyframes emoji-float {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}
.emoji-anim-pulse .emoji-shop-icon,
.emoji-anim-pulse .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-pulse .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-pulse {
  animation: emoji-pulse 1.1s ease-in-out infinite;
  animation-play-state: running;
}
.emoji-anim-heartbeat .emoji-shop-icon,
.emoji-anim-heartbeat .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-heartbeat .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-heartbeat {
  animation: emoji-heartbeat 1.4s ease-in-out infinite;
  animation-play-state: running;
}
.emoji-anim-fire .emoji-shop-icon,
.emoji-anim-fire .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-fire .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-fire {
  animation: emoji-fire 0.85s ease-in-out infinite;
  animation-play-state: running;
}
.emoji-anim-sparkle .emoji-shop-icon,
.emoji-anim-sparkle .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-sparkle .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-sparkle {
  animation: emoji-sparkle 1.2s ease-in-out infinite;
  animation-play-state: running;
}
.emoji-anim-sway .emoji-shop-icon,
.emoji-anim-sway .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-sway .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-sway {
  animation: emoji-sway 2s ease-in-out infinite;
  animation-play-state: running;
}
.emoji-anim-spin .emoji-shop-icon,
.emoji-anim-spin .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-spin .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-spin {
  animation: emoji-spin 3.5s linear infinite;
  animation-play-state: running;
}
.emoji-anim-glow .emoji-shop-icon,
.emoji-anim-glow .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-glow .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-glow {
  animation: emoji-glow 1.5s ease-in-out infinite;
  animation-play-state: running;
}
.emoji-anim-bounce .emoji-shop-icon,
.emoji-anim-bounce .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-bounce .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-bounce {
  animation: emoji-bounce 1s ease-in-out infinite;
  animation-play-state: running;
}
.emoji-anim-wiggle .emoji-shop-icon,
.emoji-anim-wiggle .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-wiggle .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-wiggle {
  animation: emoji-wiggle 0.7s ease-in-out infinite;
  animation-play-state: running;
}
.emoji-anim-float .emoji-shop-icon,
.emoji-anim-float .emoji-pick-icon,
.chat-emoji-wrap.emoji-anim-float .chat-emoji-anim,
.chat-emoji-anim.emoji-anim-float {
  animation: emoji-float 2.2s ease-in-out infinite;
  animation-play-state: running;
}
@media (prefers-reduced-motion: reduce) {
  .emoji-shop-icon,
  .emoji-pick-icon { animation: none !important; }
}
.profile-details { list-style: none; padding: 0; margin: 0.65rem 0 0; }
.profile-details li { font-size: 0.88rem; padding: 0.2rem 0; color: var(--text); }
.profile-details strong { color: var(--accent); font-weight: 600; margin-right: 0.35rem; }
.register-section { margin-top: 1rem; padding-top: 0.85rem; border-top: 1px solid var(--border); }
.register-section h4 { margin: 0 0 0.65rem; font-size: 0.95rem; color: var(--accent); }
.register-hint {
  margin: 0 0 1rem;
  padding: 0.65rem 0.75rem;
  border-radius: 12px;
  background: rgba(94, 234, 212, 0.1);
  border: 1px solid rgba(94, 234, 212, 0.35);
  font-size: 0.88rem;
  color: var(--text);
  line-height: 1.45;
}
.profile-edit-grid label { display: block; margin-top: 0.5rem; font-size: 0.85rem; }
.profile-edit-grid input, .profile-edit-grid select, .profile-edit-grid textarea {
  width: 100%; margin-top: 0.2rem;
}
body.legal-page {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
}
.legal-header {
  padding: 1.1rem 1.5rem;
  border-bottom: 1px solid var(--border);
  background: rgba(15, 10, 20, 0.9);
}
.legal-header .logo .logo-text {
  font-size: 1.2rem;
}
.legal-main {
  flex: 1;
  width: 100%;
  max-width: 42rem;
  margin: 0 auto;
  padding: 2.25rem 1.5rem 3rem;
  line-height: 1.65;
  box-sizing: border-box;
}
.legal-main h1 {
  margin: 0 0 1.5rem;
  font-size: clamp(1.65rem, 4vw, 2.1rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.2;
}
.legal-content { font-size: 1rem; }
.legal-content p { margin: 0 0 1rem; color: rgba(255, 249, 252, 0.92); }
.legal-content ul, .legal-content ol { margin: 0 0 1rem; padding-left: 1.35rem; }
.legal-content li { margin: 0.35rem 0; color: rgba(255, 249, 252, 0.88); }
.legal-content a { color: var(--mint); }
.legal-content h2 {
  margin: 2rem 0 0.75rem;
  font-size: 1.15rem;
  font-weight: 700;
}
.legal-placeholder-hint {
  margin: 0;
  padding: 1.35rem 1.5rem;
  border: 1px dashed rgba(255, 255, 255, 0.22);
  border-radius: 14px;
  color: var(--muted);
  font-size: 0.95rem;
  background: rgba(255, 255, 255, 0.04);
}
.legal-footer {
  text-align: center;
  padding: 1.75rem 1.5rem;
  color: var(--muted);
  font-size: 0.88rem;
  border-top: 1px solid var(--border);
}
.legal-footer a { color: var(--muted); margin: 0 0.35rem; text-decoration: underline; text-underline-offset: 3px; }
.legal-footer a:hover { color: var(--mint); }
.hidden { display: none !important; }
.ad-reward-card {
  background: linear-gradient(135deg, rgba(255,77,141,0.12), rgba(94,234,212,0.1));
  border: 1px solid rgba(255,77,141,0.35);
  border-radius: 16px;
  padding: 1rem 1.1rem;
  margin-bottom: 1rem;
}
.ad-reward-card h3 { margin: 0 0 0.35rem; font-size: 1.05rem; }
.no-coins-modal {
  position: fixed;
  inset: 0;
  z-index: 210;
  background: rgba(6, 4, 10, 0.92);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.25rem;
}
.no-coins-modal.hidden { display: none; }
.no-coins-modal-inner {
  width: min(400px, 100%);
  text-align: center;
  padding: 1.75rem 1.5rem 1.5rem;
  border-radius: 20px;
  background: linear-gradient(160deg, rgba(45, 22, 58, 0.98), rgba(18, 10, 28, 0.98));
  border: 1px solid rgba(255, 180, 80, 0.4);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55), 0 0 40px rgba(255, 107, 157, 0.2);
  animation: matchCelebrate 0.55s cubic-bezier(0.34, 1.35, 0.64, 1);
}
.no-coins-icon {
  display: block;
  font-size: 3rem;
  margin-bottom: 0.5rem;
  filter: drop-shadow(0 0 16px rgba(255, 200, 80, 0.45));
}
.no-coins-modal-inner h2 {
  margin: 0 0 0.65rem;
  font-size: 1.55rem;
  line-height: 1.25;
  background: linear-gradient(120deg, #fde68a, var(--accent));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
}
.no-coins-lead {
  margin: 0 0 0.5rem;
  font-size: 1.08rem;
  font-weight: 600;
  color: var(--text);
  line-height: 1.45;
}
.no-coins-modal-inner .btn-primary {
  width: 100%;
  margin-top: 1rem;
  font-size: 1.02rem;
  padding: 0.85rem 1rem;
}
.no-coins-modal-inner .btn-ghost {
  width: 100%;
  margin-top: 0.45rem;
}
.ad-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(8, 4, 12, 0.88);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}
.ad-modal-inner {
  width: min(420px, 100%);
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 1.25rem;
  text-align: center;
}
.ad-slot {
  min-height: 140px;
  border-radius: 12px;
  border: 1px dashed var(--border);
  background: rgba(0,0,0,0.25);
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  margin: 0.75rem 0;
  padding: 0.75rem;
  font-size: 0.9rem;
  color: var(--muted);
}
.ad-slot.ad-slot--youtube {
  padding: 0;
  border: none;
  background: #000;
  min-height: 0;
}
.ad-youtube-wrap {
  position: relative;
  width: 100%;
  padding-bottom: 56.25%;
  height: 0;
  border-radius: 12px;
  overflow: hidden;
  background: #000;
}
.ad-youtube-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}
.ad-youtube-hint {
  margin: 0.45rem 0.5rem 0.15rem !important;
  font-size: 0.78rem !important;
}
.ad-progress {
  height: 6px;
  background: rgba(255,255,255,0.08);
  border-radius: 99px;
  overflow: hidden;
  margin: 0.5rem 0;
}
.ad-progress > div {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg, var(--accent), #5eead4);
  transition: width 0.2s linear;
}
.toast {
  position: fixed;
  bottom: 1.25rem;
  left: 50%;
  transform: translateX(-50%);
  background: #1a1222;
  border: 1px solid var(--accent);
  padding: 0.75rem 1.25rem;
  border-radius: 12px;
  z-index: 100;
  max-width: 90%;
  text-align: center;
}

/* ─── Herzen & Funkeln (ganze Seite) ─── */
.page-fx {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  overflow: hidden;
}
body.mode-app .page-fx { opacity: 0.92; }
body.mode-app .page-fx .fx-heart { opacity: calc(var(--o, 0.35) * 0.85); }
.push-prompt-modal {
  position: fixed;
  inset: 0;
  z-index: 205;
  background: rgba(6, 4, 10, 0.75);
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
}
.push-prompt-modal.hidden { display: none; }
.push-prompt-inner {
  width: min(420px, 100%);
  background: linear-gradient(165deg, #2a1538, #1a0e24);
  border: 1px solid rgba(255, 107, 157, 0.4);
  border-radius: 18px 18px 14px 14px;
  padding: 1.15rem 1.2rem 1rem;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
}
.push-prompt-inner h3 { margin: 0 0 0.45rem; font-size: 1.05rem; }
.push-prompt-inner p { margin: 0 0 1rem; font-size: 0.9rem; color: var(--muted); line-height: 1.45; }
.push-prompt-actions { display: flex; flex-direction: column; gap: 0.45rem; }
.push-prompt-actions .btn-primary { width: 100%; }
#authScreen, #appShell, .landing-header, .app-header, .match-modal, .profile-photo-modal, .ad-modal, .no-coins-modal, .push-prompt-modal {
  position: relative;
  z-index: 2;
}
.landing-screen > *:not(.page-fx) { position: relative; z-index: 2; }
.fx-heart {
  position: absolute;
  left: var(--x);
  bottom: 0;
  font-size: var(--s, 0.85rem);
  line-height: 1;
  filter: drop-shadow(0 0 8px rgba(255,77,141,0.45));
  animation: fxFlyUp var(--d, 22s) linear infinite;
  animation-delay: var(--delay, 0s);
}
.fx-sparkle {
  position: absolute;
  left: var(--x);
  bottom: 0;
  width: var(--sz, 5px);
  height: var(--sz, 5px);
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, rgba(255,200,230,0.6) 35%, transparent 70%);
  box-shadow: 0 0 10px rgba(255,255,255,0.8), 0 0 18px rgba(255,77,141,0.35);
  animation: fxSparkleFly var(--d, 14s) linear infinite;
  animation-delay: var(--delay, 0s);
}
.fx-glitter {
  position: absolute;
  left: var(--x);
  bottom: 0;
  font-size: var(--s, 0.6rem);
  line-height: 1;
  filter: drop-shadow(0 0 6px rgba(255,255,255,0.7));
  animation: fxSparkleFly var(--d, 16s) linear infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes fxFlyUp {
  0% { transform: translate(0, 110vh) rotate(-8deg) scale(0.85); opacity: 0; }
  6% { opacity: var(--o, 0.4); }
  94% { opacity: var(--o, 0.35); }
  100% { transform: translate(var(--drift, 8px), calc(-100vh - 4rem)) rotate(12deg) scale(1.05); opacity: 0; }
}
@keyframes fxSparkleFly {
  0% { transform: translate(0, 110vh) scale(0.5); opacity: 0; }
  10% { opacity: 0.9; transform: translate(calc(var(--drift, 12px) * 0.25), 85vh) scale(1); }
  50% { opacity: 0.75; transform: translate(calc(var(--drift, 12px) * 0.5), 40vh) scale(1.1); }
  90% { opacity: 0.35; transform: translate(var(--drift, 12px), -5vh) scale(0.95); }
  100% { transform: translate(var(--drift, 18px), calc(-100vh - 3rem)) scale(0.6); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .page-fx .fx-heart,
  .page-fx .fx-sparkle,
  .page-fx .fx-glitter { animation: none; opacity: 0.15; }
  .btn-chat-pulse,
  .nav-tabs button[data-tab="matches"].nav-chat-pulse::after { animation: none; }
  .landing-urgency,
  .landing-online-dot,
  .landing-live-ticker,
  .landing-live-ticker-track,
  .landing-hook-chip--hot,
  .hero-scroll-hint,
  .landing-hero-text h1 em,
  .showcase-match-flash,
  .landing-hero-text h1,
  .landing-lead { animation: none; }
}

/* ─── Cookie-Einwilligung ─── */
.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 350;
  padding: 1rem;
  pointer-events: none;
}
.cookie-banner.hidden { display: none; }
.cookie-banner-inner {
  pointer-events: auto;
  max-width: 520px;
  margin: 0 auto;
  background: var(--glass);
  border: 1px solid var(--border);
  border-radius: 18px;
  padding: 1.15rem 1.25rem;
  box-shadow: var(--shadow);
  backdrop-filter: blur(12px);
}
.cookie-banner h3 { margin: 0 0 0.4rem; font-size: 1.05rem; }
.cookie-banner p { margin: 0 0 0.75rem; font-size: 0.85rem; color: var(--muted); line-height: 1.45; }
.cookie-banner-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.cookie-banner-actions .btn { flex: 1; min-width: 120px; font-size: 0.85rem; padding: 0.65rem; }
.cookie-settings-panel {
  margin-top: 0.85rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--border);
  display: none;
}
.cookie-settings-panel.open { display: block; }
.cookie-toggle-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.5rem 0;
  font-size: 0.82rem;
}
.cookie-toggle-row strong { display: block; color: var(--text); }
.cookie-toggle-row span { color: var(--muted); font-size: 0.78rem; }
.cookie-toggle-row input { margin-top: 0.2rem; accent-color: var(--accent); }
.cookie-toggle-row input:disabled { opacity: 0.6; }
.cookie-footer-link {
  font-size: 0.8rem;
  color: var(--muted);
  cursor: pointer;
  text-decoration: underline;
  background: none;
  border: none;
  padding: 0;
  margin-top: 0.5rem;
}
body.fonts-loaded { font-family: 'DM Sans', system-ui, sans-serif; }
body.fonts-loaded .landing-hero h1,
body.fonts-loaded .compare-card h3,
body.fonts-loaded .real-promise h2,
body.fonts-loaded .match-modal-inner h2 { font-family: 'Playfair Display', Georgia, serif; }

.landing-screen {
  display: block;
  max-width: none;
  padding: 0;
  overflow-x: hidden;
}
.beta-test-signup {
  position: relative;
  z-index: 55;
  margin: 0;
  padding: 1.35rem clamp(1.1rem, 3vw, 2.25rem) 1.75rem;
  background: transparent;
  scroll-margin-top: 4.5rem;
}
.beta-test-signup-inner {
  max-width: 960px;
  margin: 0 auto;
}
.beta-test-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.7rem 1rem;
  margin-bottom: 0.75rem;
}
.beta-test-reward {
  display: inline-block;
  margin: 0;
  padding: 0.4rem 1rem;
  border-radius: 999px;
  background: linear-gradient(135deg, #ffd700, #ffb300);
  color: #1a0a10;
  font-weight: 800;
  font-size: 1rem;
  letter-spacing: 0.01em;
}
.beta-test-signup h2 {
  margin: 0;
  font-size: clamp(1.25rem, 3.5vw, 1.65rem);
  line-height: 1.25;
  font-weight: 700;
}
.beta-test-lead {
  margin: 0 0 1.15rem;
  color: var(--muted);
  font-size: 1.02rem;
  line-height: 1.5;
}
.beta-test-form {
  display: grid;
  gap: 0.85rem;
}
@media (min-width: 560px) {
  .beta-test-form {
    grid-template-columns: 1fr 1fr auto;
    align-items: end;
  }
  .beta-test-form label:last-of-type { grid-column: auto; }
  .beta-test-form button {
    white-space: nowrap;
    min-width: 10.5rem;
    padding: 0.85rem 1.25rem;
    font-size: 1.05rem;
  }
}
.beta-test-form label span {
  display: block;
  font-size: 0.88rem;
  color: var(--muted);
  margin-bottom: 0.4rem;
}
.beta-test-form input {
  width: 100%;
  box-sizing: border-box;
  padding: 0.82rem 0.95rem;
  border-radius: 11px;
  border: 1px solid rgba(255, 255, 255, 0.15);
  background: rgba(0, 0, 0, 0.35);
  color: var(--text);
  font: inherit;
  font-size: 1.05rem;
}
.beta-test-form input:focus {
  outline: 2px solid rgba(255, 215, 0, 0.55);
  border-color: rgba(255, 215, 0, 0.45);
}
.beta-test-form.hidden { display: none; }
.beta-test-success {
  margin-top: 1rem;
  padding: 1rem 1.1rem;
  border-radius: 12px;
  background: rgba(105, 240, 174, 0.12);
  border: 1px solid rgba(105, 240, 174, 0.35);
}
.beta-test-success.hidden { display: none; }
.beta-test-success p { margin: 0 0 0.65rem; line-height: 1.5; }
.beta-test-link-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}
.beta-test-link-row a {
  color: #ffd700;
  word-break: break-all;
  font-size: 0.88rem;
}
.beta-test-steps {
  margin: 0.75rem 0 0;
  padding-left: 1.1rem;
  color: var(--muted);
  font-size: 0.85rem;
  line-height: 1.55;
}
.landing-screen .app-header { display: none; }
.landing-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem clamp(1rem, 4vw, 3rem);
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 60;
  background: linear-gradient(180deg, rgba(10,6,16,0.92), transparent);
  backdrop-filter: blur(8px);
}
.landing-nav {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.landing-nav a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9rem;
  padding: 0.5rem 0.75rem;
  display: none;
}
.landing-nav a#navLogin { display: inline; }
@media (min-width: 720px) { .landing-nav a { display: inline; } }
.landing-nav a:hover { color: var(--text); }
.btn-header-cta {
  width: auto;
  padding: 0.65rem 1.25rem;
  font-size: 0.9rem;
  border-radius: 999px;
  box-shadow: 0 8px 32px rgba(255,77,141,0.45);
}
.landing-hero {
  position: relative;
  min-height: min(88vh, 820px);
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
  align-items: center;
  padding: 6.5rem clamp(1.25rem, 5vw, 4rem) 1.5rem;
  max-width: 1200px;
  margin: 0 auto;
  overflow: hidden;
}
.landing-hero-glow {
  position: absolute;
  inset: -10% -20% auto -20%;
  height: 70%;
  background:
    radial-gradient(ellipse 80% 60% at 20% 40%, rgba(255, 77, 141, 0.22), transparent 65%),
    radial-gradient(ellipse 60% 50% at 80% 30%, rgba(184, 77, 255, 0.16), transparent 60%);
  pointer-events: none;
  z-index: 0;
}
.landing-hero-text,
.profile-showcase {
  position: relative;
  z-index: 1;
}
.landing-urgency {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin: 0 0 0.85rem;
  padding: 0.4rem 0.85rem;
  border-radius: 999px;
  font-size: 0.82rem;
  font-weight: 700;
  color: #ffe8f0;
  background: linear-gradient(90deg, rgba(255, 77, 141, 0.45), rgba(255, 153, 51, 0.35));
  border: 1px solid rgba(255, 153, 51, 0.65);
  animation: urgencyGlow 1.5s ease-in-out infinite;
}
@keyframes urgencyGlow {
  0%, 100% {
    opacity: 1;
    box-shadow:
      0 0 14px rgba(255, 120, 80, 0.55),
      0 0 28px rgba(255, 77, 141, 0.35);
  }
  50% {
    opacity: 0.82;
    box-shadow:
      0 0 28px rgba(255, 153, 51, 0.9),
      0 0 52px rgba(255, 77, 141, 0.65),
      0 0 80px rgba(255, 45, 149, 0.3);
  }
}
.landing-online-nudge {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  margin: 0 0 1rem;
  font-size: 0.88rem;
  color: var(--mint);
  font-weight: 600;
}
.landing-online-dot {
  width: 0.55rem;
  height: 0.55rem;
  border-radius: 50%;
  background: #5eead4;
  box-shadow: 0 0 10px rgba(94, 234, 212, 0.8);
  animation: onlinePulse 1.8s ease-in-out infinite;
}
@keyframes onlinePulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.25); opacity: 0.75; }
}
.landing-hook-row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.45rem;
  margin: 1.1rem 0 0;
  max-width: 32rem;
}
.landing-hook-chip {
  padding: 0.35rem 0.7rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
  color: #fff9fc;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255, 255, 255, 0.14);
}
.landing-hook-chip--hot {
  background: rgba(255, 77, 141, 0.28);
  border-color: rgba(255, 77, 141, 0.65);
  color: #ffd6e8;
  animation: hotChipBlink 1.7s ease-in-out infinite;
}
@keyframes hotChipBlink {
  0%, 100% {
    opacity: 1;
    box-shadow: 0 0 10px rgba(255, 77, 141, 0.45);
  }
  50% {
    opacity: 0.78;
    box-shadow: 0 0 24px rgba(255, 77, 141, 0.8), 0 0 40px rgba(255, 153, 51, 0.35);
  }
}
.landing-live-ticker {
  overflow: hidden;
  margin: 1.15rem 0 0;
  max-width: min(100%, 28rem);
  border-radius: 999px;
  background: rgba(10, 6, 16, 0.55);
  border: 1px solid rgba(255, 77, 141, 0.35);
  padding: 0.5rem 0;
  mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
  animation: tickerBorderGlow 2.2s ease-in-out infinite;
}
@keyframes tickerBorderGlow {
  0%, 100% { box-shadow: 0 0 8px rgba(255, 77, 141, 0.25); border-color: rgba(255, 77, 141, 0.35); }
  50% { box-shadow: 0 0 22px rgba(255, 77, 141, 0.55); border-color: rgba(255, 153, 51, 0.55); }
}
.landing-live-ticker-track {
  display: flex;
  gap: 2.5rem;
  width: max-content;
  white-space: nowrap;
  font-size: 0.8rem;
  color: rgba(255, 249, 252, 0.88);
  animation: liveTicker 28s linear infinite;
}
.landing-live-ticker-track span { padding-left: 2.5rem; }
@keyframes liveTicker {
  0% { transform: translateX(0); }
  100% { transform: translateX(-50%); }
}
.hero-scroll-hint {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  margin-top: 1.1rem;
  padding: 0.65rem 1.15rem;
  border: 1px solid rgba(255, 77, 141, 0.55);
  border-radius: 999px;
  background: linear-gradient(135deg, rgba(255, 77, 141, 0.28), rgba(255, 153, 51, 0.16));
  color: #ffe8f0;
  font-size: 0.88rem;
  font-weight: 700;
  cursor: pointer;
  animation: heroScrollGlow 1.6s ease-in-out infinite;
}
.hero-scroll-hint:hover {
  color: #fff;
  background: linear-gradient(135deg, rgba(255, 77, 141, 0.42), rgba(255, 153, 51, 0.24));
}
@keyframes heroScrollGlow {
  0%, 100% {
    opacity: 1;
    box-shadow: 0 0 16px rgba(255, 77, 141, 0.5);
    transform: translateY(0);
  }
  50% {
    opacity: 0.8;
    box-shadow: 0 0 32px rgba(255, 45, 149, 0.75), 0 0 56px rgba(255, 153, 51, 0.35);
    transform: translateY(5px);
  }
}
.landing-hero-text h1 {
  animation: heroFadeUp 0.8s ease-out both;
}
.landing-lead { animation: heroFadeUp 0.8s ease-out 0.12s both; }
@keyframes heroFadeUp {
  from { opacity: 0; transform: translateY(14px); }
  to { opacity: 1; transform: translateY(0); }
}
.showcase-match-flash {
  position: absolute;
  top: 0.65rem;
  right: 0.65rem;
  z-index: 3;
  padding: 0.25rem 0.55rem;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 800;
  color: #fff;
  background: linear-gradient(135deg, #ff4d8d, #b84dff);
  box-shadow: 0 4px 16px rgba(255, 77, 141, 0.5);
  animation: matchFlashPop 2s ease-in-out infinite;
}
@keyframes matchFlashPop {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.06); }
}
.auth-hook-banner {
  text-align: center;
  margin: 0 0 1rem;
  padding: 0;
  font-size: 0.95rem;
  font-weight: 600;
  line-height: 1.45;
  color: var(--text);
}
.landing-flow {
  position: relative;
  isolation: isolate;
  padding-bottom: clamp(2.5rem, 6vw, 4rem);
  background:
    radial-gradient(ellipse 120% 65% at 12% -5%, rgba(255, 77, 141, 0.16), transparent 58%),
    radial-gradient(ellipse 85% 50% at 88% 18%, rgba(184, 77, 255, 0.11), transparent 55%),
    linear-gradient(180deg, transparent 0%, rgba(255, 77, 141, 0.05) 52%, transparent 100%);
}
.landing-flow::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 8rem;
  background: linear-gradient(180deg, transparent, var(--bg));
  pointer-events: none;
  z-index: 1;
}
.landing-signup {
  width: min(1100px, calc(100% - 2 * clamp(1rem, 4vw, 2rem)));
  max-width: 1100px;
  margin: -2.75rem auto 0;
  padding: 0;
  scroll-margin-top: 5rem;
  position: relative;
  z-index: 3;
  box-sizing: border-box;
}
.landing-signup.auth-section,
#start.landing-signup {
  margin-left: auto;
  margin-right: auto;
}
.landing-signup-card {
  position: relative;
  width: 100%;
  max-width: 100%;
  padding: clamp(1rem, 2.8vw, 1.35rem) clamp(0.9rem, 2.8vw, 1.15rem) clamp(1.15rem, 2.8vw, 1.45rem);
  border-radius: 28px;
  background: linear-gradient(165deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.03));
  border: 1px solid rgba(255, 255, 255, 0.11);
  box-shadow:
    0 28px 64px rgba(0, 0, 0, 0.38),
    0 0 0 1px rgba(255, 77, 141, 0.1) inset;
  backdrop-filter: blur(22px);
}
.landing-signup-card::before {
  content: '';
  position: absolute;
  top: 0;
  left: 12%;
  right: 12%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 120, 180, 0.45), transparent);
}
.landing-signup-lead {
  text-align: center;
  margin: 0 0 1rem;
  font-size: clamp(0.88rem, 2.3vw, 0.98rem);
  font-weight: 600;
  line-height: 1.45;
  color: var(--text);
}
.landing-signup-card .card {
  padding: 0.95rem 1rem;
  margin-bottom: 0.65rem;
}
.landing-signup-card label { font-size: 0.82rem; margin-bottom: 0.28rem; }
.landing-signup-card input,
.landing-signup-card select {
  padding: 0.52rem 0.65rem;
  margin-bottom: 0.55rem;
  font-size: 0.9rem;
}
.landing-signup-card .register-hint {
  margin: 0 0 0.75rem;
  padding: 0.55rem 0.65rem;
  font-size: 0.82rem;
}
.landing-signup-card .btn-primary { padding: 0.7rem 0.9rem; font-size: 0.92rem; }
.landing-signup-card .auth-social .btn { padding: 0.62rem 0.75rem; font-size: 0.88rem; }
.landing-signup-card .auth-primary-actions .btn-cta-mega {
  padding: 0.85rem 1rem;
  font-size: 0.92rem;
}
.landing-signup-card .btn-auth-login { padding: 0.7rem 0.85rem; font-size: 0.9rem; }
.landing-signup-card .reg-photo-upload { margin-top: 0.25rem; }
.landing-signup-card .reg-photo-preview { max-width: 5.5rem; }
.landing-signup-card label:has(input[type="checkbox"]) { font-size: 0.78rem; line-height: 1.4; }
.landing-content {
  position: relative;
  z-index: 2;
}
.landing-content > .trust-strip,
.landing-content > .real-promise,
.landing-content > .landing-section,
.landing-content > .landing-wm-promo,
.landing-content > .app-promo,
.landing-content > .vidpuls-promo-section,
.landing-content > .cta-band {
  max-width: 1100px;
  margin-left: auto;
  margin-right: auto;
}
.landing-proof-strip {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.45rem 0;
  padding: 0 0 1.15rem;
  margin: 0 0 1.1rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.07);
  text-align: center;
  font-size: 0.84rem;
  color: var(--muted);
}
.landing-proof-strip > span {
  display: inline-flex;
  align-items: center;
  padding: 0 0.85rem;
}
.landing-proof-strip > span + span {
  border-left: 1px solid rgba(255, 255, 255, 0.12);
}
.landing-proof-strip strong { color: var(--text); }
.landing-wm-promo {
  position: relative;
  margin: 2.5rem clamp(1rem, 4vw, 2rem) 0;
  border-radius: 20px;
  overflow: hidden;
  background:
    linear-gradient(165deg, rgba(22, 101, 52, 0.95) 0%, rgba(15, 60, 32, 0.98) 45%, rgba(10, 35, 22, 1) 100%);
  border: 1px solid rgba(255, 206, 0, 0.35);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.35) inset,
    0 24px 60px rgba(0, 0, 0, 0.45),
    0 0 48px rgba(255, 206, 0, 0.12);
}
.landing-wm-promo::before {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(
    90deg,
    transparent 0,
    transparent 18px,
    rgba(255, 255, 255, 0.03) 18px,
    rgba(255, 255, 255, 0.03) 36px
  );
  pointer-events: none;
}
.landing-wm-jersey-strip {
  height: 6px;
  background: linear-gradient(90deg, #000 0%, #000 33.33%, #dd0000 33.33%, #dd0000 66.66%, #ffce00 66.66%, #ffce00 100%);
}
.landing-wm-inner {
  position: relative;
  z-index: 1;
  padding: clamp(1.35rem, 4vw, 2rem) clamp(1.1rem, 4vw, 2rem) clamp(1.5rem, 4vw, 2.1rem);
}
.landing-wm-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 0.75rem 1rem;
  margin-bottom: 1.25rem;
}
.landing-wm-head-text { flex: 1; min-width: min(100%, 16rem); }
.landing-wm-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.75rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #111;
  background: linear-gradient(135deg, #ffce00, #ffe566);
  box-shadow: 0 4px 16px rgba(255, 206, 0, 0.45);
  animation: landing-wm-badge-pulse 2.4s ease-in-out infinite;
}
@keyframes landing-wm-badge-pulse {
  0%, 100% { transform: scale(1); box-shadow: 0 4px 16px rgba(255, 206, 0, 0.45); }
  50% { transform: scale(1.03); box-shadow: 0 6px 22px rgba(255, 206, 0, 0.65); }
}
.landing-wm-ball {
  font-size: clamp(2.5rem, 8vw, 3.5rem);
  line-height: 1;
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.35));
  animation: wm-ball-bounce 2.2s ease-in-out infinite;
}
.landing-wm-promo h2 {
  margin: 0.65rem 0 0.35rem;
  font-family: 'Playfair Display', Georgia, serif;
  font-size: clamp(1.55rem, 4.5vw, 2.15rem);
  line-height: 1.15;
  color: #fff;
}
.landing-wm-promo .landing-wm-sub {
  margin: 0;
  font-size: clamp(0.95rem, 2.2vw, 1.05rem);
  color: rgba(255, 255, 255, 0.82);
  max-width: 36rem;
  line-height: 1.55;
}
.landing-wm-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
  margin: 1.35rem 0 1.5rem;
}
@media (min-width: 640px) {
  .landing-wm-grid { grid-template-columns: repeat(3, 1fr); }
}
.landing-wm-card {
  padding: 1rem 0.95rem;
  border-radius: 14px;
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid rgba(255, 255, 255, 0.12);
  backdrop-filter: blur(6px);
}
.landing-wm-card-icon { font-size: 1.65rem; display: block; margin-bottom: 0.35rem; }
.landing-wm-card h3 {
  margin: 0 0 0.35rem;
  font-size: 0.95rem;
  color: #ffce00;
}
.landing-wm-card p {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.45;
  color: rgba(255, 255, 255, 0.78);
}
.landing-wm-cta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.55rem;
}
.btn-wm-landing {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.45rem;
  width: 100%;
  max-width: 22rem;
  padding: 0.95rem 1.35rem;
  border: none;
  border-radius: 14px;
  font-size: 1rem;
  font-weight: 800;
  color: #111 !important;
  cursor: pointer;
  background: linear-gradient(135deg, #ffce00 0%, #ffe566 50%, #ffce00 100%) !important;
  box-shadow: 0 8px 28px rgba(255, 206, 0, 0.35);
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn-wm-landing:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 32px rgba(255, 206, 0, 0.5);
}
.landing-wm-disclaimer {
  margin: 0;
  font-size: 0.74rem;
  color: rgba(255, 255, 255, 0.62);
}
.landing-wm-schedule {
  margin-top: 1.25rem;
  padding: 1rem 1.1rem;
  border-radius: 16px;
  background: rgba(0, 0, 0, 0.28);
  border: 1px solid rgba(255, 255, 255, 0.12);
}
.landing-wm-schedule-title {
  margin: 0 0 0.75rem;
  font-size: 0.95rem;
  font-weight: 700;
}
.landing-wm-schedule-list {
  display: grid;
  gap: 0.55rem;
}
@media (min-width: 640px) {
  .landing-wm-schedule-list { grid-template-columns: repeat(2, 1fr); }
}
.landing-wm-fixture {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 0.35rem 0.5rem;
  align-items: center;
  padding: 0.55rem 0.65rem;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.06);
  font-size: 0.82rem;
}
.landing-wm-fixture-team {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-width: 0;
}
.landing-wm-fixture-team.away { justify-content: flex-end; text-align: right; }
.landing-wm-fixture-meta {
  grid-column: 1 / -1;
  font-size: 0.72rem;
  color: var(--muted);
  text-align: center;
}
.landing-wm-fixture--today {
  border-color: rgba(255, 215, 0, 0.55);
  background: linear-gradient(135deg, rgba(255, 215, 0, 0.12), rgba(255, 77, 141, 0.08));
  box-shadow: 0 0 0 1px rgba(255, 215, 0, 0.2);
}
.landing-wm-fixture-badge {
  display: inline-block;
  margin-right: 0.35rem;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  background: rgba(255, 215, 0, 0.22);
  color: #ffd700;
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}
.landing-bayern {
  text-align: center;
}
.landing-bayern-cities {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
  margin: 1rem 0 0.5rem;
}
.landing-bayern-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.75rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.12);
  font-size: 0.82rem;
  color: var(--text);
}
.landing-bayern-note {
  font-size: 0.85rem;
  color: var(--muted);
  margin: 0.75rem auto 0;
  max-width: 36rem;
}
.landing-hook-chip--wm {
  border-color: rgba(255, 206, 0, 0.45);
  background: rgba(22, 101, 52, 0.35);
  color: #ffe566;
}
@media (prefers-reduced-motion: reduce) {
  .landing-wm-badge,
  .landing-wm-ball { animation: none; }
}
.landing-content-divider {
  height: 1px;
  max-width: min(680px, 72vw);
  margin: 0 auto;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
}
@media (min-width: 900px) {
  .landing-hero { grid-template-columns: 1.05fr 0.95fr; min-height: 92vh; }
}
.landing-hero-text h1 {
  font-family: 'Playfair Display', Georgia, serif;
  font-size: clamp(2.4rem, 6vw, 3.75rem);
  font-weight: 700;
  line-height: 1.08;
  margin: 0 0 1rem;
  letter-spacing: -0.02em;
}
.landing-hero-text h1 em {
  font-style: italic;
  background: linear-gradient(120deg, var(--accent), var(--warm));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  animation: titleAccentGlow 2s ease-in-out infinite;
}
@keyframes titleAccentGlow {
  0%, 100% { filter: drop-shadow(0 0 6px rgba(255, 77, 141, 0.45)); opacity: 1; }
  50% { filter: drop-shadow(0 0 18px rgba(255, 77, 141, 0.9)) drop-shadow(0 0 28px rgba(255, 153, 51, 0.45)); opacity: 0.88; }
}
.landing-lead {
  font-size: clamp(1.05rem, 2.5vw, 1.25rem);
  color: var(--muted);
  line-height: 1.65;
  margin: 0 0 1.75rem;
  max-width: 32rem;
}
.landing-cta-row {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.85rem;
  margin-bottom: 1.25rem;
  max-width: 26rem;
}
.landing-cta-block {
  text-align: center;
  margin: 2.25rem auto 0;
  padding: 0 clamp(1rem, 4vw, 2rem);
  max-width: 28rem;
}
.landing-cta-block--left { text-align: left; margin-left: 0; margin-right: 0; }
.landing-cta-block--left .btn-cta-mega { margin-left: 0; }
.btn-cta-mega {
  display: inline-block;
  width: 100%;
  max-width: 24rem;
  padding: 1.2rem 2.5rem;
  font-size: clamp(1.08rem, 2.8vw, 1.32rem);
  font-weight: 800;
  letter-spacing: 0.01em;
  border: none;
  border-radius: 16px;
  color: #fff !important;
  background: linear-gradient(135deg, #ff2d95 0%, #ff4d8d 45%, #e91e8c 100%) !important;
  box-shadow:
    0 0 28px rgba(255, 45, 149, 0.55),
    0 14px 40px rgba(255, 77, 141, 0.4),
    0 2px 0 rgba(255, 255, 255, 0.15) inset;
  animation: landing-cta-pulse 2.6s ease-in-out infinite;
  cursor: pointer;
  transition: transform 0.28s ease, box-shadow 0.28s ease, filter 0.28s ease;
}
.btn-cta-mega:hover {
  filter: brightness(1.1);
  transform: translateY(-4px) scale(1.02);
  box-shadow:
    0 0 42px rgba(255, 45, 149, 0.75),
    0 20px 52px rgba(255, 77, 141, 0.55),
    0 2px 0 rgba(255, 255, 255, 0.18) inset;
}
.btn-cta-mega:active {
  transform: translateY(-1px);
  filter: brightness(0.98);
}
.landing-cta-note {
  margin: 0.75rem 0 0;
  font-size: 0.9rem;
  color: var(--muted);
  line-height: 1.45;
}
.landing-cta-note strong { color: var(--mint); }
.landing-login-link {
  font-size: 0.92rem;
  color: var(--muted);
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
}
.landing-login-link:hover { color: var(--text); }
.btn-header-cta.btn-cta-mega {
  width: auto;
  max-width: none;
  padding: 0.65rem 1.35rem;
  font-size: 0.92rem;
  animation: landing-cta-pulse 2.4s ease-in-out infinite;
}
@keyframes landing-cta-pulse {
  0%, 100% {
    box-shadow:
      0 0 24px rgba(255, 45, 149, 0.5),
      0 14px 40px rgba(255, 77, 141, 0.38),
      0 0 0 0 rgba(255, 77, 141, 0.2);
    transform: scale(1);
  }
  50% {
    box-shadow:
      0 0 36px rgba(255, 45, 149, 0.65),
      0 18px 50px rgba(255, 77, 141, 0.5),
      0 0 0 10px rgba(255, 77, 141, 0);
    transform: scale(1.015);
  }
}
@media (prefers-reduced-motion: reduce) {
  .btn-cta-mega { animation: none; }
}
.btn-outline {
  background: transparent;
  color: var(--text);
  border: 2px solid rgba(255,255,255,0.25);
}
.landing-stats {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  margin-top: 0.5rem;
}
.landing-stats strong {
  display: block;
  font-size: 1.35rem;
  color: var(--text);
}
.landing-stats span { font-size: 0.82rem; color: var(--muted); }
.landing-pay-teaser {
  margin: 0.85rem 0 0;
  font-size: 0.88rem;
  color: var(--muted);
  line-height: 1.45;
}
.landing-pay-section .landing-pay-banner {
  max-width: 40rem;
  margin-left: auto;
  margin-right: auto;
  text-align: center;
}
.landing-pay-methods {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  justify-content: center;
  margin: 0.85rem 0 0.65rem;
}
.landing-pay-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  padding: 0.45rem 0.8rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid rgba(255, 255, 255, 0.14);
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text);
}
.landing-pay-note {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.45;
  opacity: 0.92;
}
.landing-pay-facts {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.25rem 2rem;
  margin: 1.5rem auto 1.75rem;
  max-width: 36rem;
}
.landing-pay-fact {
  text-align: center;
  min-width: 6.5rem;
}
.landing-pay-fact strong {
  display: block;
  font-size: 1.2rem;
  color: var(--mint);
}
.landing-pay-fact span {
  font-size: 0.82rem;
  color: var(--muted);
}
.landing-pay-packs-title {
  text-align: center;
  margin: 0 0 0.25rem;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text);
}
.landing-pack-grid {
  max-width: 56rem;
  margin-left: auto;
  margin-right: auto;
}
.landing-pack-grid .pack-actions {
  margin-top: 0.85rem;
}
  text-align: center;
  margin: 1rem auto 0;
  max-width: 32rem;
}
.profile-showcase {
  position: relative;
  height: min(520px, 70vh);
  min-height: 360px;
}
.showcase-card {
  position: absolute;
  width: min(260px, 78vw);
  padding: 1rem;
  border-radius: 24px;
  background: var(--glass);
  border: 1px solid var(--border);
  backdrop-filter: blur(16px);
  box-shadow: var(--shadow);
  animation: floatCard 6s ease-in-out infinite;
}
.showcase-card:nth-child(1) {
  top: 8%;
  right: 5%;
  transform: rotate(6deg);
  animation-delay: 0s;
}
.showcase-card:nth-child(2) {
  top: 32%;
  left: 0;
  transform: rotate(-8deg);
  animation-delay: 1.2s;
  z-index: 2;
}
.showcase-card:nth-child(3) {
  bottom: 6%;
  right: 12%;
  transform: rotate(4deg);
  animation-delay: 2.4s;
  z-index: 1;
}
@keyframes floatCard {
  0%, 100% { translate: 0 0; }
  50% { translate: 0 -12px; }
}
.showcase-photo {
  height: 200px;
  border-radius: 18px;
  background: linear-gradient(145deg, rgba(255,77,141,0.5), rgba(184,77,255,0.35));
  overflow: hidden;
  margin-bottom: 0.75rem;
}
.showcase-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.showcase-card h4 { margin: 0 0 0.25rem; font-size: 1.15rem; }
.showcase-card .origin { margin-top: 0.35rem; }
.showcase-badge {
  position: absolute;
  top: -8px;
  left: 50%;
  transform: translateX(-50%);
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  padding: 0.4rem 1rem;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 700;
  white-space: nowrap;
  box-shadow: 0 8px 24px rgba(255,77,141,0.5);
}
.trust-strip {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.65rem 1.35rem;
  padding: clamp(2rem, 5vw, 2.75rem) clamp(1.25rem, 5vw, 3rem) clamp(1.5rem, 4vw, 2rem);
  background: transparent;
  border: none;
}
.trust-item {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  color: var(--muted);
  font-size: 0.92rem;
}
.trust-item span:first-child { font-size: 1.4rem; }
.app-promo {
  background: linear-gradient(135deg, rgba(233, 30, 99, 0.08), rgba(106, 27, 154, 0.06));
  border: none;
  padding-top: clamp(2.5rem, 6vw, 3.5rem);
  padding-bottom: clamp(2.5rem, 6vw, 3.5rem);
  border-radius: 28px;
  margin: 0.5rem auto;
  width: min(1100px, calc(100% - 2 * clamp(1rem, 4vw, 2rem)));
  max-width: 1100px;
  box-sizing: border-box;
}
.app-promo-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 2rem;
  max-width: 900px;
  margin: 0 auto;
}
.app-promo-copy { flex: 1 1 280px; }
.app-promo-kicker { font-weight: 700; color: var(--accent); margin: 0 0 0.5rem; }
.app-promo h2 { text-align: left; margin-bottom: 0.75rem; }
.app-promo .sub { text-align: left; margin: 0 0 1rem; }
.app-promo-list { margin: 0 0 1.25rem; padding-left: 0; list-style: none; color: var(--muted); }
.app-promo-list li { margin: 0.35rem 0; }
.app-promo-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; }
.app-dl-btn { width: auto; }
.app-promo-note { font-size: 0.82rem; color: var(--muted); margin: 1rem 0 0; }
.app-promo-badge {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  padding: 1.25rem;
  border-radius: 24px;
  background: rgba(255,255,255,0.06);
  border: 1px solid var(--border);
}
.app-promo-badge img { border-radius: 22%; box-shadow: 0 12px 32px rgba(0,0,0,0.35); }
.app-promo-badge span { font-weight: 700; font-size: 1.1rem; }
.landing-section {
  max-width: 1100px;
  margin: 0 auto;
  padding: clamp(2.75rem, 7vw, 4rem) clamp(1.25rem, 5vw, 3rem);
  position: relative;
}
.landing-section + .landing-section::before,
.landing-content .real-promise + .landing-section::before,
.landing-content .app-promo + .landing-section::before {
  content: '';
  position: absolute;
  top: 0;
  left: 8%;
  right: 8%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.09), transparent);
}
.landing-pay-section {
  background: linear-gradient(180deg, transparent, rgba(255, 77, 141, 0.04), transparent);
  border-radius: 32px;
  margin-top: 0.5rem;
  margin-bottom: 0.5rem;
}
.landing-section h2 {
  font-family: 'Playfair Display', serif;
  font-size: clamp(1.75rem, 4vw, 2.35rem);
  text-align: center;
  margin: 0 0 0.5rem;
}
.landing-section > p.sub {
  text-align: center;
  color: var(--muted);
  margin: 0 auto 2.5rem;
  max-width: 36rem;
}
.features-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1.25rem;
}
@media (min-width: 700px) { .features-grid { grid-template-columns: repeat(3, 1fr); } }
.feature-card {
  padding: 1.75rem 1.35rem;
  border-radius: var(--radius);
  background: linear-gradient(160deg, rgba(255,255,255,0.08), rgba(255,255,255,0.02));
  border: 1px solid var(--border);
  text-align: center;
  transition: transform 0.25s, border-color 0.25s;
}
.feature-card:hover {
  transform: translateY(-4px);
  border-color: rgba(255,107,157,0.4);
}
.feature-icon {
  font-size: 2.5rem;
  margin-bottom: 0.75rem;
  display: block;
}
.feature-card h3 { margin: 0 0 0.5rem; font-size: 1.1rem; }
.feature-card p { margin: 0; color: var(--muted); font-size: 0.9rem; line-height: 1.5; }
.steps-row {
  display: grid;
  gap: 1.5rem;
  counter-reset: step;
}
@media (min-width: 768px) { .steps-row { grid-template-columns: repeat(3, 1fr); } }
.step-card {
  position: relative;
  padding: 1.5rem 1.25rem 1.25rem 3.5rem;
  border-radius: var(--radius);
  background: var(--card);
  border: 1px solid var(--border);
}
.step-card::before {
  counter-increment: step;
  content: counter(step);
  position: absolute;
  left: 1.1rem;
  top: 1.2rem;
  width: 2rem;
  height: 2rem;
  line-height: 2rem;
  text-align: center;
  border-radius: 50%;
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  font-weight: 700;
  font-size: 0.95rem;
}
.cta-band .landing-cta-block { margin-top: 1.25rem; }
.cta-band {
  text-align: center;
  padding: clamp(2.5rem, 6vw, 3.5rem) clamp(1.5rem, 4vw, 2rem);
  margin: 0.75rem clamp(1rem, 4vw, 2rem) 1.5rem;
  border-radius: 28px;
  background: linear-gradient(135deg, rgba(255,77,141,0.22), rgba(184,77,255,0.16));
  border: 1px solid rgba(255,107,157,0.28);
}
.cta-band h2 { margin-bottom: 0.75rem; }
.cta-band p { color: var(--muted); margin-bottom: 1.5rem; }
.cta-band .btn { width: auto; min-width: 14rem; margin: 0 auto; }
.auth-section {
  margin: 0;
  padding: 0;
  scroll-margin-top: 0;
}
.auth-primary-actions {
  display: flex;
  flex-direction: column;
  gap: 0.65rem;
  margin-bottom: 1.15rem;
}
.auth-primary-actions .btn-cta-mega {
  width: 100%;
  margin: 0;
  animation: landing-cta-pulse 2.6s ease-in-out infinite;
}
.btn-auth-login {
  width: 100%;
  padding: 0.85rem 1rem;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.06);
  color: var(--text);
  font-weight: 600;
  font-size: 1rem;
  cursor: pointer;
  transition: border-color 0.2s, background 0.2s;
}
.btn-auth-login:hover,
.btn-auth-login.active {
  border-color: rgba(255, 77, 141, 0.55);
  background: rgba(255, 77, 141, 0.12);
}
.auth-section h2 {
  text-align: center;
  font-family: 'Playfair Display', serif;
  margin-bottom: 1.25rem;
}
.auth-tabs {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 1rem;
}
.auth-tabs button {
  flex: 1;
  padding: 0.75rem;
  border-radius: 12px;
  border: 1px solid var(--border);
  background: var(--card);
  color: var(--text);
  font-weight: 600;
  cursor: pointer;
}
.auth-tabs button.active {
  background: linear-gradient(135deg, var(--accent), var(--accent2));
  border-color: transparent;
}
.auth-divider {
  text-align: center;
  margin: 0.75rem 0 0.5rem;
  color: var(--muted);
  font-size: 0.85rem;
}
.auth-social {
  margin-bottom: 1rem;
}
.auth-social .btn {
  width: 100%;
  margin-top: 0.45rem;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-weight: 600;
}
.btn-oauth-google {
  background: #fff;
  color: #1f1f1f;
  border: 1px solid #dadce0;
}
.btn-oauth-facebook {
  background: #1877f2;
  color: #fff;
  border: none;
}
.auth-oauth-google {
  width: 100%;
  min-height: 44px;
  display: flex;
  justify-content: center;
  align-items: center;
}
.auth-oauth-google > div {
  width: 100%;
  display: flex;
  justify-content: center;
}
.auth-oauth-google iframe {
  max-width: 100% !important;
}
.oauth-legal {
  margin-top: 0.65rem;
  text-align: center;
  line-height: 1.35;
}
.landing-footer {
  text-align: center;
  padding: 2rem 1.25rem 2.25rem;
  color: var(--muted);
  font-size: 0.88rem;
  border-top: 1px solid var(--border);
}
.landing-footer-legal {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 0.5rem 1.5rem;
  margin-bottom: 1rem;
}
.landing-footer-legal a {
  color: rgba(255, 249, 252, 0.78);
  font-size: 0.92rem;
  font-weight: 500;
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color 0.2s ease;
}
.landing-footer-legal a:hover { color: var(--mint); }
.landing-footer a { color: var(--muted); margin: 0 0.5rem; }
.landing-footer-meta { margin: 0.75rem 0 0; font-size: 0.82rem; opacity: 0.85; }
.share-footer-btn {
  background: none;
  border: none;
  color: var(--mint);
  font: inherit;
  font-size: 0.88rem;
  cursor: pointer;
  text-decoration: underline;
  margin: 0 0.5rem;
  padding: 0;
}
.share-footer-btn:hover { color: var(--text); }
.faq-section .faq-list {
  max-width: 40rem;
  margin: 1.5rem auto 0;
  text-align: left;
}
.faq-item {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 14px;
  margin-bottom: 0.65rem;
  padding: 0.15rem 1rem;
}
.faq-item summary {
  cursor: pointer;
  font-weight: 600;
  padding: 0.85rem 0;
  list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item p {
  margin: 0 0 1rem;
  color: var(--muted);
  font-size: 0.92rem;
  line-height: 1.55;
}
.sticky-cta {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 80;
  padding: 0.65rem 1rem calc(0.65rem + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, transparent, rgba(10, 6, 16, 0.92) 35%);
  pointer-events: none;
  transition: opacity 0.3s ease, transform 0.3s ease;
}
.sticky-cta:not(.hidden) { pointer-events: auto; }
.sticky-cta.hidden {
  opacity: 0;
  transform: translateY(110%);
}
.sticky-cta-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  max-width: 520px;
  margin: 0 auto;
  padding: 0.65rem 1rem;
  border-radius: 16px;
  background: rgba(22, 12, 32, 0.96);
  border: 1px solid rgba(255, 77, 141, 0.35);
  box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.45);
}
.sticky-cta-text { font-size: 0.88rem; color: var(--muted); }
.sticky-cta-text strong { color: #fde68a; }
.pwa-install-bar {
  position: fixed;
  left: 0.75rem;
  right: 0.75rem;
  bottom: 0.75rem;
  z-index: 340;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.65rem 0.75rem;
  border-radius: 14px;
  background: var(--glass);
  border: 1px solid var(--border);
  box-shadow: var(--shadow);
  backdrop-filter: blur(12px);
}
.pwa-install-bar span { flex: 1; font-size: 0.88rem; color: var(--text); }
.pwa-install-bar .btn { padding: 0.45rem 0.75rem; font-size: 0.82rem; width: auto; }
.pwa-install-bar .btn-ghost { min-width: 2rem; padding: 0.35rem; }
@media (min-width: 720px) { .pwa-install-bar { display: none; } }
.sticky-cta-btn {
  flex-shrink: 0;
  padding: 0.55rem 1rem !important;
  font-size: 0.88rem !important;
  border-radius: 999px !important;
}
@media (min-width: 900px) {
  .sticky-cta { display: none !important; }
}
body.mode-app .landing-header { display: none !important; }
body.mode-app #authScreen.landing-screen { display: none !important; }
body.mode-app .app-header { display: flex !important; }
body.mode-landing .app-header { display: none !important; }
body.mode-landing .coin-pill { display: none !important; }
body.mode-landing #authScreen.landing-screen.active { display: block; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
button, .btn, a.btn { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }

@media (max-width: 768px) {
  body { overflow-x: hidden; }
  .page-fx { opacity: 1; }
  input, textarea, select {
    font-size: 16px;
    min-height: 44px;
  }
  .btn, .nav-tabs button, .swipe-btn {
    min-height: 44px;
  }
  .app-header {
    padding-top: max(0.65rem, env(safe-area-inset-top, 0px));
    padding-left: max(1rem, env(safe-area-inset-left, 0px));
    padding-right: max(1rem, env(safe-area-inset-right, 0px));
  }
  #appShell:not(.hidden) {
    display: flex;
    flex-direction: column;
    min-height: 100dvh;
    min-height: 100vh;
    padding-bottom: calc(4.75rem + env(safe-area-inset-bottom, 0px));
  }
  #appShell .nav-tabs {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 90;
    margin: 0;
    padding: 0.35rem 0.45rem calc(0.4rem + env(safe-area-inset-bottom, 0px));
    background: rgba(12, 8, 18, 0.97);
    border-top: 1px solid rgba(255, 255, 255, 0.1);
    backdrop-filter: blur(16px);
    -webkit-backdrop-filter: blur(16px);
    box-shadow: 0 -10px 32px rgba(0, 0, 0, 0.42);
    gap: 0.3rem;
  }
  #appShell .nav-tabs button {
    flex: 1;
    min-width: 0;
    padding: 0.45rem 0.2rem;
    font-size: 0.68rem;
    line-height: 1.15;
    border-radius: 10px;
  }
  #appShell .screen {
    max-width: none;
    width: 100%;
    padding: 0.65rem 0.85rem 0.5rem;
    padding-left: max(0.85rem, env(safe-area-inset-left, 0px));
    padding-right: max(0.85rem, env(safe-area-inset-right, 0px));
  }
  #tab-wm.wm-screen {
    padding-left: max(0.45rem, env(safe-area-inset-left, 0px));
    padding-right: max(0.45rem, env(safe-area-inset-right, 0px));
  }
  .wm-fan-lounge {
    min-height: min(82vh, 860px);
    margin-bottom: 1rem;
  }
  .wm-fan-chat-box {
    min-height: 300px;
    height: min(56vh, 460px);
    max-height: 460px;
  }
  .wm-fan-head-text h2 { font-size: 1.12rem; }
  .wm-fan-compose input { font-size: 16px; }
  .real-banner {
    flex-direction: row;
    align-items: flex-start;
    gap: 0.65rem;
    padding: 0.75rem 0.85rem;
    font-size: 0.82rem;
  }
  .filter-bar {
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem;
  }
  .filter-bar input, .filter-bar select {
    font-size: 16px;
    min-height: 44px;
  }
  #tab-discover .real-banner {
    padding: 0.55rem 0.7rem;
    font-size: 0.76rem;
    margin-bottom: 0.45rem;
  }
  #tab-discover .real-banner strong { display: block; font-size: 0.82rem; margin-bottom: 0.1rem; }
  #tab-discover #geoStatus { margin: 0 0 0.35rem; font-size: 0.75rem; }
  #tab-discover .filter-bar { margin-bottom: 0.5rem; gap: 0.4rem; }
  #tab-discover > .meta[style*="text-align:center"] { display: none; }
  .swipe-stack {
    height: min(calc(100dvh - 19.5rem), calc(100vh - 19.5rem), 360px);
    max-width: min(280px, 84vw);
    width: min(280px, 84vw);
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 0.45rem;
  }
  .swipe-stack .jami-loader { min-height: 220px; }
  .swipe-card { border-radius: 16px; }
  .swipe-card--compact .swipe-card-body {
    padding: 0.5rem 0.7rem 0.65rem;
  }
  .swipe-card--compact .swipe-card-body h3 { font-size: 1.05rem; margin-bottom: 0.2rem; }
  .swipe-card--compact .swipe-card-body .btn-chat-on-card {
    padding: 0.55rem 0.75rem;
    font-size: 0.82rem;
  }
  .swipe-card-overlay { padding: 2rem 0.85rem 0.7rem; }
  .swipe-card-name { font-size: 1.3rem; }
  .swipe-card-meta { font-size: 0.82rem; }
  .swipe-hint {
    font-size: 0.72rem;
    line-height: 1.35;
    padding: 0 0.35rem;
    margin: -0.25rem 0 0.55rem;
  }
  .swipe-actions {
    max-width: min(280px, 84vw);
    gap: 0.85rem;
    padding: 0.3rem 0 0.55rem;
  }
  .swipe-btn {
    width: 54px;
    height: 54px;
    font-size: 1.35rem;
  }
  .swipe-btn.like { width: 60px; height: 60px; font-size: 1.55rem; }
  .chat-cta-bar { margin-top: 0.35rem; }
  .chat-cta-bar .btn { padding: 0.85rem 1rem; }
  .landing-hero {
    min-height: auto;
    padding-top: 5.5rem;
    padding-bottom: 0.5rem;
    gap: 1.5rem;
    padding-left: max(1rem, env(safe-area-inset-left, 0px));
    padding-right: max(1rem, env(safe-area-inset-right, 0px));
  }
  .landing-signup {
    margin-top: -1.25rem;
    margin-left: auto;
    margin-right: auto;
    width: min(1100px, calc(100% - 2 * max(clamp(1rem, 4vw, 2rem), env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px))));
    padding-left: 0;
    padding-right: 0;
  }
  .landing-signup-card { border-radius: 22px; }
  .landing-proof-strip > span {
    padding: 0.2rem 0.55rem;
    border-left: none !important;
    width: 100%;
    justify-content: center;
  }
  .landing-hero-text h1 {
    font-size: clamp(1.85rem, 7vw, 2.5rem);
    word-break: break-word;
  }
  .landing-lead { font-size: 1rem; margin-bottom: 1.25rem; }
  .btn-cta-mega {
    max-width: none;
    width: 100%;
    padding: 1rem 1.25rem;
    font-size: 1.05rem;
  }
  .landing-cta-row { max-width: none; width: 100%; }
  .profile-showcase {
    height: min(320px, 48vh);
    min-height: 240px;
    margin: 0 auto;
    max-width: 100%;
  }
  .showcase-card {
    width: min(190px, 62vw);
    padding: 0.7rem;
  }
  .showcase-photo { height: 150px; }
  .landing-header {
    padding-top: max(0.75rem, env(safe-area-inset-top, 0px));
    padding-left: max(1rem, env(safe-area-inset-left, 0px));
    padding-right: max(1rem, env(safe-area-inset-right, 0px));
  }
  .landing-section {
    padding: 2.75rem 1rem;
  }
  .landing-section h2 { font-size: clamp(1.45rem, 5.5vw, 1.85rem); }
  .compare-card { padding: 1.1rem 1rem; }
  .trust-strip {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 1.5rem 1rem;
  }
  .app-promo-inner {
    flex-direction: column;
    text-align: center;
    gap: 1.25rem;
  }
  .app-promo h2, .app-promo .sub { text-align: center; }
  .app-promo-actions {
    flex-direction: column;
    width: 100%;
  }
  .app-promo-actions .btn { width: 100%; }
  .app-promo-badge { margin: 0 auto; }
  .sticky-cta {
    padding-bottom: max(0.65rem, env(safe-area-inset-bottom, 0px));
    padding-left: max(0.75rem, env(safe-area-inset-left, 0px));
    padding-right: max(0.75rem, env(safe-area-inset-right, 0px));
  }
  .sticky-cta-inner { padding: 0.55rem 0.75rem; }
  .sticky-cta-btn { padding: 0.5rem 0.85rem !important; font-size: 0.82rem !important; }
  .pwa-install-bar {
    left: max(0.75rem, env(safe-area-inset-left, 0px));
    right: max(0.75rem, env(safe-area-inset-right, 0px));
    bottom: calc(4.75rem + env(safe-area-inset-bottom, 0px));
  }
  body.mode-landing .pwa-install-bar {
    bottom: calc(4.5rem + env(safe-area-inset-bottom, 0px));
  }
  .match-modal-inner, .profile-photo-modal-inner {
    width: calc(100% - 1.5rem);
    max-height: calc(100dvh - 2rem);
    overflow-y: auto;
  }
  .pack-grid { grid-template-columns: 1fr; max-width: none; }
  .profile-card { grid-template-columns: 64px 1fr; gap: 0.75rem; }
  .avatar { width: 64px; height: 64px; }
  #tab-profile .profile-edit-card {
    padding: 0.85rem 0.9rem;
    margin-bottom: 0.65rem;
  }
  #tab-profile .photo-upload-row { gap: 0.45rem; }
  #tab-profile .btn-photo-pick {
    width: 100%;
    min-height: 48px;
    font-size: 0.95rem !important;
  }
  #tab-profile .photo-field-label {
    font-size: 0.85rem;
    font-weight: 600;
  }
  #tab-profile .photo-slots-summary { font-size: 0.82rem; }
  .geo-map-card {
    margin-bottom: 0.55rem;
    padding: 0.55rem 0.65rem;
  }
  .geo-map-head strong { font-size: 0.84rem; }
  .geo-map-note { font-size: 0.68rem; line-height: 1.35; }
  .auth-social { margin-top: 0.75rem; margin-bottom: 0.85rem; }
  .auth-social .btn,
  .auth-oauth-google {
    min-height: 48px;
    font-size: 0.92rem;
  }
  .landing-signup-card .reg-photo-pick-label .reg-photo-preview {
    max-width: 100%;
    width: min(160px, 42vw);
    margin: 0 auto;
  }
  .landing-signup-card .reg-photo-pick-label .btn-photo-pick {
    min-height: 48px;
  }
  #appShell #tab-profile.screen {
    padding-bottom: calc(0.75rem + env(safe-area-inset-bottom, 0px));
  }
}
@media (max-width: 420px) {
  .landing-header .logo-text { max-width: 7.5rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .btn-header-cta.btn-cta-mega {
    padding: 0.55rem 0.85rem;
    font-size: 0.8rem;
  }
  .landing-stats { gap: 1rem; }
  .filter-bar { grid-template-columns: 1fr; }
  #appShell .nav-tabs button { font-size: 0.62rem; }
  .swipe-stack {
    height: min(calc(100dvh - 18.5rem), calc(100vh - 18.5rem), 340px);
    max-width: min(260px, 82vw);
    width: min(260px, 82vw);
  }
  .swipe-actions { max-width: min(260px, 82vw); }
  .swipe-btn { width: 50px; height: 50px; font-size: 1.25rem; }
  .swipe-btn.like { width: 56px; height: 56px; font-size: 1.45rem; }
  .geo-map { height: min(42vw, 170px); min-height: 140px; }
  #tab-profile .profile-edit-card { padding: 0.75rem 0.8rem; }
  .chat-profile-sheet-body { padding-bottom: calc(1.25rem + env(safe-area-inset-bottom, 0px)); }
}

"""


def page_fx_html(*, heart_count: int = 88) -> str:
    """Schwebende Herzen links/rechts am Rand + Funkeln."""
    emojis = ("💕", "💗", "❤️", "💖", "💘", "💝", "🩷", "💓")
    lines = ['  <div class="page-fx" id="pageFx" aria-hidden="true">']
    per_side = heart_count // 2
    for i in range(heart_count):
        left_side = i < per_side
        lane_i = i if left_side else i - per_side
        if left_side:
            x = 0.5 + (lane_i * 5.7 + 1) % 14
            drift = 6 + (lane_i * 4) % 22
        else:
            x = 86 + (lane_i * 5.5 + 1) % 13
            drift = -6 - (lane_i * 4) % 22
        size = 0.48 + (lane_i % 9) * 0.052
        opacity = 0.32 + (lane_i % 7) * 0.042
        dur = 11 + (lane_i % 14)
        delay = -round((i * 1.85) % 26, 1)
        emoji = emojis[i % len(emojis)]
        lines.append(
            f'    <span class="fx-heart" style="--x:{x:.1f}%;--drift:{drift}px;'
            f'--s:{size:.2f}rem;--o:{opacity:.2f};--d:{dur}s;--delay:{delay}s">{emoji}</span>'
        )
    sparkles = (
        (3, 8, 4, 14, 0),
        (8, 14, 3, 16, -1),
        (14, 18, 5, 15, -2),
        (6, 10, 4, 17, -4),
        (11, 16, 3, 18, -6),
        (88, -8, 4, 14, -1),
        (92, -14, 5, 16, -3),
        (86, -10, 3, 15, -5),
        (94, -18, 4, 17, -7),
        (96, -12, 3, 19, -9),
    )
    for x, drift, sz, dur, delay in sparkles:
        lines.append(
            f'    <span class="fx-sparkle" style="--x:{x}%;--drift:{drift}px;'
            f'--sz:{sz}px;--d:{dur}s;--delay:{delay}s"></span>'
        )
    glitters = (
        (5, 12, 0.52, 13, -2),
        (12, 16, 0.48, 15, -5),
        (2, 10, 0.44, 16, -8),
        (88, -12, 0.52, 13, -3),
        (94, -16, 0.48, 15, -6),
        (97, -10, 0.44, 17, -10),
    )
    for x, drift, size, dur, delay in glitters:
        lines.append(
            f'    <span class="fx-glitter" style="--x:{x}%;--drift:{drift}px;'
            f'--s:{size}rem;--d:{dur}s;--delay:{delay}s">✨</span>'
        )
    lines.append("  </div>")
    return "\n".join(lines)


INDEX_HTML = """<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-visual"/>
  __JAMI_SEO_HEAD__
  <style id="jami-critical">
body{margin:0;background:#0a0610;color:#fff9fc;font-family:system-ui,-apple-system,sans-serif}
.screen{display:none}
.landing-header{display:flex;align-items:center;justify-content:space-between;padding:1rem}
body.mode-landing #authScreen.landing-screen.active,#authScreen.landing-screen.active{display:block!important}
body.mode-app #authScreen.landing-screen{display:none!important}
#appShell.hidden{display:none!important}
body.mode-app .landing-header{display:none!important}
body.mode-landing .app-header{display:none!important}
.jami-inapp-bar{display:none;position:fixed;top:0;left:0;right:0;z-index:500;align-items:center;justify-content:space-between;gap:0.5rem;padding:0.55rem 0.75rem;padding-top:max(0.55rem,env(safe-area-inset-top,0px));background:#1a1028;border-bottom:1px solid rgba(255,77,141,0.35);font-size:0.82rem;color:#fff9fc}
.jami-inapp-bar a{color:#fff;background:linear-gradient(135deg,#ff4d8d,#b84dff);padding:0.45rem 0.75rem;border-radius:999px;text-decoration:none;font-weight:600;white-space:nowrap}
html.jami-lite .jami-inapp-bar{display:flex}
html.jami-lite body{padding-top:2.75rem}
#start,.landing-signup{max-width:1100px!important;width:min(1100px,calc(100% - 2 * clamp(1rem,4vw,2rem)))!important;margin-left:auto!important;margin-right:auto!important;box-sizing:border-box}
#start .card,#start .landing-signup-card{width:100%;max-width:100%;box-sizing:border-box}
  </style>
  <link rel="stylesheet" href="css/app.css?v=__JAMI_BUILD__"/>
</head>
<body class="mode-landing">
<script>
(function(){var ua=navigator.userAgent||'';var lite=/GSA/|GoogleApp|Instagram|FBAN|FBAV|Line/i.test(ua);if(lite){window.__jamiLite=true;document.documentElement.className+=' jami-lite';var rm=document.querySelectorAll('script[src*="googletagmanager"],link[href*="fonts.googleapis"]');for(var i=0;i<rm.length;i++){if(rm[i].parentNode)rm[i].parentNode.removeChild(rm[i]);}if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then(function(rs){for(var j=0;j<rs.length;j++)rs[j].unregister();});}}window.jamiScrollRegister=function(){var t=document.getElementById('start');if(t)t.scrollIntoView({behavior:'smooth',block:'start'});};window.jamiScrollLogin=function(){window.jamiScrollRegister();if(window.showAuthForm)showAuthForm('login');};function initLandingHooks(){var el=document.getElementById('landingOnlineN');if(!el)return;var base=38+(new Date().getHours()%14)*3;el.textContent=String(base+Math.floor(Math.random()*18));}if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',initLandingHooks);else initLandingHooks();})();
</script>
  <div id="jamiInappBar" class="jami-inapp-bar">
    <span>Google-App lädt langsam?</span>
    <a href="intent://www.jami-dating.de/#Intent;scheme=https;package=com.android.chrome;S.browser_fallback_url=https%3A%2F%2Fwww.jami-dating.de%2F;end">In Chrome öffnen</a>
  </div>
__PAGE_FX__
  <header class="landing-header">
    __JAMI_LOGO__
    <nav class="landing-nav">
      <a href="#features">So funktioniert's</a>
      <a href="#beta-test">🪙 Beta-Test</a>
      <a href="#bayern">📍 Bayern</a>
      <a href="#wm-promo">⚽ WM 2026</a>
      <a href="#preise">Preise & Zahlung</a>
      <a href="#app">App</a>
      <a href="#faq">FAQ</a>
      <a href="#sicherheit">Sicherheit</a>
      <a href="#start" id="navLogin">Anmelden</a>
    </nav>
  </header>

  <header class="app-header">
    __JAMI_LOGO__
    <div class="coin-pill" id="coinDisplay">🪙 <span id="coinCount">0</span></div>
  </header>

  <div id="authScreen" class="screen landing-screen active">
    <div class="landing-flow">
    <section class="landing-hero">
      <div class="landing-hero-glow" aria-hidden="true"></div>
      <div class="landing-hero-text">
        <p class="landing-urgency">🔥 Jetzt starten – 100 Gratis-Coins für neue Profile</p>
        <p class="landing-online-nudge"><span class="landing-online-dot" aria-hidden="true"></span> <strong id="landingOnlineN">–</strong> Singles gerade aktiv</p>
        <p class="showcase-badge" style="position:static;transform:none;display:inline-block;margin-bottom:1rem;background:rgba(255,255,255,0.1);box-shadow:none">💕 Jami-Dating – Die Dating-Seite zum Verlieben</p>
        <h1>Echte Singles. Echte Dates.<br/>Garantiert ohne <em>Fake-Moderatoren</em>.</h1>
        <p class="landing-lead">❤️ Profil in 30 Sekunden – swipe, matche, chatte. <strong>100 Gratis-Coins</strong> geschenkt, kein Abo.</p>
        <div class="landing-hook-row">
          <span class="landing-hook-chip landing-hook-chip--hot">🪙 100 Coins gratis</span>
          <span class="landing-hook-chip landing-hook-chip--wm">⚽ WM 2026 live</span>
          <span class="landing-hook-chip">✓ Kostenlos</span>
          <span class="landing-hook-chip">✓ Kein Abo</span>
          <span class="landing-hook-chip">✓ Nur echte Nutzer</span>
        </div>
        <div class="landing-live-ticker" aria-hidden="true">
          <div class="landing-live-ticker-track">
            <span>⚽ WM 2026 – echte Spiele live tippen</span>
            <span>📯 Tor! · 127 Fans in der WM-Lounge</span>
            <span>🪙 Coin-Tipp gewonnen · +250 Coins</span>
            <span>💕 Neues Match · Lisa &amp; Tom · München</span>
            <span>💬 „Hey, Kaffee?“ · 3 km entfernt</span>
            <span>⭐ Super-Like erhalten · Augsburg</span>
            <span>🔥 5 Singles in deiner Nähe online</span>
            <span>⚽ WM 2026 – echte Spiele live tippen</span>
            <span>📯 Tor! · 127 Fans in der WM-Lounge</span>
            <span>🪙 Coin-Tipp gewonnen · +250 Coins</span>
            <span>💕 Neues Match · Lisa &amp; Tom · München</span>
            <span>💬 „Hey, Kaffee?“ · 3 km entfernt</span>
            <span>⭐ Super-Like erhalten · Augsburg</span>
            <span>🔥 5 Singles in deiner Nähe online</span>
          </div>
        </div>
        <p class="landing-pay-teaser">💳 Karte · Apple Pay · PayPal · Klarna · Amazon Pay · SEPA &amp; Banküberweisung – sicher über Stripe</p>
        <div class="landing-stats">
          <div><strong>100 🪙</strong><span>Start-Bonus</span></div>
          <div><strong>✓ Echt</strong><span>keine Moderatoren</span></div>
          <div><strong>30 Sek.</strong><span>bis zum Profil</span></div>
        </div>
        <button type="button" class="hero-scroll-hint" onclick="window.jamiScrollRegister&&jamiScrollRegister()">↓ Jetzt kostenlos starten</button>
      </div>
      <div class="profile-showcase" aria-hidden="true">
        <div class="showcase-card">
          <span class="showcase-match-flash">💕 Match!</span>
          <div class="showcase-photo"><img src="images/lena.jpg" alt="Lena, 26" loading="lazy" decoding="async" width="400" height="500"/></div>
          <h4>Lena, 26</h4>
          <p class="meta"><span class="dist">3,2 km</span></p>
          <div class="origin">🌍 aus Ingolstadt</div>
        </div>
        <div class="showcase-card">
          <div class="showcase-photo"><img src="images/marco.jpg" alt="Marco, 29" loading="lazy" width="400" height="500"/></div>
          <h4>Marco, 29</h4>
          <p class="meta"><span class="dist">8 km</span></p>
          <div class="origin">🌍 aus Augsburg</div>
        </div>
        <div class="showcase-card">
          <div class="showcase-photo"><img src="images/amira.jpg" alt="Amira, 25" loading="lazy" width="400" height="500"/></div>
          <h4>Amira, 25</h4>
          <p class="meta"><span class="dist">12 km</span></p>
          <div class="origin">🌍 aus München</div>
        </div>
      </div>
    </section>

    <section class="beta-test-signup" id="beta-test">
      <div class="beta-test-signup-inner">
        <div class="beta-test-head">
          <p class="beta-test-reward">🪙 1000 Coins</p>
          <h2>Play-Store Beta – Test-Link anfordern</h2>
        </div>
        <p class="beta-test-lead">Gmail eintragen → Link sofort &amp; per E-Mail. Nach Installation schreiben wir dir <strong>1000 Coins</strong> gut.</p>
        <form id="betaTestForm" class="beta-test-form">
          <label>
            <span>Gmail für Play Store *</span>
            <input type="email" id="betaTestEmail" name="email" required autocomplete="email" placeholder="deine@gmail.com" inputmode="email"/>
          </label>
          <label>
            <span>Name (optional)</span>
            <input type="text" id="betaTestName" name="name" maxlength="80" autocomplete="name" placeholder="Vorname"/>
          </label>
          <button type="submit" class="btn btn-primary" id="betaTestSubmit">Link senden</button>
        </form>
        <div id="betaTestSuccess" class="beta-test-success hidden" aria-live="polite">
          <p><strong>Danke!</strong> Der Play-Store-Link ist unten – zusätzlich kommt er per E-Mail.</p>
          <div class="beta-test-link-row">
            <a id="betaTestLinkOut" href="#" target="_blank" rel="noopener">Link öffnen</a>
            <button type="button" class="btn btn-ghost btn-sm" id="betaTestCopyBtn">Link kopieren</button>
          </div>
          <ol class="beta-test-steps">
            <li>Link am Handy öffnen (dieselbe Gmail im Play Store)</li>
            <li><strong>Tester werden</strong> → App installieren</li>
            <li>Auf Jami mit derselben E-Mail registrieren → <strong>1000 Coins</strong> von uns</li>
          </ol>
        </div>
      </div>
    </section>

    <section class="landing-signup auth-section" id="start">
      <div class="landing-signup-card">
        <div class="landing-proof-strip">
          <span>⭐ <strong>„Endlich echte Leute“</strong> – kein Moderator-Chat</span>
          <span>💬 Match-Start gratis</span>
          <span>📍 Entfernung &amp; Herkunft</span>
        </div>
        <p class="landing-signup-lead">⚡ In 30 Sekunden dabei – entdecke Singles in deiner Nähe</p>
      <div class="auth-primary-actions">
        <button type="button" class="btn btn-cta-mega active" id="btnShowRegister">Jetzt kostenlos registrieren – 100 Coins</button>
        <button type="button" class="btn btn-auth-login" id="btnShowLogin">Anmelden</button>
      </div>
      <div class="auth-social" id="authSocial">
        <p class="auth-divider">oder</p>
        <div class="auth-oauth-google" id="btnOAuthGoogleWrap">
          <button type="button" class="btn btn-oauth-google" id="btnOAuthGoogle">Mit Google anmelden</button>
        </div>
        <button type="button" class="btn btn-oauth-facebook" id="btnOAuthFacebook">Mit Facebook anmelden</button>
        <p class="meta oauth-legal">Mit Google/Facebook stimmst du den <a href="/agb">AGB</a> und der <a href="/datenschutz">Datenschutzerklärung</a> zu (18+, echte Person).</p>
      </div>
    <div class="card hidden" id="loginCard">
      <label>E-Mail</label>
      <input type="email" id="loginEmail" placeholder="deine@email.de"/>
      <label>Passwort</label>
      <input type="password" id="loginPass" placeholder="••••••••"/>
      <button class="btn btn-primary" id="btnLogin">Einloggen</button>
    </div>
    <div class="card" id="registerCard">
      <p class="register-hint"><strong>Schneller Start:</strong> Name, E-Mail, Passwort – optional ein Profilbild. Dann loslegen mit <strong>100 Gratis-Coins</strong>.</p>
      <label>Anzeigename *</label>
      <input id="regName" placeholder="z. B. Alex" autocomplete="name"/>
      <label>E-Mail *</label>
      <input type="email" id="regEmail" autocomplete="email"/>
      <label>Passwort *</label>
      <input type="password" id="regPass" autocomplete="new-password"/>
      <label>Alter *</label>
      <input type="number" id="regAge" min="18" max="99" value="25"/>
      <label>Ich bin *</label>
      <select id="regGender">
        <option>Frau</option><option>Mann</option><option>Divers</option>
      </select>
      <label>Profilbild (optional)</label>
      <div class="reg-photo-upload">
        <label class="reg-photo-pick-label" for="regPhotoFile">
          <div class="reg-photo-preview" id="regPhotoPreview"><span class="reg-photo-placeholder">📷 Foto aus Galerie wählen</span></div>
          <span class="btn btn-ghost btn-photo-pick">📷 Galerie oder Kamera</span>
        </label>
        <input type="file" id="regPhotoFile" class="photo-file-input photo-file-input-hidden" accept="image/jpeg,image/png,image/webp,image/heic,image/heif,image/*"/>
        <p class="meta">Tippe auf die Vorschau oder den Button – dann öffnet sich deine Galerie.</p>
      </div>
      <label><input type="checkbox" id="regReal" required/> Ich bin eine echte Person (18+) – kein Bot, kein Moderator-Account</label>
      <label><input type="checkbox" id="regAgree" required/> Ich akzeptiere: Jami-Dating nutzt <strong>keine Chat-Moderatoren</strong> – nur Nutzer-zu-Nutzer (<a href="/agb">AGB</a>, <a href="/datenschutz">Datenschutzerklärung</a>)</label>
      <p class="meta">Nur 18+. Fake-Profile werden gesperrt.</p>
      <button type="button" class="btn btn-primary" id="btnRegister">Konto erstellen</button>
    </div>
      </div>
    </section>
    </div>

    <div class="landing-with-sidebar">
    <div class="landing-content">

    <section class="landing-section landing-bayern" id="bayern" aria-labelledby="landingBayernTitle">
      <h2 id="landingBayernTitle">Singles in Bayern &amp; Umgebung</h2>
      <p class="sub">Jami startet stark in Süddeutschland – entdecke echte Profile in deiner Nähe, nicht nur in Großstädten.</p>
      <div class="landing-bayern-cities" aria-label="Städte">
        <span class="landing-bayern-chip">📍 München</span>
        <span class="landing-bayern-chip">📍 Nürnberg</span>
        <span class="landing-bayern-chip">📍 Augsburg</span>
        <span class="landing-bayern-chip">📍 Regensburg</span>
        <span class="landing-bayern-chip">📍 Ingolstadt</span>
        <span class="landing-bayern-chip">📍 Würzburg</span>
        <span class="landing-bayern-chip">📍 Passau</span>
        <span class="landing-bayern-chip">📍 Rosenheim</span>
        <span class="landing-bayern-chip">📍 Landshut</span>
        <span class="landing-bayern-chip">📍 Bamberg</span>
      </div>
      <p class="landing-bayern-note">Radius-Suche bis 50&nbsp;km oder „Überall“ – du entscheidest, wie weit du suchst. Nach der Anmeldung siehst du sofort Vorschläge in deiner Region.</p>
      <div class="landing-cta-block landing-cta-block--left" style="margin-top:1.25rem">
        <button type="button" class="btn btn-primary btn-cta-mega" onclick="jamiScrollRegister()">Jetzt kostenlos starten</button>
      </div>
    </section>

    <section class="landing-wm-promo" id="wm-promo" aria-labelledby="landingWmTitle">
      <div class="landing-wm-jersey-strip" aria-hidden="true"></div>
      <div class="landing-wm-inner">
        <div class="landing-wm-head">
          <div class="landing-wm-head-text">
            <span class="landing-wm-badge">⚽ WM 2026 · Exklusiv bei Jami</span>
            <h2 id="landingWmTitle">Dating &amp; Fußball-Fieber – alles in einer App</h2>
            <p class="landing-wm-sub" id="landingWmSub">WM 2026 live – echte Spiele, echte Termine. Tippe mit Coins in der App.</p>
          </div>
          <span class="landing-wm-ball" aria-hidden="true">⚽</span>
        </div>
        <div class="landing-wm-grid">
          <article class="landing-wm-card">
            <span class="landing-wm-card-icon">🪙</span>
            <h3>WM Coin-Tipps</h3>
            <p>Setze virtuelle Coins auf 1 · X · 2. Richtig getippt? Dein Coin-Guthaben wächst – perfekt für Chats &amp; Super-Likes.</p>
          </article>
          <article class="landing-wm-card">
            <span class="landing-wm-card-icon">📣</span>
            <h3>Fan-Lounge Live</h3>
            <p>Öffentlicher WM-Chat im Deutschland-Trikot-Look: Torrufe, Vuvuzela, Emoji-Reaktionen – WM-Stimmung wie im Stadion.</p>
          </article>
          <article class="landing-wm-card">
            <span class="landing-wm-card-icon">🥅</span>
            <h3>Tor-Jingle &amp; Vibes</h3>
            <p>Bei „Tor!“: Sound, Vibration und Feier-Moment. Mit Singles daten und gleichzeitig die WM erleben.</p>
          </article>
        </div>
        <div class="landing-wm-schedule" id="landingWmSchedule">
          <h3 class="landing-wm-schedule-title">📅 Nächste Spiele – jetzt tippen</h3>
          <div class="landing-wm-schedule-list" id="landingWmScheduleList">
            <p class="meta" style="margin:0">Spielplan wird geladen…</p>
          </div>
        </div>
        <div class="landing-wm-cta">
          <button type="button" class="btn-wm-landing" id="btnLandingWmCta">⚽ Kostenlos registrieren – WM mitmachen</button>
          <p class="landing-wm-disclaimer">Nur virtuelle Coins · kein Echtgeld-Wetten · ab 18 Jahren · Spielspaß in der App</p>
        </div>
      </div>
    </section>

    __APP_SECTION__

    __VIDPULS_PROMO_SECTION__

    <section class="real-promise">
      <h2>Bei uns chattest du mit echten Menschen</h2>
      <p class="sub" style="max-width:36rem;margin:0.75rem auto 0">Andere Seiten nutzen oft Moderatoren oder Fake-Profile. Jami-Dating nicht.</p>
    </section>

    <section class="landing-section">
      <h2>Alles, was du von Dating-Apps kennst</h2>
      <p class="sub">Swipe, Match, Chat, Filter – plus unser Herkunfts-Mehrwert.</p>
      <div class="compare-grid">
        <article class="compare-card good compare-box-jami">
          <h3>✓ Bei Jami-Dating</h3>
          <ul>
            <li>Swipe & Like / Pass wie Tinder</li>
            <li>Match-Moment bei gegenseitigem Like</li>
            <li>„Likes“-Übersicht & Chats</li>
            <li>Filter: Alter, Entfernung, Suche</li>
            <li>Herkunft (Stadt/Land) auf dem Profil</li>
            <li><strong>Nur echte Nutzer – keine Moderatoren</strong></li>
            <li>Blockieren & Melden</li>
          </ul>
        </article>
        <article class="compare-card bad compare-box-other">
          <h3>✕ Andere Seiten (oft)</h3>
          <ul>
            <li>Chat-Moderatoren stellen Profile nach</li>
            <li>Künstliche „Matches“ ohne echte Person</li>
            <li>Abos ohne transparente Nachrichtenpreise</li>
            <li>Keine Herkunft / keine echte Nähe</li>
          </ul>
        </article>
      </div>
    </section>

    <section class="landing-section" id="features">
      <h2>Warum Jami-Dating anders ist</h2>
      <p class="sub">Wie bei den großen Apps – aber mit echtem Mehrwert: Nähe, Herkunft und faire Preise.</p>
      <div class="features-grid">
        <article class="feature-card">
          <span class="feature-icon">💕</span>
          <h3>Verlieben & treffen</h3>
          <p>Profile entdecken, chatten, echte Verbindungen – nicht nur endlos swipen.</p>
        </article>
        <article class="feature-card">
          <span class="feature-icon">📍</span>
          <h3>In der Nähe</h3>
          <p>Entfernung in km – triff Menschen, die wirklich um die Ecke sind.</p>
        </article>
        <article class="feature-card">
          <span class="feature-icon">🌍</span>
          <h3>Herkunft zeigen</h3>
          <p>Stadt & Land auf einen Blick – Story, Kultur, gemeinsame Wurzeln.</p>
        </article>
        <article class="feature-card">
          <span class="feature-icon">✓</span>
          <h3>Echte Personen</h3>
          <p>Keine Chat-Moderatoren. Du schreibst nur mit registrierten Menschen wie dir.</p>
        </article>
      </div>
    </section>

    <section class="landing-section">
      <h2>In 3 Schritten zum ersten Date</h2>
      <p class="sub">Einfach wie Tinder & Co. – in Minuten startklar.</p>
      <div class="steps-row">
        <div class="step-card"><h3>Schneller Start</h3><p class="meta">Melde dich blitzschnell an. Vervollständige dein Profil, wann du willst, und hol dir <strong>+10 Bonus-Coins</strong>.</p></div>
        <div class="step-card"><h3>Leute entdecken</h3><p class="meta">Sortiert nach Nähe. Chat: 10 Coins/Nachricht.</p></div>
        <div class="step-card"><h3>Chatten & verlieben</h3><p class="meta">10 Coins/Nachricht · animierte Emojis.</p></div>
      </div>
    </section>

    __JAMI_PAYMENT__

    __JAMI_FAQ__
    </div>
    __VIDPULS_LANDING_SIDEBAR__
    </div>

    <footer class="landing-footer">
      <nav class="landing-footer-legal" aria-label="Rechtliches">
        <a href="/impressum">Impressum</a>
        <a href="/datenschutz">Datenschutzerklärung</a>
      </nav>
      <a href="/agb">AGB</a>
      <button type="button" class="cookie-footer-link" data-open-cookies>Cookie-Einstellungen</button>
      <button type="button" class="share-footer-btn" id="btnShareJami">Freunden empfehlen</button>
      <p class="landing-footer-meta">© Jami-Dating · David Prechtel</p>
    </footer>
  </div>

  <div id="matchModal" class="match-modal hidden" role="dialog" aria-labelledby="matchTitle">
    <div class="match-modal-inner">
      <h2 id="matchTitle">Ihr habt ein Match! 💕</h2>
      <p class="match-modal-lead" id="matchSubtitle">Die ersten 2 Nachrichten in diesem Chat sind kostenlos!</p>
      <span class="match-modal-bonus" id="matchBonusBadge">🎁 2 Gratis-Nachrichten</span>
      <p class="meta" style="margin-top:0.75rem">Nur echte Personen bei Jami – jetzt direkt schreiben.</p>
      <div class="match-avatars" id="matchAvatars"></div>
      <button type="button" class="btn btn-primary" id="btnMatchProfile">👤 Profil ansehen</button>
      <button type="button" class="btn btn-primary btn-chat-pulse" id="btnMatchChat" style="margin-top:0.5rem">💬 Jetzt chatten</button>
      <button type="button" class="btn btn-ghost" id="btnMatchClose" style="margin-top:0.5rem">Weiter swipen</button>
    </div>
  </div>

  <div id="appShell" class="hidden">
    __VIDPULS_APP_SIDEBAR__
    <nav class="nav-tabs">
      <button type="button" data-tab="discover" class="active">Entdecken</button>
      <button type="button" data-tab="likes">Likes <span class="nav-badge hidden" id="likesBadge">0</span></button>
      <button type="button" data-tab="matches" class="nav-chat-pulse">💬 Chat <span class="nav-badge hidden" id="chatBadge">0</span></button>
      <button type="button" data-tab="shop">Coins</button>
      <button type="button" data-tab="wm">⚽ WM</button>
      <button type="button" data-tab="profile">Profil</button>
    </nav>

    <section id="tab-discover" class="screen active">
      <div class="real-banner">
        <span>✓</span>
        <div><strong>Nur echte Personen</strong>Keine Chat-Moderatoren, keine Fake-Profile. Du chattest nur mit registrierten Nutzerinnen und Nutzern.</div>
      </div>
      <p class="meta" id="geoStatus">Standort wird geladen…</p>
      <div class="geo-map-card hidden" id="geoMapCard">
        <div class="geo-map-head">
          <strong>📍 Dein Live-Standort</strong>
          <span class="meta" id="geoMapHint">Standortfreigabe wird angefragt…</span>
        </div>
        <div id="geoMap" class="geo-map" aria-label="Karte mit deinem Live-Standort"></div>
        <div id="geoNearbyStrip" class="geo-nearby-strip hidden" aria-label="Singles in der Nähe">
          <div class="geo-nearby-strip-head">
            <strong>👥 In der Nähe</strong>
            <span class="meta" id="geoNearbyCount"></span>
          </div>
          <div id="geoNearbyList" class="geo-nearby-list"></div>
        </div>
        <p class="meta geo-map-note">Kreis = Umkreis (1–50 km). „Überall“ zeigt alle Profile – Entfernung trotzdem sichtbar.</p>
      </div>
      <div class="filter-bar">
        <div class="filter-km-wrap">
          <div class="filter-km-head">
            <label for="filterKm">Umkreis</label>
            <strong id="filterKmVal">25 km</strong>
          </div>
          <input type="range" id="filterKm" min="1" max="50" value="25" step="1" aria-valuemin="1" aria-valuemax="50" aria-label="Suchradius in Kilometern"/>
          <label class="filter-everywhere-toggle">
            <input type="checkbox" id="filterEverywhere" aria-label="Überall, kein Kilometer-Limit"/>
            <span>🌍 Überall (kein km-Limit)</span>
          </label>
        </div>
        <div><label>Alter von–bis</label><div style="display:flex;gap:0.35rem"><input type="number" id="filterAgeMin" min="18" max="99" value="18" style="margin:0"/><input type="number" id="filterAgeMax" min="18" max="99" value="45" style="margin:0"/></div></div>
      </div>
      <div id="swipeStack" class="swipe-stack"></div>
      <p class="swipe-hint" id="swipeHint">← Nach links wischen für das nächste Profil · → Like</p>
      <div class="swipe-actions" id="swipeActions">
        <button type="button" class="swipe-btn nope" id="btnNope" title="Pass">✕</button>
        <button type="button" class="swipe-btn super" id="btnSuper" title="Super-Like (Coins)">⭐</button>
        <button type="button" class="swipe-btn like" id="btnLike" title="Like">♥</button>
      </div>
      <div class="chat-cta-bar">
        <p class="meta"><strong>💬 Chatten</strong> – Match &amp; Nachrichten (10 Coins/Nachricht)</p>
        <button type="button" class="btn btn-primary btn-chat-pulse" id="btnOpenChats">💬 Jetzt chatten</button>
      </div>
      <p class="meta" style="text-align:center">Wischen: ← Pass · → Like · ⭐ Super-Like</p>
    </section>

    <section id="tab-likes" class="screen">
      <div class="card">
        <h2>Likes &amp; Matches</h2>
        <p class="meta">Gegenseitiges Like = Match · Profil antippen zum Anschauen · Like zurück für Match</p>
        <div id="matchesList" class="match-list-section"></div>
        <div id="likesList"></div>
      </div>
    </section>

    <section id="tab-matches" class="screen">
      <div id="matchList" class="match-chat-list"></div>
      <div id="chatPanel" class="card hidden">
        <h3 id="chatTitle">Chat</h3>
        <div class="chat-box" id="chatMessages"></div>
        <p class="meta" id="chatCostHint"></p>
        <div class="chat-panel-compose">
          <button type="button" class="btn-photo-toggle" id="btnChatPhoto" aria-label="Foto senden">📷</button>
          <div class="chat-compose-emoji">
            <button type="button" class="btn-emoji-toggle" id="btnChatEmoji" aria-expanded="false" aria-label="Emoji einfügen">😊</button>
            <div id="chatEmojiPicker" class="emoji-picker-popover hidden" role="dialog" aria-label="Emojis"></div>
          </div>
          <input type="file" id="chatPhotoInput" class="chat-photo-input" accept="image/*"/>
          <input id="chatInput" placeholder="Nachricht…" maxlength="2000" autocomplete="off"/>
          <button type="button" class="btn btn-primary" id="btnSend">Senden</button>
        </div>
      </div>
    </section>

    <section id="tab-shop" class="screen">
      <div class="card">
        <div id="adRewardCard" class="ad-reward-card hidden">
          <h3>📺 Gratis-Coins mit YouTube</h3>
          <p class="meta" id="adRewardHint">YouTube: 1 Coin pro Video · 15 Sek. · max. 10 pro Tag</p>
          <button type="button" class="btn btn-primary" id="btnWatchAd">YouTube ansehen (+1 Coin)</button>
        </div>
        <h2>🪙 Coins kaufen</h2>
        <div class="pay-methods-banner" id="payMethodsBanner">
          <strong>Zahlung:</strong> Karte, Apple Pay, PayPal, Klarna, Amazon Pay, SEPA &amp; Banküberweisung.
        </div>
        <p class="meta" id="shopHint">Zahlungsart wählen, dann „Jetzt freischalten“ – alle Optionen auch im Stripe-Checkout.</p>
        <div class="pay-method-picker" id="payMethodPicker">
          <span class="pay-method-picker-label">Zahlungsart wählen</span>
          <div class="pay-method-picker-grid">
            <button type="button" class="is-active" data-pay="all">✨ Alle</button>
            <button type="button" data-pay="card">💳 Karte</button>
            <button type="button" data-pay="paypal">🅿️ PayPal</button>
            <button type="button" data-pay="klarna">🛍️ Klarna</button>
            <button type="button" data-pay="link">🍎 Apple/Google Pay</button>
            <button type="button" data-pay="amazon_pay">📦 Amazon Pay</button>
            <button type="button" data-pay="sepa_debit">🏦 SEPA</button>
            <button type="button" data-pay="customer_balance">💶 Überweisung</button>
          </div>
        </div>
        <div id="shopWelcomeBanner" class="shop-welcome-banner hidden" role="status" aria-live="polite">
          <strong>⏳ Limitiertes Willkommens-Angebot!</strong>
          <p>Schlage jetzt zu und erhalte <strong>40 Bonus-Coins extra</strong> auf jedes Paket!</p>
          <span class="shop-welcome-timer">Noch <span id="shopWelcomeCountdown">15:00</span></span>
        </div>
        <div class="pack-grid" id="packGrid"></div>
      </div>
      <div class="card emoji-shop-card">
        <div class="emoji-shop-hero">
          <h3>✨ Animierter Emoji-Shop</h3>
          <p>Einmal kaufen – für immer im Chat nutzen (neben 😊)</p>
        </div>
        <div class="emoji-grid" id="emojiGridShop"></div>
      </div>
      <div class="card">
        <h3>Nachrichten-Preise</h3>
        <table class="tier-table" id="tierTable"></table>
      </div>
    </section>

    <section id="tab-wm" class="screen wm-screen">
      <div class="wm-fan-lounge">
        <div class="wm-fan-jersey-strip" aria-hidden="true"></div>
        <div class="wm-fan-header">
          <span class="wm-fan-ball" aria-hidden="true">⚽</span>
          <div class="wm-fan-head-text">
            <h2>🇩🇪 Fan-Lounge · WM 2026</h2>
            <p id="wmFanNextMatch">WM 2026 – Spiele werden geladen…</p>
          </div>
          <span class="wm-fan-vuvuzela" aria-hidden="true">📯</span>
        </div>
        <div class="wm-fan-stadium-deco" aria-hidden="true">
          <span>📣</span><span>⚽</span><span>🏟️</span><span>📯</span><span>🇩🇪</span>
        </div>
        <div id="wmFanChatBox" class="wm-fan-chat-box" role="log" aria-live="polite"></div>
        <div class="wm-fan-emoji-bar" id="wmFanEmojiBar" aria-label="Schnell-Emojis">
          <button type="button" data-wm-insert="⚽" title="Ball">⚽</button>
          <button type="button" data-wm-insert="🔥" title="Feuer">🔥</button>
          <button type="button" data-wm-insert="📯" title="Vuvuzela">📯</button>
          <button type="button" data-wm-insert="🇩🇪" title="Deutschland">🇩🇪</button>
          <button type="button" data-wm-insert="Tor! " title="Tor">🥅 Tor!</button>
        </div>
        <div class="wm-fan-compose">
          <input type="text" id="wmFanChatInput" maxlength="400" placeholder="Auf geht's, Deutschland! ⚽🇩🇪" autocomplete="off" aria-label="Fan-Chat Nachricht"/>
          <button type="button" class="wm-fan-send" id="wmFanChatSend">📯 Senden</button>
        </div>
      </div>
      <div class="wm-section-divider"><span>🪙 Coin-Tipps</span></div>
      <div class="wm-hero">
        <h2>⚽ WM 2026 Tipp-Spiel</h2>
        <p class="meta wm-today-banner" id="wmTodayBanner" hidden>🇩🇪 WM-Spiele – jetzt mit Coins tippen!</p>
        <p class="meta" style="margin:0">Tippe 1 · X · 2 und setze nur <strong>virtuelle Coins</strong> ein.</p>
        <p class="wm-balance">Dein Guthaben: 🪙 <span id="wmCoinBalance">0</span> Coins</p>
        <p class="wm-disclaimer">Kein Echtgeld · reiner Spielspaß · Gewinne in Coins nach Spielende</p>
      </div>
      <div id="wmMatchList" class="wm-match-list"></div>
      <div class="wm-my-bets card" id="wmMyBetsWrap">
        <h3>📋 Meine Wetten</h3>
        <div id="wmMyBets" class="wm-bet-history"></div>
      </div>
    </section>

    <section id="tab-profile" class="screen">
      <div class="card" id="myProfileCard"></div>
      <div class="card profile-complete-card" id="profileCompleteCard"></div>
      <div class="card profile-boost-card" id="profileBoostCard"></div>
      <div class="card profile-referral-card" id="profileReferralCard"></div>
      <div class="card emoji-shop-card">
        <div class="emoji-shop-hero">
          <h3>✨ Animierter Emoji-Shop</h3>
          <p>Herz pulsiert, Feuer flackert & mehr – tippen zum Kaufen · dann im Chat unter 😊</p>
        </div>
        <div class="emoji-grid" id="emojiGrid"></div>
      </div>
      <div class="card profile-emoji-display-card" id="profileEmojiDisplayCard">
        <h3>👑 Premium-Emoji am Namen</h3>
        <p class="meta" style="margin:0 0 0.5rem">Gekaufte Icons (🔥 Flamme, 💎 Krone & mehr) erscheinen <strong>neben deinem Namen in Entdecken</strong> – andere sehen sie sofort beim Swipen.</p>
        <div class="profile-emoji-picker" id="profileEmojiPicker"></div>
      </div>
      <div class="card profile-edit-card">
        <h3>📷 Deine Fotos, deine Regeln</h3>
        <p class="meta">Start: 1 Hauptfoto + 4 öffentliche Galerie-Fotos + 5 private Plätze. Weitere Fotoplätze kannst du mit Coins freischalten (bis 10 öffentlich / 10 privat).</p>
        <p class="meta">Tippe auf <strong>📷 Galerie / Kamera</strong> – auf dem Handy öffnet sich deine Fotogalerie (iPhone/Android).</p>
        <div id="myPhotoForm" class="photo-edit-grid"></div>
      </div>
      <div class="card profile-edit-card">
        <h3>🎮 Profil ausbauen</h3>
        <p class="meta">Schritt für Schritt – Fotos, Herkunft, Bio, Hobbys & Co. Bei 100&nbsp;% Vollständigkeit: <strong>+10 Coins</strong> Bonus.</p>
        <div id="myProfileForm" class="profile-edit-grid"></div>
        <button type="button" class="btn btn-primary" id="btnSaveProfile" style="margin-top:0.75rem">Profil speichern</button>
      </div>
      <div class="card" id="friendRequestsCard">
        <h3>🤝 Freundschaftsanfragen</h3>
        <div id="friendRequestsList"></div>
        <p class="meta" id="friendRequestsEmpty">Keine offenen Anfragen</p>
      </div>
      <div class="safety-card">
        <h3>🛡️ Sicherheit & Echtheit</h3>
        <ul>
          <li><strong>Keine Chat-Moderatoren</strong> – Nachrichten nur von echten Nutzern</li>
          <li><strong>Keine Telefonnummer & keine E-Mail</strong> in Profil oder Chat sichtbar</li>
          <li><strong>Nacktfotos erlaubt</strong> nur in den 5 privaten Fotos (18+, nach Freundschaft)</li>
          <li>Profile melden & Nutzer blockieren im Entdecken-Tab</li>
          <li>Nur 18+, keine Fake-Identitäten (AGB)</li>
          <li>Jede Chat-Nachricht: 10 Coins</li>
        </ul>
      </div>
      <button class="btn btn-ghost" id="btnLogout">Abmelden</button>
      <p class="meta" style="margin-top:1.5rem">
        <a href="/impressum">Impressum</a> · <a href="/datenschutz">Datenschutzerklärung</a> · <a href="/agb">AGB</a> ·
        <button type="button" class="cookie-footer-link" data-open-cookies>Cookies</button>
      </p>
    </section>
  </div>

  <div id="pushPromptModal" class="push-prompt-modal hidden" role="dialog" aria-labelledby="pushPromptTitle" aria-modal="true">
    <div class="push-prompt-inner">
      <h3 id="pushPromptTitle">🔔 Antworten nicht verpassen</h3>
      <p>Möchtest du sofort benachrichtigt werden, wenn dir jemand antwortet?</p>
      <div class="push-prompt-actions">
        <button type="button" class="btn btn-primary" id="btnPushEnable">Ja, benachrichtigen</button>
        <button type="button" class="btn btn-ghost" id="btnPushLater">Später</button>
      </div>
    </div>
  </div>
  <div id="toast" class="toast hidden"></div>
  <div id="profilePhotoModal" class="profile-photo-modal hidden" aria-hidden="true">
    <div class="profile-photo-modal-inner">
      <button type="button" class="btn btn-ghost" id="btnClosePhotoModal" style="margin-bottom:0.5rem">← Zurück</button>
      <div id="profilePhotoModalBody"></div>
    </div>
  </div>
  <div id="chatSessionOverlay" class="chat-session-overlay hidden" aria-hidden="true">
    <div class="chat-session-topbar">
      <button type="button" class="btn btn-ghost" id="btnCloseChatSession">← Zurück</button>
      <h2 class="chat-session-title">💬 Jetzt chatten</h2>
      <div class="chat-session-topbar-actions">
        <button type="button" class="btn-view-chat-profile hidden" id="btnViewChatProfile" title="Profil ansehen">👤 Profil</button>
        <button type="button" class="btn-block-user hidden" id="btnBlockChatPartner" title="Nutzer blockieren">🚫 Blockieren</button>
      </div>
    </div>
    <div class="chat-session-layout">
      <aside class="chat-session-side chat-session-left" id="chatSessionOther" aria-label="Chat-Partner links"></aside>
      <main class="chat-session-center">
        <div class="chat-session-center-inner card">
          <button type="button" class="chat-session-profile-bar hidden" id="chatSessionProfileBar" aria-label="Profil ansehen">
            <span class="chat-session-profile-bar-avatar" id="chatSessionProfileAvatar"></span>
            <span class="chat-session-profile-bar-text">
              <strong id="chatSessionProfileName"></strong>
              <span class="meta" id="chatSessionProfileMeta"></span>
            </span>
            <span class="chat-session-profile-bar-chevron">Profil ›</span>
          </button>
          <p class="chat-session-center-head" id="chatSessionHead">💬 Chat</p>
          <div class="chat-session-status" id="chatSessionStatus"></div>
          <p class="chat-typing-indicator hidden" id="chatSessionTyping" aria-live="polite"></p>
          <p class="meta" id="chatSessionCostHint"></p>
          <div class="chat-box" id="chatSessionMessages"></div>
        </div>
      </main>
      <aside class="chat-session-side chat-session-right" id="chatSessionMe" aria-label="Dein Profil rechts"></aside>
    </div>
    <div class="chat-compose-dock" id="chatComposeDock">
      <div class="chat-session-compose">
        <button type="button" class="btn-photo-toggle" id="btnSessionPhoto" aria-label="Foto senden">📷</button>
        <input type="file" id="chatSessionPhotoInput" class="chat-photo-input" accept="image/*"/>
        <div class="chat-compose-emoji">
          <button type="button" class="btn-emoji-toggle" id="btnSessionEmoji" aria-expanded="false" aria-label="Emoji einfügen">😊</button>
          <div id="sessionEmojiPicker" class="emoji-picker-popover hidden" role="dialog" aria-label="Emojis"></div>
        </div>
        <input type="text" id="chatSessionInput" placeholder="Nachricht…" maxlength="2000" autocomplete="off" enterkeyhint="send"/>
        <div class="chat-send-wrap">
          <span class="chat-send-cost" id="chatSessionSendCost" aria-live="polite"></span>
          <button type="button" class="btn btn-primary" id="btnSessionSend">Senden</button>
        </div>
      </div>
    </div>
    <div id="chatProfileSheet" class="chat-profile-sheet hidden" aria-hidden="true">
      <div class="chat-profile-sheet-backdrop" id="chatProfileSheetBackdrop"></div>
      <div class="chat-profile-sheet-panel" role="dialog" aria-labelledby="chatProfileSheetTitle">
        <div class="chat-profile-sheet-head">
          <button type="button" class="btn btn-ghost" id="btnCloseChatProfile">← Chat</button>
          <div class="chat-profile-tabs">
            <button type="button" id="btnChatProfilePartner" class="active">Partner</button>
            <button type="button" id="btnChatProfileMe">Ich</button>
          </div>
        </div>
        <div id="chatProfileSheetBody" class="chat-profile-sheet-body"></div>
      </div>
    </div>
  </div>
  <div id="noCoinsModal" class="no-coins-modal hidden" role="dialog" aria-labelledby="noCoinsTitle" aria-modal="true">
    <div class="no-coins-modal-inner">
      <span class="no-coins-icon" aria-hidden="true">🪙</span>
      <h2 id="noCoinsTitle">Keine Coins mehr?</h2>
      <p class="no-coins-lead" id="noCoinsLead">Schau dir ein kurzes YouTube-Video an und erhalte <strong>1 Coin gratis</strong>!</p>
      <p class="meta" id="noCoinsSub" style="margin:0">Video läuft in der App – nach 15 Sek. gibt es den Coin.</p>
      <button type="button" class="btn btn-primary" id="btnNoCoinsWatchVideo">📺 YouTube ansehen (+1 Coin gratis)</button>
      <button type="button" class="btn btn-ghost" id="btnNoCoinsShop">Coin-Pakete im Shop</button>
      <button type="button" class="btn btn-ghost" id="btnNoCoinsClose">Später</button>
    </div>
  </div>
  <div id="adModal" class="ad-modal hidden" aria-hidden="true">
    <div class="ad-modal-inner">
      <h3>📺 YouTube-Video</h3>
      <p class="meta">Kurz ansehen – nach 15 Sek. gibt es 1 Coin</p>
      <div id="adSlot" class="ad-slot">Video wird geladen…</div>
      <div class="ad-progress"><div id="adProgressBar"></div></div>
      <p class="meta" id="adTimerText">15 Sekunden…</p>
      <button type="button" class="btn btn-primary" id="btnAdClaim" disabled>+1 Coin holen</button>
      <button type="button" class="btn btn-ghost" id="btnAdCancel">Abbrechen</button>
    </div>
  </div>
  <div id="cookieBanner" class="cookie-banner hidden" role="dialog" aria-labelledby="cookieTitle">
    <div class="cookie-banner-inner">
      <h3 id="cookieTitle">🍪 Cookies & Speicher</h3>
      <p>Jami nutzt notwendige Speicher für Login und Einstellungen. Optional: Google-Schriftarten und externe Dienste. Details in der <a href="/datenschutz#cookies">Datenschutzerklärung</a>.</p>
      <div class="cookie-banner-actions">
        <button type="button" class="btn btn-primary" id="cookieAcceptAll">Alle akzeptieren</button>
        <button type="button" class="btn btn-ghost" id="cookieNecessary">Nur notwendige</button>
        <button type="button" class="btn btn-ghost" id="cookieSettingsBtn">Einstellungen</button>
      </div>
      <div id="cookieSettingsPanel" class="cookie-settings-panel">
        <div class="cookie-toggle-row">
          <div><strong>Notwendig</strong><span>Login, Cookie-Auswahl (immer aktiv)</span></div>
          <input type="checkbox" checked disabled aria-label="Notwendig"/>
        </div>
        <div class="cookie-toggle-row">
          <div><strong>Funktional</strong><span>Profil, Chats, Swipes im Browser</span></div>
          <input type="checkbox" id="cookieFunctional" checked/>
        </div>
        <div class="cookie-toggle-row">
          <div><strong>Extern</strong><span>Google Fonts, ggf. Werbung (AdSense)</span></div>
          <input type="checkbox" id="cookieExternal"/>
        </div>
        <button type="button" class="btn btn-primary" id="cookieSaveSettings" style="width:100%;margin-top:0.5rem">Auswahl speichern</button>
      </div>
    </div>
  </div>
  <script>
(function(){function showLanding(){try{document.body.classList.add('mode-landing');document.body.classList.remove('mode-app');var a=document.getElementById('authScreen');var s=document.getElementById('appShell');if(a){a.classList.add('active');}if(s){s.classList.add('hidden');}}catch(e){}}showLanding();window.addEventListener('DOMContentLoaded',showLanding);window.addEventListener('load',showLanding);setTimeout(function(){if(!window.__jamiBooted){showLanding();}},10000);function loadScript(src,next){var s=document.createElement('script');s.src=src;s.onload=s.onerror=function(){if(next)next();};document.body.appendChild(s);}function loadAll(){loadScript('js/cookie-consent.js?v=__JAMI_BUILD__',function(){loadScript('js/jami-api.js?v=__JAMI_BUILD__',function(){loadScript('js/app.js?v=__JAMI_BUILD__');});});}function scheduleLoad(){var run=loadAll;if(window.__jamiLite){run=function(){var go=function(){loadAll();};if(window.requestIdleCallback)requestIdleCallback(go,{timeout:2500});else setTimeout(go,200);};if(document.readyState==='complete')run();else window.addEventListener('load',run,{once:true});}else if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',run,{once:true});else run();}scheduleLoad();})();
  </script>
</body>
</html>
"""

COOKIE_CONSENT_JS = r"""
(function () {
  const KEY = 'jami_cookie_consent';
  const FONT_URL = 'https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=Playfair+Display:ital,wght@0,500;0,600;0,700;1,500&display=swap';

  function read() {
    try {
      const raw = localStorage.getItem(KEY);
      if (!raw) return null;
      return JSON.parse(raw);
    } catch (e) {
      return null;
    }
  }

  function save(consent) {
    const data = {
      v: 1,
      necessary: true,
      functional: !!consent.functional,
      external: !!consent.external,
      at: new Date().toISOString(),
    };
    localStorage.setItem(KEY, JSON.stringify(data));
    apply(data);
    hideBanner();
  }

  function loadGoogleFonts() {
    if (document.getElementById('jamiGoogleFonts')) return;
    const link = document.createElement('link');
    link.id = 'jamiGoogleFonts';
    link.rel = 'stylesheet';
    link.href = FONT_URL;
    link.onload = () => document.body.classList.add('fonts-loaded');
    document.head.appendChild(link);
  }

  function apply(consent) {
    window.jamiCookieConsent = consent;
    if (consent.external) loadGoogleFonts();
    document.dispatchEvent(new CustomEvent('jami-cookie-consent', { detail: consent }));
  }

  function showBanner() {
    const el = document.getElementById('cookieBanner');
    if (el) el.classList.remove('hidden');
  }

  function hideBanner() {
    const el = document.getElementById('cookieBanner');
    if (el) el.classList.add('hidden');
  }

  function openSettings() {
    const panel = document.getElementById('cookieSettingsPanel');
    const c = read();
    if (panel) panel.classList.add('open');
    const fn = document.getElementById('cookieFunctional');
    const ex = document.getElementById('cookieExternal');
    if (fn) fn.checked = c ? c.functional !== false : true;
    if (ex) ex.checked = !!(c && c.external);
    showBanner();
  }

  function bind() {
    document.getElementById('cookieAcceptAll')?.addEventListener('click', () => {
      save({ functional: true, external: true });
    });
    document.getElementById('cookieNecessary')?.addEventListener('click', () => {
      save({ functional: true, external: false });
    });
    document.getElementById('cookieSettingsBtn')?.addEventListener('click', () => {
      document.getElementById('cookieSettingsPanel')?.classList.toggle('open');
    });
    document.getElementById('cookieSaveSettings')?.addEventListener('click', () => {
      save({
        functional: document.getElementById('cookieFunctional')?.checked !== false,
        external: !!document.getElementById('cookieExternal')?.checked,
      });
    });
    document.querySelectorAll('[data-open-cookies]').forEach(btn => {
      btn.addEventListener('click', e => {
        e.preventDefault();
        openSettings();
      });
    });
  }

  function init() {
    bind();
    const existing = read();
    if (existing && existing.v) {
      apply(existing);
    } else {
      showBanner();
    }
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
  window.JamiCookies = { read, save, openSettings, apply };
})();
"""

APP_JS = r"""
const STORAGE_KEY = 'jami_dating_v2';
let config = {};
let pushVapidKey = '';

async function resolvePushVapidKey() {
  if (pushVapidKey) return pushVapidKey;
  pushVapidKey = (config.vapidPublicKey || '').trim();
  if (!pushVapidKey && JamiApi.mode === 'cloudflare') {
    try {
      const r = await fetch('/api/push/vapid-public');
      if (r.ok) {
        const j = await r.json();
        pushVapidKey = (j.publicKey || '').trim();
      }
    } catch (e) {}
  }
  return pushVapidKey;
}

function pushSupported() {
  return !!(
    window.isSecureContext &&
    'serviceWorker' in navigator &&
    'PushManager' in window &&
    'Notification' in window
  );
}

function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
  const raw = atob(base64);
  const out = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
  return out;
}

async function registerServiceWorker() {
  if (window.__jamiLite || !('serviceWorker' in navigator)) return null;
  try {
    return await navigator.serviceWorker.register('/sw.js', { scope: '/' });
  } catch (e) {
    console.warn('SW register', e);
    return null;
  }
}

async function registerPushServiceWorker() {
  if (!pushSupported()) return null;
  return registerServiceWorker();
}

function markPushPromptEligible() {
  try {
    localStorage.setItem('jami_push_eligible', '1');
  } catch (e) {}
}

function maybeShowPushPrompt() {
  const modal = document.getElementById('pushPromptModal');
  if (!modal || !pushSupported()) return;
  if (Notification.permission === 'granted') return;
  try {
    if (localStorage.getItem('jami_push_dismissed') === '1') return;
    if (localStorage.getItem('jami_push_subscribed') === '1') return;
    if (localStorage.getItem('jami_push_eligible') !== '1') return;
  } catch (e) {
    return;
  }
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
}

function closePushPrompt(dismiss) {
  const modal = document.getElementById('pushPromptModal');
  if (!modal) return;
  modal.classList.add('hidden');
  modal.setAttribute('aria-hidden', 'true');
  if (dismiss) {
    try {
      localStorage.setItem('jami_push_dismissed', '1');
    } catch (e) {}
  }
}

async function enableWebPush() {
  if (!me() || JamiApi.mode !== 'cloudflare') {
    toast('Push nur im Online-Betrieb');
    return false;
  }
  const vapid = await resolvePushVapidKey();
  if (!vapid) {
    toast('Push gerade nicht verfügbar');
    return false;
  }
  await registerPushServiceWorker();
  const perm = await Notification.requestPermission();
  if (perm !== 'granted') {
    toast('Benachrichtigungen abgelehnt');
    closePushPrompt(true);
    return false;
  }
  const reg = await navigator.serviceWorker.ready;
  let sub = await reg.pushManager.getSubscription();
  if (!sub) {
    sub = await reg.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(vapid),
    });
  }
  await JamiApi.pushSubscribe(sub.toJSON());
  try {
    localStorage.setItem('jami_push_subscribed', '1');
  } catch (e) {}
  closePushPrompt(false);
  toast('🔔 Push aktiv – du wirst bei Antworten benachrichtigt');
  return true;
}

async function handleDeepLinkChat() {
  const chatId = new URLSearchParams(location.search).get('chat');
  if (!chatId || !me()) return;
  let other = resolveProfileUser(chatId);
  if (!other) other = { id: chatId, displayName: 'Chat' };
  await openChatSession(chatId, other);
  history.replaceState({}, '', location.pathname || '/');
}

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.addEventListener('message', (ev) => {
    if (ev.data?.type !== 'jami-open-chat' || !ev.data.url) return;
    try {
      const u = new URL(ev.data.url, location.origin);
      const chat = u.searchParams.get('chat');
      if (chat && me()) openChatSession(chat, resolveProfileUser(chat) || { id: chat, displayName: 'Chat' });
    } catch (e) {}
  });
}
let state = { users: [], session: null, chats: {}, pendingPack: null, shopPayMethod: 'all', likes: {}, passes: {}, blocked: {}, superLikes: {}, friendRequests: { sent: {}, incoming: [] }, friendships: [] };
let swipeQueue = [];
let activeChatSessionId = null;
let activeChatPartner = null;
let chatProfileSheetView = 'partner';
let presenceHeartbeatTimer = null;
let chatPresenceTimer = null;
let chatStatusTimer = null;
let inboxPollTimer = null;
const SEEN_MATCHES_KEY = 'jami_seen_match_ids';
const JAMI_BASE_TITLE = 'Jami-Dating';
let chatTypingStopTimer = null;
let lastTypingPing = 0;
let presenceVisibilityBound = false;
let currentSwipeId = null;
let pendingMatchOtherId = null;
const threadChatMeta = {};
let adWatchToken = null;
let adTimerIv = null;
let adCfg = { coinsPerVideo: 1, secondsWatch: 15, dailyMax: 10, freeInstant: false };
let adStatus = { todayCount: 0, remaining: 10 };

async function loadConfig() {
  const fallback = { backend: 'cloudflare', siteUrl: location.origin.replace(/\/$/, '') };
  try {
    const ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
    const timer = ctrl ? setTimeout(() => ctrl.abort(), 8000) : null;
    const r = await fetch('js/config.json', ctrl ? { signal: ctrl.signal, cache: 'no-cache' } : { cache: 'no-cache' });
    if (timer) clearTimeout(timer);
    if (r.ok) {
      config = { ...fallback, ...(await r.json()) };
      return;
    }
  } catch (e) {}
  config = { ...fallback, ...config };
}

function ensureLandingVisible() {
  document.body.classList.add('mode-landing');
  document.body.classList.remove('mode-app');
  document.getElementById('authScreen')?.classList.add('active');
  document.getElementById('appShell')?.classList.add('hidden');
}

function showAppShellLoading() {
  document.body.classList.remove('mode-landing');
  document.body.classList.add('mode-app');
  document.getElementById('authScreen')?.classList.remove('active');
  document.getElementById('appShell')?.classList.remove('hidden');
  const stack = document.getElementById('swipeStack');
  if (stack) stack.innerHTML = jamiLoaderHtml('Wird geladen…');
}

function deferSecondaryAppUi() {
  const run = () => {
    renderShop().catch(() => {});
    renderEmojiShop();
    renderProfile();
    wirePhotoFileInputs();
    renderProfileEmojiPicker();
    renderProfileCompleteness();
    renderProfileBoost();
    renderReferralCard();
    renderFriendRequests();
  };
  if (typeof requestIdleCallback === 'function') requestIdleCallback(run, { timeout: 1200 });
  else setTimeout(run, 0);
}

function isMobileViewport() {
  return typeof matchMedia === 'function' && matchMedia('(max-width: 768px)').matches;
}
function isInAppBrowser() {
  return !!window.__jamiLite;
}
let mobileUiBound = false;
function initMobileAppUi() {
  if (mobileUiBound) return;
  mobileUiBound = true;
  const refreshMap = () => {
    if (geoMap) setTimeout(() => geoMap.invalidateSize(), 280);
  };
  window.addEventListener('orientationchange', refreshMap);
  if (window.visualViewport) window.visualViewport.addEventListener('resize', refreshMap);
}

function captureUtmParams() {
  try {
    const p = new URLSearchParams(location.search);
    ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content'].forEach((k) => {
      const v = (p.get(k) || '').trim();
      if (v) localStorage.setItem('jami_' + k, v.slice(0, 120));
    });
  } catch (e) {}
}

function getTrafficSource() {
  try {
    const p = new URLSearchParams(location.search);
    const utm = (p.get('utm_source') || localStorage.getItem('jami_utm_source') || '').trim();
    if (utm) return utm.slice(0, 80);
    const ref = document.referrer || '';
    if (ref) {
      const host = new URL(ref).hostname.replace(/^www\./, '');
      const mine = location.hostname.replace(/^www\./, '');
      if (host && host !== mine) return ('ref:' + host).slice(0, 80);
    }
  } catch (e) {}
  return 'direct';
}

function getTrafficMedium() {
  try {
    const p = new URLSearchParams(location.search);
    return (p.get('utm_medium') || localStorage.getItem('jami_utm_medium') || '').trim().slice(0, 80);
  } catch (e) {
    return '';
  }
}

async function trackSiteHit(type, path) {
  if (config.backend !== 'cloudflare') return;
  try {
    await fetch('/api/analytics/hit', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        type: type || 'page_view',
        path: path || location.pathname,
        source: getTrafficSource(),
      }),
    });
  } catch (e) {}
}

function initAnalytics() {
  const run = () => {
    if (config.backend !== 'cloudflare') return;
    if (!sessionStorage.getItem('jami_pv_tracked')) {
      sessionStorage.setItem('jami_pv_tracked', '1');
      trackSiteHit('page_view', location.pathname || '/');
    }
    const clickType = 'register_click';
    const trackRegisterClick = () => trackSiteHit(clickType, '/cta-register');
    document.querySelectorAll('[data-landing-register]').forEach((el) => {
      el.addEventListener('click', trackRegisterClick);
    });
    document.getElementById('btnShowRegister')?.addEventListener('click', trackRegisterClick);
  };
  if (typeof requestIdleCallback === 'function') requestIdleCallback(run, { timeout: 2500 });
  else setTimeout(run, 800);
}

function initStickyCta() {
  const bar = document.getElementById('stickyCta');
  if (!bar) return;
  const update = () => {
    if (!document.body.classList.contains('mode-landing')) {
      bar.classList.add('hidden');
      bar.setAttribute('aria-hidden', 'true');
      return;
    }
    const hero = document.querySelector('.landing-hero');
    const y = window.scrollY || document.documentElement.scrollTop || 0;
    const past = hero ? y > hero.offsetHeight * 0.5 : y > 380;
    bar.classList.toggle('hidden', !past);
    bar.setAttribute('aria-hidden', past ? 'false' : 'true');
  };
  window.addEventListener('scroll', update, { passive: true });
  update();
}

function initShareJami() {
  const btn = document.getElementById('btnShareJami');
  if (!btn) return;
  const base = (config.siteUrl || location.origin).replace(/\/$/, '');
  const text =
    'Jami-Dating – echte Singles, keine Fake-Moderatoren. 100 Gratis-Coins beim Start!';
  const url = base + '/?utm_source=share&utm_medium=referral&utm_campaign=friend';
  btn.addEventListener('click', async () => {
    if (navigator.share) {
      try {
        await navigator.share({ title: 'Jami-Dating', text, url });
        return;
      } catch (e) {}
    }
    try {
      await navigator.clipboard.writeText(url + '\\n\\n' + text);
      toast('Link kopiert – teile Jami mit Freunden!');
    } catch (e) {
      toast(url);
    }
  });
}

function initServiceWorkerEarly() {
  const run = () => registerServiceWorker().catch(() => {});
  if (document.readyState === 'complete') run();
  else window.addEventListener('load', run, { once: true });
}

function initPwaInstall() {
  let deferredPrompt = null;
  window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault();
    deferredPrompt = e;
    if (document.getElementById('pwaInstallBar')) return;
    const bar = document.createElement('div');
    bar.id = 'pwaInstallBar';
    bar.className = 'pwa-install-bar';
    bar.innerHTML =
      '<span>📱 Jami als App installieren</span>' +
      '<button type="button" class="btn btn-primary" id="btnPwaInstall">Installieren</button>' +
      '<button type="button" class="btn btn-ghost" id="btnPwaDismiss">×</button>';
    document.body.appendChild(bar);
    document.getElementById('btnPwaInstall')?.addEventListener('click', async () => {
      if (!deferredPrompt) return;
      deferredPrompt.prompt();
      await deferredPrompt.userChoice;
      deferredPrompt = null;
      bar.remove();
    });
    document.getElementById('btnPwaDismiss')?.addEventListener('click', () => bar.remove());
  });
}

function landingWmFixtureClass(m) {
  const home = String(m.homeTeam || '');
  const away = String(m.awayTeam || '');
  const isGer = home.indexOf('Deutschland') >= 0 || away.indexOf('Deutschland') >= 0;
  if (!isGer || !m.kickoffAt) return '';
  try {
    const d = new Date(m.kickoffAt);
    const now = new Date();
    if (
      d.getFullYear() === now.getFullYear() &&
      d.getMonth() === now.getMonth() &&
      d.getDate() === now.getDate()
    ) {
      return ' landing-wm-fixture--today';
    }
  } catch (e) {}
  return '';
}

function landingWmKickoffLabel(iso) {
  if (!iso) return '';
  try {
    const d = new Date(iso);
    const now = new Date();
    const sameDay =
      d.getFullYear() === now.getFullYear() &&
      d.getMonth() === now.getMonth() &&
      d.getDate() === now.getDate();
    const time = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
    if (sameDay) return 'heute · ' + time + ' Uhr';
    return wmFormatKickoff(iso);
  } catch (e) {
    return iso;
  }
}

function wmMatchHasGermany(m) {
  const home = String(m.homeTeam || m.home_team || '');
  const away = String(m.awayTeam || m.away_team || '');
  return home.indexOf('Deutschland') >= 0 || away.indexOf('Deutschland') >= 0;
}

function wmNextGermanyMatch(matches) {
  const open = (matches || []).filter(function (m) {
    return wmMatchHasGermany(m) && (m.status === 'open' || m.status === 'locked');
  });
  open.sort(function (a, b) {
    return new Date(a.kickoffAt).getTime() - new Date(b.kickoffAt).getTime();
  });
  return open[0] || null;
}

function wmLastGermanySettled(matches) {
  const settled = (matches || []).filter(function (m) {
    return wmMatchHasGermany(m) && m.status === 'settled';
  });
  settled.sort(function (a, b) {
    return new Date(b.kickoffAt).getTime() - new Date(a.kickoffAt).getTime();
  });
  return settled[0] || null;
}

function paintWmDfbCopy(summary) {
  const last = summary && summary.lastResult;
  const next = summary && summary.nextMatch;
  const parts = [];
  if (last && last.resultHome != null) {
    parts.push(
      'DFB: <strong>' +
        escHtml(last.homeFlag || '') +
        ' ' +
        escHtml(last.homeTeam) +
        ' ' +
        last.resultHome +
        ':' +
        last.resultAway +
        ' ' +
        escHtml(last.awayTeam) +
        '</strong>'
    );
  }
  if (next) {
    parts.push(
      'Nächstes Spiel: <strong>' +
        escHtml(next.homeFlag || '') +
        ' ' +
        escHtml(next.homeTeam) +
        ' vs. ' +
        escHtml(next.awayTeam) +
        ' · ' +
        escHtml(landingWmKickoffLabel(next.kickoffAt)) +
        '</strong>'
    );
  }
  return parts.length ? parts.join(' – ') + '.' : 'WM 2026 – alle Spiele live in der App tippen.';
}

function paintWmAppChrome(matches) {
  const fanEl = document.getElementById('wmFanNextMatch');
  const bannerEl = document.getElementById('wmTodayBanner');
  const nextGer = wmNextGermanyMatch(matches);
  const lastGer = wmLastGermanySettled(matches);

  if (fanEl) {
    fanEl.innerHTML = paintWmDfbCopy({ lastResult: lastGer, nextMatch: nextGer }) + ' Hier mitfiebern &amp; tippen! 📯';
  }

  if (bannerEl) {
    if (nextGer) {
      bannerEl.hidden = false;
      const isToday = landingWmFixtureClass(nextGer).indexOf('today') >= 0;
      bannerEl.innerHTML =
        escHtml(nextGer.homeFlag || '') +
        ' <strong>' +
        (isToday ? 'Heute ' : 'Nächstes DFB-Spiel ') +
        escHtml(landingWmKickoffLabel(nextGer.kickoffAt).replace(/^heute · /, '')) +
        ':</strong> ' +
        escHtml(nextGer.homeTeam) +
        ' vs. ' +
        escHtml(nextGer.awayTeam) +
        ' – jetzt mit Coins tippen!';
    } else if (lastGer && lastGer.resultHome != null) {
      bannerEl.hidden = false;
      bannerEl.innerHTML =
        '🇩🇪 Letztes Spiel: ' +
        escHtml(lastGer.homeTeam) +
        ' ' +
        lastGer.resultHome +
        ':' +
        lastGer.resultAway +
        ' ' +
        escHtml(lastGer.awayTeam) +
        ' – nächste Tipps folgen';
    } else {
      bannerEl.hidden = true;
    }
  }
}

async function refreshLandingWmPromo() {
  const subEl = document.getElementById('landingWmSub');
  if (!subEl || config.backend !== 'cloudflare') return;
  try {
    const r = await fetch('/api/wm/dfb-summary');
    const data = await r.json();
    subEl.innerHTML = paintWmDfbCopy(data) + ' Coins tippen, Fan-Lounge, zwischendurch swipen.';
    const ticker = document.querySelector('.landing-live-ticker-track');
    if (ticker && data.nextMatch) {
      const n = data.nextMatch;
      const line =
        '⚽ WM-Tipp: ' +
        (n.homeFlag || '') +
        ' ' +
        n.homeTeam +
        ' vs. ' +
        n.awayTeam +
        ' · ' +
        landingWmKickoffLabel(n.kickoffAt);
      ticker.querySelectorAll('span').forEach(function (sp, i) {
        if (i === 0 || i === 7) sp.textContent = line;
      });
    }
  } catch (e) {}
}

function paintLandingWmSchedule(listEl, matches) {
  if (!listEl) return;
  if (!matches.length) {
    listEl.innerHTML = '<p class="meta" style="margin:0">Spielplan wird bald aktualisiert – nach Login unter WM tippen.</p>';
    return;
  }
  listEl.innerHTML = matches
    .map(function (m) {
      const isGerToday = landingWmFixtureClass(m).indexOf('today') >= 0;
      const badge = isGerToday ? '<span class="landing-wm-fixture-badge">Heute</span>' : '';
      return (
        '<div class="landing-wm-fixture' +
        landingWmFixtureClass(m) +
        '">' +
        '<div class="landing-wm-fixture-team home"><span>' +
        escHtml(m.homeFlag || '') +
        '</span><span>' +
        escHtml(m.homeTeam) +
        '</span></div>' +
        '<strong>vs</strong>' +
        '<div class="landing-wm-fixture-team away"><span>' +
        escHtml(m.awayFlag || '') +
        '</span><span>' +
        escHtml(m.awayTeam) +
        '</span></div>' +
        '<div class="landing-wm-fixture-meta">' +
        badge +
        escHtml(m.stage || 'WM 2026') +
        ' · ' +
        escHtml(landingWmKickoffLabel(m.kickoffAt)) +
        '</div></div>'
      );
    })
    .join('');
}

async function refreshLandingWmSchedule() {
  const listEl = document.getElementById('landingWmScheduleList');
  if (!listEl || config.backend !== 'cloudflare') return;
  try {
    const r = await fetch('/api/wm/upcoming?limit=4');
    const data = await r.json();
    paintLandingWmSchedule(listEl, data.matches || []);
  } catch (e) {
    if (!listEl.querySelector('.landing-wm-fixture')) {
      listEl.innerHTML = '<p class="meta" style="margin:0">Spielplan online in der App nach Login.</p>';
    }
  }
  refreshLandingWmPromo().catch(function () {});
}

const WM_SCHEDULE_POLL_MS = 10 * 60 * 1000;
let wmLandingSchedulePollTimer = null;
let wmTipsPollTimer = null;

function stopWmLandingSchedulePoll() {
  if (wmLandingSchedulePollTimer) {
    clearInterval(wmLandingSchedulePollTimer);
    wmLandingSchedulePollTimer = null;
  }
}

function startWmLandingSchedulePoll() {
  stopWmLandingSchedulePoll();
  if (config.backend !== 'cloudflare') return;
  wmLandingSchedulePollTimer = setInterval(function () {
    refreshLandingWmSchedule().catch(function () {});
  }, WM_SCHEDULE_POLL_MS);
}

function stopWmTipsPoll() {
  if (wmTipsPollTimer) {
    clearInterval(wmTipsPollTimer);
    wmTipsPollTimer = null;
  }
}

function startWmTipsPoll() {
  stopWmTipsPoll();
  wmTipsPollTimer = setInterval(function () {
    renderWmTips(true).catch(function () {});
  }, WM_SCHEDULE_POLL_MS);
}

function initLandingWmSchedule() {
  refreshLandingWmSchedule().catch(function () {});
  startWmLandingSchedulePoll();
}

function prefetchWmForApp() {
  if (!useDb() || JamiApi.mode !== 'cloudflare') return;
  renderWmTips(true).catch(function () {});
}

function initGrowth() {
  captureUtmParams();
  if (!window.__jamiLite) initServiceWorkerEarly();
  initShareJami();
  if (!window.__jamiLite) initPwaInstall();
}

function betaTestCfg() {
  return (config && config.betaTest) || { enabled: true, rewardCoins: 1000 };
}

function initBetaTestSignup() {
  const form = document.getElementById('betaTestForm');
  const section = document.getElementById('beta-test');
  const cfg = betaTestCfg();
  if (!form || !section) return;
  if (cfg.enabled === false) {
    section.style.display = 'none';
    return;
  }
  const reward = cfg.rewardCoins || 1000;
  const rewardEl = section.querySelector('.beta-test-reward');
  if (rewardEl) rewardEl.textContent = '🪙 ' + reward + ' Coins';
  const emailInput = document.getElementById('betaTestEmail');
  if (emailInput && !emailInput.value) {
    const u = me();
    if (u && u.email) emailInput.value = u.email;
    else if (JamiApi.myProfile && JamiApi.myProfile.email) emailInput.value = JamiApi.myProfile.email;
  }
  const success = document.getElementById('betaTestSuccess');
  const linkOut = document.getElementById('betaTestLinkOut');
  const copyBtn = document.getElementById('betaTestCopyBtn');
  const submitBtn = document.getElementById('betaTestSubmit');
  form.addEventListener('submit', async (e) => {
    e.preventDefault();
    const email = (emailInput && emailInput.value || '').trim().toLowerCase();
    const nameEl = document.getElementById('betaTestName');
    const name = (nameEl && nameEl.value || '').trim();
    if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
      toast('Bitte gültige E-Mail eingeben');
      return;
    }
    if (submitBtn) {
      submitBtn.disabled = true;
      submitBtn.textContent = 'Senden…';
    }
    try {
      const r = await fetch('/api/beta-test/signup', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email: email, displayName: name }),
      });
      const j = await r.json();
      if (!r.ok || j.error) throw new Error(j.error || 'Anmeldung fehlgeschlagen');
      const url = j.playStoreUrl || cfg.playStoreUrl || '';
      if (linkOut && url) {
        linkOut.href = url;
        linkOut.textContent = url.length > 64 ? url.slice(0, 61) + '…' : url;
      }
      form.classList.add('hidden');
      if (success) success.classList.remove('hidden');
      if (copyBtn && url) {
        copyBtn.onclick = function () {
          navigator.clipboard.writeText(url).then(function () { toast('Link kopiert'); }).catch(function () { toast('Kopieren nicht möglich'); });
        };
      }
      toast('Link gesendet – auch per E-Mail!');
    } catch (err) {
      toast(err.message || 'Fehler');
    } finally {
      if (submitBtn) {
        submitBtn.disabled = false;
        submitBtn.textContent = 'Link senden';
      }
    }
  });
}

function useDb() {
  return JamiApi.isOnline();
}

function loadState() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY) || localStorage.getItem('jami_dating_v1');
    if (raw) state = { ...state, ...JSON.parse(raw) };
  } catch (e) {}
  state.likes = state.likes || {};
  state.passes = state.passes || {};
  state.blocked = state.blocked || {};
  state.superLikes = state.superLikes || {};
  state.friendRequests = state.friendRequests || { sent: {}, incoming: [] };
  state.friendships = state.friendships || [];
  purgeDemoFromLocalState();
}

function isDemoUser(u) {
  return !!(u?.demo || (u?.id && String(u.id).startsWith('demo_')));
}

function isNearbySeedId(id) {
  return !!(id && String(id).startsWith('nearby_seed_'));
}

function purgeDemoFromLocalState() {
  const demoIds = new Set(
    (state.users || []).filter(isDemoUser).map((u) => u.id)
  );
  if (!demoIds.size) return;
  state.users = (state.users || []).filter((u) => !demoIds.has(u.id));
  Object.keys(state.likes || {}).forEach((uid) => {
    state.likes[uid] = (state.likes[uid] || []).filter((id) => !demoIds.has(id));
    if (demoIds.has(uid)) delete state.likes[uid];
  });
  Object.keys(state.passes || {}).forEach((uid) => {
    state.passes[uid] = (state.passes[uid] || []).filter((id) => !demoIds.has(id));
    if (demoIds.has(uid)) delete state.passes[uid];
  });
  Object.keys(state.blocked || {}).forEach((uid) => {
    state.blocked[uid] = (state.blocked[uid] || []).filter((id) => !demoIds.has(id));
  });
  Object.keys(state.chats || {}).forEach((key) => {
    if (key.split('_').some((part) => demoIds.has(part))) delete state.chats[key];
  });
  if (state.friendRequests?.incoming) {
    state.friendRequests.incoming = state.friendRequests.incoming.filter(
      (x) => !demoIds.has(x.fromId)
    );
  }
  saveState();
}

function saveState() {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}

function avatarHtml(u) {
  ensurePhotoSlots(u);
  const src = (u.photos && u.photos.main) || u.photo || '';
  if (src && !isStockPhotoUrl(src)) return `<img src="${escHtml(src)}" alt="${escHtml(u.displayName)}"/>`;
  return `<span class="avatar-ph" aria-hidden="true">${u.emoji || '💫'}</span>`;
}

function presenceDotWindowMs() {
  return (config.presenceDotHours || 24) * 60 * 60 * 1000;
}

function getProfilePresenceVisual(u, isMe) {
  if (isMe || !u) return { showDot: false, showLive: false };
  const p = computePresenceClient(u);
  if (p.online) return { showDot: true, showLive: true };
  const at = u.lastActiveAt;
  if (at != null) {
    const s = String(at);
    const ms =
      typeof at === 'number'
        ? at
        : Date.parse(s.includes('T') ? s : s.replace(' ', 'T') + (s.includes('Z') ? '' : 'Z'));
    if (Number.isFinite(ms) && Date.now() - ms <= presenceDotWindowMs()) {
      return { showDot: true, showLive: false };
    }
  }
  if (p.presenceTier === 'recent' || p.presenceTier === 'today') {
    return { showDot: true, showLive: false };
  }
  return { showDot: false, showLive: false };
}

function profilePresenceOverlayHtml(u, isMe) {
  const v = getProfilePresenceVisual(u, isMe);
  let html = '';
  if (v.showLive) {
    html += '<span class="avatar-live-label">Jetzt aktiv</span>';
  }
  if (v.showDot) {
    html +=
      '<span class="avatar-presence-dot" aria-hidden="true" title="' +
      (v.showLive ? 'Gerade online' : 'In den letzten 24 Stunden aktiv') +
      '"></span>';
  }
  return html;
}

function avatarBlockHtml(u, isMe, extraClass) {
  const overlay = profilePresenceOverlayHtml(u, isMe);
  const cls =
    'avatar' + (overlay ? ' avatar-with-presence' : '') + (extraClass ? ' ' + extraClass : '');
  return '<div class="' + cls + '">' + avatarHtml(u) + overlay + '</div>';
}

function parseHobbiesInput(str) {
  if (Array.isArray(str)) return str;
  return (str || '').split(',').map(s => s.trim()).filter(Boolean);
}

function escHtml(s) {
  return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}

function stripContactInfo(text) {
  if (!text) return '';
  let s = String(text);
  s = s.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[Kontakt verborgen]');
  s = s.replace(/(\+49|0049|00\s*49)\s*[\d\s./-]{6,}/g, '[Kontakt verborgen]');
  s = s.replace(/\b0[1-9][\d\s./-]{7,}\b/g, '[Kontakt verborgen]');
  s = s.replace(/\bwhatsapp\b/gi, '[Kontakt verborgen]');
  s = s.replace(/\btelegram\b/gi, '[Kontakt verborgen]');
  return s.trim();
}

const CHAT_PHOTO_PREFIX = '[jami-img]';
const CHAT_EMOJI_PREFIX = '[jami-e:';

function shopEmojiById(id) {
  return (config.emojiShop || []).find((e) => e.id === id);
}

function encodeShopEmojiToken(id) {
  return CHAT_EMOJI_PREFIX + id + ']';
}

function upgradeLegacyShopEmojiTokens(text) {
  let s = String(text || '');
  (config.emojiShop || []).forEach((item) => {
    if (s.includes(item.icon) && !s.includes(encodeShopEmojiToken(item.id))) {
      s = s.split(item.icon).join(encodeShopEmojiToken(item.id));
    }
  });
  return s;
}

function parseChatMessageParts(text) {
  const s = upgradeLegacyShopEmojiTokens(text);
  const parts = [];
  const re = /\[jami-e:([a-z0-9_]+)\]/gi;
  let last = 0;
  let m;
  while ((m = re.exec(s)) !== null) {
    if (m.index > last) parts.push({ type: 'text', value: s.slice(last, m.index) });
    parts.push({ type: 'emoji', id: m[1].toLowerCase() });
    last = m.index + m[0].length;
  }
  if (last < s.length) parts.push({ type: 'text', value: s.slice(last) });
  if (!parts.length) parts.push({ type: 'text', value: s });
  return parts;
}

function buildChatMessageContent(text) {
  const parts = parseChatMessageParts(text);
  const hasEmoji = parts.some((p) => p.type === 'emoji');
  const hasText = parts.some((p) => p.type === 'text' && p.value.trim());
  const emojiOnly = hasEmoji && !hasText;
  const frag = document.createDocumentFragment();
  parts.forEach((p) => {
    if (p.type === 'text') {
      if (p.value) frag.appendChild(document.createTextNode(p.value));
    } else {
      const item = shopEmojiById(p.id);
      const anim = item?.anim || 'float';
      const wrap = document.createElement('span');
      wrap.className = 'chat-emoji-wrap emoji-anim-' + anim;
      const span = document.createElement('span');
      span.className = 'chat-emoji-anim';
      span.textContent = item?.icon || '✨';
      span.setAttribute('role', 'img');
      span.setAttribute('aria-label', item?.name || 'Emoji');
      wrap.appendChild(span);
      frag.appendChild(wrap);
    }
  });
  return { frag, emojiOnly, hasEmoji };
}

function isChatPhotoBody(text) {
  return String(text || '').trim().startsWith(CHAT_PHOTO_PREFIX);
}

function chatPhotoSrc(text) {
  if (!isChatPhotoBody(text)) return '';
  return String(text).trim().slice(CHAT_PHOTO_PREFIX.length);
}

function sanitizeChatMessage(text) {
  const t = String(text || '').trim();
  if (!t) return '';
  if (isChatPhotoBody(t)) return t;
  if (/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/i.test(t)) return 'E-Mail';
  if (/^(\+49|0049|00\s*49)?\s*[\d\s./-]{8,}$/.test(t) || /^0[1-9][\d\s./-]{7,}$/.test(t)) return 'Nummer';
  let s = t;
  s = s.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/gi, 'E-Mail');
  s = s.replace(/(\+49|0049|00\s*49)\s*[\d\s./-]{6,}/g, 'Nummer');
  s = s.replace(/\b0[1-9][\d\s./-]{7,}\b/g, 'Nummer');
  s = s.replace(/\bwhatsapp\b/gi, 'Nummer');
  s = s.replace(/\btelegram\b/gi, 'Nummer');
  return s.trim();
}

function readFileAsDataUrl(file) {
  return new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => resolve(String(r.result || ''));
    r.onerror = () => reject(new Error('Datei konnte nicht gelesen werden'));
    r.readAsDataURL(file);
  });
}

async function compressImageForProfile(file) {
  const mobile = typeof matchMedia === 'function' && matchMedia('(max-width: 768px)').matches;
  const maxChars = mobile ? 520000 : 900000;
  const maxW = mobile ? 720 : 900;
  if (!file) throw new Error('Keine Datei gewählt');
  const name = String(file.name || '').toLowerCase();
  const isImage =
    !file.type ||
    file.type.startsWith('image/') ||
    /\.(jpe?g|png|webp|gif|heic|heif|bmp)$/i.test(name);
  if (!isImage) throw new Error('Nur Bilder erlaubt');

  async function drawToCanvas(drawFn, srcW, srcH, cleanup) {
    let w = srcW;
    let h = srcH;
    if (w > maxW) {
      h = Math.round((h * maxW) / w);
      w = maxW;
    }
    const canvas = document.createElement('canvas');
    canvas.width = w;
    canvas.height = h;
    const ctx = canvas.getContext('2d');
    if (!ctx) throw new Error('Bildverarbeitung nicht verfügbar');
    drawFn(ctx, w, h);
    cleanup?.();
    let quality = mobile ? 0.8 : 0.86;
    let dataUrl = canvas.toDataURL('image/jpeg', quality);
    while (dataUrl.length > maxChars && quality > 0.4) {
      quality -= 0.06;
      dataUrl = canvas.toDataURL('image/jpeg', quality);
    }
    if (dataUrl.length > maxChars) throw new Error('Bild zu groß – bitte kleineres Foto wählen');
    return dataUrl;
  }

  if (typeof createImageBitmap === 'function') {
    try {
      const bitmap = await createImageBitmap(file);
      return drawToCanvas(
        (ctx, w, h) => ctx.drawImage(bitmap, 0, 0, w, h),
        bitmap.width,
        bitmap.height,
        () => bitmap.close?.()
      );
    } catch (e) {}
  }

  const tmpUrl = await readFileAsDataUrl(file);
  const img = await new Promise((resolve, reject) => {
    const el = new Image();
    el.onload = () => resolve(el);
    el.onerror = () => reject(new Error('Bild konnte nicht geladen werden – anderes Foto probieren'));
    el.src = tmpUrl;
  });
  return drawToCanvas((ctx, w, h) => ctx.drawImage(img, 0, 0, w, h), img.width, img.height);
}

async function compressImageForChat(file) {
  const maxChars = config.chatPhotoMaxChars || 480000;
  if (!file || !file.type.startsWith('image/')) throw new Error('Nur Bilder erlaubt');
  if (file.type === 'image/gif') {
    if (file.size > maxChars * 0.75) throw new Error('GIF zu groß – bitte kleineres Bild wählen');
    const dataUrl = await readFileAsDataUrl(file);
    if (dataUrl.length > maxChars) throw new Error('GIF zu groß');
    return dataUrl;
  }
  const bitmap = await createImageBitmap(file);
  let w = bitmap.width;
  let h = bitmap.height;
  const maxW = 1200;
  if (w > maxW) {
    h = Math.round((h * maxW) / w);
    w = maxW;
  }
  const canvas = document.createElement('canvas');
  canvas.width = w;
  canvas.height = h;
  const ctx = canvas.getContext('2d');
  ctx.drawImage(bitmap, 0, 0, w, h);
  bitmap.close?.();
  let quality = 0.82;
  let dataUrl = canvas.toDataURL('image/jpeg', quality);
  while (dataUrl.length > maxChars && quality > 0.42) {
    quality -= 0.08;
    dataUrl = canvas.toDataURL('image/jpeg', quality);
  }
  if (dataUrl.length > maxChars) throw new Error('Bild zu groß – bitte ein kleineres Foto wählen');
  return dataUrl;
}

function parseMsgTimeMs(at) {
  if (at == null || at === '') return null;
  if (typeof at === 'number') return at;
  const s = String(at).trim();
  const t = Date.parse(s.includes('T') ? s : s.replace(' ', 'T') + 'Z');
  return Number.isFinite(t) ? t : null;
}

function formatMsgTimeLabel(at) {
  const ms = parseMsgTimeMs(at);
  if (ms == null) return '';
  const d = new Date(ms);
  const now = new Date();
  const sameDay =
    d.getDate() === now.getDate() &&
    d.getMonth() === now.getMonth() &&
    d.getFullYear() === now.getFullYear();
  const time = d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
  if (sameDay) return time;
  return (
    d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' }) + ' ' + time
  );
}

let chatComposeViewportTeardown = null;

function syncChatDockHeight() {
  const dock = document.getElementById('chatComposeDock');
  if (!dock) return;
  const h = Math.ceil(dock.getBoundingClientRect().height);
  document.documentElement.style.setProperty('--chat-dock-h', h + 'px');
}

function wireChatComposeViewportDock() {
  if (chatComposeViewportTeardown) chatComposeViewportTeardown();
  const dock = document.getElementById('chatComposeDock');
  if (!dock) return;
  syncChatDockHeight();
  const vv = window.visualViewport;
  if (!vv) return;
  const apply = () => {
    syncChatDockHeight();
    const gap = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
    dock.style.transform = gap > 2 ? 'translateY(-' + gap + 'px)' : '';
    const box = document.getElementById('chatSessionMessages');
    if (box) box.scrollTop = box.scrollHeight;
  };
  vv.addEventListener('resize', apply);
  vv.addEventListener('scroll', apply);
  window.addEventListener('resize', syncChatDockHeight);
  apply();
  chatComposeViewportTeardown = () => {
    vv.removeEventListener('resize', apply);
    vv.removeEventListener('scroll', apply);
    window.removeEventListener('resize', syncChatDockHeight);
    dock.style.transform = '';
    document.documentElement.style.removeProperty('--chat-dock-h');
    chatComposeViewportTeardown = null;
  };
}

function unwireChatComposeViewportDock() {
  if (chatComposeViewportTeardown) chatComposeViewportTeardown();
}

function messageReadTicksHtml(read) {
  const cls = read ? 'msg-ticks-read' : 'msg-ticks-sent';
  const label = read ? 'Gelesen' : 'Gesendet';
  return (
    '<span class="msg-ticks ' +
    cls +
    '" aria-label="' +
    label +
    '"><span class="tick">✓</span><span class="tick">✓</span></span>'
  );
}

function appendChatMessageEl(box, body, sideClass, meta) {
  meta = meta || {};
  const d = document.createElement('div');
  d.className = 'msg ' + sideClass;
  if (meta.at != null) {
    const ms = parseMsgTimeMs(meta.at);
    d.dataset.at = String(ms != null ? ms : meta.at);
  }
  const bubble = document.createElement('div');
  bubble.className = 'msg-bubble';
  const safe = sanitizeChatMessage(body);
  if (isChatPhotoBody(safe)) {
    d.classList.add('msg-photo');
    const src = chatPhotoSrc(safe);
    const img = document.createElement('img');
    img.src = src;
    img.alt = 'Chat-Foto';
    img.loading = 'lazy';
    img.onclick = () => openChatPhotoLightbox(src);
    bubble.appendChild(img);
  } else {
    const { frag, emojiOnly, hasEmoji } = buildChatMessageContent(safe);
    if (emojiOnly) d.classList.add('msg-emoji-only');
    else if (hasEmoji) d.classList.add('msg-mixed-emoji');
    bubble.appendChild(frag);
  }
  d.appendChild(bubble);
  const metaRow = document.createElement('div');
  metaRow.className = 'msg-meta';
  const timeEl = document.createElement('span');
  timeEl.className = 'msg-time';
  const timeLabel = formatMsgTimeLabel(meta.at);
  if (timeLabel) timeEl.textContent = timeLabel;
  metaRow.appendChild(timeEl);
  if (sideClass === 'me') metaRow.insertAdjacentHTML('beforeend', messageReadTicksHtml(!!meta.read));
  d.appendChild(metaRow);
  box.appendChild(d);
}

function applyReadStateToChatBox(box, otherReadAt) {
  if (!box) return;
  const readMs = parseMsgTimeMs(otherReadAt);
  if (readMs == null) return;
  box.querySelectorAll('.msg.me').forEach((el) => {
    const at = parseMsgTimeMs(el.dataset.at);
    const read = at != null && readMs >= at;
    const ticks = el.querySelector('.msg-ticks');
    if (!ticks) return;
    ticks.className = 'msg-ticks ' + (read ? 'msg-ticks-read' : 'msg-ticks-sent');
    ticks.setAttribute('aria-label', read ? 'Gelesen' : 'Gesendet');
  });
}

function renderChatTypingIndicator(other, typing) {
  const el = document.getElementById('chatSessionTyping');
  if (!el) return;
  if (typing && other) {
    el.textContent = (other.displayName || 'Partner') + ' schreibt gerade…';
    el.classList.add('is-active');
    el.classList.remove('hidden');
  } else {
    el.textContent = '';
    el.classList.remove('is-active');
    el.classList.add('hidden');
  }
}

function openChatPhotoLightbox(src) {
  let modal = document.getElementById('chatPhotoLightbox');
  if (!modal) {
    modal = document.createElement('div');
    modal.id = 'chatPhotoLightbox';
    modal.className = 'profile-photo-modal';
    modal.innerHTML =
      '<div class="profile-photo-modal-inner"><button type="button" class="btn btn-ghost" id="btnCloseChatPhotoLb">← Schließen</button><img id="chatPhotoLightboxImg" alt=""/></div>';
    document.body.appendChild(modal);
    document.getElementById('btnCloseChatPhotoLb').onclick = () => {
      modal.classList.add('hidden');
      modal.setAttribute('aria-hidden', 'true');
    };
    modal.onclick = (e) => {
      if (e.target === modal) {
        modal.classList.add('hidden');
        modal.setAttribute('aria-hidden', 'true');
      }
    };
  }
  const img = document.getElementById('chatPhotoLightboxImg');
  if (img) img.src = src;
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
}

function wireChatPhotoSend(btnId, fileInputId, getOtherId, sendOpts) {
  const btn = document.getElementById(btnId);
  const input = document.getElementById(fileInputId);
  if (!btn || !input) return;
  btn.onclick = () => input.click();
  input.onchange = async () => {
    const file = input.files?.[0];
    input.value = '';
    const otherId = typeof getOtherId === 'function' ? getOtherId() : getOtherId;
    if (!file) return;
    if (!otherId) {
      toast('Bitte zuerst einen Chat öffnen');
      return;
    }
    try {
      toast('Foto wird vorbereitet…');
      const dataUrl = await compressImageForChat(file);
      await sendMessage(otherId, {
        ...sendOpts,
        body: CHAT_PHOTO_PREFIX + dataUrl,
      });
    } catch (e) {
      toast(e.message || 'Foto konnte nicht gesendet werden');
    }
  };
}

function sanitizeProfilePatch(patch) {
  const keys = ['bio', 'prompt', 'likes', 'preferences', 'petsInfo', 'childrenInfo', 'displayName', 'city'];
  const out = { ...patch };
  keys.forEach(k => { if (typeof out[k] === 'string') out[k] = stripContactInfo(out[k]); });
  return out;
}

function formatBirthdayDe(iso) {
  if (!iso) return '';
  const d = new Date(iso + 'T12:00:00');
  if (isNaN(d.getTime())) return iso;
  return d.toLocaleDateString('de-DE', { day: '2-digit', month: 'long', year: 'numeric' });
}

function profileDetailsHtml(o) {
  const rows = [];
  if (o.relationshipGoal) rows.push(['Sucht', o.relationshipGoal]);
  if (o.seeking && o.seeking !== 'Alle') rows.push(['Interesse', o.seeking]);
  if (o.birthday) rows.push(['Geburtstag', formatBirthdayDe(o.birthday)]);
  if (o.likes) rows.push(['Mag', stripContactInfo(o.likes)]);
  const hobbies = [...(o.hobbies || []), ...(o.interests || [])];
  const uniq = [...new Set(hobbies)];
  if (uniq.length) rows.push(['Hobbys', uniq.join(', ')]);
  if (o.preferences) rows.push(['Vorlieben', stripContactInfo(o.preferences)]);
  if (o.hasPets) rows.push(['Haustiere', o.hasPets + (o.petsInfo ? ' – ' + o.petsInfo : '')]);
  if (o.hasChildren) rows.push(['Kinder', o.hasChildren + (o.childrenInfo ? ' – ' + o.childrenInfo : '')]);
  if (o.smoker) rows.push(['Raucher', o.smoker]);
  if (!rows.length) return '';
  return '<ul class="profile-details">' + rows.map(([k, v]) =>
    `<li><strong>${k}:</strong> ${escHtml(v)}</li>`).join('') + '</ul>';
}

const STOCK_PHOTO_PREFIXES = ['https://images.pexels.com/'];

function isStockPhotoUrl(url) {
  const s = String(url || '').trim();
  if (!s || s.startsWith('data:image/')) return false;
  if (s.startsWith('/api/media/')) return false;
  if (STOCK_PHOTO_PREFIXES.some((p) => s.startsWith(p))) return true;
  if (s.includes('images.pexels.com/')) return true;
  return false;
}

function isNearbySeedId(id) {
  return !!(id && String(id).startsWith('nearby_seed_'));
}

const NEARBY_DISCOVER_SEEDS = [
  { id: 'nearby_seed_lena', displayName: 'Lena', age: 26, gender: 'Frau', city: 'Ingolstadt', latOff: 0.045, lonOff: 0.02, photo: 'images/lena.jpg', bio: 'Kaffee, Spazieren, echte Gespräche.' },
  { id: 'nearby_seed_marco', displayName: 'Marco', age: 29, gender: 'Mann', city: 'Augsburg', latOff: -0.035, lonOff: 0.055, photo: 'images/marco.jpg', bio: 'Sport, Reisen, gutes Essen.' },
  { id: 'nearby_seed_amira', displayName: 'Amira', age: 25, gender: 'Frau', city: 'München', latOff: 0.06, lonOff: -0.04, photo: 'images/amira.jpg', bio: 'Musik, Kunst, neue Leute kennenlernen.' },
  { id: 'nearby_seed_sofia', displayName: 'Sofia', age: 27, gender: 'Frau', city: 'Nürnberg', latOff: -0.05, lonOff: -0.03, photo: 'images/sofia.jpg', bio: 'Yoga, Serien, Sonntagsbrunch.' },
  { id: 'nearby_seed_yuki', displayName: 'Yuki', age: 24, gender: 'Divers', city: 'Regensburg', latOff: 0.02, lonOff: 0.07, photo: 'images/yuki.jpg', bio: 'Gaming, Fotografie, offen für alles.' },
  { id: 'nearby_seed_felix', displayName: 'Felix', age: 31, gender: 'Mann', city: 'Würzburg', latOff: -0.04, lonOff: 0.025, photo: 'images/marco.jpg', bio: 'Wein, Wandern, entspannte Dates.' },
  { id: 'nearby_seed_jana', displayName: 'Jana', age: 23, gender: 'Frau', city: 'Passau', latOff: 0.055, lonOff: 0.045, photo: 'images/amira.jpg', bio: 'Donau, Konzerte, spontane Pläne.' },
  { id: 'nearby_seed_tom', displayName: 'Tom', age: 28, gender: 'Mann', city: 'Rosenheim', latOff: -0.02, lonOff: -0.055, photo: 'images/marco.jpg', bio: 'Berge, Grillen, ehrliche Gespräche.' },
  { id: 'nearby_seed_nina', displayName: 'Nina', age: 26, gender: 'Frau', city: 'Landshut', latOff: 0.035, lonOff: -0.02, photo: 'images/sofia.jpg', bio: 'Lesen, Kochen, Lachen ist Pflicht.' },
  { id: 'nearby_seed_luca', displayName: 'Luca', age: 30, gender: 'Mann', city: 'Bamberg', latOff: -0.06, lonOff: 0.04, photo: 'images/marco.jpg', bio: 'Craft Beer, Fußball, offen für Neues.' },
];

function stripStockPhotos(ph) {
  if (!ph) return ph;
  if (isStockPhotoUrl(ph.main)) ph.main = '';
  ph.gallery = (ph.gallery || []).map((u) => (isStockPhotoUrl(u) ? '' : u || ''));
  ph.locked = (ph.locked || []).map((u) => (isStockPhotoUrl(u) ? '' : u || ''));
  if (isStockPhotoUrl(ph.photo)) ph.photo = '';
  return ph;
}

function photoSlotsFreeGallery() {
  return config.photoSlotsFreeGallery ?? 4;
}
function photoSlotsMaxGallery() {
  return config.photoSlotsMaxGallery ?? 9;
}
function photoSlotGalleryCost() {
  return config.photoSlotGalleryCost ?? 20;
}
function photoSlotsFreeLocked() {
  return config.photoSlotsFreeLocked ?? 5;
}
function photoSlotsMaxLocked() {
  return config.photoSlotsMaxLocked ?? 10;
}
function photoSlotLockedCost() {
  return config.photoSlotLockedCost ?? 25;
}

let regPhotoDataUrl = '';

function freshUserPhotos(mainUrl) {
  const ug = photoSlotsFreeGallery();
  const ul = photoSlotsFreeLocked();
  const main = isStockPhotoUrl(mainUrl) ? '' : (mainUrl || '');
  return {
    main,
    gallery: Array(photoSlotsMaxGallery()).fill('').slice(0, ug),
    locked: Array(photoSlotsMaxLocked()).fill('').slice(0, ul),
    unlockedGallery: ug,
    unlockedLocked: ul,
    lockedCount: ul,
  };
}

function setRegPhotoPreview(dataUrl) {
  regPhotoDataUrl = dataUrl || '';
  const box = document.getElementById('regPhotoPreview');
  if (!box) return;
  box.innerHTML = '';
  if (dataUrl) {
    const img = document.createElement('img');
    img.src = dataUrl;
    img.alt = 'Dein Profilbild';
    box.appendChild(img);
  } else {
    box.innerHTML = '<span class="reg-photo-placeholder">📷 Foto wählen</span>';
  }
}

async function handleRegPhotoFile(input) {
  const file = input?.files?.[0];
  if (!file) return;
  try {
    toast('Profilbild wird vorbereitet…');
    const dataUrl = await compressImageForProfile(file);
    setRegPhotoPreview(dataUrl);
    input.value = '';
    toast('Profilbild bereit');
  } catch (e) {
    toast(e.message || 'Foto konnte nicht geladen werden');
  }
}

function ensurePhotoSlots(u) {
  if (!u) return null;
  if (!u.photos) {
    const main = isStockPhotoUrl(u.photo) ? '' : (u.photo || '');
    u.photos = freshUserPhotos(main);
    u.photo = u.photos.main;
  } else {
    stripStockPhotos(u.photos);
    if (isStockPhotoUrl(u.photo)) u.photo = u.photos.main || '';
    else if (u.photos.main) u.photo = u.photos.main;
  }
  const ph = u.photos;
  const maxG = photoSlotsMaxGallery();
  const maxL = photoSlotsMaxLocked();
  const ug = Math.min(
    Math.max(parseInt(ph.unlockedGallery, 10) || photoSlotsFreeGallery(), photoSlotsFreeGallery()),
    maxG
  );
  const ul = Math.min(
    Math.max(parseInt(ph.unlockedLocked, 10) || photoSlotsFreeLocked(), photoSlotsFreeLocked()),
    maxL
  );
  const gallery = [...(ph.gallery || []), ...Array(maxG)].slice(0, maxG).map((x) => x || '');
  const locked = [...(ph.locked || []), ...Array(maxL)].slice(0, maxL).map((x) => (x == null ? '' : x));
  ph.unlockedGallery = ug;
  ph.unlockedLocked = ul;
  ph.gallery = gallery;
  ph.locked = locked;
  ph.lockedCount = ul;
  return ph;
}

function ensurePhotos(u) {
  return ensurePhotoSlots(u);
}

function ensureDiscoverPhotos(u) {
  ensurePhotoSlots(u);
  const ph = u.photos;
  if (!ph) return u;
  if (!ph.main) {
    ph.main = (ph.gallery || []).find((url) => url && !isStockPhotoUrl(url)) || u.photo || '';
  }
  if (ph.main) u.photo = ph.main;
  return u;
}

function buildNearbySeedProfiles(u, f) {
  const passed = myPasses();
  const blocked = myBlocked();
  const liked = myLikes();
  const out = [];
  NEARBY_DISCOVER_SEEDS.forEach((seed) => {
    if (passed.includes(seed.id) || blocked.includes(seed.id) || liked.includes(seed.id)) return;
    if (seed.age < f.ageMin || seed.age > f.ageMax) return;
    if (!matchesSeeking(u, seed)) return;
    let km = null;
    if (u.lat != null) {
      const lat = u.lat + seed.latOff;
      const lon = u.lon + seed.lonOff;
      km = haversineKm(u.lat, u.lon, lat, lon);
      if (!f.everywhere && km > f.km) return;
    }
    out.push({
      o: {
        id: seed.id,
        displayName: seed.displayName,
        age: seed.age,
        gender: seed.gender,
        seeking: 'Alle',
        city: seed.city,
        country: 'DE',
        bio: seed.bio,
        photo: seed.photo,
        photos: { main: seed.photo, gallery: [], locked: [], unlockedGallery: 4, unlockedLocked: 5, lockedCount: 5 },
        verified: true,
        lat: u.lat != null ? u.lat + seed.latOff : null,
        lon: u.lon != null ? u.lon + seed.lonOff : null,
        nearbyPreview: true,
      },
      km,
    });
  });
  return out;
}

function friendKey(a, b) {
  return [a, b].sort().join('_');
}

function isFriendsWith(otherId) {
  const myId = useDb() ? JamiApi.myProfile?.id : state.session;
  if (!myId || !otherId || myId === otherId) return true;
  if (useDb()) {
    const o = findUser(otherId);
    return o?.friendshipStatus === 'friends';
  }
  return state.friendships.includes(friendKey(myId, otherId));
}

function localFriendStatus(otherId) {
  const myId = state.session;
  if (!myId || myId === otherId) return 'self';
  if (state.friendships.includes(friendKey(myId, otherId))) return 'friends';
  if (state.friendRequests.sent[otherId]) return 'pending_sent';
  if (state.friendRequests.incoming.some(x => x.fromId === otherId)) return 'pending_received';
  return 'none';
}

function photosUnlocked(o) {
  const fs = o.friendshipStatus || (useDb() ? 'none' : localFriendStatus(o.id));
  return fs === 'friends' || fs === 'self';
}

function buildPhotoGalleryHtml(o, compact) {
  ensurePhotoSlots(o);
  const fs = o.friendshipStatus || (useDb() ? 'none' : localFriendStatus(o.id));
  const unlock = photosUnlocked(o);
  const ph = o.photos;
  const galVisible = (ph.gallery || []).slice(0, ph.unlockedGallery ?? photoSlotsFreeGallery());
  const lockVisible = (ph.locked || []).slice(0, ph.unlockedLocked ?? photoSlotsFreeLocked());
  const mainImg = ph.main
    ? `<img src="${escHtml(ph.main)}" alt="${escHtml(o.displayName)}" class="photo-gallery-main-open photo-slot--open" data-photo-url="${escHtml(ph.main)}" role="button" tabindex="0" title="Foto vergrößern"/>`
    : `<div class="photo-gallery-main-empty" aria-label="Kein Profilfoto"></div>`;
  const slot = (url, locked, i) => {
    if (!url && locked) {
      return `<div class="photo-slot locked-blur empty"><span class="lock-badge">🔒</span></div>`;
    }
    if (!url) return `<div class="photo-slot empty"></div>`;
    const blur = locked && !unlock;
    const openCls = !blur && url ? ' photo-slot--open' : '';
    const openAttr = !blur && url
      ? ` data-photo-url="${escHtml(url)}" role="button" tabindex="0" title="Foto vergrößern"`
      : blur ? ' title="Privat – Freundschaft nötig"' : '';
    return `<div class="photo-slot${blur ? ' locked-blur' : ''}${openCls}"${openAttr}>${blur ? '<span class="lock-badge">🔒</span>' : ''}<img src="${escHtml(url)}" alt=""/></div>`;
  };
  const gal = galVisible.map((u, i) => slot(u, false, i)).join('');
  const lck = lockVisible.map((u, i) => slot(u || null, true, i)).join('');
  const presenceOverlay = profilePresenceOverlayHtml(o, fs === 'self');
  const mainCls =
    'photo-gallery-main' + (presenceOverlay ? ' photo-gallery-main-with-presence' : '');
  if (compact) {
    return `<div class="photo-gallery">
      <div class="${mainCls}">${mainImg}${presenceOverlay}</div>
      <div class="photo-gallery-row">${gal}</div>
      <div class="photo-gallery-locked">${lck}</div>
      ${!unlock ? '<p class="meta" style="padding:0.35rem 0.5rem">🔒 Private Fotos – Freundschaft anfragen & annehmen</p>' : ''}
    </div>`;
  }
  return `<div class="photo-gallery">
    <div class="${mainCls}">${mainImg}${presenceOverlay}</div>
    <div class="photo-gallery-row">${gal}</div>
    <p class="meta" style="margin:0.25rem 0.5rem 0">Private Fotos ${unlock ? '(freigeschaltet)' : '(gesperrt)'}</p>
    <div class="photo-gallery-locked">${lck}</div>
  </div>`;
}

function friendActionHtml(o) {
  const fs = o.friendshipStatus || (useDb() ? 'none' : localFriendStatus(o.id));
  const myId = useDb() ? JamiApi.myProfile?.id : state.session;
  if (!myId || o.id === myId) return '';
  if (isNearbySeedId(o.id)) {
    return '<p class="meta">Beispiel-Profil – Freundschaft nur mit echten Nutzern</p>';
  }
  if (fs === 'friends') return '<p class="meta">🤝 Freundschaft – alle Fotos sichtbar</p>';
  if (fs === 'pending_sent') return '<button type="button" class="btn btn-ghost btn-friend" disabled>Anfrage gesendet …</button>';
  if (fs === 'pending_received') {
    return `<div class="friend-request-actions">
      <button type="button" class="btn btn-primary btn-friend" data-accept-friend="${o.id}">Freundschaft annehmen</button>
      <button type="button" class="btn btn-ghost btn-friend" data-reject-friend="${o.id}">Ablehnen</button>
    </div>`;
  }
  return `<button type="button" class="btn btn-ghost btn-friend" data-send-friend="${o.id}">🤝 Freundschaft anfragen</button>`;
}

async function sendFriendRequest(toId) {
  if (isNearbySeedId(toId)) {
    toast('Beispiel-Profil – Freundschaft geht nur mit echten registrierten Nutzern.');
    return;
  }
  if (useDb()) {
    try {
      const j = await JamiApi.sendFriendRequest(toId);
      const o = findUser(toId) || resolveProfileUser(toId);
      if (o) {
        o.friendshipStatus = j.status === 'friends' || j.autoAccepted ? 'friends' : 'pending_sent';
        if (j.profile?.photos) o.photos = j.profile.photos;
      }
      toast(
        j.status === 'friends' || j.autoAccepted
          ? 'Freundschaft angenommen – private Fotos frei!'
          : 'Freundschaftsanfrage gesendet'
      );
      await renderFriendRequests();
      await renderSwipeDeck();
      refreshChatSessionProfiles(toId);
    } catch (e) { toast(e.message || 'Freundschaftsanfrage fehlgeschlagen'); }
    return;
  }
  state.friendRequests.sent[toId] = true;
  saveState();
  toast('Freundschaftsanfrage gesendet (lokal)');
  await renderSwipeDeck();
}

function refreshChatSessionProfiles(otherId) {
  if (!activeChatSessionId || activeChatSessionId !== otherId) return;
  const u = me();
  const other = resolveProfileUser(otherId);
  const panelSelf = document.getElementById('chatSessionMe');
  const panelPartner = document.getElementById('chatSessionOther');
  if (u && panelSelf) {
    panelSelf.innerHTML = buildSessionProfileHtml(u, true);
    bindSessionProfilePhotos(panelSelf);
  }
  if (other && panelPartner) {
    panelPartner.innerHTML = buildSessionProfileHtml(other, false);
    bindFriendButtons(panelPartner);
    bindSessionProfilePhotos(panelPartner);
  }
}

async function respondFriendRequest(fromId, accept) {
  if (useDb()) {
    try {
      const j = await JamiApi.respondFriendRequest(fromId, accept);
      const o = findUser(fromId);
      if (o) {
        o.friendshipStatus = accept ? 'friends' : 'none';
        if (j.profile?.photos) o.photos = j.profile.photos;
      }
      toast(accept ? 'Freundschaft angenommen – private Fotos frei!' : 'Anfrage abgelehnt');
      await renderFriendRequests();
      await renderSwipeDeck();
      refreshChatSessionProfiles(fromId);
      closeProfilePhotoModal();
    } catch (e) { toast(e.message || 'Fehler'); }
    return;
  }
  state.friendRequests.incoming = state.friendRequests.incoming.filter(x => x.fromId !== fromId);
  if (accept) {
    const k = friendKey(state.session, fromId);
    if (!state.friendships.includes(k)) state.friendships.push(k);
    const o = findUser(fromId);
    if (o) {
      o.friendshipStatus = 'friends';
      ensurePhotos(o);
    }
    toast('Freundschaft angenommen – private Fotos frei!');
  } else toast('Anfrage abgelehnt');
  saveState();
  await renderFriendRequests();
  await renderSwipeDeck();
  closeProfilePhotoModal();
}

function profileSafetyActionsHtml(o) {
  const myId = useDb() ? JamiApi.myProfile?.id : state.session;
  if (!myId || !o || o.id === myId) return '';
  return `<div class="session-actions-row card-actions-row">
    <button type="button" data-report="${escHtml(o.id)}">Melden</button>
    <button type="button" class="btn-block-user" data-block="${escHtml(o.id)}">Blockieren</button>
  </div>`;
}

function bindProfileSafetyButtons(root) {
  if (!root) return;
  root.querySelectorAll('[data-block]').forEach((btn) => {
    btn.onclick = (e) => {
      e.stopPropagation();
      const id = btn.dataset.block;
      const o = resolveProfileUser(id);
      blockUser(id, o?.displayName);
    };
  });
  root.querySelectorAll('[data-report]').forEach((btn) => {
    btn.onclick = (e) => {
      e.stopPropagation();
      reportUser(btn.dataset.report);
    };
  });
}

function bindFriendButtons(root) {
  if (!root) return;
  root.querySelectorAll('[data-send-friend]').forEach(btn => {
    btn.onclick = e => { e.stopPropagation(); sendFriendRequest(btn.dataset.sendFriend); };
  });
  root.querySelectorAll('[data-accept-friend]').forEach(btn => {
    btn.onclick = e => { e.stopPropagation(); respondFriendRequest(btn.dataset.acceptFriend, true); };
  });
  root.querySelectorAll('[data-reject-friend]').forEach(btn => {
    btn.onclick = e => { e.stopPropagation(); respondFriendRequest(btn.dataset.rejectFriend, false); };
  });
}

function bindPhotoGalleryClicks(root) {
  if (!root) return;
  root.querySelectorAll('.photo-slot--open[data-photo-url], .photo-gallery-main-open[data-photo-url]').forEach((el) => {
    const url = el.dataset.photoUrl;
    if (!url) return;
    const open = () => openChatPhotoLightbox(url);
    el.onclick = (e) => {
      e.stopPropagation();
      open();
    };
    el.onkeydown = (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        open();
      }
    };
  });
}

function bindSessionProfilePhotos(root) {
  bindPhotoGalleryClicks(root);
}

function openProfilePhotoModal(o) {
  ensurePhotos(o);
  const body = document.getElementById('profilePhotoModalBody');
  const modal = document.getElementById('profilePhotoModal');
  body.innerHTML = `<h2>${escHtml(o.displayName)}, ${o.age}</h2>${buildPhotoGalleryHtml(o, false)}${profileDetailsHtml(o)}${friendActionHtml(o)}${profileSafetyActionsHtml(o)}`;
  bindFriendButtons(body);
  bindPhotoGalleryClicks(body);
  bindProfileSafetyButtons(body);
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
}

function closeProfilePhotoModal() {
  const modal = document.getElementById('profilePhotoModal');
  if (modal) {
    modal.classList.add('hidden');
    modal.setAttribute('aria-hidden', 'true');
  }
}

function resolveProfileUser(otherId) {
  let o = findUser(otherId);
  if (!o && swipeQueue.length) {
    const item = swipeQueue.find(x => x.o && x.o.id === otherId);
    if (item) o = item.o;
  }
  if (!o && useDb() && JamiApi.myProfile && otherId === JamiApi.myProfile.id) o = JamiApi.myProfile;
  return o;
}

function buildSessionPhotoGrid(o) {
  ensurePhotoSlots(o);
  const unlock = photosUnlocked(o);
  const ph = o.photos;
  const galVisible = (ph.gallery || []).slice(0, ph.unlockedGallery ?? photoSlotsFreeGallery());
  const lockVisible = (ph.locked || []).slice(0, ph.unlockedLocked ?? photoSlotsFreeLocked());
  const slot = (url, locked) => {
    if (!url && locked) {
      return `<div class="session-photo-slot locked-blur empty"><span class="lock-badge">🔒</span></div>`;
    }
    if (!url) return '';
    const blur = locked && !unlock;
    const openCls = !blur ? ' session-photo-slot--open' : '';
    const openAttr = !blur
      ? ` data-photo-url="${escHtml(url)}" role="button" tabindex="0" title="Foto vergrößern"`
      : ' title="Privat – Freundschaft nötig"';
    return `<div class="session-photo-slot${blur ? ' locked-blur' : ''}${openCls}"${openAttr}>${blur ? '<span class="lock-badge">🔒</span>' : ''}<img src="${escHtml(url)}" alt="" loading="lazy"/></div>`;
  };
  const parts = [
    slot(ph.main, false),
    ...galVisible.map((u) => slot(u, false)),
    ...lockVisible.map((u) => slot(u || null, true)),
  ].filter(Boolean);
  if (!parts.length) return '';
  const hint = unlock
    ? ''
    : '<p class="meta session-photo-hint">🔒 Private Fotos verpixelt – Freundschaft freischalten</p>';
  return `<div class="session-photo-grid">${parts.join('')}</div>${hint}`;
}

function buildSessionPhotoStrip(o) {
  return buildSessionPhotoGrid(o);
}

function presenceOnlineMs() {
  return (config.presenceOnlineMinutes || 5) * 60 * 1000;
}

function presenceRecentMs() {
  return (config.presenceRecentMinutes || 30) * 60 * 1000;
}

function presenceWeekMs() {
  return (config.presenceWeekDays || 7) * 24 * 60 * 60 * 1000;
}

function presenceFromAgoClient(agoMs) {
  if (agoMs <= presenceOnlineMs()) {
    return { online: true, presenceText: 'Gerade online', presenceTier: 'online' };
  }
  if (agoMs <= presenceRecentMs()) {
    return { online: false, presenceText: 'Kürzlich aktiv', presenceTier: 'recent' };
  }
  const mins = Math.floor(agoMs / 60000);
  if (mins < 60) {
    const presenceText = mins === 1 ? 'Aktiv vor 1 Minute' : 'Aktiv vor ' + mins + ' Minuten';
    return { online: false, presenceText, presenceTier: 'today' };
  }
  const hours = Math.floor(mins / 60);
  if (hours < 24) {
    const presenceText = hours === 1 ? 'Aktiv vor 1 Stunde' : 'Aktiv vor ' + hours + ' Stunden';
    return { online: false, presenceText, presenceTier: 'today' };
  }
  const days = Math.floor(hours / 24);
  if (days === 1) return { online: false, presenceText: 'Aktiv gestern', presenceTier: 'week' };
  if (agoMs < presenceWeekMs()) {
    return { online: false, presenceText: 'Aktiv diese Woche', presenceTier: 'week' };
  }
  if (days < 30) {
    return { online: false, presenceText: 'Zuletzt vor ' + days + ' Tagen aktiv', presenceTier: 'stale' };
  }
  return { online: false, presenceText: 'Länger nicht aktiv', presenceTier: 'stale' };
}

function isRecentlyActivePresenceText(text) {
  if (!text) return false;
  if (text === 'Gerade online' || text === 'Kürzlich aktiv') return true;
  if (text === 'Aktiv gestern' || text === 'Aktiv diese Woche') return true;
  if (text.startsWith('Aktiv vor ')) return true;
  return false;
}

function presenceBadgeClass(p) {
  if (p.online) return 'presence-online';
  const tier = p.presenceTier;
  if (tier === 'recent') return 'presence-recent';
  if (tier === 'today') return 'presence-today';
  if (tier === 'week') return 'presence-week';
  if (tier === 'stale') return 'presence-stale';
  if (tier === 'unknown') return 'presence-unknown';
  return 'presence-offline';
}

function computePresenceClient(u) {
  if (!u) return { online: false, presenceText: 'Keine Aktivität bekannt', presenceTier: 'unknown' };
  if (u.presenceText && (u.online === true || u.online === false)) {
    let tier = u.presenceTier;
    if (!tier) {
      if (u.online) tier = 'online';
      else if (u.presenceText === 'Kürzlich aktiv') tier = 'recent';
      else if (u.presenceText.startsWith('Aktiv vor ')) tier = 'today';
      else if (u.presenceText === 'Aktiv gestern' || u.presenceText === 'Aktiv diese Woche') tier = 'week';
      else if (u.presenceText === 'Länger nicht aktiv' || u.presenceText.startsWith('Zuletzt vor ')) tier = 'stale';
      else tier = 'unknown';
    }
    return { online: !!u.online, presenceText: u.presenceText, presenceTier: tier, lastActiveAt: u.lastActiveAt };
  }
  const at = u.lastActiveAt;
  if (!at) return { online: false, presenceText: 'Keine Aktivität bekannt', presenceTier: 'unknown' };
  const s = String(at);
  const ms = typeof at === 'number' ? at : Date.parse(s.includes('T') ? s : s.replace(' ', 'T') + (s.includes('Z') ? '' : 'Z'));
  if (!Number.isFinite(ms)) return { online: false, presenceText: 'Keine Aktivität bekannt', presenceTier: 'unknown' };
  return { lastActiveAt: at, ...presenceFromAgoClient(Date.now() - ms) };
}

function presenceBadgeHtml(u, isMe) {
  if (isMe) return '';
  const p = computePresenceClient(u);
  const cls = presenceBadgeClass(p);
  return `<span class="presence-badge ${cls}"><span class="presence-dot" aria-hidden="true"></span>${escHtml(p.presenceText)}</span>`;
}

function activityBadgeConfig() {
  return config.activityBadges || {};
}

function computeLocalReplyStats(profileId) {
  const cfg = activityBadgeConfig();
  const minSamples = cfg.fastReplyMinSamples || 3;
  const deltas = [];
  Object.keys(state.chats || {}).forEach((key) => {
    if (!key.includes(profileId)) return;
    const msgs = state.chats[key].msgs || [];
    for (let i = 1; i < msgs.length; i++) {
      if (msgs[i].from === profileId && msgs[i - 1].from !== profileId) {
        const prev = msgs[i - 1].at;
        const cur = msgs[i].at;
        if (prev != null && cur != null && cur > prev) deltas.push((cur - prev) / 60000);
      }
    }
  });
  if (deltas.length < minSamples) return { medianMinutes: null, samples: deltas.length };
  deltas.sort((a, b) => a - b);
  return { medianMinutes: deltas[Math.floor(deltas.length / 2)], samples: deltas.length };
}

function computeActivityBadgesClient(u) {
  if (!u) return [];
  if (Array.isArray(u.activityBadges) && u.activityBadges.length) return u.activityBadges;
  const cfg = activityBadgeConfig();
  const fastMax = cfg.fastReplyMaxMinutes || 20;
  const minMsgs = cfg.veryActiveMinMsgs7d || 2;
  const badges = [];
  const p = computePresenceClient(u);
  let msgs7d = 0;
  const weekAgo = Date.now() - 7 * 24 * 3600000;
  Object.keys(state.chats || {}).forEach((key) => {
    if (!key.includes(u.id)) return;
    (state.chats[key].msgs || []).forEach((m) => {
      if (m.from === u.id && m.at >= weekAgo) msgs7d += 1;
    });
  });
  const activeMs = u.lastActiveAt != null ? (typeof u.lastActiveAt === 'number' ? u.lastActiveAt : Date.parse(u.lastActiveAt)) : null;
  const within24h = activeMs != null && Number.isFinite(activeMs) && Date.now() - activeMs < (cfg.veryActiveHours || 24) * 3600000;
  if (p.online || isRecentlyActivePresenceText(p.presenceText) || (within24h && msgs7d >= minMsgs)) {
    badges.push({ id: 'very_active', label: 'Sehr aktiv', icon: '🔥' });
  }
  const reply = computeLocalReplyStats(u.id);
  if (reply.medianMinutes != null && reply.medianMinutes <= fastMax) {
    badges.push({ id: 'fast_replier', label: 'Antwortet besonders schnell', icon: '⚡' });
  }
  u.activityBadges = badges;
  return badges;
}

function activityBadgesHtml(u) {
  const badges = computeActivityBadgesClient(u);
  if (!badges.length) return '';
  return (
    '<div class="activity-badges" role="list">' +
    badges
      .map(
        (b) =>
          '<span class="activity-badge activity-badge-' +
          escHtml(b.id || '') +
          '" role="listitem">' +
          escHtml((b.icon || '') + ' ' + (b.label || '')) +
          '</span>'
      )
      .join('') +
    '</div>'
  );
}

function touchLocalPresence(userId) {
  const t = Date.now();
  if (useDb() && JamiApi.myProfile && JamiApi.myProfile.id === userId) {
    JamiApi.myProfile.lastActiveAt = new Date(t).toISOString();
    JamiApi.myProfile.online = true;
    JamiApi.myProfile.presenceText = 'Gerade online';
    return;
  }
  const u = state.users.find((x) => x.id === userId);
  if (u) u.lastActiveAt = t;
}

function stopPresenceHeartbeat() {
  if (presenceHeartbeatTimer) {
    clearInterval(presenceHeartbeatTimer);
    presenceHeartbeatTimer = null;
  }
  stopChatPresenceRefresh();
}

function stopChatPresenceRefresh() {
  if (chatPresenceTimer) {
    clearInterval(chatPresenceTimer);
    chatPresenceTimer = null;
  }
}

async function pingPresence() {
  if (JamiApi.mode === 'cloudflare') {
    try {
      await JamiApi.sendPresenceHeartbeat();
    } catch (e) {}
    return;
  }
  const u = me();
  if (u) touchLocalPresence(u.id);
}

function startPresenceHeartbeat() {
  stopPresenceHeartbeat();
  if (!useDb()) {
    const u = me();
    if (u) touchLocalPresence(u.id);
    const sec = config.presenceHeartbeatSec || 45;
    presenceHeartbeatTimer = setInterval(() => touchLocalPresence(u?.id), sec * 1000);
    return;
  }
  pingPresence();
  const sec = config.presenceHeartbeatSec || 45;
  presenceHeartbeatTimer = setInterval(() => pingPresence(), sec * 1000);
  if (!presenceVisibilityBound) {
    presenceVisibilityBound = true;
    document.addEventListener('visibilitychange', onPresenceVisibility);
  }
}

function onPresenceVisibility() {
  if (document.visibilityState === 'visible') pingPresence();
}

function renderChatSessionStatus(other, otherId) {
  const status = document.getElementById('chatSessionStatus');
  if (!status || !other) return;
  const km = swipeQueue.find((x) => x.o && x.o.id === otherId)?.km;
  status.innerHTML =
    (km != null ? `<span>📍 ${km.toFixed(1)} km</span>` : '') +
    activityBadgesHtml(other) +
    presenceBadgeHtml(other, false) +
    `<span>${escHtml(other.city || '')}</span>` +
    (other.relationshipGoal ? `<span>${escHtml(other.relationshipGoal)}</span>` : '');
}

async function refreshChatPartnerPresence(otherId) {
  if (!otherId || !activeChatSessionId || activeChatSessionId !== otherId) return;
  let other = resolveProfileUser(otherId);
  if (JamiApi.mode === 'cloudflare') {
    try {
      const p = await JamiApi.fetchProfileCloud(otherId);
      if (p) {
        cacheProfile(p);
        other = p;
      }
    } catch (e) {}
  }
  if (other) renderChatSessionStatus(other, otherId);
}

function startChatPresenceRefresh(otherId) {
  stopChatPresenceRefresh();
  chatPresenceTimer = setInterval(() => refreshChatPartnerPresence(otherId), 30000);
}

function chatKeyLocal(otherId) {
  return [state.session, otherId].sort().join('_');
}

function ensureLocalChatMeta(key) {
  if (!state.chats[key]) state.chats[key] = { msgs: [], msgCount: 0 };
  const c = state.chats[key];
  if (c.otherTypingUntil == null) c.otherTypingUntil = 0;
  if (c.otherReadAt == null) c.otherReadAt = 0;
  return c;
}

async function signalChatTyping(otherId, typing) {
  if (!activeChatSessionId || activeChatSessionId !== otherId) return;
  const now = Date.now();
  if (typing) {
    if (now - lastTypingPing < 1800) return;
    lastTypingPing = now;
  }
  if (JamiApi.mode === 'cloudflare') {
    try {
      await JamiApi.sendChatTypingCloud(otherId, typing);
    } catch (e) {}
  }
}

function chatNeedsCoinsFor(otherId) {
  const cost = messageCostForChat(otherId);
  return cost > 0 && (me()?.coins || 0) < cost;
}

function wireChatNoCoinsGuard(otherId, inputEl) {
  if (!inputEl || !otherId) return;
  const onWriteAttempt = async (e) => {
    if (!chatNeedsCoinsFor(otherId)) return;
    if (e?.type === 'keydown') {
      const skip =
        e.key === 'Enter' ||
        e.key === 'Tab' ||
        e.key === 'Escape' ||
        e.ctrlKey ||
        e.metaKey ||
        e.altKey ||
        e.key.startsWith('Arrow') ||
        e.key === 'Shift' ||
        e.key === 'Control';
      if (skip) return;
      if (e.key.length !== 1 && e.key !== 'Backspace' && e.key !== 'Delete') return;
      e.preventDefault();
    }
    await handleInsufficientCoins();
  };
  inputEl.addEventListener('keydown', onWriteAttempt);
  inputEl.addEventListener('paste', onWriteAttempt);
}

function wireChatTypingInput(otherId, inputEl) {
  if (!inputEl) return;
  inputEl.oninput = () => {
    if (chatNeedsCoinsFor(otherId)) return;
    signalChatTyping(otherId, true);
    clearTimeout(chatTypingStopTimer);
    chatTypingStopTimer = setTimeout(() => signalChatTyping(otherId, false), 2200);
  };
  inputEl.onblur = () => signalChatTyping(otherId, false);
}

async function pollChatLiveStatus(otherId) {
  if (activeChatSessionId !== otherId) return;
  const other = resolveProfileUser(otherId);
  if (JamiApi.mode === 'cloudflare') {
    try {
      const j = await JamiApi.fetchChatStatusCloud(otherId);
      const st = j.chatStatus || {};
      renderChatTypingIndicator(other, !!st.otherTyping);
      applyReadStateToChatBox(document.getElementById('chatSessionMessages'), st.otherReadAt);
    } catch (e) {}
    return;
  }
  const key = chatKeyLocal(otherId);
  const chat = state.chats[key];
  if (!chat) {
    renderChatTypingIndicator(other, false);
    return;
  }
  const now = Date.now();
  renderChatTypingIndicator(other, !!(chat.otherTypingUntil && chat.otherTypingUntil > now));
  applyReadStateToChatBox(document.getElementById('chatSessionMessages'), chat.otherReadAt);
}

function stopChatStatusPoll() {
  if (chatStatusTimer) {
    clearInterval(chatStatusTimer);
    chatStatusTimer = null;
  }
  clearTimeout(chatTypingStopTimer);
  chatTypingStopTimer = null;
}

function startChatStatusPoll(otherId) {
  stopChatStatusPoll();
  const sec = config.chatStatusPollSec || 2.5;
  pollChatLiveStatus(otherId);
  chatStatusTimer = setInterval(() => pollChatLiveStatus(otherId), sec * 1000);
}

async function markChatReadFor(otherId) {
  if (JamiApi.mode === 'cloudflare') {
    try {
      const j = await JamiApi.markChatReadCloud(otherId);
      applyReadStateToChatBox(document.getElementById('chatSessionMessages'), j.chatStatus?.otherReadAt);
    } catch (e) {}
    return;
  }
  const key = chatKeyLocal(otherId);
  ensureLocalChatMeta(key);
}

function buildSessionProfileHtml(o, isMe, opts) {
  opts = opts || {};
  const compact = opts.compact !== false;
  ensurePhotos(o);
  const bio = stripContactInfo(o.bio || '');
  const panelTag = isMe
    ? '<p class="session-panel-tag session-panel-tag-me">Dein Profil · rechts</p>'
    : '<p class="session-panel-tag">Chat-Partner · links</p>';
  const genderIcon = o.gender === 'Frau' ? '♀' : o.gender === 'Mann' ? '♂' : '⚧';
  const cardClass = compact
    ? 'session-profile-card session-profile-card--compact'
    : 'session-profile-card';
  const nameSize = compact ? '0.78rem' : '1rem';
  return `
    <div class="${cardClass}">
      ${panelTag}
      <div class="session-profile-header">
        ${avatarBlockHtml(o, isMe, compact ? 'avatar-compact' : '')}
        <div>
          <h3 style="margin:0;font-size:${nameSize}">${escHtml(o.displayName)}${vipGoldBadge(o)}${profileDisplayEmojiHtml(o)} (${o.age}) ${genderIcon}${verifiedBadge(o)}</h3>
          <p class="meta" style="margin:0.1rem 0 0">🌍 ${escHtml(o.city || '–')}${o.country ? ', ' + escHtml(o.country) : ''}</p>
          ${activityBadgesHtml(o)}
          ${presenceBadgeHtml(o, isMe)}
        </div>
      </div>
      ${buildSessionPhotoGrid(o)}
      <div class="session-info-section">
        <h4>Profil</h4>
        ${profileDetailsHtml(o) || '<p class="meta" style="margin:0">Noch keine Details</p>'}
        ${bio ? `<p class="session-profile-bio"><strong>Bio:</strong> ${escHtml(bio)}</p>` : ''}
      </div>
      ${isMe ? '' : friendActionHtml(o)}
      ${isMe ? '' : profileSafetyActionsHtml(o)}
    </div>`;
}

function updateChatProfileBar(other) {
  const bar = document.getElementById('chatSessionProfileBar');
  const av = document.getElementById('chatSessionProfileAvatar');
  const name = document.getElementById('chatSessionProfileName');
  const meta = document.getElementById('chatSessionProfileMeta');
  const btn = document.getElementById('btnViewChatProfile');
  if (!other) {
    bar?.classList.add('hidden');
    btn?.classList.add('hidden');
    return;
  }
  if (av) av.innerHTML = avatarHtml(other);
  if (name) name.textContent = other.displayName + ', ' + other.age;
  if (meta) {
    const bits = [other.city, other.country].filter(Boolean);
    meta.textContent = bits.length ? bits.join(', ') : 'Profil ansehen';
  }
  bar?.classList.remove('hidden');
  btn?.classList.remove('hidden');
}

function updateChatProfileTabButtons() {
  document.getElementById('btnChatProfilePartner')?.classList.toggle('active', chatProfileSheetView === 'partner');
  document.getElementById('btnChatProfileMe')?.classList.toggle('active', chatProfileSheetView === 'me');
}

function renderChatProfileSheetBody() {
  const body = document.getElementById('chatProfileSheetBody');
  if (!body) return;
  const u = me();
  const other = activeChatPartner || resolveProfileUser(activeChatSessionId);
  if (chatProfileSheetView === 'me') {
    if (!u) {
      body.innerHTML = '<p class="meta">Profil nicht verfügbar</p>';
      return;
    }
    body.innerHTML = buildSessionProfileHtml(u, true, { compact: false });
    bindSessionProfilePhotos(body);
    return;
  }
  if (!other) {
    body.innerHTML = '<p class="meta">Profil nicht verfügbar</p>';
    return;
  }
  body.innerHTML = buildSessionProfileHtml(other, false, { compact: false });
  bindFriendButtons(body);
  bindSessionProfilePhotos(body);
  bindProfileSafetyButtons(body);
}

function openChatProfileSheet(view) {
  if (!activeChatSessionId) return;
  chatProfileSheetView = view === 'me' ? 'me' : 'partner';
  const sheet = document.getElementById('chatProfileSheet');
  if (!sheet) return;
  renderChatProfileSheetBody();
  updateChatProfileTabButtons();
  sheet.classList.remove('hidden');
  sheet.setAttribute('aria-hidden', 'false');
  document.body.classList.add('chat-profile-sheet-open');
}

function closeChatProfileSheet() {
  const sheet = document.getElementById('chatProfileSheet');
  if (!sheet) return;
  sheet.classList.add('hidden');
  sheet.setAttribute('aria-hidden', 'true');
  document.body.classList.remove('chat-profile-sheet-open');
}

function bindChatProfilePanelOpen(root, view) {
  if (!root) return;
  root.querySelector('.session-profile-card')?.addEventListener('click', (e) => {
    if (e.target.closest('button,a,input,textarea,select,[data-photo-url]')) return;
    openChatProfileSheet(view);
  });
}

function matchFreeMessageCount() {
  return config.matchFreeMessages ?? 2;
}

function setThreadChatMeta(otherId, meta) {
  threadChatMeta[otherId] = Object.assign({}, threadChatMeta[otherId], meta);
}

function getThreadMsgCount(otherId) {
  if (threadChatMeta[otherId]?.msgCount != null) return threadChatMeta[otherId].msgCount;
  const key = chatKeyLocal(otherId);
  const chat = state.chats[key];
  if (!chat) return 0;
  return chat.msgCount ?? chat.msgs?.length ?? 0;
}

function messageCostForChat(otherId) {
  if (!otherId || !isMutual(otherId)) return messageCost();
  const sent = getThreadMsgCount(otherId);
  if (sent < matchFreeMessageCount()) return 0;
  return messageCost();
}

function updateChatSendCostBadge(otherId) {
  const el = document.getElementById('chatSessionSendCost');
  if (!el) return;
  const cost = otherId ? messageCostForChat(otherId) : messageCost();
  if (cost <= 0) {
    el.textContent = 'Gratis';
    el.className = 'chat-send-cost chat-send-cost-free';
    el.title = 'Kostenlose Match-Nachricht';
  } else {
    el.textContent = '-' + cost + ' 🪙';
    el.className = 'chat-send-cost';
    el.title = cost + ' Coins pro Nachricht';
  }
}

function updateChatCostHint(elId, otherId) {
  updateChatSendCostBadge(otherId);
  const el = document.getElementById(elId);
  if (!el) return;
  const base = messageCost();
  let text;
  if (otherId && isMutual(otherId)) {
    const left =
      threadChatMeta[otherId]?.freeMessagesLeft ??
      Math.max(0, matchFreeMessageCount() - getThreadMsgCount(otherId));
    if (left > 0) {
      text =
        '💕 Match: noch ' +
        left +
        ' kostenlose Nachricht' +
        (left === 1 ? '' : 'en') +
        ' · danach je ' +
        base +
        ' Coins';
    } else {
      text = 'Match-Chat · je ' + base + ' Coins · 📷 Fotos · Kontakt maskiert';
    }
  } else {
    text = 'Text & Foto: je ' + base + ' Coins · 📷 Bilder · Nummer/E-Mail maskiert';
  }
  el.textContent = text;
}

async function ensureChatThread(otherId) {
  if (!useDb()) {
    const key = chatKeyLocal(otherId);
    ensureLocalChatMeta(key);
    saveState();
    return;
  }
  await JamiApi.ensureThread(otherId);
}

async function loadMessagesIntoBox(otherId, box) {
  if (!box) return;
  box.innerHTML = '';
  const myId = String(useDb() ? JamiApi.myProfile.id : state.session);
  let otherReadAt = null;
  if (JamiApi.mode === 'cloudflare') {
    const data = await JamiApi.fetchMessagesCloud(otherId);
    const messages = data.messages;
    const chatStatus = data.chatStatus;
    setThreadChatMeta(otherId, {
      msgCount: data.msgCount ?? data.thread?.msg_count ?? 0,
      isMutualMatch: data.isMutualMatch,
      freeMessagesLeft: data.freeMessagesLeft,
    });
    otherReadAt = chatStatus?.otherReadAt;
    messages.forEach((m) => {
      const isMe = String(m.from_profile) === myId;
      appendChatMessageEl(box, m.body, isMe ? 'me' : 'them', {
        at: m.at,
        read: isMe ? !!m.read : false,
      });
    });
  } else if (useDb()) {
    const { messages } = await JamiApi.fetchMessages(otherId);
    messages.forEach((m) => {
      appendChatMessageEl(box, m.body, String(m.from_profile) === myId ? 'me' : 'them');
    });
  } else {
    const key = chatKeyLocal(otherId);
    const chat = state.chats[key];
    if (chat) {
      otherReadAt = chat.otherReadAt;
      const sent = chat.msgCount ?? chat.msgs?.length ?? 0;
      setThreadChatMeta(otherId, {
        msgCount: sent,
        freeMessagesLeft: isMutual(otherId) ? Math.max(0, matchFreeMessageCount() - sent) : 0,
      });
      chat.msgs.forEach((m) => {
        const isMe = String(m.from) === myId;
        const read =
          isMe && otherReadAt && (m.at || 0) <= (typeof otherReadAt === 'number' ? otherReadAt : parseMsgTimeMs(otherReadAt));
        appendChatMessageEl(box, m.text, isMe ? 'me' : 'them', { at: m.at, read: !!read });
      });
    }
  }
  if (otherReadAt) applyReadStateToChatBox(box, otherReadAt);
  box.scrollTop = box.scrollHeight;
  if (!useDb() || JamiApi.mode !== 'cloudflare') setLocalChatRead(otherId);
  refreshInboxBadges().catch(() => {});
}

function closeChatSession() {
  const prev = activeChatSessionId;
  if (prev) signalChatTyping(prev, false);
  activeChatSessionId = null;
  activeChatPartner = null;
  closeChatProfileSheet();
  stopChatPresenceRefresh();
  stopChatStatusPoll();
  renderChatTypingIndicator(null, false);
  const blockBtn = document.getElementById('btnBlockChatPartner');
  if (blockBtn) {
    blockBtn.classList.add('hidden');
    blockBtn.onclick = null;
  }
  document.getElementById('btnViewChatProfile')?.classList.add('hidden');
  document.getElementById('chatSessionProfileBar')?.classList.add('hidden');
  const overlay = document.getElementById('chatSessionOverlay');
  if (!overlay) return;
  overlay.setAttribute('aria-hidden', 'true');
  document.body.classList.remove('chat-session-open');
  unwireChatComposeViewportDock();
  overlay.classList.add('hidden');
}

async function openChatSession(otherId, otherHint) {
  const u = me();
  if (!u) {
    toast('Bitte einloggen');
    return;
  }
  if (myBlocked().includes(otherId)) {
    toast('Nutzer ist blockiert');
    return;
  }
  const overlay = document.getElementById('chatSessionOverlay');
  const panelPartner = document.getElementById('chatSessionOther');
  const panelSelf = document.getElementById('chatSessionMe');
  const box = document.getElementById('chatSessionMessages');
  const input = document.getElementById('chatSessionInput');
  const head = document.getElementById('chatSessionHead');
  if (!overlay || !panelPartner || !panelSelf || !box || !input) {
    toast('Chat-Fenster fehlt – Seite neu laden (Strg+F5) oder neu deployen');
    console.error('chatSessionOverlay fehlt im HTML');
    return;
  }

  hideMatchModal();
  activeChatSessionId = otherId;
  markMatchSeen(otherId);
  overlay.classList.remove('hidden');
  overlay.setAttribute('aria-hidden', 'false');
  document.body.classList.add('chat-session-open');
  wireChatComposeViewportDock();
  box.innerHTML = jamiLoaderHtml('Nachrichten laden…', true);
  input.value = '';
  panelSelf.innerHTML = buildSessionProfileHtml(u, true);
  bindSessionProfilePhotos(panelSelf);
  bindChatProfilePanelOpen(panelSelf, 'me');
  panelPartner.innerHTML = jamiLoaderHtml('Profil laden…', true);
  requestAnimationFrame(() => overlay.scrollIntoView?.({ block: 'nearest' }));
  if (head) head.textContent = '💬 Chat';

  const send = () =>
    sendMessage(otherId, {
      inputId: 'chatSessionInput',
      messagesBoxId: 'chatSessionMessages',
      costHintId: 'chatSessionCostHint',
    });
  document.getElementById('btnSessionSend').onclick = send;
  input.onkeydown = (e) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      send();
    }
  };
  wireChatTypingInput(otherId, input);
  wireChatNoCoinsGuard(otherId, input);
  initEmojiPicker('btnSessionEmoji', 'sessionEmojiPicker', 'chatSessionInput');
  wireChatPhotoSend('btnSessionPhoto', 'chatSessionPhotoInput', () => activeChatSessionId, {
    inputId: 'chatSessionInput',
    messagesBoxId: 'chatSessionMessages',
    costHintId: 'chatSessionCostHint',
  });

  let other = otherHint || resolveProfileUser(otherId);
  if (!other && JamiApi.mode === 'cloudflare') {
    try {
      other = await JamiApi.fetchProfileCloud(otherId);
    } catch (e) {}
  }
  if (!other) {
    toast('Profil nicht gefunden');
    activeChatPartner = null;
    updateChatProfileBar(null);
    panelPartner.innerHTML = '<p class="meta">Profil nicht verfügbar</p>';
    box.innerHTML = '<p class="meta">Schreibfeld unten – Profil konnte nicht geladen werden.</p>';
    updateChatCostHint('chatSessionCostHint', otherId);
    input.focus();
    return;
  }
  cacheProfile(other);
  activeChatPartner = other;
  if (!useDb() && !other.friendshipStatus) other.friendshipStatus = localFriendStatus(otherId);
  panelPartner.innerHTML = buildSessionProfileHtml(other, false);
  bindFriendButtons(panelPartner);
  bindSessionProfilePhotos(panelPartner);
  bindProfileSafetyButtons(panelPartner);
  bindChatProfilePanelOpen(panelPartner, 'partner');
  updateChatProfileBar(other);
  const blockBtn = document.getElementById('btnBlockChatPartner');
  if (blockBtn) {
    blockBtn.classList.remove('hidden');
    blockBtn.onclick = () => blockUser(otherId, other.displayName);
  }
  if (head) head.textContent = '💬 Chat mit ' + other.displayName;
  renderChatSessionStatus(other, otherId);
  startChatPresenceRefresh(otherId);
  startChatStatusPoll(otherId);

  try {
    await ensureChatThread(otherId);
  } catch (e) {
    toast(e.message || 'Chat konnte nicht gestartet werden');
  }
  try {
    await loadMessagesIntoBox(otherId, box);
    await pollChatLiveStatus(otherId);
  } catch (e) {
    box.innerHTML = '<p class="meta">Verlauf konnte nicht geladen werden – trotzdem senden möglich.</p>';
  }
  updateChatCostHint('chatSessionCostHint', otherId);
  setTimeout(() => input.focus(), 120);
}

function collectPhotosFromForm() {
  const u = me();
  const ph = ensurePhotoSlots(u);
  const main = document.getElementById('photoMain')?.value.trim() || '';
  const ug = ph.unlockedGallery;
  const ul = ph.unlockedLocked;
  const gallery = Array(photoSlotsMaxGallery()).fill('');
  const locked = Array(photoSlotsMaxLocked()).fill('');
  for (let i = 0; i < ug; i++) {
    gallery[i] = document.getElementById('photoGal' + i)?.value.trim() || '';
  }
  for (let i = 0; i < ul; i++) {
    locked[i] = document.getElementById('photoLock' + i)?.value.trim() || '';
  }
  return {
    main,
    gallery,
    locked,
    unlockedGallery: ug,
    unlockedLocked: ul,
    lockedCount: ul,
  };
}

const PHOTO_FILE_ACCEPT = 'image/jpeg,image/png,image/webp,image/heic,image/heif,image/*';

function photoUploadFieldHtml(id, label, value) {
  const fileId = 'photoFile_' + id;
  return (
    '<label class="photo-field-label">' +
    escHtml(label) +
    '</label><div class="photo-upload-row"><input id="' +
    id +
    '" value="' +
    escHtml(value || '') +
    '" placeholder="Noch kein Foto – Galerie unten" readonly tabindex="-1"/>' +
    '<label class="btn btn-ghost btn-photo-pick" for="' +
    fileId +
    '">📷 Galerie / Kamera</label>' +
    '<input type="file" id="' +
    fileId +
    '" class="photo-file-input photo-file-input-hidden" accept="' +
    PHOTO_FILE_ACCEPT +
    '" data-photo-input="' +
    id +
    '"/></div>'
  );
}

function photoSlotMetaFromInputId(inputId) {
  if (inputId === 'photoMain') return { slot: 'main', index: 0 };
  const gal = /^photoGal(\d+)$/.exec(inputId || '');
  if (gal) return { slot: 'gallery', index: parseInt(gal[1], 10) || 0 };
  const lock = /^photoLock(\d+)$/.exec(inputId || '');
  if (lock) return { slot: 'locked', index: parseInt(lock[1], 10) || 0 };
  return { slot: 'main', index: 0 };
}

async function handleProfilePhotoFile(input) {
  const targetId = input?.dataset?.photoInput;
  const target = targetId ? document.getElementById(targetId) : null;
  const file = input?.files?.[0];
  if (!target || !file) return;
  try {
    toast('Foto wird gespeichert…');
    const dataUrl = await compressImageForProfile(file);
    let url = dataUrl;
    if (useDb() && JamiApi.mode === 'cloudflare') {
      const meta = photoSlotMetaFromInputId(targetId);
      url = await JamiApi.uploadPhoto(dataUrl, meta);
      const u = me();
      if (u && JamiApi.myProfile) {
        u.photos = JamiApi.myProfile.photos;
        u.photo = JamiApi.myProfile.photo || url;
      }
    } else {
      target.value = url || dataUrl;
    }
    input.value = '';
    renderProfile();
    renderProfileCompleteness();
    await renderSwipeDeck().catch(() => {});
    toast('Foto gespeichert ✓');
  } catch (e) {
    toast(e.message || 'Foto konnte nicht gespeichert werden');
  }
}

function wirePhotoFileInputs() {
  document.querySelectorAll('.photo-file-input[data-photo-input]').forEach((input) => {
    input.onchange = () => handleProfilePhotoFile(input);
  });
  const regFile = document.getElementById('regPhotoFile');
  if (regFile) regFile.onchange = function () { handleRegPhotoFile(this); };
}

function photoEditFieldsHtml(u) {
  const ph = ensurePhotoSlots(u);
  const ug = ph.unlockedGallery;
  const ul = ph.unlockedLocked;
  const publicTotal = 1 + ug;
  let html =
    '<p class="photo-slots-summary">Öffentlich: <strong>' +
    publicTotal +
    '</strong> / 10 · Privat: <strong>' +
    ul +
    '</strong> / ' +
    photoSlotsMaxLocked() +
    '</p>';
  html += photoUploadFieldHtml('photoMain', 'Hauptfoto', ph.main);
  for (let i = 0; i < ug; i++) {
    html += photoUploadFieldHtml('photoGal' + i, 'Öffentliches Foto ' + (i + 1), ph.gallery[i] || '');
  }
  if (ug < photoSlotsMaxGallery()) {
    html +=
      '<div class="photo-slot-buy-row"><button type="button" class="btn btn-ghost btn-photo-slot-buy" data-photo-slot="gallery">+ Öffentlicher Fotoplatz (' +
      photoSlotGalleryCost() +
      ' 🪙)</button></div>';
  }
  for (let i = 0; i < ul; i++) {
    html += photoUploadFieldHtml(
      'photoLock' + i,
      'Privat ' + (i + 1) + ' (Freunde, auch Nacktfotos 18+)',
      ph.locked[i] || ''
    );
  }
  if (ul < photoSlotsMaxLocked()) {
    html +=
      '<div class="photo-slot-buy-row"><button type="button" class="btn btn-ghost btn-photo-slot-buy" data-photo-slot="locked">+ Privater Fotoplatz (' +
      photoSlotLockedCost() +
      ' 🪙)</button></div>';
  }
  return html;
}

function wirePhotoSlotBuyButtons() {
  document.querySelectorAll('[data-photo-slot]').forEach((btn) => {
    btn.onclick = () => purchasePhotoSlot(btn.dataset.photoSlot);
  });
}

async function purchasePhotoSlot(type) {
  const u = me();
  if (!u) return;
  const cost = type === 'locked' ? photoSlotLockedCost() : photoSlotGalleryCost();
  const ph = ensurePhotoSlots(u);
  const atMax =
    type === 'locked'
      ? ph.unlockedLocked >= photoSlotsMaxLocked()
      : ph.unlockedGallery >= photoSlotsMaxGallery();
  if (atMax) {
    toast('Maximale Fotoplätze erreicht');
    return;
  }
  if ((u.coins || 0) < cost) {
    toast('Zu wenig Coins – du brauchst ' + cost + ' 🪙');
    document.querySelector('[data-tab="shop"]')?.click();
    return;
  }
  if (useDb()) {
    try {
      const j = await JamiApi.buyPhotoSlot(type);
      if (JamiApi.myProfile) Object.assign(u, JamiApi.myProfile);
      else {
        if (j.coins != null) u.coins = j.coins;
        if (j.photos) u.photos = j.photos;
      }
      ensurePhotoSlots(u);
      updateCoinUI();
      renderProfile();
      toast(
        type === 'locked'
          ? '🔒 +1 privater Fotoplatz freigeschaltet!'
          : '📷 +1 öffentlicher Fotoplatz freigeschaltet!'
      );
    } catch (e) {
      toast(e.message || 'Kauf fehlgeschlagen');
    }
    return;
  }
  if (type === 'locked') ph.unlockedLocked += 1;
  else ph.unlockedGallery += 1;
  u.coins = (u.coins || 0) - cost;
  ensurePhotoSlots(u);
  saveState();
  updateCoinUI();
  renderProfile();
  toast(
    type === 'locked'
      ? '🔒 +1 privater Fotoplatz freigeschaltet!'
      : '📷 +1 öffentlicher Fotoplatz freigeschaltet!'
  );
}

async function renderFriendRequests() {
  const list = document.getElementById('friendRequestsList');
  const empty = document.getElementById('friendRequestsEmpty');
  if (!list) return;
  list.innerHTML = '';
  let incoming = [];
  let friends = [];
  if (useDb()) {
    try {
      const data = await JamiApi.fetchFriendRequests();
      incoming = data.incoming || [];
      friends = data.friends || [];
    } catch (e) {}
  } else {
    incoming = (state.friendRequests.incoming || []).map(x => ({
      fromId: x.fromId,
      profile: findUser(x.fromId),
    })).filter(x => x.profile);
    friends = state.friendships.map(k => {
      const [a, b] = k.split('_');
      const oid = a === state.session ? b : a;
      return findUser(oid);
    }).filter(Boolean);
  }
  if (!incoming.length && !friends.length) {
    if (empty) empty.classList.remove('hidden');
    return;
  }
  if (empty) empty.classList.add('hidden');
  incoming.forEach(item => {
    const o = item.profile;
    const el = document.createElement('div');
    el.className = 'friend-request-card';
    el.innerHTML = `${avatarBlockHtml(o, false)}<div><strong>${escHtml(o.displayName)}</strong> möchte Freundschaft<br><span class="meta">Dann siehst du 5 private Fotos</span></div>`;
    const actions = document.createElement('div');
    actions.className = 'friend-request-actions';
    const yes = document.createElement('button');
    yes.className = 'btn btn-primary';
    yes.textContent = 'Annehmen';
    yes.onclick = () => respondFriendRequest(item.fromId, true);
    const no = document.createElement('button');
    no.className = 'btn btn-ghost';
    no.textContent = 'Ablehnen';
    no.onclick = () => respondFriendRequest(item.fromId, false);
    actions.appendChild(yes);
    actions.appendChild(no);
    el.appendChild(actions);
    list.appendChild(el);
  });
  if (friends.length) {
    const h = document.createElement('p');
    h.className = 'meta';
    h.style.marginTop = '0.75rem';
    h.textContent = 'Deine Freunde:';
    list.appendChild(h);
    friends.forEach(o => {
      const el = document.createElement('button');
      el.type = 'button';
      el.className = 'btn btn-ghost';
      el.style.width = '100%';
      el.style.marginTop = '0.35rem';
      el.textContent = '🤝 ' + o.displayName;
      el.onclick = () => openChatSession(o.id, o);
      list.appendChild(el);
    });
  }
}

function isPlaceholderCity(city) {
  const c = String(city || '').trim();
  return !c || c === '–' || c === '-' || c.toLowerCase() === 'unbekannt';
}

function getProfileCompleteness(u) {
  ensurePhotos(u);
  const hobbies = u.hobbies || [];
  const checks = [
    { key: 'photo', label: 'Hauptfoto', ok: !!(u.photos?.main || u.photo || '').trim() },
    { key: 'city', label: 'Herkunft (Stadt)', ok: !isPlaceholderCity(u.city) },
    { key: 'bio', label: 'Über mich (mind. 15 Zeichen)', ok: String(u.bio || '').trim().length >= 15 },
    { key: 'rel', label: 'Was ich suche', ok: !!String(u.relationshipGoal || '').trim() },
    {
      key: 'prefs',
      label: 'Was ich mag oder Hobbys',
      ok: String(u.likes || '').trim().length >= 3 || hobbies.length > 0,
    },
    { key: 'bday', label: 'Geburtstag', ok: !!String(u.birthday || '').trim() },
    { key: 'pets', label: 'Haustiere', ok: u.hasPets === 'Ja' || u.hasPets === 'Nein' },
    { key: 'kids', label: 'Kinder', ok: u.hasChildren === 'Ja' || u.hasChildren === 'Nein' },
    { key: 'smoke', label: 'Raucher', ok: !!String(u.smoker || '').trim() },
  ];
  const done = checks.filter((c) => c.ok).length;
  return {
    checks,
    percent: Math.round((done / checks.length) * 100),
    complete: done === checks.length,
  };
}

function profileCompleteBonusCoins() {
  return config.profileCompleteBonusCoins || 10;
}

function handleProfileCompleteBonusResponse(bonus) {
  if (!bonus || !bonus.awarded) return;
  const n = bonus.bonusCoins || profileCompleteBonusCoins();
  celebrateCoinReward(n, {
    message: '🎉 Profil komplett! +' + n + ' Coins',
    originEl: document.getElementById('profileCompleteCard'),
    fallbackCenter: true,
  });
  toast('Profil vollständig – Bonus ist da!');
}

function tryLocalProfileCompleteBonus(u) {
  if (!u || u.profileCompleteBonus) return false;
  if (!getProfileCompleteness(u).complete) return false;
  u.profileCompleteBonus = true;
  u.coins = (u.coins || 0) + profileCompleteBonusCoins();
  return true;
}

let profileBoostTimerId = null;

function profileBoostCoins() {
  return config.profileBoostCoins || 15;
}

function profileBoostMinutes() {
  return config.profileBoostMinutes || 30;
}

function getProfileBoostRemainingMs(u) {
  const until = u?.profileBoostUntil;
  if (!until) return 0;
  const s = String(until).trim();
  const t = Date.parse(s.includes('T') ? s : s.replace(' ', 'T') + 'Z');
  if (!Number.isFinite(t)) return 0;
  return Math.max(0, t - Date.now());
}

function isProfileBoostActive(u) {
  return getProfileBoostRemainingMs(u) > 0;
}

function formatBoostRemaining(ms) {
  if (ms <= 0) return '0:00';
  const totalSec = Math.ceil(ms / 1000);
  const m = Math.floor(totalSec / 60);
  const s = totalSec % 60;
  return m + ':' + String(s).padStart(2, '0');
}

function stopProfileBoostTimer() {
  if (profileBoostTimerId) {
    clearInterval(profileBoostTimerId);
    profileBoostTimerId = null;
  }
}

function startProfileBoostTimer() {
  stopProfileBoostTimer();
  const u = me();
  if (!u || !isProfileBoostActive(u)) return;
  profileBoostTimerId = setInterval(() => {
    const cur = me();
    if (!cur || !isProfileBoostActive(cur)) {
      if (cur) cur.profileBoostActive = false;
      stopProfileBoostTimer();
      renderProfileBoost();
      return;
    }
    const card = document.getElementById('profileBoostCard');
    const el = card?.querySelector('.boost-timer');
    if (el) el.textContent = formatBoostRemaining(getProfileBoostRemainingMs(cur));
  }, 1000);
}

function renderProfileBoost() {
  const card = document.getElementById('profileBoostCard');
  const u = me();
  if (!card || !u) return;
  const cost = profileBoostCoins();
  const mins = profileBoostMinutes();
  const active = isProfileBoostActive(u);
  u.profileBoostActive = active;
  card.classList.toggle('is-active', active);
  if (active) {
    const left = formatBoostRemaining(getProfileBoostRemainingMs(u));
    card.innerHTML =
      '<h3>🚀 Rampenlicht aktiv</h3>' +
      '<p class="meta" style="margin:0">Du wirst in <strong>Entdecken</strong> bevorzugt ganz oben angezeigt – mehr Sichtbarkeit & Klicks.</p>' +
      '<p class="boost-timer" aria-live="polite">' + left + '</p>' +
      '<p class="meta" style="margin:0">Noch <strong>' + mins + ' Min.</strong> Boost-Zeit</p>';
    startProfileBoostTimer();
    return;
  }
  stopProfileBoostTimer();
  const canAfford = (u.coins || 0) >= cost;
  card.innerHTML =
    '<h3>🚀 Profil-Boost (Rampenlicht)</h3>' +
    '<p class="meta" style="margin:0 0 0.5rem">Sofort mehr Aufmerksamkeit: Dein Profil steht in Entdecken vorne – ideal für neue Matches.</p>' +
    '<button type="button" class="btn btn-boost" id="btnProfileBoost"' +
    (canAfford ? '' : ' disabled') +
    '>Für ' +
    cost +
    ' Coins für ' +
    mins +
    ' Minuten ganz oben angezeigt werden</button>' +
    (canAfford
      ? ''
      : '<p class="meta" style="margin:0.5rem 0 0">Du hast ' + (u.coins || 0) + ' Coins – unter <strong>Coins</strong> nachladen.</p>');
  const btn = document.getElementById('btnProfileBoost');
  if (btn) btn.onclick = () => activateProfileBoost();
}

async function activateProfileBoost() {
  const u = me();
  if (!u) return;
  const cost = profileBoostCoins();
  const mins = profileBoostMinutes();
  if (isProfileBoostActive(u)) {
    toast('Rampenlicht läuft bereits');
    return;
  }
  if ((u.coins || 0) < cost) {
    toast('Zu wenig Coins – du brauchst ' + cost + ' Coins');
    document.querySelector('[data-tab="shop"]')?.click();
    return;
  }
  const btn = document.getElementById('btnProfileBoost');
  if (btn) btn.disabled = true;
  if (useDb()) {
    try {
      const j = await JamiApi.activateProfileBoost();
      if (JamiApi.myProfile) Object.assign(u, JamiApi.myProfile);
      else {
        if (j.coins != null) u.coins = j.coins;
        u.profileBoostUntil = j.profileBoostUntil;
        u.profileBoostActive = !!j.profileBoostActive;
      }
      toast('🚀 Rampenlicht aktiv! ' + mins + ' Min. ganz oben in Entdecken.');
      updateCoinUI();
      renderProfile();
      renderProfileBoost();
    } catch (e) {
      toast(e.message || 'Boost fehlgeschlagen');
      renderProfileBoost();
    }
    return;
  }
  u.coins = (u.coins || 0) - cost;
  u.profileBoostUntil = new Date(Date.now() + mins * 60000).toISOString();
  u.profileBoostActive = true;
  saveState();
  toast('🚀 Rampenlicht aktiv! ' + mins + ' Min. ganz oben in Entdecken.');
  updateCoinUI();
  renderProfile();
  renderProfileBoost();
}

const REFERRAL_STORAGE_KEY = 'jami_referral';

function referralBonusCoins() {
  return config.referralBonusCoins ?? 25;
}

function captureReferralFromUrl() {
  try {
    const params = new URLSearchParams(window.location.search);
    const ref = (params.get('ref') || '').trim();
    if (!ref) return;
    localStorage.setItem(REFERRAL_STORAGE_KEY, ref);
  } catch (e) {}
}

function openRegisterIfReferralPending() {
  if (!getStoredReferrerId()) return;
  if (JamiApi.isOnline && JamiApi.isOnline()) return;
  if (me()) return;
  showAuthForm('register');
}

function getStoredReferrerId() {
  try {
    const id = (localStorage.getItem(REFERRAL_STORAGE_KEY) || '').trim();
    return id || null;
  } catch (e) {
    return null;
  }
}

function clearStoredReferrer() {
  try {
    localStorage.removeItem(REFERRAL_STORAGE_KEY);
  } catch (e) {}
}

function referralInviteUrl(userId) {
  const base = String(config.siteUrl || window.location.origin).replace(/\/$/, '');
  return base + '/register?ref=' + encodeURIComponent(userId);
}

function renderReferralCard() {
  const card = document.getElementById('profileReferralCard');
  const u = me();
  if (!card || !u) return;
  const bonus = referralBonusCoins();
  const url = referralInviteUrl(u.id);
  card.innerHTML =
    '<h3>Freunde einladen</h3>' +
    '<p class="referral-lead">Schenke deinen Freunden ' +
    bonus +
    ' Coins! Wenn sie sich über deinen Link anmelden, bekommt ihr <strong>BEIDE +' +
    bonus +
    ' Coins gratis</strong>!</p>' +
    '<div class="referral-link-row">' +
    '<input type="text" class="referral-link-input" id="referralLinkInput" readonly value="' +
    escHtml(url) +
    '" aria-label="Einladungslink">' +
    '<button type="button" class="btn btn-referral-copy" id="btnCopyReferral">Link kopieren</button>' +
    '</div>';
  const btn = document.getElementById('btnCopyReferral');
  if (btn) {
    btn.onclick = async () => {
      try {
        await navigator.clipboard.writeText(url);
        toast('Link kopiert – teile ihn mit deinen Freunden!');
      } catch (e) {
        const inp = document.getElementById('referralLinkInput');
        if (inp) {
          inp.focus();
          inp.select();
          document.execCommand('copy');
          toast('Link kopiert');
        }
      }
    };
  }
}

function applyLocalReferralBonus(newUser, referrerId) {
  if (!referrerId || referrerId === newUser.id) return false;
  const ref = state.users.find(x => x.id === referrerId);
  if (!ref) return false;
  const bonus = referralBonusCoins();
  ref.coins = (ref.coins || 0) + bonus;
  newUser.coins = (newUser.coins || 0) + bonus;
  newUser.referredById = referrerId;
  return true;
}

function renderProfileCompleteness() {
  const card = document.getElementById('profileCompleteCard');
  const u = me();
  if (!card || !u) return;
  const { checks, percent, complete } = getProfileCompleteness(u);
  const bonus = profileCompleteBonusCoins();
  const claimed = !!u.profileCompleteBonus;
  card.classList.toggle('is-done', claimed || complete);
  if (claimed) {
    card.innerHTML =
      '<h3>✓ Profil-Bonus erhalten</h3>' +
      '<p class="meta" style="margin:0">Du hast bereits <strong>+' + bonus + ' Coins</strong> für dein vollständiges Profil bekommen.</p>';
    return;
  }
  const missing = checks.filter((c) => !c.ok).map((c) => '<li class="todo">' + escHtml(c.label) + '</li>').join('');
  const doneList = checks
    .filter((c) => c.ok)
    .map((c) => '<li class="done">' + escHtml(c.label) + '</li>')
    .join('');
  card.innerHTML =
    '<h3>📋 Profil-Vollständigkeit</h3>' +
    '<p class="meta" style="margin:0 0 0.35rem">Vervollständige dein Profil und erhalte <strong>+' +
    bonus +
    ' Coins</strong> (einmalig).</p>' +
    '<div class="profile-complete-bar" aria-hidden="true"><div style="width:' +
    percent +
    '%"></div></div>' +
    '<p class="meta" style="margin:0"><strong>' +
    percent +
    '%</strong> · noch ' +
    (checks.length - checks.filter((c) => c.ok).length) +
    ' Punkt(e)</p>' +
    '<ul class="profile-complete-checks">' +
    doneList +
    missing +
    '</ul>' +
    (complete
      ? '<p class="profile-complete-reward">✨ Profil komplett – speichern oder Seite neu laden für +' + bonus + ' Coins!</p>'
      : '<p class="profile-complete-reward">🎮 Fehlt noch: Stadt, Foto, Bio & Details unten – dann Speichern</p>');
}

function openProfileTabForSetup() {
  document.querySelectorAll('.nav-tabs button').forEach((b) => b.classList.remove('active'));
  const tabBtn = document.querySelector('.nav-tabs button[data-tab="profile"]');
  if (tabBtn) tabBtn.classList.add('active');
  document.querySelectorAll('#appShell .screen').forEach((s) => s.classList.remove('active'));
  document.getElementById('tab-profile')?.classList.add('active');
  renderProfile();
  renderProfileEmojiPicker();
  renderProfileCompleteness();
  renderProfileBoost();
  renderReferralCard();
  document.getElementById('profileCompleteCard')?.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
}

function collectProfileFromForm(prefix) {
  const g = id => document.getElementById(prefix + id);
  return {
    city: g('City')?.value.trim() || '',
    country: g('Country')?.value.trim() || 'DE',
    seeking: g('Seeking')?.value || 'Alle',
    relationshipGoal: g('RelationshipGoal')?.value || '',
    likes: g('Likes')?.value.trim() || '',
    hobbies: parseHobbiesInput(g('Hobbies')?.value || ''),
    preferences: g('Preferences')?.value.trim() || '',
    birthday: g('Birthday')?.value || '',
    hasPets: g('HasPets')?.value || '',
    petsInfo: g('PetsInfo')?.value.trim() || '',
    hasChildren: g('HasChildren')?.value || '',
    childrenInfo: g('ChildrenInfo')?.value.trim() || '',
    smoker: g('Smoker')?.value || '',
  };
}

function profileEditFieldsHtml(u, prefix) {
  const h = (u.hobbies || []).join(', ');
  const cityVal = isPlaceholderCity(u.city) ? '' : (u.city || '');
  return `
    <label>Herkunft – Stadt</label>
    <input id="${prefix}City" value="${escHtml(cityVal)}" placeholder="z. B. München"/>
    <label>Herkunft – Land</label>
    <input id="${prefix}Country" value="${escHtml(u.country || 'DE')}" placeholder="Deutschland"/>
    <label>Suche (Geschlecht)</label>
    <select id="${prefix}Seeking">
      <option${u.seeking==='Alle'?' selected':''}>Alle</option>
      <option${u.seeking==='Frauen'?' selected':''}>Frauen</option>
      <option${u.seeking==='Männer'?' selected':''}>Männer</option>
    </select>
    <label>Was ich suche (Beziehung)</label>
    <select id="${prefix}RelationshipGoal">
      <option value="">Bitte wählen</option>
      <option${u.relationshipGoal==='Feste Beziehung'?' selected':''}>Feste Beziehung</option>
      <option${u.relationshipGoal==='Etwas Lockeres'?' selected':''}>Etwas Lockeres</option>
      <option${u.relationshipGoal==='Freundschaft'?' selected':''}>Freundschaft</option>
      <option${u.relationshipGoal==='Weiß noch nicht'?' selected':''}>Weiß noch nicht</option>
    </select>
    <label>Was ich mag</label>
    <textarea id="${prefix}Likes" rows="2">${escHtml(u.likes||'')}</textarea>
    <label>Hobbys (kommagetrennt)</label>
    <input id="${prefix}Hobbies" value="${escHtml(h)}"/>
    <label>Vorlieben</label>
    <textarea id="${prefix}Preferences" rows="2">${escHtml(u.preferences||'')}</textarea>
    <label>Geburtstag</label>
    <input type="date" id="${prefix}Birthday" value="${escHtml(u.birthday||'')}"/>
    <label>Haustiere</label>
    <select id="${prefix}HasPets">
      <option value="">Keine Angabe</option>
      <option${u.hasPets==='Ja'?' selected':''}>Ja</option>
      <option${u.hasPets==='Nein'?' selected':''}>Nein</option>
    </select>
    <label>Haustiere – Details</label>
    <input id="${prefix}PetsInfo" value="${escHtml(u.petsInfo||'')}"/>
    <label>Kinder</label>
    <select id="${prefix}HasChildren">
      <option value="">Keine Angabe</option>
      <option${u.hasChildren==='Ja'?' selected':''}>Ja</option>
      <option${u.hasChildren==='Nein'?' selected':''}>Nein</option>
    </select>
    <label>Kinder – Details</label>
    <input id="${prefix}ChildrenInfo" value="${escHtml(u.childrenInfo||'')}"/>
    <label>Raucher</label>
    <select id="${prefix}Smoker">
      <option value="">Keine Angabe</option>
      <option${u.smoker==='Ja'?' selected':''}>Ja</option>
      <option${u.smoker==='Nein'?' selected':''}>Nein</option>
      <option${u.smoker==='Gelegentlich'?' selected':''}>Gelegentlich</option>
    </select>
    <label>Über mich</label>
    <textarea id="${prefix}Bio" rows="3" placeholder="Was macht dich aus? Erzähl gern in 2–3 Sätzen…">${escHtml(u.bio||'')}</textarea>
    <label><input type="checkbox" id="profGeo"/> Standort teilen (für „In der Nähe“ – optional)</label>`;
}

function myLikes() {
  if (useDb()) return JamiApi.myLikeIds();
  const id = state.session;
  if (!state.likes[id]) state.likes[id] = [];
  return state.likes[id];
}
function myPasses() {
  if (useDb()) return JamiApi.myPassIds();
  const id = state.session;
  if (!state.passes[id]) state.passes[id] = [];
  return state.passes[id];
}
function myBlocked() {
  if (useDb()) return JamiApi.myBlockIds();
  const id = state.session;
  if (!state.blocked[id]) state.blocked[id] = [];
  return state.blocked[id];
}
function theyLikedMe(otherId) {
  if (useDb()) return JamiApi.theyLikedMe(otherId);
  return (state.likes[otherId] || []).includes(state.session);
}
function isMutual(otherId) {
  if (useDb()) return JamiApi.isMutual(otherId);
  return myLikes().includes(otherId) && theyLikedMe(otherId);
}

function cacheProfile(o) {
  if (!o || !o.id) return;
  if (JamiApi.mode === 'cloudflare') {
    if (!JamiApi._profileCache) JamiApi._profileCache = {};
    JamiApi._profileCache[o.id] = o;
  }
}

async function getMutualMatchProfiles() {
  if (JamiApi.mode === 'cloudflare') {
    try {
      const list = await JamiApi.fetchMutualMatchesCloud();
      const blocked = new Set(JamiApi.myBlockIds());
      return list.filter((o) => !blocked.has(o.id));
    } catch (e) {
      return [];
    }
  }
  if (useDb()) {
    await JamiApi.refreshSocial();
    return myLikes()
      .filter((id) => isMutual(id))
      .map((id) => resolveProfileUser(id))
      .filter(Boolean);
  }
  return state.users.filter((o) => o.id !== state.session && isMutual(o.id));
}

function appendMatchCardRow(list, o, opts) {
  opts = opts || {};
  const el = document.createElement('div');
  el.className = 'match-card' + (opts.unread || opts.isNewMatch ? ' has-unread' : '');
  let alertHtml = '';
  if (opts.unread) {
    alertHtml =
      '<br><span class="chat-unread-label">' +
      (opts.unread > 1 ? opts.unread + ' neue Nachrichten' : 'Neue Nachricht') +
      '</span>';
  } else if (opts.isNewMatch) {
    alertHtml = '<br><span class="chat-unread-label">💕 Neues Match</span>';
  }
  el.innerHTML =
    `${avatarBlockHtml(o, false)}` +
    `<div style="flex:1"><strong>${profileNameLineHtml(o)}</strong>` +
    `${activityBadgesHtml(o)}` +
    `<br>${presenceBadgeHtml(o, false)}` +
    `<br><span class="match-badge">💕 Match</span>` +
    alertHtml +
    `<br><span class="meta">🌍 ${escHtml(o.city || '')}</span></div>`;
  const actions = document.createElement('div');
  actions.className = 'match-card-actions';
  const btnProfile = document.createElement('button');
  btnProfile.type = 'button';
  btnProfile.className = 'btn btn-ghost';
  btnProfile.textContent = '👤 Profil';
    btnProfile.onclick = (e) => {
      e.stopPropagation();
      openChatSafe(o.id, o);
    };
  const btnChat = document.createElement('button');
  btnChat.type = 'button';
  btnChat.className = 'btn btn-primary btn-chat-pulse';
  btnChat.textContent = '💬 Jetzt chatten';
  btnChat.onclick = async (e) => {
    e.stopPropagation();
    await openChatSafe(o.id, o);
  };
  actions.appendChild(btnProfile);
  actions.appendChild(btnChat);
  const btnBlock = document.createElement('button');
  btnBlock.type = 'button';
  btnBlock.className = 'btn-block-user';
  btnBlock.textContent = '🚫';
  btnBlock.title = 'Blockieren';
  btnBlock.onclick = (e) => {
    e.stopPropagation();
    blockUser(o.id, o.displayName);
  };
  actions.appendChild(btnBlock);
  el.appendChild(actions);
  el.onclick = () => openChatSafe(o.id, o);
  list.appendChild(el);
}

async function openChatSafe(otherId, otherHint) {
  try {
    await openChatSession(otherId, otherHint);
  } catch (e) {
    console.error('openChatSession', e);
    toast(e.message || 'Chat konnte nicht geöffnet werden');
  }
}

function verifiedBadge(u) {
  return (u.verified || u.realPerson) ? '<span class="badge-verified">✓ Echt</span>' : '';
}

function vipGoldBadge(u) {
  return u?.vipGold || u?.vip_gold
    ? '<span class="badge-vip-gold" title="VIP Gold">👑</span>'
    : '';
}

function profileDisplayEmojiHtml(u) {
  const id = u?.profileEmojiId || u?.profile_emoji_id || null;
  if (!id) return '';
  const item = shopEmojiById(id);
  if (!item) return '';
  const anim = item.anim || 'float';
  return (
    '<span class="profile-name-emoji emoji-anim-' +
    escHtml(anim) +
    '" title="' +
    escHtml(item.name) +
    '">' +
    item.icon +
    '</span>'
  );
}

function profileNameLineHtml(u, opts) {
  opts = opts || {};
  const showAge = opts.showAge !== false;
  const agePart = showAge && u.age != null ? ', ' + u.age : '';
  return escHtml(u.displayName || '') + vipGoldBadge(u) + agePart + profileDisplayEmojiHtml(u) + verifiedBadge(u);
}

async function setProfileDisplayEmoji(emojiId) {
  const u = me();
  if (!u) return;
  const id = emojiId || null;
  if (id && !(u.ownedEmojis || []).includes(id)) {
    toast('Erst im Shop kaufen');
    return;
  }
  u.profileEmojiId = id;
  if (useDb()) {
    try {
      await JamiApi.updateMyProfile({ profileEmojiId: id });
      if (JamiApi.myProfile) Object.assign(u, JamiApi.myProfile);
    } catch (e) {
      toast(e.message || 'Speichern fehlgeschlagen');
      return;
    }
  } else {
    saveState();
  }
  renderProfile();
  renderProfileEmojiPicker();
  renderEmojiShop();
  toast(id ? 'Premium-Emoji wird in Entdecken angezeigt' : 'Name-Emoji entfernt');
}

function renderProfileEmojiPicker() {
  const box = document.getElementById('profileEmojiPicker');
  const card = document.getElementById('profileEmojiDisplayCard');
  const u = me();
  if (!box || !u) return;
  const owned = u.ownedEmojis || [];
  if (!owned.length) {
    if (card) card.style.display = '';
    box.innerHTML =
      '<p class="meta" style="margin:0">Noch keine Premium-Emojis – oben im Shop kaufen, dann hier auswählen.</p>';
    return;
  }
  if (card) card.style.display = '';
  box.innerHTML = '';
  const noneBtn = document.createElement('button');
  noneBtn.type = 'button';
  noneBtn.className = 'profile-emoji-pick' + (!u.profileEmojiId ? ' is-active' : '');
  noneBtn.textContent = 'Keins';
  noneBtn.onclick = () => setProfileDisplayEmoji(null);
  box.appendChild(noneBtn);
  owned.forEach((eid) => {
    const item = shopEmojiById(eid);
    if (!item) return;
    const btn = document.createElement('button');
    btn.type = 'button';
    const anim = item.anim || 'float';
    btn.className =
      'profile-emoji-pick emoji-anim-' + anim + (u.profileEmojiId === eid ? ' is-active' : '');
    btn.innerHTML = '<span class="pick-icon">' + item.icon + '</span>' + escHtml(item.name);
    btn.onclick = () => setProfileDisplayEmoji(eid);
    box.appendChild(btn);
  });
}
function matchesSeeking(me, other) {
  if (!me || me.seeking === 'Alle') return true;
  if (me.seeking === 'Frauen') return other.gender === 'Frau';
  if (me.seeking === 'Männer') return other.gender === 'Mann';
  return true;
}
const FILTER_KM_MIN = 1;
const FILTER_KM_MAX = 50;
const FILTER_KM_DEFAULT = 25;
const FILTER_KM_KEY = 'jami_filter_km';
const FILTER_EVERYWHERE_KEY = 'jami_filter_everywhere';

function clampFilterKm(v) {
  const n = parseInt(v, 10);
  if (!Number.isFinite(n)) return FILTER_KM_DEFAULT;
  return Math.max(FILTER_KM_MIN, Math.min(FILTER_KM_MAX, n));
}

function isFilterEverywhere() {
  return !!document.getElementById('filterEverywhere')?.checked;
}

function profilePassesKmFilter(u, o, f) {
  if (f.everywhere || u.lat == null || u.lon == null) return true;
  if (o.lat == null || o.lon == null) return false;
  return haversineKm(u.lat, u.lon, o.lat, o.lon) <= f.km;
}

function syncFilterKmUi() {
  const km = getFilterKm();
  const el = document.getElementById('filterKm');
  const val = document.getElementById('filterKmVal');
  const everywhere = isFilterEverywhere();
  if (el) el.disabled = everywhere;
  if (el && String(el.value) !== String(km)) el.value = String(km);
  if (val) val.textContent = everywhere ? 'Überall' : km + ' km';
}

function initFilterKm() {
  try {
    const saved = localStorage.getItem(FILTER_KM_KEY);
    if (saved != null) {
      const el = document.getElementById('filterKm');
      if (el) el.value = String(clampFilterKm(saved));
    }
    const ew = localStorage.getItem(FILTER_EVERYWHERE_KEY);
    const ewEl = document.getElementById('filterEverywhere');
    if (ewEl && ew === '1') ewEl.checked = true;
  } catch (e) {}
  syncFilterKmUi();
}

function persistFilterKm() {
  try {
    localStorage.setItem(FILTER_KM_KEY, String(getFilterKm()));
    localStorage.setItem(FILTER_EVERYWHERE_KEY, isFilterEverywhere() ? '1' : '0');
  } catch (e) {}
}

function getFilters() {
  return {
    km: getFilterKm(),
    everywhere: isFilterEverywhere(),
    ageMin: +document.getElementById('filterAgeMin')?.value || 18,
    ageMax: +document.getElementById('filterAgeMax')?.value || 99,
  };
}
async function getDiscoverQueueAsync() {
  if (JamiApi.mode === 'cloudflare') {
    return JamiApi.getDiscoverQueueCloud(getFilters());
  }
  if (useDb()) {
    await JamiApi.refreshSocial();
    return JamiApi.getDiscoverQueue(getFilters(), { matchesSeeking, haversineKm });
  }
  return getDiscoverQueueLocal();
}

function getDiscoverQueueLocal() {
  const u = me();
  if (!u) return [];
  const f = getFilters();
  const liked = myLikes();
  const passed = myPasses();
  const blocked = myBlocked();
  let queue = state.users.filter(o => {
    if (isDemoUser(o)) return false;
    if (o.id === u.id || blocked.includes(o.id)) return false;
    if (passed.includes(o.id)) return false;
    if (!matchesSeeking(u, o)) return false;
    if (o.age < f.ageMin || o.age > f.ageMax) return false;
    if (!profilePassesKmFilter(u, o, f)) return false;
    return true;
  }).map(o => {
    let km = null;
    if (u.lat != null && o.lat != null) km = haversineKm(u.lat, u.lon, o.lat, o.lon);
    return { o, km };
  }).sort((a, b) => {
    const ba = isProfileBoostActive(a.o) ? 1 : 0;
    const bb = isProfileBoostActive(b.o) ? 1 : 0;
    if (bb !== ba) return bb - ba;
    return (a.km ?? 9999) - (b.km ?? 9999);
  });
  if (!f.everywhere && queue.length < 5) {
    const seen = new Set(queue.map((x) => x.o.id));
    buildNearbySeedProfiles(u, f).forEach((item) => {
      if (seen.has(item.o.id)) return;
      queue.push(item);
      seen.add(item.o.id);
    });
    queue.sort((a, b) => {
      const pa = a.o.photo ? 1 : 0;
      const pb = b.o.photo ? 1 : 0;
      if (pb !== pa) return pb - pa;
      return (a.km ?? 9999) - (b.km ?? 9999);
    });
  }
  return queue;
}
function updateLikesBadge() {
  refreshInboxBadges().catch(() => {});
}

function getSeenMatchIds() {
  try {
    const raw = JSON.parse(localStorage.getItem(SEEN_MATCHES_KEY) || '[]');
    return new Set(Array.isArray(raw) ? raw : []);
  } catch (e) {
    return new Set();
  }
}

function markMatchSeen(id) {
  if (!id) return;
  const seen = getSeenMatchIds();
  seen.add(id);
  localStorage.setItem(SEEN_MATCHES_KEY, JSON.stringify([...seen].slice(-300)));
}

function markAllCurrentMatchesSeen() {
  if (JamiApi._mutualIds && JamiApi._mutualIds.size) {
    JamiApi._mutualIds.forEach((id) => markMatchSeen(id));
    return;
  }
  if (useDb()) {
    myLikes().filter((id) => isMutual(id)).forEach((id) => markMatchSeen(id));
  }
}

function countUnseenMatches() {
  const seen = getSeenMatchIds();
  if (JamiApi._mutualIds && JamiApi._mutualIds.size) {
    let n = 0;
    JamiApi._mutualIds.forEach((id) => {
      if (!seen.has(id)) n += 1;
    });
    return n;
  }
  if (useDb()) {
    return myLikes().filter((id) => isMutual(id) && !seen.has(id)).length;
  }
  const meId = state.session;
  return state.users.filter((o) => o.id !== meId && isMutual(o.id) && !seen.has(o.id)).length;
}

function countLocalUnreadMessages() {
  const meId = me()?.id || state.session;
  if (!meId) return 0;
  let n = 0;
  Object.values(state.chats || {}).forEach((chat) => {
    const lastRead = chat.lastReadAt || 0;
    (chat.msgs || []).forEach((m) => {
      if (m.from !== meId && (m.at || 0) > lastRead) n += 1;
    });
  });
  return n;
}

function setLocalChatRead(otherId) {
  const key = chatKeyLocal(otherId);
  if (!state.chats[key]) return;
  state.chats[key].lastReadAt = Date.now();
  saveState();
}

function updateChatBadge(unreadMessages, unseenMatches) {
  const badge = document.getElementById('chatBadge');
  const btn = document.querySelector('[data-tab="matches"]');
  const total = (unreadMessages || 0) + (unseenMatches || 0);
  if (badge) {
    badge.textContent = total > 99 ? '99+' : String(total);
    badge.classList.toggle('hidden', total === 0);
  }
  if (btn) {
    btn.classList.toggle('nav-has-alert', total > 0);
    btn.classList.toggle('nav-chat-pulse', total === 0);
  }
}

function updateLikesBadgeCount(n) {
  const badge = document.getElementById('likesBadge');
  if (!badge) return;
  badge.textContent = n > 99 ? '99+' : String(n);
  badge.classList.toggle('hidden', n <= 0);
}

function updateDocumentInboxTitle(totalAlerts) {
  document.title = totalAlerts > 0 ? '(' + totalAlerts + ') ' + JAMI_BASE_TITLE : JAMI_BASE_TITLE;
}

async function refreshInboxBadges() {
  let unreadMessages = 0;
  let incomingLikes = 0;
  let unseenMatches = 0;

  if (JamiApi.mode === 'cloudflare') {
    try {
      const s = await JamiApi.fetchInboxSummaryCloud();
      unreadMessages = s.unreadMessages || 0;
      incomingLikes = s.incomingLikes || 0;
      await JamiApi.refreshSocial();
      unseenMatches = countUnseenMatches();
    } catch (e) {}
  } else if (useDb()) {
    await JamiApi.refreshSocial();
    incomingLikes = JamiApi.getIncomingLikes().filter((o) => !myLikes().includes(o.id)).length;
    unseenMatches = countUnseenMatches();
  } else if (me() || state.session) {
    incomingLikes = state.users.filter((o) => o.id !== state.session && theyLikedMe(o.id) && !myLikes().includes(o.id)).length;
    unreadMessages = countLocalUnreadMessages();
    unseenMatches = countUnseenMatches();
  }

  updateLikesBadgeCount(incomingLikes);
  updateChatBadge(unreadMessages, unseenMatches);
  updateDocumentInboxTitle(unreadMessages + unseenMatches + incomingLikes);
}

function startInboxPolling() {
  stopInboxPolling();
  if (!me() && !state.session) return;
  refreshInboxBadges().catch(() => {});
  const sec = config.inboxPollSec || 25;
  inboxPollTimer = setInterval(() => refreshInboxBadges().catch(() => {}), sec * 1000);
}

function stopInboxPolling() {
  if (inboxPollTimer) {
    clearInterval(inboxPollTimer);
    inboxPollTimer = null;
  }
  updateDocumentInboxTitle(0);
}

function haversineKm(lat1, lon1, lat2, lon2) {
  const R = 6371;
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLon = (lon2 - lon1) * Math.PI / 180;
  const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLon/2)**2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}

function me() {
  if (useDb()) return JamiApi.myProfile;
  return state.users.find(u => u.id === state.session);
}

function findUser(id) {
  if (JamiApi.mode === 'cloudflare') {
    const p = JamiApi.findProfile(id);
    if (p) return p;
  }
  if (useDb()) return JamiApi._profiles.find(u => u.id === id);
  return state.users.find(u => u.id === id);
}

function toast(msg) {
  const el = document.getElementById('toast');
  el.textContent = msg;
  el.classList.remove('hidden');
  setTimeout(() => el.classList.add('hidden'), 3200);
}

function jamiLoaderHtml(label, compact) {
  const text = label ? escHtml(label) : 'Lädt…';
  const cls = 'jami-loader' + (compact ? ' jami-loader-compact' : '');
  return (
    '<div class="' +
    cls +
    '" role="status" aria-live="polite">' +
    '<img src="images/jami-dating-logo.png" alt="" class="jami-loader-logo" width="68" height="68" decoding="async"/>' +
    '<span class="jami-loader-brand">Jami-Dating</span>' +
    '<p class="jami-loader-label">' +
    text +
    '</p></div>'
  );
}

function pulseCoinPill() {
  const pill = document.getElementById('coinDisplay');
  if (!pill) return;
  pill.classList.remove('coin-pill-pulse');
  void pill.offsetWidth;
  pill.classList.add('coin-pill-pulse');
  setTimeout(() => pill.classList.remove('coin-pill-pulse'), 900);
}

function flyCoinsToPill(amount, originRect) {
  const pill = document.getElementById('coinDisplay');
  if (!pill) return;
  const pillRect = pill.getBoundingClientRect();
  const startX = originRect ? originRect.left + originRect.width / 2 : window.innerWidth * 0.5;
  const startY = originRect ? originRect.top + originRect.height / 2 : window.innerHeight * 0.45;
  const endX = pillRect.left + pillRect.width / 2;
  const endY = pillRect.top + pillRect.height / 2;
  const count = Math.min(Math.max(parseInt(amount, 10) || 1, 1), 14);
  for (let i = 0; i < count; i++) {
    const el = document.createElement('span');
    el.className = 'coin-fly-particle';
    el.textContent = '🪙';
    el.style.left = startX + 'px';
    el.style.top = startY + 'px';
    el.style.opacity = '1';
    document.body.appendChild(el);
    const delay = i * 70;
    const jx = (Math.random() - 0.5) * 36;
    const jy = (Math.random() - 0.5) * 28;
    setTimeout(() => {
      el.style.transform =
        'translate(calc(' + (endX - startX + jx) + 'px - 50%), calc(' + (endY - startY + jy) + 'px - 50%)) scale(0.35)';
      el.style.opacity = '0';
    }, delay + 40);
    setTimeout(() => el.remove(), delay + 720);
  }
  setTimeout(pulseCoinPill, 380);
}

function showCoinSuccessPopup(message) {
  let pop = document.getElementById('coinSuccessPopup');
  if (!pop) {
    pop = document.createElement('div');
    pop.id = 'coinSuccessPopup';
    pop.className = 'coin-success-popup hidden';
    pop.innerHTML = '<span class="coin-success-icon" aria-hidden="true">🪙</span><span class="coin-success-text"></span>';
    document.body.appendChild(pop);
  }
  const textEl = pop.querySelector('.coin-success-text');
  if (textEl) textEl.textContent = message;
  pop.classList.remove('hidden');
  pop.classList.add('show');
  clearTimeout(showCoinSuccessPopup._hideT);
  showCoinSuccessPopup._hideT = setTimeout(() => {
    pop.classList.remove('show');
    pop.classList.add('hidden');
  }, 2400);
}

function celebrateCoinReward(amount, options) {
  options = options || {};
  const amt = Math.max(1, parseInt(amount, 10) || 1);
  const msg = options.message || '+' + amt + ' Coin' + (amt === 1 ? '' : 's') + '!';
  let rect = null;
  const origin = options.originEl;
  if (origin && origin.getBoundingClientRect) rect = origin.getBoundingClientRect();
  else if (options.fallbackCenter) rect = { left: window.innerWidth / 2 - 20, top: window.innerHeight / 2 - 20, width: 40, height: 40 };
  showCoinSuccessPopup(msg);
  flyCoinsToPill(amt, rect);
  updateCoinUI();
}

function updateCoinUI() {
  const u = me();
  document.getElementById('coinCount').textContent = u ? u.coins : 0;
}

function messageCost() {
  return config.messageCostPerChat ?? 10;
}

function openChatsTab() {
  document.querySelector('[data-tab="matches"]')?.click();
  renderMatches().catch(() => {});
}

async function handleChatClick(otherId) {
  await openChatSession(otherId, resolveProfileUser(otherId));
  toast('💬 Jetzt chatten – 10 Coins pro Nachricht');
}

function showAuthForm(mode) {
  const login = document.getElementById('loginCard');
  const reg = document.getElementById('registerCard');
  const btnReg = document.getElementById('btnShowRegister');
  const btnLog = document.getElementById('btnShowLogin');
  btnReg?.classList.toggle('active', mode === 'register');
  btnLog?.classList.toggle('active', mode === 'login');
  login?.classList.toggle('hidden', mode !== 'login');
  reg?.classList.toggle('hidden', mode !== 'register');
}
window.showAuthForm = showAuthForm;

let oauthLive = { google: false, facebook: false };

async function refreshOAuthConfig() {
  try {
    const r = await fetch('/api/oauth/status', { cache: 'no-cache' });
    if (r.ok) {
      const j = await r.json();
      oauthLive = j.oauth || oauthLive;
      config.oauth = Object.assign({}, config.oauth || {}, {
        google: !!oauthLive.google,
        facebook: !!oauthLive.facebook,
        googleClientId: j.googleClientId || config.oauth?.googleClientId || '',
        facebookAppId: j.facebookAppId || config.oauth?.facebookAppId || '',
        googleHasSecret: !!j.googleRedirect,
      });
    }
  } catch (e) {
    try {
      const r = await fetch('/api/health', { cache: 'no-cache' });
      if (r.ok) oauthLive = (await r.json()).oauth || oauthLive;
    } catch (_) {}
  }
  return oauthLive;
}

function redirectOAuth(provider) {
  let url = '/api/oauth/' + provider + '/start';
  const ref = getStoredReferrerId();
  if (ref) url += '?ref=' + encodeURIComponent(ref);
  window.location.href = url;
}

function loadGoogleGsiScript() {
  if (window.google?.accounts?.id) return Promise.resolve();
  if (window.__jamiGsiPromise) return window.__jamiGsiPromise;
  window.__jamiGsiPromise = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = 'https://accounts.google.com/gsi/client';
    s.async = true;
    s.defer = true;
    s.onload = () => resolve();
    s.onerror = () => reject(new Error('Google Script'));
    document.head.appendChild(s);
  });
  return window.__jamiGsiPromise;
}

async function handleGoogleCredential(response) {
  const ref = getStoredReferrerId();
  try {
    const r = await fetch('/api/oauth/google/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ credential: response.credential, ref: ref || undefined }),
    });
    const j = await r.json();
    if (!r.ok) throw new Error(j.error || 'Google Login fehlgeschlagen');
    localStorage.setItem('jami_token', j.token);
    JamiApi._token = j.token;
    JamiApi.mode = 'cloudflare';
    if (j.profile) JamiApi.myProfile = j.profile;
    else JamiApi._profileCompleteBonus = await JamiApi._cloudLoadMe();
    clearStoredReferrer();
    await showApp();
    if (j.isNew) openProfileTabForSetup();
    toast(j.isNew ? 'Willkommen bei Jami! Profil kurz vervollständigen.' : 'Willkommen zurück!');
    window.__jamiBooted = true;
  } catch (e) {
    toast(e.message || 'Google Login fehlgeschlagen');
  }
}

async function setupGoogleSignInButton(clientId) {
  const wrap = document.getElementById('btnOAuthGoogleWrap');
  const fallback = document.getElementById('btnOAuthGoogle');
  if (!wrap || !clientId) return false;
  try {
    await loadGoogleGsiScript();
    wrap.querySelector('.gsi-button-host')?.remove();
    if (fallback) fallback.style.display = 'none';
    const host = document.createElement('div');
    host.className = 'gsi-button-host';
    wrap.appendChild(host);
    window.google.accounts.id.initialize({
      client_id: clientId,
      callback: handleGoogleCredential,
      auto_select: false,
      cancel_on_tap_outside: true,
      context: 'signin',
      ux_mode: 'popup',
      itp_support: true,
      use_fedcm_for_prompt: true,
    });
    window.google.accounts.id.renderButton(host, {
      theme: 'outline',
      size: 'large',
      text: 'signin_with',
      locale: 'de',
      width: Math.min(360, wrap.offsetWidth || 320),
    });
    return true;
  } catch (e) {
    if (fallback) fallback.style.display = '';
    wrap.querySelector('.gsi-button-host')?.remove();
    return false;
  }
}

async function initOAuthButtons() {
  const wrap = document.getElementById('authSocial');
  if (!wrap || config.backend !== 'cloudflare') return;
  wrap.classList.remove('hidden');
  const googleBtn = document.getElementById('btnOAuthGoogle');
  const fbBtn = document.getElementById('btnOAuthFacebook');
  await refreshOAuthConfig();
  const clientId = (config.oauth && config.oauth.googleClientId) || '';
  if (fbBtn) {
    fbBtn.hidden = false;
    fbBtn.disabled = !oauthLive.facebook;
    fbBtn.textContent = 'Mit Facebook anmelden';
  }
  if (clientId && oauthLive.google && !isInAppBrowser()) {
    const ok = await setupGoogleSignInButton(clientId);
    if (!ok && googleBtn) {
      googleBtn.hidden = false;
      googleBtn.disabled = false;
      googleBtn.textContent = 'Mit Google anmelden';
    }
    return;
  }
  if (clientId && oauthLive.google && isInAppBrowser()) {
    if (googleBtn) {
      googleBtn.hidden = false;
      googleBtn.disabled = false;
      googleBtn.textContent = 'Mit Google anmelden';
    }
    wrap.querySelector('.gsi-button-host')?.remove();
    return;
  }
  if (googleBtn) {
    googleBtn.hidden = false;
    googleBtn.disabled = !oauthLive.google;
    googleBtn.style.display = '';
    googleBtn.textContent = 'Mit Google anmelden';
  }
  wrap.querySelector('.gsi-button-host')?.remove();
}

async function startOAuth(provider) {
  await refreshOAuthConfig();
  if (!oauthLive[provider]) {
    toast(
      provider === 'facebook'
        ? 'Facebook Login: App-ID fehlt auf dem Server. Bitte OAuth in data/jami-secrets.local.json eintragen und deployen.'
        : 'Google Login: Client-ID fehlt auf dem Server. Bitte OAuth in data/jami-secrets.local.json eintragen und deployen.'
    );
    return;
  }
  if (provider === 'google') {
    if (isInAppBrowser() && !config.oauth?.googleHasSecret) {
      toast('Google Login: Bitte in Safari oder Chrome öffnen (Teilen → Im Browser öffnen).');
      return;
    }
    if (config.oauth?.googleHasSecret) {
      redirectOAuth('google');
      return;
    }
    const clientId = config.oauth?.googleClientId;
    if (clientId) {
      try {
        await loadGoogleGsiScript();
        let done = false;
        window.google.accounts.id.initialize({
          client_id: clientId,
          callback: (resp) => {
            done = true;
            handleGoogleCredential(resp);
          },
          auto_select: false,
          cancel_on_tap_outside: true,
          context: 'signin',
          ux_mode: 'popup',
          itp_support: true,
          use_fedcm_for_prompt: true,
        });
        window.google.accounts.id.prompt((notification) => {
          if (done) return;
          if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
            toast('Google-Fenster blockiert? Pop-up erlauben oder offiziellen Google-Button nutzen.');
          }
        });
        return;
      } catch (e) {
        toast('Google Login fehlgeschlagen – bitte erneut versuchen.');
        return;
      }
    }
  }
  redirectOAuth(provider);
}

async function handleOAuthReturn() {
  const hash = location.hash || '';
  const errM = hash.match(/jami_oauth_error=([^&]+)/);
  if (errM) {
    history.replaceState(null, '', location.pathname + location.search);
    toast(decodeURIComponent(errM[1].replace(/\+/g, ' ')));
    return false;
  }
  const m = hash.match(/jami_oauth=([^&]+)/);
  if (!m) return false;
  const isNew = /jami_oauth_new=1/.test(hash);
  const token = decodeURIComponent(m[1]);
  history.replaceState(null, '', location.pathname + location.search);
  localStorage.setItem('jami_token', token);
  JamiApi._token = token;
  JamiApi.mode = 'cloudflare';
  try {
    JamiApi._profileCompleteBonus = await JamiApi._cloudLoadMe();
    clearStoredReferrer();
    await showApp();
    if (isNew) openProfileTabForSetup();
    toast(isNew ? 'Willkommen bei Jami! Profil kurz vervollständigen.' : 'Willkommen bei Jami!');
    window.__jamiBooted = true;
    return true;
  } catch (e) {
    localStorage.removeItem('jami_token');
    JamiApi._token = '';
    toast('Anmeldung fehlgeschlagen');
    return false;
  }
}

function scrollToAuth(mode) {
  showAuthForm(mode || 'register');
  document.getElementById('start').scrollIntoView({ behavior: 'smooth', block: 'start' });
}

async function showApp() {
  document.body.classList.remove('mode-landing');
  document.body.classList.add('mode-app');
  document.getElementById('authScreen').classList.remove('active');
  document.getElementById('appShell').classList.remove('hidden');
  const stack = document.getElementById('swipeStack');
  if (stack && !stack.querySelector('.swipe-card')) {
    stack.innerHTML = jamiLoaderHtml('Wird geladen…');
  }
  const geo = document.getElementById('geoStatus');
  if (geo && useDb()) {
    geo.textContent = JamiApi.mode === 'cloudflare'
      ? '☁️ Online – echte Nutzer (Cloudflare Server)'
      : '☁️ Online-Modus (Supabase) – echte Nutzer weltweit';
  }
  updateCoinUI();
  if (JamiApi._profileCompleteBonus) {
    handleProfileCompleteBonusResponse(JamiApi._profileCompleteBonus);
    JamiApi._profileCompleteBonus = null;
  }
  await Promise.all([
    renderSwipeDeck(),
    renderLikes().catch(() => {}),
    useDb() ? renderMatches().catch(() => {}) : Promise.resolve(),
  ]);
  if (useDb() && JamiApi.mode === 'cloudflare') {
    JamiApi.refreshSocial().catch(() => {});
  }
  updateLikesBadge();
  startInboxPolling();
  deferSecondaryAppUi();
  refreshAdRewardStatus().catch(() => {});
  if (JamiApi.mode === 'cloudflare') {
    registerPushServiceWorker().catch(() => {});
    handleDeepLinkChat().catch(() => {});
  }
  requestGeo();
  startPresenceHeartbeat();
  initMobileAppUi();
  stopWmLandingSchedulePoll();
  prefetchWmForApp();
  runPendingPackCheckout().catch(function () {});
}

async function showAuth() {
  stopProfileBoostTimer();
  stopPresenceHeartbeat();
  stopInboxPolling();
  stopWmFanPoll();
  stopWmTipsPoll();
  destroyGeoMap();
  if (useDb() && JamiApi._token) await JamiApi.logout();
  state.session = null;
  saveState();
  document.body.classList.add('mode-landing');
  document.body.classList.remove('mode-app');
  document.getElementById('appShell').classList.add('hidden');
  document.getElementById('authScreen').classList.add('active');
  startWmLandingSchedulePoll();
}

document.querySelectorAll('[data-landing-register]').forEach((el) => {
  el.onclick = () => scrollToAuth('register');
});
document.querySelectorAll('[data-landing-login]').forEach((el) => {
  el.onclick = () => scrollToAuth('login');
});
document.getElementById('navLogin')?.addEventListener('click', (e) => {
  e.preventDefault();
  scrollToAuth('login');
});
document.getElementById('btnShowRegister')?.addEventListener('click', () => showAuthForm('register'));
document.getElementById('btnShowLogin')?.addEventListener('click', () => showAuthForm('login'));
document.getElementById('btnLandingWmCta')?.addEventListener('click', () => {
  if (window.jamiScrollRegister) jamiScrollRegister();
  showAuthForm('register');
});
document.querySelector('.landing-pack-grid')?.addEventListener('click', (e) => {
  const btn = e.target.closest('.landing-pack-unlock');
  if (!btn) return;
  triggerPackCheckout(btn.dataset.packId || '', btn);
});
document.getElementById('btnOAuthGoogle')?.addEventListener('click', () => startOAuth('google'));
document.getElementById('btnOAuthFacebook')?.addEventListener('click', () => startOAuth('facebook'));
document.getElementById('regPhotoFile')?.addEventListener('change', function () { handleRegPhotoFile(this); });

document.getElementById('btnLogin')?.addEventListener('click', async () => {
  const email = document.getElementById('loginEmail').value.trim().toLowerCase();
  const pass = document.getElementById('loginPass').value;
  if (useDb()) {
    const res = await JamiApi.login(email, pass);
    if (!res.ok) { toast(res.error || 'Login fehlgeschlagen'); return; }
    await showApp();
    toast('Willkommen zurück!');
    return;
  }
  const u = state.users.find(x => x.email === email && x.pass === pass);
  if (!u) { toast('Login fehlgeschlagen'); return; }
  state.session = u.id;
  saveState();
  await showApp();
});

document.getElementById('btnRegister')?.addEventListener('click', async () => {
  const email = document.getElementById('regEmail').value.trim().toLowerCase();
  const pass = document.getElementById('regPass').value;
  const displayName = document.getElementById('regName').value.trim();
  const age = +document.getElementById('regAge').value;
  if (state.users.some(u => u.email === email)) { toast('E-Mail schon registriert'); return; }
  if (!displayName) { toast('Bitte Anzeigename eingeben'); return; }
  if (!email) { toast('Bitte E-Mail eingeben'); return; }
  if (!pass || pass.length < 6) { toast('Passwort mindestens 6 Zeichen'); return; }
  if (!age || age < 18) { toast('Mindestalter 18'); return; }
  if (!document.getElementById('regReal').checked) { toast('Bitte bestätigen: echte Person'); return; }
  if (!document.getElementById('regAgree').checked) { toast('Bitte AGB & Echtheit bestätigen'); return; }
  const registeredAt = Date.now();
  const userPhotos = freshUserPhotos(regPhotoDataUrl);
  const u = {
    id: 'u_' + registeredAt,
    createdAt: registeredAt,
    email,
    pass,
    displayName,
    age,
    gender: document.getElementById('regGender').value,
    seeking: 'Alle',
    city: '–',
    country: 'DE',
    bio: '',
    relationshipGoal: '',
    likes: '',
    hobbies: [],
    preferences: '',
    birthday: '',
    hasPets: '',
    petsInfo: '',
    hasChildren: '',
    childrenInfo: '',
    smoker: '',
    photos: userPhotos,
    photo: userPhotos.main,
    lat: null,
    lon: null,
    coins: config.signupBonusCoins ?? 50,
    ownedEmojis: [],
    profileCompleteBonus: false,
    verified: true,
    realPerson: true,
    prompt: '',
    interests: [],
  };
  const referrerId = getStoredReferrerId();
  if (useDb()) {
    const res = await JamiApi.register({
      email: u.email,
      password: pass,
      displayName: u.displayName,
      age: u.age,
      gender: u.gender,
      seeking: u.seeking,
      city: u.city,
      country: u.country,
      photos: u.photos,
      coins: u.coins,
      verified: true,
      realPerson: true,
      referrerId: referrerId || undefined,
      ref: referrerId || undefined,
      utmSource: getTrafficSource(),
      utmMedium: getTrafficMedium(),
    });
    if (!res.ok) { toast(res.error || 'Registrierung fehlgeschlagen'); return; }
    clearStoredReferrer();
    await showApp();
    openProfileTabForSetup();
    if (res.referral?.applied) {
      toast('🎁 +' + (res.referral.bonus || referralBonusCoins()) + ' Gratis-Coins – Freundes-Einladung!');
    } else {
      toast('Willkommen! Vervollständige dein Profil – bis zu +10 Coins Bonus 🎮');
    }
    return;
  }
  if (referrerId && applyLocalReferralBonus(u, referrerId)) clearStoredReferrer();
  state.users.push(u);
  state.session = u.id;
  saveState();
  await showApp();
  openProfileTabForSetup();
  if (referrerId && u.referredById) {
    toast('🎁 +' + referralBonusCoins() + ' Gratis-Coins – Freundes-Einladung!');
  } else {
    toast('Willkommen! ' + u.coins + ' Coins – Profil ausbauen für Bonus 🎮');
  }
});

function requestGeo() {
  const u = me();
  const statusEl = document.getElementById('geoStatus');
  const mapCard = document.getElementById('geoMapCard');
  if (!u) {
    mapCard?.classList.add('hidden');
    return;
  }
  mapCard?.classList.remove('hidden');
  if (!navigator.geolocation) {
    if (statusEl) statusEl.textContent = 'Standort nicht verfügbar – Herkunft trotzdem sichtbar.';
    setGeoMapHint('GPS auf diesem Gerät nicht verfügbar');
    return;
  }
  stopGeoWatch();
  ensureLeafletLoaded()
    .then(() => {
      if (u.lat != null && u.lon != null) updateGeoMap(u.lat, u.lon);
      else initGeoMap();
    })
    .catch(() => setGeoMapHint('Karte konnte nicht geladen werden'));
  geoWatchId = navigator.geolocation.watchPosition(
    async (pos) => {
      const lat = pos.coords.latitude;
      const lon = pos.coords.longitude;
      u.lat = lat;
      u.lon = lon;
      await saveGeoToServer(lat, lon);
      if (statusEl) statusEl.textContent = '📍 Live-Standort aktiv – Umkreis-Suche auf der Karte';
      ensureLeafletLoaded()
        .then(() => updateGeoMap(lat, lon, pos.coords.accuracy))
        .catch(() => {});
      scheduleGeoDeckRefresh();
    },
    () => {
      if (statusEl) statusEl.textContent = 'Standort abgelehnt – nur Herkunft (Stadt), keine km.';
      setGeoMapHint('Standortfreigabe im Browser erlauben');
    },
    { enableHighAccuracy: true, maximumAge: 12000, timeout: 25000 }
  );
}

let geoMap = null;
let geoMapMarker = null;
let geoMapCircle = null;
let geoNearbyMarkers = [];
let geoWatchId = null;
let geoLeafletPromise = null;
let lastGeoSaveAt = 0;
let lastGeoSaveLat = null;
let lastGeoSaveLon = null;
let geoDeckTimer = null;

function getFilterKm() {
  return clampFilterKm(document.getElementById('filterKm')?.value);
}

function onFilterKmChanged(debounceDeck) {
  syncFilterKmUi();
  persistFilterKm();
  refreshGeoMapRadius();
  if (debounceDeck) scheduleGeoDeckRefresh();
  else renderSwipeDeck().catch(() => {});
}

function setGeoMapHint(text) {
  const el = document.getElementById('geoMapHint');
  if (el) el.textContent = text;
}

function ensureLeafletLoaded() {
  if (window.L) return Promise.resolve();
  if (geoLeafletPromise) return geoLeafletPromise;
  geoLeafletPromise = new Promise((resolve, reject) => {
    if (!document.getElementById('leafletCss')) {
      const link = document.createElement('link');
      link.id = 'leafletCss';
      link.rel = 'stylesheet';
      link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
      document.head.appendChild(link);
    }
    if (window.L) {
      resolve();
      return;
    }
    const s = document.createElement('script');
    s.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
    s.onload = () => resolve();
    s.onerror = () => reject(new Error('leaflet'));
    document.head.appendChild(s);
  });
  return geoLeafletPromise;
}

function initGeoMap() {
  const el = document.getElementById('geoMap');
  if (!el || geoMap || !window.L) return;
  geoMap = L.map(el, { zoomControl: false, attributionControl: true }).setView([51.1657, 10.4515], 6);
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; OpenStreetMap'
  }).addTo(geoMap);
  L.control.zoom({ position: 'bottomright' }).addTo(geoMap);
}

function updateGeoMap(lat, lon, accuracyM) {
  if (!window.L) return;
  if (!geoMap) initGeoMap();
  if (!geoMap) return;
  const km = getFilterKm();
  const everywhere = isFilterEverywhere();
  if (!geoMapMarker) {
    geoMapMarker = L.circleMarker([lat, lon], {
      radius: 9,
      color: '#fff',
      weight: 2,
      fillColor: '#ff4d6d',
      fillOpacity: 1
    }).addTo(geoMap);
    geoMapCircle = L.circle([lat, lon], {
      radius: km * 1000,
      color: '#ff4d6d',
      fillColor: '#ff4d6d',
      fillOpacity: everywhere ? 0 : 0.1,
      opacity: everywhere ? 0 : 1,
      weight: 2
    }).addTo(geoMap);
  } else {
    geoMapMarker.setLatLng([lat, lon]);
    geoMapCircle.setLatLng([lat, lon]);
    geoMapCircle.setRadius(km * 1000);
    geoMapCircle.setStyle({
      fillOpacity: everywhere ? 0 : 0.1,
      opacity: everywhere ? 0 : 1,
    });
  }
  if (geoMap.getZoom() < 11 && !everywhere) geoMap.setView([lat, lon], 12);
  else if (everywhere && geoMap.getZoom() < 8) geoMap.setView([lat, lon], 8);
  else geoMap.panTo([lat, lon]);
  const acc = accuracyM != null && accuracyM > 0 ? ' · ±' + Math.round(accuracyM) + ' m' : '';
  setGeoMapHint(
    everywhere
      ? lat.toFixed(4) + ', ' + lon.toFixed(4) + acc + ' · Überall'
      : lat.toFixed(4) + ', ' + lon.toFixed(4) + acc
  );
  setTimeout(() => geoMap?.invalidateSize(), 150);
  if (swipeQueue.length) updateGeoMapNearby(swipeQueue);
}

function profilePhotoSrc(u) {
  if (!u) return '';
  ensureDiscoverPhotos(u);
  const src = (u.photos && u.photos.main) || u.photo || '';
  return src && !isStockPhotoUrl(src) ? src : '';
}

function geoNearbyMarkerIcon(photoUrl, name) {
  const inner = photoUrl
    ? `<img src="${escHtml(photoUrl)}" alt="">`
    : '<span class="geo-nearby-ph">💫</span>';
  return L.divIcon({
    className: 'geo-nearby-marker-wrap',
    html: `<div class="geo-nearby-marker" title="${escHtml(name)}">${inner}</div>`,
    iconSize: [44, 44],
    iconAnchor: [22, 22],
    popupAnchor: [0, -24],
  });
}

function clearGeoNearbyMarkers() {
  if (!geoMap || !geoNearbyMarkers.length) {
    geoNearbyMarkers = [];
    return;
  }
  geoNearbyMarkers.forEach((m) => {
    try { geoMap.removeLayer(m); } catch (e) {}
  });
  geoNearbyMarkers = [];
}

function focusDiscoverProfile(profileId) {
  if (!profileId || !swipeQueue.length) return;
  const idx = swipeQueue.findIndex((x) => x.o && x.o.id === profileId);
  if (idx < 0) return;
  if (idx > 0) {
    const [item] = swipeQueue.splice(idx, 1);
    swipeQueue.unshift(item);
  }
  renderSwipeDeckFromQueue();
}

function updateGeoMapNearby(queue) {
  const u = me();
  const strip = document.getElementById('geoNearbyStrip');
  const listEl = document.getElementById('geoNearbyList');
  const countEl = document.getElementById('geoNearbyCount');
  if (!u || u.lat == null || u.lon == null || !window.L || !geoMap) {
    strip?.classList.add('hidden');
    clearGeoNearbyMarkers();
    return;
  }
  const nearby = (queue || [])
    .map((x) => ({ o: ensureDiscoverPhotos(x.o), km: x.km }))
    .filter((x) => x.o && x.o.lat != null && x.o.lon != null)
    .sort((a, b) => (a.km ?? 9999) - (b.km ?? 9999))
    .slice(0, 16);
  clearGeoNearbyMarkers();
  if (!nearby.length) {
    strip?.classList.add('hidden');
    return;
  }
  strip?.classList.remove('hidden');
  if (countEl) {
    countEl.textContent = isFilterEverywhere()
      ? nearby.length + ' · Überall'
      : nearby.length + ' · ' + getFilterKm() + ' km';
  }
  nearby.forEach(({ o, km }) => {
    const photo = profilePhotoSrc(o);
    const marker = L.marker([o.lat, o.lon], { icon: geoNearbyMarkerIcon(photo, o.displayName) });
    const kmTxt = km != null ? Math.round(km) + ' km' : 'in der Nähe';
    marker.bindPopup(
      '<strong>' + escHtml(o.displayName) + ', ' + o.age + '</strong><br>' + escHtml(kmTxt)
    );
    marker.on('click', () => focusDiscoverProfile(o.id));
    marker.addTo(geoMap);
    geoNearbyMarkers.push(marker);
  });
  if (listEl) {
    listEl.innerHTML = nearby
      .map(({ o, km }) => {
        const photo = profilePhotoSrc(o);
        const kmTxt = km != null ? Math.round(km) + ' km' : '·';
        return (
          '<button type="button" class="geo-nearby-chip" data-nearby-id="' +
          escHtml(o.id) +
          '" title="' +
          escHtml(o.displayName) +
          '">' +
          (photo
            ? '<img src="' + escHtml(photo) + '" alt="">'
            : '<span class="geo-nearby-ph">💫</span>') +
          '<span class="geo-nearby-chip-name">' +
          escHtml(o.displayName) +
          '</span>' +
          '<span class="geo-nearby-chip-km">' +
          escHtml(kmTxt) +
          '</span></button>'
        );
      })
      .join('');
    listEl.querySelectorAll('[data-nearby-id]').forEach((btn) => {
      btn.onclick = () => {
        const id = btn.dataset.nearbyId;
        const item = nearby.find((x) => x.o.id === id);
        focusDiscoverProfile(id);
        if (item && geoMap) geoMap.panTo([item.o.lat, item.o.lon], { animate: true });
      };
    });
  }
}

function refreshGeoMapRadius() {
  const u = me();
  if (!geoMapCircle || u?.lat == null || u?.lon == null) return;
  const everywhere = isFilterEverywhere();
  const km = getFilterKm();
  geoMapCircle.setRadius(km * 1000);
  geoMapCircle.setStyle({
    fillOpacity: everywhere ? 0 : 0.1,
    opacity: everywhere ? 0 : 1,
  });
  if (geoMap && window.L) {
    if (everywhere) geoMap.setView([u.lat, u.lon], Math.max(geoMap.getZoom(), 8));
    else {
      const bounds = L.circle([u.lat, u.lon], { radius: km * 1000 }).getBounds();
      geoMap.fitBounds(bounds, { padding: [20, 20], maxZoom: 14 });
    }
  }
  if (swipeQueue.length) updateGeoMapNearby(swipeQueue);
}

function scheduleGeoDeckRefresh() {
  clearTimeout(geoDeckTimer);
  geoDeckTimer = setTimeout(() => renderSwipeDeck().catch(() => {}), 900);
}

async function saveGeoToServer(lat, lon) {
  const now = Date.now();
  if (
    now - lastGeoSaveAt < 30000 &&
    lastGeoSaveLat != null &&
    haversineKm(lastGeoSaveLat, lastGeoSaveLon, lat, lon) < 0.25
  ) {
    return;
  }
  lastGeoSaveAt = now;
  lastGeoSaveLat = lat;
  lastGeoSaveLon = lon;
  if (useDb()) {
    try { await JamiApi.updateMyProfile({ lat, lon }); } catch (e) { /* retry later */ }
  } else {
    saveState();
  }
}

function stopGeoWatch() {
  if (geoWatchId != null && navigator.geolocation?.clearWatch) {
    navigator.geolocation.clearWatch(geoWatchId);
    geoWatchId = null;
  }
}

function destroyGeoMap() {
  stopGeoWatch();
  clearTimeout(geoDeckTimer);
  geoDeckTimer = null;
  clearGeoNearbyMarkers();
  if (geoMap) {
    geoMap.remove();
    geoMap = null;
    geoMapMarker = null;
    geoMapCircle = null;
  }
  document.getElementById('geoMapCard')?.classList.add('hidden');
}

function swipeCardOverlayHtml(o, km) {
  const verified =
    o.verified || o.realPerson
      ? '<span class="swipe-card-verified" title="Verifiziert" aria-label="Verifiziert">✓</span>'
      : '';
  const metaParts = [];
  if (o.age != null) metaParts.push(String(o.age));
  if (km != null) metaParts.push(km.toFixed(1) + ' km');
  else if (o.city && !isPlaceholderCity(o.city)) metaParts.push('📍 ' + o.city);
  const metaLine = metaParts.join(' · ');
  return (
    '<div class="swipe-card-overlay">' +
    '<h3 class="swipe-card-name">' +
    escHtml(o.displayName || '') +
    vipGoldBadge(o) +
    verified +
    profileDisplayEmojiHtml(o) +
    '</h3>' +
    (metaLine ? '<p class="swipe-card-meta">' + escHtml(metaLine) + '</p>' : '') +
    '</div>'
  );
}

function buildSwipeCardHtml(o, km) {
  const tags = (o.interests || []).map(t => `<span>${t}</span>`).join('');
  if (!o.friendshipStatus && !useDb()) o.friendshipStatus = localFriendStatus(o.id);
  ensureDiscoverPhotos(o);
  const cityLine = isPlaceholderCity(o.city)
    ? ''
    : '<p class="meta">🌍 ' + escHtml(o.city) + ', ' + escHtml(o.country || 'DE') + '</p>';
  return `
    <div class="swipe-card-photo" data-open-profile="${o.id}" style="cursor:pointer">${buildPhotoGalleryHtml(o, true)}${swipeCardOverlayHtml(o, km)}</div>
    <div class="swipe-card-body">
      ${o.profileBoostActive ? '<span class="boost-badge">🚀 Im Rampenlicht</span>' : ''}
      ${activityBadgesHtml(o)}
      ${presenceBadgeHtml(o, false)}
      ${cityLine}
      <p>${escHtml(stripContactInfo(o.bio || ''))}</p>
      ${profileDetailsHtml(o)}
      ${o.prompt ? `<p class="swipe-prompt">„${escHtml(o.prompt)}"</p>` : ''}
      ${tags ? `<div class="interest-tags">${tags}</div>` : ''}
      ${friendActionHtml(o)}
      <button type="button" class="btn btn-primary btn-chat-pulse btn-chat-on-card" data-chat-with="${o.id}">💬 Jetzt chatten</button>
      <div class="card-actions-row">
        <button type="button" data-report="${o.id}">Melden</button>
        <button type="button" data-block="${o.id}">Blockieren</button>
      </div>
    </div>`;
}

async function renderSwipeDeck() {
  const stack = document.getElementById('swipeStack');
  const actions = document.getElementById('swipeActions');
  if (!stack) return;
  stack.innerHTML = jamiLoaderHtml('Profile werden geladen…');
  if (actions) actions.classList.add('hidden');
  try {
    swipeQueue = await getDiscoverQueueAsync();
  } catch (e) {
    stack.innerHTML =
      '<div class="swipe-empty"><p>Profile konnten nicht geladen werden.</p><p class="meta">Bitte Seite neu laden.</p></div>';
    updateGeoMapNearby([]);
    return;
  }
  renderSwipeDeckFromQueue();
}

function renderSwipeDeckFromQueue() {
  const stack = document.getElementById('swipeStack');
  const actions = document.getElementById('swipeActions');
  if (!stack) return;
  updateGeoMapNearby(swipeQueue);
  stack.innerHTML = '';
  if (!swipeQueue.length) {
    actions?.classList.add('hidden');
    document.getElementById('swipeHint')?.classList.add('hidden');
    stack.innerHTML = '<div class="swipe-empty"><p>Keine Singles gefunden.</p><p class="meta">Umkreis vergrößern, „Überall“ aktivieren oder Alter anpassen.</p></div>';
    currentSwipeId = null;
    return;
  }
  actions?.classList.remove('hidden');
  const { o, km } = swipeQueue[0];
  currentSwipeId = o.id;
  const card = document.createElement('div');
  card.className = 'swipe-card swipe-card--compact';
  card.id = 'activeSwipeCard';
  card.innerHTML = buildSwipeCardHtml(o, km);
  const openSessionFromCard = e => {
    if (e.target.closest('[data-send-friend],[data-accept-friend],[data-reject-friend],[data-block],[data-report],[data-chat-with],button')) return;
    openChatSession(o.id, o);
  };
  card.querySelector('[data-open-profile]')?.addEventListener('click', openSessionFromCard);
  card.querySelector('.swipe-card-body')?.addEventListener('click', openSessionFromCard);
  bindFriendButtons(card);
  card.querySelector('[data-chat-with]')?.addEventListener('click', e => {
    e.stopPropagation();
    handleChatClick(o.id);
  });
  card.querySelector('[data-block]')?.addEventListener('click', e => { e.stopPropagation(); blockUser(o.id, o.displayName); });
  card.querySelector('[data-report]')?.addEventListener('click', e => { e.stopPropagation(); reportUser(o.id); });
  bindSwipeGestures(card);
  stack.appendChild(card);
  document.getElementById('swipeHint')?.classList.remove('hidden');
}

function bindSwipeGestures(card) {
  if (!card || card.dataset.swipeBound) return;
  card.dataset.swipeBound = '1';
  let startX = 0;
  let startY = 0;
  let dragging = false;
  let active = false;
  let suppressClickUntil = 0;
  const isCoarse = typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches;
  const threshold = isCoarse ? 56 : 68;

  const resetDragStyles = () => {
    card.style.transition = '';
    card.style.transform = '';
    card.classList.remove('swipe-drag-pass', 'swipe-drag-like');
  };

  const onStart = (clientX, clientY) => {
    startX = clientX;
    startY = clientY;
    dragging = true;
    active = false;
    card.style.transition = 'none';
  };

  const onMove = (clientX, clientY, e) => {
    if (!dragging) return;
    const dx = clientX - startX;
    const dy = clientY - startY;
    if (!active && Math.abs(dx) < 6 && Math.abs(dy) < 6) return;
    if (!active && Math.abs(dy) > Math.abs(dx) * 1.15) {
      dragging = false;
      resetDragStyles();
      return;
    }
    active = true;
    if (e?.cancelable) e.preventDefault();
    const rot = dx * 0.05;
    card.style.transform = `translateX(${dx}px) rotate(${rot}deg)`;
    card.classList.toggle('swipe-drag-pass', dx < -18);
    card.classList.toggle('swipe-drag-like', dx > 18);
  };

  const onEnd = (clientX) => {
    if (!dragging) return;
    dragging = false;
    const dx = clientX - startX;
    card.classList.remove('swipe-drag-pass', 'swipe-drag-like');
    if (active) suppressClickUntil = Date.now() + 450;
    if (active && dx <= -threshold) {
      resetDragStyles();
      swipeNope();
      return;
    }
    if (active && dx >= threshold) {
      resetDragStyles();
      swipeLike(false);
      return;
    }
    resetDragStyles();
  };

  card.addEventListener('click', (e) => {
    if (Date.now() < suppressClickUntil) {
      e.preventDefault();
      e.stopImmediatePropagation();
    }
  }, true);

  card.addEventListener('pointerdown', (e) => {
    if (e.button !== 0) return;
    if (e.target.closest('button,a,input,textarea,select,label')) return;
    card.setPointerCapture(e.pointerId);
    onStart(e.clientX, e.clientY);
  });
  card.addEventListener('pointermove', (e) => {
    if (!dragging) return;
    onMove(e.clientX, e.clientY, e);
  });
  card.addEventListener('pointerup', (e) => onEnd(e.clientX));
  card.addEventListener('pointercancel', (e) => onEnd(e.clientX));

  let touchId = null;
  card.addEventListener('touchstart', (e) => {
    if (e.touches.length !== 1) return;
    if (e.target.closest('button,a,input,textarea,select,label')) return;
    touchId = e.touches[0].identifier;
    onStart(e.touches[0].clientX, e.touches[0].clientY);
  }, { passive: true });
  card.addEventListener('touchmove', (e) => {
    if (touchId == null) return;
    const t = Array.from(e.touches).find(x => x.identifier === touchId);
    if (!t) return;
    onMove(t.clientX, t.clientY, e);
  }, { passive: false });
  card.addEventListener('touchend', (e) => {
    if (touchId == null) return;
    const t = Array.from(e.changedTouches).find(x => x.identifier === touchId);
    if (t) onEnd(t.clientX);
    touchId = null;
  });
  card.addEventListener('touchcancel', () => {
    touchId = null;
    dragging = false;
    resetDragStyles();
  });
}

function animateSwipe(dir, cb) {
  const card = document.getElementById('activeSwipeCard');
  if (card) {
    card.style.transform = '';
    card.style.transition = '';
    card.classList.remove('swipe-drag-pass', 'swipe-drag-like');
    card.classList.add(dir === 'left' ? 'leaving-left' : 'leaving-right');
    setTimeout(cb, 320);
  } else cb();
}

async function recordPass(otherId) {
  if (isNearbySeedId(otherId)) {
    if (useDb()) {
      try { await JamiApi.recordPass(otherId); } catch (e) { /* preview pass */ }
    } else if (!myPasses().includes(otherId)) myPasses().push(otherId);
    if (!useDb()) saveState();
    return;
  }
  if (useDb()) {
    await JamiApi.recordPass(otherId);
    return;
  }
  if (!myPasses().includes(otherId)) myPasses().push(otherId);
  saveState();
}

async function recordLike(otherId, superLike) {
  if (isNearbySeedId(otherId)) {
    toast('Beispiel-Profil – echte Singles erscheinen, sobald sich andere in deiner Nähe registrieren.');
    if (!useDb() && !myLikes().includes(otherId)) myLikes().push(otherId);
    if (!useDb()) saveState();
    return false;
  }
  if (useDb()) {
    const { mutual, other } = await JamiApi.recordLike(otherId, superLike);
    if (other) cacheProfile(other);
    if (mutual) {
      showMatchModal(other);
    } else {
      toast(superLike ? '⭐ Super-Like gesendet!' : '💕 Like! Bei Gegenseitigkeit Match.');
    }
    updateLikesBadge();
    await renderLikes();
    markPushPromptEligible();
    maybeShowPushPrompt();
    return mutual;
  }
  if (!myLikes().includes(otherId)) myLikes().push(otherId);
  if (superLike) {
    if (!state.superLikes[state.session]) state.superLikes[state.session] = [];
    state.superLikes[state.session].push(otherId);
  }
  saveState();
  const other = state.users.find(u => u.id === otherId);
  const mutual = isMutual(otherId);
  if (mutual) {
    showMatchModal(other);
  } else {
    toast(superLike ? '⭐ Super-Like gesendet!' : '💕 Like! Bei Gegenseitigkeit Match.');
  }
  updateLikesBadge();
  await renderLikes();
  markPushPromptEligible();
  maybeShowPushPrompt();
  return mutual;
}

function swipeNope() {
  if (!currentSwipeId) return;
  const id = currentSwipeId;
  animateSwipe('left', async () => {
    await recordPass(id);
    await renderSwipeDeck();
  });
}

function swipeLike(superLike) {
  if (!currentSwipeId) return;
  const id = currentSwipeId;
  const u = me();
  if (superLike) {
    const cost = config.superLikeCost || 5;
    if (u.coins < cost) { toast('Super-Like: ' + cost + ' Coins nötig'); return; }
    u.coins -= cost;
    if (useDb()) JamiApi.updateCoins(-cost).catch(() => {});
    updateCoinUI();
  }
  animateSwipe('right', async () => {
    await recordLike(id, superLike);
    await renderSwipeDeck();
  });
}

async function blockUser(otherId, displayName) {
  const name = displayName || findUser(otherId)?.displayName || resolveProfileUser(otherId)?.displayName || 'diesen Nutzer';
  if (!confirm(name + ' blockieren?\n\nIhr seht euch danach nicht mehr und könnt nicht mehr chatten.')) return;
  try {
    if (useDb()) await JamiApi.blockUser(otherId);
    else {
      if (!myBlocked().includes(otherId)) myBlocked().push(otherId);
      saveState();
    }
    toast('Nutzer blockiert');
    closeProfilePhotoModal();
    if (activeChatSessionId === otherId) closeChatSession();
    await renderSwipeDeck();
    await renderLikes();
    await renderMatches();
  } catch (e) {
    toast(e.message || 'Blockieren fehlgeschlagen');
  }
}

async function reportUser(otherId) {
  const other = findUser(otherId);
  if (useDb()) await JamiApi.reportUser(otherId, 'Meldung aus App');
  toast('Meldung gespeichert. Profil: ' + (other?.displayName || otherId));
}

function showMatchModal(other) {
  const u = me();
  if (!other) return;
  cacheProfile(other);
  pendingMatchOtherId = other.id;
  const free = matchFreeMessageCount();
  const title = document.getElementById('matchTitle');
  const sub = document.getElementById('matchSubtitle');
  const badge = document.getElementById('matchBonusBadge');
  if (title) title.textContent = 'Ihr habt ein Match! 💕';
  if (sub) {
    sub.textContent =
      'Die ersten ' +
      free +
      ' Nachrichten in diesem Chat sind kostenlos!';
  }
  if (badge) badge.textContent = '🎁 ' + free + ' Gratis-Nachricht' + (free === 1 ? '' : 'en');
  const box = document.getElementById('matchAvatars');
  box.innerHTML = `${avatarBlockHtml(u, true)}${avatarBlockHtml(other, false)}`;
  const modal = document.getElementById('matchModal');
  modal.classList.remove('hidden');
  modal.classList.add('match-modal-celebrate');
  markMatchSeen(other.id);
  refreshInboxBadges().catch(() => {});
}

function hideMatchModal() {
  const modal = document.getElementById('matchModal');
  modal.classList.add('hidden');
  modal.classList.remove('match-modal-celebrate');
  pendingMatchOtherId = null;
}

document.getElementById('btnMatchClose').onclick = hideMatchModal;
document.getElementById('btnMatchProfile').onclick = async () => {
  if (!pendingMatchOtherId) return;
  const id = pendingMatchOtherId;
  const o = resolveProfileUser(id);
  hideMatchModal();
  await openChatSafe(id, o);
};
document.getElementById('btnMatchChat').onclick = async () => {
  if (!pendingMatchOtherId) return;
  const id = pendingMatchOtherId;
  hideMatchModal();
  await openChatSafe(id, resolveProfileUser(id));
};
document.getElementById('btnOpenChats')?.addEventListener('click', () => openChatsTab());
document.getElementById('btnNope').onclick = swipeNope;
document.getElementById('btnLike').onclick = () => swipeLike(false);
document.getElementById('btnSuper').onclick = () => swipeLike(true);
initFilterKm();
const filterKmEl = document.getElementById('filterKm');
if (filterKmEl) {
  filterKmEl.addEventListener('input', () => onFilterKmChanged(true));
  filterKmEl.addEventListener('change', () => onFilterKmChanged(false));
}
document.getElementById('filterEverywhere')?.addEventListener('change', () => onFilterKmChanged(false));
['filterAgeMin', 'filterAgeMax'].forEach(id => {
  const el = document.getElementById(id);
  if (el) el.onchange = () => renderSwipeDeck().catch(() => {});
});

function appendLikeCardRow(list, o, mutual) {
  cacheProfile(o);
  const el = document.createElement('div');
  el.className = 'like-card' + (mutual ? ' like-card-match' : '');
  el.innerHTML =
    `${avatarBlockHtml(o, false)}` +
    `<div style="flex:1"><strong>${profileNameLineHtml(o)}</strong>` +
    `${activityBadgesHtml(o)}` +
    `<br>${presenceBadgeHtml(o, false)}` +
    `<br><span class="meta">🌍 ${escHtml(o.city || '')}</span>` +
    (mutual
      ? '<br><span class="match-badge">💕 Match – jetzt chatten</span>'
      : '<br><span class="meta">Mag dich – Like zurück für Match</span>') +
    '</div>';
  const actions = document.createElement('div');
  actions.className = 'match-card-actions';
  if (mutual) {
    const btnChat = document.createElement('button');
    btnChat.type = 'button';
    btnChat.className = 'btn btn-primary btn-chat-pulse';
    btnChat.style.width = '100%';
    btnChat.textContent = '💬 Jetzt chatten';
    btnChat.onclick = async (e) => {
      e.stopPropagation();
      await openChatSafe(o.id, o);
    };
    actions.appendChild(btnChat);
  } else {
    const btnProfile = document.createElement('button');
    btnProfile.type = 'button';
    btnProfile.className = 'btn btn-ghost';
    btnProfile.textContent = '👤 Profil';
    btnProfile.onclick = (e) => {
      e.stopPropagation();
      openChatSafe(o.id, o);
    };
    const btnLike = document.createElement('button');
    btnLike.type = 'button';
    btnLike.className = 'btn btn-primary';
    btnLike.textContent = '💕 Like zurück';
    btnLike.onclick = async (e) => {
      e.stopPropagation();
      await recordLike(o.id, false);
    };
    actions.appendChild(btnProfile);
    actions.appendChild(btnLike);
  }
  el.appendChild(actions);
  el.onclick = () => openChatSafe(o.id, o);
  list.appendChild(el);
}

async function renderLikes() {
  const matchesBox = document.getElementById('matchesList');
  const list = document.getElementById('likesList');
  if (!list) return;
  if (matchesBox) matchesBox.innerHTML = jamiLoaderHtml('Matches laden…', true);
  list.innerHTML = jamiLoaderHtml('Likes werden geladen…', true);
  if (useDb()) await JamiApi.refreshSocial();
  const incomingAll = JamiApi.mode === 'cloudflare'
    ? await JamiApi.getIncomingLikesCloud()
    : useDb()
    ? JamiApi.getIncomingLikes()
    : state.users.filter((o) =>
        o.id !== state.session && theyLikedMe(o.id) && !myBlocked().includes(o.id)
      );
  let matches = await getMutualMatchProfiles();
  const seen = new Set(matches.map((m) => m.id));
  incomingAll.forEach((o) => {
    if (isMutual(o.id) && !seen.has(o.id)) {
      matches.push(o);
      seen.add(o.id);
    }
  });
  if (matchesBox) matchesBox.innerHTML = '';
  list.innerHTML = '';
  if (matchesBox && matches.length) {
    const h = document.createElement('p');
    h.className = 'match-list-heading';
    h.textContent = '💕 Deine Matches';
    matchesBox.appendChild(h);
    matches.forEach((o) => appendMatchCardRow(matchesBox, o));
  }
  const incoming = incomingAll.filter((o) => !isMutual(o.id));
  if (!incoming.length && !matches.length) {
    list.innerHTML = '<p class="meta">Noch keine Likes – unter Entdecken swipen!</p>';
    return;
  }
  if (matches.length) {
    const hm = document.createElement('p');
    hm.className = 'match-list-heading';
    hm.textContent = '💕 Matches – Jetzt chatten';
    list.appendChild(hm);
    matches.forEach((o) => appendLikeCardRow(list, o, true));
  }
  if (incoming.length) {
    const h2 = document.createElement('p');
    h2.className = 'match-list-heading';
    h2.style.marginTop = matches.length ? '1rem' : '0';
    h2.textContent = 'Mag dich – Like zurück = Match';
    list.appendChild(h2);
    incoming.forEach((o) => appendLikeCardRow(list, o, false));
  }
}

async function startMatch(otherId) {
  await ensureChatThread(otherId);
  await openChatSession(otherId, resolveProfileUser(otherId));
}

async function openChat(otherId) {
  await openChatSession(otherId, resolveProfileUser(otherId));
}

async function sendMessage(otherId, opts = {}) {
  const u = me();
  const inputId = opts.inputId || 'chatInput';
  const inputEl = document.getElementById(inputId);
  const raw = opts.body != null ? String(opts.body) : (inputEl?.value || '').trim();
  const text = sanitizeChatMessage(upgradeLegacyShopEmojiTokens(raw));
  if (!text) {
    toast(opts.body != null ? 'Foto ungültig' : 'Nachricht eingeben');
    return;
  }
  const costNeeded = messageCostForChat(otherId);
  if (costNeeded > 0 && (u.coins || 0) < costNeeded) {
    await handleInsufficientCoins();
    return;
  }
  let next, cost;
  if (JamiApi.mode === 'cloudflare') {
    try {
      const j = await JamiApi.sendMessageCloud(otherId, text);
      setThreadChatMeta(otherId, {
        msgCount: j.msgCount,
        isMutualMatch: j.isMutualMatch,
        freeMessagesLeft: j.freeMessagesLeft,
      });
      if (j.messageWasFree) toast('💕 Gratis Match-Nachricht gesendet');
      await JamiApi.refreshMyProfile();
    } catch (e) {
      const msg = e.message || 'Senden fehlgeschlagen';
      if (msg.includes('Coins') || msg.includes('coin')) {
        await handleInsufficientCoins();
      } else toast(msg);
      return;
    }
  } else if (useDb()) {
    const { thread } = await JamiApi.fetchMessages(otherId);
    cost = messageCostForChat(otherId);
    if (cost > 0 && u.coins < cost) {
      await handleInsufficientCoins();
      return;
    }
    if (cost > 0) {
      u.coins -= cost;
      await JamiApi.updateCoins(-cost);
    } else if (isMutual(otherId)) toast('💕 Gratis Match-Nachricht gesendet');
    await JamiApi.sendMessage(otherId, text);
    setThreadChatMeta(otherId, { msgCount: (thread?.msg_count || getThreadMsgCount(otherId)) + 1 });
  } else {
    const key = chatKeyLocal(otherId);
    if (!state.chats[key]) state.chats[key] = { msgs: [], msgCount: 0 };
    const chat = state.chats[key];
    next = (chat.msgCount || 0) + 1;
    cost = messageCostForChat(otherId);
    if (cost > 0 && u.coins < cost) {
      await handleInsufficientCoins();
      return;
    }
    if (cost > 0) u.coins -= cost;
    else if (isMutual(otherId)) toast('💕 Gratis Match-Nachricht gesendet');
    chat.msgCount = next;
    chat.msgs.push({ from: state.session, text, at: Date.now() });
    const freeLeft = isMutual(otherId) ? Math.max(0, matchFreeMessageCount() - next) : 0;
    setThreadChatMeta(otherId, { msgCount: next, freeMessagesLeft: freeLeft });
    saveState();
  }
  if (inputEl && opts.body == null) inputEl.value = '';
  signalChatTyping(otherId, false);
  updateCoinUI();
  if (opts.messagesBoxId) {
    await loadMessagesIntoBox(otherId, document.getElementById(opts.messagesBoxId));
    pollChatLiveStatus(otherId);
    if (opts.costHintId) updateChatCostHint(opts.costHintId, otherId);
    return;
  }
  await openChat(otherId);
  if (activeChatSessionId === otherId) {
    await loadMessagesIntoBox(otherId, document.getElementById('chatSessionMessages'));
    updateChatCostHint('chatSessionCostHint', otherId);
  }
  markPushPromptEligible();
  maybeShowPushPrompt();
}

function renderMatchesEmpty(list) {
  list.innerHTML = '';
  const card = document.createElement('div');
  card.className = 'card';
  card.innerHTML =
    '<p class="meta" style="margin:0">Noch keine Gespräche.</p>' +
    '<p class="meta" style="margin:0.5rem 0 0">Unter <strong>Entdecken</strong> Profile anklicken und direkt chatten (10 Coins/Nachricht).</p>';
  list.appendChild(card);
}

function appendChatListButton(list, otherId, other, unreadCount) {
  const btn = document.createElement('button');
  btn.type = 'button';
  btn.className = 'btn btn-primary' + (unreadCount ? '' : ' btn-chat-pulse');
  const unreadNote = unreadCount ? ' · ' + (unreadCount > 1 ? unreadCount + ' neu' : 'neu') : '';
  btn.textContent = '💬 Chat mit ' + other.displayName + ' (' + other.city + ')' + unreadNote;
  btn.onclick = () => openChatSession(otherId, other);
  list.appendChild(btn);
}

async function renderMatches() {
  const list = document.getElementById('matchList');
  if (!list) return;
  list.innerHTML = jamiLoaderHtml('Chats werden geladen…', true);
  const matches = await getMutualMatchProfiles();
  const matchIds = new Set(matches.map((m) => m.id));
  let partners = [];
  if (JamiApi.mode === 'cloudflare') {
    partners = await JamiApi.listChatPartnersCloud();
  } else if (useDb()) {
    partners = await JamiApi.listChatPartners();
  } else {
    partners = Object.keys(state.chats).map((key) => {
      const ids = key.split('_');
      const otherId = ids.find((id) => id !== state.session);
      const other = state.users.find((u) => u.id === otherId);
      return other ? { otherId, other } : null;
    }).filter(Boolean);
  }
  const extra = partners.filter((p) => !matchIds.has(p.otherId));
  const unreadById = {};
  partners.forEach((p) => {
    unreadById[p.otherId] = p.unreadCount || 0;
  });
  const seen = getSeenMatchIds();
  list.innerHTML = '';
  if (matches.length) {
    const h = document.createElement('p');
    h.className = 'match-list-heading';
    h.textContent = '💕 Matches – Profil oder Chat';
    list.appendChild(h);
    matches.forEach((o) =>
      appendMatchCardRow(list, o, {
        unread: unreadById[o.id] || 0,
        isNewMatch: !seen.has(o.id) && !(unreadById[o.id] || 0),
      })
    );
  }
  if (extra.length) {
    if (matches.length) {
      const h2 = document.createElement('p');
      h2.className = 'match-list-heading';
      h2.style.marginTop = '1rem';
      h2.textContent = 'Weitere Chats';
      list.appendChild(h2);
    }
    const blocked = new Set(myBlocked());
    extra
      .filter((p) => !blocked.has(p.otherId))
      .forEach(({ otherId, other, unreadCount }) => appendChatListButton(list, otherId, other, unreadCount || 0));
  }
  if (!matches.length && !partners.length) renderMatchesEmpty(list);
}

function adTodayKey() {
  return new Date().toISOString().slice(0, 10);
}

function getAdLocalState() {
  state.adReward = state.adReward || { day: '', count: 0, pending: null };
  if (state.adReward.day !== adTodayKey()) {
    state.adReward = { day: adTodayKey(), count: 0, pending: null };
  }
  return state.adReward;
}

async function refreshAdRewardStatus() {
  adCfg = { ...(config.adReward || {}), coinsPerVideo: 1, secondsWatch: 15, dailyMax: 10, freeInstant: false };
  if (useDb() && JamiApi.myProfile) {
    try {
      const s = await JamiApi.getAdRewardStatus();
      adStatus = { todayCount: s.todayCount || 0, remaining: s.remaining ?? 0 };
      adCfg.secondsWatch = s.secondsRequired || adCfg.secondsWatch;
      adCfg.dailyMax = s.dailyMax || adCfg.dailyMax;
      adCfg.coinsPerVideo = s.coinsPerVideo || 1;
    } catch (e) {
      adStatus = { todayCount: 0, remaining: adCfg.dailyMax };
    }
  } else {
    const loc = getAdLocalState();
    adStatus = {
      todayCount: loc.count,
      remaining: Math.max(0, adCfg.dailyMax - loc.count),
    };
  }
  renderAdRewardCard();
}

function isAdRewardFreeInstant() {
  const cfg = { ...(config.adReward || {}), ...adCfg };
  if (cfg.freeInstant === true) return true;
  return (cfg.secondsWatch ?? 0) <= 0;
}

function renderAdRewardCard() {
  const card = document.getElementById('adRewardCard');
  const hint = document.getElementById('adRewardHint');
  const btn = document.getElementById('btnWatchAd');
  if (!card || !me()) return;
  if (config.adReward && config.adReward.enabled === false) {
    card.classList.add('hidden');
    return;
  }
  card.classList.remove('hidden');
  const left = adStatus.remaining ?? Math.max(0, adCfg.dailyMax - (adStatus.todayCount || 0));
  const free = isAdRewardFreeInstant();
  if (hint) {
    hint.textContent = free
      ? adCfg.coinsPerVideo +
        ' Coin gratis · heute noch ' +
        left +
        ' von ' +
        adCfg.dailyMax +
        ' · ohne Video'
      : 'YouTube: ' +
        adCfg.coinsPerVideo +
        ' Coin pro Video · ' +
        adCfg.secondsWatch +
        ' Sek. · heute noch ' +
        left +
        ' von ' +
        adCfg.dailyMax;
  }
  if (btn) {
    btn.disabled = left <= 0;
    btn.textContent =
      left <= 0
        ? 'Tageslimit erreicht'
        : free
          ? 'Gratis-Coin holen (+' + adCfg.coinsPerVideo + ' Coin)'
          : 'YouTube ansehen (+' + adCfg.coinsPerVideo + ' Coin)';
  }
}

function closeAdModal() {
  if (adTimerIv) clearInterval(adTimerIv);
  adTimerIv = null;
  adWatchToken = null;
  const modal = document.getElementById('adModal');
  if (modal) {
    modal.classList.add('hidden');
    modal.setAttribute('aria-hidden', 'true');
  }
}

function closeNoCoinsPopup() {
  const modal = document.getElementById('noCoinsModal');
  if (!modal) return;
  modal.classList.add('hidden');
  modal.setAttribute('aria-hidden', 'true');
}

async function showNoCoinsPopup() {
  const modal = document.getElementById('noCoinsModal');
  if (!modal) {
    document.querySelector('[data-tab="shop"]')?.click();
    return;
  }
  try {
    await refreshAdRewardStatus();
  } catch (e) {}
  const canWatchAd = (adStatus.remaining ?? 0) > 0;
  const lead = document.getElementById('noCoinsLead');
  const sub = document.getElementById('noCoinsSub');
  const watchBtn = document.getElementById('btnNoCoinsWatchVideo');
  const coinReward = adCfg.coinsPerVideo || 1;
  const free = isAdRewardFreeInstant();
  if (lead) {
    lead.innerHTML = canWatchAd
      ? free
        ? 'Hol dir <strong>' +
          coinReward +
          ' Coin' +
          (coinReward === 1 ? '' : 's') +
          ' gratis</strong> – sofort, ohne Video!'
        : 'Schau dir ein schnelles Video an und erhalte <strong>' +
          coinReward +
          ' Coin' +
          (coinReward === 1 ? '' : 's') +
          ' gratis</strong>!'
      : 'Heute sind alle Gratis-Boni aufgebraucht – im Shop bekommst du mehr Coins.';
  }
  if (watchBtn && canWatchAd) {
    watchBtn.textContent = free
      ? '🪙 Gratis-Coin holen (+' + coinReward + ' Coin)'
      : '📺 YouTube ansehen (+' + coinReward + ' Coin gratis)';
  }
  if (sub) {
    sub.textContent = canWatchAd
      ? free
        ? 'Ein Tipp – Coin ist sofort auf deinem Konto.'
        : 'Danach kannst du direkt weiter chatten – ohne die App zu verlassen.'
      : 'Oder warte bis morgen für einen neuen Gratis-Bonus.';
  }
  if (watchBtn) watchBtn.style.display = canWatchAd ? '' : 'none';
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
}

async function handleInsufficientCoins() {
  await showNoCoinsPopup();
}

function pickAdYoutubeVideoId() {
  const ids = (config.adReward && config.adReward.youtubeVideoIds) || [];
  const pool = ids.length ? ids : ['jfKfPfyJRdk', '5qap5aOk4XI'];
  return pool[Math.floor(Math.random() * pool.length)];
}

function fillAdSlot() {
  const slot = document.getElementById('adSlot');
  if (!slot) return;
  const videoId = pickAdYoutubeVideoId();
  const params = 'rel=0&modestbranding=1&playsinline=1&autoplay=1&mute=1';
  slot.className = 'ad-slot ad-slot--youtube';
  slot.innerHTML =
    '<div class="ad-youtube-wrap">' +
    '<iframe class="ad-youtube-frame" src="https://www.youtube-nocookie.com/embed/' +
    escHtml(videoId) +
    '?' +
    params +
    '" title="YouTube Video" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe>' +
    '</div>' +
    '<p class="meta ad-youtube-hint">Video kurz ansehen – Timer läuft unten weiter.</p>';
}

function runAdCountdown(seconds, onDone) {
  const bar = document.getElementById('adProgressBar');
  const txt = document.getElementById('adTimerText');
  const claim = document.getElementById('btnAdClaim');
  let left = seconds;
  if (claim) claim.disabled = true;
  if (bar) bar.style.width = '0%';
  if (adTimerIv) clearInterval(adTimerIv);
  adTimerIv = setInterval(() => {
    left -= 1;
    const pct = Math.min(100, ((seconds - left) / seconds) * 100);
    if (bar) bar.style.width = pct + '%';
    if (txt) txt.textContent = left > 0 ? 'Noch ' + left + ' Sekunden…' : 'Fertig – Coin holen!';
    if (left <= 0) {
      clearInterval(adTimerIv);
      adTimerIv = null;
      if (claim) claim.disabled = false;
      if (onDone) onDone();
    }
  }, 1000);
}

async function claimFreeAdRewardInstant() {
  const u = me();
  if (!u) {
    toast('Bitte einloggen');
    return;
  }
  await refreshAdRewardStatus();
  if ((adStatus.remaining ?? 0) <= 0) {
    toast('Tageslimit erreicht – morgen wieder');
    return;
  }
  if (useDb()) {
    try {
      const s = await JamiApi.startAdWatch();
      adWatchToken = s.token;
      await claimAdReward();
    } catch (e) {
      toast(e.message || 'Gratis-Coin nicht verfügbar');
    }
    return;
  }
  adWatchToken = 'local_' + Date.now();
  const loc = getAdLocalState();
  loc.pending = { token: adWatchToken, startedAt: Date.now() };
  saveState();
  await claimAdReward();
}

async function startAdWatchFlow() {
  const u = me();
  if (!u) {
    toast('Bitte einloggen');
    return;
  }
  await refreshAdRewardStatus();
  if ((adStatus.remaining ?? 0) <= 0) {
    toast('Tageslimit erreicht – morgen wieder');
    return;
  }
  if (isAdRewardFreeInstant()) {
    closeNoCoinsPopup();
    await claimFreeAdRewardInstant();
    return;
  }
  const secs = adCfg.secondsWatch || 15;
  let token = null;
  let startedAt = null;
  if (useDb()) {
    try {
      const s = await JamiApi.startAdWatch();
      token = s.token;
      adWatchToken = token;
    } catch (e) {
      toast(e.message || 'Werbung nicht verfügbar');
      return;
    }
  } else {
    const loc = getAdLocalState();
    if (loc.count >= adCfg.dailyMax) {
      toast('Tageslimit erreicht');
      return;
    }
    token = 'local_' + Date.now();
    startedAt = Date.now();
    loc.pending = { token, startedAt };
    adWatchToken = token;
    saveState();
  }
  const modal = document.getElementById('adModal');
  modal.classList.remove('hidden');
  modal.setAttribute('aria-hidden', 'false');
  fillAdSlot();
  runAdCountdown(secs);
}

async function claimAdReward() {
  const u = me();
  if (!u || !adWatchToken) return;
  const claim = document.getElementById('btnAdClaim');
  if (claim) claim.disabled = true;
  try {
    let credited = adCfg.coinsPerVideo || 1;
    let extraMsg = '';
    if (useDb()) {
      const j = await JamiApi.completeAdWatch(adWatchToken);
      u.coins = j.coins;
      if (JamiApi.myProfile) JamiApi.myProfile.coins = j.coins;
      adStatus = { todayCount: j.todayCount, remaining: j.remaining };
      credited = j.credited || 1;
      extraMsg = ' · Noch ' + j.remaining + ' Videos heute';
    } else {
      const loc = getAdLocalState();
      const p = loc.pending;
      if (!p || p.token !== adWatchToken) throw new Error('Bitte Video neu starten');
      const elapsed = (Date.now() - p.startedAt) / 1000;
      if (elapsed < (adCfg.secondsWatch || 15) - 0.5) throw new Error('Video noch nicht fertig');
      loc.count += 1;
      loc.pending = null;
      u.coins += credited;
      saveState();
      adStatus = {
        todayCount: loc.count,
        remaining: Math.max(0, adCfg.dailyMax - loc.count),
      };
    }
    closeAdModal();
    celebrateCoinReward(credited, {
      message: '+' + credited + ' Coin!' + extraMsg,
      originEl: claim || document.getElementById('adModal'),
    });
    renderProfile();
    renderAdRewardCard();
  } catch (e) {
    toast(e.message || 'Coin konnte nicht gutgeschrieben werden');
    if (claim) claim.disabled = false;
  }
}

let shopWelcomeCountdownTimer = null;

function parseProfileCreatedMs(u) {
  const at = u?.createdAt;
  if (at == null || at === '') return null;
  if (typeof at === 'number') return at;
  const s = String(at).trim();
  const t = Date.parse(s.includes('T') ? s : s.replace(' ', 'T') + 'Z');
  return Number.isFinite(t) ? t : null;
}

function getShopWelcomeOffer() {
  const u = me();
  if (!u) return null;
  const created = parseProfileCreatedMs(u);
  if (created == null) return null;
  const hours = config.shopWelcome?.newUserHours ?? 24;
  if (Date.now() - created > hours * 60 * 60 * 1000) return null;
  const mins = config.shopWelcome?.countdownMinutes ?? 15;
  let deadline = 0;
  try {
    deadline = parseInt(localStorage.getItem('jami_shop_welcome_deadline') || '0', 10);
  } catch (e) {}
  if (!deadline) {
    deadline = Date.now() + mins * 60 * 1000;
    try {
      localStorage.setItem('jami_shop_welcome_deadline', String(deadline));
    } catch (e) {}
  }
  if (Date.now() >= deadline) return { active: false, expired: true };
  return {
    active: true,
    deadline,
    bonus: config.shopWelcome?.bonusCoinsPerPack ?? 20,
  };
}

function packCoinsTotal(p, welcome) {
  const base = p.coins || 0;
  if (welcome?.active) return base + (welcome.bonus || 0);
  return base;
}

function findCoinPack(packId) {
  return (config.coinPacks || []).find(function (p) { return p.id === packId; }) || null;
}

function getShopPayMethod() {
  const active = document.querySelector('#payMethodPicker button.is-active');
  return active?.dataset?.pay || state.shopPayMethod || 'all';
}

function initPayMethodPicker() {
  const picker = document.getElementById('payMethodPicker');
  if (!picker || picker.dataset.bound) return;
  picker.dataset.bound = '1';
  picker.addEventListener('click', function (e) {
    const btn = e.target.closest('button[data-pay]');
    if (!btn) return;
    picker.querySelectorAll('button').forEach(function (b) { b.classList.remove('is-active'); });
    btn.classList.add('is-active');
    state.shopPayMethod = btn.dataset.pay || 'all';
    saveState();
  });
  const saved = state.shopPayMethod || 'all';
  const match = picker.querySelector('button[data-pay="' + saved + '"]');
  if (match) {
    picker.querySelectorAll('button').forEach(function (b) { b.classList.remove('is-active'); });
    match.classList.add('is-active');
  }
}

async function triggerPackCheckout(packId, btn) {
  const p = findCoinPack(packId);
  if (!p) {
    toast('Paket nicht gefunden');
    return { ok: false };
  }
  if (!useDb() || !JamiApi.myProfile) {
    state.pendingPack = packId;
    state.shopPayMethod = getShopPayMethod();
    saveState();
    scrollToAuth('register');
    toast('Kurz registrieren – danach startet der Checkout automatisch.');
    return { ok: false, needsAuth: true };
  }
  await JamiApi.ensureStripeConfig();
  if (!JamiApi._stripeEnabled()) {
    toast('Zahlung gerade nicht verfügbar – bitte später erneut.');
    return { ok: false };
  }
  const welcome = getShopWelcomeOffer();
  const packWithTotal = { ...p, coins: packCoinsTotal(p, welcome), baseCoins: p.coins };
  if (btn) {
    btn.disabled = true;
    btn.dataset.prevLabel = btn.textContent || 'Jetzt freischalten';
    btn.textContent = 'Wird geladen…';
  }
  const res = await JamiApi.startStripeCheckout(packWithTotal, { payWith: getShopPayMethod() });
  if (!res.ok) {
    toast(res.error || 'Checkout fehlgeschlagen');
    if (btn) {
      btn.disabled = false;
      btn.textContent = btn.dataset.prevLabel || 'Jetzt freischalten';
    }
  } else if (res.url && document.documentElement.classList.contains('jami-lite')) {
    toast('Stripe-Checkout geöffnet – Zahlung im neuen Tab abschließen.');
  }
  return res;
}

async function runPendingPackCheckout() {
  const id = state.pendingPack;
  if (!id || !useDb() || !JamiApi.myProfile) return;
  state.pendingPack = null;
  saveState();
  document.querySelector('[data-tab="shop"]')?.click();
  await renderShop();
  await new Promise(function (r) { setTimeout(r, 200); });
  await triggerPackCheckout(id);
}

function formatCountdownMs(ms) {
  const sec = Math.max(0, Math.floor(ms / 1000));
  const m = Math.floor(sec / 60);
  const s = sec % 60;
  return String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}

function stopShopWelcomeCountdown() {
  if (shopWelcomeCountdownTimer) {
    clearInterval(shopWelcomeCountdownTimer);
    shopWelcomeCountdownTimer = null;
  }
}

function renderShopWelcomeBanner(welcome) {
  const banner = document.getElementById('shopWelcomeBanner');
  const clock = document.getElementById('shopWelcomeCountdown');
  stopShopWelcomeCountdown();
  if (!banner) return welcome;
  if (!welcome?.active) {
    banner.classList.add('hidden');
    return welcome;
  }
  banner.classList.remove('hidden');
  const tick = () => {
    const left = welcome.deadline - Date.now();
    if (left <= 0) {
      stopShopWelcomeCountdown();
      banner.classList.add('hidden');
      renderShop().catch(() => {});
      return;
    }
    if (clock) clock.textContent = formatCountdownMs(left);
  };
  tick();
  shopWelcomeCountdownTimer = setInterval(tick, 1000);
  return welcome;
}

async function renderShop() {
  refreshAdRewardStatus();
  const grid = document.getElementById('packGrid');
  if (!grid) return;
  grid.innerHTML = '';
  const welcome = renderShopWelcomeBanner(getShopWelcomeOffer());
  if (useDb()) await JamiApi.ensureStripeConfig();
  const stripeOn = useDb() && JamiApi._stripeEnabled();
  const banner = document.getElementById('payMethodsBanner');
  const hint = document.getElementById('shopHint');
  if (banner) {
    banner.innerHTML = stripeOn
      ? '<strong>Zahlung:</strong> Karte, Apple Pay, Google Pay, PayPal, Klarna, Amazon Pay, SEPA-Lastschrift &amp; Banküberweisung – über Stripe Checkout.'
      : '<strong>Zahlung:</strong> Checkout noch nicht aktiv. Stripe muss auf dem Server eingerichtet sein.';
  }
  if (hint) {
    hint.textContent = stripeOn
      ? 'Zahlungsart wählen, dann „Jetzt freischalten“ – PayPal, Klarna, Karte & mehr im Checkout.'
      : useDb()
        ? 'Coin-Kauf ist vorübergehend nicht verfügbar. Bitte später erneut versuchen.'
        : 'Lokal nur Demo – online mit Cloudflare + Stripe für den Coin-Kauf.';
  }
  if (stripeOn) initPayMethodPicker();
  (config.coinPacks || []).forEach((p) => {
    const totalCoins = packCoinsTotal(p, welcome);
    const div = document.createElement('article');
    div.className = 'pack-card' + (p.featured ? ' pack-card--featured' : '');
    let badgeHtml = '';
    if (p.badge === 'BESTSELLER') {
      badgeHtml = '<span class="pack-badge pack-badge--bestseller">BESTSELLER</span>';
    } else if (p.badge === 'VIP GOLD') {
      badgeHtml = '<span class="pack-badge pack-badge--vip-gold">VIP GOLD</span>';
    } else if (p.badge) {
      badgeHtml = '<span class="pack-badge pack-badge--save">' + escHtml(p.badge) + '</span>';
    }
    const vipGoldLine = p.vipGold
      ? '<p class="pack-card-vip-gold">👑 VIP Gold-Status am Profil</p>'
      : '';
    const welcomeExtra = welcome?.active
      ? '<p class="pack-card-welcome-extra">+' + welcome.bonus + ' Bonus-Coins inkl.</p>'
      : '';
    div.innerHTML =
      badgeHtml +
      '<h3 class="pack-card-title">' +
      escHtml(p.label || 'Paket') +
      '</h3>' +
      '<p class="pack-card-coins">' +
      totalCoins +
      ' <span>Coins</span></p>' +
      welcomeExtra +
      vipGoldLine +
      '<p class="pack-card-bonus">' +
      escHtml(p.bonus || '') +
      '</p>' +
      '<p class="pack-card-price">' +
      p.price.toFixed(2).replace('.', ',') +
      ' €</p>';
    const actions = document.createElement('div');
    actions.className = 'pack-actions';
    if (stripeOn) {
      const stripeBtn = document.createElement('button');
      stripeBtn.type = 'button';
      stripeBtn.className = 'btn btn-primary btn-unlock-pulse';
      stripeBtn.textContent = 'Jetzt freischalten';
      stripeBtn.onclick = async () => {
        await triggerPackCheckout(p.id, stripeBtn);
      };
      actions.appendChild(stripeBtn);
    }
    div.appendChild(actions);
    grid.appendChild(div);
  });
  const table = document.getElementById('tierTable');
  if (table) {
    table.innerHTML = '<tr><th>Stufe</th><th>Coins</th></tr>';
    (config.messageTiers || []).forEach((t) => {
      const tr = document.createElement('tr');
      tr.innerHTML = '<td>' + t.label + '</td><td>' + t.cost + ' Coins</td>';
      table.appendChild(tr);
    });
  }
}

function insertIntoChatInput(inputId, str) {
  const el = document.getElementById(inputId);
  if (!el || !str) return;
  const start = el.selectionStart ?? el.value.length;
  const end = el.selectionEnd ?? el.value.length;
  const v = el.value || '';
  el.value = v.slice(0, start) + str + v.slice(end);
  const pos = start + str.length;
  el.setSelectionRange(pos, pos);
  el.focus();
}

async function purchaseEmojiItem(item) {
  const u = me();
  if (!u || !item) return false;
  if (!u.ownedEmojis) u.ownedEmojis = [];
  if (u.ownedEmojis.includes(item.id)) return true;
  if (u.coins < item.coins) {
    toast('Zu wenig Coins – Profil → Emoji-Shop');
    return false;
  }
  u.coins -= item.coins;
  u.ownedEmojis.push(item.id);
  if (useDb()) {
    await JamiApi.updateCoins(-item.coins);
    await JamiApi.updateMyProfile({ ownedEmojis: u.ownedEmojis, coins: u.coins });
  } else saveState();
  updateCoinUI();
  renderEmojiShop();
  renderProfileEmojiPicker();
  toast(item.icon + ' gekauft – unter Profil „Am Namen“ wählen oder im Chat nutzen');
  return true;
}

function closeEmojiPickers() {
  document.querySelectorAll('.emoji-picker-popover').forEach((p) => p.classList.add('hidden'));
  document.querySelectorAll('.btn-emoji-toggle').forEach((b) => b.setAttribute('aria-expanded', 'false'));
}

function renderEmojiPickerContent(pickerEl, inputId) {
  const u = me();
  const owned = u?.ownedEmojis || [];
  const free = config.freeChatEmojis || ['😀', '😊', '😂', '❤️', '👍', '🔥', '✨'];
  const shop = config.emojiShop || [];
  let html = '<div class="emoji-picker-section"><p class="emoji-picker-label">Gratis</p><div class="emoji-picker-row">';
  free.forEach((e) => {
    html += `<button type="button" class="emoji-pick-btn" data-emoji="${e}">${e}</button>`;
  });
  html += '</div></div>';
  const ownedItems = shop.filter((i) => owned.includes(i.id));
  if (ownedItems.length) {
    html += '<div class="emoji-picker-section"><p class="emoji-picker-label">Deine Icons (animiert)</p><div class="emoji-picker-row">';
    ownedItems.forEach((i) => {
      const anim = i.anim || 'float';
      html += `<button type="button" class="emoji-pick-btn emoji-pick-premium emoji-anim-${anim}" data-shop-insert="${i.id}"><span class="emoji-pick-icon">${i.icon}</span></button>`;
    });
    html += '</div></div>';
  }
  const locked = shop.filter((i) => !owned.includes(i.id));
  if (locked.length) {
    html += '<div class="emoji-picker-section"><p class="emoji-picker-label">Premium (Coins)</p><div class="emoji-picker-row">';
    locked.forEach((i) => {
      const anim = i.anim || 'float';
      html += `<button type="button" class="emoji-pick-btn emoji-pick-locked emoji-anim-${anim}" data-shop-id="${i.id}" title="${escHtml(i.name)} – ${i.coins} Coins"><span class="emoji-pick-icon">${i.icon}</span><span class="emoji-pick-cost">${i.coins}🪙</span></button>`;
    });
    html += '</div></div>';
  }
  pickerEl.innerHTML = html;
  pickerEl.querySelectorAll('[data-emoji]').forEach((btn) => {
    btn.onclick = (e) => {
      e.stopPropagation();
      insertIntoChatInput(inputId, btn.dataset.emoji);
      closeEmojiPickers();
    };
  });
  pickerEl.querySelectorAll('[data-shop-insert]').forEach((btn) => {
    btn.onclick = (e) => {
      e.stopPropagation();
      insertIntoChatInput(inputId, encodeShopEmojiToken(btn.dataset.shopInsert));
      closeEmojiPickers();
    };
  });
  pickerEl.querySelectorAll('[data-shop-id]').forEach((btn) => {
    btn.onclick = async (e) => {
      e.stopPropagation();
      const item = shop.find((x) => x.id === btn.dataset.shopId);
      if (!item) return;
      if ((me()?.ownedEmojis || []).includes(item.id)) {
        insertIntoChatInput(inputId, encodeShopEmojiToken(item.id));
        closeEmojiPickers();
        return;
      }
      if (await purchaseEmojiItem(item)) {
        renderEmojiPickerContent(pickerEl, inputId);
        insertIntoChatInput(inputId, encodeShopEmojiToken(item.id));
        closeEmojiPickers();
      }
    };
  });
}

function initEmojiPicker(toggleId, pickerId, inputId) {
  const toggle = document.getElementById(toggleId);
  const picker = document.getElementById(pickerId);
  if (!toggle || !picker) return;
  picker.onclick = (e) => e.stopPropagation();
  toggle.onclick = (e) => {
    e.stopPropagation();
    const wasOpen = !picker.classList.contains('hidden');
    closeEmojiPickers();
    if (!wasOpen) {
      renderEmojiPickerContent(picker, inputId);
      picker.classList.remove('hidden');
      toggle.setAttribute('aria-expanded', 'true');
    }
  };
}

function renderEmojiShopGrid(grid) {
  if (!grid) return;
  grid.innerHTML = '';
  const u = me();
  if (!u) return;
  if (!u.ownedEmojis) u.ownedEmojis = [];
  (config.emojiShop || []).forEach(item => {
    const el = document.createElement('div');
    const owned = u.ownedEmojis.includes(item.id);
    const anim = item.anim || 'float';
    el.className = 'emoji-item emoji-anim-' + anim + (owned ? ' emoji-owned' : '');
    if (item.id === 'heart_big' || item.icon === '❤️') el.classList.add('emoji-shop-heart-feature');
    el.innerHTML =
      `<span class="emoji-shop-icon">${item.icon}</span>` +
      `<strong>${escHtml(item.name)}</strong>` +
      `<small>${owned ? (u.profileEmojiId === item.id ? '✓ Am Namen aktiv' : '✓ Tippen = am Namen') : item.coins + ' 🪙'}</small>`;
    el.onclick = async () => {
      if (owned) {
        await setProfileDisplayEmoji(item.id);
        return;
      }
      await purchaseEmojiItem(item);
    };
    grid.appendChild(el);
  });
}

function renderEmojiShop() {
  document.querySelectorAll('#emojiGrid, #emojiGridShop').forEach(renderEmojiShopGrid);
}

function renderProfile() {
  const u = me();
  ensurePhotos(u);
  u.friendshipStatus = 'self';
  const myBadges = computeActivityBadgesClient(u);
  document.getElementById('myProfileCard').innerHTML = `
    <h2>${profileNameLineHtml(u)}</h2>
    ${myBadges.length ? activityBadgesHtml(u) + '<p class="profile-badges-hint">Sichtbar für andere – aktive Profile bekommen mehr Nachrichten.</p>' : '<p class="meta profile-badges-hint">💡 Chatte regelmäßig und antworte zügig, um Abzeichen wie <strong>Sehr aktiv</strong> oder <strong>Antwortet besonders schnell</strong> zu verdienen.</p>'}
    <div class="origin">🌍 ${isPlaceholderCity(u.city) ? 'Herkunft noch ergänzen' : escHtml(u.city)}, ${escHtml(u.country || 'DE')}</div>
    ${u.vipGold ? '<p class="meta" style="color:#ffd166;font-weight:700;margin:0.35rem 0">👑 VIP Gold – Krone sichtbar in Entdecken &amp; Chat</p>' : ''}
    ${buildPhotoGalleryHtml(u, false)}
    <p>${escHtml(u.bio || '')}</p>
    ${profileDetailsHtml(u)}
    <p class="meta">Kontakt (E-Mail/Telefon) wird auf Jami nicht angezeigt.</p>
    <p><strong>✓ Echtes Profil</strong> – Jami ohne Chat-Moderatoren</p>
    <p>Coins: <strong>${u.coins}</strong></p>
    <p>Icons: ${(u.ownedEmojis||[]).map(id => (config.emojiShop||[]).find(e=>e.id===id)?.icon || '').join(' ') || '—'}</p>`;
  bindPhotoGalleryClicks(document.getElementById('myProfileCard'));
  const form = document.getElementById('myProfileForm');
  if (form) form.innerHTML = profileEditFieldsHtml(u, 'prof');
  const photoForm = document.getElementById('myPhotoForm');
  if (photoForm) {
    photoForm.innerHTML = photoEditFieldsHtml(u);
    wirePhotoSlotBuyButtons();
    wirePhotoFileInputs();
  }
}

document.getElementById('btnSaveProfile').onclick = async () => {
  const u = me();
  if (!u) return;
  const photos = collectPhotosFromForm();
  const patch = sanitizeProfilePatch({
    ...collectProfileFromForm('prof'),
    bio: document.getElementById('profBio')?.value.trim() || '',
    photos,
    photo: photos.main,
  });
  if (isPlaceholderCity(patch.city)) patch.city = '–';
  Object.assign(u, patch);
  u.photos = photos;
  if (useDb()) {
    try {
      const bonus = await JamiApi.updateMyProfile(patch);
      if (JamiApi.myProfile) Object.assign(u, JamiApi.myProfile);
      handleProfileCompleteBonusResponse(bonus);
    } catch (e) {
      toast(e.message || 'Speichern fehlgeschlagen');
      return;
    }
  } else {
    if (tryLocalProfileCompleteBonus(u)) {
      handleProfileCompleteBonusResponse({
        awarded: true,
        bonusCoins: profileCompleteBonusCoins(),
      });
    }
    saveState();
  }
  renderProfile();
  renderProfileEmojiPicker();
  renderProfileCompleteness();
  renderProfileBoost();
  if (document.getElementById('profGeo')?.checked) requestGeo();
  toast('Profil gespeichert');
};

document.getElementById('btnClosePhotoModal').onclick = () => closeProfilePhotoModal();
document.getElementById('btnCloseChatSession').onclick = () => closeChatSession();
document.getElementById('btnViewChatProfile')?.addEventListener('click', () => openChatProfileSheet('partner'));
document.getElementById('chatSessionProfileBar')?.addEventListener('click', () => openChatProfileSheet('partner'));
document.getElementById('btnCloseChatProfile')?.addEventListener('click', () => closeChatProfileSheet());
document.getElementById('chatProfileSheetBackdrop')?.addEventListener('click', () => closeChatProfileSheet());
document.getElementById('btnChatProfilePartner')?.addEventListener('click', () => {
  chatProfileSheetView = 'partner';
  renderChatProfileSheetBody();
  updateChatProfileTabButtons();
});
document.getElementById('btnChatProfileMe')?.addEventListener('click', () => {
  chatProfileSheetView = 'me';
  renderChatProfileSheetBody();
  updateChatProfileTabButtons();
});
document.getElementById('chatSessionHead')?.addEventListener('click', () => openChatProfileSheet('partner'));
document.addEventListener('click', () => closeEmojiPickers());
initEmojiPicker('btnChatEmoji', 'chatEmojiPicker', 'chatInput');
wireChatPhotoSend('btnChatPhoto', 'chatPhotoInput', () => activeChatSessionId, {
  inputId: 'chatInput',
  messagesBoxId: 'chatSessionMessages',
  costHintId: 'chatSessionCostHint',
});
document.addEventListener('keydown', e => {
  if (e.key !== 'Escape') return;
  const noCoins = document.getElementById('noCoinsModal');
  if (noCoins && !noCoins.classList.contains('hidden')) {
    closeNoCoinsPopup();
    return;
  }
  if (activeChatSessionId) {
    const anyOpen = document.querySelector('.emoji-picker-popover:not(.hidden)');
    if (anyOpen) closeEmojiPickers();
    else closeChatSession();
  }
});

function wmFormatKickoff(iso) {
  if (!iso) return '';
  try {
    const d = new Date(iso);
    return d.toLocaleString('de-DE', {
      weekday: 'short',
      day: '2-digit',
      month: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
    });
  } catch (e) {
    return iso;
  }
}

function wmPickText(pick, home, away) {
  if (pick === '1') return home + ' siegt';
  if (pick === '2') return away + ' siegt';
  return 'Unentschieden';
}

function wmStatusClass(status) {
  if (status === 'open') return 'wm-status-open';
  if (status === 'locked') return 'wm-status-locked';
  return 'wm-status-settled';
}

function wmStatusText(m) {
  if (m.status === 'settled' && m.resultHome != null) {
    return 'Ergebnis ' + m.resultHome + ':' + m.resultAway;
  }
  if (m.status === 'locked') return 'Wetten zu';
  return 'Offen';
}

function renderWmMyBets(bets, matches) {
  const el = document.getElementById('wmMyBets');
  if (!el) return;
  const matchMap = new Map((matches || []).map((m) => [m.id, m]));
  const rows = (bets || []).slice(0, 25);
  if (!rows.length) {
    el.innerHTML = '<p class="meta" style="margin:0">Noch keine Wetten – wähle ein Spiel oben.</p>';
    return;
  }
  el.innerHTML = rows
    .map((b) => {
      const m = matchMap.get(b.matchId);
      const teams = m
        ? m.homeFlag + ' ' + escHtml(m.homeTeam) + ' – ' + m.awayFlag + ' ' + escHtml(m.awayTeam)
        : escHtml(b.matchId);
      const tip = m ? wmPickText(b.pick, m.homeTeam, m.awayTeam) : escHtml(b.pick);
      let outcome = '⏳ Offen';
      let cls = '';
      if (b.status === 'won') {
        outcome = '✅ +' + b.payout + ' Coins';
        cls = ' wm-bet-won';
      } else if (b.status === 'lost') {
        outcome = '❌ ' + b.stake + ' Coins';
        cls = ' wm-bet-lost';
      }
      return (
        '<div class="wm-bet-row-h' +
        cls +
        '"><strong>' +
        teams +
        '</strong><br><span class="meta">' +
        escHtml(tip) +
        ' · ' +
        b.stake +
        ' Coins @ ' +
        Number(b.odds).toFixed(2) +
        ' · ' +
        outcome +
        '</span></div>'
      );
    })
    .join('');
}

function buildWmMatchCard(m, stakes) {
  const card = document.createElement('div');
  const isToday = landingWmFixtureClass(m).indexOf('today') >= 0;
  card.className =
    'wm-match-card' + (m.status !== 'open' ? ' wm-match-closed' : '') + (isToday ? ' wm-match-today' : '');
  const open = m.status === 'open';
  let selectedPick = m.myBet?.pick || '';
  let selectedStake = m.myBet?.stake || stakes[0] || 10;
  const todayBadge = isToday ? '<span class="wm-today-badge">Heute</span>' : '';

  card.innerHTML =
    '<div class="wm-match-meta"><span class="wm-match-stage">' +
    todayBadge +
    escHtml(m.stage || 'WM 2026') +
    '</span><span class="' +
    wmStatusClass(m.status) +
    '">' +
    escHtml(wmStatusText(m)) +
    '</span></div>' +
    '<div class="wm-teams">' +
    '<div class="wm-team"><span class="wm-team-flag">' +
    escHtml(m.homeFlag) +
    '</span>' +
    escHtml(m.homeTeam) +
    '</div>' +
    '<div class="wm-vs">' +
    escHtml(landingWmKickoffLabel(m.kickoffAt)) +
    '</div>' +
    '<div class="wm-team"><span class="wm-team-flag">' +
    escHtml(m.awayFlag) +
    '</span>' +
    escHtml(m.awayTeam) +
    '</div></div>';

  if (m.myBet && m.myBet.status === 'pending') {
    const pot = Math.floor(m.myBet.stake * m.myBet.odds);
    const tipEl = document.createElement('div');
    tipEl.className = 'wm-my-tip';
    tipEl.innerHTML =
      '🎯 Dein Tipp: <strong>' +
      escHtml(wmPickText(m.myBet.pick, m.homeTeam, m.awayTeam)) +
      '</strong> · ' +
      m.myBet.stake +
      ' Coins · mögl. <strong>' +
      pot +
      '</strong> Coins';
    card.appendChild(tipEl);
  }

  if (!open) return card;

  const pickRow = document.createElement('div');
  pickRow.className = 'wm-pick-row';
  ['1', 'X', '2'].forEach((p) => {
    const label = p === '1' ? m.homeTeam : p === '2' ? m.awayTeam : 'Remis';
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'wm-pick-btn' + (selectedPick === p ? ' is-selected' : '');
    btn.innerHTML = '<strong>' + p + '</strong>' + escHtml(label) + '<br><span class="meta">' + Number(m.odds[p]).toFixed(2) + '×</span>';
    btn.onclick = () => {
      selectedPick = p;
      pickRow.querySelectorAll('.wm-pick-btn').forEach((b) => b.classList.remove('is-selected'));
      btn.classList.add('is-selected');
    };
    pickRow.appendChild(btn);
  });
  card.appendChild(pickRow);

  const stakeRow = document.createElement('div');
  stakeRow.className = 'wm-stake-row';
  stakes.forEach((s) => {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'wm-stake-btn' + (selectedStake === s ? ' is-selected' : '');
    btn.textContent = s + ' 🪙';
    btn.onclick = () => {
      selectedStake = s;
      stakeRow.querySelectorAll('.wm-stake-btn').forEach((b) => b.classList.remove('is-selected'));
      btn.classList.add('is-selected');
    };
    stakeRow.appendChild(btn);
  });
  card.appendChild(stakeRow);

  const placeBtn = document.createElement('button');
  placeBtn.type = 'button';
  placeBtn.className = 'wm-place-btn';
  placeBtn.textContent = m.myBet ? 'Tipp aktualisieren' : '🪙 Wette platzieren';
  placeBtn.onclick = async () => {
    if (!selectedPick) {
      toast('Bitte 1, X oder 2 wählen');
      return;
    }
    placeBtn.disabled = true;
    try {
      const j = await JamiApi.placeWmBet(m.id, selectedPick, selectedStake);
      if (useDb() && JamiApi.myProfile) JamiApi.myProfile.coins = j.coins;
      saveState();
      updateCoinUI();
      const pot = j.bet?.potential || Math.floor(selectedStake * (m.odds[selectedPick] || 2));
      toast('Tipp gespeichert! Möglicher Gewinn: ' + pot + ' Coins ⚽');
      await renderWmTips();
    } catch (e) {
      toast(e.message || 'Wette fehlgeschlagen');
      if ((e.message || '').toLowerCase().includes('coin')) showNoCoinsPopup?.();
    } finally {
      placeBtn.disabled = false;
    }
  };
  card.appendChild(placeBtn);
  return card;
}

let wmFanMessages = [];
let wmFanLastAt = '';
let wmFanPollTimer = null;
let wmFanAudioCtx = null;
const wmFanCelebratedIds = new Set();
const WM_FAN_REACT_EMOJIS = ['⚽', '🔥', '📯', '🇩🇪', '😂'];

function wmFanAudioContext() {
  if (!window.AudioContext && !window.webkitAudioContext) return null;
  if (!wmFanAudioCtx) wmFanAudioCtx = new (window.AudioContext || window.webkitAudioContext)();
  return wmFanAudioCtx;
}

function playWmTorJingle() {
  try {
    const ctx = wmFanAudioContext();
    if (!ctx) return;
    if (ctx.state === 'suspended') ctx.resume().catch(() => {});
    const now = ctx.currentTime;
    [392, 523.25, 659.25, 783.99].forEach((freq, i) => {
      const o = ctx.createOscillator();
      const g = ctx.createGain();
      o.type = i < 2 ? 'square' : 'sine';
      o.frequency.value = freq;
      const t0 = now + i * 0.07;
      g.gain.setValueAtTime(0.0001, t0);
      g.gain.exponentialRampToValueAtTime(0.11, t0 + 0.025);
      g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.22);
      o.connect(g);
      g.connect(ctx.destination);
      o.start(t0);
      o.stop(t0 + 0.24);
    });
  } catch (e) {}
}

function playWmSendBuzz() {
  try {
    const ctx = wmFanAudioContext();
    if (!ctx) return;
    if (ctx.state === 'suspended') ctx.resume().catch(() => {});
    const now = ctx.currentTime;
    const o = ctx.createOscillator();
    const g = ctx.createGain();
    o.type = 'sawtooth';
    o.frequency.setValueAtTime(180, now);
    o.frequency.exponentialRampToValueAtTime(120, now + 0.15);
    g.gain.setValueAtTime(0.0001, now);
    g.gain.exponentialRampToValueAtTime(0.06, now + 0.02);
    g.gain.exponentialRampToValueAtTime(0.0001, now + 0.16);
    o.connect(g);
    g.connect(ctx.destination);
    o.start(now);
    o.stop(now + 0.17);
  } catch (e) {}
}

function isWmGoalText(text) {
  const s = String(text || '').toLowerCase();
  return (
    /\b(tor|goal+|goo+al|juhu|yes+|deutschland|england)\b/.test(s) ||
    /⚽|🥅/.test(text || '')
  );
}

function wmFanVibrate(goal) {
  if (!navigator.vibrate) return;
  navigator.vibrate(goal ? [60, 40, 80, 40, 100] : [30]);
}

function showWmGoalCelebration(label) {
  const flash = document.createElement('div');
  flash.className = 'wm-goal-flash';
  document.body.appendChild(flash);
  const banner = document.createElement('div');
  banner.className = 'wm-goal-banner';
  banner.textContent = label || '⚽ TOR!';
  document.body.appendChild(banner);
  setTimeout(() => {
    flash.remove();
    banner.remove();
  }, 900);
}

function wmFanCelebrate(text, opts) {
  opts = opts || {};
  const goal = opts.forceGoal || isWmGoalText(text);
  if (goal) {
    playWmTorJingle();
    wmFanVibrate(true);
    showWmGoalCelebration(opts.banner || '⚽ TOR!');
  } else {
    playWmSendBuzz();
    wmFanVibrate(false);
  }
}

function wmFanReactionsHtml(m) {
  return (
    '<div class="wm-fan-reactions">' +
    WM_FAN_REACT_EMOJIS.map((e) => {
      const count = (m.reactions && m.reactions[e]) || 0;
      const active = m.myReaction === e;
      return (
        '<button type="button" class="wm-fan-react-btn' +
        (active ? ' is-active' : '') +
        '" data-wm-react="1" data-msg-id="' +
        escHtml(m.id) +
        '" data-emoji="' +
        escHtml(e) +
        '">' +
        e +
        (count ? '<span>' + count + '</span>' : '') +
        '</button>'
      );
    }).join('') +
    '</div>'
  );
}

function bindWmFanChatEvents() {
  const box = document.getElementById('wmFanChatBox');
  if (box && !box.dataset.wmBound) {
    box.dataset.wmBound = '1';
    box.addEventListener('click', (e) => {
      const btn = e.target.closest('[data-wm-react]');
      if (!btn) return;
      toggleWmFanReaction(btn.dataset.msgId, btn.dataset.emoji).catch(() => {});
    });
  }
  document.querySelectorAll('#wmFanEmojiBar [data-wm-insert]').forEach((btn) => {
    if (btn.dataset.wmInsertBound) return;
    btn.dataset.wmInsertBound = '1';
    btn.onclick = () => {
      const input = document.getElementById('wmFanChatInput');
      if (!input) return;
      const add = btn.dataset.wmInsert || '';
      input.value = (input.value + add).slice(0, 400);
      input.focus();
    };
  });
}

async function toggleWmFanReaction(messageId, emoji) {
  if (!useDb() || JamiApi.mode !== 'cloudflare') return;
  try {
    const j = await JamiApi.reactWmFanChat(messageId, emoji);
    const msg = wmFanMessages.find((x) => x.id === messageId);
    if (msg) {
      msg.reactions = j.reactions || {};
      msg.myReaction = j.myReaction || null;
    }
    paintWmFanChat();
    if (emoji === '⚽' || emoji === '🔥') {
      playWmTorJingle();
      wmFanVibrate(emoji === '⚽');
    } else if (emoji === '📯') {
      playWmSendBuzz();
      wmFanVibrate(false);
    }
  } catch (e) {
    toast(e.message || 'Reaktion fehlgeschlagen');
  }
}

function wmFanMaybeCelebrateIncoming(messages) {
  (messages || []).forEach((m) => {
    if (m.mine || wmFanCelebratedIds.has(m.id)) return;
    if (!isWmGoalText(m.text)) return;
    wmFanCelebratedIds.add(m.id);
    wmFanCelebrate(m.text, { forceGoal: true, banner: '⚽ ' + (m.name || 'Fan') + ': TOR!' });
  });
}

function stopWmFanPoll() {
  if (wmFanPollTimer) {
    clearInterval(wmFanPollTimer);
    wmFanPollTimer = null;
  }
}

function startWmFanPoll() {
  stopWmFanPoll();
  wmFanPollTimer = setInterval(() => pollWmFanChat().catch(() => {}), 4000);
}

function scrollWmFanChatBottom() {
  const box = document.getElementById('wmFanChatBox');
  if (box) box.scrollTop = box.scrollHeight;
}

function wmFanTimeLabel(at) {
  if (!at) return '';
  try {
    const d = new Date(String(at).includes('T') ? at : String(at).replace(' ', 'T') + 'Z');
    return d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
  } catch (e) {
    return '';
  }
}

function wmFanAvatarHtml(m) {
  if (m.photo && !isStockPhotoUrl(m.photo)) {
    return '<img class="wm-fan-avatar" src="' + escHtml(m.photo) + '" alt="">';
  }
  const initial = escHtml((m.name || '?').charAt(0).toUpperCase());
  return '<span class="wm-fan-avatar">' + initial + '</span>';
}

function paintWmFanChat() {
  const box = document.getElementById('wmFanChatBox');
  if (!box) return;
  if (!wmFanMessages.length) {
    box.innerHTML =
      '<p class="wm-fan-empty">Noch leise im Stadion… Sei der Erste! ⚽📯<br><span class="meta">Schreib Tor!, Schiri! oder 🇩🇪</span></p>';
    return;
  }
  box.innerHTML = wmFanMessages
    .map(
      (m) =>
        '<div class="wm-fan-msg' +
        (m.mine ? ' mine' : '') +
        '">' +
        wmFanAvatarHtml(m) +
        '<div class="wm-fan-bubble-wrap">' +
        (m.mine ? '' : '<div class="wm-fan-name">' + escHtml(m.name) + '</div>') +
        '<div class="wm-fan-bubble">' +
        escHtml(m.text) +
        '</div>' +
        wmFanReactionsHtml(m) +
        '<div class="wm-fan-time">' +
        escHtml(wmFanTimeLabel(m.at)) +
        '</div></div></div>'
    )
    .join('');
  bindWmFanChatEvents();
  scrollWmFanChatBottom();
}

function mergeWmFanMessages(incoming, celebrate) {
  if (!incoming?.length) return;
  if (celebrate) wmFanMaybeCelebrateIncoming(incoming);
  const ids = new Set(wmFanMessages.map((x) => x.id));
  incoming.forEach((m) => {
    if (!ids.has(m.id)) {
      wmFanMessages.push(m);
      return;
    }
    const idx = wmFanMessages.findIndex((x) => x.id === m.id);
    if (idx >= 0) {
      wmFanMessages[idx] = { ...wmFanMessages[idx], ...m };
    }
  });
  wmFanMessages.sort((a, b) => String(a.at || '').localeCompare(String(b.at || '')));
  if (wmFanMessages.length > 150) wmFanMessages = wmFanMessages.slice(-120);
  const last = wmFanMessages[wmFanMessages.length - 1];
  if (last?.at) wmFanLastAt = last.at;
}

async function loadWmFanChat(fullReset) {
  const box = document.getElementById('wmFanChatBox');
  if (!box) return;
  if (!useDb() || JamiApi.mode !== 'cloudflare') {
    box.innerHTML = '<p class="wm-fan-empty">Fan-Chat nur online auf jami-dating.de</p>';
    return;
  }
  if (fullReset) {
    wmFanMessages = [];
    wmFanLastAt = '';
    box.innerHTML = '<p class="wm-fan-empty">Stadion füllt sich… ⚽</p>';
  }
  try {
    const j = await JamiApi.getWmFanChat(fullReset ? '' : wmFanLastAt);
    mergeWmFanMessages(j.messages || [], false);
    paintWmFanChat();
  } catch (e) {
    if (fullReset) {
      box.innerHTML = '<p class="wm-fan-empty">' + escHtml(e.message || 'Chat nicht erreichbar') + '</p>';
    }
  }
}

async function pollWmFanChat() {
  if (!useDb() || JamiApi.mode !== 'cloudflare') return;
  if (!document.getElementById('tab-wm')?.classList.contains('active')) return;
  const j = await JamiApi.getWmFanChat(wmFanLastAt);
  if (j.messages?.length) {
    mergeWmFanMessages(j.messages, true);
    paintWmFanChat();
  }
}

async function sendWmFanChat() {
  const input = document.getElementById('wmFanChatInput');
  const text = (input?.value || '').trim();
  if (!text) return;
  if (!useDb() || JamiApi.mode !== 'cloudflare') {
    toast('Fan-Chat nur online');
    return;
  }
  const btn = document.getElementById('wmFanChatSend');
  if (btn) btn.disabled = true;
  try {
    const j = await JamiApi.postWmFanChat(text);
    if (j.message) {
      mergeWmFanMessages([{ ...j.message, reactions: {}, myReaction: null }]);
      paintWmFanChat();
    }
    wmFanCelebrate(text);
    if (input) input.value = '';
  } catch (e) {
    toast(e.message || 'Senden fehlgeschlagen');
  } finally {
    if (btn) btn.disabled = false;
    input?.focus();
  }
}

async function renderWmTips(silent) {
  const listEl = document.getElementById('wmMatchList');
  const balanceEl = document.getElementById('wmCoinBalance');
  const bannerEl = document.getElementById('wmTodayBanner');
  if (!listEl) return;
  if (!useDb() || JamiApi.mode !== 'cloudflare') {
    listEl.innerHTML =
      '<p class="meta">WM-Tipp ist online auf jami-dating.de verfügbar – bitte einloggen.</p>';
    return;
  }
  if (!silent) listEl.innerHTML = '<p class="meta">Spiele werden geladen…</p>';
  try {
    const data = await JamiApi.getWmMatches();
    if (balanceEl) balanceEl.textContent = String(me()?.coins ?? 0);
    const matches = data.matches || [];
    paintWmAppChrome(matches);
    const openMatches = matches.filter((m) => m.status === 'open');
    const highlight = wmNextGermanyMatch(matches) || openMatches[0] || null;
    if (bannerEl && highlight && !wmNextGermanyMatch(matches)) {
      bannerEl.hidden = false;
      bannerEl.innerHTML =
        escHtml(highlight.homeFlag || '') +
        ' <strong>' +
        escHtml(landingWmKickoffLabel(highlight.kickoffAt)) +
        ':</strong> ' +
        escHtml(highlight.homeTeam) +
        ' vs. ' +
        escHtml(highlight.awayTeam) +
        ' – jetzt mit Coins tippen!';
    }
    listEl.innerHTML = '';
    const stakes = data.stakes || [10, 25, 50, 100];
    matches.forEach((m) => {
      listEl.appendChild(buildWmMatchCard(m, stakes));
    });
    if (!matches.length) {
      listEl.innerHTML = '<p class="meta">Aktuell keine WM-Spiele geladen.</p>';
    }
    renderWmMyBets(data.myBets || [], matches);
  } catch (e) {
    listEl.innerHTML = '<p class="meta">' + escHtml(e.message || 'Laden fehlgeschlagen') + '</p>';
  }
}

document.querySelectorAll('.nav-tabs button').forEach(btn => {
  btn.onclick = () => {
    stopWmFanPoll();
    stopWmTipsPoll();
    document.querySelectorAll('.nav-tabs button').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    document.querySelectorAll('#appShell .screen').forEach(s => s.classList.remove('active'));
    document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
    if (btn.dataset.tab === 'matches') {
      markAllCurrentMatchesSeen();
      renderMatches();
      refreshInboxBadges().catch(() => {});
    }
    if (btn.dataset.tab === 'likes') {
      renderLikes();
      refreshInboxBadges().catch(() => {});
    }
    if (btn.dataset.tab === 'discover') {
      requestGeo();
      renderSwipeDeck();
      setTimeout(() => geoMap?.invalidateSize(), 200);
    }
    if (btn.dataset.tab === 'profile') {
      renderProfile();
      wirePhotoFileInputs();
      renderProfileEmojiPicker();
      renderProfileCompleteness();
      renderProfileBoost();
      renderReferralCard();
      renderFriendRequests();
      renderEmojiShop();
    }
    if (btn.dataset.tab === 'shop') {
      renderShop().catch(() => {});
      renderEmojiShop();
    }
    if (btn.dataset.tab === 'wm') {
      bindWmFanChatEvents();
      loadWmFanChat(true).catch(() => {});
      startWmFanPoll();
      renderWmTips().catch(() => {});
      startWmTipsPoll();
    }
  };
});

document.getElementById('wmFanChatSend')?.addEventListener('click', () => sendWmFanChat());
document.getElementById('wmFanChatInput')?.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault();
    sendWmFanChat();
  }
});

document.getElementById('btnLogout').onclick = () => showAuth();

document.getElementById('btnWatchAd').onclick = () => startAdWatchFlow();
document.getElementById('btnAdCancel').onclick = () => closeAdModal();
document.getElementById('btnAdClaim').onclick = () => claimAdReward();
document.getElementById('btnNoCoinsWatchVideo').onclick = async () => {
  closeNoCoinsPopup();
  await startAdWatchFlow();
};
document.getElementById('btnNoCoinsShop').onclick = () => {
  closeNoCoinsPopup();
  document.querySelector('[data-tab="shop"]')?.click();
};
document.getElementById('btnNoCoinsClose').onclick = () => closeNoCoinsPopup();
document.getElementById('btnPushEnable').onclick = () => enableWebPush();
document.getElementById('btnPushLater').onclick = () => closePushPrompt(true);

async function handlePaymentReturn() {
  const params = new URLSearchParams(window.location.search);
  if (params.get('payment') === 'success') {
    const sessionId = params.get('session_id') || '';
    window.history.replaceState({}, '', window.location.pathname || '/');
    if (useDb() && sessionId && JamiApi.mode === 'cloudflare') {
      const res = await JamiApi.confirmStripeCheckout(sessionId);
      if (res.ok) {
        const n = res.credited || 0;
        const vipNote = JamiApi.myProfile?.vipGold ? ' VIP Gold 👑 aktiv!' : '';
        toast(n > 0 ? '+' + n + ' Coins gutgeschrieben!' + vipNote : 'Zahlung bestätigt – Coins sind da.' + vipNote);
        updateCoinUI();
        renderProfile();
        return;
      }
      toast(res.error || 'Gutschrift fehlgeschlagen – bitte Support');
      await JamiApi.refreshMyProfile();
      updateCoinUI();
      return;
    }
    toast('Zahlung erfolgreich! Coins werden gutgeschrieben …');
    if (useDb()) {
      await JamiApi.refreshMyProfile();
      updateCoinUI();
      renderProfile();
    }
  } else if (params.get('payment') === 'cancel') {
    toast('Zahlung abgebrochen');
    window.history.replaceState({}, '', window.location.pathname || '/');
  }
}

(async () => {
  window.__jamiBooted = true;
  ensureLandingVisible();
  captureReferralFromUrl();
  initGrowth();
  try {
    await loadConfig();
    initBetaTestSignup();
    initLandingWmSchedule();
    initAnalytics();
    await JamiApi.init(config);
    await initOAuthButtons();
    if (await handleOAuthReturn()) return;
    openRegisterIfReferralPending();
    await handlePaymentReturn();
    if (JamiApi.isOnline() && JamiApi.myProfile) {
      showApp().catch(function () { showAuth(); });
      return;
    }
    showAuth();
    if (JamiApi.isOnline()) return;
    const savedToken = localStorage.getItem('jami_token');
    if (savedToken && config.backend === 'cloudflare') {
      JamiApi._token = savedToken;
      JamiApi.mode = 'cloudflare';
      try {
        JamiApi._profileCompleteBonus = await JamiApi._cloudLoadMe();
        showApp().catch(function () { showAuth(); });
        return;
      } catch (e) {}
    }
    loadState();
    if (state.session && me()) showApp().catch(function () { showAuth(); });
    else showAuth();
  } catch (e) {
    console.error('Jami start', e);
    showAuth();
  }
})();
"""


def copy_brand_logo(root: Path) -> None:
    """Logo aus data/ nach images/ (optimiert für Web)."""
    img_dir = root / "images"
    img_dir.mkdir(parents=True, exist_ok=True)
    dest = img_dir / "jami-dating-logo.png"
    if not LOGO_SRC.is_file():
        return
    try:
        from PIL import Image

        with Image.open(LOGO_SRC) as im:
            im = im.convert("RGBA")
            im = im.resize((256, 256), Image.Resampling.LANCZOS)
            im.save(dest, format="PNG", optimize=True, compress_level=9)
    except Exception:
        shutil.copy2(LOGO_SRC, dest)


def optimize_web_images(root: Path) -> None:
    """Showcase-Fotos für Mobile verkleinern."""
    img_dir = root / "images"
    if not img_dir.is_dir():
        return
    try:
        from PIL import Image
    except ImportError:
        return
    for name, _url in PROFILE_PHOTOS:
        path = img_dir / name
        if not path.is_file():
            continue
        try:
            with Image.open(path) as im:
                im = im.convert("RGB")
                w, h = im.size
                if w > 480:
                    nh = max(1, int(h * 480 / w))
                    im = im.resize((480, nh), Image.Resampling.LANCZOS)
                im.save(path, format="JPEG", quality=82, optimize=True)
        except OSError:
            pass


def download_profile_images(root: Path) -> None:
    """Lädt Pexels-Portraits nach images/ (lokal, schneller Laden)."""
    img_dir = root / "images"
    img_dir.mkdir(parents=True, exist_ok=True)
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        "Referer": "https://www.pexels.com/",
    }
    for name, url in (*PROFILE_PHOTOS, HERO_COUPLE):
        dest = img_dir / name
        if dest.exists() and dest.stat().st_size > 8000:
            continue
        try:
            req = urllib.request.Request(url, headers=headers)
            with urllib.request.urlopen(req, timeout=45) as resp:
                data = resp.read()
            if len(data) > 8000:
                dest.write_bytes(data)
        except OSError:
            pass


def landing_payment_section_html() -> str:
    from jam.dating_site import COIN_PACKS, MESSAGE_COST_PER_CHAT

    packs: list[str] = []
    for p in COIN_PACKS:
        featured = " pack-card--featured" if p.get("featured") else ""
        badge = ""
        if p.get("badge") == "BESTSELLER":
            badge = '<span class="pack-badge pack-badge--bestseller">BESTSELLER</span>'
        elif p.get("badge") == "VIP GOLD":
            badge = '<span class="pack-badge pack-badge--vip-gold">VIP GOLD</span>'
        elif p.get("badge"):
            badge = f'<span class="pack-badge pack-badge--save">{p["badge"]}</span>'
        vip_gold_line = (
            '<p class="pack-card-vip-gold">👑 VIP Gold-Status am Profil</p>'
            if p.get("vipGold")
            else ""
        )
        price = f'{p["price"]:.2f}'.replace(".", ",")
        packs.append(
            f"""        <article class="pack-card{featured}">
          {badge}
          <h3 class="pack-card-title">{p["label"]}</h3>
          <p class="pack-card-coins">{p["coins"]} <span>Coins</span></p>
          {vip_gold_line}
          <p class="pack-card-bonus">{p.get("bonus", "")}</p>
          <p class="pack-card-price">{price} €</p>
          <div class="pack-actions">
            <button type="button" class="btn btn-primary btn-unlock-pulse landing-pack-unlock" data-pack-id="{p["id"]}">Jetzt freischalten</button>
          </div>
        </article>"""
        )
    packs_html = "\n".join(packs)
    min_price = f'{COIN_PACKS[0]["price"]:.2f}'.replace(".", ",")
    return f"""
    <section class="landing-section landing-pay-section" id="preise">
      <h2>Faire Preise &amp; sichere Zahlung</h2>
      <p class="sub">Registrierung kostenlos mit <strong>100 Gratis-Coins</strong>. Danach zahlst du nur, was du nutzt – jede Chat-Nachricht kostet {MESSAGE_COST_PER_CHAT} Coins.</p>
      <div class="pay-methods-banner landing-pay-banner">
        <strong>Bei Jami kannst du bezahlen mit:</strong>
        <div class="landing-pay-methods">
          <span class="landing-pay-chip">💳 Karte</span>
          <span class="landing-pay-chip">🍎 Apple Pay</span>
          <span class="landing-pay-chip">🅿️ PayPal</span>
          <span class="landing-pay-chip">🛍️ Klarna</span>
          <span class="landing-pay-chip">📦 Amazon Pay</span>
          <span class="landing-pay-chip">🏦 SEPA-Lastschrift</span>
          <span class="landing-pay-chip">💶 Banküberweisung</span>
        </div>
        <p class="meta landing-pay-note">Abwicklung über <strong>Stripe Checkout</strong> (SSL-verschlüsselt). Coins werden nach erfolgreicher Zahlung automatisch gutgeschrieben.</p>
      </div>
      <div class="landing-pay-facts">
        <div class="landing-pay-fact"><strong>{MESSAGE_COST_PER_CHAT} 🪙</strong><span>pro Chat-Nachricht</span></div>
        <div class="landing-pay-fact"><strong>100 🪙</strong><span>Gratis beim Start</span></div>
        <div class="landing-pay-fact"><strong>ab {min_price} €</strong><span>Coin-Pakete</span></div>
      </div>
      <h3 class="landing-pay-packs-title">Coin-Pakete im Überblick</h3>
      <div class="pack-grid landing-pack-grid">
{packs_html}
      </div>
      <p class="meta landing-pay-hint">Tippe auf <strong>Jetzt freischalten</strong> – nach kurzer Registrierung geht es direkt zum sicheren Stripe-Checkout.</p>
    </section>"""


def write_assets(root: Path, *, site_url: str | None = None) -> None:
    from jam.jami_cloudflare import _write_static_extras, write_cloudflare_guide
    from jam.jami_domain import resolve_site_url
    from jam.jami_pwa import write_pwa_files
    from jam.jami_seo import faq_section_html, head_markup, write_seo_files
    from jam.jami_stripe_setup import write_stripe_files
    from jam.jami_supabase_setup import write_supabase_files

    from datetime import datetime, timezone

    url = (site_url or resolve_site_url()).rstrip("/")
    build_id = datetime.now(timezone.utc).strftime("%Y%m%d%H%M")
    copy_brand_logo(root)
    download_profile_images(root)
    optimize_web_images(root)
    write_supabase_files(root)
    write_stripe_files(root)
    write_cloudflare_guide(root)
    write_seo_files(root, url)
    write_pwa_files(root, site_url=url)
    _write_static_extras(root)
    from jam.vidpuls_promo_banner import site_promo_bundle

    promo = site_promo_bundle()
    (root / "css" / "app.css").write_text(
        CSS.strip() + "\n" + promo["css"], encoding="utf-8"
    )
    (root / "js" / "app.js").write_text(
        APP_JS.strip() + "\n" + promo["script"], encoding="utf-8"
    )
    (root / "js" / "cookie-consent.js").write_text(COOKIE_CONSENT_JS.strip(), encoding="utf-8")
    from jam.jami_app_page import apk_available, landing_app_section_html

    index_html = (
        INDEX_HTML.replace("__JAMI_LOGO__", LOGO_MARKUP)
        .replace("__JAMI_SEO_HEAD__", head_markup(url))
        .replace("__PAGE_FX__", page_fx_html())
        .replace("__JAMI_FAQ__", faq_section_html())
        .replace("__JAMI_PAYMENT__", landing_payment_section_html())
        .replace("__JAMI_BUILD__", build_id)
        .replace(
            "__APP_SECTION__",
            landing_app_section_html(apk_on_site=apk_available() or (root / "downloads" / "Jami-Dating.apk").is_file()),
        )
        .replace("__VIDPULS_LANDING_SIDEBAR__", promo["landing_sidebar"])
        .replace("__VIDPULS_PROMO_SECTION__", promo["landing_section"])
        .replace("__VIDPULS_APP_SIDEBAR__", promo["app_sidebar"])
        .strip()
    )
    (root / "index.html").write_text(index_html, encoding="utf-8")
    from jam.push_sw import PUSH_SW_JS

    (root / "sw.js").write_text(PUSH_SW_JS.strip(), encoding="utf-8")
