<?php
// Remote App Interface
define('DATA_DIR', __DIR__ . '/data');
$_settings_raw = file_exists(DATA_DIR . '/settings.json') ? file_get_contents(DATA_DIR . '/settings.json') : '{}';
$_branding = json_decode($_settings_raw, true) ?: [];

// === Load Rainy Window theme settings (managed by admin via theme_admin.php) ===
$_theme_raw = file_exists(DATA_DIR . '/theme_settings.json') ? file_get_contents(DATA_DIR . '/theme_settings.json') : '{}';
$_theme = json_decode($_theme_raw, true) ?: [];
$_theme += [
    'bg_url' => 'https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1920&q=80',
    'bg_type' => 'image',
    'blur' => 5, 'brightness' => 30, 'rain' => 70,
    'color' => 'none', 'overlay' => 0, 'volume' => 50,
    'show_widgets' => false, 'show_sound_btn' => false,
    'show_fullscreen_btn' => false, 'show_share_btn' => false
];

// Branding variables with defaults
$_protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$_host = $_SERVER['HTTP_HOST'] ?? 'localhost:8080';
$_base_url = $_protocol . '://' . $_host;
$b_logo       = $_branding['login_logo_url'] ?? $_base_url . '/inject/app_icons/default.png';
$b_name       = $_branding['login_brand_name'] ?? 'TEKNO-APPS';
$b_subtitle   = $_branding['login_brand_subtitle'] ?? 'Premium AI Workflow Suite';
$b_catalog    = $_branding['login_catalog_text'] ?? 'Full Katalog AI Tools';
$b_label      = $_branding['login_input_label'] ?? 'Access Code';
$b_placeholder= $_branding['login_input_placeholder'] ?? 'Enter access code...';
$b_error      = $_branding['login_error_text'] ?? 'Kode akses tidak valid.';
$b_btn        = $_branding['login_btn_text'] ?? 'Masuk';
$b_takeover   = $_branding['login_takeover_text'] ?? 'Ambil Alih Sesi';
$b_footer     = $_branding['login_footer_text'] ?? 'TEXA.STUDIO © 2026 copyright.';
$b_title      = $_branding['login_page_title'] ?? 'TEKNO-APPS';
$b_dash_name  = $_branding['login_dash_name'] ?? 'TEKNO-APPS';
?>
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title><?php echo htmlspecialchars($b_title); ?></title>
  <style>
/* ============================================
   TEKNO-APPS - Dark Ocean Blue Theme
   ============================================ */

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&display=swap');

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --ocean-deep: #020c18;
  --ocean-mid: #041628;
  --ocean-surface: #061e38;
  --blue-glow: #0066ff;
  --blue-bright: #0099ff;
  --blue-light: #4db8ff;
  --blue-pale: #a8d8ff;
  --accent-cyan: #00d4ff;
  --text-primary: #e8f4ff;
  --text-secondary: #7aaed4;
  --text-muted: #4a7a9b;
  --glass-bg: rgba(4, 22, 48, 0.85);
  --glass-border: rgba(0, 150, 255, 0.18);
  --glass-border-hover: rgba(0, 200, 255, 0.4);
  --error-color: #ff4d4d;
  --success-color: #00e676;
  --radius-lg: 20px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --popup-w: 400px;
  --popup-h: 580px;
  --dash-h: 540px;
}

html,
body {
  width: 100%;
  min-height: 100vh;
  margin: 0;
  padding: 0;
  background: var(--ocean-deep);
  font-family: 'Inter', system-ui, sans-serif;
  color: var(--text-primary);
  overflow-x: hidden;
  overflow-y: auto;
  position: relative;
}

/* ---- ANIMATED BACKGROUND ---- */
.bg-canvas {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
}

.orb {
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  opacity: 0.35;
  animation: floatOrb 10s ease-in-out infinite alternate;
}

.orb-1 {
  width: 280px;
  height: 280px;
  background: radial-gradient(circle, #0055cc, transparent 70%);
  top: -80px;
  left: -60px;
  animation-duration: 12s;
}

.orb-2 {
  width: 220px;
  height: 220px;
  background: radial-gradient(circle, #003399, transparent 70%);
  bottom: 20px;
  right: -50px;
  animation-duration: 9s;
  animation-delay: -3s;
}

.orb-3 {
  width: 160px;
  height: 160px;
  background: radial-gradient(circle, #00aaff, transparent 70%);
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  opacity: 0.15;
  animation-duration: 15s;
  animation-delay: -7s;
}

@keyframes floatOrb {
  0% {
    transform: translate(0, 0) scale(1);
  }

  33% {
    transform: translate(20px, -25px) scale(1.08);
  }

  66% {
    transform: translate(-15px, 20px) scale(0.95);
  }

  100% {
    transform: translate(10px, -10px) scale(1.05);
  }
}

/* Orb-3 needs its own transform override */
.orb-3 {
  animation-name: floatOrb3;
}

@keyframes floatOrb3 {
  0% {
    transform: translate(-50%, -50%) scale(1);
  }

  50% {
    transform: translate(-50%, -50%) scale(1.3);
  }

  100% {
    transform: translate(-50%, -50%) scale(0.85);
  }
}

/* Particles */
.particles {
  position: absolute;
  inset: 0;
}

.particle {
  position: absolute;
  border-radius: 50%;
  background: var(--blue-light);
  opacity: 0;
  animation: twinkle var(--dur, 4s) ease-in-out var(--delay, 0s) infinite;
}

@keyframes twinkle {

  0%,
  100% {
    opacity: 0;
    transform: scale(0.5);
  }

  50% {
    opacity: 0.6;
    transform: scale(1);
  }
}

/* Scan line effect */
.bg-canvas::after {
  content: '';
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(0deg,
      rgba(0, 80, 200, 0.03) 0px,
      rgba(0, 80, 200, 0.03) 1px,
      transparent 1px,
      transparent 3px);
  pointer-events: none;
}

/* ---- SCREENS ---- */
.screen {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 100vh;
  padding: 24px;
  box-sizing: border-box;
  animation: screenIn 0.4s ease-out;
}

.screen.hidden {
  display: none !important;
}

@keyframes screenIn {
  from {
    opacity: 0;
    transform: translateY(12px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ---- GLASS CARD (matches glassmorph widgets style) ---- */
.glass-card {
  position: relative;
  background: linear-gradient(135deg,
    rgba(255,255,255,0.12) 0%,
    rgba(255,255,255,0.05) 50%,
    rgba(255,255,255,0.08) 100%);
  border: 1px solid transparent;
  background-clip: padding-box;
  border-radius: 24px;
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  box-shadow:
    0 8px 32px rgba(0, 0, 0, 0.3),
    0 0 0 1px rgba(255, 255, 255, 0.05),
    inset 0 1px 0 rgba(255, 255, 255, 0.15),
    inset 0 -1px 0 rgba(0, 0, 0, 0.1);
  padding: 36px 32px;
  width: 100%;
  max-width: 360px;
  transition: all 0.4s cubic-bezier(0.16,1,0.3,1);
  transform-style: preserve-3d;
}

/* Gradient border overlay (light refraction effect) */
.glass-card::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 24px;
  padding: 1px;
  background: linear-gradient(135deg,
    rgba(255,255,255,0.3) 0%,
    rgba(255,255,255,0.05) 50%,
    rgba(255,255,255,0.1) 100%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
          mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
  z-index: 1;
}

/* Top light reflection */
.glass-card::after {
  content: "";
  position: absolute;
  top: 0; left: 10%; right: 10%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent);
  pointer-events: none;
  z-index: 1;
}

.glass-card > * { position: relative; z-index: 2; }

/* Hover lift (only for login card, not modals which have their own behavior) */
.login-card.glass-card:hover {
  transform: translateY(-2px);
  box-shadow:
    0 18px 50px rgba(0,0,0,0.45),
    0 0 0 1px rgba(255,255,255,0.1),
    inset 0 1px 0 rgba(255,255,255,0.2),
    0 0 40px rgba(102,180,255,0.12);
}

/* ---- BRAND / LOGO ---- */
.brand-logo {
  display: flex;
  justify-content: center;
  margin-bottom: 16px;
}

.logo-icon {
  width: 56px;
  height: 56px;
  border-radius: 16px;
  box-shadow: 0 0 20px rgba(0, 150, 255, 0.5), 0 8px 24px rgba(0, 0, 0, 0.4);
  position: relative;
  overflow: hidden;
}

.logo-icon::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), transparent);
}

.logo-icon.sm {
  width: 30px;
  height: 30px;
  border-radius: 8px;
  box-shadow: 0 0 12px rgba(0, 150, 255, 0.4);
}

.brand-name {
  text-align: center;
  font-size: 24px;
  font-weight: 800;
  color: #ffffff;
  letter-spacing: 0.5px;
  margin-bottom: 6px;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.5), 0 0 24px rgba(77, 184, 255, 0.25);
}

.brand-sub {
  text-align: center;
  font-size: 13px;
  color: #cfeaff;
  letter-spacing: 0.5px;
  margin-bottom: 28px;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.4);
  font-weight: 500;
}

/* ---- INPUT GROUP ---- */
.input-group {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 20px;
}

.input-label {
  font-size: 11px;
  font-weight: 700;
  color: #ffffff;
  letter-spacing: 1.5px;
  text-transform: uppercase;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.5), 0 0 8px rgba(77, 184, 255, 0.3);
}

.input-wrapper {
  position: relative;
  display: flex;
  align-items: center;
}

.input-icon {
  position: absolute;
  left: 12px;
  width: 15px;
  height: 15px;
  color: var(--text-muted);
  pointer-events: none;
  flex-shrink: 0;
}

.code-input {
  width: 100%;
  padding: 12px 44px 12px 38px;
  background: rgba(0, 20, 50, 0.6);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: 14px;
  font-family: inherit;
  outline: none;
  transition: border-color 0.25s, box-shadow 0.25s;
  letter-spacing: 1px;
}

.code-input::placeholder {
  color: var(--text-muted);
  letter-spacing: 0;
}

.code-input:focus {
  border-color: var(--blue-bright);
  box-shadow: 0 0 0 3px rgba(0, 150, 255, 0.15), 0 0 16px rgba(0, 120, 255, 0.2);
}

.toggle-eye {
  position: absolute;
  right: 10px;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  display: flex;
  align-items: center;
  padding: 4px;
  border-radius: 6px;
  transition: color 0.2s;
}

.toggle-eye:hover {
  color: var(--blue-light);
}

.input-error {
  display: none;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  color: var(--error-color);
  font-weight: 500;
  animation: none;
}

.input-error.visible {
  display: flex;
  animation: shakeError 0.4s ease-out;
}

@keyframes shakeError {

  0%,
  100% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-6px);
  }

  40% {
    transform: translateX(6px);
  }

  60% {
    transform: translateX(-4px);
  }

  80% {
    transform: translateX(4px);
  }
}

.input-wrapper.shake {
  animation: shakeInput 0.4s ease-out;
}

@keyframes shakeInput {

  0%,
  100% {
    transform: translateX(0);
  }

  20% {
    transform: translateX(-8px);
  }

  40% {
    transform: translateX(8px);
  }

  60% {
    transform: translateX(-5px);
  }

  80% {
    transform: translateX(5px);
  }
}

/* ---- BUTTONS ---- */
.btn-primary {
  width: 100%;
  padding: 13px 20px;
  background: linear-gradient(135deg, #0055cc, #0088ff);
  border: none;
  border-radius: var(--radius-sm);
  color: #fff;
  font-size: 14px;
  font-weight: 700;
  font-family: inherit;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  letter-spacing: 0.3px;
  position: relative;
  overflow: hidden;
  transition: transform 0.2s, box-shadow 0.2s;
  box-shadow: 0 4px 20px rgba(0, 100, 255, 0.4);
}

.btn-primary::before {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), transparent);
  opacity: 0;
  transition: opacity 0.2s;
}

.btn-primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 8px 28px rgba(0, 120, 255, 0.55);
}

.btn-primary:hover::before {
  opacity: 1;
}

.btn-primary:active {
  transform: translateY(1px);
}

.footer-text {
  text-align: center;
  font-size: 10px;
  color: var(--text-muted);
  margin-top: 20px;
  opacity: 0.6;
}

/* ---- LOGO LOOP ANIMATION ---- */
.logoloop-container.top-banner {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 40px;
  margin: 0;
  border-radius: 0;
  border-left: none;
  border-right: none;
  border-top: none;
  background: rgba(0, 10, 30, 0.8);
  border-bottom: 1px solid rgba(0, 150, 255, 0.2);
  z-index: 100;
  backdrop-filter: blur(10px);
}

.logoloop-container.top-banner:hover {
  background: rgba(0, 20, 50, 0.9);
  box-shadow: 0 4px 20px rgba(0, 150, 255, 0.1);
}

.logoloop-track {
  display: flex;
  width: max-content;
  animation: scroll-left 15s linear infinite;
  padding: 8px 0;
}

.logoloop-container:hover .logoloop-track {
  animation-play-state: paused;
}

.logoloop-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 20px;
}

.logoloop-item img {
  width: 20px;
  height: 20px;
  border-radius: 4px;
  object-fit: cover;
}

.logoloop-item span {
  font-size: 12px;
  font-weight: 600;
  color: #e0f0ff;
  white-space: nowrap;
}

@keyframes scroll-left {
  0% { transform: translateX(0); }
  100% { transform: translateX(-50%); }
}

/* Gradient Fade Edges */
.logoloop-container::before,
.logoloop-container::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 50px;
  z-index: 2;
  pointer-events: none;
}

.logoloop-container::before {
  left: 0;
  background: linear-gradient(to right, rgba(0, 10, 30, 1) 0%, transparent 100%);
}

.logoloop-container::after {
  right: 0;
  background: linear-gradient(to left, rgba(0, 10, 30, 1) 0%, transparent 100%);
}

/* ---- CATALOG BUTTON STATIC ---- */
.catalog-btn-static {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: #ffffff;
  text-decoration: none;
  font-size: 14px;
  font-weight: 700;
  margin: 10px auto 24px auto;
  padding: 11px 22px;
  border-radius: 12px;
  background: rgba(0, 150, 255, 0.22);
  border: 1px solid rgba(77, 184, 255, 0.55);
  box-shadow: 0 4px 15px rgba(0, 150, 255, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.15);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
  transition: all 0.3s ease;
  width: max-content;
}

.catalog-btn-static:hover {
  background: rgba(0, 150, 255, 0.35);
  border-color: rgba(77, 184, 255, 0.8);
  transform: translateY(-2px);
  box-shadow: 0 6px 22px rgba(0, 150, 255, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2);
}

/* Push login card down to make space for top banner */
.bg-canvas {
  top: 40px;
}

.screen {
  padding-top: 40px;
}

/* ---- DASHBOARD ---- */
#dashboard-screen {
  justify-content: flex-start;
  min-height: 100vh;
  padding: 0;
  width: 100%;
}

.dash-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 20px;
  border-bottom: 1px solid var(--glass-border);
  background: rgba(2, 14, 30, 0.7);
  backdrop-filter: blur(10px);
  width: 100%;
  flex-shrink: 0;
}

.dash-logo {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
  letter-spacing: 0.3px;
}

.header-badge {
  padding: 5px 10px;
  border-radius: 20px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.5px;
  text-transform: uppercase;
  display: flex;
  align-items: center;
  gap: 5px;
  background: rgba(0, 40, 100, 0.3);
  border: 1px solid var(--glass-border);
  color: var(--text-primary);
}

.header-badge.ok {
  background: rgba(0, 230, 118, 0.15);
  border-color: rgba(0, 230, 118, 0.3);
  color: #00e676;
}

.header-badge.warn {
  background: rgba(255, 170, 0, 0.15);
  border-color: rgba(255, 170, 0, 0.3);
  color: #ffcc44;
}

.header-badge.err {
  background: rgba(255, 77, 77, 0.15);
  border-color: rgba(255, 77, 77, 0.3);
  color: #ff8080;
}

.dash-title {
  padding: 18px 20px 10px;
  width: 100%;
}

.dash-title h2 {
  font-size: 17px;
  font-weight: 700;
  background: linear-gradient(135deg, #e0f0ff, #4db8ff);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  margin-bottom: 2px;
}

.logout-btn {
  background: rgba(255, 60, 60, 0.1);
  border: 1px solid rgba(255, 60, 60, 0.2);
  color: #ff6b6b;
  width: 28px;
  height: 28px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.2s;
}

.logout-btn:hover {
  background: #ff4d4d;
  color: white;
  transform: scale(1.05);
}

.dash-title p {
  font-size: 11px;
  color: var(--text-muted);
}

/* ---- APP LIST ---- */
.app-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  padding: 10px 20px 24px;
  width: 100%;
  flex: 1;
  overflow-y: visible;
  padding-bottom: 40px;
  /* buffer for bottom scrolling */
}

@media (min-width: 768px) {
  .app-grid {
    grid-template-columns: repeat(var(--desktop-cols, 4), 1fr);
    gap: 20px;
    padding: 20px 40px;
  }
}

.app-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  cursor: pointer;
  padding: 10px;
  border-radius: var(--radius-md);
  background: transparent;
  transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  animation: appItemIn 0.5s ease-out both;
  position: relative;
}

.app-item:hover {
  transform: translateY(-4px) scale(1.05);
}

.app-item:hover .app-icon-wrap {
  box-shadow: 0 8px 24px rgba(0, 150, 255, 0.4), 0 0 0 1px rgba(0, 200, 255, 0.3) inset;
}

.app-item[data-locked="true"]:hover {
  transform: none;
}

.app-item[data-locked="true"]:hover .app-icon-wrap {
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

.app-item:active {
  transform: translateY(0) scale(0.97);
}

@keyframes appItemIn {
  from {
    opacity: 0;
    transform: translateY(16px) scale(0.92);
  }

  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.app-icon-wrap {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-md);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 24px;
  position: relative;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  transition: all 0.25s ease;
}

.app-icon-wrap::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
}

.app-name {
  font-size: 10.5px;
  font-weight: 600;
  color: var(--text-primary);
  text-align: center;
  line-height: 1.2;
  letter-spacing: 0.3px;
}

.app-badge {
  opacity: 1;
}

/* ---- SERVER MODAL ---- */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(1, 8, 20, 0.75);
  backdrop-filter: blur(8px);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  animation: fadeIn 0.2s ease-out;
}

.modal-overlay.hidden {
  display: none !important;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

.modal-card {
  max-width: 340px;
  width: 100%;
  animation: modalSlideIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

@keyframes modalSlideIn {
  from {
    opacity: 0;
    transform: scale(0.85) translateY(20px);
  }

  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}

.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 20px;
}

.modal-title-group {
  display: flex;
  align-items: center;
  gap: 12px;
}

.modal-app-icon {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  flex-shrink: 0;
  position: relative;
  overflow: hidden;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}

.modal-app-icon::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
}

.modal-title {
  font-size: 16px;
  font-weight: 700;
  color: var(--text-primary);
  margin-bottom: 2px;
}

.modal-sub {
  font-size: 11px;
  color: var(--text-muted);
}

.btn-icon-close {
  width: 30px;
  height: 30px;
  border-radius: 8px;
  border: 1px solid var(--glass-border);
  background: rgba(0, 30, 70, 0.5);
  color: var(--text-secondary);
  cursor: pointer;
  font-size: 12px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
}

.btn-icon-close:hover {
  background: rgba(200, 30, 30, 0.3);
  border-color: rgba(255, 60, 60, 0.3);
  color: #ff8080;
}

/* ---- SERVER LIST ---- */
.server-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.server-btn {
  width: 100%;
  padding: 11px 16px;
  background: rgba(0, 30, 80, 0.4);
  border: 1px solid var(--glass-border);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: 13px;
  font-weight: 500;
  font-family: inherit;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 12px;
  transition: all 0.2s;
  text-align: left;
  position: relative;
  overflow: hidden;
}

.server-btn::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: linear-gradient(180deg, var(--blue-glow), var(--accent-cyan));
  border-radius: 2px;
  opacity: 0;
  transition: opacity 0.2s;
}

.server-btn:hover {
  background: rgba(0, 60, 160, 0.4);
  border-color: rgba(0, 150, 255, 0.45);
  box-shadow: 0 4px 16px rgba(0, 80, 200, 0.22);
  transform: translateX(3px);
}

.server-btn:hover::before {
  opacity: 1;
}

.server-btn:active {
  transform: translateX(1px);
}

.server-app-icon {
  width: 28px;
  height: 28px;
  border-radius: 6px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  flex-shrink: 0;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
  overflow: hidden;
  position: relative;
}

.server-app-icon::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
}

.server-info {
  flex: 1;
}

.server-name {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-primary);
}

.server-desc {
  font-size: 10px;
  color: var(--text-muted);
  margin-top: 1px;
}

.server-arrow {
  color: var(--text-muted);
  font-size: 12px;
  transition: transform 0.2s, color 0.2s;
}

.server-btn:hover .server-arrow {
  transform: translateX(3px);
  color: var(--blue-light);
}

/* Scrollbar */
::-webkit-scrollbar {
  width: 4px;
}

::-webkit-scrollbar-track {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: rgba(0, 100, 200, 0.3);
  border-radius: 2px;
}

::-webkit-scrollbar-thumb:hover {
  background: rgba(0, 150, 255, 0.5);
}

/* ============================================
   RAINY WINDOW BACKGROUND LAYERS
   Adapted from MIT-licensed CodePen by Fatih YILDIZ
   https://codepen.io/fyildiz1974/pen/RNRgjpj
   ============================================ */
#custom-bg, #custom-video {
  position: fixed;
  top: 50%; left: 50%;
  min-width: 100%; min-height: 100%;
  width: auto; height: auto;
  transform: translate(-50%, -50%) scale(1.05);
  z-index: 0;
  object-fit: cover;
  transition: filter 0.3s ease;
  will-change: filter;
  filter: blur(5px) brightness(0.3);
}
#custom-video { display: none; }
#custom-bg.hidden, #custom-video.hidden { display: none !important; }
#custom-video.active { display: block; }

#color-overlay {
  position: fixed; inset: 0;
  z-index: 0;
  pointer-events: none;
  mix-blend-mode: overlay;
  opacity: 0;
  transition: opacity 0.3s ease, background-color 0.3s ease;
}

#bg-canvas {
  position: fixed; inset: 0;
  width: 100%; height: 100%;
  z-index: 1;
  mix-blend-mode: screen;
  opacity: 0.9;
  pointer-events: none;
}

#rain-audio { display: none; }

/* ---- Floating UI buttons (settings/sound/share/fullscreen) ---- */
.tx-ui-btn {
  position: fixed;
  z-index: 1500;
  background: rgba(0, 12, 28, 0.6);
  border: 1px solid rgba(0, 150, 255, 0.25);
  color: #cfeaff;
  width: 44px; height: 44px;
  border-radius: 12px;
  font-size: 18px;
  cursor: pointer;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  display: flex; align-items: center; justify-content: center;
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  right: 16px;
  -webkit-tap-highlight-color: transparent;
}
.tx-ui-btn:hover { background: rgba(0, 100, 200, 0.35); border-color: rgba(0,200,255,0.6); transform: scale(1.05); }
.tx-ui-btn.active { background: rgba(0, 150, 255, 0.35); border-color: #4db8ff; }
#tx-btn-settings { top: 16px; }
#tx-btn-sound    { top: 70px; }
#tx-btn-share    { bottom: 70px; }
#tx-btn-fullscreen { bottom: 16px; }

body.tx-panel-active .tx-ui-btn { right: 360px; }

@media (max-width: 480px) {
  body.tx-panel-active .tx-ui-btn {
    right: auto; left: 12px;
  }
}

/* ---- Settings Panel ---- */
.tx-panel {
  position: fixed;
  top: 0; right: 0;
  width: 340px; height: 100%;
  background: rgba(2, 8, 20, 0.92);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border-left: 1px solid rgba(0, 150, 255, 0.18);
  padding: 60px 20px 20px;
  z-index: 1400;
  transform: translateX(100%);
  transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
  overflow-y: auto;
  overflow-x: hidden;
  color: #e0f0ff;
}
.tx-panel.open { transform: translateX(0); }

.tx-panel-close {
  position: absolute;
  top: 14px; right: 14px;
  width: 34px; height: 34px;
  background: rgba(255, 255, 255, 0.08);
  border: 1px solid rgba(255,255,255,.1);
  border-radius: 10px;
  color: #fff;
  font-size: 16px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
}
.tx-panel-close:hover { background: rgba(255,77,77,.25); border-color: rgba(255,77,77,.4); }

.tx-panel h3 {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  margin: 22px 0 12px;
  color: #4db8ff;
  border-bottom: 1px solid rgba(0,150,255,.12);
  padding-bottom: 8px;
}
.tx-panel h3:first-of-type { margin-top: 0; }

.tx-preset-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-bottom: 14px;
}
.tx-preset-item {
  aspect-ratio: 16/10;
  background-size: cover;
  background-position: center;
  border-radius: 8px;
  cursor: pointer;
  border: 2px solid transparent;
  opacity: 0.55;
  transition: all 0.2s ease;
  position: relative;
  overflow: hidden;
  background-color: #0a1a30;
}
.tx-preset-item:hover { opacity: 0.85; transform: scale(1.02); }
.tx-preset-item.active { border-color: #4db8ff; opacity: 1; box-shadow: 0 0 0 2px rgba(0,150,255,.25); }
.tx-preset-item::after {
  content: attr(data-label);
  position: absolute; bottom: 0; left: 0; right: 0;
  padding: 3px;
  background: linear-gradient(transparent, rgba(0,0,0,0.85));
  color: #fff; font-size: 8px; text-align: center;
}
.tx-preset-item.video::before {
  content: "▶";
  position: absolute; top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  font-size: 14px; color: #fff;
  text-shadow: 0 2px 6px rgba(0,0,0,.8);
  z-index: 1;
}

.tx-input-group { margin-top: 10px; }
.tx-input-row { display: flex; gap: 6px; margin-bottom: 8px; }
.tx-text-input {
  flex: 1;
  background: rgba(0, 30, 70, 0.5);
  border: 1px solid rgba(0,150,255,.2);
  color: #fff;
  padding: 9px 11px;
  border-radius: 8px;
  font-size: 12px;
  outline: none;
  font-family: inherit;
  min-width: 0;
}
.tx-text-input:focus { border-color: #4db8ff; box-shadow: 0 0 0 2px rgba(77,184,255,.15); }
.tx-text-input::placeholder { color: rgba(150,200,240,.5); }

.tx-btn {
  background: linear-gradient(135deg, #0066ff, #0099ff);
  border: none;
  color: #fff;
  padding: 9px 14px;
  border-radius: 8px;
  cursor: pointer;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
  font-family: inherit;
  transition: filter 0.2s, transform 0.2s;
}
.tx-btn:hover { filter: brightness(1.15); }
.tx-btn-file {
  width: 100%;
  background: rgba(0, 60, 150, 0.18);
  color: #cfeaff;
  border: 1px dashed rgba(0,150,255,.35);
  margin-top: 4px;
}
.tx-btn-file:hover { background: rgba(0, 80, 180, 0.3); }

.tx-control-group { margin-bottom: 14px; }
.tx-control-label {
  display: flex;
  justify-content: space-between;
  font-size: 11.5px;
  margin-bottom: 6px;
  color: #cfeaff;
}
.tx-control-value {
  color: #4db8ff;
  font-weight: 600;
  font-size: 11px;
}
.tx-panel input[type="range"] {
  width: 100%;
  height: 5px;
  border-radius: 3px;
  background: rgba(0, 150, 255, 0.12);
  appearance: none;
  outline: none;
  cursor: pointer;
}
.tx-panel input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 16px; height: 16px;
  background: #4db8ff;
  border-radius: 50%;
  cursor: pointer;
  box-shadow: 0 2px 6px rgba(0,0,0,.3);
}
.tx-panel input[type="range"]::-moz-range-thumb {
  width: 16px; height: 16px;
  background: #4db8ff;
  border-radius: 50%;
  cursor: pointer;
  border: none;
}

.tx-color-options {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.tx-color-btn {
  width: 26px; height: 26px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  transition: transform 0.2s;
  padding: 0;
}
.tx-color-btn:hover, .tx-color-btn.active {
  transform: scale(1.15);
  border-color: #fff;
}
.tx-color-btn.none {
  background: linear-gradient(135deg, #333 45%, #fff 45%, #fff 55%, #333 55%);
}

.tx-sound-control {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px;
  background: rgba(0, 30, 70, 0.35);
  border-radius: 10px;
  margin-bottom: 12px;
}
.tx-sound-icon {
  font-size: 18px;
  cursor: pointer;
  background: none;
  border: none;
  color: #cfeaff;
  padding: 4px;
  border-radius: 4px;
}
.tx-sound-control input[type="range"] { flex: 1; }

.tx-btn-reset {
  width: 100%;
  background: rgba(255, 100, 100, 0.18);
  color: #ff8080;
  border: 1px solid rgba(255,100,100,.3);
  margin-top: 6px;
}
.tx-btn-reset:hover { background: rgba(255, 100, 100, 0.28); }

/* Toast */
.tx-toast {
  position: fixed;
  bottom: 24px; left: 50%;
  transform: translateX(-50%) translateY(80px);
  background: rgba(2, 8, 20, 0.92);
  color: #fff;
  padding: 12px 22px;
  border-radius: 28px;
  font-size: 12px;
  z-index: 2500;
  opacity: 0;
  transition: all 0.3s ease;
  pointer-events: none;
  backdrop-filter: blur(10px);
  border: 1px solid rgba(0,150,255,.2);
}
.tx-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.tx-toast.error { border-color: rgba(255,100,100,.5); background: rgba(40,0,0,.92); }
.tx-toast.success { border-color: rgba(100,255,150,.5); }

@media (max-width: 480px) {
  .tx-panel { width: 100%; padding-top: 56px; }
  .tx-ui-btn { width: 40px; height: 40px; font-size: 16px; }
  #tx-btn-settings { top: 12px; right: 12px; }
  #tx-btn-sound    { top: 60px; right: 12px; }
  #tx-btn-share    { bottom: 60px; right: 12px; }
  #tx-btn-fullscreen { bottom: 12px; right: 12px; }
}

/* ============================================
   GLASSMORPHISM WIDGETS (Clock + Weather HUD)
   Adapted from MIT-licensed CodePen reference
   ============================================ */
@keyframes tx-widgetSlideIn {
  0%   { opacity: 0; transform: translateY(20px) scale(0.95); filter: blur(10px); }
  100% { opacity: 1; transform: translateY(0)    scale(1);    filter: blur(0); }
}
@keyframes tx-secondsPulse { 0%,100% { opacity: 0.5; } 50% { opacity: 0.95; } }
@keyframes tx-celestialGlow {
  0%,100% { filter: drop-shadow(0 0 8px rgba(255,200,100,0.3)); transform: scale(1); }
  50%     { filter: drop-shadow(0 0 16px rgba(255,200,100,0.5)); transform: scale(1.05); }
}
@keyframes tx-cloudDrift { 0%,100% { transform: translateX(0); } 50% { transform: translateX(3px); } }
@keyframes tx-rainSway   { 0%,100% { transform: rotate(-2deg); } 50% { transform: rotate(2deg); } }
@keyframes tx-shimmer    { 0% { background-position: -200% center; } 100% { background-position: 200% center; } }

.tx-widgets-container {
  position: fixed;
  top: 56px;
  left: 16px;
  z-index: 50;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 14px;
  pointer-events: none;
  perspective: 1000px;
  width: min(94vw, 320px);
}
.tx-widgets-container > * { pointer-events: auto; }
.tx-widgets-container.tx-widgets-hidden { opacity: 0; visibility: hidden; }
.tx-widgets-container.tx-widgets-visible { opacity: 1; visibility: visible; }
.tx-widgets-container.tx-widgets-visible .tx-datetime-widget { animation: tx-widgetSlideIn 0.8s cubic-bezier(0.16,1,0.3,1) forwards; }
.tx-widgets-container.tx-widgets-visible .tx-weather-widget {
  animation: tx-widgetSlideIn 0.8s cubic-bezier(0.16,1,0.3,1) 0.15s forwards;
  opacity: 0;
}

@media (max-width: 768px) {
  .tx-widgets-container { top: 50px; left: 10px; width: calc(100vw - 76px); max-width: 320px; }
}

/* Premium glass shared style */
.tx-datetime-widget,
.tx-weather-widget {
  position: relative;
  width: 100%;
  border-radius: 24px;
  color: #fff;
  background: linear-gradient(135deg,
    rgba(255,255,255,0.12) 0%,
    rgba(255,255,255,0.05) 50%,
    rgba(255,255,255,0.08) 100%);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
  border: 1px solid transparent;
  background-clip: padding-box;
  box-shadow:
    0 8px 32px rgba(0,0,0,0.3),
    0 0 0 1px rgba(255,255,255,0.05),
    inset 0 1px 0 rgba(255,255,255,0.15),
    inset 0 -1px 0 rgba(0,0,0,0.1);
  transition: all 0.4s cubic-bezier(0.16,1,0.3,1);
  transform-style: preserve-3d;
}
/* Gradient border */
.tx-datetime-widget::before,
.tx-weather-widget::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 24px;
  padding: 1px;
  background: linear-gradient(135deg,
    rgba(255,255,255,0.3) 0%,
    rgba(255,255,255,0.05) 50%,
    rgba(255,255,255,0.1) 100%);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
          mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  pointer-events: none;
}
.tx-datetime-widget::after,
.tx-weather-widget::after {
  content: "";
  position: absolute;
  top: 0; left: 10%; right: 10%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.5), transparent);
  pointer-events: none;
}
.tx-datetime-widget:hover,
.tx-weather-widget:hover {
  transform: translateY(-3px) scale(1.01);
  box-shadow:
    0 18px 50px rgba(0,0,0,0.45),
    0 0 0 1px rgba(255,255,255,0.1),
    inset 0 1px 0 rgba(255,255,255,0.2),
    0 0 40px rgba(102,180,255,0.12);
}

/* DATETIME */
.tx-datetime-widget { padding: 22px 26px; text-align: center; overflow: hidden; }
.tx-datetime-widget .tx-time {
  font-size: 52px; font-weight: 200; letter-spacing: 2px;
  line-height: 1; margin-bottom: 6px;
  text-shadow: 0 0 40px rgba(255,255,255,0.15), 0 2px 10px rgba(0,0,0,0.3);
  font-variant-numeric: tabular-nums;
}
.tx-datetime-widget .tx-time-seconds {
  font-size: 18px; font-weight: 300; opacity: 0.6;
  vertical-align: top; margin-left: 4px; min-width: 26px; display: inline-block;
  animation: tx-secondsPulse 1s ease-in-out infinite;
}
.tx-datetime-widget .tx-date {
  font-size: 13px; font-weight: 400; opacity: 0.85; letter-spacing: 0.5px;
}
.tx-datetime-widget .tx-day-name {
  font-size: 10px; font-weight: 500; opacity: 0.6;
  text-transform: uppercase; letter-spacing: 2px; margin-top: 2px;
}
.tx-datetime-widget .tx-day-phase {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  margin-top: 12px; padding: 5px 13px;
  font-size: 11px; font-weight: 500; letter-spacing: 0.4px;
  background: linear-gradient(135deg, rgba(255,255,255,0.12), rgba(255,255,255,0.05));
  border-radius: 20px;
  border: 1px solid rgba(255,255,255,0.1);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 8px rgba(0,0,0,0.15);
}

/* WEATHER */
.tx-weather-widget { padding: 18px 22px; overflow: hidden; }
.tx-weather-widget.tx-loading { min-height: 130px; }
.tx-weather-widget.tx-loading::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.08), transparent);
  background-size: 200% 100%;
  animation: tx-shimmer 2s infinite;
  border-radius: 24px;
}
.tx-weather-header {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 10px; margin-bottom: 14px;
}
.tx-weather-location {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 10px; margin: -6px -10px;
  font-size: 13px; font-weight: 500;
  border-radius: 10px; cursor: pointer;
  transition: background 0.3s ease;
}
.tx-weather-location:hover { background: rgba(255,255,255,0.1); }
.tx-weather-location-text {
  max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tx-weather-update-time { font-size: 10px; opacity: 0.5; padding-top: 4px; }
.tx-weather-current {
  display: flex; align-items: center; gap: 14px; margin-bottom: 14px;
}
.tx-weather-icon {
  font-size: 48px; line-height: 1;
  transition: all 0.3s ease;
}
.tx-weather-icon.tx-rain  { animation: tx-rainSway 2s ease-in-out infinite; }
.tx-weather-icon.tx-sun,
.tx-weather-icon.tx-moon  { animation: tx-celestialGlow 4s ease-in-out infinite; }
.tx-weather-icon.tx-cloud { animation: tx-cloudDrift 4s ease-in-out infinite; }
.tx-weather-info { flex: 1; }
.tx-weather-temp {
  font-size: 38px; font-weight: 200; line-height: 1; letter-spacing: -1px;
  text-shadow: 0 0 30px rgba(255,255,255,0.1), 0 2px 10px rgba(0,0,0,0.2);
  font-variant-numeric: tabular-nums;
}
.tx-weather-temp-unit {
  font-size: 16px; font-weight: 300; opacity: 0.6;
  vertical-align: top; margin-left: 2px;
}
.tx-weather-desc { font-size: 13px; opacity: 0.85; margin-top: 4px; }
.tx-weather-details {
  display: flex; gap: 8px;
  padding-bottom: 0;
}
.tx-weather-detail-item {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; gap: 4px;
  padding: 8px 6px;
  background: rgba(255,255,255,0.05);
  border-radius: 12px;
  border: 1px solid rgba(255,255,255,0.05);
  transition: all 0.3s ease;
}
.tx-weather-detail-item:hover {
  background: rgba(255,255,255,0.1);
  border-color: rgba(255,255,255,0.1);
}
.tx-weather-detail-icon { font-size: 14px; opacity: 0.85; }
.tx-weather-detail-item span:last-child { font-size: 11.5px; font-weight: 500; }

/* Hourly Forecast (next 3 hours) */
.tx-weather-hourly {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid rgba(255,255,255,0.08);
}
.tx-weather-hour {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 6px;
  background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.02));
  border-radius: 12px;
  border: 1px solid rgba(255,255,255,0.05);
  transition: all 0.3s ease;
}
.tx-weather-hour:hover {
  background: linear-gradient(180deg, rgba(255,255,255,0.1), rgba(255,255,255,0.04));
  transform: translateY(-2px);
}
.tx-weather-hour.tx-now {
  background: linear-gradient(180deg, rgba(102,204,255,0.18), rgba(102,204,255,0.06));
  border-color: rgba(102,204,255,0.3);
}
.tx-weather-hour.tx-now::before {
  content: "";
  position: absolute;
  top: 6px; right: 6px;
  width: 5px; height: 5px;
  background: #6cf;
  border-radius: 50%;
  box-shadow: 0 0 6px rgba(102,204,255,0.8);
  animation: tx-secondsPulse 1.5s ease-in-out infinite;
}
.tx-weather-hour-time { font-size: 10px; font-weight: 500; opacity: 0.7; }
.tx-weather-hour.tx-now .tx-weather-hour-time { color: #6cf; opacity: 1; font-weight: 600; }
.tx-weather-hour-icon { font-size: 18px; line-height: 1; }
.tx-weather-hour-temp { font-size: 13px; font-weight: 600; }

@media (max-width: 480px) {
  .tx-datetime-widget .tx-time { font-size: 44px; }
  .tx-datetime-widget { padding: 18px 22px; }
  .tx-weather-widget { padding: 16px 18px; }
  .tx-weather-icon { font-size: 40px; }
  .tx-weather-temp { font-size: 32px; }
}
</style>
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&display=swap"
    rel="stylesheet" /></style>
</head>
<body>

  <!-- ========= LOADING OVERLAY ========= -->
  <div id="loadingOverlay" class="loading-overlay">
    <div class="loading-content">
      <img src="https://media.giphy.com/media/2P0BqNiMBw4HCix9xW/giphy.gif" alt="Loading Logo" class="loading-logo">
      <div class="loading-text">Sedang Mencari server terbaik...</div>
      <div class="loading-spinner"></div>
      
      <!-- Error State -->
      <div id="loadingErrorState" class="loading-error-state" style="display: none;">
        <div class="loading-error-text">Gagal memuat server. Silakan ulangi melalui app launcher</div>
        <button class="loading-retry-btn" onclick="retryLoading()">Ulangi Pilih Aplikasi</button>
      </div>
    </div>
  </div>
  
  <style>
    .loading-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100vw;
      height: 100vh;
      background: rgba(10, 15, 30, 0.85); /* Semi-transparent dark background */
      backdrop-filter: blur(8px); /* Blur effect on background content */
      -webkit-backdrop-filter: blur(8px);
      z-index: 99999; /* Always on top */
      display: flex;
      justify-content: center;
      align-items: center;
      opacity: 0;
      visibility: hidden;
      transition: opacity 0.4s ease, visibility 0.4s ease;
      pointer-events: none; /* Default no interaction */
    }

    .loading-overlay.active {
      opacity: 1;
      visibility: visible;
      pointer-events: all; /* Block all clicks underneath */
    }

    .loading-content {
      display: flex;
      flex-direction: column;
      align-items: center;
      text-align: center;
      transform: translateY(20px);
      transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
    }

    .loading-overlay.active .loading-content {
      transform: translateY(0);
    }

    .loading-logo {
      width: 150px;
      height: 150px;
      object-fit: cover;
      border-radius: 50%;
      margin-bottom: 20px;
      box-shadow: 0 0 30px rgba(0, 204, 255, 0.2);
      border: 2px solid rgba(0, 204, 255, 0.1);
    }

    .loading-text {
      color: #ffffff;
      font-family: 'Inter', sans-serif;
      font-size: 1.1rem;
      font-weight: 500;
      letter-spacing: 0.5px;
      margin-bottom: 15px;
      text-shadow: 0 2px 4px rgba(0,0,0,0.5);
      animation: pulseText 1.5s infinite alternate;
    }

    @keyframes pulseText {
      from { opacity: 0.7; }
      to { opacity: 1; }
    }

    .loading-spinner {
      width: 40px;
      height: 40px;
      border: 3px solid rgba(0, 204, 255, 0.1);
      border-radius: 50%;
      border-top-color: #00ccff;
      animation: spin 1s ease-in-out infinite;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    /* Error State Styling */
    .loading-error-state {
      margin-top: 15px;
      animation: fadeIn 0.3s ease;
    }

    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(-10px); }
      to { opacity: 1; transform: translateY(0); }
    }

    .loading-error-text {
      color: #ff4d4d;
      font-size: 0.95rem;
      margin-bottom: 15px;
      font-weight: 500;
    }

    .loading-retry-btn {
      background: linear-gradient(135deg, #0072ff 0%, #00c6ff 100%);
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 8px;
      font-family: 'Inter', sans-serif;
      font-weight: 600;
      cursor: pointer;
      transition: all 0.2s ease;
      box-shadow: 0 4px 15px rgba(0, 114, 255, 0.3);
    }

    .loading-retry-btn:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 20px rgba(0, 114, 255, 0.4);
    }

    .loading-retry-btn:active {
      transform: translateY(0);
    }
    
    /* Blur background wrapper when loading is active */
    body.is-loading > *:not(.loading-overlay) {
      filter: blur(5px);
      pointer-events: none;
      user-select: none;
      transition: filter 0.4s ease;
    }
  </style>

  <!-- ========= RAINY WINDOW BACKGROUND (MIT © Fatih YILDIZ - codepen.io/fyildiz1974) ========= -->
  <!-- Background Image (crossorigin needed so canvas can read pixels for raindrop refraction) -->
  <img id="custom-bg" crossorigin="anonymous" src="<?php echo htmlspecialchars($_theme['bg_type'] === 'image' ? $_theme['bg_url'] : 'https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1920&q=80'); ?>" alt="Background" />
  <!-- Background Video -->
  <video id="custom-video" class="<?php echo $_theme['bg_type'] === 'video' ? 'active' : 'hidden'; ?>" muted loop playsinline webkit-playsinline preload="metadata" crossorigin="anonymous" aria-hidden="true"<?php if ($_theme['bg_type'] === 'video'): ?> src="<?php echo htmlspecialchars($_theme['bg_url']); ?>"<?php endif; ?>></video>
  <!-- Color Overlay -->
  <div id="color-overlay" aria-hidden="true"></div>
  <!-- Raindrops Canvas (WebGL) -->
  <canvas id="bg-canvas" aria-hidden="true"></canvas>
  <!-- Rain Sound (looped) - local file -->
  <audio id="rain-audio" loop preload="auto">
    <source src="<?php echo $_base_url; ?>/inject/assets/rain.mp3" type="audio/mpeg" />
  </audio>

  <!-- ========= GLASSMORPHISM HUD (Clock + Weather) — admin-toggle ========= -->
  <?php if (!empty($_theme['show_widgets'])): ?>
  <div class="tx-widgets-container tx-widgets-hidden" id="tx-widgets-container" aria-hidden="true">
    <!-- DateTime Widget -->
    <div class="tx-datetime-widget" id="tx-datetime-widget" role="timer" aria-live="polite">
      <div class="tx-time">
        <span id="tx-time-display">--:--</span>
        <span class="tx-time-seconds" id="tx-time-seconds">--</span>
      </div>
      <div class="tx-date" id="tx-date-display">--</div>
      <div class="tx-day-name" id="tx-day-display">--</div>
      <div class="tx-day-phase" id="tx-day-phase">
        <span id="tx-day-phase-icon">🌙</span>
        <span id="tx-day-phase-text">--</span>
      </div>
    </div>

    <!-- Weather Widget -->
    <div class="tx-weather-widget tx-loading" id="tx-weather-widget" role="region">
      <div id="tx-weather-content" style="display:none;">
        <div class="tx-weather-header">
          <div class="tx-weather-location" id="tx-weather-location" tabindex="0" role="button">
            <span class="tx-weather-location-icon">📍</span>
            <span class="tx-weather-location-text" id="tx-location-text">Loading...</span>
          </div>
          <div class="tx-weather-update-time" id="tx-weather-update-time"></div>
        </div>
        <div class="tx-weather-current">
          <span class="tx-weather-icon" id="tx-weather-icon">☁️</span>
          <div class="tx-weather-info">
            <div class="tx-weather-temp">
              <span id="tx-weather-temp">--</span>
              <span class="tx-weather-temp-unit">°C</span>
            </div>
            <div class="tx-weather-desc" id="tx-weather-desc">--</div>
          </div>
        </div>
        <div class="tx-weather-details">
          <div class="tx-weather-detail-item"><span class="tx-weather-detail-icon">💧</span><span id="tx-weather-humidity">--%</span></div>
          <div class="tx-weather-detail-item"><span class="tx-weather-detail-icon">💨</span><span id="tx-weather-wind">-- km/h</span></div>
          <div class="tx-weather-detail-item"><span class="tx-weather-detail-icon">🌡️</span><span id="tx-weather-feels">--°</span></div>
        </div>
        <div class="tx-weather-hourly" id="tx-weather-hourly" role="list"></div>
      </div>
    </div>
  </div>
  <?php endif; // show_widgets ?>

  <!-- Katalog Link Top Banner (Full Width) -->
  <a href="#" id="catalog-link-top" class="logoloop-container top-banner" target="_blank" style="display: none;">
    <div class="logoloop-track" id="logoloop-track-top">
      <!-- Logo items will be injected here by JS -->
    </div>
  </a>

  <!-- ========= LOGIN SCREEN ========= -->
  <div class="screen" id="login-screen">
    <div class="login-card glass-card">
      <div class="brand-logo">
        <div class="logo-icon" id="dynamic-login-logo">
          <img src="<?php echo htmlspecialchars($b_logo); ?>" alt="<?php echo htmlspecialchars($b_name); ?> Logo"
            style="width:100%;height:100%;object-fit:cover;border-radius:inherit;display:block;"
            onerror="this.src='<?php echo $_base_url; ?>/inject/app_icons/default.png'">
        </div>
      </div>
      <h1 class="brand-name" id="dynamic-login-title"><?php echo htmlspecialchars($b_name); ?></h1>
      <p class="brand-sub" id="dynamic-login-subtitle"><?php echo htmlspecialchars($b_subtitle); ?></p>

      <!-- Tombol Katalog Statis di bawah judul -->
      <a href="#" id="catalog-link-static" class="catalog-btn-static" target="_blank" style="display: none;">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
          <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
          <polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
          <line x1="12" y1="22.08" x2="12" y2="12"></line>
        </svg>
        <?php echo htmlspecialchars($b_catalog); ?>
      </a>

      <div class="input-group">
        <label class="input-label"><?php echo htmlspecialchars($b_label); ?></label>
        <div class="input-wrapper" id="input-wrapper">
          <svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <rect x="3" y="11" width="18" height="11" rx="2" />
            <path d="M7 11V7a5 5 0 0 1 10 0v4" />
          </svg>
          <input type="password" id="access-code" class="code-input" placeholder="<?php echo htmlspecialchars($b_placeholder); ?>"
            autocomplete="off" />
          <button class="toggle-eye" id="toggle-eye" title="Show/Hide">
            <svg id="eye-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16"
              height="16">
              <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
              <circle cx="12" cy="12" r="3" />
            </svg>
          </button>
        </div>
        <div class="input-error" id="login-error">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13">
            <circle cx="12" cy="12" r="10" />
            <line x1="12" y1="8" x2="12" y2="12" />
            <line x1="12" y1="16" x2="12.01" y2="16" />
          </svg>
          <span><?php echo htmlspecialchars($b_error); ?></span>
        </div>
      </div>

      <button class="btn-primary" id="login-btn">
        <span><?php echo htmlspecialchars($b_btn); ?></span>
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="16" height="16">
          <path d="M5 12h14M12 5l7 7-7 7" />
        </svg>
      </button>

      <button class="btn-primary" id="takeover-btn" style="display: none; background: #ff4d4d; margin-top: 10px; box-shadow: 0 4px 15px rgba(255, 77, 77, 0.4); color: white; border: none; align-items: center; justify-content: center; gap: 8px;">
        <span><?php echo htmlspecialchars($b_takeover); ?></span>
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="16" height="16">
          <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
          <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
        </svg>
      </button>

      <p class="footer-text"><?php echo htmlspecialchars($b_footer); ?></p>
    </div>
  </div>

  <!-- ========= DASHBOARD SCREEN ========= -->
  <div class="screen hidden" id="dashboard-screen">
    <header class="dash-header">
      <div class="dash-logo">
        <div class="logo-icon sm">
          <img src="<?php echo htmlspecialchars($b_logo); ?>" alt="Logo"
            style="width:100%;height:100%;object-fit:cover;border-radius:inherit;display:block;">
        </div>
        <span><?php echo htmlspecialchars($b_dash_name); ?></span>
      </div>
      <div style="display:flex; gap:8px; align-items:center;">
        <div id="header-badge" class="header-badge"></div>
        <button id="logout-btn" class="logout-btn" title="Keluar dari Perangkat ini">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
            <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
            <polyline points="16 17 21 12 16 7"></polyline>
            <line x1="21" y1="12" x2="9" y2="12"></line>
          </svg>
        </button>
      </div>
    </header>

    <div class="dash-title">
      <h2>App Launcher</h2>
      <p id="member-info">Pilih aplikasi untuk memulai</p>
    </div>

    <div class="app-grid" id="app-grid">
      <!-- App drawers are injected by popup.js -->
    </div>
  </div>

  <!-- ========= SERVER MODAL ========= -->
  <div class="modal-overlay hidden" id="modal-overlay">
    <div class="modal-card glass-card">
      <div class="modal-header">
        <div class="modal-title-group">
          <div class="modal-app-icon" id="modal-app-icon"></div>
          <div>
            <h3 class="modal-title" id="modal-title">Select Server</h3>
            <p class="modal-sub">Pilih server untuk terhubung</p>
          </div>
        </div>
        <button class="btn-icon-close" id="modal-close">&#10005;</button>
      </div>
      <div class="server-list" id="server-list">
        <!-- Servers injected by JS -->
      </div>
    </div>
  </div>

  <!-- ========= CUSTOM ALERT MODAL ========= -->
  <div class="modal-overlay hidden" id="custom-alert">
    <div class="modal-card glass-card" style="text-align:center; padding: 24px;">
      <div id="custom-alert-icon" style="font-size: 32px; margin-bottom: 12px;">⚠️</div>
      <h3 class="modal-title" id="custom-alert-title" style="margin-bottom: 8px;">Peringatan</h3>
      <p class="modal-sub" id="custom-alert-msg" style="margin-bottom: 20px; line-height: 1.5; color: #a1a1aa;"></p>
      <a href="#" target="_blank" class="btn-primary" id="custom-alert-action" style="width: 100%; margin-bottom: 12px; display: none; text-decoration: none; background: linear-gradient(135deg, #ffcc00, #ffaa00); color: #000; box-shadow: 0 4px 15px rgba(255, 170, 0, 0.3);">Action</a>
      <button class="btn-primary" id="custom-alert-close" style="width: 100%;">Tutup</button>
    </div>
  </div>

  
<script>
(function() {
    const urlParamsMock = new URLSearchParams(window.location.search);
    let extId = urlParamsMock.get('ext_id');
    if (extId && extId !== 'undefined' && extId !== 'null') {
        localStorage.setItem('texa_ext_id', extId);
    } else {
        extId = localStorage.getItem('texa_ext_id');
        if (extId === 'undefined' || extId === 'null') extId = null;
    }

    // To avoid infinite loop when calling original chrome API
    window.__origChrome = window.chrome;

    // ============================================================
    // postMessage Bridge Helper (untuk Orion iOS / Safari Extension)
    // Menggantikan externally_connectable yang tidak tersedia di WebKit
    // ============================================================
    const _pendingBridgeCalls = {};
    let _bridgeRequestId = 0;

    // Listener untuk menerima response dari content.js
    window.addEventListener('message', (event) => {
        if (!event.data || !event.data.__texa_bridge_response) return;
        const { requestId, response } = event.data;
        if (requestId !== undefined && _pendingBridgeCalls[requestId]) {
            const cb = _pendingBridgeCalls[requestId];
            delete _pendingBridgeCalls[requestId];
            cb(response || {});
        }
    });

    function sendViaBridge(msg, callback) {
        const requestId = _bridgeRequestId++;
        if (callback) {
            _pendingBridgeCalls[requestId] = callback;
            // Timeout fallback: jika content.js tidak merespons dalam 3 detik
            setTimeout(() => {
                if (_pendingBridgeCalls[requestId]) {
                    delete _pendingBridgeCalls[requestId];
                    callback({});
                }
            }, 3000);
        }
        
        // Native Android Bridge support
        if (window.AndroidBridge && typeof window.AndroidBridge.postMessage === 'function') {
            window['__texa_cb_' + requestId] = function(response) {
                if (_pendingBridgeCalls[requestId]) {
                    delete _pendingBridgeCalls[requestId];
                    callback(response);
                }
            };
            try {
                window.AndroidBridge.postMessage(JSON.stringify({requestId: requestId, payload: msg}));
            } catch(e) { console.error("AndroidBridge error", e); }
            return;
        }

        window.postMessage({
            __texa_bridge: true,
            requestId: requestId,
            payload: msg
        }, window.location.origin);
    }

    // PENTING: Expose ke window agar bisa diakses dari script block lain
    window.sendViaBridge = sendViaBridge;

    function sendMessage(msg, callback) {
        // ORION iOS FIX: externally_connectable dihapus dari manifest.
        // chrome.runtime.sendMessage(extId, ...) dari web page TIDAK BISA digunakan.
        // Gunakan postMessage bridge (content.js relay) sebagai satu-satunya jalur komunikasi.
        // Content.js sudah diinjeksikan oleh extension ke halaman ini (<all_urls> match).
        sendViaBridge(msg, callback);
    }



    // Setup mock chrome API
    const chromeMock = {
        runtime: {
        id: extId,
        getURL: function(path) {
            if (path === 'popup.html') {
                return '<?php echo $_base_url; ?>/inject/remote_app.php?ext_id=' + (extId || '');
            }
            return '<?php echo $_base_url; ?>/inject/' + path;
        },
        sendMessage: function(msg, callback) {
            sendMessage(msg, callback);
        },
        getManifest: function() {
            return window.__origChrome && window.__origChrome.runtime && window.__origChrome.runtime.getManifest 
                ? window.__origChrome.runtime.getManifest() 
                : {version: "5.0.0", name: "TEKNO APPS"};
        }
    },
    storage: {
        local: {
            set: function(data, cb) {
                for (let key in data) {
                    localStorage.setItem('mock_storage_' + key, typeof data[key] === 'string' ? data[key] : JSON.stringify(data[key]));
                }
                sendMessage({action: 'save_storage', data: data}, cb);
            },
            get: function(keys, cb) {
                const fallbackGet = () => {
                    let res = {};
                    keys.forEach(k => {
                        let val = localStorage.getItem('mock_storage_' + k);
                        try { 
                            if (val && (val.startsWith('{') || val.startsWith('['))) val = JSON.parse(val); 
                        } catch(e){}
                        if (val !== null) res[k] = val;
                    });
                    if (cb) cb(res);
                };

                sendMessage({action: 'get_storage', keys: keys}, (response) => {
                    if (response && response.success && response.data) {
                        // sync to localstorage just in case
                        for (let k in response.data) {
                            localStorage.setItem('mock_storage_' + k, typeof response.data[k] === 'string' ? response.data[k] : JSON.stringify(response.data[k]));
                        }
                        if (cb) cb(response.data);
                    } else {
                        fallbackGet();
                    }
                });
            },
            remove: function(keys, cb) {
                let data = {};
                keys.forEach(k => {
                    localStorage.removeItem('mock_storage_' + k);
                    data[k] = ''; // empty out in extension
                });
                sendMessage({action: 'save_storage', data: data}, cb);
            }
        },
        session: {
            get: function(keys, cb) { if (cb) cb({}); },
            set: function(data, cb) { if (cb) cb(); },
            remove: function(keys, cb) { if (cb) cb(); }
        }
    },
    tabs: {
        update: function(tabId, info) {
            window.location.href = info.url;
        },
        getCurrent: function(cb) {
            cb(null);
        },
        query: function(q, cb) {
            cb([]);
        }
    }
};
    
    // PENTING: Selalu install chromeMock ke window.chrome tanpa syarat extId.
    // Jika extId null (URL dibuka manual tanpa ?ext_id=), chromeMock tetap akan diinstall.
    // Ini mencegah TypeError "must specify Extension ID" saat webpage memanggil chrome.runtime.sendMessage.
    // Semua panggilan akan disalurkan via sendViaBridge (postMessage bridge ke content.js).
    window.chrome = chromeMock;
})();

</script>
<script>
// To make sure all functions in this block use our chromeMock
const chromeMockRef = window.chrome;

// TEKNO-APPS - Popup Logic
// Handles: Login, Dashboard, Server Modal, Floating Tab Launch

'use strict';

// API URL — auto-detected dari PHP
const TEXA_SERVER = '<?php echo $_base_url; ?>';
const API_URL = TEXA_SERVER + '/inject/index.php';


// APPS kini dideklarasikan kosong dan akan diisi langsung dari backend ketika user Login
let APPS = [];

const SERVER_REGIONS = [
    { name: 'Server US-East 1', desc: 'Virginia · Low latency' },
    { name: 'Server US-West 2', desc: 'Oregon · High throughput' },
    { name: 'Server EU-Central', desc: 'Frankfurt · GDPR compliant' },
    { name: 'Server AP-Southeast', desc: 'Singapore · Asia Pacific' },
    { name: 'Server Global CDN', desc: 'Multi-region · Balanced' }
];

// =============================================
// DOM REFERENCES
// =============================================
const loginScreen = document.getElementById('login-screen');
const dashScreen = document.getElementById('dashboard-screen');
const accessCodeInput = document.getElementById('access-code');
const loginBtn = document.getElementById('login-btn');
const loginError = document.getElementById('login-error');
const inputWrapper = document.getElementById('input-wrapper');
const appGrid = document.getElementById('app-grid');
const modalOverlay = document.getElementById('modal-overlay');
const modalClose = document.getElementById('modal-close');
const serverList = document.getElementById('server-list');
const modalTitle = document.getElementById('modal-title');
const modalAppIcon = document.getElementById('modal-app-icon');
const dashLogoutBtn = document.getElementById('logout-btn');

// =============================================
// PARTICLES
// =============================================
function generateParticles() {
    const container = document.getElementById('particles');
    if (!container) return;
    for (let i = 0; i < 30; i++) {
        const p = document.createElement('div');
        p.className = 'particle';
        const size = Math.random() * 2.5 + 1;
        p.style.cssText = `
            width: ${size}px;
            height: ${size}px;
            left: ${Math.random() * 100}%;
            top: ${Math.random() * 100}%;
            --dur: ${Math.random() * 4 + 2.5}s;
            --delay: -${Math.random() * 6}s;
        `;
        container.appendChild(p);
    }
}

// =============================================
// SESSION
// =============================================
function isLoggedIn() {
    return localStorage.getItem('texa_auth') === '1';
}

function setLoggedIn(v, name = '', expiry = '', appsConfig = null, desktopCols = 4) {
    if (v) {
        localStorage.setItem('texa_auth', '1');
        localStorage.setItem('texa_name', name || '');
        localStorage.setItem('texa_expiry', expiry || '');
        localStorage.setItem('texa_apps', appsConfig ? JSON.stringify(appsConfig) : '');
        localStorage.setItem('texa_desktop_cols', desktopCols || 4);
        document.documentElement.style.setProperty('--desktop-cols', desktopCols || 4);
    } else {
        ['texa_auth', 'texa_name', 'texa_expiry', 'texa_apps', 'texa_desktop_cols'].forEach(k => localStorage.removeItem(k));
        document.documentElement.style.removeProperty('--desktop-cols');
    }
}

function goToDashboard() {
    loginScreen.classList.add('hidden');
    dashScreen.classList.remove('hidden');

    // Sembunyikan banner logo loop saat masuk ke App Launcher (jika sudah login dari awal)
    const topBanner = document.getElementById('catalog-link-top');
    if (topBanner) topBanner.style.display = 'none';

    // Show member name + remaining days
    const name = localStorage.getItem('texa_name') || '';
    const expiry = localStorage.getItem('texa_expiry') || '';
    const badgeEl = document.getElementById('header-badge');
    const infoEl = document.getElementById('member-info');

    if (infoEl && name) {
        infoEl.textContent = `Pilih aplikasi untuk memulai`;
    }

    if (badgeEl && name) {
        let sisaText = '';
        let badgeClass = 'ok';

        if (expiry) {
            const today = new Date();
            const expDate = new Date(expiry);
            today.setHours(0, 0, 0, 0);
            expDate.setHours(0, 0, 0, 0);
            const diff = Math.ceil((expDate - today) / (1000 * 60 * 60 * 24));

            if (diff <= 0) {
                sisaText = 'Expired';
                badgeClass = 'err';
            } else if (diff <= 3) {
                sisaText = `${diff} hari`;
                badgeClass = 'warn';
            } else {
                sisaText = `${diff} hari`;
                badgeClass = 'ok';
            }
        }

        badgeEl.className = `header-badge ${badgeClass}`;
        badgeEl.innerHTML = `👤 ${name} <span style="opacity:0.6;margin:0 2px">•</span> ${sisaText}`;
    }
}

function goToLogin() {
    dashScreen.classList.add('hidden');
    loginScreen.classList.remove('hidden');

    // Tampilkan kembali banner logo loop saat di halaman login
    const topBanner = document.getElementById('catalog-link-top');
    if (topBanner && topBanner.getAttribute('href') !== '#') {
        topBanner.style.display = 'block';
    }
}

// =============================================
// LOGOUT
// =============================================
if (dashLogoutBtn) {
    dashLogoutBtn.addEventListener('click', async () => {
        const code = localStorage.getItem('texa_code');
        const deviceId = getDeviceId();
        dashLogoutBtn.disabled = true;

        if (code && deviceId) {
            try {
                await fetch(`${API_URL}?action=logout&code=${encodeURIComponent(code)}&device_id=${encodeURIComponent(deviceId)}`, { cache: 'no-store' });
            } catch (e) {
                console.error('Logout pings failed:', e);
            }
        }

        localStorage.removeItem('texa_code');
        if (chromeMockRef && chromeMockRef.storage && chromeMockRef.storage.local) {
            chromeMockRef.storage.local.remove(['texa_code', 'texa_device_id', 'texa_apps']);
        }
        setLoggedIn(false);
        dashLogoutBtn.disabled = false;
        goToLogin();
    });
}

// =============================================
// LOGIN
// =============================================
function showError(msg = 'Kode akses tidak valid.') {
    inputWrapper.classList.add('shake');
    loginError.classList.add('visible');
    const span = loginError.querySelector('span');
    if (span) span.textContent = msg;
    accessCodeInput.style.borderColor = '#ff4d4d';
    accessCodeInput.style.boxShadow = '0 0 0 3px rgba(255, 77, 77, 0.15)';
    setTimeout(() => inputWrapper.classList.remove('shake'), 500);

    // Deteksi jika pesan error mengindikasikan sesi aktif di perangkat lain
    if (msg.includes('perangkat lain')) {
        const takeoverBtn = document.getElementById('takeover-btn');
        if (takeoverBtn) {
            takeoverBtn.style.display = 'flex';
            
            // Hapus listener lama jika ada, lalu pasang yang baru
            takeoverBtn.replaceWith(takeoverBtn.cloneNode(true));
            const newTakeoverBtn = document.getElementById('takeover-btn');
            
            newTakeoverBtn.addEventListener('click', async () => {
                newTakeoverBtn.disabled = true;
                newTakeoverBtn.innerHTML = '<span>Mengambil Alih...</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/></path></svg>';
                
                try {
                    const code = accessCodeInput.value.trim();
                    const deviceId = getDeviceId();
                    const res = await fetch(`${API_URL}?action=takeover&code=${encodeURIComponent(code)}&device_id=${encodeURIComponent(deviceId)}`, {
                        cache: 'no-store'
                    });
                    
                    const data = await res.json();
                    if (data.valid) {
                        // Sesi berhasil diambil alih, lanjut login biasa
                        handleLogin();
                    } else {
                        showError(data.reason || 'Gagal mengambil alih sesi.');
                        newTakeoverBtn.style.display = 'none';
                    }
                } catch (e) {
                    showError('Terjadi kesalahan koneksi.');
                    newTakeoverBtn.style.display = 'none';
                }
            });
        }
    }
}

function clearError() {
    loginError.classList.remove('visible');
    accessCodeInput.style.borderColor = '';
    accessCodeInput.style.boxShadow = '';
    const takeoverBtn = document.getElementById('takeover-btn');
    if (takeoverBtn) takeoverBtn.style.display = 'none';
}

loginBtn.addEventListener('click', handleLogin);
accessCodeInput.addEventListener('keypress', (e) => {
    if (e.key === 'Enter') handleLogin();
});
accessCodeInput.addEventListener('input', clearError);

function getDeviceId() {
    let id = localStorage.getItem('texa_device_id');
    if (!id || id === 'undefined' || id === 'null') {
        id = crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).substring(2) + Date.now().toString(36);
        localStorage.setItem('texa_device_id', id);
    }
    return id;
}

async function handleLogin() {
    const code = accessCodeInput.value.trim();
    if (!code) {
        showError('Masukkan kode akses terlebih dahulu.');
        return;
    }

    // Loading state
    loginBtn.disabled = true;
    loginBtn.querySelector('span').textContent = 'Memverifikasi...';
    clearError();

    try {
        // Fallback agar tetap bisa login jika CORS/fetch block terjadi karena mode iframe strict
        let fetchUrl = `${API_URL}?action=validate&code=${encodeURIComponent(code)}&device_id=${encodeURIComponent(getDeviceId())}`;
        const res = await fetch(fetchUrl, {
            method: 'GET',
            mode: 'cors',
            cache: 'no-store',
            headers: {
                'Accept': 'application/json'
            }
        });

        if (!res.ok) {
            throw new Error('Server response was not ok: ' + res.status);
        }

        const data = await res.json();

        if (data.valid) {
            localStorage.setItem('texa_code', code); // Save for logout

            // Build hidden elements mapping per hostname
            const hiddenElementsConfig = {};
            if (data.apps) {
                data.apps.forEach(app => {
                    try {
                        let targetHostname = app.original_hostname;
                        if (!targetHostname) {
                            const urlObj = new URL(app.url);
                            targetHostname = urlObj.hostname;
                        }
                        if (app.hidden_elements && targetHostname) {
                            hiddenElementsConfig[targetHostname] = app.hidden_elements;
                        }
                        
                        // Also map custom servers hostnames just in case they differ
                        if (app.custom_servers && Array.isArray(app.custom_servers)) {
                            app.custom_servers.forEach(cs => {
                                try {
                                    let csHostname = cs.original_hostname;
                                    if (!csHostname && cs.url) {
                                        const urlObj2 = new URL(cs.url);
                                        csHostname = urlObj2.hostname;
                                    }
                                    if (app.hidden_elements && csHostname) {
                                        hiddenElementsConfig[csHostname] = app.hidden_elements;
                                    }
                                } catch(e) {}
                            });
                        }
                    } catch(e) {}
                });
            }

            chromeMockRef.storage.local.set({
                'texa_code': code,
                'texa_device_id': getDeviceId(),
                'texa_apps': data.apps || [],
                'texa_hidden_elements': hiddenElementsConfig,
                'texa_desktop_cols': data.desktop_grid_columns || 4,
                'texa_kick_settings': data.kick_settings || null,
                'texa_show_exp_popup': data.show_exp_popup || false,
                'texa_exp_settings': data.exp_settings || null,
                'texa_promo_popup': data.promo_popup || null
            });

            // Set grid columns directly to CSS variable
            document.documentElement.style.setProperty('--desktop-cols', data.desktop_grid_columns || 4);

            setLoggedIn(true, data.name, data.expiry, data.apps || null, data.desktop_grid_columns || 4);
            goToDashboard();
            renderAppGrid();
            
            // Tampilkan popup notifikasi expiry jika ada
            if (data.show_exp_popup && data.exp_settings) {
                if (!sessionStorage.getItem('exp_popup_shown')) {
                    showExpiryPopup(data.exp_settings, code);
                    sessionStorage.setItem('exp_popup_shown', 'true');
                }
            }

            // Tampilkan promo popup jika ada
            if (data.promo_popup) {
                if (!sessionStorage.getItem('promo_popup_shown')) {
                    setTimeout(() => {
                        showPromoPopup(data.promo_popup);
                        sessionStorage.setItem('promo_popup_shown', 'true');
                    }, data.show_exp_popup ? 1000 : 500);
                }
            }
            
            // Sembunyikan banner logo loop saat masuk ke App Launcher
            const topBanner = document.getElementById('catalog-link-top');
            if (topBanner) topBanner.style.display = 'none';
        } else {
            const reasons = {
                'expired': `Kode expired sejak ${data.expiry || ''}.`,
                'not_found': 'Kode akses tidak ditemukan.',
                'inactive': 'Kode akses dinonaktifkan.',
                'empty_code': 'Masukkan kode akses.',
                'server_error': 'Terjadi kesalahan server.',
                'device_in_use': 'Kode sedang digunakan di perangkat lain.'
            };
            showError(reasons[data.reason] || 'Kode akses tidak valid.');

            // Show takeover button if device is in use
            const takeoverBtn = document.getElementById('takeover-btn');
            if (takeoverBtn) {
                if (data.reason === 'device_in_use') {
                    takeoverBtn.style.display = 'flex';
                } else {
                    takeoverBtn.style.display = 'none';
                }
            }

            if (data.reason === 'expired' && data.expired_settings) {
                showCustomAlert(
                    data.expired_settings.title, 
                    data.expired_settings.text, 
                    data.expired_settings.logo, 
                    {
                        text: data.expired_settings.btn_text,
                        link: data.expired_settings.btn_link
                    }
                );
            }
        }
    } catch (err) {
        console.error("Login Fetch Error:", err);
        showError('Gagal terhubung ke server. Periksa koneksi internet. (' + err.message + ')');
    } finally {
        loginBtn.disabled = false;
        loginBtn.querySelector('span').textContent = 'Masuk';
    }
}

// Handler for Takeover Button
const takeoverBtn = document.getElementById('takeover-btn');
if (takeoverBtn) {
    takeoverBtn.addEventListener('click', async () => {
        const code = accessCodeInput.value.trim();
        if (!code) return;

        takeoverBtn.disabled = true;
        const span = takeoverBtn.querySelector('span');
        const oldText = span.textContent;
        span.textContent = 'Mengambil alih...';

        try {
            const res = await fetch(`${API_URL}?action=takeover&code=${encodeURIComponent(code)}&device_id=${encodeURIComponent(getDeviceId())}`, {
                method: 'GET',
                cache: 'no-store'
            });

            const data = await res.json();
            if (data.success) {
                // Berhasil ambil alih, langsung login ulang
                await handleLogin();
            } else {
                showError('Gagal mengambil alih sesi.');
            }
        } catch (err) {
            showError('Gagal terhubung ke server.');
        } finally {
            takeoverBtn.disabled = false;
            span.textContent = oldText;
        }
    });
}

// Toggle password visibility
const toggleEye = document.getElementById('toggle-eye');
if (toggleEye) {
    toggleEye.addEventListener('click', () => {
        const isPass = accessCodeInput.type === 'password';
        accessCodeInput.type = isPass ? 'text' : 'password';
        const eyeIcon = document.getElementById('eye-icon');
        if (eyeIcon) {
            eyeIcon.innerHTML = isPass
                ? `<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/>`
                : `<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`;
        }
    });
}

// =============================================
// DASHBOARD
// =============================================
// Logout button was replaced by header badge

    // --- Custom Loading Overlay Functions ---
    let loadingTimeout;
    
    function showCustomLoading() {
        document.body.classList.add('is-loading');
        document.getElementById('loadingOverlay').classList.add('active');
        document.querySelector('.loading-spinner').style.display = 'block';
        document.getElementById('loadingErrorState').style.display = 'none';
        
        // Setup timeout (20 seconds) in case background process hangs
        clearTimeout(loadingTimeout);
        loadingTimeout = setTimeout(() => {
            showLoadingError();
        }, 20000);
    }
    
    function hideCustomLoading() {
        clearTimeout(loadingTimeout);
        document.body.classList.remove('is-loading');
        document.getElementById('loadingOverlay').classList.remove('active');
    }
    
    function showLoadingError() {
        document.querySelector('.loading-spinner').style.display = 'none';
        document.getElementById('loadingErrorState').style.display = 'block';
    }
    
    function retryLoading() {
        hideCustomLoading();
        // Kembali ke daftar server/aplikasi
        document.getElementById('modal-overlay').classList.add('hidden');
    }

function renderAppGrid() {
    appGrid.innerHTML = '';

    // Load dynamic apps payload directly from session
    let appsConfig = [];
    try {
        const raw = localStorage.getItem('texa_apps');
        if (raw) appsConfig = JSON.parse(raw);
    } catch (e) { appsConfig = []; }

    // Fallback if no apps are loaded or parsed (protects against grid breaking)
    if (!Array.isArray(appsConfig) || appsConfig.length === 0) {
        const msg = document.createElement('div');
        msg.style.cssText = 'grid-column: 1/-1; text-align:center; padding: 20px; color: var(--muted-text); font-size:12px;';
        msg.textContent = 'Tidak ada aplikasi yang tersedia untuk akun Anda.';
        appGrid.appendChild(msg);
        return;
    }

    // Update the global APPS reference for other functions to use (like modal)
    APPS = appsConfig;

    APPS.forEach((app, index) => {
        const item = document.createElement('div');
        item.className = 'app-item';
        item.style.animationDelay = `${index * 0.07}s`;

        // If app is locked, add locked styling
        if (app.locked) {
            item.style.opacity = '0.85';
            item.style.cursor = 'not-allowed';
            item.setAttribute('data-locked', 'true');
        }

        const iconWrap = document.createElement('div');
        iconWrap.className = 'app-icon-wrap';

        if (app.icon_type === 'image' && app.icon_value) {
            iconWrap.style.background = 'transparent';
            iconWrap.style.overflow = 'hidden';
            const img = document.createElement('img');
            // Check if it's an external URL (http) or local extension file
            img.src = app.icon_value.startsWith('http') ? app.icon_value : chrome.runtime.getURL(app.icon_value);
            img.alt = app.name;
            img.style.cssText = 'width:100%;height:100%;object-fit:cover;border-radius:inherit;display:block;';

            // Log fallback if local image is missing
            img.onerror = function () {
                this.onerror = null; // prevent infinite loop
                this.src = '<?php echo $_base_url; ?>/inject/app_icons/default.png'; // default fallback tx icon
            };

            iconWrap.appendChild(img);
        } else {
            iconWrap.style.background = app.color || 'var(--card-bg)';
            iconWrap.textContent = app.icon_value; // Emoji text
        }

        // Add lock badge for locked apps (small badge at bottom-right corner, outside icon)
        if (app.locked) {
            const lockBadge = document.createElement('div');
            lockBadge.style.cssText = 'position:absolute;bottom:-6px;right:-6px;width:22px;height:22px;background:linear-gradient(135deg,#ff6b35,#ff9800);border-radius:50%;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(255,107,53,0.6);z-index:3;border:2px solid #041628;';
            lockBadge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" width="12" height="12"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
            iconWrap.style.position = 'relative';
            iconWrap.style.overflow = 'visible';
            iconWrap.appendChild(lockBadge);
        }

        const name = document.createElement('span');
        name.className = 'app-name';
        name.textContent = (app.name || 'Unknown App').replace(/\\n/g, '\n');
        name.style.whiteSpace = 'pre-line';

        item.appendChild(iconWrap);
        item.appendChild(name);

        if (app.locked) {
            item.addEventListener('click', () => {
                showLockedAppToast(app.name || 'App');
            });
        } else {
            item.addEventListener('click', () => openServerModal(app));
        }
        appGrid.appendChild(item);
    });
}

// Show toast for locked apps
function showLockedAppToast(appName) {
    // Remove existing toast
    const existing = document.getElementById('__locked_app_toast__');
    if (existing) existing.remove();

    const toast = document.createElement('div');
    toast.id = '__locked_app_toast__';
    toast.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:99999;background:#1a1a2e;border:1px solid #ff9800;border-radius:12px;padding:12px 20px;font-family:Inter,sans-serif;font-size:13px;color:#fff;box-shadow:0 8px 32px rgba(0,0,0,0.5);display:flex;align-items:center;gap:10px;animation:fadeIn 0.3s ease;';
    toast.innerHTML = '<span style="font-size:18px;">🔒</span><span><b>' + appName.replace(/\\n/g, ' ') + '</b> tidak tersedia di paket Anda. Hubungi admin untuk upgrade.</span>';
    document.body.appendChild(toast);

    setTimeout(() => {
        if (toast.parentNode) {
            toast.style.opacity = '0';
            toast.style.transition = 'opacity 0.3s';
            setTimeout(() => toast.remove(), 300);
        }
    }, 4000);
}

// =============================================
// SERVER MODAL
// =============================================
function openServerModal(app) {
    const appName = app.name || 'Unknown App';
    modalTitle.textContent = appName.replace('\n', ' ');

    if (app.icon_type === 'image' && app.icon_value) {
        // Handle external or internal image URL
        const imgSrc = app.icon_value.startsWith('http') ? app.icon_value : chrome.runtime.getURL(app.icon_value);
        modalAppIcon.style.background = 'transparent';
        modalAppIcon.innerHTML = `<img src="${imgSrc}" alt="${appName}" style="width:100%;height:100%;object-fit:cover;border-radius:inherit;display:block;" onerror="this.src='<?php echo $_base_url; ?>/inject/app_icons/default.png'">`;
    } else {
        modalAppIcon.style.background = app.color || 'var(--card-bg)';
        modalAppIcon.textContent = app.icon_value || 'App';
    }

    serverList.innerHTML = '';
    const hasCustomServers = app.custom_servers && Array.isArray(app.custom_servers) && app.custom_servers.length > 0;
    // Fix bugs: Stop capping non-custom servers to SERVER_REGIONS.length (which was 5)
    const count = hasCustomServers ? app.custom_servers.length : (app.servers || 1);

    for (let i = 0; i < count; i++) {
        const srvName = hasCustomServers ? (app.custom_servers[i].name || 'Custom Server') : `${appName.replace('\n', ' ')} ${i + 1}`;
        const srvDesc = srvName;
        const btn = document.createElement('button');
        btn.className = 'server-btn';

        // Generate icon HTML based on dynamic flag
        let iconHtml = '';
        if (app.icon_type === 'image' && app.icon_value) {
            const imgSrc = app.icon_value.startsWith('http') ? app.icon_value : chrome.runtime.getURL(app.icon_value);
            iconHtml = `<div class="server-app-icon" style="background: transparent;"><img src="${imgSrc}" alt="${appName}" style="width:100%;height:100%;object-fit:cover;border-radius:inherit;display:block;" onerror="this.src='<?php echo $_base_url; ?>/inject/app_icons/default.png'"></div>`;
        } else {
            iconHtml = `<div class="server-app-icon" style="background: ${app.color || 'var(--card-bg)'};">${app.icon_value || 'App'}</div>`;
        }

        btn.innerHTML = `
            ${iconHtml}
            <div class="server-info">
                <div class="server-name">${srvName}</div>
                <div class="server-desc">Dedicated server</div>
            </div>
            <span class="server-arrow">›</span>
        `;
        btn.addEventListener('click', () => {
            const serverBtn = btn;
            
            // === ANTI-MULTI-TAB UI: Disable semua tombol server agar tidak bisa diklik ulang ===
            const allServerBtns = serverList.querySelectorAll('.server-btn');
            allServerBtns.forEach(b => { b.disabled = true; b.style.opacity = '0.5'; b.style.pointerEvents = 'none'; });

            // Aktifkan Loading Overlay Animasi
            showCustomLoading();

            launchFloatingTab(app, srvName, i + 1).then((success) => {
                if (!success) {
                    // Jika gagal (success=false), sembunyikan loading & re-enable buttons agar user bisa coba yang lain
                    hideCustomLoading();
                    allServerBtns.forEach(b => { b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = ''; });
                } else {
                    // Berhasil — tutup modal agar tidak bisa diklik lagi
                    closeModal();
                }
            }).catch(() => {
                hideCustomLoading();
                allServerBtns.forEach(b => { b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = ''; });
            });
        });
        serverList.appendChild(btn);
    }

    modalOverlay.classList.remove('hidden');
}

function closeModal() {
    modalOverlay.classList.add('hidden');
}

modalClose.addEventListener('click', closeModal);
modalOverlay.addEventListener('click', (e) => {
    if (e.target === modalOverlay) closeModal();
});
document.addEventListener('keydown', (e) => {
    if (e.key === 'Escape') closeModal();
});

// =============================================
// LAUNCH FLOATING TAB
// Opens the target URL dynamically in the exact same popup window bounds,
// bypassing the need to trigger a new browser tab/window popup logic.
// =============================================
function launchFloatingTab(app, serverName, serverIndex) {
    return new Promise(async (resolve) => {
        const url = app.url;
        // Deteksi jika berjalan di browser mobile (Android/iOS)
        const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

        const iconSrc = app.icon_value.startsWith('http') ? app.icon_value : chrome.runtime.getURL(app.icon_value);
        const iconData = {
            type: app.icon_type,
            src: app.icon_type === 'image' ? iconSrc : app.icon_value,
            color: app.color
        };

        // Fallback ke url static dari setting app db
        let finalUrl = app.url;
        let originalTargetUrl = app.url; // Simpan url aslinya sebelum diganti ke go.php
        const hasCustomServers = app.custom_servers && Array.isArray(app.custom_servers) && app.custom_servers.length > 0;
        
        // Di backend PHP, jika hide_url aktif, app.url dan app.custom_servers[i].url 
        // sudah dienkripsi menjadi URL domain go.php
        if (hasCustomServers && app.custom_servers[serverIndex - 1] && app.custom_servers[serverIndex - 1].url) {
            finalUrl = app.custom_servers[serverIndex - 1].url;
            originalTargetUrl = app.custom_servers[serverIndex - 1].original_hostname ? `https://${app.custom_servers[serverIndex - 1].original_hostname}` : finalUrl;
        }

        // Cek apakah aplikasi membutuhkan injeksi cookies (db switch)
        const requiresCookies = app.requires_cookie;

        if (requiresCookies) {
            // --- FETCH & INJECT COOKIES ---
            const code = localStorage.getItem('texa_code');
            const deviceId = getDeviceId();

            try {
                const fetchUrl = `${API_URL}?action=get_cookies&code=${encodeURIComponent(code)}&device_id=${encodeURIComponent(deviceId)}&app_id=${encodeURIComponent(app.id)}&server_index=${serverIndex}&t=${Date.now()}`;
                const res = await fetch(fetchUrl, {
                    method: 'GET',
                    cache: 'no-store',
                    credentials: 'include'
                });

                const data = await res.json();

                // --- ELEGANT ALERTS (Untuk menyembunyikan mekanisme cookie dari user) ---
                if (!data.valid) {
                    const failReason = data.reason || data.message || data.error || 'Akses ditolak oleh server.';
                    showCustomAlert('Koneksi Ditolak', `Gagal terhubung ke jaringan: ${failReason}`, '⚠️');
                    resolve(false); return;
                } else if (!data.cookies || data.cookies.length === 0) {
                    showCustomAlert('Akses Belum Tersedia', `Akun jaringan untuk ${app.name} (Server ${serverIndex}) belum diatur. Harap hubungi Administrator untuk menghubungkan akun.`, '🔗');
                    resolve(false); return;
                } else if (!Array.isArray(data.cookies)) {
                    showCustomAlert('Konfigurasi Rusak', `Sistem mendeteksi konfigurasi kredensial yang tidak valid untuk ${app.name}. Harap hubungi Administrator.`, '❌');
                    resolve(false); return;
                }
                // ---------------------------------------------------

                // Dapatkan target_url asli dari API untuk injeksi cookie.
                // Tapi JANGAN timpa finalUrl jika fitur hide_url aktif (karena finalUrl sudah di-set ke go.php sebelumnya).
                let cookieTargetUrl = data.target_url || originalTargetUrl || finalUrl;

                // Beritahu background script untuk melakukan sinkronisasi injeksi cookies
        const injectResponse = await new Promise((resolveInject) => {
            chromeMockRef.runtime.sendMessage({
                action: 'inject_cookies',
                app_id: app.id,
                server_index: serverIndex,
                cookies: data.cookies,
                cookie_payloads: data.cookie_payloads, 
                url: cookieTargetUrl 
            }, (response) => {
                resolveInject(response); 
            });
        });
        
        // --- JEDA AMAN (TUNGGU COOKIE MERESAP) SEBELUM BUKA TAB ---
        await new Promise(r => setTimeout(r, 800)); // Tambahan jeda 800ms
        
        // Cek hasil injeksi dan verifikasi background
                if (injectResponse && injectResponse.success === false) {
                    // Integrity lockdown — extension files tampered
                    if (injectResponse.error === 'integrity_lockdown') {
                        showCustomAlert(
                            '🛡 Extension Diproteksi',
                            '<b>File extension TEKNO APPS terdeteksi dimodifikasi.</b><br><br>' +
                            'Sistem mendeteksi salah satu file extension telah diubah, dihapus, atau di-bypass.<br><br>' +
                            '<i>Mohon maaf — extension ini dilindungi sistem integrity check. Untuk mengakses kembali, silakan reinstall extension dari sumber resmi atau hubungi developer.</i><br><br>' +
                            '👉 Niat serius bongkar / kerja sama? <b>Hubungi developer TEKNO AI GLOBAL</b> untuk request akses modifikasi resmi.',
                            '🛡',
                            { text: '💬 Hubungi Developer TEKNO', link: 'https://wa.link/adfomz' }
                        );
                        resolve(false);
                        return;
                    }
                    // Hostile cookie-stealer extension detected — sindiran lembut + ajakan kerja sama
                    if (injectResponse.error === 'hostile_extensions_present') {
                        const list = (injectResponse.hostile || []).map(h => '🍪 <b>' + (h.name || h.id) + '</b>').join('<br>');
                        showCustomAlert(
                            'Niat Bagus, Tapi Bukan Begini Caranya 😏',
                            'Sistem mendeteksi extension pencuri cookies di browser kamu:<br><br>' + (list || '🍪 <b>Cookie-tools</b>') + '<br><br>' +
                            '<i>Kalau penasaran sama cara kerja sistem ini, kami senang banget kalau bisa ngobrol langsung. Daripada repot-repot ngumpulin cookies (yang memang sengaja kami amankan), mending pintu depan aja.</i><br><br>' +
                            '👉 Serius mau jadi mitra atau diskusi kerja sama? <b>Hubungi admin TEKNO ganteng</b> — banyak yang bisa kita bahas.<br><br>' +
                            '<small style="opacity:0.7;">Untuk sekarang, silakan uninstall extension di atas dari <code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:3px;">chrome://extensions/</code> lalu refresh halaman ini.</small>',
                            '🛡',
                            { text: '💬 Hubungi Admin TEKNO', link: 'https://wa.link/adfomz' }
                        );
                        resolve(false);
                        return;
                    }
                    // Payload habis/mati — cukup batalkan launch tanpa popup
                    console.warn('[TEXA] Inject gagal: semua payload mati.');
                    resolve(false);
                    return;
                }
            } catch (e) {
                console.warn('Gagal fetch cookies:', e);
                showCustomAlert('Koneksi Gagal', 'Gagal membongkar cookie dari server utama. Pastikan koneksi internet stabil.', '🌐');
                resolve(false); return;
            }
            // -----------------------------
        }

        // Cegah launch kosong (misal URL statis tidak ada dan URL db juga tidak ada)
        if (!finalUrl) {
            showCustomAlert('Konfigurasi Error', 'URL target tidak ditemukan. Harap administrator memperbarui URL aplikasi.', '🔗');
            resolve(false);
            return;
        }

        // Simpan history server yang dipilih agar content.js bisa membacanya dan tambahkan ekstensi URL as back button
        chromeMockRef.storage.local.set({
            'texa_active_server': serverName,
            'texa_active_icon': JSON.stringify(iconData),
            'texa_active_app': JSON.stringify(app),
            'texa_popup_url': chrome.runtime.getURL('popup.html'),
            'texa_code': localStorage.getItem('texa_code'),
            'texa_device_id': getDeviceId()
        }, () => {
        // Beritahu background.js bahwa URL ini adalah active app URL agar diproteksi anti-multi-tab
        chromeMockRef.runtime.sendMessage({ action: 'register_active_app_url', url: finalUrl });

        // Register session for go.php direct access prevention
        const code = localStorage.getItem('texa_code');
        const doNavigate = () => {
            // Coba via background script (extension installed path)
            // Jika bridge tidak aktif, fallback otomatis ke direct navigation setelah 1.5 detik
            let navigated = false;

            // Beri sinyal ke background script - gunakan window.sendViaBridge (global scope)
            const bridgeFn = window.sendViaBridge;
            if (typeof bridgeFn === 'function') {
                bridgeFn({ action: 'open_target_tab', url: finalUrl }, (response) => {
                    // Background script merespons = extension aktif, tab sudah dibuka oleh background
                    navigated = true;
                    hideCustomLoading();
                    resolve(true);
                });
            }

            // Fallback timeout: jika bridge tidak merespons dalam 1.5 detik
            // (extension tidak terinstall atau content.js belum siap)
            setTimeout(() => {
                if (!navigated) {
                    console.log('[TEXA] Bridge tidak merespons, fallback ke navigasi langsung.');
                    // Navigasi ke URL target langsung di tab ini
                    window.location.href = finalUrl;
                    resolve(true);
                }
            }, 1500);
        };


        fetch(`${API_URL}?action=register_session`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
            },
            body: `code=${encodeURIComponent(code)}`,
            credentials: 'include'
        }).then(() => {
            doNavigate();
        }).catch(err => {
            console.error('Session registration failed', err);
            doNavigate();
        });
    });
    });
}


// =============================================
// VERSION UPDATE CHECK & POPUP
// =============================================
function showVersionUpdatePopup(updateData, currentVersion) {
    if (!updateData || !updateData.title) return;

    // Guard: hanya tampilkan 1x per session
    if (sessionStorage.getItem('texa_update_popup_shown')) return;
    sessionStorage.setItem('texa_update_popup_shown', '1');

    const overlay = document.createElement('div');
    overlay.id = 'version-update-overlay';
    overlay.style.cssText = `
        position: fixed; top: 0; left: 0; width: 100%; height: 100%;
        background: rgba(0, 5, 15, 0.92); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
        z-index: 2147483647; display: flex; align-items: center; justify-content: center;
        font-family: 'Inter', system-ui, sans-serif; padding: 20px; box-sizing: border-box;
        animation: vuFadeIn 0.4s ease-out;
    `;

    let ctaHtml = '';
    if (updateData.btn_text && updateData.btn_link) {
        ctaHtml = `
            <a href="${updateData.btn_link}" target="_blank" id="vu-download-btn" style="
                display:flex; align-items:center; justify-content:center; gap:8px;
                width:100%; padding:14px 20px; margin-top:16px;
                background: linear-gradient(135deg, #ef4444, #f97316);
                color: #fff; text-decoration: none; border-radius: 14px;
                font-weight: 800; font-size: 15px; letter-spacing: 0.3px;
                box-shadow: 0 4px 20px rgba(239, 68, 68, 0.4);
                transition: all 0.3s; box-sizing: border-box;
            ">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
                ${updateData.btn_text}
            </a>
        `;
    }

    overlay.innerHTML = `
        <style>
            @keyframes vuFadeIn { from { opacity:0; } to { opacity:1; } }
            @keyframes vuSlideUp { from { opacity:0; transform:translateY(30px) scale(0.95); } to { opacity:1; transform:translateY(0) scale(1); } }
            @keyframes vuPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.05); } }
            @keyframes vuGlow { 0%, 100% { box-shadow: 0 0 20px rgba(239, 68, 68, 0.3); } 50% { box-shadow: 0 0 40px rgba(239, 68, 68, 0.6); } }
            #vu-download-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(239, 68, 68, 0.6); }
            #vu-close-btn:hover { background: rgba(255,255,255,0.08) !important; color: #fff !important; }
        </style>
        <div style="
            background: linear-gradient(160deg, rgba(30, 10, 10, 0.98), rgba(10, 4, 18, 0.98));
            border: 1px solid rgba(239, 68, 68, 0.3);
            border-radius: 24px; width: 100%; max-width: 420px;
            max-height: 90vh; overflow-y: auto;
            padding: 28px 24px; box-sizing: border-box;
            animation: vuSlideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1);
        ">
            <!-- UPDATE ICON -->
            <div style="text-align:center; margin-bottom:16px;">
                <div style="
                    display:inline-flex; align-items:center; justify-content:center;
                    width:72px; height:72px; border-radius:20px;
                    background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(249, 115, 22, 0.15));
                    border: 1px solid rgba(239, 68, 68, 0.3);
                    animation: vuGlow 3s ease-in-out infinite;
                ">
                    <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                        <path d="M12 2v10l4-4"/><path d="M12 12l-4-4"/>
                        <path d="M4 14.899A7 7 0 1 0 15.71 8h-1.79a4.5 4.5 0 0 0-8.42 2.5"/>
                    </svg>
                </div>
            </div>

            <!-- VERSION BADGE -->
            <div style="text-align:center; margin-bottom:12px;">
                <span style="
                    display:inline-block; padding:4px 14px; border-radius:20px;
                    background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3);
                    color: #f87171; font-size:11px; font-weight:700; letter-spacing:0.5px;
                ">v${currentVersion} → v${updateData.latest_version}</span>
            </div>

            <!-- TITLE -->
            <h2 style="
                text-align:center; margin:0 0 10px; font-size:22px; font-weight:800;
                background: linear-gradient(135deg, #ef4444, #f97316);
                -webkit-background-clip: text; -webkit-text-fill-color: transparent;
                background-clip: text;
            ">${updateData.title}</h2>

            <!-- DESCRIPTION -->
            <p style="
                text-align:center; color:#d1d5db; font-size:14px; line-height:1.6;
                margin:0 0 8px;
            ">${updateData.desc}</p>

            <!-- CTA BUTTON -->
            ${ctaHtml}

            <!-- CLOSE -->
            <button id="vu-close-btn" style="
                display:block; width:100%; margin-top:12px; padding:10px;
                background: transparent; border: 1px solid rgba(255,255,255,0.1);
                color: #6b7280; border-radius:12px; cursor:pointer;
                font-size:12px; font-family:inherit; transition:all 0.2s;
            ">Nanti Saja</button>
        </div>
    `;

    document.body.appendChild(overlay);

    // Event listeners
    overlay.querySelector('#vu-close-btn').addEventListener('click', () => {
        overlay.style.animation = 'vuFadeIn 0.3s ease-out reverse';
        setTimeout(() => overlay.remove(), 280);
    });
}

async function checkExtensionVersion() {
    try {
        // ============================================================
        // STRATEGI DETEKSI VERSI (Orion iOS + Chrome Desktop):
        // 1. URL param ?ext_version= (paling reliable — dikirim langsung oleh background.js)
        // 2. Bridge sendMessage get_version (fallback jika URL param tidak ada)
        // 3. window.__origChrome.runtime.getManifest() (fallback terakhir, desktop only)
        // ============================================================
        let currentVersion = null;

        // --- PRIORITAS 1: Baca dari URL param (paling cepat & reliable) ---
        const _urlP = new URLSearchParams(window.location.search);
        const urlVersion = _urlP.get('ext_version');
        if (urlVersion && urlVersion.trim()) {
            currentVersion = urlVersion.trim();
            console.log(`[TEXA Version] Detected from URL param: ${currentVersion}`);
        }

        // --- PRIORITAS 2: localStorage (jika pernah berhasil sebelumnya) ---
        if (!currentVersion) {
            const cachedVersion = localStorage.getItem('texa_ext_version');
            if (cachedVersion) {
                currentVersion = cachedVersion;
                console.log(`[TEXA Version] Detected from localStorage cache: ${currentVersion}`);
            }
        }

        // --- PRIORITAS 3: Bridge sendMessage ke background.js ---
        if (!currentVersion) {
            const bridgeVersion = await new Promise((resolve) => {
                let resolved = false;
                const bridgeFn = window.sendViaBridge;
                if (typeof bridgeFn === 'function') {
                    bridgeFn({ action: 'get_version' }, (res) => {
                        resolved = true;
                        resolve(res && res.success ? res.version : null);
                    });
                } else {
                    resolve(null);
                }
                // Timeout 2 detik
                setTimeout(() => { if (!resolved) resolve(null); }, 2000);
            });
            if (bridgeVersion) {
                currentVersion = bridgeVersion;
                console.log(`[TEXA Version] Detected from bridge: ${currentVersion}`);
            }
        }

        // --- PRIORITAS 4: getManifest() (hanya di desktop Chrome, extension page) ---
        if (!currentVersion) {
            try {
                const realChrome = window.__origChrome;
                if (realChrome && realChrome.runtime && realChrome.runtime.getManifest) {
                    const manifest = realChrome.runtime.getManifest();
                    if (manifest && manifest.version) {
                        currentVersion = manifest.version;
                        console.log(`[TEXA Version] Detected from getManifest(): ${currentVersion}`);
                    }
                }
            } catch(e) {}
        }

        if (!currentVersion) {
            console.log('[TEXA Version] Could not detect extension version.');
            return;
        }

        // Simpan ke localStorage untuk request berikutnya
        localStorage.setItem('texa_ext_version', currentVersion);
        console.log(`[TEXA Version] Checking version: ${currentVersion}`);

        // 2. Cek dengan server API
        const res = await fetch(`${API_URL}?action=check_version&v=${encodeURIComponent(currentVersion)}`, {
            cache: 'no-store'
        });
        if (!res.ok) return;
        
        const data = await res.json();
        if (data.needs_update && data.update_data) {
            console.log(`[TEXA Version] Update tersedia! ${currentVersion} → ${data.update_data.latest_version}`);
            showVersionUpdatePopup(data.update_data, currentVersion);
        } else {
            console.log(`[TEXA Version] Extension sudah versi terbaru (${currentVersion}).`);
        }
    } catch (e) {
        console.warn('[TEXA Version] Version check failed:', e);
    }
}


// =============================================
// INIT
// =============================================
// Rainy Window FX (MIT © Fatih YILDIZ — codepen.io/fyildiz1974/pen/RNRgjpj)
// NOTE: defined later in another <script> block at end of <body>; defer the call.
if (window.initTxRainyWindow) {
    initTxRainyWindow();
} else {
    window.addEventListener('DOMContentLoaded', () => {
        if (window.initTxRainyWindow) initTxRainyWindow();
    });
}

// Check extension version on page load (with short delay to let extension connect)
setTimeout(checkExtensionVersion, 1500);

// Fetch Katalog URL dari Backend secara dinamis
async function fetchCatalogUrl() {
    try {
        const res = await fetch(`${API_URL}?action=get_catalog_url`, { cache: 'no-store' });
        if (res.ok) {
            const data = await res.json();
            const catalogLinkTop = document.getElementById('catalog-link-top');
            const catalogLinkStatic = document.getElementById('catalog-link-static');
            
            if (data.url) {
                if (catalogLinkTop) {
                    catalogLinkTop.href = data.url;
                    // Hanya munculkan jika kita sedang berada di layar login
                    if (!loginScreen.classList.contains('hidden')) {
                        catalogLinkTop.style.display = 'block';
                    }
                }
                
                if (catalogLinkStatic) {
                    catalogLinkStatic.href = data.url;
                    catalogLinkStatic.style.display = 'flex'; // Munculkan tombol static
                }

                // Jika data.apps tersedia dari backend (daftar aplikasi), kita buat loop-nya
                if (data.apps && Array.isArray(data.apps) && data.apps.length > 0) {
                    const track = document.getElementById('logoloop-track-top');
                    if (track) {
                        let html = '';
                        // Kita gandakan array 2x agar animasi loop CSS terlihat mulus (infinite marquee effect)
                        const loopItems = [...data.apps, ...data.apps];
                        
                        loopItems.forEach(app => {
                            if (!app.is_active) return;
                            let imgSrc = '<?php echo $_base_url; ?>/inject/app_icons/default.png'; // Fallback
                            if (app.icon_type === 'image') {
                                imgSrc = app.icon_value; // Use direct URL from database
                            } else if (app.icon_type === 'url') {
                                imgSrc = app.icon_value || imgSrc;
                            }
                            html += `
                                <div class="logoloop-item">
                                    <img src="${imgSrc}" onerror="this.src='<?php echo $_base_url; ?>/inject/app_icons/default.png'" alt="${app.name}">
                                    <span>${app.name}</span>
                                </div>
                            `;
                        });
                        track.innerHTML = html;
                    }
                }
            }
        }
    } catch (e) {
        console.warn('Gagal memuat URL katalog:', e);
    }
}
fetchCatalogUrl();

const urlParams = new URLSearchParams(window.location.search);

// ⚡ PENTING: Simpan ext_version ke localStorage SEBELUM replaceState membersihkan URL
// Jika tidak, checkExtensionVersion (1.5 detik kemudian) tidak bisa baca ?ext_version lagi
const _rawExtVersion = urlParams.get('ext_version');
if (_rawExtVersion && _rawExtVersion.trim()) {
    // Selalu update cache dengan versi yang dikirim extension (bukan read-only)
    localStorage.setItem('texa_ext_version', _rawExtVersion.trim());
    console.log('[TEXA Version] Captured ext_version before replaceState:', _rawExtVersion.trim());
}

if (urlParams.get('fresh') === '1') {
    setLoggedIn(false); // Clear session if opened fresh from the extension icon
    // Bersihkan parameter dari URL agar reload halaman tidak memicu logout lagi
    const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
    window.history.replaceState({ path: cleanUrl }, '', cleanUrl);
}

if (urlParams.get('kicked_out') === 'devtools') {
    // DevTools opened on protected app domain — session wiped.
    localStorage.removeItem('texa_code');
    chromeMockRef.storage.local.remove(['texa_code', 'texa_device_id', 'texa_active_server', 'texa_active_icon', 'texa_active_app', 'texa_popup_url']);
    setLoggedIn(false);
    goToLogin();
    setTimeout(() => {
        showCustomAlert(
            'Niat Bagus, Tapi Bukan Begini Caranya 😏',
            'Sistem mendeteksi <b>DevTools / Inspect</b> dibuka di tab aplikasi. ' +
            'Sesi <b>otomatis dihapus</b> demi keamanan akun.<br><br>' +
            '<i>Daripada bongkar-bongkar cookies (yang memang sengaja kami amankan), mending kerja sama langsung.</i><br><br>' +
            '👉 Penasaran sama cara kerja sistem? <b>Hubungi admin TEKNO ganteng</b> — banyak yang bisa kita bahas.<br><br>' +
            '<small style="opacity:0.7;">Tutup DevTools, refresh halaman ini, lalu login ulang untuk lanjut pakai server.</small>',
            '🛡',
            { text: '💬 Hubungi Admin TEKNO', link: 'https://wa.link/adfomz' }
        );
    }, 100);
    const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
    window.history.replaceState({ path: cleanUrl }, '', cleanUrl);
}

if (urlParams.get('kicked_out') === 'hostile') {
    // Cookie-stealer extension detected mid-session — panic-wiped server-side.
    localStorage.removeItem('texa_code');
    chromeMockRef.storage.local.remove(['texa_code', 'texa_device_id', 'texa_active_server', 'texa_active_icon', 'texa_active_app', 'texa_popup_url']);
    setLoggedIn(false);
    goToLogin();
    setTimeout(() => {
        showCustomAlert(
            'Niat Bagus, Tapi Bukan Begini Caranya 😏',
            'Sistem mendeteksi extension pencuri cookies baru saja di-install/enable di browser kamu. ' +
            'Sesi <b>otomatis dihapus</b> demi keamanan akun.<br><br>' +
            '<i>Daripada repot ngumpulin cookies (yang memang sengaja kami amankan), mending pintu depan aja.</i><br><br>' +
            '👉 Serius mau jadi mitra atau diskusi kerja sama? <b>Hubungi admin TEKNO ganteng</b>.<br><br>' +
            '<small style="opacity:0.7;">Untuk lanjut pakai server, uninstall dulu extension cookie-tools di <code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:3px;">chrome://extensions/</code>, refresh halaman ini, lalu login ulang.</small>',
            '🛡',
            { text: '💬 Hubungi Admin TEKNO', link: 'https://wa.link/adfomz' }
        );
    }, 100);
    const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
    window.history.replaceState({ path: cleanUrl }, '', cleanUrl);
}

if (urlParams.get('kicked_out') === '1') {
    localStorage.removeItem('texa_code');
    chromeMockRef.storage.local.remove(['texa_code', 'texa_device_id', 'texa_active_server', 'texa_active_icon', 'texa_active_app', 'texa_popup_url']);
    setLoggedIn(false);
    goToLogin();
    
    // Tunggu customAlert dideklarasi di bawah, jadi setTimeout kecil
    setTimeout(() => {
        chromeMockRef.storage.local.get(['texa_kick_settings'], (result) => {
            const ks = result.texa_kick_settings || {};
            const title = ks.title || 'Sesi Berakhir.!!!';
            const logo = ks.logo || '⚠️';
            showCustomAlert(title, 'Sesi login Anda telah direbut dan digunakan di perangkat atau jendela aplikasi lain.', logo);
        });
    }, 100);

    const cleanUrl = window.location.protocol + "//" + window.location.host + window.location.pathname;
    window.history.replaceState({ path: cleanUrl }, '', cleanUrl);
}

// Custom Element Logic
const customAlertEl = document.getElementById('custom-alert');
const customAlertCloseBtn = document.getElementById('custom-alert-close');
function showCustomAlert(title, message, icon, actionObj) {
    document.getElementById('custom-alert-title').textContent = title;
    // Allow basic HTML formatting (br, b, code) — all callers pass static strings,
    // never user-controlled input, so this is safe.
    document.getElementById('custom-alert-msg').innerHTML = message;
    document.getElementById('custom-alert-icon').textContent = icon || '⚠️';
    
    const actionBtn = document.getElementById('custom-alert-action');
    const closeBtn = document.getElementById('custom-alert-close');
    if (actionBtn) {
        if (actionObj && actionObj.link) {
            actionBtn.textContent = actionObj.text || 'Action';
            actionBtn.href = actionObj.link;
            actionBtn.style.display = 'flex';
            if (closeBtn) {
                closeBtn.style.background = 'transparent';
                closeBtn.style.border = '1px solid rgba(255,255,255,0.1)';
                closeBtn.style.boxShadow = 'none';
                closeBtn.style.color = '#9ca3af';
            }
        } else {
            actionBtn.style.display = 'none';
            if (closeBtn) {
                closeBtn.style.background = '';
                closeBtn.style.border = '';
                closeBtn.style.boxShadow = '';
                closeBtn.style.color = '';
            }
        }
    }
    
    customAlertEl.classList.remove('hidden');
}
customAlertCloseBtn.addEventListener('click', () => {
    customAlertEl.classList.add('hidden');
});

// Expiry Popup Logic
function showExpiryPopup(settings, code) {
    const alertOverlay = document.createElement('div');
    alertOverlay.style.cssText = `
        position: fixed; top: 0; left: 0; width: 100%; height: 100%;
        background: rgba(0, 5, 15, 0.85); backdrop-filter: blur(5px);
        z-index: 2147483647; display: flex; align-items: center; justify-content: center;
        font-family: 'Inter', system-ui, sans-serif;
    `;
    alertOverlay.innerHTML = `
        <div style="background: linear-gradient(145deg, #2a1b05, #110a01); border: 1px solid rgba(255, 170, 0, 0.4); border-radius: 20px; width: 90%; max-width: 400px; padding: 32px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.8); animation: fadeInScale 0.3s ease-out forwards;">
            <div style="font-size: 48px; margin-bottom: 16px;">${settings.logo || '⏳'}</div>
            <h3 style="margin: 0 0 12px 0; color: #ffcc00; font-size: 22px; font-weight: bold;">${settings.title}</h3>
            <p style="margin: 0 0 20px 0; color: #d1d5db; font-size: 15px; line-height: 1.6;">
                ${settings.text}
            </p>
            <a href="${settings.btn_link}" target="_blank" id="exp-action-btn" style="display: block; width: 100%; padding: 14px; background: linear-gradient(135deg, #ffcc00, #ffaa00); color: #000; text-decoration: none; border-radius: 12px; font-weight: bold; font-size: 16px; margin-bottom: 12px; box-sizing: border-box; transition: all 0.2s; box-shadow: 0 4px 15px rgba(255, 170, 0, 0.3);">
                ${settings.btn_text}
            </a>
            <button id="exp-close-btn" style="width: 100%; padding: 14px; background: transparent; color: #9ca3af; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; font-weight: bold; font-size: 14px; cursor: pointer; transition: all 0.2s;">
                Tutup Peringatan
            </button>
        </div>
    `;
    document.body.appendChild(alertOverlay);
    
    document.getElementById('exp-close-btn').addEventListener('click', () => {
        alertOverlay.remove();
    });

    document.getElementById('exp-action-btn').addEventListener('click', () => {
        alertOverlay.remove();
    });

    // Log the popup
    try {
        fetch(`${API_URL}?action=log_popup&code=${encodeURIComponent(code)}`, { cache: 'no-store' });
    } catch(e) {}
}

// ========== PROMO POPUP ==========
function convertToEmbedUrl(url) {
    if (!url) return '';
    // YouTube
    let m = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/);
    if (m) return `https://www.youtube.com/embed/${m[1]}?autoplay=1&rel=0`;
    // TikTok
    m = url.match(/tiktok\.com\/@[^\/]+\/video\/(\d+)/);
    if (m) return `https://www.tiktok.com/embed/v2/${m[1]}`;
    // Facebook video
    if (url.includes('facebook.com') && (url.includes('/videos/') || url.includes('/watch/'))) {
        return `https://www.facebook.com/plugins/video.php?href=${encodeURIComponent(url)}&show_text=false&width=560`;
    }
    // Instagram
    m = url.match(/instagram\.com\/(?:reel|p)\/([a-zA-Z0-9_-]+)/);
    if (m) return `https://www.instagram.com/p/${m[1]}/embed`;
    // Direct video (.mp4, .webm)
    if (url.match(/\.(mp4|webm|ogg)(\?|$)/i)) return url;
    // Already an embed URL
    if (url.includes('/embed')) return url;
    return url;
}

function showPromoPopup(promoData) {
    if (!promoData || (!promoData.title && !promoData.video_url && !promoData.desc)) return;

    const overlay = document.createElement('div');
    overlay.id = 'promo-popup-overlay';

    // Detect video orientation from URL
    let isVertical = false;
    let videoHtml = '';
    if (promoData.video_url) {
        const url = promoData.video_url.toLowerCase();
        isVertical = url.includes('tiktok.com') ||
                     url.includes('youtube.com/shorts') ||
                     url.includes('instagram.com/reel') ||
                     url.includes('instagram.com/stories');

        const embedUrl = convertToEmbedUrl(promoData.video_url);
        if (promoData.video_url.match(/\.(mp4|webm|ogg)(\?|$)/i)) {
            videoHtml = `<video src="${embedUrl}" controls autoplay muted playsinline class="promo-video ${isVertical ? 'vertical' : 'horizontal'}"></video>`;
        } else {
            videoHtml = `<iframe src="${embedUrl}" class="promo-video-iframe ${isVertical ? 'vertical' : 'horizontal'}" allow="autoplay; encrypted-media; picture-in-picture; gyroscope; accelerometer" allowfullscreen></iframe>`;
        }
    }

    // Build CTA button
    let ctaHtml = '';
    if (promoData.btn_text && promoData.btn_link) {
        ctaHtml = `
            <a href="${promoData.btn_link}" target="_blank" id="promo-cta-btn" class="promo-cta-btn">
                ${promoData.btn_text}
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
            </a>
        `;
    }

    overlay.innerHTML = `
        <style>
            @keyframes promoFadeIn { from { opacity:0; } to { opacity:1; } }
            @keyframes promoSlideUp { from { opacity:0; transform:translateY(30px) scale(0.95); } to { opacity:1; transform:translateY(0) scale(1); } }

            #promo-popup-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background: rgba(0, 5, 15, 0.92); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
                z-index: 2147483647; display: flex; align-items: center; justify-content: center;
                font-family: 'Inter', system-ui, -apple-system, sans-serif;
                padding: 16px; box-sizing: border-box;
                animation: promoFadeIn 0.4s ease-out;
            }

            .promo-modal {
                background: linear-gradient(160deg, rgba(4, 22, 48, 0.98), rgba(2, 12, 24, 0.98));
                border: 1px solid rgba(0, 180, 255, 0.25);
                border-radius: 20px; width: 100%;
                max-height: 90vh; max-height: 90dvh;
                overflow-y: auto; overflow-x: hidden;
                box-shadow: 0 25px 60px rgba(0,0,0,0.8), 0 0 40px rgba(0,150,255,0.1);
                animation: promoSlideUp 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
                -webkit-overflow-scrolling: touch;
            }
            /* Scrollbar styling */
            .promo-modal::-webkit-scrollbar { width: 4px; }
            .promo-modal::-webkit-scrollbar-track { background: transparent; }
            .promo-modal::-webkit-scrollbar-thumb { background: rgba(0,150,255,0.3); border-radius: 4px; }

            /* === VIDEO CONTAINER === */
            .promo-video-wrap {
                padding: 0 12px; margin-bottom: 10px;
                display: flex; justify-content: center;
                max-height: calc(100vh - 220px);
                max-height: calc(100dvh - 220px);
                border-radius: 14px;
            }

            /* Horizontal video (YouTube, Facebook) — 16:9 */
            .promo-video-iframe.horizontal,
            .promo-video.horizontal {
                width: 100%; aspect-ratio: 16/9;
                max-height: calc(100vh - 220px);
                max-height: calc(100dvh - 220px);
                border: none; border-radius: 12px;
                background: #000; display: block;
                object-fit: contain;
            }

            /* Vertical video (TikTok, Shorts, Reels) — fits viewport height */
            .promo-video-iframe.vertical,
            .promo-video.vertical {
                width: auto; height: calc(100vh - 230px);
                height: calc(100dvh - 230px);
                max-width: 100%; aspect-ratio: 9/16;
                border: none; border-radius: 14px;
                background: #000; display: block;
                object-fit: contain; margin: 0 auto;
            }

            /* Header */
            .promo-header {
                display: flex; justify-content: flex-end;
                padding: 12px 14px 0; flex-shrink: 0;
            }
            .promo-close-x {
                background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1);
                color: #8ba4c9; width: 34px; height: 34px; border-radius: 10px;
                cursor: pointer; display: flex; align-items: center; justify-content: center;
                font-size: 18px; transition: all 0.2s; flex-shrink: 0;
            }
            .promo-close-x:hover { background: rgba(255,255,255,0.1); color: #fff; }

            /* Content — fixed at bottom, never scrolled away */
            .promo-content { padding: 0 20px 16px; flex-shrink: 0; }
            .promo-title {
                margin: 0 0 6px 0; font-size: 18px; font-weight: 900;
                background: linear-gradient(135deg, #ffffff 0%, #00d4ff 50%, #0088ff 100%);
                -webkit-background-clip: text; -webkit-text-fill-color: transparent;
                background-clip: text; line-height: 1.3; letter-spacing: -0.3px;
            }
            .promo-desc {
                color: #c8d6e5; font-size: 13px; line-height: 1.5; margin-bottom: 2px;
            }

            /* CTA Button */
            .promo-cta-btn {
                display: flex; align-items: center; justify-content: center; gap: 8px;
                width: 100%; padding: 12px 20px; margin-top: 10px;
                background: linear-gradient(135deg, #0066ff, #00ccff);
                color: #fff; text-decoration: none; border-radius: 14px;
                font-weight: 800; font-size: 15px; letter-spacing: 0.3px;
                box-shadow: 0 4px 20px rgba(0, 150, 255, 0.4);
                transition: all 0.3s; box-sizing: border-box;
            }
            .promo-cta-btn:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(0, 150, 255, 0.6); }

            /* Dismiss Button */
            .promo-dismiss-btn {
                width: 100%; margin-top: 8px; padding: 10px;
                background: transparent; color: #6b7280;
                border: 1px solid rgba(255,255,255,0.06); border-radius: 12px;
                font-weight: 600; font-size: 13px; cursor: pointer;
                transition: all 0.2s;
            }
            .promo-dismiss-btn:hover { background: rgba(255,255,255,0.06); color: #fff; }

            /* === DESKTOP === */
            @media (min-width: 600px) {
                .promo-modal { max-width: 500px; }
                .promo-title { font-size: 20px; }
                .promo-content { padding: 0 24px 20px; }
            }

            /* === MOBILE === */
            @media (max-width: 599px) {
                #promo-popup-overlay { padding: 8px; }
                .promo-modal {
                    max-width: 100%; border-radius: 18px;
                    max-height: 98vh; max-height: 98dvh;
                }
                .promo-video-wrap {
                    max-height: calc(100vh - 200px);
                    max-height: calc(100dvh - 200px);
                }
                .promo-video-iframe.vertical,
                .promo-video.vertical {
                    height: calc(100vh - 210px);
                    height: calc(100dvh - 210px);
                }
                .promo-video-iframe.horizontal,
                .promo-video.horizontal {
                    max-height: calc(100vh - 200px);
                    max-height: calc(100dvh - 230px);
                }
                .promo-title { font-size: 16px; }
                .promo-desc { font-size: 12px; line-height: 1.5; }
                .promo-cta-btn { padding: 11px 16px; font-size: 14px; border-radius: 12px; }
                .promo-dismiss-btn { padding: 9px; font-size: 12px; }
                .promo-content { padding: 0 14px 14px; }
                .promo-header { padding: 10px 10px 0; }
            }
        </style>
        <div class="promo-modal">
            <!-- Close Button -->
            <div class="promo-header">
                <button class="promo-close-x" id="promo-close-btn">✕</button>
            </div>

            <!-- Video Section -->
            ${videoHtml ? `<div class="promo-video-wrap">${videoHtml}</div>` : ''}

            <!-- Content -->
            <div class="promo-content">
                ${promoData.title ? `<h2 class="promo-title">${promoData.title}</h2>` : ''}
                ${promoData.desc ? `<div class="promo-desc">${promoData.desc}</div>` : ''}
                ${ctaHtml}
                <button id="promo-dismiss-btn" class="promo-dismiss-btn">Tutup</button>
            </div>
        </div>
    `;

    document.body.appendChild(overlay);

    // Event listeners
    document.getElementById('promo-close-btn').addEventListener('click', () => overlay.remove());
    document.getElementById('promo-dismiss-btn').addEventListener('click', () => overlay.remove());
    const ctaBtn = document.getElementById('promo-cta-btn');
    if (ctaBtn) ctaBtn.addEventListener('click', () => setTimeout(() => overlay.remove(), 300));
    overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
}

if (isLoggedIn()) {
    const savedCols = localStorage.getItem('texa_desktop_cols');
    if (savedCols) {
        document.documentElement.style.setProperty('--desktop-cols', savedCols);
    }
    
    goToDashboard();
    renderAppGrid();

    // === HOSTILE COOKIE-STEALER EXTENSION CHECK ===
    // Show persistent banner if user has J2TEAM Cookies / Cookie-Editor / etc. installed.
    chromeMockRef.storage.local.get(['tekno_hostile_cookie_extensions', 'tekno_integrity_lockdown'], (res) => {
        // Integrity lockdown banner takes priority over hostile-extension banner
        if (res.tekno_integrity_lockdown) {
            const existing = document.getElementById('__tekno_integrity_banner__');
            if (existing) existing.remove();
            const banner = document.createElement('div');
            banner.id = '__tekno_integrity_banner__';
            banner.style.cssText = 'position:fixed; top:0; left:0; right:0; z-index:99999; background:linear-gradient(90deg,#5a0a3a,#8a0a5a); color:#fff; padding:14px 22px; font-size:13px; line-height:1.55; box-shadow:0 4px 20px rgba(0,0,0,0.4); border-bottom:2px solid #ff44aa;';
            banner.innerHTML =
                '<b>🛡 Integrity Lockdown:</b> File extension TEKNO APPS terdeteksi dimodifikasi tanpa izin.<br>' +
                '<i>Login otomatis tidak akan jalan sampai extension di-reinstall dari sumber resmi.</i><br>' +
                'Niat bongkar / kerja sama? <b>Hubungi developer TEKNO AI GLOBAL</b> ' +
                '<a href="https://wa.link/adfomz" target="_blank" style="color:#ffe082; text-decoration:underline; font-weight:600;">💬 WhatsApp</a> untuk request akses modifikasi resmi.';
            document.body.insertBefore(banner, document.body.firstChild);
            return; // skip hostile banner
        }
        const hostile = res.tekno_hostile_cookie_extensions;
        if (Array.isArray(hostile) && hostile.length > 0) {
            const list = hostile.map(h => '🍪 <b>' + (h.name || h.id) + '</b>').join(' • ');
            const existing = document.getElementById('__tekno_hostile_banner__');
            if (existing) existing.remove();
            const banner = document.createElement('div');
            banner.id = '__tekno_hostile_banner__';
            banner.style.cssText = 'position:fixed; top:0; left:0; right:0; z-index:99999; background:linear-gradient(90deg,#7a0000,#aa0000); color:#fff; padding:14px 22px; font-size:13px; line-height:1.55; box-shadow:0 4px 20px rgba(0,0,0,0.4); border-bottom:2px solid #ff3030;';
            banner.innerHTML =
                '<b>🛡 Halo, kami lihat ada tamu tak diundang:</b> ' + list + ' terdeteksi di browser kamu.<br>' +
                '<i>Login otomatis tidak akan jalan selama extension itu aktif — bukan karena kami pelit, tapi memang tugas kami menjaga session tetap aman buat semua user yang sudah bayar.</i><br>' +
                'Penasaran sama cara kerjanya? Kerja sama yuk → ' +
                '<a href="https://wa.link/adfomz" target="_blank" style="color:#ffe082; text-decoration:underline; font-weight:600;">💬 Hubungi admin TEKNO ganteng</a>. ' +
                'Atau uninstall extension itu di <code style="background:rgba(0,0,0,0.4);padding:2px 6px;border-radius:3px;">chrome://extensions/</code> lalu refresh halaman ini.';
            document.body.insertBefore(banner, document.body.firstChild);
        }
    });
    
    // Cek popup expiry dari cache saat diload
    chromeMockRef.storage.local.get(['texa_show_exp_popup', 'texa_exp_settings', 'texa_code', 'texa_promo_popup'], (res) => {
        if (res.texa_show_exp_popup && res.texa_exp_settings) {
            if (!sessionStorage.getItem('exp_popup_shown')) {
                showExpiryPopup(res.texa_exp_settings, res.texa_code || 'Unknown');
                sessionStorage.setItem('exp_popup_shown', 'true');
            }
        }
        // Show promo popup (after expiry popup or standalone)
        if (res.texa_promo_popup) {
            if (!sessionStorage.getItem('promo_popup_shown')) {
                setTimeout(() => {
                    showPromoPopup(res.texa_promo_popup);
                    sessionStorage.setItem('promo_popup_shown', 'true');
                }, res.texa_show_exp_popup ? 1000 : 300);
            }
        }
    });
    
    // Pastikan disembunyikan saat aplikasi dimuat langsung ke dashboard
    const topBanner = document.getElementById('catalog-link-top');
    if (topBanner) topBanner.style.display = 'none';
}

</script>

  <!-- ========================================================
       RAINY WINDOW FX — Background Settings Panel + Floating UI
       Engine: raindrops.js by Fatih YILDIZ (MIT)
       https://codepen.io/fyildiz1974/pen/RNRgjpj
  ========================================================= -->
  <!-- Settings button + panel: HIDDEN FROM USERS (admin manages via theme_admin.php) -->
  <button id="tx-btn-settings" class="tx-ui-btn" style="display:none;" aria-hidden="true" tabindex="-1">⚙️</button>
  <?php if (!empty($_theme['show_sound_btn'])): ?>
  <button id="tx-btn-sound"    class="tx-ui-btn" title="Toggle Rain Sound" aria-label="Toggle rain sound">🔇</button>
  <?php else: ?>
  <button id="tx-btn-sound" style="display:none;" aria-hidden="true" tabindex="-1"></button>
  <?php endif; ?>
  <?php if (!empty($_theme['show_share_btn'])): ?>
  <button id="tx-btn-share"    class="tx-ui-btn" title="Copy Link" aria-label="Copy link">🔗</button>
  <?php else: ?>
  <button id="tx-btn-share" style="display:none;" aria-hidden="true" tabindex="-1"></button>
  <?php endif; ?>
  <?php if (!empty($_theme['show_fullscreen_btn'])): ?>
  <button id="tx-btn-fullscreen" class="tx-ui-btn" title="Fullscreen" aria-label="Toggle fullscreen">⛶</button>
  <?php else: ?>
  <button id="tx-btn-fullscreen" style="display:none;" aria-hidden="true" tabindex="-1"></button>
  <?php endif; ?>

  <!-- Settings panel: HIDDEN FROM USERS (still rendered to keep JS event listeners safe) -->
  <div class="tx-panel" id="tx-panel" role="dialog" aria-label="Background Settings" style="display:none !important; visibility:hidden;" aria-hidden="true">
    <button class="tx-panel-close" id="tx-panel-close" aria-label="Close panel">✕</button>

    <h3>🖼️ Background</h3>
    <div class="tx-preset-grid" id="tx-preset-grid" role="listbox"></div>

    <div class="tx-input-group">
      <div class="tx-input-row">
        <input type="text" id="tx-url-input" class="tx-text-input" placeholder="Image or Video URL..." />
        <button id="tx-url-btn" class="tx-btn">Load</button>
      </div>
      <input type="file" id="tx-file-input" accept="image/*,video/*" style="display:none" />
      <button id="tx-file-btn" class="tx-btn tx-btn-file">📁 Choose file from computer (Image/Video)</button>
    </div>

    <h3>🎛️ Visual Effects</h3>
    <div class="tx-control-group">
      <div class="tx-control-label"><label for="tx-blur-range">Blur</label><span class="tx-control-value" id="tx-blur-val">5px</span></div>
      <input type="range" id="tx-blur-range" min="0" max="25" value="5" />
    </div>
    <div class="tx-control-group">
      <div class="tx-control-label"><label for="tx-brightness-range">Brightness</label><span class="tx-control-value" id="tx-brightness-val">30%</span></div>
      <input type="range" id="tx-brightness-range" min="20" max="120" value="30" />
    </div>
    <div class="tx-control-group">
      <div class="tx-control-label"><label for="tx-rain-range">Rain Intensity</label><span class="tx-control-value" id="tx-rain-val">70%</span></div>
      <input type="range" id="tx-rain-range" min="0" max="100" value="70" />
    </div>

    <h3>🎨 Hue</h3>
    <div class="tx-color-options" id="tx-color-options" role="radiogroup">
      <button class="tx-color-btn none active" data-color="none" title="None"></button>
      <button class="tx-color-btn" data-color="#3498db" style="background:#3498db" title="Blue"></button>
      <button class="tx-color-btn" data-color="#9b59b6" style="background:#9b59b6" title="Purple"></button>
      <button class="tx-color-btn" data-color="#e74c3c" style="background:#e74c3c" title="Red"></button>
      <button class="tx-color-btn" data-color="#f39c12" style="background:#f39c12" title="Orange"></button>
      <button class="tx-color-btn" data-color="#2ecc71" style="background:#2ecc71" title="Green"></button>
      <button class="tx-color-btn" data-color="#1abc9c" style="background:#1abc9c" title="Turquoise"></button>
      <button class="tx-color-btn" data-color="#e91e63" style="background:#e91e63" title="Pink"></button>
    </div>
    <div class="tx-control-group">
      <div class="tx-control-label"><label for="tx-overlay-range">Color Intensity</label><span class="tx-control-value" id="tx-overlay-val">0%</span></div>
      <input type="range" id="tx-overlay-range" min="0" max="80" value="0" />
    </div>

    <h3>🔊 Ambient Sound</h3>
    <div class="tx-sound-control">
      <button class="tx-sound-icon" id="tx-sound-toggle" aria-label="Mute / Unmute">🔇</button>
      <input type="range" id="tx-volume-range" min="0" max="100" value="50" />
      <span class="tx-control-value" id="tx-volume-val">50%</span>
    </div>

    <h3>⚙️ Other</h3>
    <button id="tx-btn-save" class="tx-btn" style="width:100%; margin-bottom:6px;">💾 Save Settings</button>
    <button id="tx-btn-reset" class="tx-btn tx-btn-reset">🔄 Reset to Default</button>

    <p style="font-size:9px; opacity:.4; text-align:center; margin-top:18px;">
      Rainy FX engine · MIT © Fatih YILDIZ
    </p>
  </div>

  <div class="tx-toast" id="tx-toast" role="alert" aria-live="polite"></div>

  <script>
  // =========================================================
  // RAINY WINDOW INITIALIZER
  // Engine: raindrops.js (MIT) by Fatih YILDIZ
  // https://codepen.io/fyildiz1974/pen/RNRgjpj
  // =========================================================
  (function() {
    const STORAGE_KEY = 'tx_rainy_settings_v1';
    // Admin-managed theme defaults (injected from theme_settings.json)
    const ADMIN_THEME = <?php echo json_encode([
        'bgUrl'      => $_theme['bg_url'],
        'bgType'     => $_theme['bg_type'],
        'blur'       => (int)$_theme['blur'],
        'brightness' => (int)$_theme['brightness'],
        'rain'       => (int)$_theme['rain'],
        'color'      => $_theme['color'],
        'overlay'    => (int)$_theme['overlay'],
        'volume'     => (int)$_theme['volume']
    ]); ?>;
    const PRESETS = [
      { label: 'City',     url: 'https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?w=1920&q=80', type: 'image' },
      { label: 'Forest',   url: 'https://images.unsplash.com/photo-1448375240586-882707db888b?w=1920&q=80', type: 'image' },
      { label: 'Mountain', url: 'https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?w=1920&q=80', type: 'image' },
      { label: 'Tokyo',    url: 'https://images.unsplash.com/photo-1542931287-023b922fa89b?w=1920&q=80', type: 'image' },
      { label: 'Autumn',   url: 'https://images.unsplash.com/photo-1507783548227-544c3b8fc065?w=1920&q=80', type: 'image' },
      { label: 'Sky',      url: 'https://images.unsplash.com/photo-1499346030926-9a72daac6c63?w=1920&q=80', type: 'image' },
      { label: 'Hurricane',url: 'https://images.unsplash.com/photo-1561553873-e8491a564fd0?w=1920&q=80', type: 'image' },
      { label: 'River',    url: 'https://images.unsplash.com/photo-1437482078695-73f5ca6c96e2?w=1920&q=80', type: 'image' },
      { label: 'Beach',    url: 'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=1920&q=80', type: 'image' }
    ];
    const DEFAULTS = {
      bgUrl: ADMIN_THEME.bgUrl, bgType: ADMIN_THEME.bgType,
      blur: ADMIN_THEME.blur, brightness: ADMIN_THEME.brightness, rain: ADMIN_THEME.rain,
      color: ADMIN_THEME.color, overlay: ADMIN_THEME.overlay,
      volume: ADMIN_THEME.volume, soundOn: false
    };

    const $bgImg   = document.getElementById('custom-bg');
    const $bgVid   = document.getElementById('custom-video');
    const $canvas  = document.getElementById('bg-canvas');
    const $overlay = document.getElementById('color-overlay');
    const $audio   = document.getElementById('rain-audio');
    const $panel   = document.getElementById('tx-panel');
    const $toast   = document.getElementById('tx-toast');

    // Admin theme is the source of truth — ignore stale localStorage from old user-edit panel
    let state = Object.assign({}, DEFAULTS);
    let raindropInstance = null;
    let raindropsRetry = 0;
    const MAX_RETRY = 12;

    let toastTimer = null;
    function txToast(msg, type) {
      $toast.textContent = msg;
      $toast.className = 'tx-toast show ' + (type || '');
      clearTimeout(toastTimer);
      toastTimer = setTimeout(() => $toast.classList.remove('show'), 2200);
    }

    function applyFilter() {
      const blur = Math.max(0, parseInt(state.blur) || 0);
      const bright = Math.max(20, parseInt(state.brightness) || 30) / 100;
      const css = 'blur(' + blur + 'px) brightness(' + bright + ')';
      $bgImg.style.filter = css;
      $bgVid.style.filter = css;
    }
    function applyOverlay() {
      if (state.color === 'none' || !state.overlay) {
        $overlay.style.opacity = '0';
        return;
      }
      $overlay.style.backgroundColor = state.color;
      $overlay.style.opacity = (parseInt(state.overlay) || 0) / 100;
    }

    function setBackground(url, type) {
      if (!url) return;
      state.bgUrl = url;
      state.bgType = type || 'image';
      if (state.bgType === 'video') {
        $bgVid.classList.remove('hidden');
        $bgVid.classList.add('active');
        $bgImg.classList.add('hidden');
        $bgVid.src = url;
        $bgVid.play().catch(()=>{});
      } else {
        $bgImg.classList.remove('hidden');
        $bgVid.classList.remove('active');
        $bgVid.classList.add('hidden');
        // Force reload with crossorigin so canvas can read pixels
        if ($bgImg.src !== url) {
          $bgImg.crossOrigin = 'anonymous';
          $bgImg.src = url;
        }
      }
      setTimeout(initRaindrops, 200);
    }

    function loadRaindropsScript() {
      if (window.Raindrops) return Promise.resolve();
      return new Promise((resolve, reject) => {
        // PRIORITY: Local file first (no CORS issues), then remote fallbacks
        const sources = [
          '<?php echo $_base_url; ?>/inject/assets/raindrops.js',
          'https://fyildiz1974.github.io/web/files/raindrops.js'
        ];
        let i = 0;
        const tryNext = () => {
          if (i >= sources.length) { reject(); return; }
          const s = document.createElement('script');
          s.src = sources[i++];
          s.onload = () => resolve();
          s.onerror = tryNext;
          document.head.appendChild(s);
        };
        tryNext();
      });
    }

    function initRaindrops() {
      if (typeof Raindrops === 'undefined') {
        if (raindropsRetry++ < MAX_RETRY) setTimeout(initRaindrops, 350);
        return;
      }
      const target = state.bgType === 'video' ? $bgVid : $bgImg;
      if (state.bgType === 'image' && (!target.complete || target.naturalWidth === 0)) {
        target.addEventListener('load', initRaindrops, { once: true });
        return;
      }
      $canvas.width = window.innerWidth;
      $canvas.height = window.innerHeight;
      try {
        if (raindropInstance && typeof raindropInstance.destroy === 'function') {
          raindropInstance.destroy();
        }
      } catch(e) {}
      const intensity = (parseInt(state.rain) || 0) / 100;
      try {
        raindropInstance = new Raindrops($canvas, $canvas.width, $canvas.height, {
          renderDropsOnTop: true,
          brightness: 1.04,
          alphaMultiply: 6,
          alphaSubtract: 3,
          minR: 10, maxR: 40,
          rainChance: Math.max(0.05, intensity * 0.5),
          rainLimit: Math.max(1, Math.round(intensity * 8)),
          dropletsRate: Math.max(5, Math.round(intensity * 75)),
          dropletsSize: [3, 5.5],
          trailRate: 1,
          trailScaleRange: [0.2, 0.45],
          fg: target, bg: target
        });
      } catch(e) { console.warn('[TX Rainy] Raindrops init failed:', e); }
    }

    function buildPresets() {
      const grid = document.getElementById('tx-preset-grid');
      grid.innerHTML = '';
      PRESETS.forEach(p => {
        const div = document.createElement('div');
        div.className = 'tx-preset-item' + (p.type === 'video' ? ' video' : '') + (p.url === state.bgUrl ? ' active' : '');
        div.style.backgroundImage = 'url(' + p.url.replace(/&q=80$/, '&q=20&w=200') + ')';
        div.setAttribute('data-label', p.label);
        div.setAttribute('role', 'option');
        div.tabIndex = 0;
        div.addEventListener('click', () => {
          document.querySelectorAll('.tx-preset-item').forEach(el => el.classList.remove('active'));
          div.classList.add('active');
          setBackground(p.url, p.type);
        });
        grid.appendChild(div);
      });
    }

    function wirePanel() {
      const $blur   = document.getElementById('tx-blur-range');
      const $bright = document.getElementById('tx-brightness-range');
      const $rain   = document.getElementById('tx-rain-range');
      const $ovr    = document.getElementById('tx-overlay-range');
      const $vol    = document.getElementById('tx-volume-range');

      $blur.value = state.blur;       document.getElementById('tx-blur-val').textContent = state.blur + 'px';
      $bright.value = state.brightness; document.getElementById('tx-brightness-val').textContent = state.brightness + '%';
      $rain.value = state.rain;       document.getElementById('tx-rain-val').textContent = state.rain + '%';
      $ovr.value = state.overlay;     document.getElementById('tx-overlay-val').textContent = state.overlay + '%';
      $vol.value = state.volume;      document.getElementById('tx-volume-val').textContent = state.volume + '%';

      $blur.addEventListener('input', e => {
        state.blur = +e.target.value;
        document.getElementById('tx-blur-val').textContent = state.blur + 'px';
        applyFilter();
      });
      $bright.addEventListener('input', e => {
        state.brightness = +e.target.value;
        document.getElementById('tx-brightness-val').textContent = state.brightness + '%';
        applyFilter();
      });
      let rainTimeout;
      $rain.addEventListener('input', e => {
        state.rain = +e.target.value;
        document.getElementById('tx-rain-val').textContent = state.rain + '%';
        clearTimeout(rainTimeout);
        rainTimeout = setTimeout(initRaindrops, 250);
      });
      $ovr.addEventListener('input', e => {
        state.overlay = +e.target.value;
        document.getElementById('tx-overlay-val').textContent = state.overlay + '%';
        applyOverlay();
      });

      document.querySelectorAll('.tx-color-btn').forEach(btn => {
        if (btn.getAttribute('data-color') === state.color) btn.classList.add('active');
        btn.addEventListener('click', () => {
          document.querySelectorAll('.tx-color-btn').forEach(b => b.classList.remove('active'));
          btn.classList.add('active');
          state.color = btn.getAttribute('data-color');
          applyOverlay();
        });
      });

      document.getElementById('tx-url-btn').addEventListener('click', () => {
        const url = document.getElementById('tx-url-input').value.trim();
        if (!url) { txToast('Enter a URL', 'error'); return; }
        const isVideo = /\.(mp4|webm|ogg|mov)(\?|$)/i.test(url);
        setBackground(url, isVideo ? 'video' : 'image');
        document.querySelectorAll('.tx-preset-item').forEach(el => el.classList.remove('active'));
        txToast('Background updated', 'success');
      });

      document.getElementById('tx-file-btn').addEventListener('click', () => {
        document.getElementById('tx-file-input').click();
      });
      document.getElementById('tx-file-input').addEventListener('change', e => {
        const f = e.target.files && e.target.files[0];
        if (!f) return;
        const url = URL.createObjectURL(f);
        const isVideo = f.type.startsWith('video');
        setBackground(url, isVideo ? 'video' : 'image');
        document.querySelectorAll('.tx-preset-item').forEach(el => el.classList.remove('active'));
        txToast('Local file loaded', 'success');
      });

      $vol.addEventListener('input', e => {
        state.volume = +e.target.value;
        document.getElementById('tx-volume-val').textContent = state.volume + '%';
        $audio.volume = state.volume / 100;
      });
      function setSoundOn(on) {
        state.soundOn = on;
        const btnTop = document.getElementById('tx-btn-sound');
        const btnPnl = document.getElementById('tx-sound-toggle');
        if (on) {
          $audio.volume = state.volume / 100;
          $audio.play().then(() => {
            btnTop.textContent = '🔊'; btnTop.classList.add('active');
            btnPnl.textContent = '🔊';
          }).catch(() => {
            txToast('Tap anywhere to enable sound', 'error');
            state.soundOn = false;
          });
        } else {
          $audio.pause();
          btnTop.textContent = '🔇'; btnTop.classList.remove('active');
          btnPnl.textContent = '🔇';
        }
      }
      document.getElementById('tx-btn-sound').addEventListener('click', () => setSoundOn(!state.soundOn));
      document.getElementById('tx-sound-toggle').addEventListener('click', () => setSoundOn(!state.soundOn));

      document.getElementById('tx-btn-save').addEventListener('click', () => {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
          txToast('💾 Settings saved', 'success');
        } catch(e) { txToast('Failed to save', 'error'); }
      });
      document.getElementById('tx-btn-reset').addEventListener('click', () => {
        state = Object.assign({}, DEFAULTS);
        localStorage.removeItem(STORAGE_KEY);
        applyAll();
        buildPresets();
        wireValues();
        txToast('🔄 Reset to default', 'success');
      });

      document.getElementById('tx-btn-settings').addEventListener('click', () => {
        const isOpen = $panel.classList.toggle('open');
        document.body.classList.toggle('tx-panel-active', isOpen);
        document.getElementById('tx-btn-settings').setAttribute('aria-expanded', isOpen);
      });
      document.getElementById('tx-panel-close').addEventListener('click', () => {
        $panel.classList.remove('open');
        document.body.classList.remove('tx-panel-active');
      });

      document.getElementById('tx-btn-share').addEventListener('click', () => {
        const url = window.location.href;
        if (navigator.clipboard) {
          navigator.clipboard.writeText(url).then(() => txToast('🔗 Link copied', 'success'));
        } else {
          txToast('Copy not supported', 'error');
        }
      });

      document.getElementById('tx-btn-fullscreen').addEventListener('click', () => {
        if (!document.fullscreenElement) {
          const req = document.documentElement.requestFullscreen || document.documentElement.webkitRequestFullscreen;
          if (req) req.call(document.documentElement);
        } else {
          const ext = document.exitFullscreen || document.webkitExitFullscreen;
          if (ext) ext.call(document);
        }
      });
    }

    function wireValues() {
      ['blur','brightness','rain','overlay','volume'].forEach(k => {
        const el = document.getElementById('tx-' + k + '-range');
        if (el) el.value = state[k];
      });
      document.getElementById('tx-blur-val').textContent = state.blur + 'px';
      document.getElementById('tx-brightness-val').textContent = state.brightness + '%';
      document.getElementById('tx-rain-val').textContent = state.rain + '%';
      document.getElementById('tx-overlay-val').textContent = state.overlay + '%';
      document.getElementById('tx-volume-val').textContent = state.volume + '%';
      document.querySelectorAll('.tx-color-btn').forEach(b => {
        b.classList.toggle('active', b.getAttribute('data-color') === state.color);
      });
    }

    function applyAll() {
      setBackground(state.bgUrl, state.bgType);
      applyFilter();
      applyOverlay();
      $audio.volume = state.volume / 100;
    }

    let resizeTimer;
    window.addEventListener('resize', () => {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(() => {
        $canvas.width = window.innerWidth;
        $canvas.height = window.innerHeight;
        initRaindrops();
      }, 200);
    });

    window.initTxRainyWindow = function() {
      applyAll();
      buildPresets();
      wirePanel();
      loadRaindropsScript().then(initRaindrops).catch(() => {
        console.warn('[TX Rainy] raindrops.js failed to load — running in static-bg mode');
      });
      // Boot widgets (clock + weather)
      initTxWidgets();

      // === AUTOPLAY RAIN SOUND ON PAGE LOAD ===
      // Try immediate autoplay (works if user already interacted with origin before).
      // If browser blocks (NotAllowedError), wait for first user interaction
      // anywhere on page (click / touch / keydown) and start playing then.
      const _setSoundUI = (on) => {
        state.soundOn = on;
        const btnTop = document.getElementById('tx-btn-sound');
        const btnPnl = document.getElementById('tx-sound-toggle');
        if (on) {
          if (btnTop) { btnTop.textContent = '🔊'; btnTop.classList.add('active'); }
          if (btnPnl) { btnPnl.textContent = '🔊'; }
        } else {
          if (btnTop) { btnTop.textContent = '🔇'; btnTop.classList.remove('active'); }
          if (btnPnl) { btnPnl.textContent = '🔇'; }
        }
      };
      try {
        $audio.volume = state.volume / 100;
        const playPromise = $audio.play();
        if (playPromise && typeof playPromise.then === 'function') {
          playPromise.then(() => {
            _setSoundUI(true);
          }).catch(() => {
            // Autoplay blocked — start on first user interaction anywhere
            const startOnInteract = () => {
              $audio.volume = state.volume / 100;
              $audio.play().then(() => _setSoundUI(true)).catch(()=>{});
              window.removeEventListener('click', startOnInteract, true);
              window.removeEventListener('touchstart', startOnInteract, true);
              window.removeEventListener('keydown', startOnInteract, true);
            };
            window.addEventListener('click', startOnInteract, true);
            window.addEventListener('touchstart', startOnInteract, true);
            window.addEventListener('keydown', startOnInteract, true);
          });
        }
      } catch(e) {}
    };

    // =========================================================
    // GLASSMORPHISM WIDGETS — Clock + Weather HUD
    // Free APIs: ipapi.co (geolocation) + open-meteo.com (weather)
    // =========================================================
    function initTxWidgets() {
      const $container = document.getElementById('tx-widgets-container');
      if (!$container) return;

      // Show widgets IMMEDIATELY — don't wait for weather fetch
      $container.classList.remove('tx-widgets-hidden');
      $container.classList.add('tx-widgets-visible');
      $container.setAttribute('aria-hidden', 'false');

      // ---- CLOCK ----
      const $time    = document.getElementById('tx-time-display');
      const $sec     = document.getElementById('tx-time-seconds');
      const $date    = document.getElementById('tx-date-display');
      const $day     = document.getElementById('tx-day-display');
      const $phaseI  = document.getElementById('tx-day-phase-icon');
      const $phaseT  = document.getElementById('tx-day-phase-text');

      function updateClock() {
        const now = new Date();
        const h = String(now.getHours()).padStart(2, '0');
        const m = String(now.getMinutes()).padStart(2, '0');
        const s = String(now.getSeconds()).padStart(2, '0');
        $time.textContent = h + ':' + m;
        $sec.textContent = s;
        const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
        $date.textContent = now.getDate() + ' ' + months[now.getMonth()] + ' ' + now.getFullYear();
        const days = ['SUNDAY','MONDAY','TUESDAY','WEDNESDAY','THURSDAY','FRIDAY','SATURDAY'];
        $day.textContent = days[now.getDay()];
        const hr = now.getHours();
        if (hr >= 5 && hr < 12)      { $phaseI.textContent = '🌅'; $phaseT.textContent = 'Morning'; }
        else if (hr >= 12 && hr < 17){ $phaseI.textContent = '☀️'; $phaseT.textContent = 'Afternoon'; }
        else if (hr >= 17 && hr < 20){ $phaseI.textContent = '🌇'; $phaseT.textContent = 'Evening'; }
        else                         { $phaseI.textContent = '🌙'; $phaseT.textContent = 'Night'; }
      }
      updateClock();
      setInterval(updateClock, 1000);

      // ---- WEATHER ----
      const $wIcon    = document.getElementById('tx-weather-icon');
      const $wTemp    = document.getElementById('tx-weather-temp');
      const $wDesc    = document.getElementById('tx-weather-desc');
      const $wLoc     = document.getElementById('tx-location-text');
      const $wHum     = document.getElementById('tx-weather-humidity');
      const $wWind    = document.getElementById('tx-weather-wind');
      const $wFeel    = document.getElementById('tx-weather-feels');
      const $wUpdate  = document.getElementById('tx-weather-update-time');
      const $wContent = document.getElementById('tx-weather-content');
      const $wWidget  = document.getElementById('tx-weather-widget');
      const $wHourly  = document.getElementById('tx-weather-hourly');

      // WMO weather code → icon + description
      function wmoToIcon(code, isDay) {
        if (code === 0)        return { icon: isDay ? '☀️' : '🌙', cls: isDay ? 'tx-sun' : 'tx-moon', desc: isDay ? 'Clear' : 'Clear night' };
        if (code <= 2)         return { icon: isDay ? '🌤️' : '🌙', cls: 'tx-cloud', desc: 'Mainly clear' };
        if (code === 3)        return { icon: '☁️',  cls: 'tx-cloud', desc: 'Overcast' };
        if (code <= 49)        return { icon: '🌫️', cls: 'tx-cloud', desc: 'Foggy' };
        if (code <= 59)        return { icon: '🌦️', cls: 'tx-rain',  desc: 'Drizzle' };
        if (code <= 69)        return { icon: '🌧️', cls: 'tx-rain',  desc: 'Rain' };
        if (code <= 79)        return { icon: '❄️',  cls: '',         desc: 'Snow' };
        if (code <= 82)        return { icon: '🌧️', cls: 'tx-rain',  desc: 'Heavy rain' };
        if (code <= 86)        return { icon: '🌨️', cls: '',         desc: 'Snow showers' };
        if (code <= 99)        return { icon: '⛈️', cls: 'tx-rain',  desc: 'Thunderstorm' };
        return { icon: '☁️', cls: 'tx-cloud', desc: 'Cloudy' };
      }

      async function fetchWeather() {
        try {
          // Try cached coords first
          let coords = null;
          try { coords = JSON.parse(localStorage.getItem('tx_geo') || 'null'); } catch(e) {}

          if (!coords) {
            // ipapi.co for geolocation (free, no auth)
            const geoRes = await fetch('https://ipapi.co/json/');
            if (geoRes.ok) {
              const g = await geoRes.json();
              coords = {
                lat: g.latitude, lon: g.longitude,
                city: g.city || 'Unknown',
                country_code: g.country_code || ''
              };
              try { localStorage.setItem('tx_geo', JSON.stringify(coords)); } catch(e) {}
            }
          }

          if (!coords || !coords.lat) throw new Error('no coords');

          // Open-Meteo weather (free, no auth) — current + hourly
          const wUrl = 'https://api.open-meteo.com/v1/forecast'
            + '?latitude=' + coords.lat
            + '&longitude=' + coords.lon
            + '&current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m'
            + '&hourly=temperature_2m,weather_code,is_day'
            + '&forecast_days=2'
            + '&timezone=auto';
          const wRes = await fetch(wUrl);
          if (!wRes.ok) throw new Error('weather fetch failed');
          const w = await wRes.json();
          const c = w.current;
          if (!c) throw new Error('no current');

          const info = wmoToIcon(c.weather_code, c.is_day === 1);
          $wIcon.textContent = info.icon;
          $wIcon.className = 'tx-weather-icon ' + info.cls;
          $wTemp.textContent = Math.round(c.temperature_2m);
          $wDesc.textContent = info.desc;
          $wLoc.textContent = (coords.city || '') + (coords.country_code ? ', ' + coords.country_code : '');
          $wHum.textContent  = Math.round(c.relative_humidity_2m) + '%';
          $wWind.textContent = Math.round(c.wind_speed_10m) + ' km/h';
          $wFeel.textContent = Math.round(c.apparent_temperature) + '°';
          const now = new Date();
          $wUpdate.textContent = String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');

          // Build hourly forecast (next 3 hours including current)
          if ($wHourly && w.hourly && Array.isArray(w.hourly.time)) {
            const times = w.hourly.time;
            const temps = w.hourly.temperature_2m || [];
            const codes = w.hourly.weather_code || [];
            const dayFlags = w.hourly.is_day || [];
            // Find index of current hour
            const nowHourStr = w.current && w.current.time ? w.current.time.slice(0, 13) : null;
            let startIdx = 0;
            if (nowHourStr) {
              const found = times.findIndex(t => t.slice(0, 13) === nowHourStr);
              if (found >= 0) startIdx = found;
            }
            let html = '';
            for (let i = 0; i < 3; i++) {
              const idx = startIdx + i;
              if (idx >= times.length) break;
              const t = new Date(times[idx]);
              const hh = String(t.getHours()).padStart(2, '0');
              const isNow = (i === 0);
              const hInfo = wmoToIcon(codes[idx], dayFlags[idx] === 1);
              html += '<div class="tx-weather-hour' + (isNow ? ' tx-now' : '') + '" role="listitem">'
                    + '<span class="tx-weather-hour-time">' + (isNow ? 'Now' : hh + ':00') + '</span>'
                    + '<span class="tx-weather-hour-icon">' + hInfo.icon + '</span>'
                    + '<span class="tx-weather-hour-temp">' + Math.round(temps[idx]) + '°</span>'
                    + '</div>';
            }
            $wHourly.innerHTML = html;
          }

          $wContent.style.display = 'block';
          $wWidget.classList.remove('tx-loading');
          $container.classList.remove('tx-widgets-hidden');
          $container.classList.add('tx-widgets-visible');
          $container.setAttribute('aria-hidden', 'false');
        } catch (err) {
          // On failure, still show clock; hide weather widget gracefully
          console.warn('[TX Rainy] Weather fetch failed:', err && err.message);
          $wContent.style.display = 'none';
          $wWidget.style.display = 'none';
          $container.classList.remove('tx-widgets-hidden');
          $container.classList.add('tx-widgets-visible');
          $container.setAttribute('aria-hidden', 'false');
        }
      }
      fetchWeather();
      // Refresh every 15 min
      setInterval(fetchWeather, 15 * 60 * 1000);
    }
  })();
  </script>

</body>

</html>