UI polish, bug fixes, and README
Changes since last commit: NATS airspace - Remove D and D_OTHER types from parser — UK small-arms ranges covered the entire country and made the layer unusable - Replace title-attribute tooltips on type filters with clickable ⓘ icons (mobile-friendly); info text appears in a shared box below the grid - Add Select All / Deselect All controls (moved to bottom of all layers, now applies to every layer checkbox not just NATS types) - Fix per-type filter using expression syntax ['match', ...] — legacy filter syntax was unreliable in MapLibre GL JS 4.x Map style switching - Fix overlay layers being lost when switching Terrain / Satellite / Streets by waiting for the 'idle' event instead of 'style.load'; MapTiler styles fire style.load before the map is ready to accept addSource/addLayer calls - Defensive cleanup at top of addAllLayers() removes all custom layers and sources before re-adding, preventing "source already exists" crashes Location tracking - Move locate button from floating bottom-right into the left panel - Auto-request location on page load - Dynamic button label: "Getting location…" → "Stop Tracking" → "Track location" - Suppress alert on permission-denied (error code 1); show inline message for other errors Panel UX - Move planning disclaimer out of panel body into a ⚠ icon in the header; click to expand, includes hyperlinked NOTAM link - Add NOTAM link to the data-links section at the bottom - Danger / Restricted and MoD / Military rows now have ⓘ icons explaining that these layers have no built-in data and require a GeoJSON upload; removes the old "Override: load GeoJSON below" note and "(override)" labels from the dropdown - Load Data section: stacked full-width layout (dropdown above button) - Lighten grey text (#555 → #888, #666 → #999) across section labels, layer notes, info icons, hints, data links, and popup close button Map bounds and attribution - Restrict panning to UK + Channel Islands with maxBounds - Replace default attribution control with custom one appending "© Bournemouth Technology" with link to bournemouthtechnology.co.uk README - Add full project description, feature list, tech stack table, setup instructions, and data source reference table
This commit is contained in:
86
README.md
86
README.md
@@ -0,0 +1,86 @@
|
|||||||
|
# DroneMapUK
|
||||||
|
|
||||||
|
A browser-based UK drone airspace planning tool built with MapLibre GL JS. Visualises the airspace restrictions, protected areas, and managed land relevant to UK drone operations in a single interactive map.
|
||||||
|
|
||||||
|
> **Planning aid only.** Always verify with official CAA sources and check NOTAMs at [aurora.nats.co.uk](https://www.aurora.nats.co.uk/) before any flight.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **NATS AIP Airspace** — parses the official NATS AIXM 5.1 XML dataset in a Web Worker (off the main thread) to render Restricted, Prohibited, CTR, CTA, TMA, RAS, and TMZ zones with per-type toggles and colour coding
|
||||||
|
- **Flight Restriction Zones (FRZ)** — 5 km radius circles around ~40 UK licensed aerodromes, hardcoded from CAA reference points
|
||||||
|
- **Danger / Restricted Areas** — load your own GeoJSON (e.g. converted from NATS ENR 5.1 KMZ)
|
||||||
|
- **MoD / Military Areas** — load your own GeoJSON
|
||||||
|
- **National Parks (GB)** — fetched automatically from the OS Open Geography Portal ArcGIS REST API
|
||||||
|
- **Royal Parks (London)** — 8 London parks with approximate boundaries
|
||||||
|
- **SSSIs** — England auto-loaded viewport-lazy from Natural England ArcGIS API (zoom ≥ 8); Scotland/Wales via file upload
|
||||||
|
- **Forestry managed land** — load GeoJSON from Forestry England, FLS Scotland, or NRW Wales
|
||||||
|
- **GPS location tracking** — live position with accuracy circle, auto-centres on first fix
|
||||||
|
- **Map styles** — Terrain, Satellite, Streets (MapTiler)
|
||||||
|
- **3D terrain** — MapTiler terrain-rgb-v2 DEM with pitch and sky layer
|
||||||
|
- **Hillshade overlay** — best on Satellite style
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
The app is three plain files — `index.html`, `style.css`, and `app.js` — with no build step, no bundler, and no framework.
|
||||||
|
|
||||||
|
| Concern | Detail |
|
||||||
|
|---|---|
|
||||||
|
| Map rendering | [MapLibre GL JS 4.x](https://maplibre.org/) via CDN |
|
||||||
|
| Base tiles | [MapTiler](https://maptiler.com) (requires a free API key) |
|
||||||
|
| NATS parsing | `nats-worker.js` Web Worker — receives the 72 MB AIXM XML as an `ArrayBuffer`, parses GML geometry (GeodesicString, CircleByCenterPoint, ArcByCenterPoint), and posts GeoJSON back to the main thread |
|
||||||
|
| API key storage | `localStorage` key `droneMapUK_maptilerKey` |
|
||||||
|
| SSSI loading | Viewport-lazy fetch from Natural England ArcGIS FeatureServer, debounced 450 ms on map move |
|
||||||
|
| National Parks | Single fetch from OS Open Geography Portal on map load |
|
||||||
|
| Style switching | Uses MapLibre `idle` event (not `style.load`) to re-add sources and layers after `setStyle()` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Get a free API key at [maptiler.com](https://maptiler.com)
|
||||||
|
2. Serve the directory from a local web server — the NATS XML fetch requires HTTP, not `file://`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python
|
||||||
|
python3 -m http.server 8080
|
||||||
|
|
||||||
|
# Node
|
||||||
|
npx serve .
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Open `http://localhost:8080` in a browser and enter your MapTiler key when prompted
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NATS airspace data
|
||||||
|
|
||||||
|
Download the official NATS AIP dataset from [nats-uk.ead-it.com](https://nats-uk.ead-it.com/cms-nats/opencms/en/uas-restriction-zones/) and place the extracted XML file at the path referenced in `app.js`:
|
||||||
|
|
||||||
|
```
|
||||||
|
EG_AIP_DS_20260514_XML/EG_AIP_DS_FULL_20260514.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
Update `NATS_XML_PATH` in `app.js` if your filename differs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GeoJSON data sources
|
||||||
|
|
||||||
|
| Layer | Source |
|
||||||
|
|---|---|
|
||||||
|
| Danger / Restricted | [NATS UAS Zones](https://nats-uk.ead-it.com/cms-nats/opencms/en/uas-restriction-zones/) |
|
||||||
|
| Forestry England | [data-forestry.opendata.arcgis.com](https://data-forestry.opendata.arcgis.com/) |
|
||||||
|
| FLS Scotland | [ArcGIS Viewer](https://www.arcgis.com/apps/webappviewer/index.html?id=e4b9f5f437474e0481a3cec097e4c2ec) |
|
||||||
|
| NRW Wales | [datamap.gov.wales](https://datamap.gov.wales/) |
|
||||||
|
| SSSIs (Scotland/Wales) | [Natural England / Defra Open Data](https://naturalengland-defra.opendata.arcgis.com/) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
Map tiles © [MapTiler](https://maptiler.com) · Data © [OpenStreetMap contributors](https://www.openstreetmap.org/copyright)
|
||||||
|
A [Bournemouth Technology](https://bournemouthtechnology.co.uk) product
|
||||||
|
|||||||
89
app.js
89
app.js
@@ -532,11 +532,17 @@ function createLocationElement() {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setLocateLabel(text) {
|
||||||
|
const el = document.getElementById('locate-label');
|
||||||
|
if (el) el.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
function onLocationUpdate(pos) {
|
function onLocationUpdate(pos) {
|
||||||
const { longitude: lon, latitude: lat, accuracy } = pos.coords;
|
const { longitude: lon, latitude: lat, accuracy } = pos.coords;
|
||||||
const locateBtn = document.getElementById('locate-btn');
|
const btn = document.getElementById('locate-btn');
|
||||||
locateBtn.classList.remove('locating');
|
btn.classList.remove('locating');
|
||||||
locateBtn.classList.add('tracking');
|
btn.classList.add('tracking');
|
||||||
|
setLocateLabel('Stop Tracking');
|
||||||
|
|
||||||
if (locationMarker) {
|
if (locationMarker) {
|
||||||
locationMarker.setLngLat([lon, lat]);
|
locationMarker.setLngLat([lon, lat]);
|
||||||
@@ -562,12 +568,14 @@ function onLocationUpdate(pos) {
|
|||||||
|
|
||||||
function onLocationError(err) {
|
function onLocationError(err) {
|
||||||
stopTracking();
|
stopTracking();
|
||||||
alert(`Location error: ${err.message}`);
|
// Permission denied (code 1) — user chose not to share, no need to alert
|
||||||
|
if (err.code !== 1) setLocateLabel('Location error — click to retry');
|
||||||
}
|
}
|
||||||
|
|
||||||
function startTracking() {
|
function startTracking() {
|
||||||
if (!navigator.geolocation) { alert('Geolocation not supported by this browser.'); return; }
|
if (!navigator.geolocation) { setLocateLabel('Geolocation not supported'); return; }
|
||||||
document.getElementById('locate-btn').classList.add('locating');
|
document.getElementById('locate-btn').classList.add('locating');
|
||||||
|
setLocateLabel('Getting location…');
|
||||||
isFirstFix = true;
|
isFirstFix = true;
|
||||||
locationWatcher = navigator.geolocation.watchPosition(
|
locationWatcher = navigator.geolocation.watchPosition(
|
||||||
onLocationUpdate, onLocationError,
|
onLocationUpdate, onLocationError,
|
||||||
@@ -580,6 +588,7 @@ function stopTracking() {
|
|||||||
if (locationMarker) { locationMarker.remove(); locationMarker = null; }
|
if (locationMarker) { locationMarker.remove(); locationMarker = null; }
|
||||||
if (map && map.getSource('location-accuracy')) map.getSource('location-accuracy').setData(emptyFC());
|
if (map && map.getSource('location-accuracy')) map.getSource('location-accuracy').setData(emptyFC());
|
||||||
document.getElementById('locate-btn').classList.remove('locating', 'tracking');
|
document.getElementById('locate-btn').classList.remove('locating', 'tracking');
|
||||||
|
setLocateLabel('Track location');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Auto-load NATS XML from local path ───────────────────────────────────────
|
// ── Auto-load NATS XML from local path ───────────────────────────────────────
|
||||||
@@ -645,15 +654,26 @@ function initMap(key) {
|
|||||||
zoom: 5.5,
|
zoom: 5.5,
|
||||||
minZoom: 4,
|
minZoom: 4,
|
||||||
maxPitch: 85,
|
maxPitch: 85,
|
||||||
attributionControl: true,
|
attributionControl: false,
|
||||||
|
// Constrain panning to the UK + Channel Islands + a small buffer
|
||||||
|
maxBounds: [[-11, 47.5], [4, 62.5]],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
map.addControl(new maplibregl.AttributionControl({
|
||||||
|
customAttribution: '© <a href="https://bournemouthtechnology.co.uk" target="_blank" rel="noopener">Bournemouth Technology</a>',
|
||||||
|
}), 'bottom-right');
|
||||||
map.addControl(new maplibregl.NavigationControl({ showCompass: true }), 'bottom-right');
|
map.addControl(new maplibregl.NavigationControl({ showCompass: true }), 'bottom-right');
|
||||||
|
|
||||||
map.on('load', () => {
|
// style.load fires when the initial style is ready — add layers here.
|
||||||
addAllLayers();
|
// Style *switches* use idle (see switchStyle) because setStyle fires style.load
|
||||||
|
// prematurely on some MapTiler styles before the map is ready to accept sources.
|
||||||
|
map.once('style.load', addAllLayers);
|
||||||
|
|
||||||
|
// load fires once after the first full render.
|
||||||
|
map.once('load', () => {
|
||||||
document.getElementById('key-modal').classList.add('hidden');
|
document.getElementById('key-modal').classList.add('hidden');
|
||||||
loadNATSLocal();
|
loadNATSLocal();
|
||||||
|
startTracking();
|
||||||
});
|
});
|
||||||
|
|
||||||
map.on('moveend', () => {
|
map.on('moveend', () => {
|
||||||
@@ -682,8 +702,13 @@ function initMap(key) {
|
|||||||
function switchStyle(styleKey) {
|
function switchStyle(styleKey) {
|
||||||
if (styleKey === currentStyle) return;
|
if (styleKey === currentStyle) return;
|
||||||
currentStyle = styleKey;
|
currentStyle = styleKey;
|
||||||
|
// Cancel any previous pending idle handler (rapid clicks), then wait for
|
||||||
|
// the map to be truly idle after the style swap before re-adding layers.
|
||||||
|
// Using 'idle' rather than 'style.load' because MapTiler styles fire
|
||||||
|
// style.load before they're fully ready to accept addSource/addLayer calls.
|
||||||
|
map.off('idle', addAllLayers);
|
||||||
|
map.once('idle', addAllLayers);
|
||||||
map.setStyle(STYLES[styleKey](apiKey));
|
map.setStyle(STYLES[styleKey](apiKey));
|
||||||
map.once('style.load', () => addAllLayers());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API key modal ─────────────────────────────────────────────────────────────
|
// ── API key modal ─────────────────────────────────────────────────────────────
|
||||||
@@ -698,6 +723,10 @@ function wireControls() {
|
|||||||
document.getElementById('panel-body').classList.toggle('collapsed');
|
document.getElementById('panel-body').classList.toggle('collapsed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('warn-btn').addEventListener('click', () => {
|
||||||
|
document.getElementById('warn-popup').classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelectorAll('[data-layer]').forEach(cb => {
|
document.querySelectorAll('[data-layer]').forEach(cb => {
|
||||||
cb.addEventListener('change', () => {
|
cb.addEventListener('change', () => {
|
||||||
const id = cb.dataset.layer;
|
const id = cb.dataset.layer;
|
||||||
@@ -714,6 +743,48 @@ function wireControls() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('layers-select-all').addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('[data-layer]').forEach(cb => {
|
||||||
|
cb.checked = true;
|
||||||
|
const id = cb.dataset.layer;
|
||||||
|
layerVisibility[id] = true;
|
||||||
|
if (map) setLayerVisibility(id, true);
|
||||||
|
if (id === 'sssi') scheduleSSSIRefresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('layers-deselect-all').addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('[data-layer]').forEach(cb => {
|
||||||
|
cb.checked = false;
|
||||||
|
const id = cb.dataset.layer;
|
||||||
|
layerVisibility[id] = false;
|
||||||
|
if (map) setLayerVisibility(id, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Info buttons — click to show/hide description; each button specifies its box via data-infobox
|
||||||
|
let activeInfoBtn = null;
|
||||||
|
document.querySelectorAll('.info-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const box = document.getElementById(btn.dataset.infobox || 'nats-type-info');
|
||||||
|
if (activeInfoBtn === btn) {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
box.classList.add('hidden');
|
||||||
|
activeInfoBtn = null;
|
||||||
|
} else {
|
||||||
|
if (activeInfoBtn) {
|
||||||
|
const prevBox = document.getElementById(activeInfoBtn.dataset.infobox || 'nats-type-info');
|
||||||
|
prevBox.classList.add('hidden');
|
||||||
|
activeInfoBtn.classList.remove('active');
|
||||||
|
}
|
||||||
|
btn.classList.add('active');
|
||||||
|
box.textContent = btn.dataset.info;
|
||||||
|
box.classList.remove('hidden');
|
||||||
|
activeInfoBtn = btn;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.querySelectorAll('.style-btn').forEach(btn => {
|
document.querySelectorAll('.style-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.style-btn').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.style-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
|||||||
70
index.html
70
index.html
@@ -26,8 +26,16 @@
|
|||||||
<div id="panel">
|
<div id="panel">
|
||||||
<div id="panel-header">
|
<div id="panel-header">
|
||||||
<span class="panel-title">DroneMapUK</span>
|
<span class="panel-title">DroneMapUK</span>
|
||||||
|
<div class="header-btns">
|
||||||
|
<button id="warn-btn" title="Planning disclaimer">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
|
||||||
|
</button>
|
||||||
<button id="panel-toggle" title="Toggle panel">☰</button>
|
<button id="panel-toggle" title="Toggle panel">☰</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="warn-popup" class="hidden">
|
||||||
|
Planning aid only. Always check <a href="https://www.aurora.nats.co.uk/" target="_blank" rel="noopener">NOTAMs</a> and official CAA sources before any flight.
|
||||||
|
</div>
|
||||||
<div id="panel-body">
|
<div id="panel-body">
|
||||||
|
|
||||||
<!-- ── Airspace restrictions ── -->
|
<!-- ── Airspace restrictions ── -->
|
||||||
@@ -43,16 +51,15 @@
|
|||||||
<div id="nats-progress-fill" class="progress-fill"></div>
|
<div id="nats-progress-fill" class="progress-fill"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="nats-type-grid">
|
<div class="nats-type-grid">
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="R" checked><span class="tc-r">R</span></label>
|
<div class="type-cell"><label class="type-check"><input type="checkbox" data-nats-type="R" checked><span class="tc-r">R</span></label><button class="info-btn" data-info="Restricted Area — access limited by national authority"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button></div>
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="P" checked><span class="tc-r">P</span></label>
|
<div class="type-cell"><label class="type-check"><input type="checkbox" data-nats-type="P" checked><span class="tc-r">P</span></label><button class="info-btn" data-info="Prohibited Area — flight permanently prohibited"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button></div>
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="D" checked><span class="tc-d">D</span></label>
|
<div class="type-cell"><label class="type-check"><input type="checkbox" data-nats-type="CTR" checked><span class="tc-ctr">CTR</span></label><button class="info-btn" data-info="Control Zone — controlled airspace immediately around an aerodrome, surface to upper limit"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button></div>
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="D_OTHER" checked><span class="tc-d">D_OTH</span></label>
|
<div class="type-cell"><label class="type-check"><input type="checkbox" data-nats-type="CTA" checked><span class="tc-cta">CTA</span></label><button class="info-btn" data-info="Control Area — larger block of controlled airspace above a minimum altitude"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button></div>
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="CTR" checked><span class="tc-ctr">CTR</span></label>
|
<div class="type-cell"><label class="type-check"><input type="checkbox" data-nats-type="TMA" checked><span class="tc-cta">TMA</span></label><button class="info-btn" data-info="Terminal Manoeuvring Area — controlled airspace around a busy airport or cluster of airports"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button></div>
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="CTA" checked><span class="tc-cta">CTA</span></label>
|
<div class="type-cell"><label class="type-check"><input type="checkbox" data-nats-type="RAS" checked><span class="tc-ras">RAS</span></label><button class="info-btn" data-info="Radar Advisory Service — ATC provides traffic information and avoidance advice on request"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button></div>
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="TMA" checked><span class="tc-cta">TMA</span></label>
|
<div class="type-cell"><label class="type-check"><input type="checkbox" data-nats-type="OTHER:TMZ" checked><span class="tc-tmz">TMZ</span></label><button class="info-btn" data-info="Transponder Mandatory Zone — Mode C or S transponder required when flying inside"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button></div>
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="RAS" checked><span class="tc-ras">RAS</span></label>
|
|
||||||
<label class="type-check"><input type="checkbox" data-nats-type="OTHER:TMZ" checked><span class="tc-tmz">TMZ</span></label>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div id="nats-type-info" class="type-info-box hidden"></div>
|
||||||
|
|
||||||
<label class="layer-row">
|
<label class="layer-row">
|
||||||
<input type="checkbox" data-layer="frz" checked>
|
<input type="checkbox" data-layer="frz" checked>
|
||||||
@@ -61,18 +68,24 @@
|
|||||||
</label>
|
</label>
|
||||||
<p class="layer-note">5 km radius around licensed aerodromes (approx.)</p>
|
<p class="layer-note">5 km radius around licensed aerodromes (approx.)</p>
|
||||||
|
|
||||||
|
<div class="layer-row-outer">
|
||||||
<label class="layer-row">
|
<label class="layer-row">
|
||||||
<input type="checkbox" data-layer="danger" checked>
|
<input type="checkbox" data-layer="danger" checked>
|
||||||
<span class="dot dot-danger"></span>
|
<span class="dot dot-danger"></span>
|
||||||
<span>Danger / Restricted Areas</span>
|
<span>Danger / Restricted Areas</span>
|
||||||
</label>
|
</label>
|
||||||
|
<button class="info-btn" data-info="No built-in data — upload a GeoJSON file via Load Data below. Each upload replaces the previous data." data-infobox="layer-override-info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layer-row-outer">
|
||||||
<label class="layer-row">
|
<label class="layer-row">
|
||||||
<input type="checkbox" data-layer="military" checked>
|
<input type="checkbox" data-layer="military" checked>
|
||||||
<span class="dot dot-military"></span>
|
<span class="dot dot-military"></span>
|
||||||
<span>MoD / Military Areas</span>
|
<span>MoD / Military Areas</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="layer-note">Override: load GeoJSON below</p>
|
<button class="info-btn" data-info="No built-in data — upload a GeoJSON file via Load Data below. Each upload replaces the previous data." data-infobox="layer-override-info"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></button>
|
||||||
|
</div>
|
||||||
|
<div id="layer-override-info" class="type-info-box hidden"></div>
|
||||||
|
|
||||||
<!-- ── Protected nature ── -->
|
<!-- ── Protected nature ── -->
|
||||||
<div class="section-label" style="margin-top:12px">PROTECTED AREAS</div>
|
<div class="section-label" style="margin-top:12px">PROTECTED AREAS</div>
|
||||||
@@ -105,6 +118,20 @@
|
|||||||
<span>Forestry managed land</span>
|
<span>Forestry managed land</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="layer-note">Forestry England / FLS / NRW — load GeoJSON below</p>
|
<p class="layer-note">Forestry England / FLS / NRW — load GeoJSON below</p>
|
||||||
|
<div class="type-all-row">
|
||||||
|
<button class="type-all-btn" id="layers-select-all">Select all</button>
|
||||||
|
<button class="type-all-btn" id="layers-deselect-all">Deselect all</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Location ── -->
|
||||||
|
<div class="section-label" style="margin-top:12px">LOCATION</div>
|
||||||
|
<button id="locate-btn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
|
||||||
|
<circle cx="12" cy="12" r="9" stroke-dasharray="3 3" opacity=".4"/>
|
||||||
|
</svg>
|
||||||
|
<span id="locate-label">Track location</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- ── Map style ── -->
|
<!-- ── Map style ── -->
|
||||||
<div class="section-label" style="margin-top:12px">MAP STYLE</div>
|
<div class="section-label" style="margin-top:12px">MAP STYLE</div>
|
||||||
@@ -132,40 +159,25 @@
|
|||||||
|
|
||||||
<!-- ── Load data ── -->
|
<!-- ── Load data ── -->
|
||||||
<div class="section-label" style="margin-top:12px">LOAD DATA</div>
|
<div class="section-label" style="margin-top:12px">LOAD DATA</div>
|
||||||
|
|
||||||
<p class="layer-note" style="margin-top:6px">Other GeoJSON layers:</p>
|
|
||||||
<div class="load-row">
|
|
||||||
<select id="layer-select" class="layer-select">
|
<select id="layer-select" class="layer-select">
|
||||||
<option value="danger">Danger / Restricted (override)</option>
|
<option value="danger">Danger / Restricted</option>
|
||||||
<option value="military">Military Areas (override)</option>
|
<option value="military">Military Areas</option>
|
||||||
<option value="forestry">Forestry managed land</option>
|
<option value="forestry">Forestry managed land</option>
|
||||||
<option value="sssi">SSSIs (Scotland / Wales)</option>
|
<option value="sssi">SSSIs (Scotland / Wales)</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="load-btn" id="geojson-btn">Load GeoJSON…</button>
|
<button class="load-btn load-btn-full" id="geojson-btn">Load GeoJSON…</button>
|
||||||
</div>
|
|
||||||
<input type="file" id="geojson-file" accept=".geojson,.json" style="display:none">
|
<input type="file" id="geojson-file" accept=".geojson,.json" style="display:none">
|
||||||
|
|
||||||
<div class="data-links">
|
<div class="data-links">
|
||||||
|
<a href="https://www.aurora.nats.co.uk/" target="_blank" rel="noopener">NOTAMs</a> ·
|
||||||
<a href="https://nats-uk.ead-it.com/cms-nats/opencms/en/uas-restriction-zones/" target="_blank" rel="noopener">NATS UAS Zones</a> ·
|
<a href="https://nats-uk.ead-it.com/cms-nats/opencms/en/uas-restriction-zones/" target="_blank" rel="noopener">NATS UAS Zones</a> ·
|
||||||
<a href="https://data-forestry.opendata.arcgis.com/" target="_blank" rel="noopener">Forestry England</a> ·
|
<a href="https://data-forestry.opendata.arcgis.com/" target="_blank" rel="noopener">Forestry England</a> ·
|
||||||
<a href="https://www.arcgis.com/apps/webappviewer/index.html?id=e4b9f5f437474e0481a3cec097e4c2ec" target="_blank" rel="noopener">FLS Scotland</a> ·
|
<a href="https://www.arcgis.com/apps/webappviewer/index.html?id=e4b9f5f437474e0481a3cec097e4c2ec" target="_blank" rel="noopener">FLS Scotland</a> ·
|
||||||
<a href="https://datamap.gov.wales/" target="_blank" rel="noopener">NRW Wales</a> ·
|
<a href="https://datamap.gov.wales/" target="_blank" rel="noopener">NRW Wales</a> ·
|
||||||
<a href="https://naturalengland-defra.opendata.arcgis.com/" target="_blank" rel="noopener">Natural England</a>
|
<a href="https://naturalengland-defra.opendata.arcgis.com/" target="_blank" rel="noopener">Natural England</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="disclaimer">
|
|
||||||
⚠ Planning aid only. Always check NOTAMs and official CAA sources before any flight.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Locate button -->
|
|
||||||
<button id="locate-btn" title="Track my location">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
|
|
||||||
<circle cx="12" cy="12" r="9" stroke-dasharray="3 3" opacity=".4"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Click info popup -->
|
<!-- Click info popup -->
|
||||||
<div id="popup" class="hidden">
|
<div id="popup" class="hidden">
|
||||||
|
|||||||
108
style.css
108
style.css
@@ -44,12 +44,28 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
padding: 10px 14px; border-bottom: 1px solid #2a2d38;
|
padding: 10px 14px; border-bottom: 1px solid #2a2d38;
|
||||||
}
|
}
|
||||||
.panel-title { font-weight: 700; font-size: .95rem; letter-spacing: .5px; }
|
.panel-title { font-weight: 700; font-size: .95rem; letter-spacing: .5px; }
|
||||||
|
.header-btns { display: flex; align-items: center; gap: 2px; }
|
||||||
|
|
||||||
|
#warn-btn {
|
||||||
|
background: none; border: none; color: #f59e0b; cursor: pointer;
|
||||||
|
padding: 2px 5px; border-radius: 4px; line-height: 1;
|
||||||
|
display: flex; align-items: center;
|
||||||
|
}
|
||||||
|
#warn-btn:hover { background: #ffffff15; }
|
||||||
|
#warn-btn svg { width: 15px; height: 15px; }
|
||||||
|
|
||||||
#panel-toggle {
|
#panel-toggle {
|
||||||
background: none; border: none; color: #aaa; font-size: 1.1rem;
|
background: none; border: none; color: #aaa; font-size: 1.1rem;
|
||||||
cursor: pointer; line-height: 1; padding: 2px 4px; border-radius: 4px;
|
cursor: pointer; line-height: 1; padding: 2px 4px; border-radius: 4px;
|
||||||
}
|
}
|
||||||
#panel-toggle:hover { background: #ffffff15; color: #fff; }
|
#panel-toggle:hover { background: #ffffff15; color: #fff; }
|
||||||
|
|
||||||
|
#warn-popup {
|
||||||
|
padding: 8px 14px; border-bottom: 1px solid #5a4200;
|
||||||
|
background: #2a2206; font-size: .7rem; color: #c9a84c; line-height: 1.5;
|
||||||
|
}
|
||||||
|
#warn-popup.hidden { display: none; }
|
||||||
|
|
||||||
#panel-body {
|
#panel-body {
|
||||||
padding: 12px 14px 14px;
|
padding: 12px 14px 14px;
|
||||||
max-height: calc(100dvh - 90px);
|
max-height: calc(100dvh - 90px);
|
||||||
@@ -59,7 +75,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
font-size: .62rem; font-weight: 700; letter-spacing: 1.3px;
|
font-size: .62rem; font-weight: 700; letter-spacing: 1.3px;
|
||||||
color: #555; margin: 8px 0 5px; text-transform: uppercase;
|
color: #888; margin: 8px 0 5px; text-transform: uppercase;
|
||||||
border-top: 1px solid #2a2d38; padding-top: 8px;
|
border-top: 1px solid #2a2d38; padding-top: 8px;
|
||||||
}
|
}
|
||||||
.section-label:first-child { border-top: none; padding-top: 0; margin-top: 0; }
|
.section-label:first-child { border-top: none; padding-top: 0; margin-top: 0; }
|
||||||
@@ -83,8 +99,13 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
.dot-sssi { background: #06b6d4; }
|
.dot-sssi { background: #06b6d4; }
|
||||||
.dot-forestry { background: #4d7c0f; }
|
.dot-forestry { background: #4d7c0f; }
|
||||||
|
|
||||||
|
.layer-row-outer {
|
||||||
|
display: flex; align-items: center; gap: 4px;
|
||||||
|
}
|
||||||
|
.layer-row-outer .layer-row { flex: 1; }
|
||||||
|
|
||||||
.layer-note {
|
.layer-note {
|
||||||
font-size: .7rem; color: #555; margin: 1px 0 4px 23px; line-height: 1.4;
|
font-size: .7rem; color: #888; margin: 1px 0 4px 23px; line-height: 1.4;
|
||||||
}
|
}
|
||||||
.layer-note a { color: #5aacff; text-decoration: none; }
|
.layer-note a { color: #5aacff; text-decoration: none; }
|
||||||
.layer-note a:hover { text-decoration: underline; }
|
.layer-note a:hover { text-decoration: underline; }
|
||||||
@@ -104,7 +125,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
display: flex; align-items: center; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
margin-top: 7px; padding: 2px 1px; font-size: .82rem; color: #ccc;
|
margin-top: 7px; padding: 2px 1px; font-size: .82rem; color: #ccc;
|
||||||
}
|
}
|
||||||
.hint { font-size: .68rem; color: #555; }
|
.hint { font-size: .68rem; color: #888; }
|
||||||
|
|
||||||
.switch { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; }
|
.switch { position: relative; display: inline-block; width: 36px; height: 20px; flex-shrink: 0; }
|
||||||
.switch input { opacity: 0; width: 0; height: 0; }
|
.switch input { opacity: 0; width: 0; height: 0; }
|
||||||
@@ -122,21 +143,21 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
.switch input:checked ~ .switch-track .switch-thumb { transform: translateX(16px); background: #fff; }
|
.switch input:checked ~ .switch-track .switch-thumb { transform: translateX(16px); background: #fff; }
|
||||||
|
|
||||||
/* ── Load data ── */
|
/* ── Load data ── */
|
||||||
.load-row { display: flex; gap: 6px; margin-top: 4px; align-items: center; }
|
|
||||||
.layer-select {
|
.layer-select {
|
||||||
flex: 1; padding: 6px 8px; border-radius: 6px; border: 1px solid #333;
|
width: 100%; margin-top: 5px; padding: 7px 8px; border-radius: 6px;
|
||||||
background: #2a2d38; color: #ccc; font-size: .75rem; cursor: pointer;
|
border: 1px solid #333; background: #2a2d38; color: #ccc;
|
||||||
appearance: none; outline: none;
|
font-size: .75rem; cursor: pointer; appearance: none; outline: none;
|
||||||
}
|
}
|
||||||
.layer-select:focus { border-color: #555; }
|
.layer-select:focus { border-color: #555; }
|
||||||
|
|
||||||
.load-btn {
|
.load-btn {
|
||||||
flex-shrink: 0; padding: 6px 10px; border-radius: 6px;
|
padding: 7px 10px; border-radius: 6px;
|
||||||
border: 1px solid #333; background: #2a2d38;
|
border: 1px solid #333; background: #2a2d38;
|
||||||
color: #ccc; font-size: .75rem; cursor: pointer; white-space: nowrap;
|
color: #ccc; font-size: .75rem; cursor: pointer; white-space: nowrap;
|
||||||
transition: background .15s;
|
transition: background .15s;
|
||||||
}
|
}
|
||||||
.load-btn:hover { background: #34384a; color: #fff; }
|
.load-btn:hover { background: #34384a; color: #fff; }
|
||||||
|
.load-btn-full { display: block; width: 100%; margin-top: 5px; text-align: center; }
|
||||||
|
|
||||||
/* ── Progress bar ── */
|
/* ── Progress bar ── */
|
||||||
.progress-bar {
|
.progress-bar {
|
||||||
@@ -153,45 +174,67 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
/* ── NATS type filter grid ── */
|
/* ── NATS type filter grid ── */
|
||||||
.nats-type-grid {
|
.nats-type-grid {
|
||||||
display: grid; grid-template-columns: repeat(3, 1fr);
|
display: grid; grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 2px 4px; margin: 3px 0 5px 22px;
|
gap: 2px 4px; margin: 3px 0 3px 22px;
|
||||||
}
|
}
|
||||||
|
.type-cell { display: flex; align-items: center; gap: 2px; }
|
||||||
.type-check {
|
.type-check {
|
||||||
display: flex; align-items: center; gap: 3px;
|
display: flex; align-items: center; gap: 3px; flex: 1;
|
||||||
font-size: .7rem; cursor: pointer; user-select: none; color: #999;
|
font-size: .7rem; cursor: pointer; user-select: none; color: #999;
|
||||||
}
|
}
|
||||||
.type-check input[type=checkbox] { width: 11px; height: 11px; cursor: pointer; flex-shrink: 0; }
|
.type-check input[type=checkbox] { width: 11px; height: 11px; cursor: pointer; flex-shrink: 0; }
|
||||||
.tc-r { color: #ef4444; font-weight: 600; }
|
.tc-r { color: #ef4444; font-weight: 600; }
|
||||||
.tc-d { color: #f97316; font-weight: 600; }
|
|
||||||
.tc-ctr { color: #facc15; font-weight: 600; }
|
.tc-ctr { color: #facc15; font-weight: 600; }
|
||||||
.tc-cta { color: #60a5fa; font-weight: 600; }
|
.tc-cta { color: #60a5fa; font-weight: 600; }
|
||||||
.tc-ras { color: #818cf8; font-weight: 600; }
|
.tc-ras { color: #818cf8; font-weight: 600; }
|
||||||
.tc-tmz { color: #c084fc; font-weight: 600; }
|
.tc-tmz { color: #c084fc; font-weight: 600; }
|
||||||
|
|
||||||
|
.info-btn {
|
||||||
|
width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0;
|
||||||
|
border: none; background: none;
|
||||||
|
color: #888; cursor: pointer; padding: 0;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: color .15s;
|
||||||
|
}
|
||||||
|
.info-btn:hover { color: #5aacff; }
|
||||||
|
.info-btn.active { color: #5aacff; }
|
||||||
|
.info-btn svg { width: 13px; height: 13px; }
|
||||||
|
|
||||||
|
.type-all-row {
|
||||||
|
display: flex; gap: 6px; margin: 4px 0 2px 22px;
|
||||||
|
}
|
||||||
|
.type-all-btn {
|
||||||
|
flex: 1; padding: 4px 6px; border-radius: 5px;
|
||||||
|
border: 1px solid #333; background: #2a2d38;
|
||||||
|
color: #bbb; font-size: .68rem; cursor: pointer;
|
||||||
|
transition: background .15s, color .15s;
|
||||||
|
}
|
||||||
|
.type-all-btn:hover { background: #34384a; color: #fff; }
|
||||||
|
|
||||||
|
.type-info-box {
|
||||||
|
margin: 4px 0 4px 22px; padding: 6px 8px; border-radius: 5px;
|
||||||
|
background: #1a1d24; border: 1px solid #2a2d38;
|
||||||
|
font-size: .7rem; color: #aaa; line-height: 1.45;
|
||||||
|
}
|
||||||
|
.type-info-box.hidden { display: none; }
|
||||||
|
|
||||||
.data-links {
|
.data-links {
|
||||||
margin-top: 7px; font-size: .68rem; color: #555; line-height: 1.9;
|
margin-top: 7px; font-size: .68rem; color: #888; line-height: 1.9;
|
||||||
}
|
}
|
||||||
.data-links a { color: #5aacff; text-decoration: none; }
|
.data-links a { color: #5aacff; text-decoration: none; }
|
||||||
.data-links a:hover { text-decoration: underline; }
|
.data-links a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
.disclaimer {
|
/* ── Locate button (inside panel) ── */
|
||||||
margin-top: 12px; padding: 9px 10px; border-radius: 6px;
|
|
||||||
background: #2a2206; border: 1px solid #5a4200;
|
|
||||||
font-size: .7rem; color: #c9a84c; line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Locate button ── */
|
|
||||||
#locate-btn {
|
#locate-btn {
|
||||||
position: absolute; bottom: 30px; right: 12px; z-index: 100;
|
display: flex; align-items: center; gap: 8px;
|
||||||
width: 44px; height: 44px; border-radius: 50%;
|
width: 100%; margin-top: 4px; padding: 8px 8px; border-radius: 6px;
|
||||||
border: 1px solid #333; background: #1e2027cc; backdrop-filter: blur(10px);
|
border: 1px solid #333; background: #2a2d38;
|
||||||
color: #ccc; cursor: pointer; display: flex; align-items: center; justify-content: center;
|
color: #ccc; font-size: .83rem; cursor: pointer; text-align: left;
|
||||||
transition: all .15s;
|
transition: background .15s, color .15s, border-color .15s;
|
||||||
}
|
}
|
||||||
#locate-btn:hover { background: #2a2d38cc; color: #fff; }
|
#locate-btn:hover { background: #34384a; color: #fff; }
|
||||||
#locate-btn.locating { color: #2563eb; animation: btn-pulse 1s infinite; }
|
#locate-btn.locating { color: #2563eb; border-color: #2563eb44; animation: btn-pulse 1s infinite; }
|
||||||
#locate-btn.tracking { color: #2563eb; border-color: #2563eb44; background: #1a2f5acc; }
|
#locate-btn.tracking { color: #2563eb; border-color: #2563eb55; background: #1a2f5a88; }
|
||||||
#locate-btn.tracking:hover { background: #1d3560cc; }
|
#locate-btn svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||||||
#locate-btn svg { width: 20px; height: 20px; }
|
|
||||||
|
|
||||||
@keyframes btn-pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
|
@keyframes btn-pulse { 0%,100% { opacity: 1; } 50% { opacity: .4; } }
|
||||||
|
|
||||||
@@ -215,7 +258,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
|
|
||||||
/* ── Popup ── */
|
/* ── Popup ── */
|
||||||
#popup {
|
#popup {
|
||||||
position: absolute; bottom: 84px; right: 12px; z-index: 100;
|
position: absolute; bottom: 30px; right: 12px; z-index: 100;
|
||||||
width: min(280px, calc(100vw - 24px));
|
width: min(280px, calc(100vw - 24px));
|
||||||
background: #1e2027ee; backdrop-filter: blur(10px);
|
background: #1e2027ee; backdrop-filter: blur(10px);
|
||||||
border: 1px solid #333; border-radius: 10px; padding: 14px 16px;
|
border: 1px solid #333; border-radius: 10px; padding: 14px 16px;
|
||||||
@@ -223,7 +266,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
#popup.hidden { display: none; }
|
#popup.hidden { display: none; }
|
||||||
#popup-close {
|
#popup-close {
|
||||||
position: absolute; top: 10px; right: 12px;
|
position: absolute; top: 10px; right: 12px;
|
||||||
background: none; border: none; color: #666; font-size: .85rem;
|
background: none; border: none; color: #999; font-size: .85rem;
|
||||||
cursor: pointer; line-height: 1;
|
cursor: pointer; line-height: 1;
|
||||||
}
|
}
|
||||||
#popup-close:hover { color: #fff; }
|
#popup-close:hover { color: #fff; }
|
||||||
@@ -255,6 +298,5 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; b
|
|||||||
/* ── Mobile ── */
|
/* ── Mobile ── */
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
#panel { width: calc(100vw - 24px); top: 8px; left: 8px; right: 8px; }
|
#panel { width: calc(100vw - 24px); top: 8px; left: 8px; right: 8px; }
|
||||||
#locate-btn { bottom: 20px; right: 8px; }
|
#popup { right: 8px; bottom: 20px; width: calc(100vw - 16px); }
|
||||||
#popup { right: 8px; bottom: 74px; width: calc(100vw - 16px); }
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user