{"project":{"id":"y4ZOXnC","userId":"davidyarham@gmail.com","username":null,"userPicture":null,"name":"3D Flight Tracker","visible":true,"contributors":"","githubRepo":"davidyarham/blnq-3d-flight-tracker1","forkedFrom":null,"isTemplate":false,"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","hasThumbnail":true}}