diff --git a/index.html b/index.html
index e8b0b8d..f8cb50c 100644
--- a/index.html
+++ b/index.html
@@ -562,6 +562,9 @@
+
diff --git a/src/browserGeolocation.js b/src/browserGeolocation.js
new file mode 100644
index 0000000..a4a0f03
--- /dev/null
+++ b/src/browserGeolocation.js
@@ -0,0 +1,251 @@
+/**
+ * Browser Geolocation helpers for one-shot fly-to and optional live follow.
+ * Uses navigator.geolocation only — no Google / Cesium ion keys required.
+ */
+
+export const GEOLOCATION_UNAVAILABLE = 'unavailable';
+export const GEOLOCATION_DENIED = 'denied';
+export const GEOLOCATION_TIMEOUT = 'timeout';
+export const GEOLOCATION_POSITION_UNAVAILABLE = 'position-unavailable';
+export const GEOLOCATION_UNKNOWN = 'unknown';
+
+const DEFAULT_POSITION_OPTIONS = Object.freeze({
+ enableHighAccuracy: true,
+ timeout: 15000,
+ maximumAge: 10000,
+});
+
+/**
+ * @returns {boolean}
+ */
+export function isGeolocationAvailable(geo = globalThis.navigator?.geolocation) {
+ return Boolean(geo && typeof geo.getCurrentPosition === 'function');
+}
+
+/**
+ * Map a GeolocationPositionError (or synthetic) to a stable code.
+ * @param {GeolocationPositionError|{code?:number,message?:string}|null} err
+ * @returns {string}
+ */
+export function classifyGeolocationError(err) {
+ if (!err) return GEOLOCATION_UNKNOWN;
+ const code = Number(err.code);
+ if (code === 1) return GEOLOCATION_DENIED;
+ if (code === 2) return GEOLOCATION_POSITION_UNAVAILABLE;
+ if (code === 3) return GEOLOCATION_TIMEOUT;
+ const message = String(err.message || '').toLowerCase();
+ if (message.includes('denied') || message.includes('permission')) return GEOLOCATION_DENIED;
+ if (message.includes('timeout')) return GEOLOCATION_TIMEOUT;
+ if (message.includes('unavailable')) return GEOLOCATION_POSITION_UNAVAILABLE;
+ return GEOLOCATION_UNKNOWN;
+}
+
+/**
+ * User-facing toast text for a classified error.
+ * @param {string} code
+ * @returns {string}
+ */
+export function geolocationErrorMessage(code) {
+ switch (code) {
+ case GEOLOCATION_UNAVAILABLE:
+ return 'Location not supported in this browser';
+ case GEOLOCATION_DENIED:
+ return 'Location permission denied';
+ case GEOLOCATION_TIMEOUT:
+ return 'Location request timed out';
+ case GEOLOCATION_POSITION_UNAVAILABLE:
+ return 'Location unavailable';
+ default:
+ return 'Could not get your location';
+ }
+}
+
+/**
+ * Normalize a GeolocationPosition into { lat, lon, accuracy, heading, speed, timestamp }.
+ * @param {GeolocationPosition} position
+ * @returns {{lat:number,lon:number,accuracy:number|null,heading:number|null,speed:number|null,timestamp:number}}
+ */
+export function normalizeGeolocationPosition(position) {
+ const coords = position?.coords;
+ if (!coords || !Number.isFinite(coords.latitude) || !Number.isFinite(coords.longitude)) {
+ throw new Error('Invalid geolocation position');
+ }
+ return {
+ lat: coords.latitude,
+ lon: coords.longitude,
+ accuracy: Number.isFinite(coords.accuracy) ? coords.accuracy : null,
+ heading: Number.isFinite(coords.heading) ? coords.heading : null,
+ speed: Number.isFinite(coords.speed) ? coords.speed : null,
+ timestamp: Number.isFinite(position.timestamp) ? position.timestamp : Date.now(),
+ };
+}
+
+/**
+ * One-shot current position.
+ * @param {object} [options]
+ * @param {Geolocation} [options.geo]
+ * @param {PositionOptions} [options.positionOptions]
+ * @returns {Promise<{lat:number,lon:number,accuracy:number|null,heading:number|null,speed:number|null,timestamp:number}>}
+ */
+export function getCurrentBrowserPosition({
+ geo = globalThis.navigator?.geolocation,
+ positionOptions = DEFAULT_POSITION_OPTIONS,
+} = {}) {
+ if (!isGeolocationAvailable(geo)) {
+ const err = new Error(geolocationErrorMessage(GEOLOCATION_UNAVAILABLE));
+ err.code = GEOLOCATION_UNAVAILABLE;
+ return Promise.reject(err);
+ }
+ return new Promise((resolve, reject) => {
+ geo.getCurrentPosition(
+ (position) => {
+ try {
+ resolve(normalizeGeolocationPosition(position));
+ } catch (err) {
+ const wrapped = new Error(geolocationErrorMessage(GEOLOCATION_UNKNOWN));
+ wrapped.code = GEOLOCATION_UNKNOWN;
+ wrapped.cause = err;
+ reject(wrapped);
+ }
+ },
+ (error) => {
+ const code = classifyGeolocationError(error);
+ const err = new Error(geolocationErrorMessage(code));
+ err.code = code;
+ err.cause = error;
+ reject(err);
+ },
+ positionOptions,
+ );
+ });
+}
+
+/**
+ * Live watch. Caller must stop() when done.
+ * @param {object} options
+ * @param {(fix:{lat:number,lon:number,accuracy:number|null,heading:number|null,speed:number|null,timestamp:number}) => void} options.onUpdate
+ * @param {(err:Error) => void} [options.onError]
+ * @param {Geolocation} [options.geo]
+ * @param {PositionOptions} [options.positionOptions]
+ * @returns {{stop:() => void, watchId:number|null}}
+ */
+export function watchBrowserPosition({
+ onUpdate,
+ onError = null,
+ geo = globalThis.navigator?.geolocation,
+ positionOptions = DEFAULT_POSITION_OPTIONS,
+} = {}) {
+ if (typeof onUpdate !== 'function') {
+ throw new Error('watchBrowserPosition requires onUpdate');
+ }
+ if (!isGeolocationAvailable(geo) || typeof geo.watchPosition !== 'function') {
+ const err = new Error(geolocationErrorMessage(GEOLOCATION_UNAVAILABLE));
+ err.code = GEOLOCATION_UNAVAILABLE;
+ onError?.(err);
+ return { stop() {}, watchId: null };
+ }
+
+ let stopped = false;
+ const watchId = geo.watchPosition(
+ (position) => {
+ if (stopped) return;
+ try {
+ onUpdate(normalizeGeolocationPosition(position));
+ } catch (err) {
+ const wrapped = new Error(geolocationErrorMessage(GEOLOCATION_UNKNOWN));
+ wrapped.code = GEOLOCATION_UNKNOWN;
+ wrapped.cause = err;
+ onError?.(wrapped);
+ }
+ },
+ (error) => {
+ if (stopped) return;
+ const code = classifyGeolocationError(error);
+ const err = new Error(geolocationErrorMessage(code));
+ err.code = code;
+ err.cause = error;
+ onError?.(err);
+ },
+ positionOptions,
+ );
+
+ return {
+ watchId,
+ stop() {
+ if (stopped) return;
+ stopped = true;
+ try { geo.clearWatch?.(watchId); } catch { /* no-op */ }
+ },
+ };
+}
+
+/**
+ * Detect click vs long-press for the My Location control.
+ * Short release (< holdMs) → 'click'. Held ≥ holdMs → 'hold' (fires once while pressed).
+ * @param {object} options
+ * @param {number} [options.holdMs=550]
+ * @param {() => void} options.onClick
+ * @param {() => void} options.onHold
+ * @returns {{attach:(el:HTMLElement)=>() => void}}
+ */
+export function createPressGesture({ holdMs = 550, onClick, onHold } = {}) {
+ if (typeof onClick !== 'function' || typeof onHold !== 'function') {
+ throw new Error('createPressGesture requires onClick and onHold');
+ }
+ return {
+ attach(el) {
+ if (!el?.addEventListener) return () => {};
+ let timer = null;
+ let holdFired = false;
+ let pointerId = null;
+
+ const clear = () => {
+ if (timer != null) {
+ clearTimeout(timer);
+ timer = null;
+ }
+ };
+
+ const onDown = (event) => {
+ if (event.button != null && event.button !== 0) return;
+ holdFired = false;
+ pointerId = event.pointerId ?? 'mouse';
+ clear();
+ timer = setTimeout(() => {
+ timer = null;
+ holdFired = true;
+ onHold();
+ }, holdMs);
+ };
+
+ const onUp = (event) => {
+ if (pointerId != null && event.pointerId != null && event.pointerId !== pointerId) return;
+ const wasHold = holdFired;
+ clear();
+ pointerId = null;
+ if (!wasHold) onClick();
+ holdFired = false;
+ };
+
+ const onCancel = () => {
+ clear();
+ pointerId = null;
+ holdFired = false;
+ };
+
+ el.addEventListener('pointerdown', onDown);
+ el.addEventListener('pointerup', onUp);
+ el.addEventListener('pointerleave', onCancel);
+ el.addEventListener('pointercancel', onCancel);
+ el.addEventListener('contextmenu', (e) => e.preventDefault());
+
+ return () => {
+ clear();
+ el.removeEventListener('pointerdown', onDown);
+ el.removeEventListener('pointerup', onUp);
+ el.removeEventListener('pointerleave', onCancel);
+ el.removeEventListener('pointercancel', onCancel);
+ };
+ },
+ };
+}
diff --git a/src/browserGeolocation.test.mjs b/src/browserGeolocation.test.mjs
new file mode 100644
index 0000000..b6efb24
--- /dev/null
+++ b/src/browserGeolocation.test.mjs
@@ -0,0 +1,145 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+ GEOLOCATION_DENIED,
+ GEOLOCATION_TIMEOUT,
+ GEOLOCATION_UNAVAILABLE,
+ classifyGeolocationError,
+ createPressGesture,
+ geolocationErrorMessage,
+ getCurrentBrowserPosition,
+ isGeolocationAvailable,
+ normalizeGeolocationPosition,
+ watchBrowserPosition,
+} from './browserGeolocation.js';
+
+test('isGeolocationAvailable requires getCurrentPosition', () => {
+ assert.equal(isGeolocationAvailable(null), false);
+ assert.equal(isGeolocationAvailable({}), false);
+ assert.equal(isGeolocationAvailable({ getCurrentPosition() {} }), true);
+});
+
+test('classifyGeolocationError maps standard codes', () => {
+ assert.equal(classifyGeolocationError({ code: 1 }), GEOLOCATION_DENIED);
+ assert.equal(classifyGeolocationError({ code: 3 }), GEOLOCATION_TIMEOUT);
+ assert.equal(classifyGeolocationError({ message: 'User denied Geolocation' }), GEOLOCATION_DENIED);
+});
+
+test('normalizeGeolocationPosition extracts lat/lon', () => {
+ const fix = normalizeGeolocationPosition({
+ timestamp: 1000,
+ coords: {
+ latitude: 30.2672,
+ longitude: -97.7431,
+ accuracy: 12,
+ heading: null,
+ speed: null,
+ },
+ });
+ assert.equal(fix.lat, 30.2672);
+ assert.equal(fix.lon, -97.7431);
+ assert.equal(fix.accuracy, 12);
+});
+
+test('getCurrentBrowserPosition resolves a fix', async () => {
+ const geo = {
+ getCurrentPosition(success) {
+ success({
+ timestamp: 42,
+ coords: { latitude: 1, longitude: 2, accuracy: 5, heading: NaN, speed: NaN },
+ });
+ },
+ };
+ const fix = await getCurrentBrowserPosition({ geo });
+ assert.deepEqual(fix, {
+ lat: 1,
+ lon: 2,
+ accuracy: 5,
+ heading: null,
+ speed: null,
+ timestamp: 42,
+ });
+});
+
+test('getCurrentBrowserPosition rejects when unavailable', async () => {
+ await assert.rejects(
+ () => getCurrentBrowserPosition({ geo: null }),
+ (err) => {
+ assert.equal(err.code, GEOLOCATION_UNAVAILABLE);
+ assert.equal(err.message, geolocationErrorMessage(GEOLOCATION_UNAVAILABLE));
+ return true;
+ },
+ );
+});
+
+test('getCurrentBrowserPosition rejects permission denied', async () => {
+ const geo = {
+ getCurrentPosition(_success, error) {
+ error({ code: 1, message: 'denied' });
+ },
+ };
+ await assert.rejects(
+ () => getCurrentBrowserPosition({ geo }),
+ (err) => err.code === GEOLOCATION_DENIED,
+ );
+});
+
+test('watchBrowserPosition streams updates and stop clears watch', () => {
+ const updates = [];
+ let cleared = null;
+ const geo = {
+ getCurrentPosition() {},
+ watchPosition(success) {
+ success({
+ timestamp: 1,
+ coords: { latitude: 10, longitude: 20, accuracy: 3, heading: 90, speed: 1 },
+ });
+ return 77;
+ },
+ clearWatch(id) { cleared = id; },
+ };
+ const handle = watchBrowserPosition({
+ geo,
+ onUpdate: (fix) => updates.push(fix),
+ });
+ assert.equal(handle.watchId, 77);
+ assert.equal(updates.length, 1);
+ assert.equal(updates[0].lat, 10);
+ handle.stop();
+ assert.equal(cleared, 77);
+ handle.stop(); // idempotent
+ assert.equal(cleared, 77);
+});
+
+test('createPressGesture fires click on short press and hold on long press', async () => {
+ const clicks = [];
+ const holds = [];
+ const listeners = new Map();
+ const el = {
+ addEventListener(type, fn) {
+ listeners.set(type, fn);
+ },
+ removeEventListener(type) {
+ listeners.delete(type);
+ },
+ };
+ const gesture = createPressGesture({
+ holdMs: 40,
+ onClick: () => clicks.push('click'),
+ onHold: () => holds.push('hold'),
+ });
+ const detach = gesture.attach(el);
+
+ listeners.get('pointerdown')({ button: 0, pointerId: 1 });
+ listeners.get('pointerup')({ pointerId: 1 });
+ assert.deepEqual(clicks, ['click']);
+ assert.deepEqual(holds, []);
+
+ listeners.get('pointerdown')({ button: 0, pointerId: 2 });
+ await new Promise((r) => setTimeout(r, 60));
+ assert.deepEqual(holds, ['hold']);
+ listeners.get('pointerup')({ pointerId: 2 });
+ assert.deepEqual(clicks, ['click']); // no second click after hold
+
+ detach();
+});
diff --git a/src/ui.js b/src/ui.js
index 668e752..251dc09 100644
--- a/src/ui.js
+++ b/src/ui.js
@@ -12,7 +12,13 @@ import {
clampBloomIntensity,
decodeBloomIntensity,
} from './bloom.js';
-import { LOCATIONS, CITY_POIS, GLOBE_VIEW, flyToGlobeView, flyToPresetLocation, flyToPOI, searchAndFlyTo } from './locations.js';
+import { LOCATIONS, CITY_POIS, GLOBE_VIEW, flyToGlobeView, flyToLandmark, flyToPresetLocation, flyToPOI, searchAndFlyTo } from './locations.js';
+import {
+ createPressGesture,
+ getCurrentBrowserPosition,
+ isGeolocationAvailable,
+ watchBrowserPosition,
+} from './browserGeolocation.js';
import { locationMiniStatus } from './locationStatus.js';
import { interruptCameraMotion } from './cameraVerbs.js';
import {
@@ -2368,6 +2374,12 @@ export class StyleManager {
this._toast = document.getElementById('toast');
this._locationSearch = document.getElementById('location-search');
this._searchToggle = document.getElementById('search-toggle');
+ this._myLocationBtn = document.getElementById('my-location-btn');
+ this._myLocationFollow = false;
+ this._myLocationWatch = null;
+ this._myLocationEntity = null;
+ this._myLocationDetachGesture = null;
+ this._myLocationUserInterruptHandler = null;
this._locationPills = document.getElementById('location-pills');
this._poiRow = document.getElementById('poi-row');
this._locationBarDivider = document.getElementById('location-bar-divider');
@@ -9342,6 +9354,207 @@ export class StyleManager {
}
}
});
+
+ this._initMyLocationControl();
+ }
+
+ /**
+ * Wire the My Location control: click = one-shot fly-to GPS, hold = toggle follow.
+ * @returns {void}
+ */
+ _initMyLocationControl() {
+ if (!this._myLocationBtn) return;
+ if (!isGeolocationAvailable()) {
+ this._myLocationBtn.disabled = true;
+ this._myLocationBtn.title = 'Location not supported in this browser';
+ return;
+ }
+
+ const gesture = createPressGesture({
+ holdMs: 550,
+ onClick: () => {
+ void this._goToMyLocation({ follow: false });
+ },
+ onHold: () => {
+ if (this._myLocationFollow) this._stopMyLocationFollow({ toast: 'Stopped following location' });
+ else void this._goToMyLocation({ follow: true });
+ },
+ });
+ this._myLocationDetachGesture = gesture.attach(this._myLocationBtn);
+ }
+
+ /**
+ * Request browser GPS and fly (and optionally follow) the camera there.
+ * @param {{follow?: boolean}} [options]
+ * @returns {Promise}
+ */
+ async _goToMyLocation({ follow = false } = {}) {
+ if (this._disposed) return;
+ if (!isGeolocationAvailable()) {
+ this._showToast('Location not supported in this browser');
+ return;
+ }
+
+ const generation = this._beginDeferredNavigation('location');
+ if (generation === false) return;
+
+ this._myLocationBtn?.classList.add('searching');
+ try {
+ const fix = await getCurrentBrowserPosition();
+ if (this._disposed || generation !== this._navigationGeneration) return;
+ if (!this._reassertNavigationHandoff(generation)) return;
+
+ this._applyMyLocationFix(fix, { follow, generation });
+ } catch (err) {
+ if (this._disposed || generation !== this._navigationGeneration) return;
+ this._showToast(err?.message || 'Could not get your location');
+ if (follow) this._stopMyLocationFollow();
+ } finally {
+ this._myLocationBtn?.classList.remove('searching');
+ }
+ }
+
+ /**
+ * Apply a GPS fix: marker, mini-status, fly-to, and optional live follow.
+ * @param {{lat:number,lon:number,accuracy?:number|null}} fix
+ * @param {{follow?:boolean, generation?:number|null, animate?:boolean}} [options]
+ * @returns {void}
+ */
+ _applyMyLocationFix(fix, { follow = false, generation = null, animate = true } = {}) {
+ if (this._disposed || !fix) return;
+ if (generation != null && generation !== this._navigationGeneration) return;
+
+ const range = Math.min(
+ Math.max(Number(fix.accuracy) > 0 ? Number(fix.accuracy) * 4 : 900, 400),
+ 2500,
+ );
+
+ this._ensureMyLocationEntity(fix);
+ this._searchedLocationLabel = 'My location';
+ this._setActiveLocation(null);
+ this._currentPoi = null;
+ this._collapsePOIRow();
+ this._updateLocationMiniStatus();
+
+ if (animate) {
+ const flight = flyToLandmark(this.viewer, fix.lat, fix.lon, {
+ range,
+ pitch: -45,
+ buildingHeight: 0,
+ duration: 2.2,
+ onStart: () => this._beginWorldJumpTransition(),
+ onComplete: () => this._endWorldJumpTransition(),
+ onCancel: () => this._endWorldJumpTransition(),
+ });
+ this._currentTarget = flight?.targetPosition || null;
+ } else if (this._myLocationFollow && this.viewer?.camera) {
+ const target = Cesium.Cartesian3.fromDegrees(fix.lon, fix.lat, 0);
+ this._currentTarget = target;
+ this.viewer.camera.lookAt(
+ target,
+ new Cesium.HeadingPitchRange(0, Cesium.Math.toRadians(-45), range),
+ );
+ this.viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
+ }
+
+ if (follow) this._startMyLocationFollow(fix);
+ }
+
+ /**
+ * Create or update the temporary "you are here" entity.
+ * @param {{lat:number,lon:number}} fix
+ * @returns {void}
+ */
+ _ensureMyLocationEntity(fix) {
+ if (!this.viewer?.entities) return;
+ const position = Cesium.Cartesian3.fromDegrees(fix.lon, fix.lat, 0);
+ if (!this._myLocationEntity) {
+ this._myLocationEntity = this.viewer.entities.add({
+ id: 'gev-my-location',
+ position,
+ point: {
+ pixelSize: 12,
+ color: Cesium.Color.fromCssColorString('#40b4ff'),
+ outlineColor: Cesium.Color.WHITE,
+ outlineWidth: 2,
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
+ },
+ label: {
+ text: 'You',
+ font: '12px sans-serif',
+ fillColor: Cesium.Color.WHITE,
+ outlineColor: Cesium.Color.BLACK,
+ outlineWidth: 2,
+ style: Cesium.LabelStyle.FILL_AND_OUTLINE,
+ pixelOffset: new Cesium.Cartesian2(0, -18),
+ heightReference: Cesium.HeightReference.CLAMP_TO_GROUND,
+ disableDepthTestDistance: Number.POSITIVE_INFINITY,
+ },
+ });
+ return;
+ }
+ this._myLocationEntity.position = position;
+ }
+
+ /**
+ * Begin live GPS follow after an initial fix.
+ * @param {{lat:number,lon:number}} initialFix
+ * @returns {void}
+ */
+ _startMyLocationFollow(initialFix) {
+ this._stopMyLocationFollow({ toast: null, keepEntity: true });
+ this._myLocationFollow = true;
+ this._myLocationBtn?.classList.add('following');
+ this._myLocationBtn?.setAttribute('aria-pressed', 'true');
+ this._myLocationBtn && (this._myLocationBtn.title = 'Following you — hold to stop');
+ this._showToast('Following your location');
+ this._ensureMyLocationEntity(initialFix);
+
+ this._myLocationUserInterruptHandler = () => {
+ if (!this._myLocationFollow) return;
+ this._stopMyLocationFollow({ toast: 'Stopped following location' });
+ };
+ this.viewer?.canvas?.addEventListener('pointerdown', this._myLocationUserInterruptHandler);
+ this.viewer?.canvas?.addEventListener('wheel', this._myLocationUserInterruptHandler, { passive: true });
+
+ this._myLocationWatch = watchBrowserPosition({
+ onUpdate: (fix) => {
+ if (this._disposed || !this._myLocationFollow) return;
+ this._applyMyLocationFix(fix, { follow: false, animate: false });
+ },
+ onError: (err) => {
+ if (this._disposed) return;
+ this._showToast(err?.message || 'Could not get your location');
+ this._stopMyLocationFollow();
+ },
+ });
+ }
+
+ /**
+ * Stop live GPS follow and optionally remove the marker.
+ * @param {{toast?:string|null, keepEntity?:boolean}} [options]
+ * @returns {void}
+ */
+ _stopMyLocationFollow({ toast = null, keepEntity = false } = {}) {
+ this._myLocationWatch?.stop?.();
+ this._myLocationWatch = null;
+ this._myLocationFollow = false;
+ this._myLocationBtn?.classList.remove('following');
+ this._myLocationBtn?.setAttribute('aria-pressed', 'false');
+ if (this._myLocationBtn) {
+ this._myLocationBtn.title = 'My location (click: go there · hold: follow)';
+ }
+ if (this._myLocationUserInterruptHandler) {
+ this.viewer?.canvas?.removeEventListener('pointerdown', this._myLocationUserInterruptHandler);
+ this.viewer?.canvas?.removeEventListener('wheel', this._myLocationUserInterruptHandler);
+ this._myLocationUserInterruptHandler = null;
+ }
+ if (!keepEntity && this._myLocationEntity && this.viewer?.entities) {
+ this.viewer.entities.remove(this._myLocationEntity);
+ this._myLocationEntity = null;
+ }
+ if (toast) this._showToast(toast);
}
/**
@@ -10234,6 +10447,9 @@ export class StyleManager {
document.removeEventListener('keydown', this._poiKeydownHandler);
this._poiKeydownHandler = null;
}
+ this._myLocationDetachGesture?.();
+ this._myLocationDetachGesture = null;
+ this._stopMyLocationFollow({ toast: null, keepEntity: false });
// Cancel the rAF animation loop and release its governor hold; also stop
// the traffic-chip ticker the loop no longer carries. (perf wave 2 fix)
if (this._animFrameId) {
diff --git a/style.css b/style.css
index 3941027..4e6faa7 100644
--- a/style.css
+++ b/style.css
@@ -1842,6 +1842,17 @@ body.ui-clean-view #scene-runtime.active {
border-color: var(--glass-border-hover);
}
+.my-location-btn.following,
+.my-location-btn[aria-pressed="true"] {
+ background: rgba(64, 180, 255, 0.22);
+ border-color: rgba(64, 180, 255, 0.55);
+ box-shadow: 0 0 0 1px rgba(64, 180, 255, 0.25);
+}
+
+.my-location-btn.searching {
+ opacity: 0.65;
+}
+
#location-search {
width: 0;
padding: 0;