Commit 76a7982f authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(dashboard): the game's real numbers

The admin dashboard opens on the numbers a store game is run by, from
manager_insights() in the player database (one call, cached 5 minutes,
7/30/90-day windows; test accounts never count): online now, DAU/WAU/MAU
and stickiness, new players, share of guests, retention (back the next
day, after a week, after a month), matches per active player, daily
charts of active and new players and of matches per game, a per-game
table (online vs bot, players, finished vs abandoned, average length),
time in the app and platforms (recorded from today), countries, coins
given and spent by source, social and tournaments. Pending reports and
approvals sit on top when there are any. Charts are plain SVG.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 855056dc
......@@ -85,13 +85,33 @@ class DashboardController
$gamesToday[$game] = $db->count($table, ['created_at' => 'gte.' . $since]);
}
$insights = $this->insights($db, max(7, min(90, (int)($_GET['days'] ?? 30))));
return compact(
'totalPlayers', 'onlinePlayers', 'totalMatches', 'activeMatches',
'activeTournaments', 'pendingReports', 'totalAdminUsers', 'pendingApprovals',
'recentActivity', 'supabaseHealth', 'stockfishHealth', 'swissHealth', 'gamesToday'
'recentActivity', 'supabaseHealth', 'stockfishHealth', 'swissHealth', 'gamesToday', 'insights'
);
}
/**
* The game's numbers (manager_insights() in the player database, one
* call), kept five minutes per window so the dashboard opens fast.
*/
private function insights(Database $db, int $days): array
{
$file = STORAGE_PATH . '/cache/insights_' . $days . '.json';
if (is_file($file) && time() - filemtime($file) < 300 && empty($_GET['fresh'])) {
$cached = json_decode((string)file_get_contents($file), true);
if (is_array($cached)) return $cached;
}
$data = $db->rpc('manager_insights', ['p_days' => $days]);
if (!is_array($data) || !isset($data['players'])) return [];
if (!is_dir(dirname($file))) @mkdir(dirname($file), 0755, true);
@file_put_contents($file, json_encode($data));
return $data;
}
private function getModeratorData(Database $db): array
{
$pendingReports = $db->count('cheat_reports', ['status' => 'eq.pending']);
......
<?php
/**
* The game's numbers, from manager_insights() (player database): who plays,
* who comes back, what they play and for how long. Charts are drawn here as
* plain SVG (no chart library). Test accounts never count.
*
* Expects $insights (array, may be empty), $pendingReports, $pendingApprovals.
*/
$I = $insights ?? [];
$P = $I['players'] ?? [];
$num = fn($v) => number_format((float)($v ?? 0));
$pct = fn($a, $b) => $b > 0 ? round(100 * $a / $b) . '%' : '—';
$gameNames = ['chess' => 'شطرنج', 'ludo' => 'لودو', 'domino' => 'دومينو', 'backgammon' => 'طاولة'];
$gameColors = ['chess' => '#6B3FD4', 'ludo' => '#E5484D', 'domino' => '#16A05A', 'backgammon' => '#E8A90F'];
$series = $I['series'] ?? [];
$days = (int)($I['days'] ?? 30);
/** Bars for one value per day (an optional second value drawn on top, darker). */
$bars = function (array $series, string $key, string $color, ?string $key2 = null, string $color2 = '') {
$w = 640; $h = 150; $pad = 4;
$n = max(1, count($series));
$max = max(1, ...array_map(fn($d) => (int)($d[$key] ?? 0), $series ?: [[$key => 1]]));
$bw = ($w - $pad * 2) / $n;
$out = '<svg class="ins-chart" viewBox="0 0 ' . $w . ' ' . ($h + 18) . '" preserveAspectRatio="none" role="img">';
foreach ($series as $i => $d) {
$v = (int)($d[$key] ?? 0);
$bh = $v / $max * ($h - 10);
$x = $pad + $i * $bw;
$out .= '<rect x="' . round($x + 1, 1) . '" y="' . round($h - $bh, 1) . '" width="' . round(max(1, $bw - 2), 1) . '" height="' . round($bh, 1) . '" rx="2" fill="' . $color . '"><title>' . htmlspecialchars(($d['day'] ?? '') . ': ' . $v) . '</title></rect>';
if ($key2 !== null) {
$v2 = min($v, (int)($d[$key2] ?? 0));
$bh2 = $v2 / $max * ($h - 10);
$out .= '<rect x="' . round($x + 1, 1) . '" y="' . round($h - $bh2, 1) . '" width="' . round(max(1, $bw - 2), 1) . '" height="' . round($bh2, 1) . '" rx="2" fill="' . $color2 . '"><title>' . htmlspecialchars(($d['day'] ?? '') . ': ' . $v2) . '</title></rect>';
}
}
$first = $series[0]['day'] ?? ''; $last = $series[count($series) - 1]['day'] ?? '';
$out .= '<text x="' . $pad . '" y="' . ($h + 14) . '" class="ins-axis">' . htmlspecialchars(substr($first, 5)) . '</text>';
$out .= '<text x="' . ($w - $pad) . '" y="' . ($h + 14) . '" class="ins-axis" text-anchor="end">' . htmlspecialchars(substr($last, 5)) . '</text>';
$out .= '<text x="' . ($w - $pad) . '" y="12" class="ins-axis" text-anchor="end">' . $max . '</text>';
return $out . '</svg>';
};
/** Matches per day, stacked by game. */
$stack = function (array $series) use ($gameColors) {
$w = 640; $h = 150; $pad = 4;
$n = max(1, count($series));
$tot = fn($d) => array_sum(array_map('intval', (array)($d['matches'] ?? [])));
$max = max(1, ...array_map($tot, $series ?: [[]]));
$bw = ($w - $pad * 2) / $n;
$out = '<svg class="ins-chart" viewBox="0 0 ' . $w . ' ' . ($h + 18) . '" preserveAspectRatio="none" role="img">';
foreach ($series as $i => $d) {
$y = $h; $x = $pad + $i * $bw;
foreach ($gameColors as $g => $c) {
$v = (int)(($d['matches'] ?? [])[$g] ?? 0);
if ($v <= 0) continue;
$bh = $v / $max * ($h - 10);
$y -= $bh;
$out .= '<rect x="' . round($x + 1, 1) . '" y="' . round($y, 1) . '" width="' . round(max(1, $bw - 2), 1) . '" height="' . round($bh, 1) . '" fill="' . $c . '"><title>' . htmlspecialchars(($d['day'] ?? '') . ' ' . $g . ': ' . $v) . '</title></rect>';
}
}
$out .= '<text x="' . ($w - $pad) . '" y="12" class="ins-axis" text-anchor="end">' . $max . '</text>';
return $out . '</svg>';
};
$ret = $I['retention'] ?? [];
$r = fn($k) => $ret[$k] ?? ['cohort' => 0, 'back' => 0];
$avgDau = (float)($P['avg_dau_30'] ?? 0);
$mau = (int)($P['mau'] ?? 0);
?>
<?php if (!$I): ?>
<div class="card mb-6"><p class="text-secondary">الأرقام مش متاحة دلوقتي. جرّب تفتح الصفحة تاني بعد شوية.</p></div>
<?php else: ?>
<div class="ins-head mb-4">
<h2 class="ins-title">أرقام اللعبة</h2>
<div class="ins-range">
<?php foreach ([7, 30, 90] as $dd): ?>
<a href="?days=<?= $dd ?>" class="ins-chip <?= $days === $dd ? 'is-on' : '' ?>">آخر <?= $dd ?> يوم</a>
<?php endforeach; ?>
</div>
</div>
<?php if (($pendingReports ?? 0) > 0 || ($pendingApprovals ?? 0) > 0): ?>
<div class="ins-attention mb-4">
<?php if ($pendingReports > 0): ?><a href="/moderation" class="ins-chip is-warn"><?= $pendingReports ?> بلاغ مستني مراجعة</a><?php endif; ?>
<?php if ($pendingApprovals > 0): ?><a href="/workflows?status=pending" class="ins-chip is-warn"><?= $pendingApprovals ?> موافقة مستنية</a><?php endif; ?>
</div>
<?php endif; ?>
<div class="ins-kpis mb-6">
<div class="ins-kpi"><span class="ins-kpi-label">أونلاين دلوقتي</span><span class="ins-kpi-value"><?= $num($P['online_now'] ?? 0) ?></span></div>
<div class="ins-kpi"><span class="ins-kpi-label">لعبوا النهارده (DAU)</span><span class="ins-kpi-value"><?= $num($P['dau'] ?? 0) ?></span></div>
<div class="ins-kpi"><span class="ins-kpi-label">آخر 7 أيام (WAU)</span><span class="ins-kpi-value"><?= $num($P['wau'] ?? 0) ?></span></div>
<div class="ins-kpi"><span class="ins-kpi-label">آخر 30 يوم (MAU)</span><span class="ins-kpi-value"><?= $num($mau) ?></span></div>
<div class="ins-kpi"><span class="ins-kpi-label">الالتزام (DAU ÷ MAU)</span><span class="ins-kpi-value"><?= $mau > 0 ? round(100 * $avgDau / $mau) . '%' : '—' ?></span><span class="ins-kpi-sub">متوسط <?= $avgDau ?> لاعب في اليوم</span></div>
<div class="ins-kpi"><span class="ins-kpi-label">لاعبين جداد</span><span class="ins-kpi-value"><?= $num($P['new_today'] ?? 0) ?></span><span class="ins-kpi-sub"><?= $num($P['new_7'] ?? 0) ?> في 7 أيام · <?= $num($P['new_30'] ?? 0) ?> في 30</span></div>
<div class="ins-kpi"><span class="ins-kpi-label">كل اللاعبين</span><span class="ins-kpi-value"><?= $num($P['total'] ?? 0) ?></span><span class="ins-kpi-sub"><?= $pct((int)($P['guests'] ?? 0), (int)($P['total'] ?? 0)) ?> ضيوف من غير حساب</span></div>
<div class="ins-kpi"><span class="ins-kpi-label">ماتشات لكل لاعب نشط</span><span class="ins-kpi-value"><?= htmlspecialchars((string)($I['session']['matches_per_active'] ?? '—')) ?></span><span class="ins-kpi-sub">في اليوم، آخر 30 يوم</span></div>
</div>
<div class="grid grid-2 gap-4 mb-6">
<div class="card">
<div class="ins-card-head"><h3>اللاعبين النشطين كل يوم</h3><span class="ins-legend"><i style="background:#C9B8F2"></i>نشط <i style="background:#6B3FD4"></i>جديد</span></div>
<?= $bars($series, 'active', '#C9B8F2', 'new', '#6B3FD4') ?>
</div>
<div class="card">
<div class="ins-card-head"><h3>الماتشات كل يوم</h3>
<span class="ins-legend"><?php foreach ($gameColors as $g => $c): ?><i style="background:<?= $c ?>"></i><?= $gameNames[$g] ?> <?php endforeach; ?></span></div>
<?= $stack($series) ?>
</div>
</div>
<div class="grid grid-3 gap-4 mb-6">
<div class="card">
<h3 class="ins-h3">بيرجعوا؟</h3>
<p class="text-xs text-secondary mb-3">من اللي سجّلوا في آخر 90 يوم، كام رجع يلعب</p>
<?php foreach (['d1' => 'تاني يوم', 'd7' => 'بعد أسبوع', 'd30' => 'بعد شهر'] as $k => $label): $x = $r($k); $rate = $x['cohort'] > 0 ? $x['back'] / $x['cohort'] : 0; ?>
<div class="ins-meter">
<span class="ins-meter-label"><?= $label ?></span>
<span class="ins-meter-bar"><i style="inline-size:<?= round(100 * $rate) ?>%"></i></span>
<span class="ins-meter-value"><?= $pct($x['back'], $x['cohort']) ?></span>
</div>
<?php endforeach; ?>
</div>
<div class="card">
<h3 class="ins-h3">وقت اللعب والأجهزة</h3>
<?php $mins = $I['session']['avg_minutes_per_active'] ?? null; ?>
<div class="ins-row"><span>متوسط الوقت في التطبيق</span><b><?= $mins !== null ? htmlspecialchars((string)$mins) . ' دقيقة في اليوم' : 'بيتحسب من النهارده' ?></b></div>
<?php $plat = $I['platforms'] ?? []; $platTotal = array_sum(array_map('intval', $plat)); ?>
<?php foreach (['android' => 'أندرويد', 'ios' => 'آيفون', 'web' => 'المتصفح', 'unknown' => 'مش معروف'] as $k => $label): if (empty($plat[$k])) continue; ?>
<div class="ins-row"><span><?= $label ?></span><b><?= $num($plat[$k]) ?> · <?= $pct((int)$plat[$k], $platTotal) ?></b></div>
<?php endforeach; ?>
<?php if (!$platTotal): ?><p class="text-xs text-muted">الأجهزة بتتسجل من النهارده.</p><?php endif; ?>
</div>
<div class="card">
<h3 class="ins-h3">منين؟</h3>
<?php foreach (($I['countries'] ?? []) as $c): ?>
<div class="ins-row"><span><?= $c['code'] === '--' ? 'مش محدد' : htmlspecialchars($c['code']) ?></span><b><?= $num($c['players']) ?></b></div>
<?php endforeach; ?>
</div>
</div>
<div class="card mb-6">
<h3 class="ins-h3">الألعاب (آخر 30 يوم)</h3>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>اللعبة</th><th>ماتشات</th><th>أونلاين</th><th>ضد البوت</th><th>لاعبين</th><th>كمّلوا</th><th>اتسابوا</th><th>متوسط الماتش</th></tr></thead>
<tbody>
<?php foreach ($gameNames as $g => $name): $x = ($I['games'] ?? [])[$g] ?? null; if (!$x) continue; $done = (int)$x['completed'] + (int)$x['abandoned']; ?>
<tr>
<td><span class="ins-dot" style="background:<?= $gameColors[$g] ?>"></span><?= $name ?></td>
<td><?= $num($x['matches']) ?></td>
<td><?= $num($x['online']) ?></td>
<td><?= $num($x['vs_bot']) ?></td>
<td><?= $num($x['players']) ?></td>
<td><?= $pct((int)$x['completed'], $done) ?></td>
<td><?= $pct((int)$x['abandoned'], $done) ?></td>
<td><?= $x['avg_minutes'] !== null ? htmlspecialchars((string)$x['avg_minutes']) . ' دقيقة' : '—' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="grid grid-3 gap-4 mb-6">
<?php $E = $I['economy'] ?? []; ?>
<div class="card">
<h3 class="ins-h3">العملات (آخر 30 يوم)</h3>
<div class="ins-row"><span>اتوزعت</span><b><?= $num($E['earned'] ?? 0) ?></b></div>
<div class="ins-row"><span>اتصرفت</span><b><?= $num($E['spent'] ?? 0) ?></b></div>
<div class="ins-row"><span>مع اللاعبين دلوقتي</span><b><?= $num($E['coins_held'] ?? 0) ?></b></div>
<?php $typeNames = ['game_reward' => 'مكافآت الماتشات', 'daily_reward' => 'الهدية اليومية', 'purchase' => 'شراء من المتجر', 'tournament_prize' => 'جوايز البطولات', 'tournament_fee' => 'رسوم البطولات', 'redeem' => 'أكواد']; ?>
<?php foreach (($E['by_type'] ?? []) as $type => $sum): ?>
<div class="ins-row ins-row-sub"><span><?= htmlspecialchars($typeNames[$type] ?? $type) ?></span><b><?= $num(abs((float)$sum)) ?></b></div>
<?php endforeach; ?>
</div>
<?php $S = $I['social'] ?? []; ?>
<div class="card">
<h3 class="ins-h3">الاجتماعي (آخر 30 يوم)</h3>
<div class="ins-row"><span>صداقات جديدة</span><b><?= $num($S['friendships_30'] ?? 0) ?></b></div>
<div class="ins-row"><span>رسايل</span><b><?= $num($S['messages_30'] ?? 0) ?></b></div>
<div class="ins-row"><span>أعضاء في نوادي</span><b><?= $num($S['org_members'] ?? 0) ?></b></div>
</div>
<?php $T = $I['tournaments'] ?? []; ?>
<div class="card">
<h3 class="ins-h3">البطولات</h3>
<div class="ins-row"><span>شغالة دلوقتي</span><b><?= $num($T['running'] ?? 0) ?></b></div>
<div class="ins-row"><span>تسجيلها مفتوح</span><b><?= $num($T['open'] ?? 0) ?></b></div>
<div class="ins-row"><span>تسجيلات آخر 30 يوم</span><b><?= $num($T['registrations_30'] ?? 0) ?></b></div>
<div class="ins-row"><span>زيارات الصفحات اللايف</span><b><?= $num($T['page_views_30'] ?? 0) ?></b></div>
</div>
</div>
<?php endif; ?>
......@@ -24,71 +24,7 @@
<?php endforeach; ?>
</div>
<!-- Stat Cards -->
<div class="grid grid-4 stagger mb-6">
<div class="stat-card">
<div class="stat-icon blue">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">اللاعبون المتصلون / الإجمالي</div>
<div class="stat-value"><span data-count="<?= $onlinePlayers ?>">0</span> / <?= number_format($totalPlayers) ?></div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon orange">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="6" width="20" height="12" rx="2"/><path d="M12 12h.01"/><path d="M17 12h.01"/><path d="M7 12h.01"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">المباريات النشطة / الإجمالي</div>
<div class="stat-value"><span data-count="<?= $activeMatches ?>">0</span> / <?= number_format($totalMatches) ?></div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon gold">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">البطولات الجارية</div>
<div class="stat-value" data-count="<?= $activeTournaments ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon red">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">البلاغات المعلقة</div>
<div class="stat-value" data-count="<?= $pendingReports ?>">0</div>
</div>
</div>
</div>
<!-- Admin Users & Approvals -->
<div class="grid grid-2 mb-6">
<div class="stat-card">
<div class="stat-icon purple">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="M9 12l2 2 4-4"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">إجمالي مستخدمي الإدارة</div>
<div class="stat-value" data-count="<?= $totalAdminUsers ?>">0</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon warning">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
</div>
<div class="stat-content">
<div class="stat-label">الموافقات المعلقة</div>
<div class="stat-value" data-count="<?= $pendingApprovals ?>">0</div>
</div>
</div>
</div>
<?php include __DIR__ . '/_insights.php'; ?>
<!-- Service Health -->
<div class="grid grid-2 mb-6">
......
......@@ -984,3 +984,33 @@ textarea.form-input {
.game-domino { --tile-tint: #E4F5EC; }
.game-backgammon { --tile-tint: #FFF3D6; }
@media (max-width: 900px) { .game-strip { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* Dashboard insights (modules/dashboard/views/_insights.php) */
.ins-head { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; }
.ins-title { font-size: var(--font-size-lg); font-weight: var(--font-weight-bold); }
.ins-range, .ins-attention { display: flex; gap: var(--space-2); flex-wrap: wrap; }
.ins-chip { padding: 6px 12px; border-radius: var(--radius-pill); border: 1px solid var(--border); background: var(--bg-secondary); color: var(--text-secondary); font-size: var(--font-size-xs); font-weight: var(--font-weight-semibold); text-decoration: none; }
.ins-chip.is-on { background: var(--accent); border-color: var(--accent); color: var(--text-inverse); }
.ins-chip.is-warn { background: var(--warning-bg); border-color: transparent; color: var(--warning); }
.ins-kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-4); }
.ins-kpi { display: flex; flex-direction: column; gap: 2px; padding: var(--space-4); border-radius: var(--radius-lg); background: var(--bg-secondary); border: 1px solid var(--border); box-shadow: var(--shadow-sm); }
.ins-kpi-label { font-size: var(--font-size-xs); color: var(--text-secondary); font-weight: var(--font-weight-medium); }
.ins-kpi-value { font-size: var(--font-size-xl); font-weight: 800; line-height: 1.2; font-variant-numeric: tabular-nums; }
.ins-kpi-sub { font-size: var(--font-size-xs); color: var(--text-muted); }
.ins-card-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); flex-wrap: wrap; margin-bottom: var(--space-3); }
.ins-card-head h3, .ins-h3 { font-size: var(--font-size-md); font-weight: var(--font-weight-bold); margin-bottom: var(--space-2); }
.ins-legend { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: var(--font-size-xs); color: var(--text-secondary); }
.ins-legend i { display: inline-block; inline-size: 10px; block-size: 10px; border-radius: 3px; }
.ins-chart { display: block; inline-size: 100%; block-size: 170px; direction: ltr; }
.ins-axis { font-size: 11px; fill: var(--text-muted); font-family: var(--font-family); }
.ins-meter { display: grid; grid-template-columns: 80px 1fr 48px; align-items: center; gap: var(--space-2); margin-block: var(--space-2); font-size: var(--font-size-sm); }
.ins-meter-bar { block-size: 10px; border-radius: var(--radius-pill); background: var(--bg-hover); overflow: hidden; }
.ins-meter-bar i { display: block; block-size: 100%; background: var(--accent); border-radius: inherit; }
.ins-meter-value { font-weight: var(--font-weight-bold); text-align: end; font-variant-numeric: tabular-nums; }
.ins-row { display: flex; justify-content: space-between; gap: var(--space-3); padding-block: 6px; border-block-end: 1px solid var(--border); font-size: var(--font-size-sm); }
.ins-row:last-child { border-block-end: 0; }
.ins-row span { color: var(--text-secondary); }
.ins-row b { font-variant-numeric: tabular-nums; }
.ins-row-sub { font-size: var(--font-size-xs); padding-inline-start: var(--space-3); }
.ins-dot { display: inline-block; inline-size: 10px; block-size: 10px; border-radius: 50%; margin-inline-end: 8px; }
@media (max-width: 1100px) { .ins-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
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