Refactor static file serving in WebUI to allow CSS and JS access without authentication, and implement theme toggle functionality in login and main pages

This commit is contained in:
Andy Oknen 2025-07-30 13:21:30 +00:00
parent fc354865ea
commit 3187114780
7 changed files with 408 additions and 126 deletions

View file

@ -15,11 +15,20 @@ import (
// setupStaticHandler configures static file serving for development (files from disk)
func setupStaticHandler(mux *http.ServeMux, server *WebUIServer) {
// Serve static files from disk for development - with auth
// Serve static files from disk for development
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("src/webui/static/")))
mux.HandleFunc("/static/", server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/static/", func(rw http.ResponseWriter, r *http.Request) {
// Allow access to CSS and JS files without auth (needed for login page)
path := strings.TrimPrefix(r.URL.Path, "/static/")
if strings.HasSuffix(path, ".css") || strings.HasSuffix(path, ".js") {
staticHandler.ServeHTTP(rw, r)
}))
return
}
// For other static files, require auth
server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
staticHandler.ServeHTTP(rw, r)
})(rw, r)
})
}
// serveFile serves any file from disk or returns 404 if not found

View file

@ -25,12 +25,22 @@ func setupStaticHandler(mux *http.ServeMux, server *WebUIServer) {
panic("failed to get embedded static files: " + err.Error())
}
// Serve static files from embedded FS - with auth
// Serve static files from embedded FS
staticHandler := http.FileServer(http.FS(staticFS))
mux.HandleFunc("/static/", server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
mux.HandleFunc("/static/", func(rw http.ResponseWriter, r *http.Request) {
// Allow access to CSS and JS files without auth (needed for login page)
path := strings.TrimPrefix(r.URL.Path, "/static/")
if strings.HasSuffix(path, ".css") || strings.HasSuffix(path, ".js") {
// Strip the /static/ prefix before serving
http.StripPrefix("/static/", staticHandler).ServeHTTP(rw, r)
}))
return
}
// For other static files, require auth
server.authMiddleware(func(rw http.ResponseWriter, r *http.Request) {
// Strip the /static/ prefix before serving
http.StripPrefix("/static/", staticHandler).ServeHTTP(rw, r)
})(rw, r)
})
}
// serveFile serves any file from embedded files or returns 404 if not found

View file

@ -19,7 +19,10 @@
<p data-key="subtitle">Network mesh management dashboard</p>
</div>
<div class="header-actions">
<div class="language-switcher">
<div class="controls-group">
<button onclick="toggleTheme()" class="theme-btn" id="theme-btn">
<span class="theme-icon">🌙</span>
</button>
<button onclick="switchLanguage('ru')" class="lang-btn" id="lang-ru">RU</button>
<button onclick="switchLanguage('en')" class="lang-btn" id="lang-en">EN</button>
</div>
@ -126,6 +129,7 @@
<script>
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
function updateTexts() {
const elements = document.querySelectorAll('[data-key]');
@ -137,6 +141,28 @@
});
}
function toggleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
localStorage.setItem('yggdrasil-theme', currentTheme);
applyTheme();
}
function applyTheme() {
const body = document.body;
const themeIcon = document.querySelector('.theme-icon');
const themeBtn = document.getElementById('theme-btn');
if (currentTheme === 'dark') {
body.setAttribute('data-theme', 'dark');
themeIcon.textContent = '☀️';
themeBtn.title = window.translations[currentLanguage]['theme_light'] || 'Light theme';
} else {
body.removeAttribute('data-theme');
themeIcon.textContent = '🌙';
themeBtn.title = window.translations[currentLanguage]['theme_dark'] || 'Dark theme';
}
}
function switchLanguage(lang) {
currentLanguage = lang;
localStorage.setItem('yggdrasil-language', lang);
@ -146,6 +172,7 @@
document.getElementById('lang-' + lang).classList.add('active');
updateTexts();
applyTheme(); // Update theme button tooltip
}
function logout() {
@ -174,12 +201,14 @@
event.target.closest('.nav-item').classList.add('active');
}
// Initialize language on page load
// Initialize language and theme on page load
document.addEventListener('DOMContentLoaded', function () {
// Set active language button
document.getElementById('lang-' + currentLanguage).classList.add('active');
// Update all texts
updateTexts();
// Apply saved theme
applyTheme();
});
</script>
</body>

View file

@ -29,5 +29,12 @@ window.translations.en = {
'network_settings_description': 'Network interaction parameters',
'coming_soon': 'Coming soon...',
'footer_text': 'Yggdrasil Network • Minimal WebUI v1.0',
'logout_confirm': 'Are you sure you want to logout?'
'logout_confirm': 'Are you sure you want to logout?',
'theme_light': 'Light theme',
'theme_dark': 'Dark theme',
'login_subtitle': 'Enter password to access the web interface',
'password_label': 'Password:',
'access_dashboard': 'Access Dashboard',
'error_invalid_password': 'Invalid password. Please try again.',
'error_too_many_attempts': 'Too many failed attempts. Please wait 1 minute before trying again.'
};

View file

@ -29,5 +29,12 @@ window.translations.ru = {
'network_settings_description': 'Параметры сетевого взаимодействия',
'coming_soon': 'Функция в разработке...',
'footer_text': 'Yggdrasil Network • Minimal WebUI v1.0',
'logout_confirm': 'Вы уверены, что хотите выйти?'
'logout_confirm': 'Вы уверены, что хотите выйти?',
'theme_light': 'Светлая тема',
'theme_dark': 'Темная тема',
'login_subtitle': 'Введите пароль для доступа к веб-интерфейсу',
'password_label': 'Пароль:',
'access_dashboard': 'Войти в панель',
'error_invalid_password': 'Неверный пароль. Попробуйте снова.',
'error_too_many_attempts': 'Слишком много неудачных попыток. Подождите 1 минуту перед повторной попыткой.'
};

View file

@ -6,7 +6,18 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Yggdrasil Web Interface - Login</title>
<link rel="stylesheet" href="static/style.css">
<script src="static/lang/ru.js"></script>
<script src="static/lang/en.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
min-height: 100vh;
color: var(--text-primary);
margin: 0;
padding: 0;
}
.login-container {
display: flex;
align-items: center;
@ -16,24 +27,27 @@
}
.login-form {
background: white;
border-radius: 12px;
background: var(--bg-info-card);
border-radius: 4px;
padding: 40px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px var(--shadow-dark);
border: 1px solid var(--border-card);
width: 100%;
max-width: 400px;
text-align: center;
}
.login-form h1 {
color: #495057;
color: var(--text-heading);
margin-bottom: 10px;
font-size: 2rem;
font-weight: 700;
}
.login-form p {
color: #6c757d;
color: var(--text-body);
margin-bottom: 30px;
font-weight: 500;
}
.form-group {
@ -44,95 +58,164 @@
.form-group label {
display: block;
margin-bottom: 5px;
color: #495057;
font-weight: 500;
color: var(--text-heading);
font-weight: 600;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #ced4da;
border-radius: 6px;
border: 1px solid var(--border-card);
border-radius: 2px;
font-size: 16px;
transition: border-color 0.2s ease;
background: var(--bg-status-card);
color: var(--text-body);
font-weight: 500;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
border-color: var(--border-hover);
box-shadow: 0 1px 4px var(--shadow-light);
}
.form-group input:disabled {
background-color: #f8f9fa;
border-color: #e9ecef;
color: #6c757d;
background-color: var(--bg-nav-item);
border-color: var(--border-nav-item);
color: var(--text-muted);
cursor: not-allowed;
}
.login-button {
width: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px;
border-radius: 6px;
background: var(--bg-nav-active);
color: var(--text-white);
border: 1px solid var(--bg-nav-active-border);
padding: 12px 16px;
border-radius: 2px;
font-size: 16px;
font-weight: 500;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
transition: background 0.2s ease, box-shadow 0.2s ease;
}
.login-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.login-button:active {
transform: translateY(0);
background: var(--bg-nav-active-border);
box-shadow: 0 2px 8px var(--shadow-medium);
}
.error-message {
background: #f8d7da;
color: #721c24;
border: 1px solid #f1aeb5;
border-radius: 6px;
padding: 10px;
background: var(--bg-status-card);
color: var(--bg-logout);
border: 1px solid var(--border-logout);
border-radius: 2px;
padding: 12px;
margin-bottom: 20px;
display: none;
font-weight: 500;
}
.lock-icon {
font-size: 3rem;
margin-bottom: 20px;
color: #667eea;
color: var(--bg-nav-active);
}
.login-header {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
}
.login-container {
position: relative;
z-index: 1;
}
</style>
</head>
<body>
<div class="login-header">
<div class="controls-group">
<button onclick="toggleTheme()" class="theme-btn" id="theme-btn">
<span class="theme-icon">🌙</span>
</button>
<button onclick="switchLanguage('ru')" class="lang-btn" id="lang-ru">RU</button>
<button onclick="switchLanguage('en')" class="lang-btn" id="lang-en">EN</button>
</div>
</div>
<div class="login-container">
<form class="login-form" onsubmit="return handleLogin(event)">
<div class="lock-icon">🔒</div>
<h1>🌳 Yggdrasil</h1>
<p>Enter password to access the web interface</p>
<h1 data-key="title">🌳 Yggdrasil</h1>
<p data-key="login_subtitle">Enter password to access the web interface</p>
<div class="error-message" id="errorMessage">
Invalid password. Please try again.
<span data-key="error_invalid_password">Invalid password. Please try again.</span>
</div>
<div class="form-group">
<label for="password">Password:</label>
<label for="password" data-key="password_label">Password:</label>
<input type="password" id="password" name="password" required autofocus>
</div>
<button type="submit" class="login-button">
<button type="submit" class="login-button" data-key="access_dashboard">
Access Dashboard
</button>
</form>
</div>
<script>
let currentLanguage = localStorage.getItem('yggdrasil-language') || 'ru';
let currentTheme = localStorage.getItem('yggdrasil-theme') || 'light';
function updateTexts() {
const elements = document.querySelectorAll('[data-key]');
elements.forEach(element => {
const key = element.getAttribute('data-key');
if (window.translations && window.translations[currentLanguage] && window.translations[currentLanguage][key]) {
element.textContent = window.translations[currentLanguage][key];
}
});
}
function toggleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
localStorage.setItem('yggdrasil-theme', currentTheme);
applyTheme();
}
function applyTheme() {
const body = document.body;
const themeIcon = document.querySelector('.theme-icon');
const themeBtn = document.getElementById('theme-btn');
if (currentTheme === 'dark') {
body.setAttribute('data-theme', 'dark');
themeIcon.textContent = '☀️';
themeBtn.title = window.translations[currentLanguage]['theme_light'] || 'Light theme';
} else {
body.removeAttribute('data-theme');
themeIcon.textContent = '🌙';
themeBtn.title = window.translations[currentLanguage]['theme_dark'] || 'Dark theme';
}
}
function switchLanguage(lang) {
currentLanguage = lang;
localStorage.setItem('yggdrasil-language', lang);
// Update active button
document.querySelectorAll('.lang-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById('lang-' + lang).classList.add('active');
updateTexts();
applyTheme(); // Update theme button tooltip
}
async function handleLogin(event) {
event.preventDefault();
@ -153,7 +236,7 @@
window.location.href = '/';
} else if (response.status === 429) {
// Too many requests - IP blocked
errorMessage.textContent = 'Too many failed attempts. Please wait 1 minute before trying again.';
errorMessage.querySelector('span').textContent = window.translations[currentLanguage]['error_too_many_attempts'] || 'Too many failed attempts. Please wait 1 minute before trying again.';
errorMessage.style.display = 'block';
document.getElementById('password').value = '';
document.getElementById('password').disabled = true;
@ -166,7 +249,7 @@
}, 60000);
} else {
// Invalid password
errorMessage.textContent = 'Invalid password. Please try again.';
errorMessage.querySelector('span').textContent = window.translations[currentLanguage]['error_invalid_password'] || 'Invalid password. Please try again.';
errorMessage.style.display = 'block';
document.getElementById('password').value = '';
document.getElementById('password').focus();
@ -183,6 +266,16 @@
document.getElementById('password').addEventListener('input', function () {
document.getElementById('errorMessage').style.display = 'none';
});
// Initialize language and theme on page load
document.addEventListener('DOMContentLoaded', function () {
// Set active language button
document.getElementById('lang-' + currentLanguage).classList.add('active');
// Update all texts
updateTexts();
// Apply saved theme
applyTheme();
});
</script>
</body>

View file

@ -1,3 +1,91 @@
/* Light theme (default) */
:root {
/* Background colors */
--bg-primary: #667eea;
--bg-secondary: #764ba2;
--bg-sidebar: #ffffff;
--bg-nav-item: #f8f9fa;
--bg-nav-hover: #e9ecef;
--bg-nav-active: #3498db;
--bg-nav-active-border: #2980b9;
--bg-main-content: #fafafa;
--bg-status-card: #f5f5f5;
--bg-info-card: #ffffff;
--bg-logout: #dc3545;
--bg-logout-hover: #c82333;
--bg-lang-switcher: rgba(255, 255, 255, 0.2);
--bg-lang-btn-hover: rgba(255, 255, 255, 0.3);
--bg-lang-btn-active: rgba(255, 255, 255, 0.4);
/* Border colors */
--border-sidebar: #e0e0e0;
--border-nav-item: #dee2e6;
--border-main: #e0e0e0;
--border-card: #e0e0e0;
--border-hover: #3498db;
--border-footer: #dee2e6;
--border-logout: #c82333;
--border-lang: rgba(255, 255, 255, 0.3);
/* Text colors */
--text-primary: #333;
--text-white: white;
--text-nav: #495057;
--text-heading: #343a40;
--text-body: #495057;
--text-muted: #6c757d;
/* Shadow colors */
--shadow-light: rgba(0, 0, 0, 0.1);
--shadow-medium: rgba(0, 0, 0, 0.15);
--shadow-dark: rgba(0, 0, 0, 0.2);
--shadow-heavy: rgba(0, 0, 0, 0.3);
}
/* Dark theme */
[data-theme="dark"] {
/* Background colors */
--bg-primary: #2c3e50;
--bg-secondary: #34495e;
--bg-sidebar: #37474f;
--bg-nav-item: #455a64;
--bg-nav-hover: #546e7a;
--bg-nav-active: #3498db;
--bg-nav-active-border: #2980b9;
--bg-main-content: #37474f;
--bg-status-card: #455a64;
--bg-info-card: #455a64;
--bg-logout: #dc3545;
--bg-logout-hover: #c82333;
--bg-lang-switcher: rgba(0, 0, 0, 0.3);
--bg-lang-btn-hover: rgba(255, 255, 255, 0.1);
--bg-lang-btn-active: rgba(255, 255, 255, 0.2);
/* Border colors */
--border-sidebar: #455a64;
--border-nav-item: #546e7a;
--border-main: #455a64;
--border-card: #546e7a;
--border-hover: #3498db;
--border-footer: #546e7a;
--border-logout: #c82333;
--border-lang: rgba(255, 255, 255, 0.3);
/* Text colors */
--text-primary: #333;
--text-white: white;
--text-nav: #eceff1;
--text-heading: #eceff1;
--text-body: #cfd8dc;
--text-muted: #b0bec5;
/* Shadow colors */
--shadow-light: rgba(0, 0, 0, 0.2);
--shadow-medium: rgba(0, 0, 0, 0.3);
--shadow-dark: rgba(0, 0, 0, 0.4);
--shadow-heavy: rgba(0, 0, 0, 0.5);
}
* {
margin: 0;
padding: 0;
@ -5,10 +93,10 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
min-height: 100vh;
color: #333;
color: var(--text-primary);
}
.container {
@ -19,7 +107,7 @@ body {
header {
margin-bottom: 40px;
color: white;
color: var(--text-white);
}
.header-content {
@ -35,33 +123,58 @@ header {
gap: 15px;
}
.language-switcher {
.controls-group {
display: flex;
background: rgba(255, 255, 255, 0.15);
border-radius: 8px;
padding: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: var(--bg-lang-switcher);
border-radius: 2px;
padding: 2px;
border: 1px solid var(--border-lang);
gap: 2px;
}
.theme-btn {
background: transparent;
color: var(--text-white);
border: none;
padding: 6px 12px;
border-radius: 1px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.theme-btn:hover {
background: var(--bg-lang-btn-hover);
}
.theme-icon {
display: block;
transition: transform 0.3s ease;
font-size: 12px;
}
.lang-btn {
background: transparent;
color: white;
color: var(--text-white);
border: none;
padding: 6px 12px;
border-radius: 6px;
border-radius: 1px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: all 0.2s ease;
font-weight: 600;
transition: background 0.2s ease;
}
.lang-btn:hover {
background: rgba(255, 255, 255, 0.2);
background: var(--bg-lang-btn-hover);
}
.lang-btn.active {
background: rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
background: var(--bg-lang-btn-active);
}
.header-content > div:first-child {
@ -70,9 +183,11 @@ header {
}
header h1 {
font-size: 2.5rem;
font-size: 2.2rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
text-shadow: 1px 1px 2px var(--shadow-heavy);
font-weight: 700;
letter-spacing: -0.5px;
}
header p {
@ -83,20 +198,20 @@ header p {
.sidebar-footer {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
border-top: 1px solid var(--border-footer);
}
.logout-btn {
width: 100%;
background: rgba(220, 53, 69, 0.2);
color: white;
border: 1px solid rgba(220, 53, 69, 0.3);
padding: 12px 16px;
border-radius: 10px;
background: var(--bg-logout);
color: var(--text-white);
border: 1px solid var(--border-logout);
padding: 10px 16px;
border-radius: 2px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
font-weight: 600;
transition: background 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
@ -104,9 +219,7 @@ header p {
}
.logout-btn:hover {
background: rgba(220, 53, 69, 0.3);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(220, 53, 69, 0.2);
background: var(--bg-logout-hover);
}
.logout-btn:before {
@ -122,12 +235,11 @@ header p {
.sidebar {
min-width: 250px;
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border-radius: 12px;
background: var(--bg-sidebar);
border-radius: 4px;
padding: 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 2px 8px var(--shadow-heavy);
border: 1px solid var(--border-sidebar);
display: flex;
flex-direction: column;
justify-content: space-between;
@ -143,25 +255,24 @@ header p {
display: flex;
align-items: center;
gap: 12px;
padding: 15px 18px;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 12px 16px;
background: var(--bg-nav-item);
border-radius: 2px;
cursor: pointer;
transition: all 0.3s ease;
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: background 0.2s ease;
color: var(--text-nav);
border: 1px solid var(--border-nav-item);
font-weight: 500;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
background: var(--bg-nav-hover);
}
.nav-item.active {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.4);
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
background: var(--bg-nav-active);
border-color: var(--bg-nav-active-border);
color: var(--text-white);
}
.nav-icon {
@ -175,12 +286,11 @@ header p {
.main-content {
flex: 1;
background: rgba(255, 255, 255, 0.95);
border-radius: 12px;
background: var(--bg-main-content);
border-radius: 4px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
box-shadow: 0 2px 8px var(--shadow-dark);
border: 1px solid var(--border-main);
}
.content-section {
@ -192,13 +302,25 @@ header p {
}
.status-card {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 15px;
padding: 25px;
background: var(--bg-status-card);
border-radius: 4px;
padding: 20px;
margin-bottom: 30px;
text-align: center;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
border: 1px solid rgba(255, 255, 255, 0.5);
box-shadow: 0 1px 4px var(--shadow-light);
border: 1px solid var(--border-card);
}
.status-card h2 {
color: var(--text-heading);
font-weight: 700;
margin-bottom: 15px;
font-size: 1.3rem;
}
.status-card p {
color: var(--text-body);
font-weight: 500;
}
.status-indicator {
@ -213,11 +335,11 @@ header p {
width: 12px;
height: 12px;
border-radius: 50%;
background: #6c757d;
background: var(--text-muted);
}
.status-dot.active {
background: #28a745;
background: var(--bg-nav-active);
animation: pulse 2s infinite;
}
@ -234,39 +356,44 @@ header p {
}
.info-card {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: 1px solid rgba(233, 236, 239, 0.6);
border-radius: 15px;
padding: 25px;
background: var(--bg-info-card);
border: 1px solid var(--border-card);
border-radius: 4px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
transition: box-shadow 0.2s ease;
box-shadow: 0 1px 4px var(--shadow-light);
}
.info-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
border-color: rgba(102, 126, 234, 0.3);
box-shadow: 0 2px 8px var(--shadow-medium);
border-color: var(--border-hover);
}
.info-card h3 {
color: #495057;
color: var(--text-heading);
margin-bottom: 10px;
font-weight: 600;
font-size: 1.1rem;
}
.info-card p {
color: #6c757d;
color: var(--text-body);
margin-bottom: 10px;
font-weight: 500;
}
.info-card small {
color: #adb5bd;
font-style: italic;
color: var(--text-muted);
font-weight: 500;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.5px;
}
footer {
text-align: center;
color: white;
color: var(--text-white);
opacity: 0.8;
}
@ -310,7 +437,7 @@ footer {
margin-left: 15px;
padding-top: 0;
border-top: none;
border-left: 1px solid rgba(255, 255, 255, 0.2);
border-left: 1px solid var(--border-footer);
padding-left: 15px;
}