mirror of
https://github.com/headporter81/specialsource-homepage-frontend.git
synced 2026-08-08 15:21:11 +09:00
This commit is contained in:
+112
-68
@@ -5,8 +5,12 @@
|
|||||||
<span class="status-dot" :class="{ 'secure-mode': isLoggedIn }"></span>
|
<span class="status-dot" :class="{ 'secure-mode': isLoggedIn }"></span>
|
||||||
<span class="brand">SPECIALSOURCE.SYSTEM</span>
|
<span class="brand">SPECIALSOURCE.SYSTEM</span>
|
||||||
|
|
||||||
|
<div v-if="isLoggedIn" class="nav-stats">
|
||||||
|
[LIVE:<span class="pulse-text">{{ activeNow }}</span>|TODAY:{{ todayUnique }}]
|
||||||
|
</div>
|
||||||
|
|
||||||
<select v-model="activeTheme" class="theme-selector">
|
<select v-model="activeTheme" class="theme-selector">
|
||||||
<option value="green">P3_GREEN (녹색)</option>
|
<option value="green">P3_GREEN (기본)</option>
|
||||||
<option value="amber">HERCULES (호박)</option>
|
<option value="amber">HERCULES (호박)</option>
|
||||||
<option value="vga">VGA_GRAY (회색)</option>
|
<option value="vga">VGA_GRAY (회색)</option>
|
||||||
<option value="ega">EGA_CYAN (청록)</option>
|
<option value="ega">EGA_CYAN (청록)</option>
|
||||||
@@ -30,14 +34,12 @@
|
|||||||
<button @click="handleRegister" class="nav-item btn-reg">SIGN UP</button>
|
<button @click="handleRegister" class="nav-item btn-reg">SIGN UP</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="auth-group">
|
<div v-else class="auth-group">
|
||||||
<span class="user-info">ACCESS GRANTED: {{ loggedInUser.toUpperCase() }}</span>
|
<span class="user-info">ACCESS GRANTED: ADMIN</span>
|
||||||
<button @click="handleLogout" class="nav-item btn-logout">LOGOUT</button>
|
<button @click="handleLogout" class="nav-item btn-logout">LOGOUT</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<nav class="auth-nav" style="display: none;"></nav>
|
|
||||||
|
|
||||||
<main class="content-area">
|
<main class="content-area">
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@
|
|||||||
type="text"
|
type="text"
|
||||||
class="login-input"
|
class="login-input"
|
||||||
placeholder="ENTER ACCESS ID"
|
placeholder="ENTER ACCESS ID"
|
||||||
autocomplete="off" @keyup.enter="handleLoginSubmit"
|
@keyup.enter="handleLoginSubmit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-input-group">
|
<div class="login-input-group">
|
||||||
@@ -87,7 +89,7 @@
|
|||||||
type="password"
|
type="password"
|
||||||
class="login-input"
|
class="login-input"
|
||||||
placeholder="ENTER ENCRYPTION KEY"
|
placeholder="ENTER ENCRYPTION KEY"
|
||||||
autocomplete="new-password" @keyup.enter="handleLoginSubmit"
|
@keyup.enter="handleLoginSubmit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button @click="handleLoginSubmit" class="login-submit-btn">
|
<button @click="handleLoginSubmit" class="login-submit-btn">
|
||||||
@@ -139,14 +141,14 @@
|
|||||||
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="isProcessing" class="tui-global-overlay">
|
<div v-if="isProcessing" class="tui-global-overlay">
|
||||||
<TuiProgressBar :progress="overlayProgress" :status="overlayStatus" :username="loggedInUser" />
|
<TuiProgressBar :progress="overlayProgress" :status="overlayStatus" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, watch } from 'vue'
|
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import TuiProgressBar from '@/components/TuiProgressBar.vue';
|
import TuiProgressBar from '@/components/TuiProgressBar.vue';
|
||||||
|
|
||||||
@@ -159,12 +161,16 @@ const loginPw = ref('');
|
|||||||
|
|
||||||
const activeMenu = ref('main');
|
const activeMenu = ref('main');
|
||||||
const authToken = ref(localStorage.getItem('token'));
|
const authToken = ref(localStorage.getItem('token'));
|
||||||
const activeTheme = ref(localStorage.getItem('theme') || 'green');
|
const activeTheme = ref('green');
|
||||||
|
|
||||||
const isProcessing = ref(false);
|
const isProcessing = ref(false);
|
||||||
const overlayProgress = ref(0);
|
const overlayProgress = ref(0);
|
||||||
const overlayStatus = ref('');
|
const overlayStatus = ref('');
|
||||||
|
|
||||||
|
const todayUnique = ref(0);
|
||||||
|
const activeNow = ref(0);
|
||||||
|
let analyticsTimer = null;
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ id: 'main', name: 'HOME' },
|
{ id: 'main', name: 'HOME' },
|
||||||
{ id: 'youtube', name: 'YOUTUBE' },
|
{ id: 'youtube', name: 'YOUTUBE' },
|
||||||
@@ -173,9 +179,6 @@ const menuItems = [
|
|||||||
{ id: 'contact', name: 'CONTACT' }
|
{ id: 'contact', name: 'CONTACT' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const loggedInUser = ref(localStorage.getItem('username') || 'anonymous');
|
|
||||||
|
|
||||||
// 🟢 [★복구] 유실되었던 로그인 상태 체크 computed 함수를 제자리에 원상 복구 완료
|
|
||||||
const isLoggedIn = computed(() => !!authToken.value);
|
const isLoggedIn = computed(() => !!authToken.value);
|
||||||
|
|
||||||
const rawAscii = `
|
const rawAscii = `
|
||||||
@@ -202,6 +205,19 @@ const formattedAscii = computed(() => {
|
|||||||
return rawAscii.replace(/ /g, '<span class="bg-block">█</span>');
|
return rawAscii.replace(/ /g, '<span class="bg-block">█</span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const fetchAnalyticsData = async () => {
|
||||||
|
try {
|
||||||
|
const apiBaseUrl = import.meta.env.VITE_API_URL || '';
|
||||||
|
const res = await axios.get(`${apiBaseUrl}/api/analytics/live`, { withCredentials: true });
|
||||||
|
if (res.data) {
|
||||||
|
todayUnique.value = res.data.todayUnique || 0;
|
||||||
|
activeNow.value = res.data.activeNow || 0;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("관제 통계 데이터 로드 실패:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const changeMenu = (id) => {
|
const changeMenu = (id) => {
|
||||||
if (activeMenu.value === id) return;
|
if (activeMenu.value === id) return;
|
||||||
|
|
||||||
@@ -261,37 +277,16 @@ const triggerOverlayProcess = (durationMs, initialMsg) => {
|
|||||||
}, stepTime);
|
}, stepTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
const apiKey = import.meta.env.VITE_YOUTUBE_API_KEY;
|
const apiKey = 'AIzaSyD51LHwqoY8spjq6rsY_RlhBXzb96j7D6o';
|
||||||
const channelId = 'UC8L92ju0V88s6TcmLPb8Bkg';
|
const channelId = 'UC8L92ju0V88s6TcmLPb8Bkg';
|
||||||
const maxResults = 8;
|
const maxResults = 8;
|
||||||
const apiUrl = `https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${channelId}&maxResults=${maxResults}&order=date&type=video&key=${apiKey}`;
|
const apiUrl = `https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${channelId}&maxResults=${maxResults}&order=date&type=video&key=${apiKey}`;
|
||||||
|
|
||||||
const fetchVideos = async () => {
|
const fetchVideos = async () => {
|
||||||
try {
|
try {
|
||||||
// 💡 유튜브 규칙상 채널 ID의 앞 글자 'UC'를 'UU'로 바꾸면
|
const res = await axios.get(apiUrl);
|
||||||
// 해당 채널의 '전체 업로드 영상 플레이리스트' ID가 됩니다. (비용 100유닛 -> 1유닛으로 단축)
|
videos.value = res.data.items || [];
|
||||||
const uploadPlaylistId = channelId.replace(/^UC/, 'UU');
|
} catch (err) { console.error("유튜브 로드 실패:", err); }
|
||||||
const playlistApiUrl = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=${uploadPlaylistId}&maxResults=${maxResults}&key=${apiKey}`;
|
|
||||||
|
|
||||||
const res = await axios.get(playlistApiUrl);
|
|
||||||
|
|
||||||
// 🌟 [매핑 매직] 유저님이 짜두신 기존 <template>의 video.id.videoId 및 video.snippet 구조와
|
|
||||||
// 100% 호환되도록 데이터 골격을 가공하여 videos 배열에 주입합니다.
|
|
||||||
if (res.data && res.data.items) {
|
|
||||||
videos.value = res.data.items.map(item => ({
|
|
||||||
id: { videoId: item.snippet.resourceId?.videoId },
|
|
||||||
snippet: item.snippet
|
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
videos.value = [];
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("유튜브 로드 실패:", err);
|
|
||||||
// 에러 원인 추적용 가이드 로그
|
|
||||||
if (err.response && err.response.status === 403) {
|
|
||||||
console.warn("⚠️ [인프라 경고] 유튜브 API 할당량이 만료되었거나, 구글에 의해 API 키가 차단되었습니다.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openVideo = (id) => window.open(`https://www.youtube.com/watch?v=${id}`);
|
const openVideo = (id) => window.open(`https://www.youtube.com/watch?v=${id}`);
|
||||||
@@ -299,15 +294,10 @@ const handleRegister = () => alert("회원가입은 현재 관리자 승인제
|
|||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
localStorage.removeItem('username');
|
|
||||||
|
|
||||||
authToken.value = null;
|
authToken.value = null;
|
||||||
loggedInUser.value = 'anonymous';
|
|
||||||
activeMenu.value = 'main';
|
activeMenu.value = 'main';
|
||||||
|
|
||||||
if (termInstance.value) {
|
if (termInstance.value) {
|
||||||
termInstance.value.echo(`\n[[b;red;][ALERT]] SESSION TERMINATED. USER LOGGED OUT.`);
|
termInstance.value.echo(`\n[[b;red;][ALERT]] SESSION TERMINATED. USER LOGGED OUT.`);
|
||||||
termInstance.value.set_prompt(`anonymous@specialsource:~$ `);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -340,18 +330,14 @@ const handleLoginSubmit = async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.data && response.data.token) {
|
if (response.data && response.data.token) {
|
||||||
localStorage.setItem('token', response.data.token);
|
|
||||||
localStorage.setItem('username', loginId.value);
|
|
||||||
|
|
||||||
authToken.value = response.data.token;
|
|
||||||
loggedInUser.value = loginId.value;
|
|
||||||
|
|
||||||
overlayProgress.value = 85;
|
overlayProgress.value = 85;
|
||||||
overlayStatus.value = 'Access Granted. Generating JWT Session token...';
|
overlayStatus.value = 'Access Granted. Generating JWT Session token...';
|
||||||
|
|
||||||
|
localStorage.setItem('token', response.data.token);
|
||||||
|
authToken.value = response.data.token;
|
||||||
|
|
||||||
if (termInstance.value) {
|
if (termInstance.value) {
|
||||||
termInstance.value.echo(`[[b;var(--tui-color);][SUCCESS]] ACCESS GRANTED. CREDENTIALS VERIFIED.`);
|
termInstance.value.echo(`[[b;var(--tui-color);][SUCCESS]] ACCESS GRANTED. CREDENTIALS VERIFIED.`);
|
||||||
termInstance.value.set_prompt(`${loginId.value}@specialsource:~$ `);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
overlayProgress.value = 100;
|
overlayProgress.value = 100;
|
||||||
@@ -378,24 +364,59 @@ const handleLoginSubmit = async () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchVideos();
|
fetchVideos();
|
||||||
|
|
||||||
|
fetchAnalyticsData();
|
||||||
|
analyticsTimer = setInterval(fetchAnalyticsData, 30000);
|
||||||
|
|
||||||
const globaljQuery = window.$;
|
const globaljQuery = window.$;
|
||||||
if (globaljQuery && terminalRef.value) {
|
if (globaljQuery && terminalRef.value) {
|
||||||
termInstance.value = globaljQuery(terminalRef.value).terminal({
|
termInstance.value = globaljQuery(terminalRef.value).terminal({
|
||||||
'hello': function () { this.echo('Hello, Welcome to Specialsource.company'); },
|
'hello': function () { this.echo('Hello, Welcome to Specialsource.company'); },
|
||||||
'help': function () {
|
|
||||||
this.echo(`- hello : Say hello\n- specialsource : Core info\n- email : Contact\n- clear : Clear console`);
|
|
||||||
},
|
|
||||||
'specialsource': function () { this.echo('SPECIALSOURCE Established 2021.01.'); },
|
'specialsource': function () { this.echo('SPECIALSOURCE Established 2021.01.'); },
|
||||||
'email': function () { this.echo('specialsource.company@gmail.com'); },
|
'email': function () { this.echo('specialsource.company@gmail.com'); },
|
||||||
|
|
||||||
|
// 🟢 [NEW] 리얼타임 레이아웃 텍스트 순차 타이핑 프린팅 기믹 함수 구현
|
||||||
|
'status': function () {
|
||||||
|
const term = this;
|
||||||
|
term.pause(); // 타이핑 진행 중엔 추가 명령어 입력을 잠금(인프라 보호 감성)
|
||||||
|
|
||||||
|
// 프린팅할 덤프 행렬 배열화
|
||||||
|
const lines = [
|
||||||
|
"============================================",
|
||||||
|
" 📟 SPECIALSOURCE TRAFFIC ARCHITECTURE",
|
||||||
|
"============================================",
|
||||||
|
`- CURRENT ACTIVE USERS (5m) : ${activeNow.value} ACTIVE`,
|
||||||
|
`- TOTAL UNIQUE VISITORS TODAY : ${todayUnique.value} UVs`,
|
||||||
|
"--------------------------------------------"
|
||||||
|
];
|
||||||
|
|
||||||
|
term.echo("\n[SYS] INITIALIZING SECURE PIPELINE CHANNEL...");
|
||||||
|
term.echo("[SYS] EXTRACTING POSTGRESQL METADATA... SNAPSHOT READY.");
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
// 110ms 간격으로 스태거드 라인 출력 기믹 기동
|
||||||
|
const typingInterval = setInterval(() => {
|
||||||
|
if (index < lines.length) {
|
||||||
|
term.echo(lines[index]);
|
||||||
|
index++;
|
||||||
|
} else {
|
||||||
|
clearInterval(typingInterval);
|
||||||
|
term.resume(); // 타이핑 종료 후 유저 입력 언락
|
||||||
|
}
|
||||||
|
}, 110);
|
||||||
|
},
|
||||||
|
|
||||||
|
'help': function () {
|
||||||
|
this.echo(`- hello : Say hello\n- specialsource : Core info\n- email : Contact\n- status : Check Live Connected Users (접속자 확인)\n- clear : Clear console`);
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
greetings: `[SYSTEM V3.2] INTERFACE ONLINE. TYPE 'help' FOR COMMANDS.`,
|
greetings: `[SYSTEM V3.2] INTERFACE ONLINE. TYPE 'help' FOR COMMANDS.`,
|
||||||
prompt: `${loggedInUser.value}@specialsource:~$ `
|
prompt: 'admin@specialsource:~$ '
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(activeTheme, (newTheme) => {
|
onUnmounted(() => {
|
||||||
localStorage.setItem('theme', newTheme);
|
if (analyticsTimer) clearInterval(analyticsTimer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -412,12 +433,13 @@ watch(activeTheme, (newTheme) => {
|
|||||||
.about-text,
|
.about-text,
|
||||||
.contact-form,
|
.contact-form,
|
||||||
.theme-selector,
|
.theme-selector,
|
||||||
|
.nav-stats,
|
||||||
:deep(.terminal),
|
:deep(.terminal),
|
||||||
:deep(.terminal span) {
|
:deep(.terminal span) {
|
||||||
font-family: 'JetBrains Mono', monospace !important;
|
font-family: 'JetBrains Mono', monospace !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 🌟 레트로 마스터 테마 컬러 시스템 변수 */
|
/* 변수 모음 */
|
||||||
.theme-green {
|
.theme-green {
|
||||||
--tui-color: #00ff00;
|
--tui-color: #00ff00;
|
||||||
--tui-dim: #008800;
|
--tui-dim: #008800;
|
||||||
@@ -458,6 +480,23 @@ watch(activeTheme, (newTheme) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-stats {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--tui-color);
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px dashed var(--tui-border);
|
||||||
|
padding: 3px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-text {
|
||||||
|
color: var(--tui-color);
|
||||||
|
font-weight: bold;
|
||||||
|
animation: textPulse 2s infinite ease-in-out;
|
||||||
|
margin: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.theme-selector {
|
.theme-selector {
|
||||||
background: #111;
|
background: #111;
|
||||||
border: 1px solid var(--tui-border);
|
border: 1px solid var(--tui-border);
|
||||||
@@ -705,20 +744,25 @@ iframe { width: 100%; height: 180px; border-radius: 4px; }
|
|||||||
}
|
}
|
||||||
@keyframes blink { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
|
@keyframes blink { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
|
||||||
|
|
||||||
/* 🟢 [★완벽 교정] 모든 주요 타임라인 프레임에 color: var(--tui-color)를 강제 각인하여 테마 실시간 연동 보장 */
|
@keyframes textPulse {
|
||||||
|
0%, 100% { opacity: 0.3; }
|
||||||
|
50% { opacity: 1; text-shadow: 0 0 8px var(--tui-glow); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 일회성 사이버 글리치 키프레임 */
|
||||||
@keyframes once-cyber-glitch-wobble {
|
@keyframes once-cyber-glitch-wobble {
|
||||||
0%, 25% { transform: translate(0) scale(1) skewX(0deg); color: var(--tui-color); text-shadow: 0 0 8px var(--tui-glow); opacity: 1; }
|
0%, 25% { transform: translate(0) scale(1) skewX(0deg); color: var(--tui-color) !important; text-shadow: 0 0 8px var(--tui-glow) !important; opacity: 1; }
|
||||||
26% { transform: translate(-3px, 1.5px) scale(1.02); color: #ffffff; text-shadow: 2px 0 #ff0000, -2px 0 #0000ff; }
|
26% { transform: translate(-3px, 1.5px) scale(1.02); color: #ffffff !important; text-shadow: 2px 0 #ff0000, -2px 0 #0000ff !important; }
|
||||||
27% { transform: translate(3px, -1.5px) skewX(-2deg); color: var(--tui-color); }
|
27% { transform: translate(3px, -1.5px) skewX(-2deg); color: var(--tui-color) !important; }
|
||||||
28% { transform: translate(-5px, 0) skewX(10deg); color: #ff00ff; text-shadow: -3px 0 magenta, 3px 0 cyan;}
|
28% { transform: translate(-5px, 0) skewX(10deg); color: #ff00ff !important; text-shadow: -3px 0 magenta, 3px 0 cyan;}
|
||||||
29% { transform: translate(5px, 0) skewX(-10deg); color: #00ffff; text-shadow: 3px 0 magenta, -3px 0 cyan;}
|
29% { transform: translate(5px, 0) skewX(-10deg); color: #00ffff !important; text-shadow: 3px 0 magenta, -3px 0 cyan;}
|
||||||
30% { transform: translate(0); color: var(--tui-color); text-shadow: 0 0 15px var(--tui-color);}
|
30% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 15px var(--tui-color) !important;}
|
||||||
31%, 50% { transform: translate(0); color: var(--tui-color); text-shadow: 0 0 8px var(--tui-glow); }
|
31%, 50% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 8px var(--tui-glow) !important;}
|
||||||
51% { transform: scale(1.05) skewY(1deg); color: #ffffff; opacity: 0.05; }
|
51% { transform: scale(1.05) skewY(1deg); color: #ffffff !important; opacity: 0.05; }
|
||||||
53% { opacity: 1; transform: translate(0); color: var(--tui-color); text-shadow: 0 0 20px var(--tui-color), 0 0 40px var(--tui-glow); }
|
53% { opacity: 1; transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 20px var(--tui-color), 0 0 40px var(--tui-glow) !important; }
|
||||||
90% { transform: translate(-2px, 1px); text-shadow: 2px 0 red, -2px 0 blue;}
|
90% { transform: translate(-2px, 1px); text-shadow: 2px 0 red, -2px 0 blue;}
|
||||||
92% { transform: translate(2px, -1px); text-shadow: -2px 0 red, 2px 0 blue;}
|
92% { transform: translate(2px, -1px); text-shadow: -2px 0 red, 2px 0 blue;}
|
||||||
95% { transform: translate(0); color: var(--tui-color); text-shadow: 0 0 20px var(--tui-color); }
|
95% { transform: translate(0); color: var(--tui-color) !important; text-shadow: 0 0 20px var(--tui-color) !important;}
|
||||||
100% { transform: translate(0) scale(1) skewX(0deg); color: var(--tui-color); text-shadow: 0 0 15px var(--tui-color), 0 0 30px var(--tui-glow); opacity: 1; }
|
100% { transform: translate(0) scale(1) skewX(0deg); color: var(--tui-color) !important; text-shadow: 0 0 15px var(--tui-color), 0 0 30px var(--tui-glow) !important; opacity: 1; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user