Login Page in collabration with @REPLIT

 

AI login

<!DOCTYPE html>

<html lang="en">

  <head>

    <meta charset="UTF-8" />

    <meta

      name="viewport"

      content="width=device-width, initial-scale=1.0, maximum-scale=1"

    />

    <meta

      name="description"

      content="Lumen is a quiet, intelligent workspace where focused professionals come to do their best work."

    />

    <title>Lumen | Sign In</title>

    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />

    <link rel="preconnect" href="https://fonts.googleapis.com" />

    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

    <link

      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"

      rel="stylesheet"

    />

    <script type="importmap">

      {

        "imports": {

          "three": "https://unpkg.com/three@0.160.0/build/three.module.js"

        }

      }

    </script>

    <style>

      :root {

        --bg: #f3f7f6;

        --bg-grad-1: rgba(17, 94, 89, 0.10);

        --bg-grad-2: rgba(13, 148, 136, 0.08);

        --card: rgba(255, 255, 255, 0.65);

        --card-border: rgba(15, 75, 70, 0.10);

        --text: #0f1f1d;

        --text-muted: #5b6b69;

        --primary: #0d6e64;

        --primary-hover: #0a5953;

        --primary-text: #ffffff;

        --input-bg: rgba(255, 255, 255, 0.7);

        --input-border: rgba(15, 75, 70, 0.18);

        --input-border-focus: #0d6e64;

        --face-fill: #ffffff;

        --face-stroke: #d8e3e1;

        --hair: #0d6e64;

        --eye: #1c2826;

        --hand-fill: #fde7d3;

        --hand-stroke: #c9a886;

        --blush: #ffb3a8;

        --error: #b54848;

        --shadow: 0 30px 80px -20px rgba(15, 75, 70, 0.25),

          0 10px 30px -10px rgba(15, 75, 70, 0.15);

        --toggle-bg: rgba(255, 255, 255, 0.7);

        --divider: rgba(15, 75, 70, 0.12);

        --three-color: #0d6e64;

      }

      html.dark {

        --bg: #06100f;

        --bg-grad-1: rgba(45, 212, 191, 0.18);

        --bg-grad-2: rgba(34, 197, 184, 0.10);

        --card: rgba(15, 30, 28, 0.55);

        --card-border: rgba(94, 234, 212, 0.12);

        --text: #e6f3f1;

        --text-muted: #8aa3a0;

        --primary: #2dd4bf;

        --primary-hover: #5eead4;

        --primary-text: #052622;

        --input-bg: rgba(7, 22, 20, 0.55);

        --input-border: rgba(94, 234, 212, 0.18);

        --input-border-focus: #2dd4bf;

        --face-fill: #e6f3f1;

        --face-stroke: #2dd4bf;

        --hair: #2dd4bf;

        --eye: #06100f;

        --hand-fill: #f5d4b3;

        --hand-stroke: #b08864;

        --blush: #ff8d80;

        --error: #ff8a8a;

        --shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.6),

          0 10px 30px -10px rgba(0, 0, 0, 0.4);

        --toggle-bg: rgba(15, 30, 28, 0.7);

        --divider: rgba(94, 234, 212, 0.12);

        --three-color: #2dd4bf;

      }

      * { box-sizing: border-box; }

      html, body {

        margin: 0; padding: 0; height: 100%;

        font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;

        background: var(--bg); color: var(--text);

        transition: background 0.5s ease, color 0.5s ease;

        -webkit-font-smoothing: antialiased; overflow: hidden;

      }

      #bg-canvas { position: fixed; inset: 0; z-index: 0; pointer-events: none; }

      .bg-fallback {

        position: fixed; inset: 0; z-index: 0; pointer-events: none;

        background:

          radial-gradient(ellipse at 20% 20%, var(--bg-grad-1), transparent 55%),

          radial-gradient(ellipse at 80% 75%, var(--bg-grad-2), transparent 55%);

      }

      .layout {

        position: relative; z-index: 1; min-height: 100vh;

        display: flex; align-items: center; justify-content: center;

        padding: 24px; overflow-y: auto;

      }

      .stack {

        display: flex; flex-direction: column; align-items: center;

        gap: 0; width: 100%; max-width: 420px;

      }

      .avatar-wrap {

        position: relative; width: 160px; height: 160px;

        margin-bottom: -36px; z-index: 2;

        filter: drop-shadow(0 18px 30px rgba(0, 0, 0, 0.18));

        animation: avatarIntro 0.8s ease-out backwards;

      }

      @keyframes avatarIntro {

        from { opacity: 0; transform: translateY(-12px) scale(0.85); }

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

      }

      .avatar-wrap svg { width: 100%; height: 100%; overflow: visible; }

      .card {

        width: 100%; background: var(--card);

        backdrop-filter: blur(28px) saturate(140%);

        -webkit-backdrop-filter: blur(28px) saturate(140%);

        border: 1px solid var(--card-border);

        border-radius: 24px; padding: 56px 36px 32px;

        box-shadow: var(--shadow);

        animation: cardIntro 0.8s 0.1s ease-out backwards;

      }

      @keyframes cardIntro {

        from { opacity: 0; transform: translateY(20px); }

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

      }

      .card h1 { margin: 0; font-size: 26px; font-weight: 600; text-align: center; letter-spacing: -0.01em; }

      .card .subtitle { margin: 8px 0 28px; text-align: center; color: var(--text-muted); font-size: 14px; }

      .field { margin-bottom: 16px; }

      .field-label-row { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }

      .field label { font-size: 13px; font-weight: 500; color: var(--text); }

      .field a.link { font-size: 12px; color: var(--primary); text-decoration: none; font-weight: 500; }

      .field a.link:hover { text-decoration: underline; }

      .input-wrap { position: relative; }

      .field input[type="email"], .field input[type="password"], .field input[type="text"] {

        width: 100%; height: 44px; padding: 0 14px;

        font-family: inherit; font-size: 14px; color: var(--text);

        background: var(--input-bg); border: 1px solid var(--input-border);

        border-radius: 10px; outline: none;

        transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;

      }

      .field input::placeholder { color: var(--text-muted); }

      .field input:focus { border-color: var(--input-border-focus); box-shadow: 0 0 0 3px rgba(13, 110, 100, 0.12); }

      html.dark .field input:focus { box-shadow: 0 0 0 3px rgba(45, 212, 191, 0.18); }

      .pwd-toggle {

        position: absolute; right: 8px; top: 50%; transform: translateY(-50%);

        width: 32px; height: 32px;

        display: flex; align-items: center; justify-content: center;

        border: none; background: transparent; color: var(--text-muted);

        cursor: pointer; border-radius: 6px;

      }

      .pwd-toggle:hover { color: var(--text); background: rgba(0, 0, 0, 0.04); }

      html.dark .pwd-toggle:hover { background: rgba(255, 255, 255, 0.06); }

      .field-error { font-size: 12px; color: var(--error); margin-top: 6px; min-height: 14px; }

      .row-checkbox {

        display: flex; align-items: center; gap: 8px;

        margin: 4px 0 22px; font-size: 13px; color: var(--text-muted);

      }

      .row-checkbox input { accent-color: var(--primary); width: 14px; height: 14px; }

      .row-checkbox label { cursor: pointer; }

      .btn-primary {

        width: 100%; height: 46px;

        background: var(--primary); color: var(--primary-text);

        border: none; border-radius: 10px;

        font-family: inherit; font-size: 14px; font-weight: 600;

        cursor: pointer; transition: background 0.2s ease, transform 0.06s ease;

        display: flex; align-items: center; justify-content: center; gap: 8px;

      }

      .btn-primary:hover { background: var(--primary-hover); }

      .btn-primary:active { transform: scale(0.99); }

      .btn-primary:disabled { opacity: 0.7; cursor: progress; }

      .spinner {

        width: 16px; height: 16px;

        border: 2px solid rgba(255, 255, 255, 0.4);

        border-top-color: var(--primary-text);

        border-radius: 50%; animation: spin 0.7s linear infinite;

      }

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

      .divider {

        display: flex; align-items: center; gap: 12px;

        margin: 20px 0; font-size: 11px; color: var(--text-muted);

        letter-spacing: 0.08em; text-transform: uppercase;

      }

      .divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--divider); }

      .sso-row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }

      .sso-btn {

        height: 44px; background: var(--input-bg);

        border: 1px solid var(--input-border); border-radius: 10px;

        cursor: pointer; display: flex; align-items: center; justify-content: center;

        color: var(--text); transition: background 0.15s ease, transform 0.06s ease;

      }

      .sso-btn:hover { background: rgba(0, 0, 0, 0.03); }

      html.dark .sso-btn:hover { background: rgba(255, 255, 255, 0.04); }

      .sso-btn svg { width: 18px; height: 18px; }

      .footer-text { margin-top: 22px; text-align: center; font-size: 13px; color: var(--text-muted); }

      .footer-text a { color: var(--primary); text-decoration: none; font-weight: 500; }

      .footer-text a:hover { text-decoration: underline; }

      .theme-toggle {

        position: fixed; top: 20px; right: 20px; z-index: 10;

        width: 40px; height: 40px; border-radius: 50%;

        background: var(--toggle-bg); border: 1px solid var(--card-border);

        color: var(--text); cursor: pointer;

        display: flex; align-items: center; justify-content: center;

        backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);

        transition: background 0.2s ease;

      }

      .theme-toggle:hover { background: var(--input-bg); }

      .theme-toggle svg { width: 18px; height: 18px; }

      .success-banner {

        position: absolute; inset: 0;

        background: var(--card);

        backdrop-filter: blur(28px); -webkit-backdrop-filter: blur(28px);

        border-radius: 24px;

        display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 8px;

        opacity: 0; pointer-events: none; transition: opacity 0.4s ease;

        font-weight: 600; font-size: 18px;

      }

      .success-banner.show { opacity: 1; }

      .success-banner small { font-weight: 400; font-size: 13px; color: var(--text-muted); }

      .card-wrap { position: relative; width: 100%; }


      /* Avatar parts */

      .face-circle { fill: var(--face-fill); stroke: var(--face-stroke); stroke-width: 2; transition: fill 0.3s ease, stroke 0.3s ease; }

      .hair { fill: var(--hair); transition: fill 0.3s ease; }

      .eye-pupil {

        fill: var(--eye);

        transition: cx 0.25s cubic-bezier(0.34, 1.56, 0.64, 1),

                    cy 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), fill 0.3s ease;

      }

      .eye-white { fill: var(--face-fill); stroke: var(--eye); stroke-width: 1.2; transition: fill 0.3s ease, stroke 0.3s ease; }

      .eyelid {

        fill: var(--face-fill); stroke: var(--eye); stroke-width: 2; stroke-linecap: round;

        transform-origin: center; transform: scaleY(0); transform-box: fill-box;

        transition: transform 0.18s ease;

      }

      .closed .eyelid { transform: scaleY(1); }

      .blink { animation: blink 5s infinite; }

      .closed .blink { animation: none; }

      @keyframes blink {

        0%, 92%, 100% { transform: scaleY(0); }

        94%, 96%      { transform: scaleY(1); }

      }

      .blush { fill: var(--blush); opacity: 0; transition: opacity 0.3s ease; }

      .closed .blush, .smiling .blush { opacity: 0.55; }

      .mouth { fill: none; stroke: var(--eye); stroke-width: 2.6; stroke-linecap: round; transition: d 0.3s ease; }

      .hands {

        opacity: 0; transform: translateY(40px); transform-origin: 80px 130px;

        transition: opacity 0.32s ease, transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);

      }

      .closed .hands { opacity: 1; transform: translateY(0); }

      .hand { fill: var(--hand-fill); stroke: var(--hand-stroke); stroke-width: 1.4; stroke-linejoin: round; stroke-linecap: round; }

      .head-group { transform-origin: 80px 110px; transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); }

      .tilted .head-group { transform: rotate(-5deg); }

      .sparkle { opacity: 0; transition: opacity 0.3s ease; }

      .successful .sparkle { opacity: 1; }

      .visually-hidden {

        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;

        overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;

      }

    </style>

  </head>

  <body>

    <canvas id="bg-canvas"></canvas>

    <div class="bg-fallback" id="bg-fallback" style="display:none"></div>


    <button class="theme-toggle" id="themeToggle" type="button" aria-label="Toggle theme">

      <svg id="themeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">

        <circle cx="12" cy="12" r="4"></circle>

        <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path>

      </svg>

    </button>


    <main class="layout">

      <div class="stack">

        <div class="avatar-wrap" id="avatar">

          <svg viewBox="0 0 160 160" aria-hidden="true">

            <defs>

              <clipPath id="faceClip"><circle cx="80" cy="80" r="60" /></clipPath>

            </defs>

            <g class="head-group">

              <circle class="face-circle" cx="80" cy="80" r="60" />

              <g clip-path="url(#faceClip)">

                <path class="hair" d="M 18 70 C 28 22, 132 22, 142 70 C 132 36, 28 36, 18 70 Z" />

                <path class="hair" d="M 22 78 C 24 60, 36 50, 50 50 L 50 78 Z" opacity="0.85" />

                <path class="hair" d="M 138 78 C 136 60, 124 50, 110 50 L 110 78 Z" opacity="0.85" />

              </g>

              <ellipse class="face-circle" cx="22" cy="86" rx="6" ry="9" />

              <ellipse class="face-circle" cx="138" cy="86" rx="6" ry="9" />

              <ellipse class="blush" cx="50" cy="98" rx="8" ry="5" />

              <ellipse class="blush" cx="110" cy="98" rx="8" ry="5" />

              <g id="eyesGroup">

                <ellipse class="eye-white" cx="60" cy="82" rx="9" ry="11" />

                <ellipse class="eye-white" cx="100" cy="82" rx="9" ry="11" />

                <circle class="eye-pupil" id="leftPupil"  cx="60"  cy="82" r="4.5" />

                <circle class="eye-pupil" id="rightPupil" cx="100" cy="82" r="4.5" />

                <circle id="leftHL"  class="eye-highlight" cx="61.4"  cy="80.6" r="1.4" fill="white" opacity="0.9" />

                <circle id="rightHL" class="eye-highlight" cx="101.4" cy="80.6" r="1.4" fill="white" opacity="0.9" />

                <g class="sparkle">

                  <path d="M 50 70 l 2 2 l -2 2 l -2 -2 z" fill="#ffd166" />

                  <path d="M 110 70 l 2 2 l -2 2 l -2 -2 z" fill="#ffd166" />

                </g>

              </g>

              <path class="eyelid" d="M 51 82 Q 60 76 69 82" />

              <path class="eyelid" d="M 91 82 Q 100 76 109 82" />

              <path d="M 51 67 Q 60 63 69 67" stroke="var(--eye)" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.85" />

              <path d="M 91 67 Q 100 63 109 67" stroke="var(--eye)" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.85" />

              <path id="mouth" class="mouth" d="M 70 110 Q 80 112 90 110" />

              <g class="hands" id="hands">

                <g transform="translate(0,0)">

                  <path class="hand" d="M 38 100 Q 36 84 46 78 Q 58 74 70 80 L 72 96 Q 70 108 60 112 Q 46 114 38 110 Z" />

                  <path class="hand" d="M 44 86 Q 44 76 48 76 Q 52 76 52 86 L 52 92 Z" />

                  <path class="hand" d="M 53 84 Q 53 72 57 72 Q 61 72 61 84 L 61 92 Z" />

                  <path class="hand" d="M 62 84 Q 62 74 66 74 Q 70 74 70 84 L 70 92 Z" />

                  <path class="hand" d="M 71 88 Q 71 80 75 80 Q 79 80 79 88 L 79 96 Z" opacity="0.95" />

                </g>

                <g transform="translate(160,0) scale(-1,1)">

                  <path class="hand" d="M 38 100 Q 36 84 46 78 Q 58 74 70 80 L 72 96 Q 70 108 60 112 Q 46 114 38 110 Z" />

                  <path class="hand" d="M 44 86 Q 44 76 48 76 Q 52 76 52 86 L 52 92 Z" />

                  <path class="hand" d="M 53 84 Q 53 72 57 72 Q 61 72 61 84 L 61 92 Z" />

                  <path class="hand" d="M 62 84 Q 62 74 66 74 Q 70 74 70 84 L 70 92 Z" />

                  <path class="hand" d="M 71 88 Q 71 80 75 80 Q 79 80 79 88 L 79 96 Z" opacity="0.95" />

                </g>

              </g>

            </g>

          </svg>

        </div>


        <div class="card-wrap">

          <div class="card">

            <h1>Sign in to Lumen</h1>

            <p class="subtitle">Welcome back to your workspace</p>


            <form id="loginForm" novalidate>

              <div class="field">

                <div class="field-label-row"><label for="email">Email</label></div>

                <div class="input-wrap">

                  <input id="email" name="email" type="email" placeholder="name@company.com" autocomplete="email" required />

                </div>

                <div class="field-error" id="emailError"></div>

              </div>


              <div class="field">

                <div class="field-label-row">

                  <label for="password">Password</label>

                  <a class="link" href="#">Forgot password?</a>

                </div>

                <div class="input-wrap">

                  <input id="password" name="password" type="password" placeholder="••••••••" autocomplete="current-password" required />

                  <button class="pwd-toggle" type="button" id="pwdToggle" aria-label="Show password">

                    <svg id="pwdIconOpen" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">

                      <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>

                      <circle cx="12" cy="12" r="3"></circle>

                    </svg>

                    <svg id="pwdIconOff" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none">

                      <path d="M17.94 17.94A10.94 10.94 0 0 1 12 20c-7 0-11-8-11-8a19.5 19.5 0 0 1 4.22-5.22"></path>

                      <path d="M9.9 4.24A10.94 10.94 0 0 1 12 4c7 0 11 8 11 8a19.5 19.5 0 0 1-3.17 4.31"></path>

                      <path d="M14.12 14.12A3 3 0 1 1 9.88 9.88"></path>

                      <line x1="1" y1="1" x2="23" y2="23"></line>

                    </svg>

                  </button>

                </div>

                <div class="field-error" id="passwordError"></div>

              </div>


              <div class="row-checkbox">

                <input id="remember" type="checkbox" />

                <label for="remember">Remember me for 30 days</label>

              </div>


              <button class="btn-primary" type="submit" id="submitBtn">

                <span id="submitLabel">Sign in</span>

              </button>


              <div class="divider">Or continue with</div>


              <div class="sso-row">

                <button class="sso-btn" type="button" aria-label="Sign in with Google">

                  <svg viewBox="0 0 24 24">

                    <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>

                    <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.99.66-2.25 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>

                    <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/>

                    <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>

                  </svg>

                </button>

                <button class="sso-btn" type="button" aria-label="Sign in with GitHub">

                  <svg viewBox="0 0 24 24" fill="currentColor">

                    <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56 0-.27-.01-1.18-.02-2.13-3.2.69-3.87-1.36-3.87-1.36-.52-1.33-1.27-1.69-1.27-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.75 2.68 1.24 3.34.95.1-.74.4-1.24.72-1.53-2.55-.29-5.24-1.27-5.24-5.66 0-1.25.45-2.27 1.18-3.07-.12-.29-.51-1.46.11-3.04 0 0 .96-.31 3.15 1.17.91-.25 1.89-.38 2.86-.39.97.01 1.95.14 2.86.39 2.18-1.48 3.14-1.17 3.14-1.17.62 1.58.23 2.75.11 3.04.74.8 1.18 1.82 1.18 3.07 0 4.4-2.69 5.36-5.25 5.65.41.36.78 1.06.78 2.14 0 1.55-.01 2.79-.01 3.17 0 .31.21.67.8.56C20.21 21.39 23.5 17.07 23.5 12 23.5 5.65 18.35.5 12 .5z"/>

                  </svg>

                </button>

                <button class="sso-btn" type="button" aria-label="Sign in with Apple">

                  <svg viewBox="0 0 24 24" fill="currentColor">

                    <path d="M16.365 1.43c0 1.14-.493 2.27-1.177 3.08-.744.9-1.99 1.57-2.987 1.57-.12 0-.23-.02-.3-.03-.01-.06-.04-.22-.04-.39 0-1.15.572-2.27 1.206-2.98.804-.94 2.142-1.64 3.248-1.68.03.13.05.28.05.43zM21.97 17.4c-.55 1.27-1.32 2.51-2.5 3.45-.92.74-1.68 1.05-2.71 1.05-1.13 0-1.84-.4-2.83-.4-1.04 0-1.84.43-2.92.43-1.06 0-1.96-.6-2.93-1.45-2.32-2.04-3.92-5.94-2.42-8.84.86-1.65 2.62-2.7 4.32-2.73 1.14-.02 2.21.78 2.86.78.66 0 1.96-.96 3.41-.82.6.03 2.41.24 3.59 1.84-3.21 1.86-2.7 6.43.06 7.69z"/>

                  </svg>

                </button>

              </div>


              <p class="footer-text">Don't have an account? <a href="#">Sign up</a></p>

            </form>

          </div>


          <div class="success-banner" id="successBanner">

            Welcome back

            <small>Redirecting to your workspace…</small>

          </div>

        </div>

      </div>

    </main>


    <script type="module">

      import * as THREE from "three";


      // ---------- Theme handling ----------

      const root = document.documentElement;

      const themeIcon = document.getElementById("themeIcon");

      const themeBtn = document.getElementById("themeToggle");


      const SUN_PATH = `<circle cx="12" cy="12" r="4"></circle>

        <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"></path>`;

      const MOON_PATH = `<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>`;


      const stored = localStorage.getItem("lumen-theme");

      let manualOverride = stored === "light" || stored === "dark";

      let theme = stored || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");

      applyTheme(theme);


      function applyTheme(t) {

        if (t === "dark") { root.classList.add("dark"); themeIcon.innerHTML = SUN_PATH; }

        else { root.classList.remove("dark"); themeIcon.innerHTML = MOON_PATH; }

        theme = t;

        if (window.__updateThreeTheme) window.__updateThreeTheme(t);

      }


      themeBtn.addEventListener("click", () => {

        const next = root.classList.contains("dark") ? "light" : "dark";

        manualOverride = true;

        localStorage.setItem("lumen-theme", next);

        applyTheme(next);

      });


      window.matchMedia("(prefers-color-scheme: dark)")

        .addEventListener("change", (e) => {

          if (manualOverride) return;

          applyTheme(e.matches ? "dark" : "light");

        });


      // ---------- 3D Background ----------

      function initThree() {

        const canvas = document.getElementById("bg-canvas");

        let renderer;

        try {

          renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });

        } catch (e) {

          document.getElementById("bg-fallback").style.display = "block";

          canvas.style.display = "none";

          return;

        }

        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));


        const scene = new THREE.Scene();

        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);

        camera.position.z = 5;


        const colorFromTheme = (t) => new THREE.Color(t === "dark" ? "#2dd4bf" : "#0d6e64");


        const PARTICLE_COUNT = 1800;

        const positions = new Float32Array(PARTICLE_COUNT * 3);

        for (let i = 0; i < PARTICLE_COUNT; i++) {

          positions[i * 3]     = (Math.random() - 0.5) * 22;

          positions[i * 3 + 1] = (Math.random() - 0.5) * 22;

          positions[i * 3 + 2] = (Math.random() - 0.5) * 22;

        }

        const pGeom = new THREE.BufferGeometry();

        pGeom.setAttribute("position", new THREE.BufferAttribute(positions, 3));

        const pMat = new THREE.PointsMaterial({

          size: 0.04, color: colorFromTheme(theme),

          transparent: true, opacity: 0.55, sizeAttenuation: true,

        });

        const points = new THREE.Points(pGeom, pMat);

        scene.add(points);


        const wireMat = new THREE.MeshBasicMaterial({

          color: colorFromTheme(theme),

          wireframe: true, transparent: true, opacity: 0.18,

        });

        const torusKnot = new THREE.Mesh(new THREE.TorusKnotGeometry(1.4, 0.38, 128, 16), wireMat);

        torusKnot.position.set(-2, 0, -4); scene.add(torusKnot);


        const ico = new THREE.Mesh(new THREE.IcosahedronGeometry(1.8, 0), wireMat.clone());

        ico.position.set(2.6, 1.6, -6); scene.add(ico);


        const torus = new THREE.Mesh(new THREE.TorusGeometry(1.1, 0.28, 16, 64), wireMat.clone());

        torus.position.set(0, -2.4, -5); scene.add(torus);


        window.__updateThreeTheme = (t) => {

          const c = colorFromTheme(t);

          pMat.color.copy(c);

          torusKnot.material.color.copy(c);

          ico.material.color.copy(c);

          torus.material.color.copy(c);

        };


        function resize() {

          camera.aspect = window.innerWidth / window.innerHeight;

          camera.updateProjectionMatrix();

          renderer.setSize(window.innerWidth, window.innerHeight, false);

        }

        resize();

        window.addEventListener("resize", resize);


        const start = performance.now();

        function tick() {

          const t = (performance.now() - start) / 1000;

          points.rotation.y = t * 0.05;

          points.rotation.x = Math.sin(t * 0.05) * 0.4;

          torusKnot.rotation.x = t * 0.18;

          torusKnot.rotation.y = t * 0.14;

          ico.rotation.x = -t * 0.12;

          ico.rotation.y = t * 0.16;

          torus.rotation.x = t * 0.1;

          torus.rotation.z = t * 0.08;

          renderer.render(scene, camera);

          requestAnimationFrame(tick);

        }

        tick();


        canvas.addEventListener("webglcontextlost", (e) => {

          e.preventDefault();

          document.getElementById("bg-fallback").style.display = "block";

          canvas.style.display = "none";

        });

      }


      try { initThree(); }

      catch (err) {

        document.getElementById("bg-fallback").style.display = "block";

        document.getElementById("bg-canvas").style.display = "none";

      }


      // ---------- Avatar reactivity ----------

      const avatar = document.getElementById("avatar");

      const leftPupil = document.getElementById("leftPupil");

      const rightPupil = document.getElementById("rightPupil");

      const leftHL = document.getElementById("leftHL");

      const rightHL = document.getElementById("rightHL");

      const mouth = document.getElementById("mouth");


      const MOUTH = {

        idle: "M 70 110 Q 80 112 90 110",

        email: "M 70 110 Q 80 114 90 110",

        password: "M 66 108 Q 80 122 94 108",

        "password-visible": "M 68 109 Q 80 118 92 109",

        submitting: "M 72 110 Q 80 116 88 110",

        success: "M 62 106 Q 80 130 98 106",

      };


      const EYE_POS = {

        idle:    { l: { x: 60, y: 82 }, r: { x: 100, y: 82 } },

        email:   { l: { x: 56, y: 86 }, r: { x: 96,  y: 86 } },

        success: { l: { x: 60, y: 80 }, r: { x: 100, y: 80 } },

      };


      function setAvatarState(state) {

        avatar.classList.remove("closed", "smiling", "tilted", "successful");

        mouth.setAttribute("d", MOUTH[state] || MOUTH.idle);


        const pos = state === "email" ? EYE_POS.email

                  : state === "success" ? EYE_POS.success

                  : EYE_POS.idle;

        leftPupil.setAttribute("cx", pos.l.x);  leftPupil.setAttribute("cy", pos.l.y);

        rightPupil.setAttribute("cx", pos.r.x); rightPupil.setAttribute("cy", pos.r.y);

        leftHL.setAttribute("cx", pos.l.x + 1.4);  leftHL.setAttribute("cy", pos.l.y - 1.4);

        rightHL.setAttribute("cx", pos.r.x + 1.4); rightHL.setAttribute("cy", pos.r.y - 1.4);


        if (state === "email")             avatar.classList.add("tilted");

        else if (state === "password")     avatar.classList.add("closed", "smiling");

        else if (state === "password-visible") avatar.classList.add("smiling");

        else if (state === "success")      avatar.classList.add("smiling", "successful");

      }


      function passwordFocusState() {

        return pwdInput.type === "text" ? "password-visible" : "password";

      }


      setAvatarState("idle");


      // ---------- Form behavior ----------

      const emailInput = document.getElementById("email");

      const pwdInput = document.getElementById("password");

      const emailError = document.getElementById("emailError");

      const pwdError = document.getElementById("passwordError");

      const form = document.getElementById("loginForm");

      const submitBtn = document.getElementById("submitBtn");

      const submitLabel = document.getElementById("submitLabel");

      const successBanner = document.getElementById("successBanner");


      let currentFocus = null;


      emailInput.addEventListener("focus", () => { currentFocus = "email"; setAvatarState("email"); });

      emailInput.addEventListener("blur",  () => { if (currentFocus === "email") currentFocus = null; if (!currentFocus) setAvatarState("idle"); });

      pwdInput.addEventListener("focus",   () => { currentFocus = "password"; setAvatarState(passwordFocusState()); });

      pwdInput.addEventListener("blur",    () => { if (currentFocus === "password") currentFocus = null; if (!currentFocus) setAvatarState("idle"); });


      function validEmail(v) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }

      emailInput.addEventListener("input", () => {

        if (emailInput.value && !validEmail(emailInput.value)) emailError.textContent = "Enter a valid email address";

        else emailError.textContent = "";

      });

      pwdInput.addEventListener("input", () => { pwdError.textContent = ""; });


      // Password show/hide

      const pwdToggle = document.getElementById("pwdToggle");

      const pwdIconOpen = document.getElementById("pwdIconOpen");

      const pwdIconOff = document.getElementById("pwdIconOff");

      pwdToggle.addEventListener("click", () => {

        const willShow = pwdInput.type === "password";

        pwdInput.type = willShow ? "text" : "password";

        pwdIconOpen.style.display = willShow ? "none" : "";

        pwdIconOff.style.display  = willShow ? ""     : "none";

        pwdToggle.setAttribute("aria-label", willShow ? "Hide password" : "Show password");

        pwdInput.focus();

        if (currentFocus === "password") setAvatarState(passwordFocusState());

      });


      // Submit

      form.addEventListener("submit", async (e) => {

        e.preventDefault();

        let ok = true;

        if (!validEmail(emailInput.value)) { emailError.textContent = "Enter a valid email address"; ok = false; }

        if (pwdInput.value.length < 6)     { pwdError.textContent = "Password must be at least 6 characters"; ok = false; }

        if (!ok) return;


        submitBtn.disabled = true;

        submitLabel.textContent = "Signing in";

        const sp = document.createElement("span");

        sp.className = "spinner";

        submitBtn.prepend(sp);

        emailInput.blur(); pwdInput.blur();

        setAvatarState("submitting");


        await new Promise((r) => setTimeout(r, 1300));


        setAvatarState("success");

        submitBtn.disabled = false;

        sp.remove();

        submitLabel.textContent = "Sign in";

        successBanner.classList.add("show");


        setTimeout(() => {

          successBanner.classList.remove("show");

          setAvatarState("idle");

          form.reset();

          emailError.textContent = "";

          pwdError.textContent = "";

        }, 2200);

      });

    </script>

  </body>

</html>



Comments