Files
invyone/frontend/invion-preview-v5.html
T
2026-04-06 15:54:35 +09:00

1050 lines
62 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>INVION - Preview v5</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<style>
:root {
--bg:#fafaff; --bg-subtle:#f3f2fa; --surface:rgba(255,255,255,0.55); --surface-solid:#ffffff;
--surface-hover:rgba(255,255,255,0.7); --text:#0f0e1a; --text-sec:#6b6a80; --text-muted:#9998ad;
--primary:#6c5ce7; --primary-light:#a29bfe; --primary-glow:rgba(108,92,231,0.25);
--cyan:#00cec9; --cyan-glow:rgba(0,206,201,0.2); --pink:#fd79a8; --pink-glow:rgba(253,121,168,0.15);
--red:#ff4757; --green:#00b894; --amber:#fdcb6e;
--border:rgba(108,92,231,0.12); --border-subtle:rgba(0,0,0,0.05);
--glass:rgba(255,255,255,0.45); --glass-strong:rgba(255,255,255,0.65);
--glass-border:rgba(108,92,231,0.12);
--glow-sm:0 0 20px rgba(108,92,231,0.12); --glow-md:0 0 40px rgba(108,92,231,0.2);
--glow-lg:0 0 80px rgba(108,92,231,0.25);
--sidebar-w:240px;
}
.dark {
--bg:#06050e; --bg-subtle:#0c0b18; --surface:rgba(17,16,42,0.5); --surface-solid:#11102a;
--surface-hover:rgba(25,24,64,0.6); --text:#eae8f4; --text-sec:#8d8ba8; --text-muted:#5a587a;
--primary:#a29bfe; --primary-light:#c8c4ff; --primary-glow:rgba(162,155,254,0.25);
--cyan:#55efc4; --cyan-glow:rgba(85,239,196,0.15); --pink:#fd79a8; --red:#ff6b6b;
--green:#55efc4; --amber:#ffeaa7;
--border:rgba(162,155,254,0.1); --border-subtle:rgba(255,255,255,0.04);
--glass:rgba(17,16,42,0.45); --glass-strong:rgba(17,16,42,0.65);
--glass-border:rgba(162,155,254,0.12);
--glow-sm:0 0 20px rgba(162,155,254,0.1); --glow-md:0 0 40px rgba(162,155,254,0.18);
--glow-lg:0 0 80px rgba(162,155,254,0.22);
}
*{margin:0;padding:0;box-sizing:border-box;}
html,body{height:100%;overflow:hidden;}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);transition:background .5s,color .4s;}
::-webkit-scrollbar{width:5px;} ::-webkit-scrollbar-track{background:transparent;}
::-webkit-scrollbar-thumb{background:rgba(108,92,231,0.2);border-radius:3px;}
.screen{position:fixed;inset:0;z-index:1;}
.screen.hidden{opacity:0;pointer-events:none;}
/* ===== COSMIC BACKGROUND (shared) ===== */
.cosmos{position:fixed;inset:0;pointer-events:none;z-index:0;overflow:hidden;}
/* Stars — dark only */
.star{position:absolute;width:2px;height:2px;background:white;border-radius:50%;
animation:twinkle var(--d,3s) ease-in-out infinite alternate;animation-delay:var(--dl,0s);opacity:0;}
.star.c{width:3px;height:3px;background:var(--sc);}
@keyframes twinkle{0%{opacity:0;transform:scale(.5)}100%{opacity:var(--mo,.7);transform:scale(1)}}
/* Light mode: stars become soft light sparkles */
html:not(.dark) .star{background:var(--primary);width:3px;height:3px;}
html:not(.dark) .star.c{width:4px;height:4px;}
/* Nebulae — dark: space nebula / light: soft clouds */
.neb{position:absolute;border-radius:50%;filter:blur(140px);animation:drift 16s ease-in-out infinite alternate;}
.neb-1{width:700px;height:700px;top:-20%;right:-15%;background:radial-gradient(circle,var(--primary-glow),transparent 70%);animation-duration:18s;}
.neb-2{width:600px;height:600px;bottom:-25%;left:-10%;background:radial-gradient(circle,var(--cyan-glow),transparent 70%);animation-duration:14s;animation-delay:-4s;}
.neb-3{width:450px;height:450px;top:35%;left:40%;background:radial-gradient(circle,var(--pink-glow),transparent 70%);animation-duration:12s;animation-delay:-8s;}
.neb-4{width:350px;height:350px;top:60%;right:25%;background:radial-gradient(circle,rgba(108,92,231,0.08),transparent 70%);animation-duration:20s;animation-delay:-2s;}
@keyframes drift{0%{transform:translate(0,0) scale(1)}100%{transform:translate(30px,-25px) scale(1.1)}}
/* Light mode: sky above clouds — no stars, no shooting stars */
html:not(.dark) .cosmos{background:linear-gradient(180deg,#e8e4ff 0%,#f0edff 30%,#fafaff 60%,#f5f0ff 100%);}
html:not(.dark) .star{display:none;}
html:not(.dark) .shooting-star{display:none;}
html:not(.dark) .particle{display:none;}
html:not(.dark) .neb{filter:blur(100px);}
/* Clouds — large, soft, low in the viewport */
html:not(.dark) .neb-1{width:1200px;height:500px;top:auto;bottom:-10%;right:-15%;border-radius:50%;
background:radial-gradient(ellipse,rgba(255,255,255,0.9),rgba(230,225,255,0.5),transparent 70%);animation-duration:25s;}
html:not(.dark) .neb-2{width:1000px;height:400px;top:auto;bottom:-5%;left:-10%;
background:radial-gradient(ellipse,rgba(255,255,255,0.85),rgba(200,240,255,0.4),transparent 70%);animation-duration:20s;}
html:not(.dark) .neb-3{width:800px;height:350px;top:auto;bottom:5%;left:30%;
background:radial-gradient(ellipse,rgba(255,255,255,0.8),rgba(240,220,255,0.3),transparent 70%);animation-duration:22s;}
/* Upper sky glow */
html:not(.dark) .neb-4{width:600px;height:600px;top:-10%;right:20%;bottom:auto;
background:radial-gradient(circle,rgba(108,92,231,0.08),rgba(0,206,201,0.04),transparent 70%);}
/* Shooting stars */
.shooting-star{position:absolute;width:80px;height:1px;
background:linear-gradient(90deg,rgba(255,255,255,0.6),transparent);
transform:rotate(35deg);animation:shoot 5s ease-in-out infinite;opacity:0;}
.shooting-star:nth-child(6){animation-delay:3s;top:25%;left:65%;width:55px;transform:rotate(40deg);}
@keyframes shoot{0%{opacity:0;transform:rotate(35deg) translateX(0)}3%{opacity:0.7}12%{opacity:0;transform:rotate(35deg) translateX(-350px)}100%{opacity:0}}
/* Light: shooting stars are golden streaks */
html:not(.dark) .shooting-star{background:linear-gradient(90deg,rgba(108,92,231,0.25),transparent);}
/* Light: particles are soft light motes */
html:not(.dark) .particle{box-shadow:0 0 6px rgba(108,92,231,0.2);}
/* Particles */
.particle{position:absolute;width:var(--sz,4px);height:var(--sz,4px);background:var(--pc,var(--primary));
border-radius:50%;opacity:0;animation:floatup var(--fd,9s) ease-in-out infinite;animation-delay:var(--fdl,0s);}
@keyframes floatup{0%{opacity:0;transform:translateY(100vh) scale(0)}10%{opacity:.4}90%{opacity:.4}100%{opacity:0;transform:translateY(-80px) scale(1)}}
/* ===== LOGIN ===== */
#login-screen{display:flex;align-items:center;justify-content:center;z-index:5;}
.login-card{
width:400px;backdrop-filter:blur(30px) saturate(1.5);-webkit-backdrop-filter:blur(30px) saturate(1.5);
border-radius:28px;padding:2.5rem 2.5rem 2rem;background:var(--glass-strong);
border:1px solid var(--glass-border);
box-shadow:0 12px 48px rgba(0,0,0,0.08),inset 0 0 0 1px rgba(255,255,255,0.18);
animation:cardIn 1s cubic-bezier(.16,1,.3,1) .3s both;position:relative;z-index:5;
}
.dark .login-card{box-shadow:0 12px 48px rgba(0,0,0,0.6),var(--glow-md),inset 0 0 0 1px rgba(162,155,254,0.06);}
@keyframes cardIn{from{opacity:0;transform:translateY(40px) scale(.96)}to{opacity:1;transform:none}}
/* Orbital decoration */
.login-orbit{width:80px;height:80px;margin:0 auto 1rem;position:relative;animation:logoIn 1.2s cubic-bezier(.16,1,.3,1) .4s both;}
.orbit-core{position:absolute;inset:22px;border-radius:50%;
background:linear-gradient(135deg,var(--primary),var(--cyan));
box-shadow:0 0 30px var(--primary-glow),0 0 60px rgba(0,206,201,0.15);
animation:corePulse 3s ease-in-out infinite;}
@keyframes corePulse{0%,100%{box-shadow:0 0 30px var(--primary-glow),0 0 60px rgba(0,206,201,0.15)}
50%{box-shadow:0 0 40px var(--primary-glow),0 0 80px rgba(0,206,201,0.25)}}
.orbit-ring{position:absolute;inset:0;border-radius:50%;border:1.5px solid transparent;
border-top-color:var(--primary);border-right-color:var(--cyan);
animation:orbitSpin 4s linear infinite;opacity:.6;}
.orbit-ring-2{position:absolute;inset:6px;border-radius:50%;border:1px solid transparent;
border-bottom-color:var(--pink);border-left-color:var(--primary-light);
animation:orbitSpin 6s linear infinite reverse;opacity:.4;}
@keyframes orbitSpin{to{transform:rotate(360deg)}}
.orbit-dot{position:absolute;width:6px;height:6px;border-radius:50%;background:var(--cyan);
top:-1px;left:50%;margin-left:-3px;box-shadow:0 0 10px var(--cyan-glow);
animation:orbitSpin 4s linear infinite;transform-origin:3px 41px;}
/* Login fail — warp failure */
@keyframes denied{
0%{transform:scale(1.1);box-shadow:0 0 60px rgba(255,71,87,0.4),0 0 0 2px rgba(255,71,87,0.5),inset 0 0 30px rgba(255,71,87,0.05)}
18%{transform:scale(1) translateX(-6px)}
36%{transform:scale(1) translateX(5px)}
52%{transform:scale(1) translateX(-4px)}
66%{transform:scale(1) translateX(3px)}
78%{transform:scale(1) translateX(-1px)}
100%{transform:scale(1) translateX(0);box-shadow:none}
}
.login-card.denied{animation:denied .55s cubic-bezier(.36,.07,.19,.97) both;}
.fi.error{border-color:rgba(255,71,87,0.4) !important;box-shadow:0 0 12px rgba(255,71,87,0.12) !important;}
.err-msg{font-size:.72rem;color:var(--red);text-align:center;margin-top:.75rem;
opacity:0;transform:translateY(-5px);transition:opacity .4s,transform .4s;}
.err-msg.show{opacity:1;transform:translateY(0);}
.logo{text-align:center;margin-bottom:.15rem;animation:logoIn 1.2s cubic-bezier(.16,1,.3,1) .5s both;}
.logo h1{font-size:1.8rem;font-weight:900;letter-spacing:-.04em;
background:linear-gradient(135deg,var(--primary),var(--cyan) 60%,var(--pink));
background-size:200% 200%;-webkit-background-clip:text;-webkit-text-fill-color:transparent;
background-clip:text;animation:gshift 4s ease-in-out infinite;}
@keyframes logoIn{from{opacity:0;transform:translateY(-20px);filter:blur(10px)}to{opacity:1;transform:none;filter:none}}
@keyframes gshift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.login-sub{text-align:center;font-size:.78rem;color:var(--text-muted);margin-bottom:1.8rem;animation:fadeIn .8s .7s both;}
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
.fg{margin-bottom:1rem;animation:fgIn .6s cubic-bezier(.16,1,.3,1) both;}
.fg:nth-child(1){animation-delay:.8s}.fg:nth-child(2){animation-delay:.9s}
@keyframes fgIn{from{opacity:0;transform:translateX(-20px)}to{opacity:1;transform:none}}
.fi-wrap{position:relative;display:flex;align-items:center;}
.fi-icon{position:absolute;left:14px;color:var(--text-muted);pointer-events:none;transition:color .3s;flex-shrink:0;}
.fi-wrap:focus-within .fi-icon{color:var(--primary);}
.fi{width:100%;height:48px;padding:0 1rem 0 2.6rem;border:1.5px solid var(--border);border-radius:14px;
background:var(--surface);color:var(--text);font-size:.875rem;font-family:inherit;outline:none;
backdrop-filter:blur(8px);transition:all .3s cubic-bezier(.4,0,.2,1);}
.fi::placeholder{color:var(--text-muted);font-weight:400;}
.fi:focus{border-color:var(--primary);box-shadow:0 0 0 4px var(--primary-glow),var(--glow-sm);}
.pw-w .fi{padding-right:2.8rem;}
.pw-w{position:relative;display:flex;align-items:center;}
.pw-b{position:absolute;right:14px;top:50%;transform:translateY(-50%);background:none;border:none;
color:var(--text-muted);cursor:pointer;padding:4px;transition:color .2s;}
.pw-b:hover{color:var(--primary);}
.login-divider{display:flex;align-items:center;gap:.75rem;margin:1.25rem 0;animation:fgIn .6s cubic-bezier(.16,1,.3,1) 1s both;}
.login-divider::before,.login-divider::after{content:'';flex:1;height:1px;background:var(--border);}
.login-divider span{font-size:.6rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;}
.lbtn{width:100%;height:50px;border:none;border-radius:14px;font-size:.9rem;font-weight:700;
font-family:inherit;color:white;cursor:pointer;position:relative;overflow:hidden;
display:flex;align-items:center;justify-content:center;gap:.5rem;
background:linear-gradient(135deg,var(--primary),var(--cyan));
box-shadow:0 4px 20px var(--primary-glow);transition:all .4s cubic-bezier(.4,0,.2,1);
animation:fgIn .6s cubic-bezier(.16,1,.3,1) 1.1s both;}
.lbtn:hover{transform:translateY(-2px);box-shadow:var(--glow-lg),0 8px 30px var(--primary-glow);}
.lbtn:active{transform:translateY(0) scale(.98);}
.lbtn::before{content:'';position:absolute;top:0;left:-100%;width:100%;height:100%;
background:linear-gradient(90deg,transparent,rgba(255,255,255,.2),transparent);animation:shimmer 3s ease-in-out infinite;}
@keyframes shimmer{0%{left:-100%}50%{left:100%}100%{left:100%}}
.rip{position:absolute;border-radius:50%;background:rgba(255,255,255,.4);transform:scale(0);animation:ripX .6s ease-out;pointer-events:none;}
@keyframes ripX{to{transform:scale(4);opacity:0}}
.login-ft{text-align:center;margin-top:1.25rem;font-size:.65rem;color:var(--text-muted);animation:fadeIn .8s 1.2s both;letter-spacing:.02em;}
.spinner{display:inline-block;width:20px;height:20px;border:2.5px solid rgba(255,255,255,.3);
border-top-color:white;border-radius:50%;animation:spin .6s linear infinite;}
@keyframes spin{to{transform:rotate(360deg)}}
/* Warp canvas — sits behind login card so warp shows through as card fades */
#warp-canvas{position:fixed;inset:0;z-index:2;pointer-events:none;opacity:0;}
#warp-canvas.active{opacity:1;}
/* ===== MAIN — COSMIC COMMAND CENTER ===== */
#main-screen{display:flex;flex-direction:column;height:100%;}
/* Glass header */
.hdr{height:50px;display:flex;align-items:center;justify-content:space-between;padding:0 1.25rem;
background:var(--glass);backdrop-filter:blur(20px) saturate(1.4);-webkit-backdrop-filter:blur(20px) saturate(1.4);
border-bottom:1px solid var(--glass-border);position:relative;z-index:20;}
.hdr-l{display:flex;align-items:center;gap:1rem;}
.hdr-logo{font-size:1.05rem;font-weight:900;letter-spacing:-.03em;
background:linear-gradient(135deg,var(--primary),var(--cyan));-webkit-background-clip:text;
-webkit-text-fill-color:transparent;background-clip:text;}
.hdr-bc{font-size:.72rem;color:var(--text-muted);}
.hdr-bc b{color:var(--text);font-weight:600;}
.hdr-r{display:flex;align-items:center;gap:.65rem;}
.pill{display:flex;background:var(--surface);backdrop-filter:blur(8px);border:1px solid var(--glass-border);
border-radius:999px;padding:2px;}
.pill button{padding:.22rem .65rem;border-radius:999px;border:none;background:transparent;
color:var(--text-muted);cursor:pointer;font-size:.6rem;font-weight:600;font-family:inherit;
transition:all .3s cubic-bezier(.4,0,.2,1);}
.pill button.on{background:var(--primary);color:white;box-shadow:var(--glow-sm);}
.bell{position:relative;width:32px;height:32px;border-radius:10px;border:1px solid var(--glass-border);
background:var(--surface);backdrop-filter:blur(8px);color:var(--text-muted);cursor:pointer;
display:flex;align-items:center;justify-content:center;transition:all .2s;}
.bell:hover{border-color:var(--primary);color:var(--primary);box-shadow:var(--glow-sm);}
.bell-dot{position:absolute;top:5px;right:5px;width:7px;height:7px;background:var(--red);border-radius:50%;animation:pdot 2s infinite;}
@keyframes pdot{0%,100%{box-shadow:0 0 0 0 rgba(255,71,87,.4)}50%{box-shadow:0 0 0 5px rgba(255,71,87,0)}}
.avatar{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--primary),var(--cyan));
display:flex;align-items:center;justify-content:center;font-size:.7rem;font-weight:700;color:white;
cursor:pointer;transition:transform .2s,box-shadow .3s;}
.avatar:hover{transform:scale(1.1);box-shadow:var(--glow-sm);}
/* Tab bar glass */
.tabs{height:36px;display:flex;align-items:stretch;padding:0 .5rem;gap:1px;overflow-x:auto;
background:var(--glass);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
border-bottom:1px solid var(--glass-border);position:relative;z-index:15;}
.tab{display:flex;align-items:center;gap:.4rem;padding:0 .85rem;font-size:.7rem;font-weight:500;
color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;white-space:nowrap;transition:all .25s;}
.tab:hover{color:var(--text-sec);background:var(--surface-hover);}
.tab.on{color:var(--primary);font-weight:600;border-bottom-color:var(--primary);background:var(--surface);}
.tab-x{width:14px;height:14px;border-radius:3px;border:none;background:transparent;color:var(--text-muted);
font-size:.6rem;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:0;transition:all .15s;}
.tab:hover .tab-x{opacity:1;}.tab-x:hover{background:rgba(255,71,87,.15);color:var(--red);}
/* Body */
.body{display:flex;flex:1;overflow:hidden;position:relative;z-index:5;}
/* Glass sidebar */
.side{width:var(--sidebar-w);background:var(--glass);backdrop-filter:blur(20px) saturate(1.3);
-webkit-backdrop-filter:blur(20px) saturate(1.3);border-right:1px solid var(--glass-border);
padding:.85rem .6rem;overflow-y:auto;display:flex;flex-direction:column;gap:1px;}
.side-sec{font-size:.55rem;font-weight:700;text-transform:uppercase;letter-spacing:.12em;
color:var(--text-muted);padding:1rem .65rem .35rem;}
.side-sec:first-child{padding-top:.25rem;}
.si{padding:.5rem .7rem;border-radius:10px;font-size:.77rem;color:var(--text-sec);cursor:pointer;
transition:all .2s cubic-bezier(.4,0,.2,1);font-weight:450;display:flex;align-items:center;gap:.6rem;
position:relative;overflow:hidden;}
.si .ic{width:16px;height:16px;display:flex;align-items:center;justify-content:center;opacity:.65;flex-shrink:0;}
.si:hover{background:var(--surface-hover);color:var(--text);transform:translateX(2px);}
.si.on{background:linear-gradient(135deg,rgba(108,92,231,.12),rgba(108,92,231,.05));
color:var(--primary);font-weight:600;border:1px solid rgba(108,92,231,.15);box-shadow:var(--glow-sm);}
.si.on .ic{opacity:1;}
.dark .si.on{background:linear-gradient(135deg,rgba(162,155,254,.14),rgba(162,155,254,.05));border-color:rgba(162,155,254,.15);}
.si::before{content:'';position:absolute;left:0;top:0;width:3px;height:100%;background:var(--primary);
border-radius:0 2px 2px 0;transform:scaleY(0);transition:transform .2s cubic-bezier(.4,0,.2,1);}
.si.on::before{transform:scaleY(1);}
/* Sidebar animate */
.side-anim .si{animation:slideR .4s cubic-bezier(.16,1,.3,1) both;}
@keyframes slideR{from{opacity:0;transform:translateX(-16px)}to{opacity:1;transform:none}}
/* Content — glass cards over cosmic bg */
.content{flex:1;overflow-y:auto;padding:1rem 1.25rem;display:flex;flex-direction:column;gap:.75rem;}
/* Greeting */
.greet-row{display:flex;justify-content:space-between;align-items:center;}
.greet-row h2{font-size:1.05rem;font-weight:800;letter-spacing:-.02em;}
.greet-row .dt{font-size:.72rem;color:var(--text-muted);}
/* Glass card */
.gc{background:var(--glass);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);
border:1px solid var(--glass-border);border-radius:16px;padding:.85rem 1rem;
transition:all .35s cubic-bezier(.4,0,.2,1);animation:cardUp .5s cubic-bezier(.16,1,.3,1) both;
position:relative;overflow:hidden;}
.gc:hover{transform:translateY(-2px);box-shadow:var(--glow-sm),0 8px 30px rgba(0,0,0,.08);
border-color:rgba(108,92,231,.2);}
.dark .gc:hover{box-shadow:var(--glow-md),0 8px 30px rgba(0,0,0,.3);}
/* Glow line on top */
.gc::after{content:'';position:absolute;top:0;left:0;right:0;height:2px;
background:linear-gradient(90deg,var(--primary),var(--cyan));transform:scaleX(0);
transform-origin:left;transition:transform .4s cubic-bezier(.4,0,.2,1);}
.gc:hover::after{transform:scaleX(1);}
@keyframes cardUp{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:none}}
.gc-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem;}
.gc-t{font-size:.72rem;font-weight:700;color:var(--text-sec);}
.gc-more{font-size:.58rem;color:var(--primary);font-weight:600;cursor:pointer;}
/* Stats row */
.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:.6rem;}
.kpi-label{font-size:.63rem;font-weight:600;color:var(--text-muted);}
.kpi-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:.3rem;}
.kpi-badge{font-size:.5rem;font-weight:700;padding:.1rem .35rem;border-radius:999px;}
.kpi-badge.up{background:rgba(0,184,148,.12);color:var(--green);}
.kpi-badge.dn{background:rgba(255,71,87,.1);color:var(--red);}
.kpi-val{font-size:1.45rem;font-weight:800;letter-spacing:-.03em;
background:linear-gradient(135deg,var(--primary),var(--cyan));-webkit-background-clip:text;
-webkit-text-fill-color:transparent;background-clip:text;}
.kpi-spark{display:flex;align-items:flex-end;gap:2px;height:26px;margin-top:.3rem;}
.kpi-bar{flex:1;border-radius:2px;background:var(--primary);opacity:.15;transition:all .3s;min-width:3px;}
.gc:hover .kpi-bar{opacity:.4;}
.kpi-bar:last-child{opacity:.6;background:var(--cyan);}
/* Widgets row */
.widgets{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;}
/* Approval */
.ap{display:flex;align-items:center;justify-content:space-between;padding:.4rem 0;
border-bottom:1px solid var(--border-subtle);cursor:pointer;transition:all .15s;}
.ap:last-child{border-bottom:none;}
.ap:hover{background:var(--surface-hover);margin:0 -.5rem;padding:.4rem .5rem;border-radius:8px;}
.ap-l{display:flex;align-items:center;gap:.4rem;flex:1;min-width:0;}
.dot{width:6px;height:6px;border-radius:50%;flex-shrink:0;}
.dot-r{background:var(--red);box-shadow:0 0 8px rgba(255,71,87,.5);animation:pdot 2s infinite;}
.dot-y{background:var(--amber);box-shadow:0 0 6px rgba(253,203,110,.4);}
.dot-g{background:var(--green);}
.ap-t{font-size:.7rem;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.ap-time{font-size:.58rem;color:var(--text-muted);flex-shrink:0;margin-left:.4rem;}
/* Activity */
.act{display:flex;gap:.45rem;padding:.35rem 0;}
.act-av{width:24px;height:24px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;
justify-content:center;font-size:.55rem;font-weight:700;color:white;}
.act-txt{font-size:.68rem;line-height:1.35;}
.act-txt b{font-weight:600;}
.act-tm{font-size:.55rem;color:var(--text-muted);margin-top:.1rem;}
/* Donut */
.donut-w{display:flex;align-items:center;gap:.7rem;}
.donut{width:88px;height:88px;border-radius:50%;position:relative;
background:conic-gradient(var(--green) 0deg 194deg,var(--amber) 194deg 248deg,var(--primary) 248deg 302deg,var(--red) 302deg 330deg,var(--cyan) 330deg 360deg);
animation:dSpin 1.2s cubic-bezier(.16,1,.3,1) both;box-shadow:0 0 30px rgba(0,184,148,.1);}
@keyframes dSpin{from{transform:rotate(-90deg);opacity:0}to{transform:none;opacity:1}}
.donut-hole{position:absolute;inset:15px;border-radius:50%;background:var(--surface-solid);
backdrop-filter:blur(8px);display:flex;flex-direction:column;align-items:center;justify-content:center;}
.dark .donut-hole{background:var(--bg-subtle);}
.dn-num{font-size:1rem;font-weight:800;}
.dn-sub{font-size:.48rem;color:var(--text-muted);}
.legend{display:flex;flex-direction:column;gap:.28rem;flex:1;}
.leg{display:flex;align-items:center;gap:.35rem;font-size:.6rem;}
.leg-d{width:7px;height:7px;border-radius:2px;flex-shrink:0;}
.leg-v{margin-left:auto;font-weight:700;font-size:.63rem;}
/* Bar chart */
.chart-w{display:grid;grid-template-columns:1.4fr 1fr;gap:.6rem;}
.bars{display:flex;align-items:flex-end;gap:6px;height:130px;padding-top:.5rem;}
.bar-col{flex:1;display:flex;flex-direction:column;align-items:center;gap:.3rem;}
.bar-fill{width:100%;border-radius:6px 6px 2px 2px;transition:height .8s cubic-bezier(.16,1,.3,1);position:relative;}
.bar-fill:hover{filter:brightness(1.2);box-shadow:0 0 12px var(--primary-glow);}
.bar-lbl{font-size:.5rem;color:var(--text-muted);font-weight:600;}
/* Shortcuts */
.sc-grid{display:grid;grid-template-columns:1fr 1fr;gap:.45rem;}
.sc{display:flex;align-items:center;gap:.5rem;padding:.5rem .65rem;border-radius:12px;
background:var(--surface);backdrop-filter:blur(8px);cursor:pointer;transition:all .25s;
border:1px solid transparent;}
.sc:hover{border-color:var(--glass-border);transform:translateX(3px);box-shadow:var(--glow-sm);}
.sc-ic{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.sc-t{font-size:.67rem;font-weight:600;}
.sc-s{font-size:.53rem;color:var(--text-muted);}
/* Table */
.tbl-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem;}
.tbl-t{font-size:.8rem;font-weight:700;}
.tbl-acts{display:flex;gap:.35rem;}
.btn-s{height:26px;padding:0 .6rem;border-radius:8px;border:1px solid var(--glass-border);
background:var(--surface);backdrop-filter:blur(8px);color:var(--text-sec);font-size:.62rem;font-weight:500;
font-family:inherit;cursor:pointer;display:flex;align-items:center;gap:.25rem;transition:all .2s;}
.btn-s:hover{border-color:var(--primary);color:var(--primary);box-shadow:var(--glow-sm);}
.btn-s.pr{background:var(--primary);color:white;border-color:var(--primary);}
.btn-s.pr:hover{box-shadow:var(--glow-md);transform:translateY(-1px);}
.tbl-scroll{overflow-x:auto;}
table{width:100%;border-collapse:collapse;font-size:.7rem;}
th{text-align:left;padding:.5rem .75rem;background:var(--surface);color:var(--text-muted);
font-weight:600;font-size:.58rem;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border);}
td{padding:.45rem .75rem;border-bottom:1px solid var(--border-subtle);transition:background .15s;}
tr:hover td{background:var(--surface-hover);}
tr{animation:rowIn .4s cubic-bezier(.16,1,.3,1) both;}
@keyframes rowIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
.badge{display:inline-flex;align-items:center;padding:.12rem .45rem;border-radius:999px;font-size:.55rem;font-weight:600;}
.bg-g{background:rgba(0,184,148,.12);color:var(--green);}
.bg-y{background:rgba(253,203,110,.18);color:#b8860b;}.dark .bg-y{color:var(--amber);background:rgba(253,203,110,.1);}
.bg-p{background:rgba(108,92,231,.12);color:var(--primary);}
.bg-r{background:rgba(255,71,87,.12);color:var(--red);}
.bg-c{background:rgba(0,206,201,.12);color:var(--cyan);}
.pulse{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--green);margin-right:3px;animation:pG 2s infinite;}
@keyframes pG{0%,100%{box-shadow:0 0 0 0 rgba(0,184,148,.4)}50%{box-shadow:0 0 0 4px rgba(0,184,148,0)}}
/* Toast */
.toast{position:fixed;bottom:1.25rem;right:1.25rem;background:var(--glass-strong);
backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:14px;padding:.7rem 1.1rem;
box-shadow:var(--glow-sm);display:flex;align-items:center;gap:.6rem;z-index:1000;
transform:translateY(120%) scale(.9);opacity:0;transition:all .5s cubic-bezier(.68,-.55,.27,1.55);}
.toast.show{transform:none;opacity:1;}
.toast-bar{width:3px;height:28px;border-radius:2px;background:var(--green);}
.toast-t{font-size:.72rem;font-weight:600;}
.toast-s{font-size:.62rem;color:var(--text-muted);margin-top:.05rem;}
/* Theme transition overlay */
.theme-fade{position:fixed;inset:0;z-index:9999;pointer-events:none;opacity:0;
transition:opacity .4s cubic-bezier(.4,0,.2,1);}
.theme-fade.in{opacity:1;}
.preview-tag{position:fixed;top:0;left:50%;transform:translateX(-50%);z-index:10000;
background:linear-gradient(135deg,var(--primary),var(--cyan));color:white;font-size:.55rem;
font-weight:700;padding:.2rem 1.2rem;border-radius:0 0 10px 10px;letter-spacing:.06em;
box-shadow:0 4px 15px var(--primary-glow);}
</style>
</head>
<body>
<div class="preview-tag">INVION v5 — COSMIC COMMAND CENTER</div>
<div class="theme-fade" id="theme-fade"></div>
<canvas id="warp-canvas"></canvas>
<!-- Cosmic background — always visible -->
<div class="cosmos" id="cosmos">
<div class="neb neb-1"></div><div class="neb neb-2"></div><div class="neb neb-3"></div><div class="neb neb-4"></div>
<div class="shooting-star" style="top:12%;left:70%"></div>
<div class="shooting-star" style="top:35%;left:55%"></div>
</div>
<!-- LOGIN -->
<div class="screen" id="login-screen">
<div style="position:absolute;top:1.5rem;right:1.5rem;z-index:20;">
<div class="pill">
<button class="on" onclick="setTheme('light')">Light</button>
<button onclick="setTheme('dark')">Dark</button>
</div>
</div>
<div class="login-card">
<div class="login-orbit">
<div class="orbit-ring"></div>
<div class="orbit-ring-2"></div>
<div class="orbit-dot"></div>
<div class="orbit-core"></div>
</div>
<div class="logo"><h1>INVION</h1></div>
<div class="login-sub">Cosmic Command Center</div>
<div class="fg">
<div class="fi-wrap">
<svg class="fi-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<input class="fi" placeholder="User ID" value="admin">
</div>
</div>
<div class="fg">
<div class="fi-wrap pw-w">
<svg class="fi-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
<input class="fi" type="password" placeholder="Password" value="invion2024" id="pw">
<button class="pw-b" onclick="let p=document.getElementById('pw');p.type=p.type==='password'?'text':'password'">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
</button>
</div>
</div>
<div class="login-divider"><span>ready to launch</span></div>
<button class="lbtn" id="lbtn" onclick="doLogin()">
<span>Launch</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
</button>
<div class="err-msg" id="err-msg">아이디 또는 비밀번호를 확인해주세요</div>
<div class="login-ft">&copy; 2026 INVION</div>
</div>
</div>
<!-- MAIN -->
<div class="screen hidden" id="main-screen">
<header class="hdr">
<div class="hdr-l">
<div class="hdr-logo">INVION</div>
<div class="hdr-bc">&rsaquo; <b>대시보드</b></div>
</div>
<div class="hdr-r">
<div class="pill">
<button class="on" onclick="setTheme('light')">Light</button>
<button onclick="setTheme('dark')">Dark</button>
</div>
<button class="bell"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg><div class="bell-dot"></div></button>
<div class="avatar">P</div>
</div>
</header>
<div class="tabs">
<div class="tab on"><span>대시보드</span><button class="tab-x">&times;</button></div>
<div class="tab"><span>DTG 관리</span><button class="tab-x">&times;</button></div>
<div class="tab"><span>사용자 관리</span><button class="tab-x">&times;</button></div>
<div class="tab"><span>정산</span><button class="tab-x">&times;</button></div>
</div>
<div class="body">
<nav class="side side-anim" id="side">
<div class="side-sec">관리</div>
<div class="si on"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></span>대시보드</div>
<div class="si"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg></span>DTG 관리</div>
<div class="si"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg></span>정산</div>
<div class="si"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="3" width="15" height="13"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg></span>물류</div>
<div class="side-sec">설정</div>
<div class="si"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>메뉴 관리</div>
<div class="si"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></span>사용자 관리</div>
<div class="si"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg></span>화면 빌더</div>
<div class="side-sec">분석</div>
<div class="si"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></span>리포트</div>
<div class="si"><span class="ic"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg></span>AI 어시스턴트</div>
</nav>
<main class="content" id="content">
<div class="greet-row" style="animation:cardUp .5s cubic-bezier(.16,1,.3,1) .05s both">
<h2>안녕하세요, Park님</h2><div class="dt">2026. 3. 30 월요일</div>
</div>
<!-- Stats -->
<div class="stats">
<div class="gc" style="animation-delay:.08s">
<div class="kpi-top"><div class="kpi-label">전체 장비</div><div class="kpi-badge up">+12</div></div>
<div class="kpi-val" id="s1">0</div><div class="kpi-spark" id="sp1"></div>
</div>
<div class="gc" style="animation-delay:.12s">
<div class="kpi-top"><div class="kpi-label">가동률</div><div class="kpi-badge up">+0.3%</div></div>
<div class="kpi-val" id="s2">0%</div><div class="kpi-spark" id="sp2"></div>
</div>
<div class="gc" style="animation-delay:.16s">
<div class="kpi-top"><div class="kpi-label">활성 세션</div><div class="kpi-badge dn">-8</div></div>
<div class="kpi-val" id="s3">0</div><div class="kpi-spark" id="sp3"></div>
</div>
<div class="gc" style="animation-delay:.2s">
<div class="kpi-top"><div class="kpi-label">평균 지연</div><div class="kpi-badge up">-3ms</div></div>
<div class="kpi-val" id="s4">0ms</div><div class="kpi-spark" id="sp4"></div>
</div>
</div>
<!-- Widgets -->
<div class="widgets">
<div class="gc" style="animation-delay:.24s">
<div class="gc-head"><div class="gc-t">결재함 <span style="color:var(--red);font-weight:700">3건 대기</span></div><span class="gc-more">전체보기</span></div>
<div class="ap"><div class="ap-l"><div class="dot dot-r"></div><div class="ap-t">DTG-078 설치 승인요청</div></div><div class="ap-time">2분 전</div></div>
<div class="ap"><div class="ap-l"><div class="dot dot-y"></div><div class="ap-t">3월 정산 보고서 검토</div></div><div class="ap-time">14분 전</div></div>
<div class="ap"><div class="ap-l"><div class="dot dot-y"></div><div class="ap-t">신규 사용자 계정 생성</div></div><div class="ap-time">1시간 전</div></div>
<div class="ap"><div class="ap-l"><div class="dot dot-g"></div><div class="ap-t">서버 점검 일정 확인</div></div><div class="ap-time">3시간 전</div></div>
<div class="ap"><div class="ap-l"><div class="dot dot-g"></div><div class="ap-t">DTG-065 펌웨어 업데이트</div></div><div class="ap-time">어제</div></div>
</div>
<div class="gc" style="animation-delay:.28s">
<div class="gc-head"><div class="gc-t">최근 활동 <span class="pulse"></span></div><span class="gc-more">전체보기</span></div>
<div class="act"><div class="act-av" style="background:var(--primary)"></div><div><div class="act-txt"><b>김대리</b>가 DTG-003 → <b>설치중</b></div><div class="act-tm">방금 전</div></div></div>
<div class="act"><div class="act-av" style="background:var(--cyan)"></div><div><div class="act-txt"><b>이과장</b>이 정산 보고서 <b>제출</b></div><div class="act-tm">5분 전</div></div></div>
<div class="act"><div class="act-av" style="background:var(--pink)"></div><div><div class="act-txt"><b>박사원</b>이 메뉴 <b>물류현황</b> 추가</div><div class="act-tm">12분 전</div></div></div>
<div class="act"><div class="act-av" style="background:var(--green)">S</div><div><div class="act-txt"><b>System</b> 자동 백업 완료 (2.3GB)</div><div class="act-tm">30분 전</div></div></div>
<div class="act"><div class="act-av" style="background:var(--amber)"></div><div><div class="act-txt"><b>최부장</b>이 결재 <b>3건 승인</b></div><div class="act-tm">1시간 전</div></div></div>
</div>
<div class="gc" style="animation-delay:.32s">
<div class="gc-head"><div class="gc-t">장비 현황</div><span class="gc-more">상세</span></div>
<div class="donut-w">
<div class="donut"><div class="donut-hole"><div class="dn-num" id="dtot">0</div><div class="dn-sub">전체</div></div></div>
<div class="legend">
<div class="leg"><div class="leg-d" style="background:var(--green)"></div>운행중<span class="leg-v">672</span></div>
<div class="leg"><div class="leg-d" style="background:var(--amber)"></div>대기<span class="leg-v">186</span></div>
<div class="leg"><div class="leg-d" style="background:var(--primary)"></div>설치중<span class="leg-v">189</span></div>
<div class="leg"><div class="leg-d" style="background:var(--red)"></div>오프라인<span class="leg-v">97</span></div>
<div class="leg"><div class="leg-d" style="background:var(--cyan)"></div>동기화<span class="leg-v">103</span></div>
</div>
</div>
</div>
</div>
<!-- Chart + Shortcuts -->
<div class="chart-w">
<div class="gc" style="animation-delay:.36s">
<div class="gc-head"><div class="gc-t">주간 장비 등록</div><span class="gc-more">상세보기</span></div>
<div class="bars" id="chart"></div>
</div>
<div class="gc" style="animation-delay:.4s">
<div class="gc-head"><div class="gc-t">바로가기</div></div>
<div class="sc-grid">
<div class="sc"><div class="sc-ic" style="background:rgba(108,92,231,.1);color:var(--primary)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div><div><div class="sc-t">결재함</div><div class="sc-s">3건 대기</div></div></div>
<div class="sc"><div class="sc-ic" style="background:rgba(0,206,201,.1);color:var(--cyan)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></div><div><div class="sc-t">사용자</div><div class="sc-s">128명 활성</div></div></div>
<div class="sc"><div class="sc-ic" style="background:rgba(253,121,168,.1);color:var(--pink)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/></svg></div><div><div class="sc-t">공지사항</div><div class="sc-s">새 글 2건</div></div></div>
<div class="sc"><div class="sc-ic" style="background:rgba(0,184,148,.1);color:var(--green)"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg></div><div><div class="sc-t">리포트</div><div class="sc-s">이번 주 분석</div></div></div>
</div>
</div>
</div>
<!-- Table -->
<div class="gc" style="animation-delay:.44s">
<div class="tbl-bar">
<div class="tbl-t">최근 등록 장비</div>
<div class="tbl-acts">
<button class="btn-s"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> 검색</button>
<button class="btn-s">Excel</button>
<button class="btn-s pr">+ 장비 등록</button>
</div>
</div>
<div class="tbl-scroll">
<table>
<thead><tr><th>장비코드</th><th>시리얼</th><th>제조사</th><th>모델</th><th>지역</th><th>상태</th><th>최근 통신</th></tr></thead>
<tbody>
<tr style="animation-delay:.48s"><td style="font-weight:600">DTG-001</td><td>0004</td><td>LOOP</td><td>LDT400BS</td><td>서울</td><td><span class="badge bg-g"><span class="pulse"></span>운행중</span></td><td style="color:var(--text-muted)">방금 전</td></tr>
<tr style="animation-delay:.51s"><td style="font-weight:600">DTG-002</td><td>0001</td><td>LOOP</td><td>LDT400BS</td><td>부산</td><td><span class="badge bg-y">대기</span></td><td style="color:var(--text-muted)">3분 전</td></tr>
<tr style="animation-delay:.54s"><td style="font-weight:600">DTG-003</td><td>0003</td><td>LOOP</td><td>LDT400BS</td><td>인천</td><td><span class="badge bg-p">설치중</span></td><td style="color:var(--text-muted)">12분 전</td></tr>
<tr style="animation-delay:.57s"><td style="font-weight:600">DTG-004</td><td>0005</td><td>LOOP</td><td>LDT400BS</td><td>대구</td><td><span class="badge bg-r">오프라인</span></td><td style="color:var(--text-muted)">2시간 전</td></tr>
<tr style="animation-delay:.6s"><td style="font-weight:600">DTG-005</td><td>0007</td><td>LOOP</td><td>LDT500BS</td><td>제주</td><td><span class="badge bg-c">동기화</span></td><td style="color:var(--text-muted)">1분 전</td></tr>
<tr style="animation-delay:.63s"><td style="font-weight:600">DTG-006</td><td>0009</td><td>LOOP</td><td>LDT500BS</td><td>광주</td><td><span class="badge bg-g"><span class="pulse"></span>운행중</span></td><td style="color:var(--text-muted)">방금 전</td></tr>
</tbody>
</table>
</div>
</div>
</main>
</div>
</div>
<div class="toast" id="toast"><div class="toast-bar"></div><div><div class="toast-t">로그인 성공</div><div class="toast-s">Park님 환영합니다</div></div></div>
<script>
// Cosmic background — stars + particles (always running)
(()=>{
const co=document.getElementById('cosmos'),cs=['rgba(162,155,254,.8)','rgba(85,239,196,.7)','rgba(253,121,168,.7)'];
for(let i=0;i<150;i++){const s=document.createElement('div');s.className='star'+(Math.random()>.83?' c':'');
if(s.classList.contains('c'))s.style.setProperty('--sc',cs[Math.random()*3|0]);
s.style.left=Math.random()*100+'%';s.style.top=Math.random()*100+'%';
s.style.setProperty('--d',(2+Math.random()*5)+'s');s.style.setProperty('--dl',Math.random()*5+'s');
s.style.setProperty('--mo',(0.3+Math.random()*.7)+'');co.appendChild(s);}
const pc=['var(--primary)','var(--cyan)','var(--pink)'];
for(let i=0;i<20;i++){const p=document.createElement('div');p.className='particle';
p.style.left=Math.random()*100+'%';p.style.setProperty('--sz',(2+Math.random()*4)+'px');
p.style.setProperty('--pc',pc[Math.random()*3|0]);p.style.setProperty('--fd',(7+Math.random()*12)+'s');
p.style.setProperty('--fdl',Math.random()*10+'s');co.appendChild(p);}
})();
function setTheme(t){
if(window._themeSwitching)return;
const cur=document.documentElement.classList.contains('dark')?'dark':'light';
if(cur===t)return;
window._themeSwitching=true;
const fade=document.getElementById('theme-fade');
// Overlay = TARGET theme color (no mid-switch change = no flicker)
fade.style.background=t==='dark'
?'radial-gradient(ellipse at center,#0c0b18,#06050e)'
:'radial-gradient(ellipse at center,#f3f2fa,#fafaff)';
fade.classList.add('in');
setTimeout(()=>{
document.documentElement.classList.toggle('dark',t==='dark');
document.querySelectorAll('.pill button').forEach(b=>b.classList.toggle('on',(t==='dark'&&b.textContent==='Dark')||(t==='light'&&b.textContent==='Light')));
setTimeout(()=>{fade.classList.remove('in');window._themeSwitching=false;},50);
},420);
}
function countUp(id,end,dur,sfx){
const el=document.getElementById(id),t0=performance.now(),fl=end%1!==0;
!function tick(now){const p=Math.min((now-t0)/dur,1),e=1-Math.pow(1-p,3),v=end*e;
el.textContent=(fl?v.toFixed(1):Math.floor(v).toLocaleString())+sfx;if(p<1)requestAnimationFrame(tick);}(performance.now());
}
function mkSparks(){
if(mkSparks._done)return;mkSparks._done=true;
const d={'sp1':[40,55,45,60,50,65,55,70,60,75,80,85],'sp2':[90,88,91,89,92,90,93,91,94,95,96,98],
'sp3':[60,55,70,65,50,55,45,60,50,40,45,42],'sp4':[25,20,22,18,20,15,18,14,16,13,12,12]};
for(const[id,v]of Object.entries(d)){const el=document.getElementById(id),mx=Math.max(...v);
v.forEach(val=>{const b=document.createElement('div');b.className='kpi-bar';b.style.height=(val/mx*100)+'%';el.appendChild(b);});}
}
function mkChart(){
if(mkChart._done)return;mkChart._done=true;
const el=document.getElementById('chart'),
data=[{l:'월',v:42,c:'var(--primary)'},{l:'화',v:58,c:'var(--primary)'},{l:'수',v:35,c:'var(--primary)'},{l:'목',v:71,c:'var(--cyan)'},{l:'금',v:64,c:'var(--primary)'},{l:'토',v:28,c:'var(--text-muted)'},{l:'일',v:15,c:'var(--text-muted)'}],
mx=Math.max(...data.map(d=>d.v));
data.forEach((d,i)=>{const col=document.createElement('div');col.className='bar-col';
const fill=document.createElement('div');fill.className='bar-fill';fill.style.background=d.c;fill.style.height='0';
setTimeout(()=>{fill.style.height=(d.v/mx*100)+'%';},400+i*80);
const lbl=document.createElement('div');lbl.className='bar-lbl';lbl.textContent=d.l;
col.appendChild(fill);col.appendChild(lbl);el.appendChild(col);});
}
// ===== WARP ENGINE v5 — stars warp FIRST, then main screen appears =====
function doLogin(){
if(window._failAnimRunning||window._warpRunning)return;
const btn=document.getElementById('lbtn');
const pwField=document.getElementById('pw');
// Login fail: password empty
if(!pwField.value.trim()){
window._failAnimRunning=true;
const canvas=document.getElementById('warp-canvas');
const ctx=canvas.getContext('2d');
canvas.width=window.innerWidth;canvas.height=window.innerHeight;
const cxF=canvas.width/2,cyF=canvas.height/2;
const isDk=document.documentElement.classList.contains('dark');
const cosmos=document.getElementById('cosmos');
const card=document.querySelector('.login-card');
const FDUR=1500;
let fStart=null,fDone=false;
function eI(t){return t*t*t;}
function eO(t){return 1-Math.pow(1-t,3);}
function elastic(t){return Math.pow(2,-10*t)*Math.sin((t-0.075)*2*Math.PI/0.3)+1;}
card.style.transition='transform .5s cubic-bezier(.4,0,1,1), opacity .5s, filter .5s';
card.style.transform='scale(0.7)';card.style.opacity='0.3';card.style.filter='blur(6px)';
// 이전 애니메이션 루프 강제 중단
if(window._failAnimId){cancelAnimationFrame(window._failAnimId);window._failAnimId=null;}
// 이전 실패 잔여 상태 초기화
card.classList.remove('denied');
card.style.transition='none';
card.style.transform='scale(1)';
card.style.opacity='1';
card.style.filter='blur(0)';
canvas.classList.remove('active');canvas.style.opacity='';
ctx.clearRect(0,0,canvas.width,canvas.height);
cosmos.style.transition='none';cosmos.style.opacity='1';
void card.offsetWidth;
canvas.classList.add('active');
if(isDk){
// ===== DARK: Star warp failure =====
cosmos.style.transition='opacity .1s';cosmos.style.opacity='0';
const existSt=document.querySelectorAll('#cosmos .star');
const fStars=[];
existSt.forEach(el=>{
const r=el.getBoundingClientRect();
const sx=(r.left+r.width/2-cxF)/cxF,sy=(r.top+r.height/2-cyF)/cyF;
const dist=Math.sqrt(sx*sx+sy*sy)||0.01;
const angle=Math.atan2(sy,sx);
const z=0.5+Math.random()*0.5;
fStars.push({angle,dist:dist*0.5,z,origZ:z,speed:0.005+Math.random()*0.006});
});
function failDarkDraw(now){
if(!fStart)fStart=now;if(fDone)return;
const p=Math.min((now-fStart)/FDUR,1);
const tr=p>0.35?0.5:0.3;
ctx.fillStyle=`rgba(6,5,14,${tr})`;ctx.fillRect(0,0,canvas.width,canvas.height);
if(p<0.4){const fp=eI(p/0.4);const spd=15+fp*70;
for(const s of fStars){s.z-=s.speed*spd*0.016;if(s.z<0.02)s.z=0.02;}}
if(p>=0.4&&!card._bounced){card._bounced=true;
card.style.transition='none';card.style.opacity='1';card.style.filter='blur(0)';card.style.transform='';
card.classList.remove('denied');void card.offsetWidth;card.classList.add('denied');}
if(p>=0.47){const rp=Math.min(1,(p-0.47)/0.53);const ep=elastic(rp);
for(const s of fStars){s.z=s.z+(s.origZ-s.z)*Math.min(1,ep*0.12);}}
let starColor='#ffffff';
if(p>0.38&&p<0.75){const redP=p<0.47?((p-0.38)/0.09):1-eO((p-0.47)/0.28);
starColor=`rgb(255,${Math.round(255-redP*155)},${Math.round(255-redP*155)})`;}
for(const s of fStars){
const x=Math.cos(s.angle)*s.dist,y=Math.sin(s.angle)*s.dist;
const sx=cxF+x/s.z*cxF,sy=cyF+y/s.z*cyF;
const tz=s.z+0.015;const tx=cxF+x/tz*cxF,ty=cyF+y/tz*cyF;
const a=Math.min(0.55,(1-s.z)*1)*Math.min(1,p*5);
const w=Math.max(0.4,Math.min(2,(1-s.z)*1.8));
const len=Math.sqrt((sx-tx)**2+(sy-ty)**2);
if(len>1){ctx.beginPath();ctx.moveTo(tx,ty);ctx.lineTo(sx,sy);
ctx.strokeStyle=starColor;ctx.globalAlpha=a;ctx.lineWidth=w;ctx.lineCap='round';ctx.stroke();
}else{ctx.beginPath();ctx.arc(sx,sy,Math.max(0.5,w*0.6),0,Math.PI*2);
ctx.fillStyle=starColor;ctx.globalAlpha=a;ctx.fill();}
}
ctx.globalAlpha=1;
if(p>0.8)canvas.style.opacity=String(1-eO((p-0.8)/0.2));
if(p>=1){failCleanup();return;}
window._failAnimId=requestAnimationFrame(failDarkDraw);
}
window._failAnimId=requestAnimationFrame(failDarkDraw);
} else {
// ===== LIGHT: Fog engulf failure — fog rushes in then gets blown back =====
cosmos.style.transition='opacity .2s';cosmos.style.opacity='0.3';
// Init fog textures (reuse if already made)
if(!canvas._fogReady){
canvas._fogReady=true;canvas._fogLayers=[];
for(let layer=0;layer<3;layer++){
const fc=document.createElement('canvas');const sz=256;fc.width=sz;fc.height=sz;
const fctx=fc.getContext('2d');const img=fctx.createImageData(sz,sz);const d=img.data;
const grid=32>>(layer);const noise=[];
for(let i=0;i<(grid+1)*(grid+1);i++)noise.push(Math.random());
function lerp(a,b,t){return a+(b-a)*t;}
function ss(t){return t*t*(3-2*t);}
function smp(x,y){const gx=x/sz*grid,gy=y/sz*grid;const ix=Math.floor(gx),iy=Math.floor(gy);
const fx=ss(gx-ix),fy=ss(gy-iy);const g1=grid+1;
return lerp(lerp(noise[(iy%grid)*g1+(ix%grid)],noise[(iy%grid)*g1+((ix+1)%grid)],fx),
lerp(noise[((iy+1)%grid)*g1+(ix%grid)],noise[((iy+1)%grid)*g1+((ix+1)%grid)],fx),fy);}
for(let y=0;y<sz;y++)for(let x=0;x<sz;x++){const v=smp(x,y);const i4=(y*sz+x)*4;
d[i4]=d[i4+1]=d[i4+2]=255;d[i4+3]=v*([100,70,50][layer]);}
fctx.putImageData(img,0,0);canvas._fogLayers.push(fc);
}
}
const W=canvas.width,H=canvas.height;
function failLightDraw(now){
if(!fStart)fStart=now;if(fDone)return;
const p=Math.min((now-fStart)/FDUR,1);
ctx.clearRect(0,0,W,H);
let fogAlpha,xShiftMul;
if(p<0.4){
// Fog rolls in — same as success, engulfing
fogAlpha=eI(p/0.4)*0.85;
xShiftMul=eI(p/0.4)*1;
} else if(p<0.5){
// Pause — max fog
fogAlpha=0.85;xShiftMul=1;
} else {
// Fog quietly retreats — reverses back out
const rp=eO((p-0.5)/0.5);
fogAlpha=0.85*(1-rp);
xShiftMul=1-rp*2.5;
}
// Card denied
if(p>=0.42&&!card._bounced){card._bounced=true;
card.style.transition='none';card.style.opacity='1';card.style.filter='blur(0)';card.style.transform='';
card.classList.remove('denied');void card.offsetWidth;card.classList.add('denied');}
// Tint
ctx.fillStyle=`rgba(235,230,255,${fogAlpha*0.5})`;
ctx.fillRect(0,0,W,H);
// Perlin fog layers — same quality as success
const speeds=[0.7,1.0,1.4];const yOff=[0,-30,20];
for(let i=0;i<3;i++){
const xShift=xShiftMul*speeds[i]*-300;
ctx.globalAlpha=fogAlpha*(0.6-i*0.12);
const texW=W*1.5,texH=H*1.3;const xPos=xShift%(texW/2);
ctx.drawImage(canvas._fogLayers[i],xPos,yOff[i],texW,texH);
ctx.drawImage(canvas._fogLayers[i],xPos+texW/2,yOff[i],texW,texH);
}
ctx.globalAlpha=1;
if(p>0.8)canvas.style.opacity=String(1-eO((p-0.8)/0.2));
if(p>=1){failCleanup();return;}
window._failAnimId=requestAnimationFrame(failLightDraw);
}
window._failAnimId=requestAnimationFrame(failLightDraw);
}
function failCleanup(){
fDone=true;window._failAnimRunning=false;
cosmos.style.transition='none';cosmos.style.opacity='1';
canvas.classList.remove('active');canvas.style.opacity='';
ctx.clearRect(0,0,canvas.width,canvas.height);
card._bounced=false;
setTimeout(()=>{
card.classList.remove('denied');
card.style.transition='none';
card.style.transform='scale(1)';
card.style.opacity='1';
card.style.filter='blur(0)';
},600);
pwField.classList.add('error');
document.getElementById('err-msg').classList.add('show');
setTimeout(()=>{pwField.classList.remove('error');},1500);
setTimeout(()=>{document.getElementById('err-msg').classList.remove('show');},3000);
}
return;
}
if(window._warpRunning)return;window._warpRunning=true;
btn.innerHTML='<span class="spinner"></span>';btn.style.pointerEvents='none';
const canvas=document.getElementById('warp-canvas');
const ctx=canvas.getContext('2d');
canvas.width=window.innerWidth;canvas.height=window.innerHeight;
const cx=canvas.width/2,cy=canvas.height/2;
const isDark=document.documentElement.classList.contains('dark');
// Grab real CSS star positions
const existingStars=document.querySelectorAll('#cosmos .star');
const stars=[];
existingStars.forEach(el=>{
const rect=el.getBoundingClientRect();
const sx=(rect.left+rect.width/2-cx)/cx;
const sy=(rect.top+rect.height/2-cy)/cy;
const dist=Math.sqrt(sx*sx+sy*sy)||0.01;
const angle=Math.atan2(sy,sx);
stars.push({angle,dist:dist*0.5,z:0.6+Math.random()*0.4,speed:0.004+Math.random()*0.006});
});
const cosmos=document.getElementById('cosmos');
const DUR=1300;
let startTime=null,done=false,midFired=false;
function easeIn(t){return t*t*t;}
function easeOut(t){return 1-Math.pow(1-t,3);}
// ALL AT ONCE
const card=document.querySelector('.login-card');
const loginScreen=document.getElementById('login-screen');
// Card pulls in — same as fail start
card.style.transition='transform .5s cubic-bezier(.4,0,1,1), opacity .5s, filter .5s';
card.style.transform='scale(0.7)';card.style.opacity='0';card.style.filter='blur(6px)';
// Screen fades after card
setTimeout(()=>{
loginScreen.style.transition='opacity .3s';
loginScreen.style.opacity='0';
},400);
cosmos.style.transition='opacity .15s';cosmos.style.opacity='0';
canvas.classList.add('active');
// Step 2: Warp animation on canvas (login screen still showing behind)
function draw(now){
if(!startTime)startTime=now;
if(done)return;
const p=Math.min((now-startTime)/DUR,1);
// Speed ramp
let spd;
if(p<0.05) spd=15+easeIn(p/0.05)*20;
else if(p<0.6) spd=35+easeIn((p-0.05)/0.55)*50;
else spd=85-easeOut((p-0.6)/0.4)*70;
if(isDark){
// === DARK: Star warp ===
ctx.fillStyle='rgba(6,5,14,0.35)';
ctx.fillRect(0,0,canvas.width,canvas.height);
for(const s of stars){
const x=Math.cos(s.angle)*s.dist,y=Math.sin(s.angle)*s.dist;
s.z-=s.speed*spd*0.016;
if(s.z<=0.001) s.z=1;
const sx=cx+x/s.z*cx,sy=cy+y/s.z*cy;
const tz=s.z+s.speed*spd*0.016*(1+spd*0.015);
const tx=cx+x/tz*cx,ty=cy+y/tz*cy;
const a=Math.min(0.55,(1-s.z)*1)*Math.min(1,p*5);
const w=Math.max(0.4,(1-s.z)*1.8);
ctx.beginPath();ctx.moveTo(tx,ty);ctx.lineTo(sx,sy);
ctx.strokeStyle='#ffffff';ctx.globalAlpha=a;
ctx.lineWidth=w;ctx.lineCap='round';ctx.stroke();
}
} else {
// === LIGHT: Perlin noise fog ===
const W=canvas.width,H=canvas.height;
// Init fog textures once
if(!canvas._fogReady){
canvas._fogReady=true;
// Generate 3 fog layers using Perlin noise on offscreen canvases
canvas._fogLayers=[];
for(let layer=0;layer<3;layer++){
const fc=document.createElement('canvas');
const sz=256;fc.width=sz;fc.height=sz;
const fctx=fc.getContext('2d');
const img=fctx.createImageData(sz,sz);
const d=img.data;
// Simple value noise with interpolation
const grid=32>>(layer); // 32,16,8 — different scales per layer
const noise=[];for(let i=0;i<(grid+1)*(grid+1);i++)noise.push(Math.random());
function lerp(a,b,t){return a+(b-a)*t;}
function smoothstep(t){return t*t*(3-2*t);}
function sample(x,y){
const gx=x/sz*grid,gy=y/sz*grid;
const ix=Math.floor(gx),iy=Math.floor(gy);
const fx=smoothstep(gx-ix),fy=smoothstep(gy-iy);
const g1=grid+1;
const v00=noise[(iy%grid)*g1+(ix%grid)];
const v10=noise[(iy%grid)*g1+((ix+1)%grid)];
const v01=noise[((iy+1)%grid)*g1+(ix%grid)];
const v11=noise[((iy+1)%grid)*g1+((ix+1)%grid)];
return lerp(lerp(v00,v10,fx),lerp(v01,v11,fx),fy);
}
for(let y=0;y<sz;y++)for(let x=0;x<sz;x++){
const v=sample(x,y);
const i=(y*sz+x)*4;
d[i]=d[i+1]=d[i+2]=255;
d[i+3]=v*([100,70,50][layer]); // alpha varies per layer
}
fctx.putImageData(img,0,0);
canvas._fogLayers.push(fc);
}
}
// Animate fog layers
let fogAlpha,xShiftMul;
if(p<0.3){
// Fog creeps in from sides
fogAlpha=easeIn(p/0.3)*0.85;
xShiftMul=easeIn(p/0.3);
} else if(p<0.65){
// Full fog, rushing through
fogAlpha=0.85;
xShiftMul=1+(p-0.3)/0.35*3;
} else {
// Clears
const cp=(p-0.65)/0.35;
fogAlpha=0.85*(1-easeOut(cp));
xShiftMul=4+cp*3;
}
// Tint base
ctx.fillStyle=`rgba(235,230,255,${fogAlpha*0.5})`;
ctx.fillRect(0,0,W,H);
// Draw 3 fog layers at different speeds/positions
const speeds=[0.7,1.0,1.4];
const yOffsets=[0,-30,20];
for(let i=0;i<3;i++){
const xShift=xShiftMul*speeds[i]*-300;
ctx.globalAlpha=fogAlpha*(0.6-i*0.12);
// Tile the texture across for seamless scroll
const texW=W*1.5,texH=H*1.3;
const xPos=xShift%(texW/2);
ctx.drawImage(canvas._fogLayers[i],xPos,yOffsets[i],texW,texH);
ctx.drawImage(canvas._fogLayers[i],xPos+texW/2,yOffsets[i],texW,texH);
}
ctx.globalAlpha=1;
}
ctx.globalAlpha=1;
// At 70%: main screen starts appearing — blurry, from far away
if(p>0.6&&!midFired){
midFired=true;
const main=document.getElementById('main-screen');
main.classList.remove('hidden');
main.style.opacity='0';main.style.filter='blur(6px)';
main.style.transition='opacity .4s ease-out, filter .4s ease-out';
requestAnimationFrame(()=>{main.style.opacity='1';main.style.filter='blur(0)';});
canvas.style.transition='opacity .4s ease-out';
canvas.style.opacity='0';
cosmos.style.transition='opacity .4s';cosmos.style.opacity='1';
countUp('s1',1247,1200,'');countUp('s2',98.5,1400,'%');
countUp('s3',342,1000,'');countUp('s4',12,800,'ms');
countUp('dtot',1247,1500,'');mkSparks();mkChart();
document.querySelectorAll('#side .si').forEach((n,i)=>{n.style.animationDelay=(.1+i*.04)+'s';});
}
// Step 3: Warp done → cleanup
if(p>=1&&!done){
done=true;
// Hide login completely
document.getElementById('login-screen').style.opacity='0';
document.getElementById('login-screen').style.pointerEvents='none';
// Kill canvas
canvas.classList.remove('active');canvas.style.transition='';canvas.style.opacity='';
ctx.clearRect(0,0,canvas.width,canvas.height);
document.getElementById('login-screen').classList.add('hidden');
const main=document.getElementById('main-screen');
main.style.transition='';main.style.opacity='';main.style.filter='';
// Toast notification
setTimeout(()=>{const t=document.getElementById('toast');t.classList.add('show');
setTimeout(()=>t.classList.remove('show'),3000);},300);
return;
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
}
document.getElementById('lbtn').addEventListener('mousedown',function(e){
const r=this.getBoundingClientRect(),d=document.createElement('div');d.className='rip';
const sz=Math.max(r.width,r.height)*2;d.style.width=d.style.height=sz+'px';
d.style.left=(e.clientX-r.left-sz/2)+'px';d.style.top=(e.clientY-r.top-sz/2)+'px';
this.appendChild(d);setTimeout(()=>d.remove(),600);});
document.querySelectorAll('.si').forEach(n=>n.addEventListener('click',function(){
document.querySelectorAll('.si').forEach(i=>i.classList.remove('on'));this.classList.add('on');}));
document.querySelectorAll('.tab').forEach(t=>t.addEventListener('click',function(e){
if(e.target.classList.contains('tab-x'))return;
document.querySelectorAll('.tab').forEach(i=>i.classList.remove('on'));this.classList.add('on');}));
document.querySelectorAll('.tab-x').forEach(x=>x.addEventListener('click',function(e){
e.stopPropagation();const tab=this.closest('.tab');if(!tab)return;
const wasOn=tab.classList.contains('on');tab.remove();
if(wasOn){const first=document.querySelector('.tab');if(first)first.classList.add('on');}
}));
</script>
</body>
</html>