feat: Add comprehensive documentation suite and reorganize project structure

- Created complete documentation in docs/ directory
- Added PROJECT_OVERVIEW.md with feature highlights and getting started guide
- Added ARCHITECTURE.md with system design and technical details
- Added SECURITY.md with comprehensive security implementation guide
- Added DEVELOPMENT.md with development workflows and best practices
- Added DEPLOYMENT.md with production deployment instructions
- Added API.md with complete REST API documentation
- Added CONTRIBUTING.md with contribution guidelines
- Added CHANGELOG.md with version history and migration notes
- Reorganized all documentation files into docs/ directory for better organization
- Updated README.md with proper documentation links and quick navigation
- Enhanced project structure with professional documentation standards
This commit is contained in:
SamiAhmed7777
2025-10-21 00:39:45 -07:00
commit 0b7e2d0a5b
6080 changed files with 1332936 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
<?php
/**
* Admin panel bootstrap helpers.
* Handles session protection and PDO access for lightweight admin tools.
*/
declare(strict_types=1);
define('_ISVALID', true);
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (empty($_SESSION['admin_logged_in'])) {
header('Location: /login.php');
exit;
}
/**
* Resolve an environment variable or fallback to .env contents/default value.
*/
function admin_env(string $key, ?string $default = null): ?string
{
$fromRuntime = getenv($key);
if ($fromRuntime !== false) {
return $fromRuntime;
}
if (isset($_ENV[$key])) {
return $_ENV[$key];
}
static $envFile = null;
if ($envFile === null) {
$envFile = [];
$path = dirname(__DIR__, 2) . DIRECTORY_SEPARATOR . '.env';
if (is_readable($path)) {
foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (str_starts_with($line, '#') || !str_contains($line, '=')) {
continue;
}
[$envKey, $envValue] = array_map('trim', explode('=', $line, 2));
$envFile[$envKey] = $envValue;
}
}
}
return $envFile[$key] ?? $default;
}
/**
* Shared PDO handle for admin tooling.
*/
function admin_pdo(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$host = admin_env('DB_HOST', 'db');
$dbname = admin_env('DB_NAME', 'easystream');
$user = admin_env('DB_USER', 'easystream');
$pass = admin_env('DB_PASS', 'easystream');
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=utf8mb4', $host, $dbname);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, $user, $pass, $options);
return $pdo;
}
/**
* Ensure we always have a friendly display name for the admin bar.
*/
if (empty($_SESSION['ADMIN_NAME'])) {
$_SESSION['ADMIN_NAME'] = $_SESSION['admin_user']
?? ($_SESSION['username'] ?? 'Administrator');
}
/**
* Helper for formatting numbers consistently.
*/
function admin_format_number(mixed $value, int $decimals = 0): string
{
if ($value === null || $value === '') {
$value = 0;
}
return number_format((float) $value, $decimals);
}
/**
* Escape helper for HTML output.
*/
function admin_escape(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
/**
* Convenience helper for date formatting.
*/
function admin_format_datetime(?string $date, string $format = 'M j, Y H:i'): string
{
if (!$date) {
return 'N/A';
}
try {
return (new DateTime($date))->format($format);
} catch (Exception) {
return $date;
}
}
/**
* Determine whether the initial token setup wizard has been completed.
*/
function admin_is_token_setup_complete(): bool
{
static $cached = null;
if ($cached !== null) {
return $cached;
}
try {
$pdo = admin_pdo();
$stmt = $pdo->prepare("SELECT cfg_data FROM db_settings WHERE cfg_name = 'token_setup_complete' LIMIT 1");
$stmt->execute();
$value = $stmt->fetchColumn();
$cached = $value === '1';
return $cached;
} catch (Exception) {
return false;
}
}
/**
* Mark the token setup wizard as complete.
*/
function admin_mark_token_setup_complete(): void
{
$pdo = admin_pdo();
$stmt = $pdo->prepare("
INSERT INTO db_settings (cfg_name, cfg_data, cfg_info)
VALUES ('token_setup_complete', '1', 'Flag indicating initial token setup completion')
ON DUPLICATE KEY UPDATE cfg_data = '1', cfg_info = 'Flag indicating initial token setup completion'
");
$stmt->execute();
}
/**
* Generate (or retrieve) a CSRF token for the given action.
*/
function admin_csrf_token(string $action): string
{
$key = "admin_csrf_{$action}";
if (empty($_SESSION[$key])) {
$_SESSION[$key] = bin2hex(random_bytes(32));
}
return $_SESSION[$key];
}
/**
* Validate a CSRF token for the given action.
*/
function admin_validate_csrf(string $action, ?string $token): bool
{
$key = "admin_csrf_{$action}";
if (empty($_SESSION[$key]) || !is_string($token)) {
return false;
}
return hash_equals($_SESSION[$key], $token);
}

File diff suppressed because it is too large Load Diff

597
admin/includes/layout.php Normal file
View File

@@ -0,0 +1,597 @@
<?php
/**
* Shared layout helpers for the admin panel pages.
*/
declare(strict_types=1);
require_once __DIR__ . '/bootstrap.php';
function admin_page_start(string $title, string $activeTab = 'dashboard', array $options = []): void
{
$navItems = [
'dashboard' => ['label' => 'Dashboard', 'url' => '/admin.php', 'icon' => '&#128202;'],
'users' => ['label' => 'Users', 'url' => '/admin_users.php', 'icon' => '&#128101;'],
'content' => ['label' => 'Content', 'url' => '/admin_content_management.php', 'icon' => '&#127916;'],
'tokens' => ['label' => 'Tokens', 'url' => '/admin_token_dashboard.php', 'icon' => '&#128176;'],
'settings' => ['label' => 'Settings', 'url' => '/admin_settings.php', 'icon' => '&#9881;'],
];
$skipTokenCheck = $options['skip_token_setup_check'] ?? false;
if (!$skipTokenCheck && !admin_is_token_setup_complete()) {
$current = basename(parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH));
if ($current !== 'admin_token_setup.php') {
header('Location: /admin_token_setup.php');
exit;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= admin_escape($title) ?> · EasyStream Admin</title>
<style>
:root {
--gradient: linear-gradient(135deg, #6366f1, #8b5cf6);
--muted: #f4f5f7;
--card: #ffffff;
--border: rgba(99, 102, 241, 0.18);
--text: #1f2937;
--text-muted: #6b7280;
--radius-lg: 18px;
--radius-md: 12px;
--radius-sm: 8px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
background: var(--muted);
color: var(--text);
line-height: 1.5;
}
a {
color: inherit;
text-decoration: none;
}
header {
background: var(--gradient);
color: #ffffff;
padding: 24px 0 16px;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
}
.admin-header {
max-width: 1280px;
margin: 0 auto;
padding: 0 24px;
display: flex;
flex-direction: column;
gap: 18px;
}
.admin-header__top {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.admin-header__title {
font-size: 1.8rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.admin-header__actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.admin-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: var(--radius-sm);
border: 1px solid rgba(255, 255, 255, 0.25);
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.admin-button:hover {
background: rgba(255, 255, 255, 0.25);
}
.admin-button--primary {
background: #22c55e;
border-color: #22c55e;
}
.admin-button--primary:hover {
background: #16a34a;
border-color: #16a34a;
}
.admin-button--ghost {
background: transparent;
color: #4338ca;
border-color: rgba(99, 102, 241, 0.35);
}
.admin-button--ghost:hover {
background: rgba(99, 102, 241, 0.12);
}
nav ul {
display: flex;
gap: 12px;
list-style: none;
flex-wrap: wrap;
}
nav a {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 18px;
border-radius: var(--radius-sm);
font-weight: 600;
font-size: 0.95rem;
color: rgba(255, 255, 255, 0.85);
transition: background 0.2s ease, color 0.2s ease;
}
nav a:hover {
background: rgba(255, 255, 255, 0.2);
color: #ffffff;
}
nav a.active {
background: rgba(255, 255, 255, 0.95);
color: #4338ca;
}
main {
max-width: 1280px;
margin: 32px auto;
padding: 0 24px 64px;
}
.grid {
display: grid;
gap: 20px;
}
.grid--two {
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}
.stats-grid {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
margin-bottom: 28px;
}
.stat-card {
background: var(--card);
border-radius: var(--radius-lg);
padding: 24px;
border: 1px solid var(--border);
box-shadow: 0 12px 28px rgba(99, 102, 241, 0.08);
display: flex;
flex-direction: column;
gap: 12px;
}
.stat-card__icon {
font-size: 1.8rem;
}
.stat-card__label {
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.75rem;
}
.stat-card__value {
font-size: 2.2rem;
font-weight: 700;
letter-spacing: -0.04em;
}
.stat-card__meta {
display: flex;
gap: 18px;
flex-wrap: wrap;
color: var(--text-muted);
font-size: 0.9rem;
}
.card {
background: var(--card);
border-radius: var(--radius-lg);
padding: 24px;
border: 1px solid var(--border);
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.06);
}
.card__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
}
.card__header-actions {
display: flex;
align-items: center;
gap: 10px;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid #e5e7eb;
font-size: 0.92rem;
}
th {
font-weight: 600;
color: var(--text-muted);
background: #f9fafb;
}
tbody tr:hover {
background: #f9fafb;
}
.badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 600;
}
.badge--success {
background: rgba(34, 197, 94, 0.12);
color: #15803d;
}
.badge--warning {
background: rgba(234, 179, 8, 0.15);
color: #b45309;
}
.badge--danger {
background: rgba(239, 68, 68, 0.12);
color: #b91c1c;
}
.empty-state {
text-align: center;
color: var(--text-muted);
padding: 24px 12px;
border: 2px dashed #e5e7eb;
border-radius: var(--radius-md);
}
.timeline {
display: flex;
flex-direction: column;
gap: 18px;
}
.timeline__item {
display: flex;
gap: 14px;
}
.timeline__icon {
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(99, 102, 241, 0.08);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.1rem;
color: #4338ca;
}
.timeline__title {
font-weight: 600;
margin-bottom: 4px;
}
.timeline__meta {
color: var(--text-muted);
font-size: 0.82rem;
margin-bottom: 4px;
}
.timeline__details {
font-size: 0.85rem;
color: var(--text);
opacity: 0.85;
}
.health-grid {
display: grid;
gap: 12px;
}
.health-card {
display: flex;
gap: 12px;
align-items: center;
border-radius: var(--radius-md);
padding: 14px 16px;
border: 1px solid transparent;
}
.health-card__icon {
font-size: 1.4rem;
}
.health-card__title {
font-weight: 600;
}
.health-card__details {
font-size: 0.88rem;
color: var(--text-muted);
}
.health-card--success {
background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.35);
}
.health-card--warning {
background: rgba(234, 179, 8, 0.12);
border-color: rgba(234, 179, 8, 0.35);
}
.health-card--danger {
background: rgba(239, 68, 68, 0.12);
border-color: rgba(239, 68, 68, 0.35);
}
.health-card--muted {
background: rgba(148, 163, 184, 0.12);
border-color: rgba(148, 163, 184, 0.35);
}
.quick-actions {
display: grid;
gap: 16px;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.quick-actions__item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: rgba(255, 255, 255, 0.92);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.quick-actions__item:hover {
transform: translateY(-3px);
box-shadow: 0 12px 20px rgba(79, 70, 229, 0.15);
}
.quick-actions__icon {
font-size: 1.6rem;
}
.quick-actions__title {
font-weight: 600;
margin-bottom: 4px;
}
.quick-actions__desc {
font-size: 0.85rem;
color: var(--text-muted);
}
.spinner {
width: 18px;
height: 18px;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #4338ca;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.admin-header__top {
flex-direction: column;
align-items: flex-start;
}
nav ul {
flex-wrap: wrap;
}
.stat-card__value {
font-size: 1.8rem;
}
}
</style>
</head>
<body>
<header>
<div class="admin-header">
<div class="admin-header__top">
<div>
<div class="admin-header__title"><?= admin_escape($title) ?></div>
<div style="opacity: 0.8; margin-top: 4px;">
Signed in as <?= admin_escape($_SESSION['ADMIN_NAME'] ?? 'Administrator') ?>
</div>
</div>
<div class="admin-header__actions">
<a class="admin-button" href="/" target="_blank" rel="noopener">View Site</a>
<a class="admin-button" href="/f_modules/m_backend/main.php" target="_blank" rel="noopener">Legacy Admin</a>
<a class="admin-button admin-button--primary" href="/login.php?logout=1">Logout</a>
</div>
</div>
<nav>
<ul>
<?php foreach ($navItems as $key => $item): ?>
<li>
<a href="<?= admin_escape($item['url']) ?>"
class="<?= $key === $activeTab ? 'active' : '' ?>">
<span><?= $item['icon'] ?></span>
<span><?= admin_escape($item['label']) ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
</nav>
</div>
</header>
<main>
<?php
}
function admin_page_end(string $extraScripts = ''): void
{
?>
</main>
<?php if ($extraScripts !== ''): ?>
<script><?= $extraScripts ?></script>
<?php endif; ?>
</body>
</html>
<?php
}
/**
* Render sidebar navigation for Bootstrap-based admin pages
*/
function render_admin_sidebar(string $activeTab = 'dashboard'): void
{
$navItems = [
'dashboard' => ['label' => 'Dashboard', 'url' => '/admin.php', 'icon' => 'bi-speedometer2'],
'users' => ['label' => 'Users', 'url' => '/admin_users.php', 'icon' => 'bi-people-fill'],
'content' => ['label' => 'Content', 'url' => '/admin_content_management.php', 'icon' => 'bi-collection-play-fill'],
'tokens' => ['label' => 'Tokens', 'url' => '/admin_token_dashboard.php', 'icon' => 'bi-coin'],
'settings' => ['label' => 'Settings', 'url' => '/admin_settings.php', 'icon' => 'bi-gear-fill'],
];
?>
<style>
.admin-sidebar {
position: fixed;
left: 0;
top: 0;
width: 250px;
height: 100vh;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: white;
padding: 20px 0;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
z-index: 1000;
}
.admin-sidebar__brand {
padding: 0 20px 20px;
font-size: 1.5rem;
font-weight: 700;
border-bottom: 1px solid rgba(255,255,255,0.2);
margin-bottom: 20px;
}
.admin-sidebar__nav {
list-style: none;
padding: 0;
margin: 0;
}
.admin-sidebar__nav-item {
margin-bottom: 5px;
}
.admin-sidebar__nav-link {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
color: rgba(255,255,255,0.8);
transition: all 0.2s;
text-decoration: none;
}
.admin-sidebar__nav-link:hover {
background: rgba(255,255,255,0.1);
color: white;
}
.admin-sidebar__nav-link.active {
background: rgba(255,255,255,0.15);
color: white;
border-left: 3px solid white;
}
.admin-sidebar__footer {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
padding: 0 20px;
font-size: 0.75rem;
color: rgba(255,255,255,0.6);
text-align: center;
}
</style>
<div class="admin-sidebar">
<div class="admin-sidebar__brand">
EasyStream Admin
</div>
<ul class="admin-sidebar__nav">
<?php foreach ($navItems as $key => $item): ?>
<li class="admin-sidebar__nav-item">
<a href="<?= htmlspecialchars($item['url']) ?>"
class="admin-sidebar__nav-link <?= $key === $activeTab ? 'active' : '' ?>">
<i class="bi <?= htmlspecialchars($item['icon']) ?>"></i>
<span><?= htmlspecialchars($item['label']) ?></span>
</a>
</li>
<?php endforeach; ?>
</ul>
<div class="admin-sidebar__footer">
Logged in as<br>
<?= htmlspecialchars($_SESSION['ADMIN_NAME'] ?? 'Administrator') ?>
</div>
</div>
<?php
}

View File

@@ -0,0 +1,224 @@
<?php
/**
* Search and Filter JavaScript for Admin Settings
* This file provides client-side search and filtering for settings
*/
?>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Add search box HTML if not already present
if (!document.getElementById('settings-search-container')) {
const searchHTML = `
<div id="settings-search-container" class="row mb-3">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" id="settings-search" class="form-control"
placeholder="Search settings... (Ctrl+K or Cmd+K)">
<button class="btn btn-outline-secondary" type="button" id="clear-search">
<i class="bi bi-x"></i>
</button>
</div>
<div id="search-results-count" class="text-muted small mt-1"></div>
</div>
<div class="col-md-4">
<select id="settings-category-filter" class="form-select">
<option value="">All Categories</option>
<option value="general">General</option>
<option value="modules">Modules</option>
<option value="branding">Branding</option>
<option value="payment">Payments</option>
<option value="email">Email</option>
<option value="payout">Payouts</option>
<option value="seo">SEO</option>
<option value="security">Security</option>
</select>
</div>
</div>
`;
// Insert before the tab content
const tabContent = document.querySelector('.tab-content');
if (tabContent) {
tabContent.insertAdjacentHTML('beforebegin', searchHTML);
}
}
const searchInput = document.getElementById('settings-search');
const clearBtn = document.getElementById('clear-search');
const categoryFilter = document.getElementById('settings-category-filter');
const resultsCount = document.getElementById('search-results-count');
if (!searchInput || !categoryFilter) return;
// Add data attributes to all form groups for searchability
function tagSettingFields() {
const currentTab = new URLSearchParams(window.location.search).get('tab') || 'general';
document.querySelectorAll('.mb-3, .form-group, .module-item').forEach(formGroup => {
const label = formGroup.querySelector('label, strong');
const input = formGroup.querySelector('input, select, textarea');
const helpText = formGroup.querySelector('.form-text, small');
if (label) {
const fieldName = input ? (input.name || input.id) : '';
const labelText = label.textContent.trim();
const helpContent = helpText ? helpText.textContent.trim() : '';
formGroup.setAttribute('data-setting-name', fieldName.toLowerCase());
formGroup.setAttribute('data-setting-label', labelText.toLowerCase());
formGroup.setAttribute('data-setting-info', helpContent.toLowerCase());
formGroup.setAttribute('data-setting-category', currentTab);
}
});
}
// Filter settings based on search and category
function filterSettings() {
const searchTerm = searchInput.value.toLowerCase().trim();
const category = categoryFilter.value;
let visibleCount = 0;
let totalCount = 0;
document.querySelectorAll('[data-setting-name]').forEach(element => {
totalCount++;
const name = element.getAttribute('data-setting-name') || '';
const label = element.getAttribute('data-setting-label') || '';
const info = element.getAttribute('data-setting-info') || '';
const settingCategory = element.getAttribute('data-setting-category') || '';
const matchesSearch = !searchTerm ||
name.includes(searchTerm) ||
label.includes(searchTerm) ||
info.includes(searchTerm);
const matchesCategory = !category || settingCategory === category;
const isVisible = matchesSearch && matchesCategory;
element.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
updateResultCount(visibleCount, totalCount, searchTerm);
// Highlight search terms
if (searchTerm) {
highlightSearchTerms(searchTerm);
} else {
removeHighlights();
}
}
function updateResultCount(visible, total, searchTerm) {
if (!resultsCount) return;
if (searchTerm || categoryFilter.value) {
resultsCount.textContent = `Showing ${visible} of ${total} settings`;
resultsCount.style.display = 'block';
} else {
resultsCount.textContent = '';
resultsCount.style.display = 'none';
}
}
function highlightSearchTerms(term) {
removeHighlights();
document.querySelectorAll('[data-setting-name]:not([style*="display: none"])').forEach(element => {
const labels = element.querySelectorAll('label, .form-text, small');
labels.forEach(label => {
const text = label.textContent;
const regex = new RegExp(`(${term})`, 'gi');
if (regex.test(text)) {
label.innerHTML = text.replace(regex, '<mark>$1</mark>');
}
});
});
}
function removeHighlights() {
document.querySelectorAll('mark').forEach(mark => {
mark.replaceWith(mark.textContent);
});
}
function clearSearch() {
searchInput.value = '';
categoryFilter.value = '';
filterSettings();
searchInput.focus();
}
// Event listeners
searchInput.addEventListener('input', filterSettings);
categoryFilter.addEventListener('change', filterSettings);
clearBtn.addEventListener('click', clearSearch);
// Keyboard shortcuts
document.addEventListener('keydown', function(e) {
// Ctrl+K or Cmd+K to focus search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
searchInput.focus();
searchInput.select();
}
// Escape to clear search
if (e.key === 'Escape' && document.activeElement === searchInput) {
clearSearch();
}
});
// Initialize
tagSettingFields();
// Re-tag when tab changes
const tabLinks = document.querySelectorAll('.nav-tabs a');
tabLinks.forEach(link => {
link.addEventListener('click', function() {
setTimeout(tagSettingFields, 100);
});
});
// Show keyboard shortcut hint
searchInput.setAttribute('title', 'Press Ctrl+K (or Cmd+K) to focus, Esc to clear');
});
</script>
<style>
#settings-search-container {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
mark {
background-color: #ffeb3b;
padding: 2px 4px;
border-radius: 3px;
font-weight: 600;
}
#search-results-count {
margin-top: 5px;
font-size: 0.875rem;
}
#settings-search:focus {
border-color: #1a73e8;
box-shadow: 0 0 0 0.2rem rgba(26, 115, 232, 0.25);
}
.input-group .btn-outline-secondary {
border-color: #ced4da;
}
.input-group .btn-outline-secondary:hover {
background-color: #dc3545;
border-color: #dc3545;
color: white;
}
</style>
<?php