Commit 6853d8e8 authored by Mahmoud Aglan's avatar Mahmoud Aglan

style(manager): a light El3ab theme, the crown, the game's font and icons

One light theme, set entirely in public/css/variables.css: the game's ink
(#1A1722), a violet accent, crown gold, soft surfaces and shadows, Rubik
self-hosted. The sidebar is the game's night ink and re-scopes the text
tokens, so every nav rule works unchanged. The crown and the العب wordmark
replace the "E" mark in the sidebar and on the sign-in page; the dashboard
opens on the four games with today's matches.

- The page grid reserved a sidebar column beside the fixed sidebar, leaving
  an empty band on the far side in RTL; content is now centred in the space
  beside the sidebar.
- The dark/light toggle, the manager-palette tab (and its routes) and the
  stored colour overrides are gone: one designed theme, nothing to drift.
  The per-browser preferences stay; an accent someone picked still applies,
  the old blue default does not.
- Old blue rgba() values now use --accent-rgb; Google Fonts are no longer
  loaded.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent d5c60c34
...@@ -281,8 +281,6 @@ return [ ...@@ -281,8 +281,6 @@ return [
// Branding // Branding
'branding' => ['module' => 'branding', 'action' => 'index'], 'branding' => ['module' => 'branding', 'action' => 'index'],
'branding/update-color' => ['module' => 'branding', 'action' => 'updateColor'],
'branding/reset-colors' => ['module' => 'branding', 'action' => 'resetColors'],
// Analytics // Analytics
'analytics' => ['module' => 'analytics', 'action' => 'index'], 'analytics' => ['module' => 'analytics', 'action' => 'index'],
......
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $pageTitle ?? 'لوحة التحكم' ?> — El3ab</title> <title><?= $pageTitle ?? 'لوحة التحكم' ?> — El3ab</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/public/css/variables.css"> <link rel="stylesheet" href="/public/css/variables.css">
<link rel="stylesheet" href="/public/css/reset.css"> <link rel="stylesheet" href="/public/css/reset.css">
<link rel="stylesheet" href="/public/css/layout.css"> <link rel="stylesheet" href="/public/css/layout.css">
...@@ -18,7 +15,6 @@ ...@@ -18,7 +15,6 @@
<?php if (isset($moduleCSS)): ?> <?php if (isset($moduleCSS)): ?>
<link rel="stylesheet" href="/modules/<?= $moduleCSS ?>/assets/<?= basename($moduleCSS) ?>.css"> <link rel="stylesheet" href="/modules/<?= $moduleCSS ?>/assets/<?= basename($moduleCSS) ?>.css">
<?php endif; ?> <?php endif; ?>
<?= ThemeService::renderCssOverrides() ?>
<meta name="csrf-token" content="<?= Auth::csrfToken() ?>"> <meta name="csrf-token" content="<?= Auth::csrfToken() ?>">
</head> </head>
<body> <body>
......
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>تسجيل الدخول — El3ab</title> <title>تسجيل الدخول — El3ab</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/public/css/variables.css"> <link rel="stylesheet" href="/public/css/variables.css">
<link rel="stylesheet" href="/public/css/reset.css"> <link rel="stylesheet" href="/public/css/reset.css">
<link rel="stylesheet" href="/public/css/components.css"> <link rel="stylesheet" href="/public/css/components.css">
...@@ -18,7 +15,7 @@ ...@@ -18,7 +15,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: var(--bg-primary); background: var(--brand-navy);
padding: var(--space-4); padding: var(--space-4);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
...@@ -28,9 +25,9 @@ ...@@ -28,9 +25,9 @@
position: absolute; position: absolute;
width: 600px; width: 600px;
height: 600px; height: 600px;
background: radial-gradient(circle, rgba(32, 130, 240, 0.08) 0%, transparent 70%); background: radial-gradient(circle, rgba(255, 197, 49, 0.16) 0%, transparent 70%);
top: -200px; inset-block-start: -200px;
left: -200px; inset-inline-start: -200px;
pointer-events: none; pointer-events: none;
} }
.auth-page::after { .auth-page::after {
...@@ -38,9 +35,9 @@ ...@@ -38,9 +35,9 @@
position: absolute; position: absolute;
width: 400px; width: 400px;
height: 400px; height: 400px;
background: radial-gradient(circle, rgba(104, 52, 190, 0.06) 0%, transparent 70%); background: radial-gradient(circle, rgb(var(--accent-rgb) / 0.22) 0%, transparent 70%);
bottom: -100px; inset-block-end: -100px;
right: -100px; inset-inline-end: -100px;
pointer-events: none; pointer-events: none;
} }
.auth-card { .auth-card {
...@@ -49,23 +46,18 @@ ...@@ -49,23 +46,18 @@
background: var(--bg-elevated); background: var(--bg-elevated);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-xl); border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
padding: var(--space-8); padding: var(--space-8);
animation: fadeInUp var(--duration-slow) var(--ease-out); animation: fadeInUp var(--duration-slow) var(--ease-out);
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.auth-logo { .auth-logo {
width: 64px; display: block;
height: 64px; width: 72px;
border-radius: var(--radius-lg); height: 72px;
background: linear-gradient(135deg, var(--brand-blue), var(--brand-purple)); object-fit: contain;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto var(--space-5); margin: 0 auto var(--space-5);
font-size: 28px;
font-weight: 800;
color: white;
} }
.auth-title { .auth-title {
text-align: center; text-align: center;
......
...@@ -21,8 +21,8 @@ try { $pendingWorkflows = $isAdmin ? $db->count('approval_requests', ['status' = ...@@ -21,8 +21,8 @@ try { $pendingWorkflows = $isAdmin ? $db->count('approval_requests', ['status' =
?> ?>
<aside class="sidebar" id="sidebar"> <aside class="sidebar" id="sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<div class="sidebar-logo">E</div> <img class="sidebar-logo" src="/public/img/crown.png" width="36" height="36" alt="">
<span class="sidebar-title">El3ab إدارة</span> <span class="sidebar-title">العب<small>لوحة الإدارة</small></span>
</div> </div>
<div class="favorites-bar" id="favoritesBar" style="display:none;"></div> <div class="favorites-bar" id="favoritesBar" style="display:none;"></div>
......
...@@ -32,9 +32,6 @@ $currentRole = Auth::role(); ...@@ -32,9 +32,6 @@ $currentRole = Auth::role();
</div> </div>
<div class="topbar-left"> <div class="topbar-left">
<div class="kbd-hint hide-mobile"><kbd>Ctrl</kbd><kbd>K</kbd> بحث</div> <div class="kbd-hint hide-mobile"><kbd>Ctrl</kbd><kbd>K</kbd> بحث</div>
<div class="theme-toggle" onclick="toggleTheme()" title="تبديل المظهر">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</div>
<div class="topbar-user"> <div class="topbar-user">
<div class="topbar-avatar"> <div class="topbar-avatar">
<?= mb_substr(Auth::user()['display_name'] ?? 'م', 0, 1) ?> <?= mb_substr(Auth::user()['display_name'] ?? 'م', 0, 1) ?>
......
<div class="auth-logo">E</div> <img class="auth-logo" src="/public/img/crown.png" width="72" height="72" alt="العب">
<h1 class="auth-title">مرحباً بك</h1> <h1 class="auth-title">أهلاً بيك</h1>
<p class="auth-subtitle">سجل دخولك للوصول إلى لوحة الإدارة</p> <p class="auth-subtitle">ادخل لوحة إدارة العب</p>
<?php if (!empty($error)): ?> <?php if (!empty($error)): ?>
<div class="auth-error"><?= View::e($error) ?></div> <div class="auth-error"><?= View::e($error) ?></div>
......
...@@ -169,61 +169,4 @@ class BrandingController ...@@ -169,61 +169,4 @@ class BrandingController
return $out; return $out;
} }
public function updateColor(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('superadmin');
$id = (string)($_POST['id'] ?? '');
$value = (string)($_POST['value'] ?? '');
// Only the manager's own palette. A key the game reads (GAME_COLORS,
// or anything outside ThemeService's map) is the game's look, locked.
if (!self::isManagerOnlyKey($id)) {
Response::error('هذا المفتاح يقرؤه التطبيق؛ هوية التطبيق مقفلة', '/branding');
return;
}
if (!preg_match('/^#[0-9A-Fa-f]{6}$/', $value)) {
Response::error('قيمة لون غير صحيحة', '/branding');
return;
}
$ok = $this->db->update('platform_theme', ['id' => "eq.{$id}", 'category' => 'eq.color'], [
'value' => $value,
'updated_at' => date('c'),
]);
if ($ok === null) {
Response::error('لم يُحفظ اللون: ' . $this->db->lastErrorMessage(), '/branding');
return;
}
ThemeService::invalidateCache();
AuditLog::log('update', 'platform_theme', $id, null, ['value' => $value]);
Response::success('تم تحديث اللون', '/branding');
}
public function resetColors(array $params, string $method): void
{
Auth::requireCsrf();
Auth::requireRole('superadmin');
$colors = $this->db->select('platform_theme', [
'select' => 'id,default_value',
'category' => 'eq.color',
]);
foreach ($colors as $color) {
if (!self::isManagerOnlyKey($color['id'])) continue; // the game's keys are locked
if (!empty($color['default_value'])) {
$this->db->update('platform_theme', ['id' => "eq.{$color['id']}"], [
'value' => $color['default_value'],
'updated_at' => date('c'),
]);
}
}
ThemeService::invalidateCache();
AuditLog::log('reset', 'platform_theme', null, null, ['action' => 'reset_to_defaults']);
Response::success('تم إعادة جميع الألوان إلى هوية El3ab الأصلية', '/branding');
}
} }
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
<div class="tabs mb-5"> <div class="tabs mb-5">
<button class="tab active" onclick="switchBrandTab(this, 'gameColorsTab')">ألوان التطبيق</button> <button class="tab active" onclick="switchBrandTab(this, 'gameColorsTab')">ألوان التطبيق</button>
<button class="tab" onclick="switchBrandTab(this, 'assetsTab')">صور وأيقونات التطبيق</button> <button class="tab" onclick="switchBrandTab(this, 'assetsTab')">صور وأيقونات التطبيق</button>
<button class="tab" onclick="switchBrandTab(this, 'colorsTab')">ألوان لوحة الإدارة</button>
<button class="tab" onclick="switchBrandTab(this, 'customizeTab')">تخصيص لوحة الإدارة</button> <button class="tab" onclick="switchBrandTab(this, 'customizeTab')">تخصيص لوحة الإدارة</button>
<button class="tab" onclick="switchBrandTab(this, 'previewTab')">معاينة</button> <button class="tab" onclick="switchBrandTab(this, 'previewTab')">معاينة</button>
</div> </div>
...@@ -105,7 +104,7 @@ ...@@ -105,7 +104,7 @@
<div class="customize-card-icon blue"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="10.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="6.5" cy="12.5" r="2.5"/><path d="M12 22c5.5 0 10-4.5 10-10S17.5 2 12 2 2 6.5 2 12s4.5 10 10 10z"/></svg></div> <div class="customize-card-icon blue"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="13.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="10.5" r="2.5"/><circle cx="8.5" cy="7.5" r="2.5"/><circle cx="6.5" cy="12.5" r="2.5"/><path d="M12 22c5.5 0 10-4.5 10-10S17.5 2 12 2 2 6.5 2 12s4.5 10 10 10z"/></svg></div>
<div><div class="customize-card-title">اللون الرئيسي</div><div class="customize-card-desc">لون التمييز العام</div></div> <div><div class="customize-card-title">اللون الرئيسي</div><div class="customize-card-desc">لون التمييز العام</div></div>
</div> </div>
<input type="color" class="customize-color-input" id="accentColorPicker" value="#2082F0" onchange="UIPrefs.set('accentColor', this.value)"> <input type="color" class="customize-color-input" id="accentColorPicker" value="#6B3FD4" onchange="UIPrefs.set('accentColor', this.value)">
</div> </div>
<!-- 6. Table Density --> <!-- 6. Table Density -->
...@@ -306,51 +305,6 @@ ...@@ -306,51 +305,6 @@
</div> </div>
<!-- Colors Tab --> <!-- Colors Tab -->
<div id="colorsTab" class="tab-content hidden">
<?php if (empty($colors)): ?>
<div class="card"><div class="empty-state"><h3 class="empty-state-title">لا توجد ألوان محفوظة</h3><p class="empty-state-text">أضف ألوان العلامة التجارية من جدول platform_theme</p></div></div>
<?php else: ?>
<div class="flex items-center justify-between mb-4">
<p class="text-sm text-secondary">ألوان لوحة الإدارة هذه فقط. التطبيق لا يقرأ هذه المفاتيح؛ أي مفتاح يقرؤه التطبيق مقفل هنا.</p>
<form method="POST" action="/branding/reset-colors" style="margin:0;">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<button type="submit" class="btn btn-ghost btn-sm" onclick="return confirm('هل أنت متأكد؟ سيتم إعادة جميع الألوان إلى الافتراضي.')">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
إعادة للهوية الأصلية
</button>
</form>
</div>
<div class="grid grid-3">
<?php foreach ($colors as $color): ?>
<div class="card">
<div class="flex items-center gap-4">
<div style="width: 48px; height: 48px; border-radius: var(--radius-md); background: <?= View::e($color['value']) ?>; border: 1px solid var(--border); flex-shrink: 0;"></div>
<div class="flex-1">
<div class="font-medium text-sm"><?= View::e($color['label_ar'] ?? $color['label']) ?></div>
<div class="text-xs text-muted" dir="ltr"><?= View::e($color['id']) ?></div>
</div>
</div>
<?php if (isset(BrandingController::GAME_COLORS[$color['id']])): ?>
<div class="flex items-center gap-2 mt-3">
<input type="text" class="form-input" style="width: 100px; padding: var(--space-1) var(--space-2); font-size: 12px;" value="<?= View::e($color['value']) ?>" dir="ltr" readonly>
<span class="text-xs text-muted">يقرؤه التطبيق أيضًا؛ مقفل</span>
</div>
<?php else: ?>
<form method="POST" action="/branding/update-color" class="flex items-center gap-2 mt-3">
<input type="hidden" name="_csrf" value="<?= Auth::csrfToken() ?>">
<input type="hidden" name="id" value="<?= View::e($color['id']) ?>">
<input type="color" name="value" value="<?= View::e($color['value']) ?>" style="width: 40px; height: 32px; border: none; cursor: pointer;">
<input type="text" class="form-input" style="width: 100px; padding: var(--space-1) var(--space-2); font-size: 12px;" value="<?= View::e($color['value']) ?>" dir="ltr" readonly>
<button type="submit" class="btn btn-primary btn-sm">حفظ</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- Assets Tab -->
<div id="assetsTab" class="tab-content hidden"> <div id="assetsTab" class="tab-content hidden">
<?php $stateLabels = ['live' => ['يقرؤه التطبيق', 'badge-success'], 'conditional' => ['بشرط', 'badge-warning'], 'ignored' => ['لا يقرؤه التطبيق', 'badge-default']]; ?> <?php $stateLabels = ['live' => ['يقرؤه التطبيق', 'badge-success'], 'conditional' => ['بشرط', 'badge-warning'], 'ignored' => ['لا يقرؤه التطبيق', 'badge-default']]; ?>
<div class="card mb-4 p-4"> <div class="card mb-4 p-4">
...@@ -443,6 +397,6 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -443,6 +397,6 @@ document.addEventListener('DOMContentLoaded', function() {
// Set color picker // Set color picker
const colorPicker = document.getElementById('accentColorPicker'); const colorPicker = document.getElementById('accentColorPicker');
if (colorPicker) colorPicker.value = prefs.accentColor; if (colorPicker) colorPicker.value = prefs.accentColor || '#6B3FD4';
}); });
</script> </script>
...@@ -78,10 +78,17 @@ class DashboardController ...@@ -78,10 +78,17 @@ class DashboardController
$stockfishHealth = ApiProxy::healthCheck(STOCKFISH_API_URL . '/health'); $stockfishHealth = ApiProxy::healthCheck(STOCKFISH_API_URL . '/health');
$swissHealth = ApiProxy::healthCheck(SWISS_API_BASE . '/health'); $swissHealth = ApiProxy::healthCheck(SWISS_API_BASE . '/health');
// Matches started today (Cairo), per game, for the games strip.
$since = (new DateTimeImmutable('today', new DateTimeZone('Africa/Cairo')))->format('c');
$gamesToday = [];
foreach (['chess' => 'matches', 'ludo' => 'ludo_matches', 'domino' => 'domino_matches', 'backgammon' => 'backgammon_matches'] as $game => $table) {
$gamesToday[$game] = $db->count($table, ['created_at' => 'gte.' . $since]);
}
return compact( return compact(
'totalPlayers', 'onlinePlayers', 'totalMatches', 'activeMatches', 'totalPlayers', 'onlinePlayers', 'totalMatches', 'activeMatches',
'activeTournaments', 'pendingReports', 'totalAdminUsers', 'pendingApprovals', 'activeTournaments', 'pendingReports', 'totalAdminUsers', 'pendingApprovals',
'recentActivity', 'supabaseHealth', 'stockfishHealth', 'swissHealth' 'recentActivity', 'supabaseHealth', 'stockfishHealth', 'swissHealth', 'gamesToday'
); );
} }
......
...@@ -13,6 +13,17 @@ ...@@ -13,6 +13,17 @@
</div> </div>
</div> </div>
<!-- The four games, with today's matches -->
<div class="game-strip mb-6">
<?php foreach (['chess' => 'شطرنج', 'ludo' => 'لودو', 'domino' => 'دومينو', 'backgammon' => 'طاولة'] as $g => $name): ?>
<a href="/games" class="game-tile game-<?= $g ?>">
<img src="/public/img/games/<?= $g ?>.svg" width="44" height="44" alt="">
<span class="game-tile-name"><?= $name ?></span>
<span class="game-tile-count"><?= number_format($gamesToday[$g] ?? 0) ?> ماتش النهارده</span>
</a>
<?php endforeach; ?>
</div>
<!-- Stat Cards --> <!-- Stat Cards -->
<div class="grid grid-4 stagger mb-6"> <div class="grid grid-4 stagger mb-6">
<div class="stat-card"> <div class="stat-card">
......
...@@ -76,8 +76,8 @@ ...@@ -76,8 +76,8 @@
} }
@keyframes glow { @keyframes glow {
0%, 100% { box-shadow: 0 0 5px rgba(32, 130, 240, 0.3); } 0%, 100% { box-shadow: 0 0 5px rgb(var(--accent-rgb) / 0.3); }
50% { box-shadow: 0 0 20px rgba(32, 130, 240, 0.6); } 50% { box-shadow: 0 0 20px rgb(var(--accent-rgb) / 0.6); }
} }
/* Animation utilities */ /* Animation utilities */
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
.btn-primary:hover { .btn-primary:hover {
background: #1a6fd4; background: #1a6fd4;
box-shadow: 0 0 20px rgba(32, 130, 240, 0.4); box-shadow: 0 0 20px rgb(var(--accent-rgb) / 0.4);
color: white; color: white;
} }
...@@ -166,7 +166,7 @@ ...@@ -166,7 +166,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
.stat-icon.blue { background: rgba(32, 130, 240, 0.15); color: var(--brand-blue); } .stat-icon.blue { background: rgb(var(--accent-rgb) / 0.15); color: var(--brand-blue); }
.stat-icon.orange { background: rgba(232, 77, 30, 0.15); color: var(--brand-orange); } .stat-icon.orange { background: rgba(232, 77, 30, 0.15); color: var(--brand-orange); }
.stat-icon.gold { background: rgba(228, 172, 56, 0.15); color: var(--brand-gold); } .stat-icon.gold { background: rgba(228, 172, 56, 0.15); color: var(--brand-gold); }
.stat-icon.purple { background: rgba(104, 52, 190, 0.15); color: var(--brand-purple); } .stat-icon.purple { background: rgba(104, 52, 190, 0.15); color: var(--brand-purple); }
...@@ -222,7 +222,7 @@ ...@@ -222,7 +222,7 @@
.form-input:focus { .form-input:focus {
border-color: var(--brand-blue); border-color: var(--brand-blue);
box-shadow: 0 0 0 3px rgba(32, 130, 240, 0.1); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.1);
} }
.form-input::placeholder { .form-input::placeholder {
...@@ -271,7 +271,7 @@ ...@@ -271,7 +271,7 @@
.form-select:focus { .form-select:focus {
border-color: var(--brand-blue); border-color: var(--brand-blue);
box-shadow: 0 0 0 3px rgba(32, 130, 240, 0.1); box-shadow: 0 0 0 3px rgb(var(--accent-rgb) / 0.1);
} }
textarea.form-input { textarea.form-input {
...@@ -965,3 +965,22 @@ textarea.form-input { ...@@ -965,3 +965,22 @@ textarea.form-input {
accent-color: var(--brand-blue); accent-color: var(--brand-blue);
cursor: pointer; cursor: pointer;
} }
/* Dashboard games strip: the game's own icons on soft tints. */
.game-strip { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); }
.game-tile {
display: grid; grid-template-columns: auto 1fr; grid-template-rows: auto auto; column-gap: var(--space-3);
align-items: center; padding: var(--space-4); border-radius: var(--radius-lg);
background: var(--bg-secondary); border: 1px solid var(--border); box-shadow: var(--shadow-sm);
color: var(--text-primary); text-decoration: none;
transition: transform var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast);
}
.game-tile:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.game-tile img { grid-row: span 2; width: 44px; height: 44px; object-fit: contain; padding: 6px; border-radius: var(--radius-md); background: var(--tile-tint); }
.game-tile-name { font-weight: var(--font-weight-bold); font-size: var(--font-size-md); }
.game-tile-count { font-size: var(--font-size-xs); color: var(--text-secondary); }
.game-chess { --tile-tint: #EDE7FB; }
.game-ludo { --tile-tint: #FDE8EA; }
.game-domino { --tile-tint: #E4F5EC; }
.game-backgammon { --tile-tint: #FFF3D6; }
@media (max-width: 900px) { .game-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
...@@ -9,9 +9,9 @@ ...@@ -9,9 +9,9 @@
[data-card-style="glass"] .card, [data-card-style="glass"] .card,
[data-card-style="glass"] .stat-card { [data-card-style="glass"] .stat-card {
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08); border: 1px solid var(--border);
} }
/* Toast Position */ /* Toast Position */
...@@ -22,30 +22,30 @@ ...@@ -22,30 +22,30 @@
/* Sidebar Style */ /* Sidebar Style */
[data-sidebar-style="transparent"] .sidebar { [data-sidebar-style="transparent"] .sidebar {
background: rgba(17, 17, 35, 0.7); background: rgba(22, 19, 29, 0.85);
backdrop-filter: blur(16px); backdrop-filter: blur(16px);
} }
[data-sidebar-style="gradient"] .sidebar { [data-sidebar-style="gradient"] .sidebar {
background: linear-gradient(180deg, var(--bg-secondary) 0%, rgba(32, 130, 240, 0.05) 100%); background: linear-gradient(180deg, var(--bg-secondary) 0%, rgb(var(--accent-rgb) / 0.05) 100%);
} }
/* High Contrast */ /* High Contrast */
.high-contrast { .high-contrast {
--text-primary: #ffffff; --text-primary: #000000;
--text-secondary: #e0e0e0; --text-secondary: #2A2633;
--text-muted: #b0b0b0; --text-muted: #4B4558;
--border: rgba(255, 255, 255, 0.25); --border: rgba(26, 23, 34, 0.35);
--bg-hover: rgba(255, 255, 255, 0.12); --bg-hover: rgba(26, 23, 34, 0.08);
} }
.high-contrast .card, .high-contrast .card,
.high-contrast .stat-card { .high-contrast .stat-card {
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(26, 23, 34, 0.3);
} }
.high-contrast .nav-item.active { .high-contrast .nav-item.active {
background: rgba(32, 130, 240, 0.2); background: rgb(var(--accent-rgb) / 0.2);
} }
/* Color Blind Modes */ /* Color Blind Modes */
...@@ -148,7 +148,7 @@ ...@@ -148,7 +148,7 @@
flex-shrink: 0; flex-shrink: 0;
} }
.customize-card-icon.blue { background: rgba(32, 130, 240, 0.15); color: var(--brand-blue); } .customize-card-icon.blue { background: rgb(var(--accent-rgb) / 0.15); color: var(--brand-blue); }
.customize-card-icon.purple { background: rgba(139, 92, 246, 0.15); color: var(--brand-purple); } .customize-card-icon.purple { background: rgba(139, 92, 246, 0.15); color: var(--brand-purple); }
.customize-card-icon.gold { background: rgba(228, 172, 56, 0.15); color: var(--brand-gold); } .customize-card-icon.gold { background: rgba(228, 172, 56, 0.15); color: var(--brand-gold); }
.customize-card-icon.green { background: rgba(16, 185, 129, 0.15); color: var(--success); } .customize-card-icon.green { background: rgba(16, 185, 129, 0.15); color: var(--success); }
...@@ -188,7 +188,7 @@ ...@@ -188,7 +188,7 @@
} }
.customize-option.active { .customize-option.active {
background: rgba(32, 130, 240, 0.15); background: rgb(var(--accent-rgb) / 0.15);
border-color: var(--brand-blue); border-color: var(--brand-blue);
color: var(--brand-blue); color: var(--brand-blue);
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
......
...@@ -214,31 +214,6 @@ ...@@ -214,31 +214,6 @@
color: var(--text-primary); color: var(--text-primary);
} }
/* Light theme overrides */
[data-theme="light"] {
--bg-primary: #f8f9fc;
--bg-secondary: #ffffff;
--bg-elevated: #ffffff;
--bg-hover: #f1f3f9;
--bg-active: #e8ecf4;
--border: rgba(0, 0, 0, 0.08);
--border-hover: rgba(0, 0, 0, 0.15);
--text-primary: #1a1a2e;
--text-secondary: rgba(26, 26, 46, 0.6);
--text-muted: rgba(26, 26, 46, 0.35);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.12);
}
[data-theme="light"] .sidebar {
box-shadow: -1px 0 0 var(--border);
}
[data-theme="light"] .topbar {
box-shadow: 0 1px 0 var(--border);
}
/* 10. Copy Button */ /* 10. Copy Button */
.copy-btn { .copy-btn {
...@@ -443,7 +418,7 @@ ...@@ -443,7 +418,7 @@
} }
.sparkline .sparkline-fill { .sparkline .sparkline-fill {
fill: rgba(32, 130, 240, 0.1); fill: rgb(var(--accent-rgb) / 0.1);
stroke: none; stroke: none;
} }
......
/* The sidebar is fixed; topbar and content sit beside it by their
margin-inline-start. (The grid used to reserve a sidebar column too, which
left an empty band on the far side in RTL.) */
.app-layout { .app-layout {
display: grid; display: grid;
grid-template-columns: 1fr var(--sidebar-width); grid-template-columns: minmax(0, 1fr);
grid-template-rows: var(--topbar-height) 1fr; grid-template-rows: var(--topbar-height) 1fr;
grid-template-areas: grid-template-areas: "topbar" "content";
"topbar sidebar"
"content sidebar";
min-height: 100vh; min-height: 100vh;
} }
/* The sidebar is the game's night ink: it re-scopes the text and surface
tokens so every nav rule keeps working unchanged. */
.sidebar {
--text-primary: #F4F1FA;
--text-secondary: #B9B2C8;
--text-muted: #857E95;
--bg-hover: rgba(255, 255, 255, 0.06);
--bg-active: rgba(255, 255, 255, 0.10);
--border: rgba(255, 255, 255, 0.08);
--accent: #B79BFF;
--accent-rgb: 183 155 255;
--brand-blue: #B79BFF;
color: var(--text-primary);
}
.sidebar { .sidebar {
grid-area: sidebar; grid-area: sidebar;
background: var(--sidebar-bg, var(--bg-secondary)); background: var(--sidebar-bg, var(--bg-secondary));
...@@ -38,20 +54,17 @@ ...@@ -38,20 +54,17 @@
.sidebar-logo { .sidebar-logo {
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: var(--radius-md); object-fit: contain;
background: linear-gradient(135deg, var(--brand-blue), var(--brand-purple));
display: flex;
align-items: center;
justify-content: center;
font-weight: var(--font-weight-bold);
font-size: var(--font-size-lg);
color: white;
flex-shrink: 0; flex-shrink: 0;
} }
.sidebar-title { .sidebar-title {
font-size: var(--font-size-md); display: flex;
font-weight: var(--font-weight-bold); flex-direction: column;
line-height: 1.15;
font-size: var(--font-size-lg);
font-weight: 800;
color: var(--brand-sand);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -100,7 +113,7 @@ ...@@ -100,7 +113,7 @@
} }
.nav-item.active { .nav-item.active {
background: rgba(32, 130, 240, 0.1); background: rgb(var(--accent-rgb) / 0.1);
color: var(--brand-blue); color: var(--brand-blue);
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
} }
...@@ -196,9 +209,16 @@ ...@@ -196,9 +209,16 @@
.content { .content {
grid-area: content; grid-area: content;
padding: var(--space-6); padding: var(--space-6);
/* Centred in the space beside the sidebar, up to the max width. */
padding-inline: max(var(--space-6), calc((100vw - var(--sidebar-width) - var(--content-max-width)) / 2));
margin-inline-start: var(--sidebar-width); margin-inline-start: var(--sidebar-width);
min-height: calc(100vh - var(--topbar-height)); min-height: calc(100vh - var(--topbar-height));
max-width: var(--content-max-width); }
.sidebar-title small {
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
color: var(--text-secondary);
} }
.content-header { .content-header {
......
...@@ -82,7 +82,7 @@ h1, h2, h3, h4, h5, h6 { ...@@ -82,7 +82,7 @@ h1, h2, h3, h4, h5, h6 {
} }
::selection { ::selection {
background: rgba(32, 130, 240, 0.3); background: rgb(var(--accent-rgb) / 0.3);
color: var(--text-primary); color: var(--text-primary);
} }
......
/* El3ab manager theme: one light theme, the game's own ink, crown gold and
violet. Every colour the manager uses comes from these tokens; change a
value here and every page follows. The sidebar re-scopes a few tokens to
the game's night ink (layout.css, .sidebar). */
@font-face {
font-family: 'Rubik';
src: url('/public/fonts/rubik-arabic.woff2') format('woff2');
font-weight: 300 900;
font-display: swap;
unicode-range: U+0600-06FF, U+0750-077F, U+0870-0891, U+0897-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC;
}
@font-face {
font-family: 'Rubik';
src: url('/public/fonts/rubik-latin.woff2') format('woff2');
font-weight: 300 900;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
:root { :root {
/* Brand Colors */ color-scheme: light;
--brand-blue: #2082F0;
--brand-orange: #E84D1E; /* Accent: buttons, links, the active nav item, focus rings */
--brand-gold: #E4AC38; --accent: #6B3FD4;
--brand-sand: #FFCC66; --accent-rgb: 107 63 212;
--brand-navy: #152132; --accent-hover: #5A31BD;
--brand-cyan: #00FFFF; --accent-soft: #EFE9FC;
--brand-purple: #6834BE;
/* Brand (older names kept: every stylesheet already uses them) */
--brand-blue: var(--accent);
--brand-purple: #8D63F0;
--brand-gold: #E8A90F;
--brand-sand: #FFC531;
--brand-orange: #E8572A;
--brand-navy: #16131D;
--brand-cyan: #1FB5C9;
/* Surface Colors (Dark Theme — El3ab Navy) */ /* Surfaces */
--bg-primary: #152132; --bg-primary: #F5F3F8;
--bg-secondary: #1C2A3F; --bg-secondary: #FFFFFF;
--bg-elevated: #253A52; --bg-elevated: #FFFFFF;
--bg-hover: #2D4562; --bg-hover: #F1EDF7;
--bg-active: #354F6E; --bg-active: #E8E1F4;
--sidebar-bg: #0F1923; --sidebar-bg: #16131D;
--input-bg: #1A2638; --input-bg: #FFFFFF;
--border: rgba(255, 255, 255, 0.06); --border: rgba(26, 23, 34, 0.10);
--border-hover: rgba(255, 255, 255, 0.12); --border-hover: rgba(26, 23, 34, 0.20);
--border-focus: rgba(32, 130, 240, 0.5); --border-focus: rgb(var(--accent-rgb) / 0.55);
/* Text Colors */ /* Text */
--text-primary: #ffffff; --text-primary: #1A1722;
--text-secondary: rgba(255, 255, 255, 0.6); --text-secondary: #4B4558;
--text-muted: rgba(255, 255, 255, 0.3); --text-muted: #7D768C;
--text-inverse: #0a0e1a; --text-inverse: #FFFFFF;
/* Status Colors */ /* Status */
--success: #10B981; --success: #16A05A;
--success-bg: rgba(16, 185, 129, 0.1); --success-bg: #E6F6EE;
--warning: #F59E0B; --warning: #C77A06;
--warning-bg: rgba(245, 158, 11, 0.1); --warning-bg: #FDF2DF;
--danger: #EF4444; --danger: #D93445;
--danger-bg: rgba(239, 68, 68, 0.1); --danger-bg: #FCE8EA;
--info: #3B82F6; --info: #2F6FDB;
--info-bg: rgba(59, 130, 246, 0.1); --info-bg: #E8F0FD;
/* Spacing */ /* Spacing */
--space-1: 4px; --space-1: 4px;
...@@ -48,7 +76,7 @@ ...@@ -48,7 +76,7 @@
--space-9: 48px; --space-9: 48px;
--space-10: 64px; --space-10: 64px;
/* Border Radius */ /* Radius */
--radius-sm: 6px; --radius-sm: 6px;
--radius-md: 8px; --radius-md: 8px;
--radius-lg: 12px; --radius-lg: 12px;
...@@ -56,8 +84,8 @@ ...@@ -56,8 +84,8 @@
--radius-full: 50%; --radius-full: 50%;
--radius-pill: 999px; --radius-pill: 999px;
/* Typography */ /* Type: Rubik, the game's face */
--font-family: 'IBM Plex Sans Arabic', sans-serif; --font-family: 'Rubik', 'IBM Plex Sans Arabic', system-ui, sans-serif;
--font-size-xs: 12px; --font-size-xs: 12px;
--font-size-sm: 14px; --font-size-sm: 14px;
--font-size-md: 16px; --font-size-md: 16px;
...@@ -70,12 +98,12 @@ ...@@ -70,12 +98,12 @@
--font-weight-bold: 700; --font-weight-bold: 700;
--line-height: 1.6; --line-height: 1.6;
/* Shadows */ /* Shadows: soft, for a light page */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-sm: 0 1px 2px rgba(26, 23, 34, 0.06);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3); --shadow-md: 0 2px 8px rgba(26, 23, 34, 0.07);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4); --shadow-lg: 0 8px 24px rgba(26, 23, 34, 0.10);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5); --shadow-xl: 0 16px 40px rgba(26, 23, 34, 0.14);
--shadow-glow: 0 0 20px rgba(32, 130, 240, 0.3); --shadow-glow: 0 0 0 3px rgb(var(--accent-rgb) / 0.18);
/* Motion */ /* Motion */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1); --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
...@@ -86,8 +114,8 @@ ...@@ -86,8 +114,8 @@
--duration-slow: 400ms; --duration-slow: 400ms;
/* Layout */ /* Layout */
--sidebar-width: 280px; --sidebar-width: 264px;
--sidebar-collapsed: 72px; --sidebar-collapsed: 72px;
--topbar-height: 64px; --topbar-height: 64px;
--content-max-width: 1400px; --content-max-width: 1320px;
} }
Copyright 2015 The Rubik Project Authors (https://github.com/googlefonts/rubik)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
# Credits
- `crown.png` and `games/*.svg`: copied from the El3ab player app (`public/assets/game/`).
The game icons are baked from game-icons.net silhouettes, CC BY 3.0
(https://creativecommons.org/licenses/by/3.0/): chess by Skoll; ludo, domino,
backgammon and trophy by Delapouite (https://delapouite.com).
- `../fonts/rubik-*.woff2`: Rubik, SIL Open Font License 1.1 (`../fonts/Rubik-OFL.txt`).
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-14 -14 540 572">
<defs><path id="p" d="M64 32L16 496h96L64 32zm48 464h48c-20.053 0-37.38-12.205-44.943-29.55L112 496zm48 0h48l-3.057-29.547C197.38 483.798 180.053 496 160 496zm48 0h48c-20.052 0-37.38-12.202-44.943-29.547L208 496zm48 0h48l-3.057-29.55C293.38 483.794 276.053 496 256 496zm48 0h96L352 32l-48 464zm96 0h48c-20.053 0-37.38-12.205-44.943-29.55L400 496zm48 0h48l-3.057-29.55C485.38 483.794 468.053 496 448 496zM160 32l-28.914 279.512c8.12-5.97 18.12-9.512 28.914-9.512s20.795 3.542 28.914 9.512L160 32zm96 0l-40.016 386.82C224.877 406.25 239.516 398 256 398c16.486 0 31.123 8.254 40.016 20.826L256 32zm192 0l-18.373 177.605C435.31 207.288 441.51 206 448 206c6.49 0 12.69 1.288 18.373 3.605L448 32zm0 192c-17.227 0-31 13.773-31 31s13.773 31 31 31 31-13.773 31-31-13.773-31-31-31zm-27.273 71.648l-1.64 15.858c5.583-4.105 12.058-7.062 19.07-8.506-6.336-1.305-12.232-3.846-17.43-7.352zm54.546 0c-5.198 3.506-11.094 6.047-17.43 7.352 7.012 1.444 13.487 4.4 19.07 8.506l-1.64-15.858zM448 320c-17.227 0-31 13.773-31 31s13.773 31 31 31 31-13.773 31-31-13.773-31-31-31zm-324.387 63.732l-3.63 35.08c7.077-9.997 17.788-17.26 30.173-19.812-10.42-2.146-19.66-7.627-26.543-15.268zm72.774 0c-6.884 7.64-16.122 13.122-26.543 15.268 12.384 2.55 23.096 9.813 30.172 19.81l-3.63-35.078zm215.226 0l-3.63 35.08c7.077-9.997 17.788-17.26 30.173-19.812-10.42-2.146-19.66-7.627-26.543-15.268zm72.774 0c-6.884 7.64-16.122 13.122-26.543 15.268 12.385 2.55 23.096 9.815 30.172 19.813l-3.63-35.08zM448 416c-17.227 0-31 13.773-31 31s13.773 31 31 31 31-13.773 31-31-13.773-31-31-31z"/></defs>
<use href="#p" xlink:href="#p" fill="#C2203A" stroke="#C2203A" stroke-width="6" stroke-linejoin="round" transform="translate(0 28)"/>
<use href="#p" xlink:href="#p" fill="#FF4D5E" stroke="#FF4D5E" stroke-width="6" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-14 -14 540 572">
<defs><path id="p" d="M60.81 476.91h300v-60h-300v60zm233.79-347.3l13.94 7.39c31.88-43.62 61.34-31.85 61.34-31.85l-21.62 53 35.64 19 2.87 33 64.42 108.75-43.55 29.37s-26.82-36.39-39.65-43.66c-10.66-6-41.22-10.25-56.17-12l-67.54-76.91-12 10.56 37.15 42.31c-.13.18-.25.37-.38.57-35.78 58.17 23 105.69 68.49 131.78H84.14C93 85 294.6 129.61 294.6 129.61z"/></defs>
<use href="#p" xlink:href="#p" fill="#169150" stroke="#169150" stroke-width="6" stroke-linejoin="round" transform="translate(0 28)"/>
<use href="#p" xlink:href="#p" fill="#2ED47A" stroke="#2ED47A" stroke-width="6" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-14 -14 540 572">
<defs><path id="p" d="M329.6 69.81c-6.6 0-12.8 2.32-16.3 5.84l-90.5 90.55c-3.6 3.5-5.9 9.8-5.9 16.3-.1 6.4 2.2 12.6 5.4 15.7L449.1 425c3.5 3.6 9.7 5.9 16.2 5.9s12.7-2.3 16.2-5.9l12.5-12.5V224L345.8 75.65c-3.5-3.52-9.7-5.84-16.2-5.84zM46.69 81.12c-6.49 0-12.73 2.32-16.26 5.85L18 99.39V288.1l148.2 148.2c3.5 3.6 9.7 5.9 16.2 5.9 6.6 0 12.8-2.3 16.3-5.9l90.5-90.5c3.6-3.5 5.9-9.7 5.9-16.2 0-6.6-2.3-12.8-5.9-16.3L62.95 86.97c-3.53-3.53-9.77-5.85-16.25-5.85zm283.01 17.4c4.2 0 8.1 1.78 11.1 4.68 6.3 6.3 6.3 16.4 0 22.7-6.2 6.2-16.4 6.2-22.6 0-6.3-6.3-6.3-16.4 0-22.7 3.1-3 7.2-4.74 11.5-4.68zM46.9 109.9c4.16.1 8.14 1.8 11.08 4.7 6.23 6.2 6.23 16.4 0 22.6-6.25 6.2-16.37 6.2-22.62 0-6.23-6.2-6.23-16.4 0-22.6 3.05-3.1 7.22-4.8 11.54-4.7zm214.9 56.5h.1c4.1.1 8.1 1.8 11 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zm135.8 0c4.2.1 8.2 1.7 11.1 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zM46.9 177.7c4.16.1 8.14 1.8 11.08 4.7 6.3 6.3 6.3 16.4 0 22.7-6.25 6.2-16.37 6.2-22.62 0-6.3-6.3-6.3-16.4 0-22.7 3.05-3.1 7.22-4.8 11.54-4.7zm67.8 0h.1c4.2.1 8.2 1.7 11.1 4.7 6.3 6.3 6.3 16.4 0 22.7-6.3 6.3-16.4 6.3-22.7 0-6.3-6.3-6.3-16.4 0-22.7 3-3.1 7.2-4.7 11.5-4.7zm321.6 21l12.8 12.8-90.5 90.5-12.8-12.8zM153.4 210l12.8 12.8-90.51 90.5-12.73-12.8zm176.3 24.3c4.2.1 8.2 1.7 11.1 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zm135.8 0c4.2.1 8.2 1.7 11.1 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zM46.9 245.6c4.16.1 8.14 1.8 11.08 4.7 6.23 6.2 6.23 16.4 0 22.6-6.25 6.2-16.37 6.2-22.62 0-6.23-6.2-6.23-16.4 0-22.6 3.05-3.1 7.22-4.8 11.54-4.7zm135.7 0h.1c4.1.1 8.1 1.8 11 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zm282.9 56.6c4.2.1 8.2 1.7 11.1 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zm-350.8 11.3h.1c4.2.1 8.2 1.7 11.1 4.7 6.2 6.2 6.2 16.4 0 22.6-6.3 6.3-16.4 6.3-22.7 0-6.23-6.2-6.23-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zm135.8 0c4.2.1 8.2 1.7 11.1 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zm215 56.6c4.2.1 8.2 1.7 11.1 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7zm-282.9 11.3h.1c4.1.1 8.1 1.8 11 4.7 6.2 6.2 6.2 16.4 0 22.6-6.2 6.2-16.4 6.2-22.6 0-6.2-6.2-6.2-16.4 0-22.6 3-3.1 7.2-4.7 11.5-4.7z"/></defs>
<use href="#p" xlink:href="#p" fill="#5F33C2" stroke="#5F33C2" stroke-width="6" stroke-linejoin="round" transform="translate(0 28)"/>
<use href="#p" xlink:href="#p" fill="#9C6DFF" stroke="#9C6DFF" stroke-width="6" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-14 -14 540 572">
<defs><path id="p" d="M256 54.99c-27 0-46.418 14.287-57.633 32.23-10.03 16.047-14.203 34.66-15.017 50.962-30.608 15.135-64.515 30.394-91.815 45.994-14.32 8.183-26.805 16.414-36.203 25.26C45.934 218.28 39 228.24 39 239.99c0 5 2.44 9.075 5.19 12.065 2.754 2.99 6.054 5.312 9.812 7.48 7.515 4.336 16.99 7.95 27.412 11.076 15.483 4.646 32.823 8.1 47.9 9.577-14.996 25.84-34.953 49.574-52.447 72.315C56.65 378.785 39 403.99 39 431.99c0 4-.044 7.123.31 10.26.355 3.137 1.256 7.053 4.41 10.156 3.155 3.104 7.017 3.938 10.163 4.28 3.146.345 6.315.304 10.38.304h111.542c8.097 0 14.026.492 20.125-3.43 6.1-3.92 8.324-9.275 12.67-17.275l.088-.16.08-.166s9.723-19.77 21.324-39.388c5.8-9.808 12.097-19.576 17.574-26.498 2.74-3.46 5.304-6.204 7.15-7.754.564-.472.82-.56 1.184-.76.363.2.62.288 1.184.76 1.846 1.55 4.41 4.294 7.15 7.754 5.477 6.922 11.774 16.69 17.574 26.498 11.6 19.618 21.324 39.387 21.324 39.387l.08.165.088.16c4.346 8 6.55 13.323 12.61 17.254 6.058 3.93 11.974 3.45 19.957 3.45H448c4 0 7.12.043 10.244-.304 3.123-.347 6.998-1.21 10.12-4.332 3.12-3.122 3.984-6.997 4.33-10.12.348-3.122.306-6.244.306-10.244 0-28-17.65-53.205-37.867-79.488-17.493-22.74-37.45-46.474-52.447-72.315 15.077-1.478 32.417-4.93 47.9-9.576 10.422-3.125 19.897-6.74 27.412-11.075 3.758-2.168 7.058-4.49 9.81-7.48 2.753-2.99 5.192-7.065 5.192-12.065 0-11.75-6.934-21.71-16.332-30.554-9.398-8.846-21.883-17.077-36.203-25.26-27.3-15.6-61.207-30.86-91.815-45.994-.814-16.3-4.988-34.915-15.017-50.96C302.418 69.276 283 54.99 256 54.99z"/></defs>
<use href="#p" xlink:href="#p" fill="#C75E14" stroke="#C75E14" stroke-width="6" stroke-linejoin="round" transform="translate(0 28)"/>
<use href="#p" xlink:href="#p" fill="#FF8A34" stroke="#FF8A34" stroke-width="6" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-14 -14 540 572">
<defs><path id="p" d="M98.398 21.146a17.092 17.092 0 0 0-4.636.521c-20.49 5.262-33.163 20.63-36.116 38.649-2.952 18.019 2.168 38.346 12.676 58.193 20.695 39.086 63.262 77.08 117.852 85.85-5.61-6.72-11.05-14.246-16.274-22.375-39.008-12.57-70.021-42.344-85.67-71.899-9.206-17.387-12.846-34.491-10.82-46.857C77.437 50.862 83.482 42.89 98.238 39.1c.065-.017.068-.034.092-.053-.065-.143.105-.08 0 0 .022.049.061.11.176.217.527.493 1.689 2.24 2.207 5.14 1.036 5.804-.413 15.593-8.135 25.68l14.293 10.942c10.418-13.61 13.65-28.086 11.56-39.785-1.044-5.85-3.396-11.165-7.628-15.124-3.174-2.969-7.747-4.868-12.405-4.972zm315.204 0c-4.658.104-9.23 2.003-12.405 4.972-4.232 3.96-6.584 9.274-7.629 15.124-2.089 11.699 1.143 26.174 11.56 39.785l14.294-10.942c-7.722-10.087-9.171-19.876-8.135-25.68.518-2.9 1.68-4.647 2.207-5.14a.695.695 0 0 0 .176-.217c-.105-.08.065-.143 0 0 .024.019.027.036.092.053 14.756 3.79 20.801 11.76 22.828 24.127 2.026 12.366-1.614 29.47-10.82 46.857-15.649 29.555-46.662 59.33-85.67 71.899-5.223 8.129-10.665 15.655-16.274 22.375 54.59-8.77 97.157-46.764 117.852-85.85 10.508-19.847 15.628-40.174 12.676-58.193-2.953-18.02-15.626-33.387-36.116-38.649a17.092 17.092 0 0 0-4.636-.521zm-276.166 7.713c2.146 36.533 16.76 83.07 36.537 120.824 10.707 20.442 22.876 38.334 34.761 50.685C220.62 212.72 232 218.858 240 218.858h32c8 0 19.38-6.138 31.266-18.49 11.885-12.351 24.054-30.243 34.761-50.685 19.777-37.755 34.39-84.29 36.537-120.824H137.436zm95.564 208v16h46v-16h-46zm6.445 34c-2.458 25.967-12.796 57.873-24.437 76h81.984c-11.64-18.127-21.979-50.033-24.437-76h-33.11zm-38.445 94v14h110v-14H201zm-32 32v94h174v-94H169zm23 23h128v48H192v-48z"/></defs>
<use href="#p" xlink:href="#p" fill="#D18D08" stroke="#D18D08" stroke-width="6" stroke-linejoin="round" transform="translate(0 28)"/>
<use href="#p" xlink:href="#p" fill="#FFC531" stroke="#FFC531" stroke-width="6" stroke-linejoin="round"/>
</svg>
\ No newline at end of file
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
fontSize: 'medium', // small | medium | large fontSize: 'medium', // small | medium | large
cardStyle: 'elevated', // flat | elevated | glass cardStyle: 'elevated', // flat | elevated | glass
animationSpeed: 'normal', // off | fast | normal animationSpeed: 'normal', // off | fast | normal
accentColor: '#2082F0', // hex accentColor: null, // hex; null = the theme's violet
tableDensity: 'comfortable', // compact | comfortable | spacious tableDensity: 'comfortable', // compact | comfortable | spacious
toastPosition: 'top-left', // top-left | top-right | bottom-left | bottom-right toastPosition: 'top-left', // top-left | top-right | bottom-left | bottom-right
contentWidth: 'standard', // narrow | standard | full contentWidth: 'standard', // narrow | standard | full
...@@ -61,8 +61,15 @@ ...@@ -61,8 +61,15 @@
root.style.setProperty('--duration-fast', durations[prefs.animationSpeed] || '150ms'); root.style.setProperty('--duration-fast', durations[prefs.animationSpeed] || '150ms');
root.style.setProperty('--duration-normal', prefs.animationSpeed === 'off' ? '0ms' : '250ms'); root.style.setProperty('--duration-normal', prefs.animationSpeed === 'off' ? '0ms' : '250ms');
// 5. Accent color // 5. Accent color: only a colour someone picked (the old blue default is ignored).
root.style.setProperty('--brand-blue', prefs.accentColor); const accent = /^#[0-9a-f]{6}$/i.test(prefs.accentColor || '') && prefs.accentColor.toLowerCase() !== '#2082f0' ? prefs.accentColor : null;
if (accent) {
root.style.setProperty('--accent', accent);
root.style.setProperty('--brand-blue', accent);
root.style.setProperty('--accent-rgb', [1, 3, 5].map(i => parseInt(accent.slice(i, i + 2), 16)).join(' '));
} else {
['--accent', '--brand-blue', '--accent-rgb'].forEach(v => root.style.removeProperty(v));
}
// 6. Table density // 6. Table density
const densities = { compact: '4px 8px', comfortable: '10px 14px', spacious: '14px 18px' }; const densities = { compact: '4px 8px', comfortable: '10px 14px', spacious: '14px 18px' };
......
...@@ -212,18 +212,12 @@ ...@@ -212,18 +212,12 @@
}); });
// ─── 4. Theme Toggle (Dark/Light) ────────────────────────────────────── // ─── 4. Theme Toggle (Dark/Light) ──────────────────────────────────────
// One theme now (variables.css). The old dark/light choice is dropped.
function initTheme() { function initTheme() {
const saved = localStorage.getItem('el3ab-theme') || 'dark'; document.documentElement.removeAttribute('data-theme');
document.documentElement.setAttribute('data-theme', saved); try { localStorage.removeItem('el3ab-theme'); } catch (e) {}
} }
window.toggleTheme = function() {
const current = document.documentElement.getAttribute('data-theme') || 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('el3ab-theme', next);
};
// ─── 5. Copy to Clipboard ────────────────────────────────────────────── // ─── 5. Copy to Clipboard ──────────────────────────────────────────────
window.copyToClipboard = function(text, btn) { window.copyToClipboard = function(text, btn) {
navigator.clipboard.writeText(text).then(() => { navigator.clipboard.writeText(text).then(() => {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment