{"project":{"id":"y4ZOXnC","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"3D Flight Tracker","thumbnail":"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","visible":true,"contributors":"","githubRepo":"davidyarham/blnq-3d-flight-tracker1","forkedFrom":null,"tags":"","files":{"folder":"","files":[{"name":"index.html","content":"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Untitled</title>\n  <link rel=\"stylesheet\" href=\"style.css\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap\" rel=\"stylesheet\">\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/lucide@0.468.0/dist/umd/lucide.min.js\"></script>\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js\"></script>\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js\"></script>\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/CopyShader.js\"></script>\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/shaders/LuminosityHighPassShader.js\"></script>\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/EffectComposer.js\"></script>\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/RenderPass.js\"></script>\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/ShaderPass.js\"></script>\n<script crossorigin=\"anonymous\" src=\"https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/postprocessing/UnrealBloomPass.js\"></script>\n</head>\n<body>\n<div class=\"app flight-app\">\n\t<aside class=\"flight-app__sidebar panel\">\n\t\t<div class=\"panel__brand\">\n\t\t\t<div class=\"panel__brand-mark\">\n\t\t\t\t<i data-lucide=\"orbit\"></i>\n\t\t\t</div>\n\t\t\t<div>\n\t\t\t\t<p class=\"panel__eyebrow\">Live Airspace</p>\n\t\t\t\t<h1 class=\"panel__title\">3D Flight Tracker</h1>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"panel__section panel__section--status\">\n\t\t\t<div class=\"stat-card\">\n\t\t\t\t<span class=\"stat-card__label\">Data source</span>\n\t\t\t\t<strong class=\"stat-card__value\" id=\"sourceLabel\">Initializing</strong>\n\t\t\t</div>\n\t\t\t<div class=\"stat-card\">\n\t\t\t\t<span class=\"stat-card__label\">Tracked aircraft</span>\n\t\t\t\t<strong class=\"stat-card__value\" id=\"planeCount\">0</strong>\n\t\t\t</div>\n\t\t\t<div class=\"stat-card\">\n\t\t\t\t<span class=\"stat-card__label\">Last refresh</span>\n\t\t\t\t<strong class=\"stat-card__value\" id=\"refreshTime\">—</strong>\n\t\t\t</div>\n\t\t\t<div class=\"stat-card\">\n\t\t\t\t<span class=\"stat-card__label\">Selection</span>\n\t\t\t\t<strong class=\"stat-card__value\" id=\"selectionLabel\">None</strong>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"panel__section\">\n\t\t\t<div class=\"panel__section-head\">\n\t\t\t\t<h2>Aircraft Details</h2>\n\t\t\t\t<span class=\"panel__badge\" id=\"liveBadge\">Connecting</span>\n\t\t\t</div>\n\t\t\t<dl class=\"details-list\" id=\"detailsList\">\n\t\t\t\t<div class=\"details-list__row\">\n\t\t\t\t\t<dt>Callsign</dt>\n\t\t\t\t\t<dd>—</dd>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"details-list__row\">\n\t\t\t\t\t<dt>Country</dt>\n\t\t\t\t\t<dd>—</dd>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"details-list__row\">\n\t\t\t\t\t<dt>Altitude</dt>\n\t\t\t\t\t<dd>—</dd>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"details-list__row\">\n\t\t\t\t\t<dt>Velocity</dt>\n\t\t\t\t\t<dd>—</dd>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"details-list__row\">\n\t\t\t\t\t<dt>Heading</dt>\n\t\t\t\t\t<dd>—</dd>\n\t\t\t\t</div>\n\t\t\t</dl>\n\t\t</div>\n\n\t\t<div class=\"panel__section panel__section--legend\">\n\t\t\t<div class=\"panel__section-head\">\n\t\t\t\t<h2>Controls</h2>\n\t\t\t</div>\n\t\t\t<ul class=\"legend-list\">\n\t\t\t\t<li><i data-lucide=\"mouse-pointer-2\"></i><span>Drag to orbit the globe</span></li>\n\t\t\t\t<li><i data-lucide=\"smartphone\"></i><span>Tap aircraft to inspect</span></li>\n\t\t\t\t<li><i data-lucide=\"circle\"></i><span>Aircraft appear as bright red dots with route arcs</span></li>\n\t\t\t\t<li><i data-lucide=\"activity\"></i><span>Auto-refresh every 15 seconds</span></li>\n\t\t\t</ul>\n\t\t</div>\n\t</aside>\n\n\t<main class=\"flight-app__main\">\n\t\t<div class=\"scene-shell\" id=\"sceneShell\">\n\t\t\t<canvas id=\"sceneCanvas\"></canvas>\n\t\t\t<div class=\"scene-shell__overlay scene-shell__overlay--top\">\n\t\t\t\t<div class=\"pill pill--glow\">\n\t\t\t\t\t<i data-lucide=\"radio-tower\"></i>\n\t\t\t\t\t<span id=\"networkStatus\">Connecting to OpenSky Network</span>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"scene-shell__overlay scene-shell__overlay--bottom\">\n\t\t\t\t<div class=\"info-strip\">\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"info-strip__label\">Mode</span>\n\t\t\t\t\t\t<strong>Global Airspace</strong>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"info-strip__label\">Update cadence</span>\n\t\t\t\t\t\t<strong>15s polling + smooth interpolation</strong>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<span class=\"info-strip__label\">Markers</span>\n\t\t\t\t\t\t<strong>Red aircraft dots + route arcs</strong>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div class=\"tooltip\" id=\"planeTooltip\" hidden></div>\n\t\t</div>\n\t</main>\n</div>\n  <script type=\"module\" src=\"main.js\"></script>\n</body>\n</html>"},{"name":"main.js","content":"const canvas = document.getElementById('sceneCanvas');\nconst planeCountEl = document.getElementById('planeCount');\nconst refreshTimeEl = document.getElementById('refreshTime');\nconst sourceLabelEl = document.getElementById('sourceLabel');\nconst selectionLabelEl = document.getElementById('selectionLabel');\nconst networkStatusEl = document.getElementById('networkStatus');\nconst detailsListEl = document.getElementById('detailsList');\nconst tooltipEl = document.getElementById('planeTooltip');\nconst liveBadgeEl = document.getElementById('liveBadge');\n\nfunction setStatus(mode, message, badgeText) {\n\tsourceLabelEl.textContent = mode;\n\tnetworkStatusEl.textContent = message;\n\tliveBadgeEl.textContent = badgeText;\n}\n\nwindow.addEventListener('error', (event) => {\n\tif (event.target && event.target !== window && (event.target.src || event.target.href)) {\n\t\tconst failedUrl = event.target.src || event.target.href;\n\t\tconsole.error('External resource failed to load:', failedUrl);\n\t\tsetStatus('Load error', `Failed to load: ${failedUrl.split('/').pop()}`, 'Error');\n\t\treturn;\n\t}\n\n\tconsole.error('Runtime error:', event.message, event.error || '');\n\tif (event.message === 'Script error.') {\n\t\tsetStatus('Load error', 'External script failed to load', 'Error');\n\t}\n});\n\nconst THREE_LIB = window.THREE;\nif (!THREE_LIB) {\n\tthrow new Error('Three.js failed to load');\n}\nif (!THREE_LIB.OrbitControls) {\n\tthrow new Error('OrbitControls failed to load');\n}\nif (!THREE_LIB.EffectComposer || !THREE_LIB.RenderPass || !THREE_LIB.UnrealBloomPass || !THREE_LIB.ShaderPass) {\n\tthrow new Error('Three.js postprocessing dependencies failed to load');\n}\n\nconst renderer = new THREE_LIB.WebGLRenderer({\n\tcanvas,\n\tantialias: true,\n\talpha: true\n});\nrenderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));\nif ('outputEncoding' in renderer) renderer.outputEncoding = THREE_LIB.sRGBEncoding;\nif ('toneMapping' in renderer) renderer.toneMapping = THREE_LIB.ACESFilmicToneMapping;\nrenderer.toneMappingExposure = 1.1;\n\nconst scene = new THREE_LIB.Scene();\nscene.fog = new THREE_LIB.FogExp2(0x040814, 0.045);\n\nconst camera = new THREE_LIB.PerspectiveCamera(42, 1, 0.1, 1000);\ncamera.position.set(0, 0, 18);\n\nconst controls = new THREE_LIB.OrbitControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\ncontrols.enablePan = false;\ncontrols.minDistance = 10;\ncontrols.maxDistance = 32;\ncontrols.rotateSpeed = 0.7;\ncontrols.zoomSpeed = 0.8;\ncontrols.enableTouchPan = false;\n\nconst composer = new THREE_LIB.EffectComposer(renderer);\nconst renderPass = new THREE_LIB.RenderPass(scene, camera);\ncomposer.addPass(renderPass);\nconst bloomPass = new THREE_LIB.UnrealBloomPass(new THREE_LIB.Vector2(1, 1), 0.12, 0.7, 0.9);\ncomposer.addPass(bloomPass);\n\nconst ambientLight = new THREE_LIB.AmbientLight(0x7fa8ff, 0.65);\nscene.add(ambientLight);\nconst hemiLight = new THREE_LIB.HemisphereLight(0x9fd0ff, 0x08101d, 0.9);\nscene.add(hemiLight);\nconst sunLight = new THREE_LIB.DirectionalLight(0xffffff, 1.8);\nsunLight.position.set(14, 10, 12);\nscene.add(sunLight);\n\nconst worldGroup = new THREE_LIB.Group();\nscene.add(worldGroup);\n\nconst starsGeometry = new THREE_LIB.BufferGeometry();\nconst starCount = 2500;\nconst starPositions = new Float32Array(starCount * 3);\nfor (let i = 0; i < starCount; i += 1) {\n\tconst radius = 120 + Math.random() * 180;\n\tconst theta = Math.random() * Math.PI * 2;\n\tconst phi = Math.acos(2 * Math.random() - 1);\n\tstarPositions[i * 3] = radius * Math.sin(phi) * Math.cos(theta);\n\tstarPositions[i * 3 + 1] = radius * Math.cos(phi);\n\tstarPositions[i * 3 + 2] = radius * Math.sin(phi) * Math.sin(theta);\n}\nstarsGeometry.setAttribute('position', new THREE_LIB.BufferAttribute(starPositions, 3));\nconst starsMaterial = new THREE_LIB.PointsMaterial({\n\tcolor: 0xb9d6ff,\n\tsize: 0.42,\n\ttransparent: true,\n\topacity: 0.82\n});\nscene.add(new THREE_LIB.Points(starsGeometry, starsMaterial));\n\nconst earthRadius = 5;\nconst planeBaseAltitude = 0.16;\nconst planeAltitudeScale = 0.00011;\n\nconst textureLoader = new THREE_LIB.TextureLoader();\nconst earthTexture = textureLoader.load('https://threejs.org/examples/textures/planets/earth_atmos_2048.jpg');\nconst bumpTexture = textureLoader.load('https://threejs.org/examples/textures/planets/earth_bump_2048.jpg');\nconst cloudTexture = textureLoader.load('https://threejs.org/examples/textures/planets/earth_clouds_1024.png');\n\nearthTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();\nbumpTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();\ncloudTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();\n\nconst earthMesh = new THREE_LIB.Mesh(\n\tnew THREE_LIB.SphereGeometry(earthRadius, 96, 96),\n\tnew THREE_LIB.MeshStandardMaterial({\n\t\tmap: earthTexture,\n\t\tbumpMap: bumpTexture,\n\t\tbumpScale: 0.16,\n\t\troughness: 0.94,\n\t\tmetalness: 0.05,\n\t\temissive: new THREE_LIB.Color(0x07111f),\n\t\temissiveIntensity: 0.45\n\t})\n);\nworldGroup.add(earthMesh);\n\nconst cloudMesh = new THREE_LIB.Mesh(\n\tnew THREE_LIB.SphereGeometry(earthRadius + 0.05, 64, 64),\n\tnew THREE_LIB.MeshStandardMaterial({\n\t\tmap: cloudTexture,\n\t\ttransparent: true,\n\t\topacity: 0.22,\n\t\tdepthWrite: false,\n\t\tblending: THREE_LIB.AdditiveBlending\n\t})\n);\nworldGroup.add(cloudMesh);\n\nconst atmosphereMesh = new THREE_LIB.Mesh(\n\tnew THREE_LIB.SphereGeometry(earthRadius + 0.22, 64, 64),\n\tnew THREE_LIB.ShaderMaterial({\n\t\tuniforms: {\n\t\t\tglowColor: {\n\t\t\t\tvalue: new THREE_LIB.Color(0x4dbdff)\n\t\t\t}\n\t\t},\n\t\tvertexShader: `\n\t\t\tvarying vec3 vNormal;\n\t\t\tvoid main() {\n\t\t\t\tvNormal = normalize(normalMatrix * normal);\n\t\t\t\tgl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n\t\t\t}\n\t\t`,\n\t\tfragmentShader: `\n\t\t\tvarying vec3 vNormal;\n\t\t\tuniform vec3 glowColor;\n\t\t\tvoid main() {\n\t\t\t\tfloat intensity = pow(0.78 - dot(vNormal, vec3(0.0, 0.0, 1.0)), 3.2);\n\t\t\t\tgl_FragColor = vec4(glowColor, intensity * 0.82);\n\t\t\t}\n\t\t`,\n\t\tblending: THREE_LIB.AdditiveBlending,\n\t\tside: THREE_LIB.BackSide,\n\t\ttransparent: true,\n\t\tdepthWrite: false\n\t})\n);\nworldGroup.add(atmosphereMesh);\n\nconst flightGroup = new THREE_LIB.Group();\nearthMesh.add(flightGroup);\n\nconst raycaster = new THREE_LIB.Raycaster();\nraycaster.params.Points = {\n\tthreshold: 0.35\n};\nconst pointer = new THREE_LIB.Vector2();\nconst trackedPlanes = new Map();\nlet selectedPlaneId = null;\n\nfunction formatTime(date) {\n\treturn new Intl.DateTimeFormat([], {\n\t\thour: '2-digit',\n\t\tminute: '2-digit',\n\t\tsecond: '2-digit'\n\t}).format(date);\n}\n\nfunction formatAltitude(meters) {\n\tif (!Number.isFinite(meters)) return '—';\n\tconst feet = meters * 3.28084;\n\treturn `${Math.round(meters).toLocaleString()} m / ${Math.round(feet).toLocaleString()} ft`;\n}\n\nfunction formatVelocity(ms) {\n\tif (!Number.isFinite(ms)) return '—';\n\tconst kmh = ms * 3.6;\n\treturn `${Math.round(ms)} m/s / ${Math.round(kmh)} km/h`;\n}\n\nfunction latLonAltToVector3(lat, lon, altitude) {\n\tconst safeAltitude = Number.isFinite(altitude) ? altitude : 0;\n\tconst phi = (90 - lat) * (Math.PI / 180);\n\tconst theta = (lon + 180) * (Math.PI / 180);\n\tconst radius = earthRadius + planeBaseAltitude + Math.max(0, safeAltitude) * planeAltitudeScale;\n\treturn new THREE_LIB.Vector3(\n\t\t-radius * Math.sin(phi) * Math.cos(theta),\n\t\tradius * Math.cos(phi),\n\t\tradius * Math.sin(phi) * Math.sin(theta)\n\t);\n}\n\nfunction buildPlaneMarker() {\n\tconst marker = new THREE_LIB.Group();\n\n\tconst core = new THREE_LIB.Mesh(\n\t\tnew THREE_LIB.SphereGeometry(0.065, 18, 18),\n\t\tnew THREE_LIB.MeshBasicMaterial({\n\t\t\tcolor: 0xff0d00\n\t\t})\n\t);\n\tmarker.add(core);\n\n\tconst halo = new THREE_LIB.Mesh(\n\t\tnew THREE_LIB.SphereGeometry(0.11, 18, 18),\n\t\tnew THREE_LIB.MeshBasicMaterial({\n\t\t\tcolor: 0xff1408,\n\t\t\ttransparent: true,\n\t\t\topacity: 0.1,\n\t\t\tdepthWrite: false\n\t\t})\n\t);\n\tmarker.add(halo);\n\n\tconst hitArea = new THREE_LIB.Mesh(\n\t\tnew THREE_LIB.SphereGeometry(0.3, 16, 16),\n\t\tnew THREE_LIB.MeshBasicMaterial({\n\t\t\ttransparent: true,\n\t\t\topacity: 0,\n\t\t\tdepthWrite: false\n\t\t})\n\t);\n\tmarker.add(hitArea);\n\n\tmarker.userData = {\n\t\tcore,\n\t\thalo,\n\t\thitArea\n\t};\n\treturn marker;\n}\n\nfunction updateDetails(plane) {\n\tif (!plane) {\n\t\tselectionLabelEl.textContent = 'None';\n\t\tdetailsListEl.innerHTML = `\n\t\t\t<div class=\"details-list__row\"><dt>Callsign</dt><dd>—</dd></div>\n\t\t\t<div class=\"details-list__row\"><dt>Country</dt><dd>—</dd></div>\n\t\t\t<div class=\"details-list__row\"><dt>Altitude</dt><dd>—</dd></div>\n\t\t\t<div class=\"details-list__row\"><dt>Velocity</dt><dd>—</dd></div>\n\t\t\t<div class=\"details-list__row\"><dt>Heading</dt><dd>—</dd></div>\n\t\t`;\n\t\treturn;\n\t}\n\n\tselectionLabelEl.textContent = plane.callsign || plane.icao24;\n\tdetailsListEl.innerHTML = `\n\t\t<div class=\"details-list__row\"><dt>Callsign</dt><dd>${plane.callsign || 'Unknown'}</dd></div>\n\t\t<div class=\"details-list__row\"><dt>Country</dt><dd>${plane.origin_country || 'Unknown'}</dd></div>\n\t\t<div class=\"details-list__row\"><dt>Altitude</dt><dd>${formatAltitude(plane.baro_altitude)}</dd></div>\n\t\t<div class=\"details-list__row\"><dt>Velocity</dt><dd>${formatVelocity(plane.velocity)}</dd></div>\n\t\t<div class=\"details-list__row\"><dt>Heading</dt><dd>${Number.isFinite(plane.true_track) ? `${Math.round(plane.true_track)}°` : '—'}</dd></div>\n\t`;\n}\n\nfunction normalizeState(state) {\n\tif (!state) return null;\n\treturn {\n\t\ticao24: state[0],\n\t\tcallsign: state[1] ? state[1].trim() : '',\n\t\torigin_country: state[2],\n\t\tlongitude: state[5],\n\t\tlatitude: state[6],\n\t\tbaro_altitude: state[7],\n\t\tvelocity: state[9],\n\t\ttrue_track: state[10],\n\t\tvertical_rate: state[11],\n\t\tgeo_altitude: state[13],\n\t\tlast_contact: state[4]\n\t};\n}\n\nfunction generateDemoFlights() {\n\tconst flights = [];\n\tfor (let i = 0; i < 48; i += 1) {\n\t\tflights.push({\n\t\t\ticao24: `demo${i}`,\n\t\t\tcallsign: `SKY${100 + i}`,\n\t\t\torigin_country: 'Simulated',\n\t\t\tlatitude: -60 + Math.random() * 120,\n\t\t\tlongitude: -180 + Math.random() * 360,\n\t\t\tbaro_altitude: 1000 + Math.random() * 11000,\n\t\t\tvelocity: 140 + Math.random() * 160,\n\t\t\ttrue_track: Math.random() * 360,\n\t\t\tvertical_rate: -8 + Math.random() * 16,\n\t\t\tgeo_altitude: null,\n\t\t\tlast_contact: Math.floor(Date.now() / 1000)\n\t\t});\n\t}\n\treturn flights;\n}\n\nfunction makeArcLine(points) {\n\tconst curvePoints = [];\n\tfor (let i = 0; i < points.length; i += 1) {\n\t\tconst p = points[i].clone();\n\t\tconst lift = 1 + Math.sin((i / Math.max(1, points.length - 1)) * Math.PI) * 0.06;\n\t\tcurvePoints.push(p.normalize().multiplyScalar(p.length() * lift));\n\t}\n\treturn curvePoints;\n}\n\nfunction syncFlights(flights) {\n\tconst nextIds = new Set();\n\n\tflights.forEach((plane) => {\n\t\tif (!Number.isFinite(plane.latitude) || !Number.isFinite(plane.longitude)) return;\n\n\t\tconst id = plane.icao24;\n\t\tnextIds.add(id);\n\t\tconst altitude = plane.geo_altitude != null ? plane.geo_altitude : plane.baro_altitude;\n\t\tconst targetPosition = latLonAltToVector3(plane.latitude, plane.longitude, altitude);\n\n\t\tlet entry = trackedPlanes.get(id);\n\t\tif (!entry) {\n\t\t\tconst marker = buildPlaneMarker();\n\t\t\tmarker.position.copy(targetPosition);\n\t\t\tmarker.userData.id = id;\n\t\t\tflightGroup.add(marker);\n\n\t\t\tconst trailGeometry = new THREE_LIB.BufferGeometry().setFromPoints([targetPosition.clone(), targetPosition.clone()]);\n\t\t\tconst trailMaterial = new THREE_LIB.LineBasicMaterial({\n\t\t\t\tcolor: 0x76d7ff,\n\t\t\t\ttransparent: true,\n\t\t\t\topacity: 0.85\n\t\t\t});\n\t\t\tconst trail = new THREE_LIB.Line(trailGeometry, trailMaterial);\n\t\t\tflightGroup.add(trail);\n\n\t\t\tentry = {\n\t\t\t\tmarker,\n\t\t\t\ttrail,\n\t\t\t\tpoints: [targetPosition.clone()],\n\t\t\t\tcurrentPosition: targetPosition.clone(),\n\t\t\t\ttargetPosition: targetPosition.clone(),\n\t\t\t\tdata: plane,\n\t\t\t\theading: plane.true_track || 0\n\t\t\t};\n\t\t\ttrackedPlanes.set(id, entry);\n\t\t}\n\n\t\tentry.data = plane;\n\t\tentry.targetPosition.copy(targetPosition);\n\t\tentry.heading = plane.true_track || entry.heading;\n\n\t\tconst lastPoint = entry.points[entry.points.length - 1];\n\t\tif (!lastPoint || lastPoint.distanceTo(targetPosition) > 0.08) {\n\t\t\tentry.points.push(targetPosition.clone());\n\t\t\tif (entry.points.length > 28) entry.points.shift();\n\t\t}\n\n\t\tentry.trail.geometry.setFromPoints(makeArcLine(entry.points));\n\t});\n\n\ttrackedPlanes.forEach((entry, id) => {\n\t\tif (nextIds.has(id)) return;\n\t\tflightGroup.remove(entry.marker);\n\t\tflightGroup.remove(entry.trail);\n\n\t\tentry.marker.traverse((child) => {\n\t\t\tif (child.geometry) child.geometry.dispose();\n\t\t\tif (child.material) {\n\t\t\t\tif (Array.isArray(child.material)) child.material.forEach((mat) => mat.dispose());\n\t\t\t\telse child.material.dispose();\n\t\t\t}\n\t\t});\n\t\tentry.trail.geometry.dispose();\n\t\tentry.trail.material.dispose();\n\t\ttrackedPlanes.delete(id);\n\n\t\tif (selectedPlaneId === id) {\n\t\t\tselectedPlaneId = null;\n\t\t\tupdateDetails(null);\n\t\t}\n\t});\n\n\tplaneCountEl.textContent = trackedPlanes.size.toLocaleString();\n}\n\nasync function fetchFlights() {\n\tconst endpoint = 'https://opensky-network.org/api/states/all';\n\ttry {\n\t\tsetStatus('OpenSky', 'Fetching live OpenSky aircraft states', 'Live');\n\t\tconst response = await fetch(endpoint, {\n\t\t\theaders: {\n\t\t\t\tAccept: 'application/json'\n\t\t\t}\n\t\t});\n\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\tconst data = await response.json();\n\t\tconst flights = (data.states || []).map(normalizeState).filter(Boolean).slice(0, 220);\n\t\tif (!flights.length) throw new Error('No aircraft returned');\n\t\tsyncFlights(flights);\n\t\trefreshTimeEl.textContent = formatTime(new Date());\n\t\tsetStatus('OpenSky', 'Connected to OpenSky Network', 'Live');\n\t} catch (error) {\n\t\tconsole.warn('Live OpenSky fetch failed, falling back to demo traffic.', error);\n\t\tsyncFlights(generateDemoFlights());\n\t\trefreshTimeEl.textContent = formatTime(new Date());\n\t\tsetStatus('Demo fallback', 'Live feed unavailable, showing simulated traffic', 'Fallback');\n\t}\n}\n\nfunction updateTooltip(clientX, clientY, plane) {\n\tif (!plane) {\n\t\ttooltipEl.hidden = true;\n\t\treturn;\n\t}\n\n\tconst rect = renderer.domElement.getBoundingClientRect();\n\tconst offsetX = 16;\n\tconst offsetY = 16;\n\tconst localX = clientX - rect.left;\n\tconst localY = clientY - rect.top;\n\n\ttooltipEl.hidden = false;\n\ttooltipEl.innerHTML = `\n\t\t<p class=\"tooltip__title\">${plane.callsign || plane.icao24}</p>\n\t\t<p class=\"tooltip__meta\">${plane.origin_country || 'Unknown'} · ${formatAltitude(plane.baro_altitude)}</p>\n\t`;\n\n\tconst tooltipWidth = tooltipEl.offsetWidth || 220;\n\tconst tooltipHeight = tooltipEl.offsetHeight || 72;\n\tconst maxX = rect.width - tooltipWidth - 12;\n\tconst maxY = rect.height - tooltipHeight - 12;\n\tconst left = Math.max(12, Math.min(localX + offsetX, maxX));\n\tconst top = Math.max(12, Math.min(localY + offsetY, maxY));\n\n\ttooltipEl.style.left = `${left}px`;\n\ttooltipEl.style.top = `${top}px`;\n}\n\nfunction setPointerFromEvent(event) {\n\tconst rect = renderer.domElement.getBoundingClientRect();\n\tconst clientX = event.clientX ?? (event.touches && event.touches[0] ? event.touches[0].clientX : 0);\n\tconst clientY = event.clientY ?? (event.touches && event.touches[0] ? event.touches[0].clientY : 0);\n\tpointer.x = ((clientX - rect.left) / rect.width) * 2 - 1;\n\tpointer.y = -((clientY - rect.top) / rect.height) * 2 + 1;\n\treturn {\n\t\tclientX,\n\t\tclientY\n\t};\n}\n\nfunction pickPlane(event) {\n\tconst {\n\t\tclientX,\n\t\tclientY\n\t} = setPointerFromEvent(event);\n\traycaster.setFromCamera(pointer, camera);\n\tconst hitTargets = Array.from(trackedPlanes.values()).map((entry) => entry.marker);\n\tconst intersects = raycaster.intersectObjects(hitTargets, true);\n\n\tlet plane = null;\n\tif (intersects.length) {\n\t\tconst hit = intersects[0].object;\n\t\tconst marker = hit.parent.userData && hit.parent.userData.id ? hit.parent : hit.parent?.parent;\n\t\tconst id = marker && marker.userData ? marker.userData.id : null;\n\t\tif (id && trackedPlanes.has(id)) {\n\t\t\tselectedPlaneId = id;\n\t\t\tplane = trackedPlanes.get(id).data;\n\t\t}\n\t} else {\n\t\tselectedPlaneId = null;\n\t}\n\n\tupdateDetails(plane);\n\tupdateTooltip(clientX, clientY, plane);\n}\n\nrenderer.domElement.addEventListener('click', pickPlane);\nrenderer.domElement.addEventListener('touchstart', (event) => {\n\tpickPlane(event);\n}, {\n\tpassive: true\n});\n\nrenderer.domElement.addEventListener('mousemove', (event) => {\n\tconst {\n\t\tclientX,\n\t\tclientY\n\t} = setPointerFromEvent(event);\n\traycaster.setFromCamera(pointer, camera);\n\tconst hitTargets = Array.from(trackedPlanes.values()).map((entry) => entry.marker);\n\tconst intersects = raycaster.intersectObjects(hitTargets, true);\n\tif (intersects.length) {\n\t\tconst hit = intersects[0].object;\n\t\tconst marker = hit.parent.userData && hit.parent.userData.id ? hit.parent : hit.parent?.parent;\n\t\tconst id = marker && marker.userData ? marker.userData.id : null;\n\t\tconst plane = id && trackedPlanes.has(id) ? trackedPlanes.get(id).data : null;\n\t\tupdateTooltip(clientX, clientY, plane);\n\t\trenderer.domElement.style.cursor = 'pointer';\n\t} else {\n\t\ttooltipEl.hidden = true;\n\t\trenderer.domElement.style.cursor = 'grab';\n\t}\n});\n\nfunction resizeScene() {\n\tconst rect = canvas.parentElement.getBoundingClientRect();\n\tconst width = rect.width;\n\tconst height = rect.width > 980 ? rect.height : Math.max(window.innerHeight * 0.7, 540);\n\trenderer.setSize(width, height);\n\tcomposer.setSize(width, height);\n\tcamera.aspect = width / height;\n\tcamera.updateProjectionMatrix();\n\tif (bloomPass.setSize) bloomPass.setSize(width, height);\n}\nwindow.addEventListener('resize', resizeScene);\nresizeScene();\n\nconst clock = new THREE_LIB.Clock();\n\nfunction animate() {\n\tconst delta = clock.getDelta();\n\tconst elapsed = clock.elapsedTime;\n\n\tearthMesh.rotation.y += delta * 0.03;\n\tcloudMesh.rotation.y += delta * 0.04;\n\tstarsMaterial.opacity = 0.72 + Math.sin(elapsed * 0.35) * 0.08;\n\n\ttrackedPlanes.forEach((entry, id) => {\n\t\tentry.currentPosition.lerp(entry.targetPosition, 0.08);\n\t\tentry.marker.position.copy(entry.currentPosition);\n\n\t\tconst isSelected = selectedPlaneId === id;\n\t\tentry.marker.scale.setScalar(isSelected ? 1.38 : 1.02);\n\t\tentry.marker.userData.core.material.color.set(isSelected ? 0xff220a : 0xff0d00);\n\t\tentry.marker.userData.halo.material.color.set(isSelected ? 0xff2a12 : 0xff1408);\n\t\tentry.marker.userData.halo.material.opacity = isSelected ? 0.12 : 0.06;\n\t\tentry.trail.material.opacity = isSelected ? 1 : 0.72;\n\t\tentry.trail.material.color.set(isSelected ? 0xbaf2ff : 0x76d7ff);\n\t});\n\n\tcontrols.update();\n\tcomposer.render();\n\trequestAnimationFrame(animate);\n}\n\nfetchFlights();\nsetInterval(fetchFlights, 15000);\nanimate();\nupdateDetails(null);\n\nif (window.lucide) {\n\tlucide.createIcons();\n}"},{"name":"style.css","content":":root {\n\tcolor-scheme: dark;\n\tfont-family: Inter, system-ui, sans-serif;\n\t--bg: #040814;\n\t--bg-panel: rgba(10, 16, 30, 0.72);\n\t--border: rgba(150, 190, 255, 0.16);\n\t--text: #eef4ff;\n\t--muted: #8da4c7;\n\t--accent: #6cc8ff;\n\t--accent-strong: #9e7bff;\n\t--success: #6ef7c8;\n\t--warning: #ffd06b;\n\t--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);\n}\n\n* {\n\tbox-sizing: border-box;\n}\n\nhtml,\nbody {\n\tmargin: 0;\n\tmin-height: 100%;\n\tbackground:\n\t\tradial-gradient(circle at top, rgba(80, 124, 255, 0.22), transparent 30%),\n\t\tradial-gradient(circle at 80% 20%, rgba(122, 84, 255, 0.2), transparent 24%),\n\t\tlinear-gradient(180deg, #07101f 0%, #040814 48%, #02050d 100%);\n\tcolor: var(--text);\n}\n\nbody {\n\tmin-height: 100vh;\n\toverflow: hidden;\n\toverflow-y: auto;\n}\n\ncanvas {\n\tdisplay: block;\n}\n\n.flight-app {\n\tdisplay: grid;\n\tgrid-template-columns: 360px minmax(0, 1fr);\n\tmin-height: 100vh;\n}\n\n.panel {\n\tposition: relative;\n\tz-index: 2;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1.25rem;\n\tpadding: 1.5rem;\n\tbackground: linear-gradient(180deg, rgba(6, 12, 24, 0.92), rgba(7, 12, 22, 0.72));\n\tborder-right: 1px solid var(--border);\n\tbackdrop-filter: blur(24px);\n\tbox-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.04);\n\tmax-height: 100vh;\n\tscrollbar-width: thin;\n\tscrollbar-color: rgba(140, 180, 255, 0.35) transparent;\n}\n\n.panel__brand,\n.panel__section,\n.stat-card,\n.pill,\n.info-strip,\n.tooltip {\n\tborder: 1px solid var(--border);\n\tbackground: var(--bg-panel);\n\tbox-shadow: var(--shadow);\n\tbackdrop-filter: blur(18px);\n}\n\n.panel__brand {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 1rem;\n\tpadding: 1rem;\n\tborder-radius: 1.5rem;\n}\n\n.panel__brand-mark {\n\tdisplay: grid;\n\tplace-items: center;\n\twidth: 3rem;\n\theight: 3rem;\n\tborder-radius: 1rem;\n\tcolor: var(--bg);\n\tbackground: linear-gradient(135deg, var(--accent), #b2f1ff);\n}\n\n.panel__brand-mark i,\n.legend-list i,\n.pill i {\n\twidth: 1.1rem;\n\theight: 1.1rem;\n}\n\n.panel__eyebrow,\n.info-strip__label,\n.stat-card__label {\n\tmargin: 0 0 0.25rem;\n\tfont-size: 0.72rem;\n\tletter-spacing: 0.12em;\n\ttext-transform: uppercase;\n\tcolor: var(--muted);\n}\n\n.panel__title {\n\tmargin: 0;\n\tfont-size: 1.5rem;\n}\n\n.panel__section {\n\tpadding: 1rem;\n\tborder-radius: 1.5rem;\n}\n\n.panel__section--status {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n\tgap: 0.85rem;\n}\n\n.stat-card {\n\tpadding: 0.9rem;\n\tborder-radius: 1.1rem;\n}\n\n.stat-card__value {\n\tdisplay: block;\n\tfont-size: 1rem;\n}\n\n.panel__section-head {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: space-between;\n\tgap: 1rem;\n\tmargin-bottom: 0.9rem;\n}\n\n.panel__section-head h2 {\n\tmargin: 0;\n\tfont-size: 0.98rem;\n}\n\n.panel__badge {\n\tpadding: 0.3rem 0.6rem;\n\tborder-radius: 999px;\n\tfont-size: 0.72rem;\n\tcolor: var(--success);\n\tbackground: rgba(110, 247, 200, 0.12);\n\tborder: 1px solid rgba(110, 247, 200, 0.2);\n}\n\n.details-list {\n\tmargin: 0;\n}\n\n.details-list__row {\n\tdisplay: flex;\n\tjustify-content: space-between;\n\tgap: 1rem;\n\tpadding: 0.7rem 0;\n\tborder-bottom: 1px solid rgba(255, 255, 255, 0.06);\n}\n\n.details-list__row:last-child {\n\tborder-bottom: 0;\n}\n\n.details-list__row dt {\n\tcolor: var(--muted);\n}\n\n.details-list__row dd {\n\tmargin: 0;\n\ttext-align: right;\n\tfont-weight: 600;\n}\n\n.legend-list {\n\tdisplay: grid;\n\tgap: 0.9rem;\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n.legend-list li {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 0.8rem;\n\tcolor: var(--text);\n}\n\n.legend-list i {\n\tcolor: var(--accent);\n\tflex: 0 0 auto;\n}\n\n.flight-app__main,\n.scene-shell {\n\tposition: relative;\n\tmin-width: 0;\n}\n\n.scene-shell {\n\theight: 100vh;\n\toverflow: hidden;\n\ttouch-action: none;\n}\n\n#sceneCanvas {\n\twidth: 100%;\n\theight: 100%;\n}\n\n.scene-shell__overlay {\n\tposition: absolute;\n\tleft: 1.5rem;\n\tright: 1.5rem;\n\tz-index: 3;\n\tpointer-events: none;\n}\n\n.scene-shell__overlay--top {\n\ttop: 1.5rem;\n\tdisplay: flex;\n\tjustify-content: flex-end;\n}\n\n.scene-shell__overlay--bottom {\n\tbottom: 1.5rem;\n}\n\n.pill {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 0.75rem;\n\tpadding: 0.8rem 1rem;\n\tborder-radius: 999px;\n}\n\n.pill--glow {\n\tbox-shadow: 0 0 0 1px rgba(108, 200, 255, 0.16), 0 0 40px rgba(108, 200, 255, 0.12), var(--shadow);\n}\n\n.pill i {\n\tcolor: var(--accent);\n}\n\n.info-strip {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(3, minmax(0, 1fr));\n\tgap: 1rem;\n\tpadding: 1rem 1.15rem;\n\tborder-radius: 1.35rem;\n}\n\n.info-strip strong {\n\tdisplay: block;\n\tfont-size: 0.95rem;\n}\n\n.tooltip {\n\tposition: absolute;\n\tz-index: 4;\n\tmin-width: 220px;\n\tmax-width: min(280px, calc(100vw - 2rem));\n\tpadding: 0.85rem 0.95rem;\n\tborder-radius: 1rem;\n\ttransform: translate(14px, 14px);\n\tpointer-events: none;\n}\n\n.tooltip__title {\n\tmargin: 0 0 0.25rem;\n\tfont-size: 0.92rem;\n}\n\n.tooltip__meta {\n\tmargin: 0.15rem 0 0;\n\tcolor: var(--muted);\n\tfont-size: 0.8rem;\n}\n\n.tooltip__meta strong {\n\tcolor: var(--text);\n}\n\n@media (max-width: 980px) {\n\t.panel::-webkit-scrollbar {\n\t\twidth: 10px;\n\t}\n\n\t.panel::-webkit-scrollbar-track {\n\t\tbackground: transparent;\n\t}\n\n\t.panel::-webkit-scrollbar-thumb {\n\t\tbackground: rgba(140, 180, 255, 0.28);\n\t\tborder-radius: 999px;\n\t\tborder: 2px solid transparent;\n\t\tbackground-clip: padding-box;\n\t}\n\n\t.panel::-webkit-scrollbar-thumb:hover {\n\t\tbackground: rgba(140, 180, 255, 0.4);\n\t\tborder: 2px solid transparent;\n\t\tbackground-clip: padding-box;\n\t}\n\n\tbody {\n\t\toverflow: auto;\n\t}\n\n\t.flight-app {\n\t\tgrid-template-columns: 1fr;\n\t}\n\n\t.panel {\n\t\tborder-right: 0;\n\t\tborder-bottom: 1px solid var(--border);\n\t}\n\n\t.flight-app__main,\n\t.scene-shell {\n\t\theight: 70vh;\n\t\tmin-height: 540px;\n\t}\n\n\t.info-strip {\n\t\tgrid-template-columns: 1fr;\n\t}\n}\n\n@media (max-width: 640px) {\n\t.panel__section--status {\n\t\tgrid-template-columns: 1fr 1fr;\n\t}\n\n\t.scene-shell__overlay {\n\t\tleft: 1rem;\n\t\tright: 1rem;\n\t}\n\n\t.scene-shell__overlay--top {\n\t\tjustify-content: flex-start;\n\t\ttop: 1rem;\n\t}\n\n\t.scene-shell__overlay--bottom {\n\t\tbottom: 1rem;\n\t}\n\n\t.tooltip {\n\t\ttransform: translate(10px, -110%);\n\t}\n}"}],"folders":[]},"variants":null,"createdAt":"2026-03-07T09:02:58.222Z","updatedAt":"2026-03-13T15:35:52.819Z"}}