logger

Self-hosted WhatsApp logger us...
Log | Files | Refs | README | LICENSE

index.html (103860B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3 <head>
      4     <meta charset="UTF-8">
      5     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
      6     <title>WhatsApp Logger</title>
      7     
      8     <!-- PWA Manifest -->
      9     <link rel="manifest" href="/assets/manifest-wpChat.json">
     10     <meta name="theme-color" content="#006C4C">
     11     
     12     <!-- SVG Favicon -->
     13     <link rel="icon" href="/assets/icon.svg">
     14 
     15     <!-- Fonts & Icons -->
     16     <link rel="preconnect" href="https://fonts.googleapis.com">
     17     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
     18     <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet">
     19     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" />
     20 
     21     <!-- Firebase SDKs -->
     22     <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-app-compat.js"></script>
     23     <script src="https://www.gstatic.com/firebasejs/10.8.0/firebase-firestore-compat.js"></script>
     24 
     25     <style>
     26         :root {
     27             /* Light Theme Tokens */
     28             --md-sys-color-primary: #006C4C;
     29             --md-sys-color-on-primary: #FFFFFF;
     30             --md-sys-color-primary-container: #89F8C7;
     31             --md-sys-color-on-primary-container: #002114;
     32             
     33             --md-sys-color-secondary: #4D6357;
     34             --md-sys-color-on-secondary: #FFFFFF;
     35             --md-sys-color-secondary-container: #CFE9D9;
     36             --md-sys-color-on-secondary-container: #092016;
     37             
     38             --md-sys-color-surface: #FBFDF9;
     39             --md-sys-color-surface-dim: #D9DBD9;
     40             --md-sys-color-surface-container: #ECEEEB;
     41             --md-sys-color-surface-container-high: #E6E8E5;
     42             
     43             --md-sys-color-on-surface: #191C1A;
     44             --md-sys-color-on-surface-variant: #404944;
     45             --md-sys-color-outline: #707974;
     46             --md-sys-color-outline-variant: #BFC9C2;
     47 
     48             --md-sys-color-error: #BA1A1A;
     49             --md-sys-color-error-container: #FFDAD6;
     50             --md-sys-color-on-error-container: #410002;
     51             
     52             /* WhatsApp Specifics */
     53             --chat-bubble-me: #E2FFEC;
     54             --chat-bubble-them: #FFFFFF;
     55             --chat-bg-color: #EFE7DE;
     56             --chat-bg-image-opacity: 0.08;
     57             --search-highlight: #ffeb3b;
     58             
     59             /* Elevations & Shapes */
     60             --elevation-1: 0px 1px 3px 1px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3);
     61             --elevation-2: 0px 2px 6px 2px rgba(0,0,0,0.15), 0px 1px 2px 0px rgba(0,0,0,0.3);
     62             
     63             /* Settings */
     64             --base-font-size: 15px;
     65         }
     66 
     67         /* Dark Theme Tokens */
     68         [data-theme="dark"] {
     69             --md-sys-color-primary: #89F8C7;
     70             --md-sys-color-on-primary: #003828;
     71             --md-sys-color-primary-container: #005138;
     72             --md-sys-color-on-primary-container: #89F8C7;
     73             
     74             --md-sys-color-secondary: #BACCB3;
     75             --md-sys-color-on-secondary: #25342D;
     76             --md-sys-color-secondary-container: #3B4B43;
     77             --md-sys-color-on-secondary-container: #D6E8D8;
     78             
     79             --md-sys-color-surface: #111412;
     80             --md-sys-color-surface-dim: #111412;
     81             --md-sys-color-surface-container: #1E211F;
     82             --md-sys-color-surface-container-high: #282B29;
     83             
     84             --md-sys-color-on-surface: #E2E3DD;
     85             --md-sys-color-on-surface-variant: #C2C8C5;
     86             --md-sys-color-outline: #8C938F;
     87             --md-sys-color-outline-variant: #424945;
     88 
     89             --md-sys-color-error: #FFB4AB;
     90             --md-sys-color-error-container: #93000A;
     91             --md-sys-color-on-error-container: #FFDAD6;
     92             
     93             --chat-bubble-me: #005C4B;
     94             --chat-bubble-them: #202C33;
     95             --chat-bg-color: #0B141A;
     96             --chat-bg-image-opacity: 0.05;
     97             --search-highlight: #6b6200;
     98         }
     99 
    100         * { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
    101 
    102         body {
    103             font-family: 'Roboto', sans-serif;
    104             background-color: var(--md-sys-color-surface);
    105             color: var(--md-sys-color-on-surface);
    106             height: 100vh;
    107             overflow: hidden;
    108             display: flex;
    109             justify-content: center;
    110             transition: background-color 0.3s, color 0.3s;
    111         }
    112 
    113         /* --- ANIMATIONS --- */
    114         .hidden { display: none !important; }
    115         .fade-in { animation: fadeIn 0.4s cubic-bezier(0.2, 0.0, 0.0, 1.0); }
    116         @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
    117         .ripple-surface { position: relative; overflow: hidden; transform: translate3d(0, 0, 0); transition: transform 0.1s; }
    118         .ripple-surface:active { transform: scale(0.98); }
    119         
    120         @keyframes scaleIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
    121         @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
    122         .animate-enter { animation: scaleIn 0.3s cubic-bezier(0.2, 0.0, 0, 1.0); }
    123         .shake { animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both; }
    124         @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } }
    125 
    126         /* --- M3 STAGGERED ENTRANCE --- */
    127         @keyframes fadeInUp {
    128             from { opacity: 0; transform: translateY(30px); }
    129             to { opacity: 1; transform: translateY(0); }
    130         }
    131         .anim-up { opacity: 0; animation: fadeInUp 0.6s cubic-bezier(0.2, 0, 0, 1) forwards; }
    132         .delay-1 { animation-delay: 0.1s; }
    133         .delay-2 { animation-delay: 0.2s; }
    134         .delay-3 { animation-delay: 0.3s; }
    135         .delay-4 { animation-delay: 0.4s; }
    136         .delay-5 { animation-delay: 0.5s; }
    137 
    138         /* --- AUTH SCREEN (UPDATED to M3 Classic) --- */
    139         #auth-screen {
    140             position: fixed; inset: 0; z-index: 999;
    141             /* Static blue-green background */
    142             background: radial-gradient(circle at 0% 0%, #C1E8FB 0%, transparent 50%), 
    143                         radial-gradient(circle at 100% 100%, #89F8C7 0%, transparent 50%), 
    144                         #FBFDF9;
    145             display: flex; flex-direction: column; align-items: center; justify-content: center;
    146             padding: 24px; height: 100dvh;
    147             transition: opacity 0.3s;
    148         }
    149         
    150         /* Dark Mode Override for Auth Background */
    151         [data-theme="dark"] #auth-screen {
    152              background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%), 
    153                         radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%), 
    154                         #111412;
    155         }
    156         
    157         /* Updated M3 Auth Card (Classic Style) */
    158         .auth-card { 
    159             background: rgba(251, 253, 249, 0.7); /* Translucent */
    160             backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
    161             border-radius: 28px;
    162             padding: 32px 24px; 
    163             width: 90%; max-width: 380px;
    164             box-shadow: var(--elevation-2);
    165             display: flex; flex-direction: column; gap: 20px;
    166         }
    167         
    168         [data-theme="dark"] .auth-card {
    169             background: rgba(30, 33, 31, 0.7);
    170         }
    171 
    172         .brand-icon { 
    173             width: 64px; height: 64px; 
    174             display: flex; align-items: center; justify-content: center; 
    175             margin: 0 auto 8px;
    176         }
    177         .brand-icon img { width: 100%; height: 100%; object-fit: contain; }
    178 
    179         .auth-header { text-align: center; }
    180         .auth-header h1 { 
    181             font-family: 'Google Sans', sans-serif; 
    182             font-size: 28px; 
    183             font-weight: 400;
    184             color: var(--md-sys-color-on-surface);
    185             margin: 8px 0;
    186         }
    187         .auth-header p {
    188             font-family: 'Roboto', sans-serif;
    189             font-size: 14px; 
    190             color: var(--md-sys-color-on-surface-variant);
    191             margin: 0;
    192         }
    193 
    194         /* --- LOCK SCREEN (PIN/BIO) --- */
    195         #lock-screen {
    196             position: fixed; inset: 0; z-index: 2000;
    197             background: var(--md-sys-color-surface);
    198             display: flex; flex-direction: column; align-items: center; justify-content: space-between;
    199             padding: 48px 24px; opacity: 0; visibility: hidden; transition: opacity 0.3s;
    200         }
    201         #lock-screen.active { opacity: 1; visibility: visible; }
    202         
    203         .lock-header { text-align: center; margin-top: 4vh; }
    204         .lock-icon { width: 64px; height: 64px; margin-bottom: 16px; color: var(--md-sys-color-primary); }
    205         .lock-title { font-family: 'Google Sans'; font-size: 24px; margin-bottom: 8px; }
    206         .lock-sub { color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
    207 
    208         /* PIN Dots */
    209         .pin-dots { display: flex; gap: 16px; margin: 32px 0; justify-content: center; }
    210         .pin-dot {
    211             width: 14px; height: 14px; border-radius: 50%;
    212             border: 1px solid var(--md-sys-color-outline);
    213             transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    214         }
    215         .pin-dot.filled { background: var(--md-sys-color-on-surface); border-color: var(--md-sys-color-on-surface); transform: scale(1.1); }
    216         .pin-dot.error { border-color: var(--md-sys-color-error); background: var(--md-sys-color-error); }
    217 
    218         /* Native Keypad */
    219         .keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px 32px; margin-bottom: 2vh; }
    220         .key-btn {
    221             width: 72px; height: 72px; border-radius: 50%; border: none; background: transparent;
    222             font-size: 28px; font-weight: 400; color: var(--md-sys-color-on-surface);
    223             display: flex; align-items: center; justify-content: center; cursor: pointer;
    224             transition: background 0.2s; font-family: 'Google Sans', sans-serif;
    225         }
    226         .key-btn:active { background: var(--md-sys-color-surface-container-high); }
    227         .key-backspace { font-size: 24px; color: var(--md-sys-color-on-surface-variant); }
    228 
    229         .forgot-pin-btn {
    230             margin-top: 16px; color: var(--md-sys-color-primary); background: none; border: none;
    231             font-weight: 500; font-size: 14px; padding: 8px 16px; border-radius: 20px; cursor: pointer;
    232         }
    233 
    234         /* --- LOCKOUT UI --- */
    235         .lockout-container {
    236             display: flex; flex-direction: column; align-items: center; justify-content: center;
    237             text-align: center; width: 100%; animation: fadeIn 0.3s; flex: 1;
    238         }
    239         .lockout-icon-bg {
    240             width: 80px; height: 80px; border-radius: 50%;
    241             background: var(--md-sys-color-error-container);
    242             color: var(--md-sys-color-on-error-container);
    243             display: flex; align-items: center; justify-content: center;
    244             margin-bottom: 24px;
    245         }
    246         .lockout-icon { font-size: 40px; }
    247         .lockout-title { font-family: 'Google Sans'; font-size: 28px; margin-bottom: 8px; color: var(--md-sys-color-on-surface); }
    248         .lockout-msg { color: var(--md-sys-color-on-surface-variant); margin-bottom: 32px; font-size: 16px; }
    249         .lockout-timer { 
    250             font-family: 'Google Sans'; font-size: 56px; font-weight: 400; 
    251             color: var(--md-sys-color-error); font-feature-settings: "tnum"; 
    252             margin-bottom: 8px;
    253         }
    254         .lockout-sub { margin-top: 16px; color: var(--md-sys-color-on-surface-variant); font-size: 14px; }
    255         
    256         /* Utility to hide PIN/BIO when locked */
    257         .locked-mode #pinUI, 
    258         .locked-mode #bioUI, 
    259         .locked-mode .forgot-pin-btn { 
    260             display: none !important; 
    261         }
    262 
    263         /* --- UI ELEMENTS --- */
    264         .m3-field {
    265             position: relative; margin-bottom: 0;
    266             background: var(--md-sys-color-surface-container-highest);
    267             border-radius: 4px 4px 0 0;
    268             border-bottom: 1px solid var(--md-sys-color-outline);
    269             transition: background 0.3s;
    270             height: 56px;
    271         }
    272         .m3-field:hover {
    273             background: rgba(29, 25, 43, 0.08);
    274         }
    275         .m3-field:focus-within { 
    276             border-bottom: 2px solid var(--md-sys-color-primary); 
    277         }
    278         .m3-input {
    279             width: 100%; height: 100%; padding: 24px 16px 8px 16px;
    280             border: none; background: transparent; outline: none;
    281             font-size: 16px; color: var(--md-sys-color-on-surface); font-family: inherit;
    282         }
    283         .m3-label {
    284             position: absolute; left: 16px; top: 18px;
    285             color: var(--md-sys-color-on-surface-variant);
    286             font-size: 16px; pointer-events: none; transition: 0.2s cubic-bezier(0.2, 0, 0, 1);
    287         }
    288         .m3-input:focus ~ .m3-label, .m3-input:not(:placeholder-shown) ~ .m3-label {
    289             top: 8px; font-size: 12px; color: var(--md-sys-color-primary);
    290         }
    291         .eye-toggle {
    292             position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
    293             border: none; background: none; cursor: pointer; color: var(--md-sys-color-on-surface-variant);
    294             padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background 0.2s;
    295         }
    296         .eye-toggle:hover { background: rgba(0,0,0,0.05); color: var(--md-sys-color-on-surface); }
    297 
    298         .m3-btn {
    299             width: 100%; height: 48px; border-radius: 24px;
    300             border: none; outline: none; cursor: pointer;
    301             font-family: 'Google Sans', sans-serif; font-weight: 500; font-size: 14px;
    302             display: flex; align-items: center; justify-content: center; gap: 8px;
    303             transition: 0.2s; position: relative; overflow: hidden;
    304         }
    305         .m3-btn.filled {
    306             background: var(--md-sys-color-primary);
    307             color: var(--md-sys-color-on-primary);
    308             box-shadow: none; 
    309         }
    310         .m3-btn.filled:hover { 
    311             box-shadow: var(--elevation-1); 
    312         }
    313         .m3-btn:disabled { opacity: 0.38; cursor: not-allowed; background: var(--md-sys-color-on-surface); color: var(--md-sys-color-surface); box-shadow: none; }
    314 
    315         /* Spinner */
    316         .spinner {
    317             width: 20px; height: 20px; border: 2px solid rgba(255,255,255,0.3);
    318             border-radius: 50%; border-top-color: white;
    319             animation: spin 0.8s linear infinite; display: none;
    320         }
    321         .m3-btn.loading .btn-text { visibility: hidden; }
    322         .m3-btn.loading .btn-icon { display: none; }
    323         .m3-btn.loading .spinner { display: block; position: absolute; }
    324         @keyframes spin { to { transform: rotate(360deg); } }
    325 
    326         /* --- APP LAYOUT --- */
    327         #app-layout {
    328             display: flex; width: 100%; height: 100%; max-width: 1600px;
    329             background: var(--md-sys-color-surface); opacity: 0; transition: opacity 0.6s ease-in-out;
    330             visibility: hidden; 
    331         }
    332         #app-layout.visible { opacity: 1; visibility: visible; }
    333 
    334         /* SIDEBAR */
    335         .sidebar {
    336             width: 400px; display: flex; flex-direction: column;
    337             border-right: 1px solid var(--md-sys-color-outline-variant);
    338             background: var(--md-sys-color-surface); z-index: 10;
    339         }
    340         .sidebar-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; }
    341         .icon-btn {
    342             width: 40px; height: 40px; border-radius: 50%; border: none; background: transparent; cursor: pointer;
    343             color: var(--md-sys-color-on-surface-variant); display: flex; align-items: center; justify-content: center; transition: 0.2s;
    344         }
    345         .icon-btn:hover { background: rgba(128,128,128,0.1); color: var(--md-sys-color-on-surface); }
    346         
    347         .search-container { padding: 0 16px 12px; }
    348         .search-bar {
    349             background: var(--md-sys-color-surface-container-high);
    350             border-radius: 28px; height: 48px; display: flex; align-items: center; padding: 0 16px;
    351         }
    352         .search-bar input {
    353             border: none; background: transparent; outline: none; flex: 1; margin-left: 12px;
    354             font-size: 16px; color: var(--md-sys-color-on-surface);
    355         }
    356         .contact-list { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 8px; }
    357         .contact-item {
    358             display: flex; align-items: center; padding: 12px 16px; border-radius: 16px; cursor: pointer; margin-bottom: 2px;
    359             animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0) backwards;
    360         }
    361         .contact-item:hover { background: rgba(128,128,128,0.05); }
    362         .contact-item.active { background: var(--md-sys-color-secondary-container); }
    363         .contact-item.active .contact-name { color: var(--md-sys-color-on-secondary-container); font-weight: 700; }
    364         .avatar {
    365             width: 48px; height: 48px; border-radius: 50%;
    366             background: var(--md-sys-color-primary-container);
    367             color: var(--md-sys-color-on-primary-container);
    368             display: flex; align-items: center; justify-content: center;
    369             font-size: 18px; font-weight: 500; flex-shrink: 0; margin-right: 16px;
    370         }
    371         .contact-info { flex: 1; min-width: 0; }
    372         .contact-name { font-size: 16px; font-weight: 500; color: var(--md-sys-color-on-surface); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    373         .contact-sub { font-size: 14px; color: var(--md-sys-color-on-surface-variant); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    374 
    375         /* CHAT AREA */
    376         .chat-area {
    377             flex: 1; display: flex; flex-direction: column; position: relative;
    378             background-color: var(--chat-bg-color);
    379             transition: background-color 0.3s;
    380         }
    381         .chat-bg-pattern {
    382             position: absolute; inset: 0; opacity: var(--chat-bg-image-opacity); z-index: 0; pointer-events: none;
    383             background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 1.79 4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%236e7874' fill-opacity='0.08' fill-rule='evenodd'/%3E%3C/svg%3E");
    384         }
    385         .chat-header {
    386             height: 64px; background: var(--md-sys-color-surface);
    387             display: flex; align-items: center; padding: 0 16px;
    388             border-bottom: 1px solid var(--md-sys-color-outline-variant); z-index: 5;
    389         }
    390         .chat-content {
    391             flex: 1; overflow-y: auto; padding: 16px; z-index: 1;
    392             display: flex; flex-direction: column; gap: 4px;
    393             scroll-behavior: auto; /* Removed smooth scrolling for initial load */
    394             scroll-behavior: smooth;
    395         }
    396         /* Override specifically to remove default smooth behavior that causes load animation */
    397         .chat-content {
    398             scroll-behavior: auto !important;
    399         }
    400 
    401         .msg-bubble {
    402             max-width: 80%; padding: 8px 12px; position: relative;
    403             font-size: var(--base-font-size); line-height: 1.4;
    404             box-shadow: 0 1px 2px rgba(0,0,0,0.1);
    405             display: flex; flex-direction: column;
    406             animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0) backwards;
    407         }
    408         .msg-in {
    409             align-self: flex-start; background: var(--chat-bubble-them);
    410             border-radius: 0 16px 16px 16px; color: var(--md-sys-color-on-surface);
    411         }
    412         .msg-out {
    413             align-self: flex-end; background: var(--chat-bubble-me);
    414             border-radius: 16px 0 16px 16px; color: var(--md-sys-color-on-surface);
    415         }
    416         
    417         /* Message Grouping - WhatsApp Style */
    418         .msg-out.group-top { border-bottom-right-radius: 4px; margin-bottom: 2px; }
    419         .msg-out.group-mid { border-top-right-radius: 4px; border-bottom-right-radius: 4px; margin-bottom: 2px; }
    420         .msg-out.group-bot { border-top-right-radius: 4px; margin-top: 0; }
    421         
    422         .msg-in.group-top { border-bottom-left-radius: 4px; margin-bottom: 2px; }
    423         .msg-in.group-mid { border-top-left-radius: 4px; border-bottom-left-radius: 4px; margin-bottom: 2px; }
    424         .msg-in.group-bot { border-top-left-radius: 4px; margin-top: 0; }
    425 
    426         .msg-sender { font-size: 12px; font-weight: 700; color: var(--md-sys-color-primary); margin-bottom: 2px; }
    427         .msg-meta { font-size: 10px; opacity: 0.7; align-self: flex-end; margin-top: 2px; }
    428         
    429         /* DATE DIVIDERS */
    430         .chat-date-group {
    431             display: flex; flex-direction: column; gap: 4px; 
    432             position: relative; /* Scope for sticky */
    433         }
    434         .date-divider {
    435             align-self: center; margin: 16px 0;
    436             background: var(--md-sys-color-surface-container-high);
    437             color: var(--md-sys-color-on-surface-variant);
    438             font-size: 12px; font-weight: 500;
    439             padding: 6px 12px; border-radius: 12px;
    440             box-shadow: 0 1px 2px rgba(0,0,0,0.1);
    441             position: sticky; top: 8px; z-index: 2;
    442         }
    443 
    444         .highlight { background-color: var(--search-highlight); color: black; border-radius: 2px; padding: 0 2px; }
    445 
    446         /* Search In Chat */
    447         .in-chat-search {
    448             background: var(--md-sys-color-surface-container);
    449             padding: 8px 16px; display: flex; align-items: center; gap: 10px;
    450             border-bottom: 1px solid var(--md-sys-color-outline-variant);
    451             animation: slideUp 0.3s cubic-bezier(0.2, 0.0, 0, 1.0);
    452         }
    453         .in-chat-search input {
    454             border: none; background: transparent; outline: none; flex: 1;
    455             color: var(--md-sys-color-on-surface);
    456         }
    457 
    458         /* Menu */
    459         .menu-container { position: relative; }
    460         .dropdown-menu {
    461             position: absolute; top: 100%; right: 0;
    462             background: var(--md-sys-color-surface-container-high);
    463             border-radius: 12px; padding: 8px 0; min-width: 200px;
    464             display: none; flex-direction: column; z-index: 100;
    465             box-shadow: 0 4px 8px rgba(0,0,0,0.2);
    466             animation: scaleIn 0.2s cubic-bezier(0.2, 0.0, 0, 1.0); transform-origin: top right;
    467         }
    468         .dropdown-menu.show { display: flex; }
    469         .menu-item {
    470             padding: 12px 16px; display: flex; align-items: center; gap: 12px;
    471             color: var(--md-sys-color-on-surface); background: none; border: none;
    472             font-family: inherit; font-size: 14px; cursor: pointer; text-align: left;
    473         }
    474         .menu-item:hover { background: rgba(128,128,128,0.1); }
    475         
    476         /* Scroll To Bottom FAB */
    477         .scroll-fab {
    478             position: fixed; bottom: 24px; right: 24px;
    479             width: 40px; height: 40px; border-radius: 50%;
    480             background: var(--md-sys-color-surface-container-high);
    481             color: var(--md-sys-color-on-surface-variant);
    482             border: 1px solid var(--md-sys-color-outline-variant);
    483             box-shadow: 0 2px 6px rgba(0,0,0,0.15);
    484             display: flex; align-items: center; justify-content: center;
    485             cursor: pointer; z-index: 20; opacity: 0; transform: translateY(20px); pointer-events: none;
    486             transition: 0.3s;
    487         }
    488         .scroll-fab.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
    489 
    490         /* Calendar Modal */
    491         .calendar-card {
    492             background: var(--md-sys-color-surface);
    493             padding: 0; border-radius: 28px; width: 320px;
    494             overflow: hidden; display: flex; flex-direction: column;
    495         }
    496         .cal-header {
    497             background: var(--md-sys-color-surface-container-high);
    498             padding: 16px 24px;
    499         }
    500         .cal-header h3 { font-size: 14px; color: var(--md-sys-color-on-surface-variant); font-weight: 500; }
    501         .cal-header h2 { font-size: 32px; color: var(--md-sys-color-on-surface); font-weight: 400; margin-top: 4px; }
    502         
    503         .cal-grid { padding: 16px 12px 0; }
    504         .cal-nav { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding: 0 12px; }
    505         .cal-days { display: grid; grid-template-columns: repeat(7, 1fr); text-align: center; margin-bottom: 8px; }
    506         .cal-day-label { font-size: 12px; color: var(--md-sys-color-on-surface-variant); width: 36px; }
    507         
    508         .cal-dates { display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 16px; }
    509         .cal-date { 
    510             width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;
    511             border-radius: 50%; font-size: 14px; cursor: pointer; color: var(--md-sys-color-on-surface);
    512         }
    513         .cal-date:hover { background: var(--md-sys-color-surface-container-high); }
    514         .cal-date.active { background: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); }
    515         .cal-date.empty { pointer-events: none; }
    516 
    517         /* Settings Modal */
    518         .modal-overlay {
    519             position: fixed; inset: 0; z-index: 2000;
    520             background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
    521             display: flex; align-items: center; justify-content: center;
    522             opacity: 0; visibility: hidden; transition: 0.3s;
    523         }
    524         .modal-overlay.active { opacity: 1; visibility: visible; }
    525         .settings-card {
    526             background: var(--md-sys-color-surface);
    527             width: 90%; max-width: 600px; height: 80vh;
    528             border-radius: 28px; display: flex; flex-direction: column;
    529             overflow: hidden; animation: scaleIn 0.3s var(--anim-enter);
    530         }
    531         .settings-header {
    532             padding: 20px 24px; border-bottom: 1px solid var(--md-sys-color-outline-variant);
    533             display: flex; justify-content: space-between; align-items: center;
    534         }
    535         .settings-content { flex: 1; overflow-y: auto; padding: 24px; }
    536         .setting-section { margin-bottom: 32px; }
    537         .setting-title { font-size: 14px; font-weight: 500; color: var(--md-sys-color-primary); margin-bottom: 16px; text-transform: uppercase; letter-spacing: 1px; }
    538         .setting-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
    539         .setting-label h4 { font-size: 16px; font-weight: 400; margin-bottom: 4px; }
    540         .setting-label p { font-size: 14px; color: var(--md-sys-color-on-surface-variant); }
    541         
    542         /* Toggle Switch */
    543         .switch { position: relative; display: inline-block; width: 50px; height: 28px; }
    544         .switch input { opacity: 0; width: 0; height: 0; }
    545         .slider {
    546             position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0;
    547             background-color: var(--md-sys-color-surface-container-high); border-radius: 34px; transition: .4s;
    548             border: 1px solid var(--md-sys-color-outline);
    549         }
    550         .slider:before {
    551             position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 3px;
    552             background-color: var(--md-sys-color-outline); border-radius: 50%; transition: .4s;
    553         }
    554         input:checked + .slider { background-color: var(--md-sys-color-primary); border-color: var(--md-sys-color-primary); }
    555         input:checked + .slider:before { transform: translateX(20px); background-color: var(--md-sys-color-on-primary); }
    556 
    557         /* Accordion */
    558         .faq-item { border-bottom: 1px solid var(--md-sys-color-outline-variant); }
    559         .faq-question {
    560             width: 100%; padding: 16px 0; text-align: left; background: none; border: none;
    561             font-size: 16px; color: var(--md-sys-color-on-surface); cursor: pointer;
    562             display: flex; justify-content: space-between;
    563         }
    564         .faq-answer { display: none; padding-bottom: 16px; color: var(--md-sys-color-on-surface-variant); line-height: 1.5; }
    565         .faq-item.open .faq-answer { display: block; }
    566 
    567         /* Background Selector */
    568         .bg-selector { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 10px; }
    569         .bg-option {
    570             height: 60px; border-radius: 8px; cursor: pointer; border: 2px solid transparent;
    571             background-size: cover; position: relative; overflow: hidden;
    572         }
    573         .bg-option.selected { border-color: var(--md-sys-color-primary); }
    574         .bg-1 { background-color: #EFE7DE; }
    575         .bg-2 { background-color: #d1d7db; }
    576         .bg-3 { background-color: #e5ddd5; }
    577         .bg-4 { background-color: #ffffff; }
    578 
    579         /* Install Sheet - Hidden by default on PC */
    580         .install-sheet { display: none; }
    581         
    582         /* Settings Install Card */
    583         .install-card {
    584             background: var(--md-sys-color-primary-container);
    585             color: var(--md-sys-color-on-primary-container);
    586             padding: 16px; border-radius: 12px; margin-bottom: 32px;
    587             display: flex; align-items: center; justify-content: space-between;
    588         }
    589         .install-card h4 { margin-bottom: 4px; }
    590         .install-card p { font-size: 12px; }
    591         .install-card .m3-btn {
    592             width: auto; height: 36px; padding: 0 16px;
    593             font-size: 14px; white-space: nowrap; flex-shrink: 0;
    594             box-shadow: none;
    595         }
    596 
    597         @media (max-width: 768px) {
    598             .sidebar { 
    599                 width: 100%; 
    600                 flex: 1; 
    601                 border-right: none;
    602                 /* Mobile-only background gradient */
    603                 background: radial-gradient(circle at 0% 0%, #c1e8fb49 0%, transparent 50%), 
    604                             radial-gradient(circle at 100% 100%, #89f8c86b 0%, transparent 50%), 
    605                             #fbfdf96c;
    606             }
    607             
    608             /* Dark mode override for sidebar on mobile */
    609             [data-theme="dark"] .sidebar {
    610                  background: radial-gradient(circle at 0% 0%, #001F29 0%, transparent 50%), 
    611                             radial-gradient(circle at 100% 100%, #002114 0%, transparent 50%), 
    612                             #111412;
    613             }
    614 
    615             .chat-area { position: fixed; inset: 0; z-index: 20; transform: translateX(100%); transition: transform 0.3s; background-color: var(--chat-bg-color); }
    616             .chat-area.active { transform: translateX(0); }
    617             .back-btn { display: flex !important; margin-right: 8px; }
    618             
    619             /* Bottom Sheet Install Prompt - Only visible on Mobile */
    620             .install-sheet {
    621                 display: block; /* Enable it for mobile logic */
    622                 position: fixed; bottom: 0; left: 0; right: 0; z-index: 3000;
    623                 background: var(--md-sys-color-surface);
    624                 border-top-left-radius: 28px; border-top-right-radius: 28px;
    625                 padding: 24px; box-shadow: 0 -4px 20px rgba(0,0,0,0.15);
    626                 transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.2, 0, 0, 1);
    627             }
    628             .install-sheet.visible { transform: translateY(0); }
    629             .sheet-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
    630             .sheet-content { text-align: center; }
    631             .sheet-actions { display: flex; flex-direction: column; gap: 12px; margin-top: 24px; }
    632         }
    633         .back-btn { display: none; margin-right: 8px; }
    634 
    635         /* UTILITY MODALS STYLING FIX */
    636         .modal-card {
    637             background-color: var(--md-sys-color-surface);
    638             padding: 24px;
    639             border-radius: 28px;
    640             width: 90%;
    641             max-width: 320px;
    642             transform: scale(0.9); transition: 0.3s cubic-bezier(0.2, 0, 0, 1);
    643             animation: scaleIn 0.3s var(--anim-enter);
    644         }
    645         .modal-overlay.active .modal-card { transform: scale(1); }
    646         
    647         .modal-card.utility {
    648             width: 320px;
    649             padding: 24px;
    650             border-radius: 28px;
    651         }
    652         .modal-actions {
    653             display: flex;
    654             justify-content: flex-end;
    655             gap: 8px;
    656             margin-top: 24px;
    657         }
    658         .modal-actions .m3-btn {
    659             width: auto;
    660             min-width: 80px;
    661             height: 40px;
    662             padding: 0 16px;
    663         }
    664 
    665         /* Empty State Centering */
    666         .empty-state {
    667             display: flex; flex-direction: column; align-items: center; justify-content: center;
    668             height: 100%; width: 100%; color: var(--md-sys-color-outline);
    669             text-align: center; animation: fadeIn 0.5s;
    670         }
    671 
    672         /* Skeleton */
    673         .skeleton {
    674             background: linear-gradient(90deg, var(--md-sys-color-surface-container-high) 25%, var(--md-sys-color-surface-container) 50%, var(--md-sys-color-surface-container-high) 75%);
    675             background-size: 200% 100%; animation: shimmer 1.5s infinite linear; border-radius: 4px;
    676         }
    677         @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
    678         .skel-contact { display: flex; align-items: center; padding: 12px 16px; gap: 16px; width: 100%; }
    679         .skel-avatar { width: 48px; height: 48px; border-radius: 50%; }
    680         .skel-text-col { flex: 1; display: flex; flex-direction: column; gap: 8px; }
    681         .skel-msg-group { display: flex; flex-direction: column; gap: 12px; padding: 20px; width: 100%; }
    682         .skel-bubble { height: 40px; border-radius: 12px; width: 40%; margin-bottom: 8px;}
    683         .skel-left { align-self: flex-start; border-bottom-left-radius: 0; }
    684         .skel-right { align-self: flex-end; border-bottom-right-radius: 0; width: 50%; }
    685 
    686     </style>
    687 </head>
    688 <body>
    689 
    690     <!-- AUTH SCREEN -->
    691     <div id="auth-screen">
    692         <div class="auth-card">
    693             <div style="text-align: center;" class="anim-up delay-1">
    694                 <div class="brand-icon">
    695                     <img src="/assets/icon.svg" alt="App Logo">
    696                 </div>
    697                 <h1 style="font-family:'Google Sans'; font-size:28px; margin:8px 0; font-weight:400;">Welcome Back</h1>
    698                 <p style="color:var(--md-sys-color-on-surface-variant); font-size:14px;">Sign in to your chat log</p>
    699             </div>
    700 
    701             <div class="m3-field anim-up delay-2">
    702                 <input type="text" id="username" class="m3-input" placeholder=" ">
    703                 <label class="m3-label">Username</label>
    704             </div>
    705 
    706             <div class="m3-field anim-up delay-3">
    707                 <input type="password" id="password" class="m3-input" placeholder=" ">
    708                 <label class="m3-label">Password</label>
    709                 <button class="eye-toggle" onclick="togglePasswordVisibility('password', 'passIcon')">
    710                     <span class="material-symbols-rounded" id="passIcon">visibility</span>
    711                 </button>
    712             </div>
    713 
    714             <div class="anim-up delay-4" style="display:flex; align-items:center; padding-left:4px;">
    715                 <div style="display:flex; align-items:center; gap:8px;">
    716                     <input type="checkbox" id="rememberMe" style="width:18px; height:18px; accent-color: var(--md-sys-color-primary);">
    717                     <label for="rememberMe" style="font-size: 14px; color: var(--md-sys-color-on-surface);">Remember me</label>
    718                 </div>
    719             </div>
    720 
    721             <div class="anim-up delay-5" style="display: flex; flex-direction: column; gap: 12px; margin-top: 8px;">
    722                 <button class="m3-btn filled" id="loginBtn" onclick="attemptLogin()">
    723                     <span class="btn-text">Sign In</span>
    724                     <span class="material-symbols-rounded btn-icon" style="font-size:20px;">arrow_forward</span>
    725                     <div class="spinner"></div>
    726                 </button>
    727                 <p id="authError" style="color: var(--md-sys-color-error); font-size: 14px; opacity: 0; text-align: center;">Invalid Credentials</p>
    728             </div>
    729         </div>
    730     </div>
    731 
    732     <!-- LOCK SCREEN (PIN / BIO / LOCKOUT) -->
    733     <div id="lock-screen">
    734         <!-- LOCKOUT UI -->
    735         <div id="lockoutUI" class="hidden lockout-container">
    736             <div class="lockout-icon-bg">
    737                 <span class="material-symbols-rounded lockout-icon" id="lockoutIcon">lock_clock</span>
    738             </div>
    739             <h2 class="lockout-title" id="lockoutTitle">Locked</h2>
    740             <p class="lockout-msg" id="lockoutMsg">Too many attempts.</p>
    741             <div class="lockout-timer" id="lockoutTimer">00:30</div>
    742             <p class="lockout-sub" id="lockoutSub">Try again later</p>
    743             <div id="permanentLockActions" class="hidden" style="margin-top:24px; display:flex; gap:12px;">
    744                  <button class="m3-btn filled" onclick="showForgotPin()" style="background:var(--md-sys-color-error); width:auto; padding:0 32px;">Forgot PIN</button>
    745             </div>
    746         </div>
    747 
    748         <!-- HEADER (Hidden during lockout) -->
    749         <div class="lock-header" id="lockHeader">
    750             <span class="material-symbols-rounded lock-icon" id="lockIcon">lock</span>
    751             <div class="lock-title" id="lockTitle">Enter PIN</div>
    752             <div class="lock-sub" id="lockSub">Protected by App Lock</div>
    753         </div>
    754         
    755         <!-- PIN UI -->
    756         <div id="pinUI">
    757             <div class="pin-dots" id="pinDots">
    758                 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
    759                 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
    760             </div>
    761 
    762             <div class="keypad">
    763                 <button class="key-btn ripple-surface" onclick="pressKey(1)">1</button>
    764                 <button class="key-btn ripple-surface" onclick="pressKey(2)">2</button>
    765                 <button class="key-btn ripple-surface" onclick="pressKey(3)">3</button>
    766                 <button class="key-btn ripple-surface" onclick="pressKey(4)">4</button>
    767                 <button class="key-btn ripple-surface" onclick="pressKey(5)">5</button>
    768                 <button class="key-btn ripple-surface" onclick="pressKey(6)">6</button>
    769                 <button class="key-btn ripple-surface" onclick="pressKey(7)">7</button>
    770                 <button class="key-btn ripple-surface" onclick="pressKey(8)">8</button>
    771                 <button class="key-btn ripple-surface" onclick="pressKey(9)">9</button>
    772                 <button class="key-btn ripple-surface" style="visibility:hidden"> </button>
    773                 <button class="key-btn ripple-surface" onclick="pressKey(0)">0</button>
    774                 <button class="key-btn ripple-surface" onclick="pressKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
    775             </div>
    776         </div>
    777 
    778         <!-- BIO ONLY UI -->
    779         <div id="bioUI" class="hidden" style="flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;">
    780              <div class="ripple-surface" onclick="triggerBiometricAuth()" style="width:96px; height:96px; border-radius:50%; background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); display:flex; align-items:center; justify-content:center; cursor:pointer;">
    781                 <span class="material-symbols-rounded" style="font-size:48px;">fingerprint</span>
    782             </div>
    783             <p style="margin-top:16px; color:var(--md-sys-color-on-surface-variant);">Tap to Unlock</p>
    784         </div>
    785 
    786         <div style="text-align: center;">
    787             <button class="forgot-pin-btn" onclick="showForgotPin()">Forgot PIN / Use Password</button>
    788         </div>
    789     </div>
    790 
    791     <!-- MAIN APP -->
    792     <div id="app-layout">
    793         <aside class="sidebar" id="sidebar">
    794             <div class="sidebar-header">
    795                 <div style="display: flex; align-items: center; gap: 12px; padding: 12px 0;">
    796                     <!-- Title -->
    797                 <h2 style="font-family: 'Google Sans', sans-serif; font-size: 20px; font-weight: 500; color: var(--md-sys-color-on-surface); margin: 0;">
    798                     Chat Log
    799                 </h2>
    800 
    801                 <!-- Version (Tighter Gap) -->
    802                 <span style="font-family: 'Google Sans', sans-serif; font-size: 12px; font-weight: 500; color: var(--md-sys-color-on-surface-variant); background-color: var(--md-sys-color-surface-container-high); padding: 2px 8px; border-radius: 100px; display: inline-flex; align-items: center;">
    803                     v3.10.5
    804                 </span>
    805                 </div>
    806 
    807                 <div style="display:flex; gap: 8px;">
    808                     <button class="icon-btn ripple-surface" onclick="refreshApp()"><span class="material-symbols-rounded">refresh</span></button>
    809                     <button class="icon-btn ripple-surface" onclick="openSettings()" title="Settings"><span class="material-symbols-rounded">settings</span></button>
    810                 </div>
    811             </div>
    812             <div class="search-container">
    813                 <div class="search-bar">
    814                     <span class="material-symbols-rounded" style="color: var(--md-sys-color-on-surface-variant);">search</span>
    815                     <input type="text" id="searchInput" placeholder="Search...">
    816                 </div>
    817             </div>
    818             <div class="contact-list" id="contactList"></div>
    819         </aside>
    820 
    821         <main class="chat-area" id="chatArea">
    822             <header class="chat-header">
    823                 <button class="icon-btn back-btn ripple-surface" onclick="closeChatMobile()"><span class="material-symbols-rounded">arrow_back</span></button>
    824                 <div id="chatHeaderContent" style="display:flex; align-items:center; gap:12px; flex:1; opacity:0;">
    825                     <div class="avatar" id="headerAvatar">A</div>
    826                     <div style="flex:1;">
    827                         <div id="headerName" style="font-weight: 500; font-size: 16px;">Alice</div>
    828                         <div id="headerId" style="font-size: 12px; opacity: 0.7;">Online</div>
    829                     </div>
    830                     <div class="menu-container">
    831                         <button class="icon-btn ripple-surface" onclick="toggleChatMenu(event)"><span class="material-symbols-rounded">more_vert</span></button>
    832                         <div class="dropdown-menu" id="chatMenu">
    833                             <button class="menu-item" onclick="toggleChatSearch()"><span class="material-symbols-rounded">search</span>Search in Chat</button>
    834                             <button class="menu-item" onclick="openDateSearch()"><span class="material-symbols-rounded">calendar_month</span>Search by Date</button>
    835                             <button class="menu-item" onclick="showRenameModal()"><span class="material-symbols-rounded">edit</span>Rename</button>
    836                             <button class="menu-item" onclick="downloadChat()"><span class="material-symbols-rounded">download</span>Download</button>
    837                         </div>
    838                     </div>
    839                 </div>
    840             </header>
    841             
    842             <div id="inChatSearch" class="in-chat-search hidden">
    843                 <input type="text" id="msgSearchInput" placeholder="Find message...">
    844                 <div id="searchCount" style="font-size:12px; margin-right:8px;"></div>
    845                 <button class="icon-btn" style="width:30px; height:30px;" onclick="toggleChatSearch()"><span class="material-symbols-rounded">close</span></button>
    846             </div>
    847 
    848             <div class="chat-bg-pattern"></div>
    849             <div class="chat-content" id="messagesContainer">
    850                 <div class="empty-state">
    851                     <span class="material-symbols-rounded" style="font-size: 64px; opacity: 0.2;">chat</span>
    852                     <p>Select a conversation</p>
    853                 </div>
    854             </div>
    855             
    856             <!-- Scroll to Bottom FAB - Now triggers SMOOTH scroll manually -->
    857             <button class="scroll-fab" id="scrollFab" onclick="scrollToBottom('smooth')">
    858                 <span class="material-symbols-rounded">arrow_downward</span>
    859             </button>
    860         </main>
    861     </div>
    862 
    863     <!-- SETTINGS MODAL -->
    864     <div id="settingsModal" class="modal-overlay">
    865         <div class="settings-card">
    866             <div class="settings-header">
    867                 <h2 style="font-family:'Google Sans'">Settings</h2>
    868                 <button class="icon-btn" onclick="closeModal('settingsModal')"><span class="material-symbols-rounded">close</span></button>
    869             </div>
    870             <div class="settings-content">
    871                 
    872                 <!-- Install App -->
    873                 <div id="installAppSection" class="install-card hidden">
    874                     <div>
    875                         <h4>Install App</h4>
    876                         <p>Get the app for better experience</p>
    877                     </div>
    878                     <button class="m3-btn filled" onclick="installPWA()">Install</button>
    879                 </div>
    880                 
    881                 <!-- Security (Only visible on Mobile App) -->
    882                 <div class="setting-section hidden" id="mobileSecuritySection">
    883                     <div class="setting-title">App Security</div>
    884                     <div class="setting-row">
    885                         <div class="setting-label"><h4>PIN Lock</h4><p>Require PIN on startup</p></div>
    886                         <label class="switch"><input type="checkbox" id="pinLockToggle" onchange="togglePinSetting()"><span class="slider"></span></label>
    887                     </div>
    888                     <div class="setting-row">
    889                         <div class="setting-label"><h4>Fingerprint Lock</h4><p>Use biometrics</p></div>
    890                         <label class="switch"><input type="checkbox" id="bioLockToggle" onchange="toggleBioSetting()"><span class="slider"></span></label>
    891                     </div>
    892                 </div>
    893 
    894                 <!-- Profile -->
    895                 <div class="setting-section">
    896                     <div class="setting-title">Profile</div>
    897                     <div class="m3-field"><input type="text" id="profileName" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Your Name</label></div>
    898                     <div class="m3-field"><input type="text" id="profilePhone" class="m3-input" placeholder=" " onchange="saveProfile()"><label class="m3-label">Phone Number</label></div>
    899                 </div>
    900 
    901                 <!-- Appearance -->
    902                 <div class="setting-section">
    903                     <div class="setting-title">Appearance</div>
    904                     <div class="setting-row">
    905                         <div class="setting-label"><h4>Dark Mode</h4><p>Easier on the eyes</p></div>
    906                         <label class="switch"><input type="checkbox" id="darkModeToggle" onchange="toggleTheme()"><span class="slider"></span></label>
    907                     </div>
    908                     <div class="setting-row">
    909                         <div class="setting-label"><h4>Text Size</h4><p>Adjust message font size</p></div>
    910                         <input type="range" min="12" max="20" value="15" oninput="changeFontSize(this.value)">
    911                     </div>
    912                     
    913                     <div class="setting-label" style="margin-bottom:8px"><h4>Chat Wallpaper</h4></div>
    914                     <div class="bg-selector">
    915                         <div class="bg-option bg-1 selected" onclick="changeBg(1)"></div>
    916                         <div class="bg-option bg-2" onclick="changeBg(2)"></div>
    917                         <div class="bg-option bg-3" onclick="changeBg(3)"></div>
    918                         <div class="bg-option bg-4" onclick="changeBg(4)"></div>
    919                     </div>
    920                 </div>
    921 
    922                 <!-- Help & Legal -->
    923                 <div class="setting-section">
    924                     <div class="setting-title">Help & Support</div>
    925                     <div id="faqContainer">
    926                         <!-- FAQs injected via JS -->
    927                     </div>
    928                     <div style="margin-top:24px; display:flex; gap:12px;">
    929                         <button class="m3-btn filled" 
    930                                 style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container); cursor: pointer;" 
    931                                 onclick="window.open('https://amit.is-a.dev/logger#faq', '_blank')">
    932                             View FAQ
    933                         </button>
    934                     </div>
    935                     <div style="margin-top:24px; display:flex; gap:12px;">
    936                         <button class="m3-btn filled" style="background:var(--md-sys-color-secondary-container); color:var(--md-sys-color-on-secondary-container)" onclick="window.open('mailto:mail@amit.is-a.dev')">Email Support</button>
    937                     </div>
    938                 </div>
    939 
    940                 <div class="setting-section">
    941                     <div class="setting-title">Legal</div>
    942                     <p style="font-size:12px; color:var(--md-sys-color-on-surface-variant); line-height:1.5;">
    943                         <strong>MIT License</strong><br>
    944                         <strong>Security:</strong> Logs are stored strictly in your private Firebase. No external access.
    945                         <br>Check out this project on <a href="https://amit.is-a.dev/logger">amit.is-a.dev</a> or <a href="https://github.com/notamitgamer/WhatsApp-Logger-Self-Hosted-">Github</a>
    946                         <br>Developed by <a href="https://github.com/notamitgamer">@notamitgamer</a>
    947                     </p>
    948                 </div>
    949 
    950                 <button class="m3-btn filled" style="background:var(--md-sys-color-error);" onclick="showLogoutModal()">Log Out</button>
    951             </div>
    952         </div>
    953     </div>
    954 
    955     <!-- UTILITY MODALS -->
    956     <div id="logoutModal" class="modal-overlay">
    957         <div class="modal-card utility">
    958             <h3 class="modal-title">Log Out?</h3>
    959             <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">Session will end.</p>
    960             <div class="modal-actions">
    961                 <button class="m3-btn text" onclick="closeModal('logoutModal')">Cancel</button>
    962                 <button class="m3-btn filled" onclick="handleLogout()">Logout</button>
    963             </div>
    964         </div>
    965     </div>
    966     
    967     <!-- FORGOT PIN MODAL -->
    968     <div id="forgotPinModal" class="modal-overlay">
    969         <div class="modal-card utility">
    970             <h3 class="modal-title">Reset App Lock</h3>
    971             <p style="margin-top:8px; color:var(--md-sys-color-on-surface-variant); font-size:14px;">Enter your server credentials to reset local security.</p>
    972             
    973             <div class="m3-field" style="margin-top:16px;">
    974                  <input type="text" id="resetUsername" class="m3-input" placeholder=" "><label class="m3-label">Username</label>
    975             </div>
    976             <div class="m3-field">
    977                 <input type="password" id="resetPassword" class="m3-input" placeholder=" ">
    978                 <label class="m3-label">Password</label>
    979                 <button class="eye-toggle" onclick="togglePasswordVisibility('resetPassword', 'resetPassIcon')">
    980                     <span class="material-symbols-rounded" id="resetPassIcon">visibility</span>
    981                 </button>
    982             </div>
    983 
    984             <p id="resetError" style="color:var(--md-sys-color-error); font-size:12px; margin-bottom:12px; display:none;">Invalid credentials</p>
    985 
    986             <div class="modal-actions">
    987                 <button class="m3-btn text" onclick="closeModal('forgotPinModal')">Cancel</button>
    988                 <button class="m3-btn filled" id="resetBtn" onclick="attemptPinReset()">
    989                     <span class="btn-text">Reset</span>
    990                     <div class="spinner"></div>
    991                 </button>
    992             </div>
    993         </div>
    994     </div>
    995 
    996     <!-- RENAME MODAL -->
    997     <div id="renameModal" class="modal-overlay">
    998         <div class="modal-card utility">
    999             <h3 class="modal-title">Rename Chat</h3>
   1000             <div class="m3-field" style="margin-top:16px;"><input type="text" id="renameInput" class="m3-input"><label class="m3-label">New Name</label></div>
   1001             <div class="modal-actions">
   1002                 <button class="m3-btn text" onclick="closeModal('renameModal')">Cancel</button>
   1003                 <button class="m3-btn filled" onclick="saveRename()">Save</button>
   1004             </div>
   1005         </div>
   1006     </div>
   1007     
   1008     <!-- NOT FOUND MODAL -->
   1009     <div id="notFoundModal" class="modal-overlay">
   1010         <div class="modal-card utility">
   1011             <h3 class="modal-title">No Messages Found</h3>
   1012             <p style="margin:16px 0; color:var(--md-sys-color-on-surface-variant);">There are no messages from this date onwards.</p>
   1013             <div class="modal-actions">
   1014                 <button class="m3-btn filled" onclick="closeModal('notFoundModal')">OK</button>
   1015             </div>
   1016         </div>
   1017     </div>
   1018     
   1019     <!-- PIN SETUP MODAL -->
   1020     <div id="pinSetupModal" class="modal-overlay">
   1021         <div class="modal-card utility" style="text-align:center;">
   1022              <h3 class="modal-title" id="pinSetupTitle">Set 6-Digit PIN</h3>
   1023              <div class="pin-dots" id="setupDots" style="margin: 24px 0;">
   1024                 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
   1025                 <div class="pin-dot"></div><div class="pin-dot"></div><div class="pin-dot"></div>
   1026             </div>
   1027             <p style="color:var(--md-sys-color-on-surface-variant); font-size:12px;" id="pinSetupMsg">Use the number pad below</p>
   1028             
   1029             <div class="keypad" style="transform: scale(0.9); margin-bottom:0;">
   1030                 <button class="key-btn ripple-surface" onclick="pressSetupKey(1)">1</button>
   1031                 <button class="key-btn ripple-surface" onclick="pressSetupKey(2)">2</button>
   1032                 <button class="key-btn ripple-surface" onclick="pressSetupKey(3)">3</button>
   1033                 <button class="key-btn ripple-surface" onclick="pressSetupKey(4)">4</button>
   1034                 <button class="key-btn ripple-surface" onclick="pressSetupKey(5)">5</button>
   1035                 <button class="key-btn ripple-surface" onclick="pressSetupKey(6)">6</button>
   1036                 <button class="key-btn ripple-surface" onclick="pressSetupKey(7)">7</button>
   1037                 <button class="key-btn ripple-surface" onclick="pressSetupKey(8)">8</button>
   1038                 <button class="key-btn ripple-surface" onclick="pressSetupKey(9)">9</button>
   1039                 <button class="key-btn ripple-surface" style="visibility:hidden"> </button>
   1040                 <button class="key-btn ripple-surface" onclick="pressSetupKey(0)">0</button>
   1041                 <button class="key-btn ripple-surface" onclick="pressSetupKey(-1)"><span class="material-symbols-rounded key-backspace">backspace</span></button>
   1042             </div>
   1043              <div class="modal-actions" style="justify-content:center; margin-top:8px;">
   1044                 <button class="m3-btn text" onclick="cancelPinSetup()">Cancel</button>
   1045             </div>
   1046         </div>
   1047     </div>
   1048     
   1049     <!-- CALENDAR MODAL -->
   1050     <div id="calendarModal" class="modal-overlay">
   1051         <div class="calendar-card animate-enter">
   1052             <div class="cal-header">
   1053                 <h3>Select date</h3>
   1054                 <h2 id="calSelectedDateStr">Tue, Oct 24</h2>
   1055             </div>
   1056             <div class="cal-grid">
   1057                 <div class="cal-nav">
   1058                     <span id="calMonthYear" style="font-weight:500">October 2023</span>
   1059                     <div style="display:flex; gap:12px;">
   1060                         <button class="icon-btn" onclick="changeCalMonth(-1)"><span class="material-symbols-rounded">chevron_left</span></button>
   1061                         <button class="icon-btn" onclick="changeCalMonth(1)"><span class="material-symbols-rounded">chevron_right</span></button>
   1062                     </div>
   1063                 </div>
   1064                 <div class="cal-days">
   1065                     <span class="cal-day-label">S</span><span class="cal-day-label">M</span><span class="cal-day-label">T</span>
   1066                     <span class="cal-day-label">W</span><span class="cal-day-label">T</span><span class="cal-day-label">F</span><span class="cal-day-label">S</span>
   1067                 </div>
   1068                 <div class="cal-dates" id="calDatesGrid"></div>
   1069             </div>
   1070             <div class="modal-actions" style="padding: 16px 24px;">
   1071                 <button class="m3-btn text" onclick="closeModal('calendarModal')">Cancel</button>
   1072                 <button class="m3-btn filled" onclick="jumpToDate()" style="width:auto; padding:0 24px; height:40px;">OK</button>
   1073             </div>
   1074         </div>
   1075     </div>
   1076 
   1077     <!-- PWA INSTALL SHEET (Mobile Only) -->
   1078     <div id="installSheet" class="install-sheet">
   1079         <div class="sheet-header">
   1080             <h3 style="font-family:'Google Sans'; margin: 0;">Install App</h3>
   1081             <button class="icon-btn" onclick="closeInstallSheet()"><span class="material-symbols-rounded">close</span></button>
   1082         </div>
   1083         <div class="sheet-content">
   1084             <div style="width:64px; height:64px; margin:0 auto 16px; display:flex; align-items:center; justify-content:center;">
   1085                 <img src="/assets/icon.svg" alt="App Logo" style="width:100%; height:100%; object-fit: contain;">
   1086             </div>
   1087             <p style="color:var(--md-sys-color-on-surface-variant); margin-bottom:24px;">Install WhatsApp Logger App for a better experience, full screen view, and enhanced security.</p>
   1088             <div class="sheet-actions">
   1089                 <button class="m3-btn filled" onclick="installPWA()">Install App</button>
   1090                 <button class="m3-btn text" onclick="closeInstallSheet()">Continue in Browser</button>
   1091             </div>
   1092         </div>
   1093     </div>
   1094 
   1095     <script>
   1096         const RENDER_BACKEND_URL = "https://logger-8i35.onrender.com"; 
   1097         const firebaseConfig = {
   1098             apiKey: "AIzaSyDqOELjpsI7nbPdK7Z6X57rVRyKUlH4bzk",
   1099             authDomain: "wp-logger.firebaseapp.com",
   1100             projectId: "wp-logger",
   1101             storageBucket: "wp-logger.firebasestorage.app",
   1102             messagingSenderId: "642661549198",
   1103             appId: "1:642661549198:web:ec94fa62aea1c4e2b375df"
   1104         };
   1105         
   1106         firebase.initializeApp(firebaseConfig);
   1107         const db = firebase.firestore();
   1108         let activeChatId = null;
   1109         let deferredPrompt;
   1110         let currentMessages = [];
   1111         
   1112         // --- SECURITY VARS ---
   1113         let enteredPin = "";
   1114         let setupPin = "";
   1115         let setupStep = 0; 
   1116         let isLocked = false;
   1117         // Lockout Variables
   1118         let failAttempts = parseInt(localStorage.getItem('wp_fail_attempts') || '0');
   1119         let lockUntil = parseInt(localStorage.getItem('wp_lock_until') || '0');
   1120         let lockoutTimerInterval;
   1121         
   1122         // --- CALENDAR VARS ---
   1123         let calCurrentDate = new Date();
   1124         let calSelectedDate = new Date();
   1125 
   1126         // --- HELPERS ---
   1127         const arrayBufferToBase64 = (buffer) => {
   1128             let binary = '';
   1129             const bytes = new Uint8Array(buffer);
   1130             const len = bytes.byteLength;
   1131             for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
   1132             return window.btoa(binary);
   1133         };
   1134         const base64ToArrayBuffer = (base64) => {
   1135             const binary_string = window.atob(base64);
   1136             const len = binary_string.length;
   1137             const bytes = new Uint8Array(len);
   1138             for (let i = 0; i < len; i++) bytes[i] = binary_string.charCodeAt(i);
   1139             return bytes.buffer;
   1140         };
   1141         
   1142         function formatDividerDate(ts) {
   1143             const date = new Date(ts * 1000);
   1144             const today = new Date();
   1145             const yesterday = new Date();
   1146             yesterday.setDate(yesterday.getDate() - 1);
   1147             
   1148             if (date.toDateString() === today.toDateString()) return "Today";
   1149             if (date.toDateString() === yesterday.toDateString()) return "Yesterday";
   1150             return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' });
   1151         }
   1152 
   1153         // --- PWA INSTALLATION ---
   1154         if ('serviceWorker' in navigator && (window.location.protocol === 'https:' || window.location.protocol === 'http:')) {
   1155             navigator.serviceWorker.register('/assets/sw-wpChat.js').catch(err => {
   1156                 console.warn('SW Register Fail:', err);
   1157             });
   1158         }
   1159         window.addEventListener('beforeinstallprompt', (e) => {
   1160             e.preventDefault(); 
   1161             deferredPrompt = e;
   1162             document.getElementById('installAppSection').classList.remove('hidden');
   1163             if (window.innerWidth <= 768) {
   1164                 setTimeout(() => document.getElementById('installSheet').classList.add('visible'), 2000);
   1165             }
   1166         });
   1167         async function installPWA() {
   1168             if (!deferredPrompt) return;
   1169             deferredPrompt.prompt();
   1170             const { outcome } = await deferredPrompt.userChoice;
   1171             if (outcome === 'accepted') {
   1172                 closeInstallSheet();
   1173                 document.getElementById('installAppSection').classList.add('hidden');
   1174             }
   1175             deferredPrompt = null;
   1176         }
   1177 
   1178         function closeInstallSheet() {
   1179             document.getElementById('installSheet').classList.remove('visible');
   1180         }
   1181 
   1182         // --- AUTH & BOOT SEQUENCE ---
   1183         function isMobileApp() {
   1184             const isStandalone = (window.matchMedia('(display-mode: standalone)').matches) || (window.navigator.standalone) || document.referrer.includes('android-app://');
   1185             const isMobile = window.innerWidth <= 768;
   1186             return isStandalone && isMobile;
   1187         }
   1188 
   1189         window.onload = () => {
   1190             if (localStorage.getItem('theme') === 'dark') {
   1191                 document.body.setAttribute('data-theme', 'dark');
   1192                 document.getElementById('darkModeToggle').checked = true;
   1193             }
   1194             document.getElementById('profileName').value = localStorage.getItem('profileName') || '';
   1195             document.getElementById('profilePhone').value = localStorage.getItem('profilePhone') || '';
   1196             const bg = localStorage.getItem('chatBg') || 1;
   1197             changeBg(bg, false);
   1198             populateFAQ();
   1199 
   1200             if(isMobileApp()) {
   1201                 document.getElementById('mobileSecuritySection').classList.remove('hidden');
   1202                 
   1203                 const hasPin = localStorage.getItem('wp_app_pin');
   1204                 const useBio = localStorage.getItem('wp_app_bio') === 'true';
   1205                 document.getElementById('pinLockToggle').checked = !!hasPin;
   1206                 document.getElementById('bioLockToggle').checked = useBio;
   1207 
   1208                 const authValid = localStorage.getItem('wp_auth_expiry') > Date.now();
   1209                 
   1210                 if (authValid && (hasPin || useBio)) {
   1211                     document.getElementById('auth-screen').style.display = 'none';
   1212                     initLockScreen(hasPin, useBio);
   1213                 } else if (authValid) {
   1214                     document.getElementById('auth-screen').style.display = 'none';
   1215                     document.getElementById('app-layout').classList.add('visible');
   1216                     loadChats();
   1217                 } else {
   1218                     document.getElementById('auth-screen').style.opacity = '1';
   1219                 }
   1220             } else {
   1221                 if(localStorage.getItem('wp_auth_expiry') > Date.now()) {
   1222                     document.getElementById('auth-screen').style.display = 'none';
   1223                     document.getElementById('app-layout').classList.add('visible');
   1224                     loadChats();
   1225                 } else {
   1226                     document.getElementById('auth-screen').style.opacity = '1';
   1227                 }
   1228             }
   1229             
   1230             // Scroll Listener for FAB
   1231             document.getElementById('messagesContainer').addEventListener('scroll', function() {
   1232                 const fab = document.getElementById('scrollFab');
   1233                 const st = this.scrollTop;
   1234                 const sh = this.scrollHeight;
   1235                 const ch = this.clientHeight;
   1236                 
   1237                 // Show if we are more than 500px from the bottom
   1238                 if (sh - st - ch > 500) {
   1239                     fab.classList.add('visible');
   1240                 } else {
   1241                     fab.classList.remove('visible');
   1242                 }
   1243             });
   1244 
   1245             // Fix for mobile keyboard covering inputs
   1246             const inputs = document.querySelectorAll('input[type="text"], input[type="password"]');
   1247             inputs.forEach(input => {
   1248                 input.addEventListener('focus', () => {
   1249                     if (window.innerWidth <= 768) {
   1250                         setTimeout(() => {
   1251                             input.scrollIntoView({ behavior: 'smooth', block: 'center' });
   1252                         }, 300);
   1253                     }
   1254                 });
   1255             });
   1256         };
   1257 
   1258         // --- LOCK SCREEN & LOCKOUT LOGIC ---
   1259         function initLockScreen(hasPin, useBio) {
   1260             const lockScreen = document.getElementById('lock-screen');
   1261             lockScreen.classList.add('active');
   1262             isLocked = true;
   1263             enteredPin = "";
   1264             updatePinDots(0);
   1265             
   1266             // Check Lockout State First
   1267             if (checkLockoutState()) return;
   1268 
   1269             if(hasPin) {
   1270                 document.getElementById('pinUI').classList.remove('hidden');
   1271                 document.getElementById('bioUI').classList.add('hidden');
   1272                 document.getElementById('lockTitle').innerText = "Enter PIN";
   1273                 document.getElementById('lockIcon').innerText = "lock";
   1274             } else if (useBio && !hasPin) {
   1275                 document.getElementById('pinUI').classList.add('hidden');
   1276                 document.getElementById('bioUI').classList.remove('hidden');
   1277                 document.getElementById('lockTitle').innerText = "Scan Fingerprint";
   1278                 document.getElementById('lockIcon').innerText = "fingerprint";
   1279                 triggerBiometricAuth();
   1280             }
   1281         }
   1282 
   1283         function checkLockoutState() {
   1284             const now = Date.now();
   1285             
   1286             // Permanent Lock
   1287             if (failAttempts >= 5) {
   1288                 renderPermanentLock();
   1289                 return true;
   1290             }
   1291             
   1292             // Temporary Timer Lock
   1293             if (lockUntil > now) {
   1294                 startLockoutTimer();
   1295                 return true;
   1296             }
   1297             
   1298             // Reset if timer expired
   1299             if (lockUntil > 0 && lockUntil <= now) {
   1300                 lockUntil = 0;
   1301                 localStorage.setItem('wp_lock_until', '0');
   1302                 // Don't reset failAttempts to 0 here to keep escalating. 
   1303                 // But we need to hide lockout UI
   1304                 document.getElementById('lockoutUI').classList.add('hidden');
   1305                 document.getElementById('lockHeader').classList.remove('hidden');
   1306                 document.getElementById('lock-screen').classList.remove('locked-mode');
   1307             }
   1308             
   1309             return false;
   1310         }
   1311 
   1312         function handleFailedAttempt() {
   1313             failAttempts++;
   1314             localStorage.setItem('wp_fail_attempts', failAttempts);
   1315 
   1316             if (failAttempts === 3 || failAttempts === 4) {
   1317                 lockUntil = Date.now() + (30 * 1000); // 30 seconds
   1318                 localStorage.setItem('wp_lock_until', lockUntil);
   1319                 startLockoutTimer();
   1320             } else if (failAttempts >= 5) {
   1321                 renderPermanentLock();
   1322             } else {
   1323                 // Shake animation for attempt 1 and 2
   1324                 document.getElementById('pinDots').classList.add('shake');
   1325                 updatePinDots(6, true);
   1326                 setTimeout(() => {
   1327                     document.getElementById('pinDots').classList.remove('shake');
   1328                     enteredPin = "";
   1329                     updatePinDots(0);
   1330                 }, 400);
   1331             }
   1332         }
   1333 
   1334         function startLockoutTimer() {
   1335             const ui = document.getElementById('lockoutUI');
   1336             const screen = document.getElementById('lock-screen');
   1337             const timerDisplay = document.getElementById('lockoutTimer');
   1338             
   1339             ui.classList.remove('hidden');
   1340             document.getElementById('lockHeader').classList.add('hidden');
   1341             screen.classList.add('locked-mode'); // Hides PIN/BIO inputs
   1342             
   1343             document.getElementById('lockoutTitle').innerText = "Locked";
   1344             document.getElementById('lockoutMsg').innerText = `Too many attempts (${failAttempts})`;
   1345             document.getElementById('lockoutSub').innerText = "Try again later";
   1346             document.getElementById('permanentLockActions').classList.add('hidden');
   1347             document.getElementById('lockoutTimer').classList.remove('hidden');
   1348             
   1349             if(lockoutTimerInterval) clearInterval(lockoutTimerInterval);
   1350             
   1351             lockoutTimerInterval = setInterval(() => {
   1352                 const left = Math.ceil((lockUntil - Date.now()) / 1000);
   1353                 if (left <= 0) {
   1354                     clearInterval(lockoutTimerInterval);
   1355                     timerDisplay.innerText = "00:00";
   1356                     // Reset View
   1357                     initLockScreen(localStorage.getItem('wp_app_pin'), localStorage.getItem('wp_app_bio') === 'true');
   1358                     document.getElementById('lockoutUI').classList.add('hidden');
   1359                     document.getElementById('lockHeader').classList.remove('hidden');
   1360                     screen.classList.remove('locked-mode');
   1361                 } else {
   1362                     const secs = left < 10 ? `0${left}` : left;
   1363                     timerDisplay.innerText = `00:${secs}`;
   1364                 }
   1365             }, 1000);
   1366         }
   1367 
   1368         function renderPermanentLock() {
   1369             const ui = document.getElementById('lockoutUI');
   1370             const screen = document.getElementById('lock-screen');
   1371             
   1372             ui.classList.remove('hidden');
   1373             document.getElementById('lockHeader').classList.add('hidden');
   1374             screen.classList.add('locked-mode');
   1375             
   1376             document.getElementById('lockoutIcon').innerText = "gpp_bad";
   1377             document.getElementById('lockoutTitle').innerText = "Security Lockout";
   1378             document.getElementById('lockoutMsg').innerText = "Maximum attempts exceeded. App is locked.";
   1379             document.getElementById('lockoutSub').innerText = "Use Forgot Password to reset.";
   1380             document.getElementById('lockoutTimer').classList.add('hidden');
   1381             document.getElementById('permanentLockActions').classList.remove('hidden');
   1382         }
   1383 
   1384         function pressKey(num) {
   1385             if(num === -1) {
   1386                 enteredPin = enteredPin.slice(0, -1);
   1387             } else if (enteredPin.length < 6) {
   1388                 enteredPin += num;
   1389             }
   1390             updatePinDots(enteredPin.length);
   1391             
   1392             if(enteredPin.length === 6) {
   1393                 verifyPin();
   1394             }
   1395         }
   1396 
   1397         function updatePinDots(count, isError=false) {
   1398             const dots = document.querySelectorAll('#pinDots .pin-dot');
   1399             dots.forEach((d, i) => {
   1400                 if(isError) {
   1401                     d.classList.add('error');
   1402                     setTimeout(() => d.classList.remove('error'), 400);
   1403                 } else {
   1404                     d.classList.remove('error');
   1405                     if(i < count) d.classList.add('filled');
   1406                     else d.classList.remove('filled');
   1407                 }
   1408             });
   1409         }
   1410 
   1411         function verifyPin() {
   1412             // Re-check lockout before verifying
   1413             if(checkLockoutState()) return;
   1414 
   1415             const storedPin = localStorage.getItem('wp_app_pin');
   1416             if(storedPin && enteredPin === storedPin) {
   1417                 // Success - Reset Fail Counters
   1418                 failAttempts = 0;
   1419                 localStorage.setItem('wp_fail_attempts', '0');
   1420                 localStorage.setItem('wp_lock_until', '0');
   1421                 
   1422                 const useBio = localStorage.getItem('wp_app_bio') === 'true';
   1423                 if (useBio) {
   1424                     document.getElementById('pinUI').classList.add('hidden');
   1425                     document.getElementById('bioUI').classList.remove('hidden');
   1426                     document.getElementById('lockTitle').innerText = "Verify Identity";
   1427                     document.getElementById('lockIcon').innerText = "fingerprint";
   1428                     triggerBiometricAuth();
   1429                 } else {
   1430                     unlockApp();
   1431                 }
   1432             } else {
   1433                 handleFailedAttempt();
   1434             }
   1435         }
   1436 
   1437         async function triggerBiometricAuth() {
   1438             if(checkLockoutState()) return;
   1439             
   1440             if(document.getElementById('bioUI').classList.contains('hidden') === false) {
   1441                  document.getElementById('lockTitle').innerText = "Verify Identity";
   1442             }
   1443             try {
   1444                 const storedId = localStorage.getItem('wp_bio_id');
   1445                 const options = {
   1446                     publicKey: {
   1447                         challenge: new Uint8Array([1, 2, 3, 4]), 
   1448                         timeout: 60000,
   1449                         userVerification: "required"
   1450                     }
   1451                 };
   1452                 if (storedId) {
   1453                     options.publicKey.allowCredentials = [{
   1454                         type: 'public-key',
   1455                         id: base64ToArrayBuffer(storedId),
   1456                         transports: ['internal']
   1457                     }];
   1458                 }
   1459                 const credential = await navigator.credentials.get(options);
   1460                 if(credential) {
   1461                     // Success Bio - Reset Counters
   1462                     failAttempts = 0;
   1463                     localStorage.setItem('wp_fail_attempts', '0');
   1464                     localStorage.setItem('wp_lock_until', '0');
   1465                     unlockApp();
   1466                 }
   1467             } catch (e) { 
   1468                 console.warn("Bio failed", e);
   1469                 // Note: We generally don't lock on Bio failures here as OS handles retries.
   1470                 // But if user cancels and goes to PIN, PIN failures will trigger lock.
   1471             }
   1472         }
   1473 
   1474         function unlockApp() {
   1475             isLocked = false;
   1476             document.getElementById('lock-screen').classList.remove('active');
   1477             document.getElementById('app-layout').classList.add('visible');
   1478             loadChats();
   1479         }
   1480 
   1481         // --- SECURITY SETTINGS ---
   1482         function togglePinSetting() {
   1483             const chk = document.getElementById('pinLockToggle');
   1484             if(chk.checked) openPinSetup();
   1485             else localStorage.removeItem('wp_app_pin');
   1486         }
   1487 
   1488         function toggleBioSetting() {
   1489             const chk = document.getElementById('bioLockToggle');
   1490             if(chk.checked) registerBiometric();
   1491             else {
   1492                 localStorage.removeItem('wp_app_bio');
   1493                 localStorage.removeItem('wp_bio_id');
   1494             }
   1495         }
   1496 
   1497         function openPinSetup() {
   1498             document.getElementById('pinSetupModal').classList.add('active');
   1499             setupStep = 1;
   1500             setupPin = "";
   1501             enteredPin = ""; 
   1502             updateSetupDots(0);
   1503             document.getElementById('pinSetupTitle').innerText = "Set 6-Digit PIN";
   1504             document.getElementById('pinSetupMsg').innerText = "Enter a new PIN";
   1505         }
   1506         
   1507         function updateSetupDots(count) {
   1508             const dots = document.querySelectorAll('#setupDots .pin-dot');
   1509             dots.forEach((d, i) => i < count ? d.classList.add('filled') : d.classList.remove('filled'));
   1510         }
   1511 
   1512         function pressSetupKey(num) {
   1513             if(num === -1) {
   1514                 enteredPin = enteredPin.slice(0, -1);
   1515             } else if (enteredPin.length < 6) {
   1516                 enteredPin += num;
   1517             }
   1518             updateSetupDots(enteredPin.length);
   1519 
   1520             if(enteredPin.length === 6) {
   1521                 if(setupStep === 1) {
   1522                     setupPin = enteredPin;
   1523                     enteredPin = "";
   1524                     setupStep = 2;
   1525                     setTimeout(() => {
   1526                          updateSetupDots(0);
   1527                          document.getElementById('pinSetupTitle').innerText = "Confirm PIN";
   1528                          document.getElementById('pinSetupMsg').innerText = "Re-enter to confirm";
   1529                     }, 200);
   1530                 } else if (setupStep === 2) {
   1531                     if(enteredPin === setupPin) {
   1532                         localStorage.setItem('wp_app_pin', setupPin);
   1533                         closeModal('pinSetupModal');
   1534                         enteredPin = "";
   1535                     } else {
   1536                         const dots = document.getElementById('setupDots');
   1537                         dots.classList.add('shake');
   1538                         setTimeout(() => {
   1539                             dots.classList.remove('shake');
   1540                             enteredPin = "";
   1541                             setupPin = "";
   1542                             setupStep = 1;
   1543                             updateSetupDots(0);
   1544                             document.getElementById('pinSetupTitle').innerText = "Mismatch. Try Again";
   1545                             document.getElementById('pinSetupMsg').innerText = "Enter a new PIN";
   1546                         }, 500);
   1547                     }
   1548                 }
   1549             }
   1550         }
   1551 
   1552         function cancelPinSetup() {
   1553             document.getElementById('pinLockToggle').checked = false;
   1554             closeModal('pinSetupModal');
   1555         }
   1556 
   1557         async function registerBiometric() {
   1558             try {
   1559                 const iconPath = new URL('/assets/icon.svg', window.location.href).toString();
   1560                 const credential = await navigator.credentials.create({
   1561                     publicKey: {
   1562                         challenge: new Uint8Array([1, 2, 3, 4]),
   1563                         rp: { name: "WhatsApp Logger", icon: iconPath },
   1564                         user: { id: new Uint8Array([1]), name: "User", displayName: "App User" },
   1565                         pubKeyCredParams: [{type: "public-key", alg: -7}],
   1566                         authenticatorSelection: { authenticatorAttachment: "platform", userVerification: "required" },
   1567                         timeout: 60000
   1568                     }
   1569                 });
   1570                 
   1571                 if(credential) {
   1572                     const idBase64 = arrayBufferToBase64(credential.rawId);
   1573                     localStorage.setItem('wp_bio_id', idBase64);
   1574                     localStorage.setItem('wp_app_bio', 'true');
   1575                 } else { throw new Error("No cred"); }
   1576             } catch (e) {
   1577                 console.error(e);
   1578                 document.getElementById('bioLockToggle').checked = false;
   1579             }
   1580         }
   1581 
   1582         function showForgotPin() {
   1583             document.getElementById('forgotPinModal').classList.add('active');
   1584         }
   1585         
   1586         async function attemptPinReset() {
   1587              const btn = document.getElementById('resetBtn');
   1588              const err = document.getElementById('resetError');
   1589              const u = document.getElementById('resetUsername').value;
   1590              const p = document.getElementById('resetPassword').value;
   1591              
   1592              btn.classList.add('loading');
   1593              btn.disabled = true;
   1594              err.style.display = 'none';
   1595              
   1596              try {
   1597                 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
   1598                     method: 'POST',
   1599                     headers: {'Content-Type': 'application/json'},
   1600                     body: JSON.stringify({ username: u, password: p })
   1601                 });
   1602                 const data = await response.json();
   1603                 
   1604                 if(data.success) {
   1605                     // RESET EVERYTHING
   1606                     localStorage.removeItem('wp_app_pin');
   1607                     localStorage.removeItem('wp_app_bio');
   1608                     localStorage.removeItem('wp_bio_id');
   1609                     localStorage.removeItem('wp_fail_attempts');
   1610                     localStorage.removeItem('wp_lock_until');
   1611                     failAttempts = 0;
   1612                     lockUntil = 0;
   1613                     
   1614                     document.getElementById('pinLockToggle').checked = false;
   1615                     document.getElementById('bioLockToggle').checked = false;
   1616                     
   1617                     // Reset UI State
   1618                     document.getElementById('lockoutUI').classList.add('hidden');
   1619                     document.getElementById('lockHeader').classList.remove('hidden');
   1620                     document.getElementById('lock-screen').classList.remove('locked-mode');
   1621 
   1622                     closeModal('forgotPinModal');
   1623                     unlockApp();
   1624                 } else { throw new Error(); }
   1625              } catch (e) {
   1626                  err.style.display = 'block';
   1627              } finally {
   1628                  btn.classList.remove('loading');
   1629                  btn.disabled = false;
   1630              }
   1631         }
   1632 
   1633         function togglePasswordVisibility(id, iconId) {
   1634             const input = document.getElementById(id);
   1635             const icon = document.getElementById(iconId);
   1636             if (input.type === 'password') {
   1637                 input.type = 'text';
   1638                 icon.innerText = 'visibility_off';
   1639             } else {
   1640                 input.type = 'password';
   1641                 icon.innerText = 'visibility';
   1642             }
   1643         }
   1644 
   1645         async function attemptLogin() {
   1646             const btn = document.getElementById('loginBtn');
   1647             const err = document.getElementById('authError');
   1648             btn.classList.add('loading');
   1649             btn.disabled = true;
   1650             err.style.opacity = '0';
   1651 
   1652             try {
   1653                 const response = await fetch(`${RENDER_BACKEND_URL}/api/verify`, {
   1654                     method: 'POST',
   1655                     headers: {'Content-Type': 'application/json'},
   1656                     body: JSON.stringify({
   1657                         username: document.getElementById('username').value,
   1658                         password: document.getElementById('password').value
   1659                     })
   1660                 });
   1661                 const data = await response.json();
   1662                 if(data.success) {
   1663                     if(document.getElementById('rememberMe').checked) {
   1664                         localStorage.setItem('wp_auth_expiry', Date.now() + (3 * 24 * 60 * 60 * 1000));
   1665                     }
   1666                     revealApp();
   1667                 } else throw new Error();
   1668             } catch (e) {
   1669                 err.style.opacity = '1';
   1670                 btn.classList.remove('loading');
   1671                 btn.disabled = false;
   1672             }
   1673         }
   1674 
   1675         function revealApp() {
   1676             document.getElementById('auth-screen').style.display = 'none';
   1677             document.getElementById('app-layout').classList.add('visible');
   1678             loadChats();
   1679         }
   1680 
   1681         function openSettings() { document.getElementById('settingsModal').classList.add('active'); }
   1682         function closeModal(id) { document.getElementById(id).classList.remove('active'); }
   1683         function toggleTheme() {
   1684             const isDark = document.getElementById('darkModeToggle').checked;
   1685             document.body.setAttribute('data-theme', isDark ? 'dark' : 'light');
   1686             localStorage.setItem('theme', isDark ? 'dark' : 'light');
   1687         }
   1688         function changeFontSize(val) { document.documentElement.style.setProperty('--base-font-size', `${val}px`); }
   1689         function changeBg(id, save=true) {
   1690             document.querySelectorAll('.bg-option').forEach(el => el.classList.remove('selected'));
   1691             const colors = ['#EFE7DE', '#d1d7db', '#e5ddd5', '#ffffff'];
   1692             document.documentElement.style.setProperty('--chat-bg-color', colors[id-1]);
   1693             const option = document.querySelector(`.bg-${id}`);
   1694             if(option) option.classList.add('selected');
   1695             if(save) localStorage.setItem('chatBg', id);
   1696         }
   1697         function saveProfile() {
   1698             localStorage.setItem('profileName', document.getElementById('profileName').value);
   1699             localStorage.setItem('profilePhone', document.getElementById('profilePhone').value);
   1700         }
   1701         function populateFAQ() {
   1702             const faqs = [
   1703                 {q: "Is this secure?", a: "Yes, data lives in your private Firebase. No one else can access it."},
   1704                 {q: "Does it track deleted messages?", a: "Yes, once a message is received by the server, it is logged forever, even if the sender deletes it."},
   1705                 {q: "Does my phone need to be online?", a: "No. Once linked, your Render server handles logging even if your phone is powered off."},
   1706                 {q: "Can I reply from here?", a: "No, this is a read-only archive designed for logging and privacy, not active chatting."},
   1707                 {q: "What is the 'Pinger' for?", a: "It keeps your free Render server awake so it never misses a message due to inactivity."}
   1708             ];
   1709             const container = document.getElementById('faqContainer');
   1710             faqs.forEach(item => {
   1711                 const div = document.createElement('div');
   1712                 div.className = 'faq-item';
   1713                 div.innerHTML = `<button class="faq-question" onclick="this.parentElement.classList.toggle('open')">${item.q} <span class="material-symbols-rounded">expand_more</span></button><div class="faq-answer">${item.a}</div>`;
   1714                 container.appendChild(div);
   1715             });
   1716         }
   1717 
   1718         function toggleChatMenu(event) {
   1719             if(event) event.stopPropagation();
   1720             document.getElementById('chatMenu').classList.toggle('show');
   1721         }
   1722         
   1723         document.addEventListener('click', function(e) {
   1724             const menu = document.getElementById('chatMenu');
   1725             if(menu.classList.contains('show') && !e.target.closest('.menu-container')) {
   1726                 menu.classList.remove('show');
   1727             }
   1728         });
   1729 
   1730         function toggleChatSearch() {
   1731             document.getElementById('inChatSearch').classList.toggle('hidden');
   1732             document.getElementById('chatMenu').classList.remove('show');
   1733             document.getElementById('msgSearchInput').focus();
   1734         }
   1735         
   1736         document.getElementById('msgSearchInput').addEventListener('input', (e) => {
   1737             const term = e.target.value.toLowerCase();
   1738             const bubbles = document.querySelectorAll('.msg-bubble');
   1739             let count = 0;
   1740             document.querySelectorAll('.highlight').forEach(span => { const parent = span.parentNode; parent.innerHTML = parent.textContent; });
   1741 
   1742             bubbles.forEach(b => {
   1743                 const textDiv = b.querySelector('div:not(.msg-sender):not(.msg-meta)'); 
   1744                 if(textDiv) {
   1745                     const originalText = textDiv.textContent;
   1746                     if(term && originalText.toLowerCase().includes(term)) {
   1747                         b.style.display = 'flex';
   1748                         const regex = new RegExp(`(${term})`, 'gi');
   1749                         textDiv.innerHTML = originalText.replace(regex, '<span class="highlight">$1</span>');
   1750                         count++;
   1751                     } else {
   1752                         b.style.display = term ? 'none' : 'flex';
   1753                         textDiv.textContent = originalText;
   1754                     }
   1755                 }
   1756             });
   1757             document.getElementById('searchCount').innerText = term ? `${count} found` : '';
   1758         });
   1759 
   1760         async function loadChats() {
   1761             const list = document.getElementById('contactList');
   1762             list.innerHTML = '';
   1763             for(let i=0; i<5; i++) {
   1764                 list.innerHTML += `<div class="skel-contact fade-in"><div class="skeleton skel-avatar"></div><div class="skel-text-col"><div class="skeleton" style="width:60%; height:16px;"></div><div class="skeleton" style="width:40%; height:12px;"></div></div></div>`;
   1765             }
   1766 
   1767             try {
   1768                 const snap = await db.collection('Chats').get();
   1769                 const groups = {};
   1770                 const excluded = ['917278779512@s.whatsapp.net', '201554426618024@lid'];
   1771 
   1772                 snap.forEach(doc => {
   1773                     const d = doc.data();
   1774                     if (!d.lastActive || excluded.includes(doc.id)) return;
   1775                     const phone = d.phoneNumber || doc.id.split('@')[0];
   1776                     if (!groups[phone]) groups[phone] = { main: {id:doc.id, ...d}, ids: [doc.id] };
   1777                     else {
   1778                         groups[phone].ids.push(doc.id);
   1779                         if (d.lastActive > groups[phone].main.lastActive) groups[phone].main = {id:doc.id, ...d};
   1780                     }
   1781                 });
   1782 
   1783                 const chats = Object.values(groups).map(g => ({...g.main, ids: g.ids})).sort((a,b) => b.lastActive - a.lastActive);
   1784                 
   1785                 list.innerHTML = '';
   1786                 if(chats.length === 0) {
   1787                     list.innerHTML = `<div style="text-align:center; padding:32px; color:var(--md-sys-color-outline);">No chats found</div>`;
   1788                     return;
   1789                 }
   1790 
   1791                 chats.forEach(c => {
   1792                     const el = document.createElement('div');
   1793                     el.className = 'contact-item';
   1794                     const name = c.customName || c.displayName || c.phoneNumber || c.id;
   1795                     const isGroup = c.id.includes('@g.us');
   1796                     const avatarContent = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase();
   1797 
   1798                     el.innerHTML = `<div class="avatar">${avatarContent}</div><div class="contact-info"><div class="contact-name">${name}</div><div class="contact-sub">${c.phoneNumber || c.id}</div></div>`;
   1799                     el.onclick = () => openChat(c.ids, name, el, isGroup, c.phoneNumber || c.id);
   1800                     list.appendChild(el);
   1801                 });
   1802             } catch (err) { console.error(err); }
   1803         }
   1804 
   1805         function openChat(ids, name, el, isGroup, displayId) {
   1806             activeChatId = ids[0];
   1807             isCurrentChatGroup = isGroup;
   1808             document.getElementById('headerName').innerText = name;
   1809             document.getElementById('chatHeaderContent').style.opacity = '1';
   1810             document.getElementById('headerId').innerText = displayId;
   1811             document.getElementById('headerAvatar').innerHTML = isGroup ? `<span class="material-symbols-rounded">group</span>` : name[0].toUpperCase();
   1812 
   1813             if (window.matchMedia('(max-width: 768px)').matches) {
   1814                 window.history.pushState({chatOpen: true}, "");
   1815                 document.getElementById('sidebar').classList.add('inactive');
   1816                 document.getElementById('chatArea').classList.add('active');
   1817             }
   1818 
   1819             if(window.unsub) window.unsub.forEach(u => u());
   1820             window.unsub = [];
   1821             
   1822             const container = document.getElementById('messagesContainer');
   1823             container.innerHTML = `<div class="skel-msg-group"><div class="skeleton skel-bubble skel-left"></div><div class="skeleton skel-bubble skel-right" style="width:60%"></div><div class="skeleton skel-bubble skel-left" style="width:30%"></div></div>`;
   1824             
   1825             const msgs = new Map();
   1826             ids.forEach(id => {
   1827                 window.unsub.push(db.collection('Chats').doc(id).collection('Messages').orderBy('timestamp').onSnapshot(snap => {
   1828                     snap.forEach(d => msgs.set(d.id, d.data()));
   1829                     renderMsgs(msgs);
   1830                 }));
   1831             });
   1832         }
   1833 
   1834         // --- UPDATED RENDER MESSAGES (Date Groups for Sticky Header Fix) ---
   1835         function renderMsgs(map) {
   1836             const container = document.getElementById('messagesContainer');
   1837             container.innerHTML = '';
   1838             currentMessages = Array.from(map.values()).sort((a,b) => a.timestamp - b.timestamp);
   1839             
   1840             if(currentMessages.length === 0) {
   1841                 container.innerHTML = '<div class="empty-state"><p>No messages recorded</p></div>';
   1842                 return;
   1843             }
   1844 
   1845             let lastDateStr = "";
   1846             let currentGroup = null;
   1847 
   1848             currentMessages.forEach((m, index) => {
   1849                 const date = new Date(m.timestamp * 1000);
   1850                 const dateStr = date.toDateString();
   1851 
   1852                 // Start New Date Group if date changed
   1853                 if(dateStr !== lastDateStr) {
   1854                     // Close previous group if exists
   1855                     if(currentGroup) container.appendChild(currentGroup);
   1856                     
   1857                     // Create new group container
   1858                     currentGroup = document.createElement('div');
   1859                     currentGroup.className = 'chat-date-group';
   1860                     
   1861                     // Add Sticky Divider inside group
   1862                     const div = document.createElement('div');
   1863                     div.className = 'date-divider';
   1864                     div.innerText = formatDividerDate(m.timestamp);
   1865                     currentGroup.appendChild(div);
   1866                     
   1867                     lastDateStr = dateStr;
   1868                 }
   1869 
   1870                 // 2. GROUPING LOGIC (Using previous logic but appending to group)
   1871                 const div = document.createElement('div');
   1872                 div.className = `msg-bubble ${m.fromMe ? 'msg-out' : 'msg-in'}`;
   1873                 
   1874                 const nextMsg = currentMessages[index + 1];
   1875                 const prevMsg = currentMessages[index - 1];
   1876                 
   1877                 const isSamePrev = (prevMsg && prevMsg.fromMe === m.fromMe && (!isCurrentChatGroup || prevMsg.senderName === m.senderName) && new Date(prevMsg.timestamp*1000).toDateString() === dateStr);
   1878                 const isSameNext = (nextMsg && nextMsg.fromMe === m.fromMe && (!isCurrentChatGroup || nextMsg.senderName === m.senderName) && new Date(nextMsg.timestamp*1000).toDateString() === dateStr);
   1879 
   1880                 if (isSamePrev && isSameNext) div.classList.add('group-mid');
   1881                 else if (isSamePrev) div.classList.add('group-bot');
   1882                 else if (isSameNext) div.classList.add('group-top');
   1883                 
   1884                 const showSenderName = !m.fromMe && isCurrentChatGroup && m.senderName && m.senderName !== "Unknown" && !isSamePrev; 
   1885                 
   1886                 div.id = `msg-${m.timestamp}`;
   1887                 div.dataset.date = dateStr;
   1888 
   1889                 div.innerHTML = `
   1890                     ${showSenderName ? `<div class="msg-sender">${m.senderName}</div>` : ''}
   1891                     <div>${m.text}</div>
   1892                     <div class="msg-meta">${date.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}</div>
   1893                 `;
   1894                 
   1895                 // Append message to current GROUP
   1896                 if (currentGroup) currentGroup.appendChild(div);
   1897             });
   1898 
   1899             // Append the final group
   1900             if(currentGroup) container.appendChild(currentGroup);
   1901             
   1902             // Force scroll to bottom on every update/load (now instant)
   1903             setTimeout(() => scrollToBottom('auto'), 50);
   1904         }
   1905 
   1906         // --- SCROLL TO BOTTOM ---
   1907         function scrollToBottom(behavior = 'auto') {
   1908             const c = document.getElementById('messagesContainer');
   1909             c.scrollTo({ top: c.scrollHeight, behavior: behavior });
   1910         }
   1911 
   1912         // --- CALENDAR LOGIC ---
   1913         function openDateSearch() {
   1914             document.getElementById('chatMenu').classList.remove('show');
   1915             document.getElementById('calendarModal').classList.add('active');
   1916             renderCalendar(calCurrentDate);
   1917         }
   1918 
   1919         function renderCalendar(date) {
   1920             const year = date.getFullYear();
   1921             const month = date.getMonth();
   1922             
   1923             // Header
   1924             document.getElementById('calMonthYear').innerText = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(date);
   1925             
   1926             // Grid
   1927             const firstDay = new Date(year, month, 1).getDay();
   1928             const daysInMonth = new Date(year, month + 1, 0).getDate();
   1929             const grid = document.getElementById('calDatesGrid');
   1930             grid.innerHTML = '';
   1931 
   1932             // Empty slots for prev month
   1933             for(let i=0; i<firstDay; i++) {
   1934                 grid.innerHTML += `<div class="cal-date empty"></div>`;
   1935             }
   1936 
   1937             // Days
   1938             for(let d=1; d<=daysInMonth; d++) {
   1939                 const dayEl = document.createElement('div');
   1940                 dayEl.className = 'cal-date ripple-surface';
   1941                 dayEl.innerText = d;
   1942                 
   1943                 // Check if selected
   1944                 if(calSelectedDate && calSelectedDate.getDate() === d && calSelectedDate.getMonth() === month && calSelectedDate.getFullYear() === year) {
   1945                     dayEl.classList.add('active');
   1946                 }
   1947                 
   1948                 dayEl.onclick = () => selectDate(new Date(year, month, d));
   1949                 grid.appendChild(dayEl);
   1950             }
   1951         }
   1952 
   1953         function changeCalMonth(delta) {
   1954             calCurrentDate.setMonth(calCurrentDate.getMonth() + delta);
   1955             renderCalendar(calCurrentDate);
   1956         }
   1957 
   1958         function selectDate(date) {
   1959             calSelectedDate = date;
   1960             document.getElementById('calSelectedDateStr').innerText = date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
   1961             renderCalendar(calCurrentDate); // Re-render to show active state
   1962         }
   1963 
   1964         function jumpToDate() {
   1965             if(!currentMessages.length) {
   1966                 closeModal('calendarModal');
   1967                 return;
   1968             }
   1969             
   1970             // Find first message >= selected date
   1971             // Set time to 00:00:00 for comparison
   1972             const targetTs = calSelectedDate.setHours(0,0,0,0) / 1000;
   1973             const targetDateStr = calSelectedDate.toDateString();
   1974 
   1975             // Try to find exact match or next closest
   1976             const msg = currentMessages.find(m => m.timestamp >= targetTs);
   1977             
   1978             closeModal('calendarModal');
   1979 
   1980             if(msg) {
   1981                 const el = document.getElementById(`msg-${msg.timestamp}`);
   1982                 if(el) {
   1983                     el.scrollIntoView({behavior: 'smooth', block: 'center'});
   1984                     el.style.backgroundColor = 'var(--search-highlight)'; // Flash highlight
   1985                     setTimeout(() => el.style.backgroundColor = '', 2000);
   1986                 }
   1987             } else {
   1988                 document.getElementById('notFoundModal').classList.add('active');
   1989             }
   1990         }
   1991 
   1992         function handleLogout() {
   1993             localStorage.removeItem('wp_auth_expiry');
   1994             localStorage.removeItem('wp_app_pin');
   1995             localStorage.removeItem('wp_app_bio');
   1996             location.reload();
   1997         }
   1998         
   1999         function closeChatMobile() {
   2000             window.history.back();
   2001         }
   2002         
   2003         window.addEventListener('popstate', () => {
   2004             document.getElementById('chatArea').classList.remove('remove');
   2005             document.getElementById('chatArea').classList.remove('active');
   2006         });
   2007         
   2008         function downloadChat() {
   2009             if(!currentMessages.length) return;
   2010             const text = currentMessages.map(m => `[${new Date(m.timestamp*1000).toLocaleString()}] ${m.senderName}: ${m.text}`).join('\n');
   2011             const blob = new Blob([text], {type:'text/plain'});
   2012             const a = document.createElement('a');
   2013             a.href = URL.createObjectURL(blob);
   2014             a.download = 'chat.txt';
   2015             a.click();
   2016         }
   2017         
   2018         function showRenameModal() {
   2019             if(!activeChatId) return;
   2020             document.getElementById('renameModal').classList.add('active');
   2021             document.getElementById('renameInput').value = document.getElementById('headerName').innerText;
   2022             document.getElementById('chatMenu').classList.remove('show');
   2023         }
   2024         
   2025         async function saveRename() {
   2026             const name = document.getElementById('renameInput').value;
   2027             if(name) {
   2028                 await db.collection('Chats').doc(activeChatId).set({customName: name}, {merge:true});
   2029                 document.getElementById('headerName').innerText = name;
   2030                 closeModal('renameModal');
   2031                 loadChats();
   2032             }
   2033         }
   2034         
   2035         function showLogoutModal() {
   2036             document.getElementById('logoutModal').classList.add('active');
   2037         }
   2038 
   2039         document.getElementById('searchInput').addEventListener('input', (e) => {
   2040             const val = e.target.value.toLowerCase();
   2041             document.querySelectorAll('.contact-item').forEach(el => {
   2042                 const name = el.innerText.toLowerCase();
   2043                 el.style.display = name.includes(val) ? 'flex' : 'none';
   2044             });
   2045         });
   2046 
   2047         function promptAddChat() {
   2048             const id = prompt("Enter WhatsApp ID (e.g. 12345@s.whatsapp.net)");
   2049             if(id) {
   2050                 db.collection('Chats').doc(id).set({displayName:'Manual Add', id:id, lastActive: Math.floor(Date.now()/1000)}, {merge:true});
   2051                 setTimeout(loadChats, 1000);
   2052             }
   2053         }
   2054 
   2055         async function refreshApp() {
   2056             const btn = document.querySelector('.icon-btn span[class*="refresh"]');
   2057             if(btn) btn.style.animation = 'spin 1s infinite linear';
   2058             if ('caches' in window) {
   2059                 try {
   2060                     const keys = await caches.keys();
   2061                     await Promise.all(keys.map(key => caches.delete(key)));
   2062                 } catch(e) { console.log("Cache clear error", e); }
   2063             }
   2064             if ('serviceWorker' in navigator) {
   2065                 const registrations = await navigator.serviceWorker.getRegistrations();
   2066                 for (let registration of registrations) {
   2067                     await registration.unregister();
   2068                 }
   2069             }
   2070             window.location.reload();
   2071         }
   2072     </script>
   2073 </body>
   2074 </html>
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror