diff --git a/app/static/app.js b/app/static/app.js
new file mode 100644
index 0000000..a744b98
--- /dev/null
+++ b/app/static/app.js
@@ -0,0 +1,578 @@
+// Frigate Alerts - Web UI
+(function () {
+ 'use strict';
+
+ let currentView = 'dashboard';
+ let currentTagEventId = null;
+ let refreshTimer = null;
+ let knownObjectsCache = [];
+
+ // --- Initialization ---
+
+ document.addEventListener('DOMContentLoaded', () => {
+ setupNavigation();
+ setupTagModal();
+ loadView('dashboard');
+ loadStats();
+ });
+
+ // --- Navigation ---
+
+ function setupNavigation() {
+ document.querySelectorAll('.sidebar-item').forEach(item => {
+ item.addEventListener('click', (e) => {
+ e.preventDefault();
+ const view = item.dataset.view;
+ loadView(view);
+ });
+ });
+ }
+
+ function setActiveNav(view) {
+ document.querySelectorAll('.sidebar-item').forEach(item => {
+ item.classList.toggle('active', item.dataset.view === view);
+ });
+ }
+
+ // --- View Router ---
+
+ function loadView(view) {
+ currentView = view;
+ setActiveNav(view);
+ clearAutoRefresh();
+
+ switch (view) {
+ case 'dashboard':
+ loadDashboard();
+ startAutoRefresh();
+ break;
+ case 'review':
+ loadReview();
+ startAutoRefresh();
+ break;
+ case 'known':
+ loadKnown();
+ break;
+ case 'settings':
+ loadSettings();
+ break;
+ }
+ }
+
+ // --- Auto Refresh ---
+
+ function startAutoRefresh() {
+ refreshTimer = setInterval(() => {
+ if (currentView === 'dashboard') {
+ loadDashboard();
+ } else if (currentView === 'review') {
+ loadReview();
+ }
+ loadStats();
+ }, 30000);
+ }
+
+ function clearAutoRefresh() {
+ if (refreshTimer) {
+ clearInterval(refreshTimer);
+ refreshTimer = null;
+ }
+ }
+
+ // --- Dashboard View ---
+
+ function loadDashboard() {
+ const content = document.getElementById('main-content');
+ content.innerHTML = '
Loading events
';
+
+ fetch('/api/events?limit=50')
+ .then(r => r.json())
+ .then(events => {
+ if (!events.length) {
+ content.innerHTML = `
+ Dashboard
+
+
No events recorded yet.
+
Events will appear here when Frigate detects objects.
+
`;
+ return;
+ }
+ content.innerHTML = `
+
+
+
+
+
+
+ `;
+
+ populateFilterOptions(events);
+ renderEvents(events);
+ })
+ .catch(err => {
+ content.innerHTML = `Dashboard
Error loading events: ${err.message}
`;
+ });
+ }
+
+ function populateFilterOptions(events) {
+ const cameras = [...new Set(events.map(e => e.camera).filter(Boolean))].sort();
+ const labels = [...new Set(events.map(e => e.label).filter(Boolean))].sort();
+
+ const cameraSelect = document.getElementById('filter-camera');
+ const labelSelect = document.getElementById('filter-label');
+
+ if (cameraSelect) {
+ cameras.forEach(c => {
+ const opt = document.createElement('option');
+ opt.value = c;
+ opt.textContent = c;
+ cameraSelect.appendChild(opt);
+ });
+ }
+
+ if (labelSelect) {
+ labels.forEach(l => {
+ const opt = document.createElement('option');
+ opt.value = l;
+ opt.textContent = l;
+ labelSelect.appendChild(opt);
+ });
+ }
+ }
+
+ // Expose filter function globally for inline onchange handlers
+ window._applyFilters = function () {
+ const camera = document.getElementById('filter-camera')?.value || '';
+ const label = document.getElementById('filter-label')?.value || '';
+ const status = document.getElementById('filter-status')?.value || '';
+
+ let url = '/api/events?limit=50';
+ if (camera) url += `&camera=${encodeURIComponent(camera)}`;
+ if (label) url += `&label=${encodeURIComponent(label)}`;
+ if (status) url += `&status=${encodeURIComponent(status)}`;
+
+ fetch(url)
+ .then(r => r.json())
+ .then(events => renderEvents(events))
+ .catch(err => console.error('Filter error:', err));
+ };
+
+ function renderEvents(events) {
+ const container = document.getElementById('events-list');
+ if (!container) return;
+
+ if (!events.length) {
+ container.innerHTML = 'No events match the current filters.
';
+ return;
+ }
+
+ container.innerHTML = events.map(event => `
+
+

+
+
+
${formatTime(event.timestamp)}
+ ${event.description ? `
${escapeHtml(event.description)}
` : ''}
+ ${event.identified_as ? `
Identified as: ${escapeHtml(event.identified_as)}
` : ''}
+
+
+
+
+
+
+ `).join('');
+ }
+
+ // --- Review View ---
+
+ function loadReview() {
+ const content = document.getElementById('main-content');
+ content.innerHTML = 'Loading review queue
';
+
+ fetch('/api/events?limit=50&status=unreviewed')
+ .then(r => r.json())
+ .then(events => {
+ if (!events.length) {
+ content.innerHTML = `
+ Review Queue
+
+
No events to review.
+
All events have been reviewed or dismissed.
+
`;
+ return;
+ }
+ content.innerHTML = `
+
+ `;
+ renderEvents(events);
+ })
+ .catch(err => {
+ content.innerHTML = `Review Queue
Error loading events: ${err.message}
`;
+ });
+ }
+
+ // --- Known Objects View ---
+
+ function loadKnown() {
+ const content = document.getElementById('main-content');
+ content.innerHTML = 'Loading known objects
';
+
+ fetch('/api/known-objects')
+ .then(r => r.json())
+ .then(objects => {
+ knownObjectsCache = objects;
+
+ if (!objects.length) {
+ content.innerHTML = `
+ Known Objects
+
+
No known objects registered.
+
Tag events from the Dashboard to add known objects.
+
`;
+ return;
+ }
+
+ content.innerHTML = `
+
+
+ ${objects.map(obj => `
+
+
${escapeHtml(obj.name)}
+
${escapeHtml(obj.type)}
+
${obj.embedding_count || 0} embedding${(obj.embedding_count || 0) !== 1 ? 's' : ''}
+ ${obj.samples && obj.samples.length ? `
+
+ ${obj.samples.map(s => `

`).join('')}
+
+ ` : ''}
+
+
+
+
+ `).join('')}
+
`;
+ })
+ .catch(err => {
+ content.innerHTML = `Known Objects
Error loading objects: ${err.message}
`;
+ });
+ }
+
+ // --- Settings View ---
+
+ function loadSettings() {
+ const content = document.getElementById('main-content');
+ content.innerHTML = 'Loading settings
';
+
+ fetch('/api/settings')
+ .then(r => r.json())
+ .then(settings => {
+ content.innerHTML = `
+ Settings
+ `;
+ })
+ .catch(err => {
+ content.innerHTML = `Settings
Error loading settings: ${err.message}
`;
+ });
+ }
+
+ window._saveSettings = function () {
+ const settings = {
+ vision_backend: document.getElementById('setting-vision-backend').value,
+ recognition_enabled: document.getElementById('setting-recognition-enabled').checked,
+ event_retention_days: parseInt(document.getElementById('setting-retention-days').value, 10)
+ };
+
+ fetch('/api/settings', {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(settings)
+ })
+ .then(r => {
+ if (!r.ok) throw new Error('Failed to save settings');
+ return r.json();
+ })
+ .then(() => showToast('Settings saved', 'success'))
+ .catch(err => showToast('Error: ' + err.message, 'error'));
+ };
+
+ // --- Stats ---
+
+ function loadStats() {
+ fetch('/api/stats')
+ .then(r => r.json())
+ .then(stats => {
+ const el = document.getElementById('stats-summary');
+ if (!el) return;
+
+ el.innerHTML = `
+
+ Total Events
+ ${stats.total_events || 0}
+
+
+ Unreviewed
+ ${stats.unreviewed || 0}
+
+
+ Known
+ ${stats.known || 0}
+
+
+ Unknown
+ ${stats.unknown || 0}
+
+
+ Known Objects
+ ${stats.known_objects || 0}
+
`;
+ })
+ .catch(() => {
+ // Stats are non-critical, silently fail
+ });
+ }
+
+ // --- Tag Modal ---
+
+ function setupTagModal() {
+ document.getElementById('tag-cancel').addEventListener('click', closeTagModal);
+ document.getElementById('tag-save').addEventListener('click', saveTag);
+
+ document.getElementById('tag-existing-object').addEventListener('change', (e) => {
+ const newFields = document.getElementById('tag-new-fields');
+ newFields.style.display = e.target.value ? 'none' : 'block';
+ });
+
+ // Close modal on backdrop click
+ document.getElementById('tag-modal').addEventListener('click', (e) => {
+ if (e.target === document.getElementById('tag-modal')) {
+ closeTagModal();
+ }
+ });
+
+ // Close modal on Escape key
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') closeTagModal();
+ });
+ }
+
+ window._tagEvent = function (eventId) {
+ currentTagEventId = eventId;
+
+ // Show snapshot in modal
+ const snapshotContainer = document.getElementById('tag-modal-snapshot');
+ snapshotContainer.innerHTML = `
`;
+
+ // Load known objects for dropdown
+ fetch('/api/known-objects')
+ .then(r => r.json())
+ .then(objects => {
+ knownObjectsCache = objects;
+ const select = document.getElementById('tag-existing-object');
+ select.innerHTML = '';
+ objects.forEach(obj => {
+ const opt = document.createElement('option');
+ opt.value = obj.id;
+ opt.textContent = `${obj.name} (${obj.type})`;
+ select.appendChild(opt);
+ });
+ })
+ .catch(() => { });
+
+ // Reset form
+ document.getElementById('tag-name').value = '';
+ document.getElementById('tag-type').value = 'person';
+ document.getElementById('tag-new-fields').style.display = 'block';
+ document.getElementById('tag-existing-object').value = '';
+
+ document.getElementById('tag-modal').classList.remove('hidden');
+ };
+
+ function closeTagModal() {
+ document.getElementById('tag-modal').classList.add('hidden');
+ currentTagEventId = null;
+ }
+
+ function saveTag() {
+ if (!currentTagEventId) return;
+
+ const existingId = document.getElementById('tag-existing-object').value;
+ let body;
+
+ if (existingId) {
+ body = { known_object_id: existingId };
+ } else {
+ const name = document.getElementById('tag-name').value.trim();
+ const type = document.getElementById('tag-type').value;
+ if (!name) {
+ showToast('Please enter a name', 'error');
+ return;
+ }
+ body = { name: name, type: type };
+ }
+
+ fetch(`/api/events/${currentTagEventId}/tag`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body)
+ })
+ .then(r => {
+ if (!r.ok) throw new Error('Failed to tag event');
+ return r.json();
+ })
+ .then(() => {
+ showToast('Event tagged successfully', 'success');
+ closeTagModal();
+ loadView(currentView);
+ loadStats();
+ })
+ .catch(err => showToast('Error: ' + err.message, 'error'));
+ }
+
+ // --- Event Actions ---
+
+ window._dismissEvent = function (eventId) {
+ fetch(`/api/events/${eventId}/dismiss`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ })
+ .then(r => {
+ if (!r.ok) throw new Error('Failed to dismiss event');
+ return r.json();
+ })
+ .then(() => {
+ showToast('Event dismissed', 'success');
+ // Remove card from DOM for immediate feedback
+ const card = document.querySelector(`.event-card[data-event-id="${eventId}"]`);
+ if (card) {
+ card.style.opacity = '0';
+ card.style.transition = 'opacity 0.3s';
+ setTimeout(() => card.remove(), 300);
+ }
+ loadStats();
+ })
+ .catch(err => showToast('Error: ' + err.message, 'error'));
+ };
+
+ // --- Known Object Actions ---
+
+ window._deleteKnownObject = function (objectId, name) {
+ if (!confirm(`Delete known object "${name}"? This cannot be undone.`)) return;
+
+ fetch(`/api/known-objects/${objectId}`, {
+ method: 'DELETE'
+ })
+ .then(r => {
+ if (!r.ok) throw new Error('Failed to delete object');
+ return r.json();
+ })
+ .then(() => {
+ showToast(`Deleted "${name}"`, 'success');
+ loadKnown();
+ loadStats();
+ })
+ .catch(err => showToast('Error: ' + err.message, 'error'));
+ };
+
+ // --- Utilities ---
+
+ function escapeHtml(str) {
+ if (!str) return '';
+ const div = document.createElement('div');
+ div.textContent = str;
+ return div.innerHTML;
+ }
+
+ function formatTime(timestamp) {
+ if (!timestamp) return '';
+ const date = new Date(timestamp);
+ const now = new Date();
+ const diffMs = now - date;
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMins < 1) return 'Just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ if (diffDays < 7) return `${diffDays}d ago`;
+
+ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+ }
+
+ function showToast(message, type) {
+ const toast = document.createElement('div');
+ toast.className = `toast toast-${type}`;
+ toast.textContent = message;
+ document.body.appendChild(toast);
+ setTimeout(() => {
+ toast.style.opacity = '0';
+ toast.style.transition = 'opacity 0.3s';
+ setTimeout(() => toast.remove(), 300);
+ }, 3000);
+ }
+
+})();
diff --git a/app/static/index.html b/app/static/index.html
new file mode 100644
index 0000000..bb607a1
--- /dev/null
+++ b/app/static/index.html
@@ -0,0 +1,61 @@
+
+
+
+
+
+ Frigate Alerts
+
+
+
+
+
+
+
+
+
+
+
Tag as Known Object
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/static/style.css b/app/static/style.css
new file mode 100644
index 0000000..fbc281d
--- /dev/null
+++ b/app/static/style.css
@@ -0,0 +1,534 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: #1a1a2e;
+ color: #e0e0e0;
+ display: flex;
+ min-height: 100vh;
+}
+
+/* Sidebar */
+#sidebar {
+ width: 220px;
+ background: #16213e;
+ padding: 20px 0;
+ position: fixed;
+ height: 100vh;
+ overflow-y: auto;
+}
+
+.sidebar-header {
+ padding: 0 20px 20px;
+ border-bottom: 1px solid #0f3460;
+}
+
+.sidebar-header h2 {
+ color: #00b4d8;
+ font-size: 1.2em;
+}
+
+.sidebar-section {
+ padding: 10px 0;
+}
+
+.sidebar-item {
+ display: block;
+ padding: 10px 20px;
+ color: #a0a0a0;
+ text-decoration: none;
+ transition: all 0.2s;
+}
+
+.sidebar-item:hover {
+ background: #0f3460;
+ color: #fff;
+}
+
+.sidebar-item.active {
+ background: #0f3460;
+ color: #00b4d8;
+ border-left: 3px solid #00b4d8;
+}
+
+.sidebar-stats {
+ padding: 20px;
+ font-size: 0.85em;
+ color: #888;
+}
+
+/* Main content */
+#content {
+ margin-left: 220px;
+ flex: 1;
+ padding: 20px;
+}
+
+/* Page headers */
+h1 {
+ margin-bottom: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+/* Filters */
+.filters {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+}
+
+.filters select,
+.filters input {
+ padding: 6px 10px;
+ background: #16213e;
+ border: 1px solid #0f3460;
+ color: #e0e0e0;
+ border-radius: 4px;
+}
+
+.filters select:focus,
+.filters input:focus {
+ outline: none;
+ border-color: #00b4d8;
+}
+
+/* Event cards */
+.event-card {
+ background: #16213e;
+ border-radius: 8px;
+ padding: 15px;
+ margin-bottom: 15px;
+ display: flex;
+ gap: 15px;
+ transition: border-color 0.2s;
+ border: 1px solid transparent;
+}
+
+.event-card:hover {
+ border-color: #0f3460;
+}
+
+.event-card img {
+ width: 160px;
+ height: 120px;
+ object-fit: cover;
+ border-radius: 4px;
+ background: #1a1a2e;
+}
+
+.event-info {
+ flex: 1;
+}
+
+.event-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+}
+
+.event-camera {
+ font-weight: bold;
+ color: #00b4d8;
+}
+
+.event-label {
+ color: #a0a0a0;
+}
+
+.event-score {
+ color: #888;
+ font-size: 0.85em;
+}
+
+.event-description {
+ color: #ccc;
+ margin: 8px 0;
+ font-size: 0.9em;
+ line-height: 1.4;
+}
+
+.event-time {
+ color: #666;
+ font-size: 0.8em;
+ margin-bottom: 6px;
+}
+
+.event-actions {
+ margin-top: 10px;
+ display: flex;
+ gap: 8px;
+}
+
+/* Status badges */
+.badge {
+ padding: 3px 10px;
+ border-radius: 12px;
+ font-size: 0.8em;
+ font-weight: bold;
+}
+
+.badge-known {
+ background: #2d6a4f;
+ color: #95d5b2;
+}
+
+.badge-unknown {
+ background: #9a4c1a;
+ color: #f4a261;
+}
+
+.badge-ambiguous {
+ background: #7c6f09;
+ color: #ffd166;
+}
+
+.badge-unreviewed {
+ background: #444;
+ color: #aaa;
+}
+
+/* Buttons */
+.btn {
+ padding: 6px 14px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.85em;
+ transition: all 0.2s;
+ color: #ccc;
+ background: #2a2a4a;
+}
+
+.btn:hover {
+ background: #3a3a5a;
+ color: #fff;
+}
+
+.btn-primary {
+ background: #0f3460;
+ color: #fff;
+}
+
+.btn-primary:hover {
+ background: #00b4d8;
+}
+
+.btn-danger {
+ background: #7c2929;
+ color: #fff;
+}
+
+.btn-danger:hover {
+ background: #c0392b;
+}
+
+.btn-sm {
+ padding: 4px 10px;
+ font-size: 0.8em;
+}
+
+/* Modal */
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.modal.hidden {
+ display: none;
+}
+
+.modal-content {
+ background: #16213e;
+ border-radius: 8px;
+ padding: 25px;
+ width: 450px;
+ max-width: 90vw;
+}
+
+.modal-content h3 {
+ margin-bottom: 15px;
+ color: #00b4d8;
+}
+
+.modal-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+ margin-top: 15px;
+}
+
+#tag-modal-snapshot {
+ margin-bottom: 15px;
+}
+
+#tag-modal-snapshot img {
+ width: 100%;
+ max-height: 200px;
+ object-fit: cover;
+ border-radius: 4px;
+}
+
+/* Form elements */
+.form-group {
+ margin-bottom: 12px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 4px;
+ color: #a0a0a0;
+ font-size: 0.9em;
+}
+
+.form-group input,
+.form-group select {
+ width: 100%;
+ padding: 8px 10px;
+ background: #1a1a2e;
+ border: 1px solid #0f3460;
+ color: #e0e0e0;
+ border-radius: 4px;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: #00b4d8;
+}
+
+.form-group input[type="checkbox"] {
+ width: auto;
+ margin-right: 8px;
+}
+
+.form-group .checkbox-label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+}
+
+.form-group .help-text {
+ font-size: 0.8em;
+ color: #666;
+ margin-top: 4px;
+}
+
+/* Known objects grid */
+.known-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 15px;
+}
+
+.known-card {
+ background: #16213e;
+ border-radius: 8px;
+ padding: 15px;
+ border: 1px solid transparent;
+ transition: border-color 0.2s;
+}
+
+.known-card:hover {
+ border-color: #0f3460;
+}
+
+.known-card h4 {
+ color: #00b4d8;
+ margin-bottom: 5px;
+}
+
+.known-card .type-badge {
+ font-size: 0.8em;
+ color: #a0a0a0;
+ text-transform: capitalize;
+}
+
+.known-card .embedding-count {
+ font-size: 0.8em;
+ color: #666;
+ margin-top: 6px;
+}
+
+.known-card .known-card-actions {
+ margin-top: 10px;
+ display: flex;
+ gap: 8px;
+}
+
+.known-card .known-card-samples {
+ display: flex;
+ gap: 5px;
+ margin-top: 10px;
+ overflow-x: auto;
+}
+
+.known-card .known-card-samples img {
+ width: 60px;
+ height: 60px;
+ object-fit: cover;
+ border-radius: 4px;
+ background: #1a1a2e;
+}
+
+/* Settings */
+.settings-form {
+ max-width: 600px;
+}
+
+.settings-form .form-group {
+ margin-bottom: 15px;
+}
+
+.settings-section {
+ margin-bottom: 30px;
+}
+
+.settings-section h3 {
+ color: #00b4d8;
+ margin-bottom: 15px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #0f3460;
+}
+
+/* Stats in sidebar */
+.stat-item {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+}
+
+.stat-value {
+ color: #00b4d8;
+ font-weight: bold;
+}
+
+/* Empty state */
+.empty-state {
+ text-align: center;
+ color: #666;
+ padding: 60px 20px;
+}
+
+.empty-state p {
+ margin-top: 10px;
+ font-size: 0.9em;
+}
+
+/* Loading spinner */
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: #666;
+}
+
+.loading::after {
+ content: '';
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ border: 2px solid #0f3460;
+ border-top-color: #00b4d8;
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ margin-left: 10px;
+ vertical-align: middle;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Toast notifications */
+.toast {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ padding: 12px 20px;
+ border-radius: 6px;
+ color: #fff;
+ font-size: 0.9em;
+ z-index: 2000;
+ animation: slideIn 0.3s ease-out;
+}
+
+.toast-success {
+ background: #2d6a4f;
+}
+
+.toast-error {
+ background: #7c2929;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateY(20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ #sidebar {
+ width: 100%;
+ height: auto;
+ position: relative;
+ }
+
+ .sidebar-section {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ .sidebar-item {
+ padding: 8px 15px;
+ }
+
+ .sidebar-stats {
+ display: none;
+ }
+
+ #content {
+ margin-left: 0;
+ }
+
+ body {
+ flex-direction: column;
+ }
+
+ .event-card {
+ flex-direction: column;
+ }
+
+ .event-card img {
+ width: 100%;
+ height: auto;
+ }
+
+ .known-grid {
+ grid-template-columns: 1fr;
+ }
+}