map overlap bug fixed

This commit is contained in:
serversdwn
2025-12-15 18:27:00 +00:00
parent 3a41b81bb6
commit 6db958ffa6
10 changed files with 276 additions and 108 deletions

View File

@@ -5,6 +5,28 @@ All notable changes to Seismo Fleet Manager will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.3.3] - 2025-12-12
### Changed
- **Mobile Navigation**: Moved hamburger menu button from floating top-right to bottom navigation bar
- Bottom nav now shows: Menu (hamburger), Dashboard, Roster, Settings
- Removed "Add Unit" from bottom nav (still accessible via sidebar menu)
- Hamburger no longer floats over content on mobile
- **Status Dot Visibility**: Increased status dot size from 12px to 16px (w-3/h-3 → w-4/h-4) in dashboard fleet overview for better at-a-glance visibility
- Affects both Active and Benched tabs in dashboard
- Makes status colors (green/yellow/red) easier to spot during quick scroll
### Fixed
- **Location Navigation**: Moved tap-to-navigate functionality from roster card view to unit detail modal only
- Roster cards now show simple location text with pin emoji
- Navigation links (opening Maps app) only appear in the modal when tapping a unit
- Reduces visual clutter and accidental navigation triggers
### Technical Details
- Bottom navigation remains at 4 buttons, first button now triggers sidebar menu
- Removed standalone hamburger button element and associated CSS
- Modal already had navigation links, no changes needed there
## [0.3.2] - 2025-12-12
### Added
@@ -209,6 +231,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Photo management per unit
- Automated status categorization (OK/Pending/Missing)
[0.3.3]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.3.2...v0.3.3
[0.3.2]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.3.1...v0.3.2
[0.3.1]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.3.0...v0.3.1
[0.3.0]: https://github.com/serversdwn/seismo-fleet-manager/compare/v0.2.1...v0.3.0

View File

@@ -1,4 +1,4 @@
# Seismo Fleet Manager v0.3.2
# Seismo Fleet Manager v0.3.3
Backend API and HTMX-powered web interface for managing a mixed fleet of seismographs and field modems. Track deployments, monitor health in real time, merge roster intent with incoming telemetry, and control your fleet through a unified database and dashboard.
## Features
@@ -437,6 +437,11 @@ docker compose down -v
## Release Highlights
### v0.3.3 — 2025-12-12
- **Improved Mobile Navigation**: Hamburger menu moved to bottom nav bar (no more floating button covering content)
- **Better Status Visibility**: Larger status dots (16px) in dashboard fleet overview for easier at-a-glance status checks
- **Cleaner Roster Cards**: Location navigation links moved to detail modal only, reducing clutter in card view
### v0.3.2 — 2025-12-12
- **Progressive Web App (PWA)**: Complete mobile optimization with offline support, installable as standalone app
- **Mobile-First UI**: Hamburger menu, bottom navigation bar, card-based roster view optimized for touch
@@ -494,9 +499,11 @@ MIT
## Version
**Current: 0.3.2**Progressive Web App with mobile optimization (2025-12-12)
**Current: 0.3.3**Mobile navigation improvements and better status visibility (2025-12-12)
Previous: 0.3.1Dashboard alerts and status fixes (2025-12-12)
Previous: 0.3.2Progressive Web App with mobile optimization (2025-12-12)
0.3.1 — Dashboard alerts and status fixes (2025-12-12)
0.3.0 — Series 4 support, settings redesign, user preferences (2025-12-09)

View File

@@ -20,7 +20,7 @@ Base.metadata.create_all(bind=engine)
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
# Initialize FastAPI app
VERSION = "0.3.2"
VERSION = "0.3.3"
app = FastAPI(
title="Seismo Fleet Manager",
description="Backend API for managing seismograph fleet status",

View File

@@ -455,6 +455,54 @@
}
}
/* ===== MAP OVERLAP FIX ===== */
/* Prevent map and controls from overlapping UI elements on mobile */
@media (max-width: 767px) {
/* Constrain leaflet container to prevent overflow */
.leaflet-container {
max-width: 100%;
overflow: hidden;
}
/* Override Leaflet's default high z-index values */
/* Bottom nav is z-20, sidebar is z-40, so map must be below */
.leaflet-pane,
.leaflet-tile-pane,
.leaflet-overlay-pane,
.leaflet-shadow-pane,
.leaflet-marker-pane,
.leaflet-tooltip-pane,
.leaflet-popup-pane {
z-index: 1 !important;
}
/* Map controls should also be below navigation elements */
.leaflet-control-container,
.leaflet-top,
.leaflet-bottom,
.leaflet-left,
.leaflet-right {
z-index: 1 !important;
}
.leaflet-control {
z-index: 1 !important;
}
/* When sidebar is open, hide all Leaflet controls (zoom, attribution, etc) */
body.menu-open .leaflet-control-container {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease-in-out;
}
/* Ensure map tiles are non-interactive when sidebar is open */
body.menu-open #fleet-map,
body.menu-open #unit-map {
pointer-events: none;
}
}
/* ===== PENDING SYNC BADGE ===== */
.pending-sync-badge {
display: inline-flex;

View File

@@ -20,12 +20,14 @@ function toggleMenu() {
backdrop.classList.remove('show');
hamburgerBtn?.classList.remove('menu-open');
document.body.style.overflow = '';
document.body.classList.remove('menu-open');
} else {
// Open menu
sidebar.classList.add('open');
backdrop.classList.add('show');
hamburgerBtn?.classList.add('menu-open');
document.body.style.overflow = 'hidden';
document.body.classList.add('menu-open');
}
}
}
@@ -41,6 +43,7 @@ function closeMenuFromBackdrop() {
backdrop.classList.remove('show');
hamburgerBtn?.classList.remove('menu-open');
document.body.style.overflow = '';
document.body.classList.remove('menu-open');
}
}
@@ -56,6 +59,7 @@ function handleResize() {
backdrop.classList.remove('show');
hamburgerBtn?.classList.remove('menu-open');
document.body.style.overflow = '';
document.body.classList.remove('menu-open');
}
}
}

View File

@@ -69,14 +69,6 @@
{% block extra_head %}{% endblock %}
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<!-- Hamburger Button (Mobile Only) -->
<button id="hamburgerBtn" class="hamburger-btn md:hidden" onclick="toggleMenu()" aria-label="Menu">
<div class="hamburger-icon">
<div class="hamburger-line"></div>
<div class="hamburger-line"></div>
<div class="hamburger-line"></div>
</div>
</button>
<!-- Offline Indicator -->
<div id="offlineIndicator" class="offline-indicator">
@@ -172,6 +164,12 @@
<!-- Bottom Navigation (Mobile Only) -->
<nav class="bottom-nav">
<div class="grid grid-cols-4 h-16">
<button id="hamburgerBtn" class="bottom-nav-btn" onclick="toggleMenu()" aria-label="Menu">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
<span>Menu</span>
</button>
<button class="bottom-nav-btn" data-href="/" onclick="window.location.href='/'">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
@@ -184,12 +182,6 @@
</svg>
<span>Roster</span>
</button>
<button class="bottom-nav-btn" data-href="/roster?action=add" onclick="window.location.href='/roster?action=add'">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
<span>Add Unit</span>
</button>
<button class="bottom-nav-btn" data-href="/settings" onclick="window.location.href='/settings'">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
@@ -368,10 +360,10 @@
</script>
<!-- Offline Database -->
<script src="/static/offline-db.js?v=0.3.2"></script>
<script src="/static/offline-db.js?v=0.3.3"></script>
<!-- Mobile JavaScript -->
<script src="/static/mobile.js?v=0.3.2"></script>
<script src="/static/mobile.js?v=0.3.3"></script>
{% block extra_scripts %}{% endblock %}
</body>

View File

@@ -30,16 +30,21 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Fleet Summary Card -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
<div class="flex items-center justify-between mb-4">
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="fleet-summary-card">
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-summary')">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Fleet Summary</h2>
<div class="flex items-center gap-2">
<svg class="w-6 h-6 text-seismo-orange" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z">
</path>
</svg>
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="fleet-summary-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
<div class="space-y-3">
</div>
<div class="space-y-3 card-content" id="fleet-summary-content">
<div class="flex justify-between items-center">
<span class="text-gray-600 dark:text-gray-400">Total Units</span>
<span id="total-units" class="text-3xl md:text-2xl font-bold text-gray-900 dark:text-white">--</span>
@@ -92,31 +97,41 @@
</div>
<!-- Recent Alerts Card -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
<div class="flex items-center justify-between mb-4">
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="recent-alerts-card">
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('recent-alerts')">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Alerts</h2>
<div class="flex items-center gap-2">
<svg class="w-6 h-6 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z">
</path>
</svg>
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="recent-alerts-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
<div id="alerts-list" class="space-y-3">
</div>
<div id="alerts-list" class="space-y-3 card-content" id-content="recent-alerts-content">
<p class="text-sm text-gray-500 dark:text-gray-400">Loading alerts...</p>
</div>
</div>
<!-- Recent Photos Card -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
<div class="flex items-center justify-between mb-4">
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="recent-photos-card">
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('recent-photos')">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Photos</h2>
<div class="flex items-center gap-2">
<svg class="w-6 h-6 text-seismo-burgundy" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
</path>
</svg>
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="recent-photos-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
<div class="text-center text-gray-500 dark:text-gray-400">
</div>
<div class="text-center text-gray-500 dark:text-gray-400 card-content" id="recent-photos-content">
<svg class="w-16 h-16 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z">
@@ -129,28 +144,41 @@
</div>
<!-- Fleet Map -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6 mb-8">
<div class="flex items-center justify-between mb-4">
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6 mb-8" id="fleet-map-card">
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-map')">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Fleet Map</h2>
<div class="flex items-center gap-2">
<span class="text-sm text-gray-500 dark:text-gray-400">Deployed units</span>
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="fleet-map-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<div class="card-content" id="fleet-map-content">
<div id="fleet-map" class="w-full h-64 md:h-96 rounded-lg"></div>
</div>
</div>
<!-- Fleet Status Section with Tabs -->
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6">
<div class="rounded-xl shadow-lg bg-white dark:bg-slate-800 p-6" id="fleet-status-card">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center justify-between mb-4 cursor-pointer md:cursor-default" onclick="toggleCard('fleet-status')">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Fleet Status</h2>
<a href="/roster" class="text-seismo-orange hover:text-seismo-burgundy font-medium flex items-center">
<div class="flex items-center gap-2">
<a href="/roster" class="text-seismo-orange hover:text-seismo-burgundy font-medium flex items-center" onclick="event.stopPropagation()">
Full Roster
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
</svg>
</a>
<svg class="w-5 h-5 text-gray-500 transition-transform md:hidden chevron" id="fleet-status-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</div>
</div>
<div class="card-content" id="fleet-status-content">
<!-- Tab Bar -->
<div class="flex border-b border-gray-200 dark:border-gray-700 mb-4">
<button
@@ -177,6 +205,7 @@
hx-swap="innerHTML">
<p class="text-gray-500 dark:text-gray-400">Loading fleet data...</p>
</div>
</div>
</div>
</div>
@@ -195,10 +224,82 @@
color: #b84a12 !important; /* seismo orange */
border-bottom: 2px solid #b84a12 !important;
}
/* Collapsible cards (mobile only) */
@media (max-width: 767px) {
.card-content.collapsed {
display: none;
}
.chevron.collapsed {
transform: rotate(-90deg);
}
}
</style>
<script>
// Toggle card collapse/expand (mobile only)
function toggleCard(cardName) {
// Only work on mobile
if (window.innerWidth >= 768) return;
const content = document.getElementById(`${cardName}-content`);
const chevron = document.getElementById(`${cardName}-chevron`);
if (!content || !chevron) return;
// Toggle collapsed state
const isCollapsed = content.classList.contains('collapsed');
if (isCollapsed) {
content.classList.remove('collapsed');
chevron.classList.remove('collapsed');
// If expanding the fleet map, invalidate size after animation
if (cardName === 'fleet-map' && window.fleetMap) {
setTimeout(() => {
window.fleetMap.invalidateSize();
}, 300);
}
} else {
content.classList.add('collapsed');
chevron.classList.add('collapsed');
}
// Save state to localStorage
const cardStates = JSON.parse(localStorage.getItem('dashboardCardStates') || '{}');
cardStates[cardName] = !isCollapsed;
localStorage.setItem('dashboardCardStates', JSON.stringify(cardStates));
}
// Restore card states from localStorage on page load
function restoreCardStates() {
const cardStates = JSON.parse(localStorage.getItem('dashboardCardStates') || '{}');
const cardNames = ['fleet-summary', 'recent-alerts', 'recent-photos', 'fleet-map', 'fleet-status'];
cardNames.forEach(cardName => {
const content = document.getElementById(`${cardName}-content`);
const chevron = document.getElementById(`${cardName}-chevron`);
if (!content || !chevron) return;
// Default to expanded (true) if no saved state
const isCollapsed = cardStates[cardName] === false;
if (isCollapsed) {
content.classList.add('collapsed');
chevron.classList.add('collapsed');
}
});
}
// Restore states when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', restoreCardStates);
} else {
restoreCardStates();
}
function updateDashboard(event) {
try {
const data = JSON.parse(event.detail.xhr.response);
@@ -276,15 +377,24 @@ let fleetMap = null;
let fleetMarkers = [];
let fleetMapInitialized = false;
// Make fleetMap accessible globally for toggleCard function
window.fleetMap = null;
function initFleetMap() {
// Initialize the map centered on the US (can adjust based on your deployment area)
fleetMap = L.map('fleet-map').setView([39.8283, -98.5795], 4);
window.fleetMap = fleetMap;
// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 18
}).addTo(fleetMap);
// Force map to recalculate size after a brief delay to ensure container is fully rendered
setTimeout(() => {
fleetMap.invalidateSize();
}, 100);
}
function updateFleetMap(data) {
@@ -336,9 +446,11 @@ function updateFleetMap(data) {
}
});
// Fit map to show all markers only on first load
if (bounds.length > 0 && !fleetMapInitialized) {
fleetMap.fitBounds(bounds, { padding: [50, 50] });
// Fit map to show all markers
if (bounds.length > 0) {
// Use different padding for mobile vs desktop
const padding = window.innerWidth < 768 ? [20, 20] : [50, 50];
fleetMap.fitBounds(bounds, { padding: padding });
fleetMapInitialized = true;
}
}

View File

@@ -6,11 +6,11 @@
<!-- Status Indicator -->
<div class="flex-shrink-0">
{% if unit.status == 'OK' %}
<span class="w-3 h-3 rounded-full bg-green-500" title="OK"></span>
<span class="w-4 h-4 rounded-full bg-green-500" title="OK"></span>
{% elif unit.status == 'Pending' %}
<span class="w-3 h-3 rounded-full bg-yellow-500" title="Pending"></span>
<span class="w-4 h-4 rounded-full bg-yellow-500" title="Pending"></span>
{% else %}
<span class="w-3 h-3 rounded-full bg-red-500" title="Missing"></span>
<span class="w-4 h-4 rounded-full bg-red-500" title="Missing"></span>
{% endif %}
</div>

View File

@@ -5,7 +5,7 @@
<div class="flex items-center space-x-3 flex-1">
<!-- Status Indicator (grayed out for benched) -->
<div class="flex-shrink-0">
<span class="w-3 h-3 rounded-full bg-gray-400" title="Benched"></span>
<span class="w-4 h-4 rounded-full bg-gray-400" title="Benched"></span>
</div>
<!-- Unit Info -->

View File

@@ -229,32 +229,14 @@
{% endif %}
</div>
<!-- Location (Tap to Navigate) -->
<!-- Location -->
{% if unit.address %}
<div class="text-sm mb-1">
<a href="https://www.google.com/maps/search/?api=1&query={{ unit.address | urlencode }}"
target="_blank"
onclick="event.stopPropagation()"
class="flex items-center gap-1 text-seismo-orange hover:text-orange-600 dark:text-seismo-orange dark:hover:text-orange-400">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span class="underline">{{ unit.address }}</span>
</a>
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">
📍 {{ unit.address }}
</div>
{% elif unit.coordinates %}
<div class="text-sm mb-1">
<a href="https://www.google.com/maps/search/?api=1&query={{ unit.coordinates | urlencode }}"
target="_blank"
onclick="event.stopPropagation()"
class="flex items-center gap-1 text-seismo-orange hover:text-orange-600 dark:text-seismo-orange dark:hover:text-orange-400">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
<span class="font-mono underline">{{ unit.coordinates }}</span>
</a>
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">
📍 {{ unit.coordinates }}
</div>
{% endif %}