@jjlmoya/utils-astronomy 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.json +60 -0
  2. package/src/category/i18n/en.ts +57 -0
  3. package/src/category/i18n/es.ts +57 -0
  4. package/src/category/i18n/fr.ts +58 -0
  5. package/src/category/index.ts +16 -0
  6. package/src/category/seo.astro +15 -0
  7. package/src/components/PreviewNavSidebar.astro +116 -0
  8. package/src/components/PreviewToolbar.astro +143 -0
  9. package/src/data.ts +19 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +22 -0
  12. package/src/layouts/PreviewLayout.astro +117 -0
  13. package/src/pages/[locale]/[slug].astro +146 -0
  14. package/src/pages/[locale].astro +251 -0
  15. package/src/pages/index.astro +4 -0
  16. package/src/tests/faq_count.test.ts +24 -0
  17. package/src/tests/mocks/astro_mock.js +2 -0
  18. package/src/tests/seo_length.test.ts +57 -0
  19. package/src/tests/tool_validation.test.ts +145 -0
  20. package/src/tool/bortleVisualizer/bibliography.astro +14 -0
  21. package/src/tool/bortleVisualizer/component.astro +491 -0
  22. package/src/tool/bortleVisualizer/i18n/en.ts +153 -0
  23. package/src/tool/bortleVisualizer/i18n/es.ts +161 -0
  24. package/src/tool/bortleVisualizer/i18n/fr.ts +153 -0
  25. package/src/tool/bortleVisualizer/index.ts +41 -0
  26. package/src/tool/bortleVisualizer/logic.ts +118 -0
  27. package/src/tool/bortleVisualizer/seo.astro +61 -0
  28. package/src/tool/bortleVisualizer/style.css +5 -0
  29. package/src/tool/deepSpaceScope/bibliography.astro +14 -0
  30. package/src/tool/deepSpaceScope/component.astro +849 -0
  31. package/src/tool/deepSpaceScope/i18n/en.ts +157 -0
  32. package/src/tool/deepSpaceScope/i18n/es.ts +157 -0
  33. package/src/tool/deepSpaceScope/i18n/fr.ts +157 -0
  34. package/src/tool/deepSpaceScope/index.ts +48 -0
  35. package/src/tool/deepSpaceScope/logic.ts +41 -0
  36. package/src/tool/deepSpaceScope/seo.astro +61 -0
  37. package/src/tool/deepSpaceScope/style.css +5 -0
  38. package/src/tool/starExposureCalculator/bibliography.astro +14 -0
  39. package/src/tool/starExposureCalculator/component.astro +562 -0
  40. package/src/tool/starExposureCalculator/i18n/en.ts +163 -0
  41. package/src/tool/starExposureCalculator/i18n/es.ts +163 -0
  42. package/src/tool/starExposureCalculator/i18n/fr.ts +158 -0
  43. package/src/tool/starExposureCalculator/index.ts +53 -0
  44. package/src/tool/starExposureCalculator/logic.ts +49 -0
  45. package/src/tool/starExposureCalculator/seo.astro +61 -0
  46. package/src/tool/starExposureCalculator/style.css +5 -0
  47. package/src/tool/telescopeResolution/bibliography.astro +14 -0
  48. package/src/tool/telescopeResolution/component.astro +556 -0
  49. package/src/tool/telescopeResolution/i18n/en.ts +168 -0
  50. package/src/tool/telescopeResolution/i18n/es.ts +163 -0
  51. package/src/tool/telescopeResolution/i18n/fr.ts +168 -0
  52. package/src/tool/telescopeResolution/index.ts +52 -0
  53. package/src/tool/telescopeResolution/logic.ts +39 -0
  54. package/src/tool/telescopeResolution/seo.astro +61 -0
  55. package/src/tool/telescopeResolution/style.css +5 -0
  56. package/src/tools.ts +19 -0
  57. package/src/types.ts +71 -0
@@ -0,0 +1,849 @@
1
+ ---
2
+ import type { DeepSpaceScopeUI } from "./index";
3
+ import type { KnownLocale } from "../../types";
4
+ import { Icon } from "astro-icon/components";
5
+
6
+ interface Props {
7
+ ui: DeepSpaceScopeUI;
8
+ locale?: KnownLocale;
9
+ }
10
+
11
+ const { ui } = Astro.props;
12
+ ---
13
+
14
+ <div class="scope-container" id="scope-container" aria-label={ui.toolTitle}>
15
+ <div id="world-layer" class="world-layer">
16
+ <canvas id="sky-canvas" class="sky-canvas"></canvas>
17
+ <div id="objects-layer" class="objects-layer"></div>
18
+ </div>
19
+
20
+ <div class="scope-vignette"></div>
21
+ <div id="pollution-layer" class="scope-pollution-layer"></div>
22
+
23
+ <div class="scope-drag-hint">
24
+ <span class="hint-mobile">{ui.dragHintMobile}</span>
25
+ <span class="hint-desktop">{ui.dragHint}</span>
26
+ </div>
27
+
28
+ <div id="info-modal" class="scope-modal modal-hidden" aria-hidden="true">
29
+ <div class="scope-modal-content" id="modal-content">
30
+ <div class="modal-header">
31
+ <div class="modal-header-info">
32
+ <div id="modal-icon" class="modal-icon"></div>
33
+ <div>
34
+ <h3 id="modal-title" class="modal-title">Object Name</h3>
35
+ <p id="modal-type" class="modal-type">Type</p>
36
+ </div>
37
+ </div>
38
+ <button id="close-modal" class="modal-close" aria-label="Cerrar">
39
+ <Icon name="mdi:close" />
40
+ </button>
41
+ </div>
42
+ <div class="modal-body">
43
+ <div class="modal-stat">
44
+ <span class="modal-stat-label">{ui.magnitudeLabel}</span>
45
+ <span id="modal-mag" class="modal-stat-value">1.5</span>
46
+ </div>
47
+ <div class="modal-stat">
48
+ <span class="modal-stat-label">{ui.coordinatesLabel}</span>
49
+ <div class="modal-coords">
50
+ <div id="modal-az" class="modal-coord">Az: 120°</div>
51
+ <div id="modal-alt" class="modal-coord modal-coord-secondary">
52
+ Alt: 45°
53
+ </div>
54
+ </div>
55
+ </div>
56
+ <p id="modal-desc" class="modal-desc">Description...</p>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="scope-controls-wrapper">
62
+ <div class="scope-controls">
63
+ <div class="scope-control-grid">
64
+ <div class="scope-control-group">
65
+ <div class="scope-control-header">
66
+ <label for="aperture" class="scope-control-label scope-label-cyan">
67
+ <Icon name="mdi:telescope" />
68
+ {ui.apertureLabel}
69
+ </label>
70
+ <span id="aperture-display" class="scope-control-value">200mm</span>
71
+ </div>
72
+ <input
73
+ type="range"
74
+ id="aperture"
75
+ min="0"
76
+ max="100"
77
+ step="1"
78
+ value="50"
79
+ class="scope-range scope-range-cyan"
80
+ aria-label={ui.apertureLabel}
81
+ />
82
+ </div>
83
+
84
+ <div class="scope-limit-display">
85
+ <div class="scope-limit-label">{ui.limitMagLabel}</div>
86
+ <div id="limit-mag" class="scope-limit-value">12.5</div>
87
+ <div class="scope-azimuth" id="azimuth-display">
88
+ {ui.azimuthLabel}: 0°
89
+ </div>
90
+ </div>
91
+
92
+ <div class="scope-control-group">
93
+ <div class="scope-control-header">
94
+ <label for="bortle" class="scope-control-label scope-label-amber">
95
+ <Icon name="mdi:weather-night" />
96
+ {ui.bortleLabel}
97
+ </label>
98
+ <span id="bortle-display" class="scope-control-value">Class 5</span>
99
+ </div>
100
+ <input
101
+ type="range"
102
+ id="bortle"
103
+ min="1"
104
+ max="9"
105
+ step="1"
106
+ value="5"
107
+ class="scope-range scope-range-amber"
108
+ aria-label={ui.bortleLabel}
109
+ />
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <div class="scope-legend">
115
+ <span class="legend-item legend-planet">
116
+ <Icon name="mdi:circle" />
117
+ {ui.planetLabel}
118
+ </span>
119
+ <span class="legend-item legend-star">
120
+ <Icon name="mdi:star-four-points" />
121
+ {ui.starLabel}
122
+ </span>
123
+ <span class="legend-item legend-deep">
124
+ <Icon name="mdi:creation" />
125
+ {ui.deepSpaceLabel}
126
+ </span>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ <script>
132
+ import {
133
+ DEEP_SPACE_OBJECTS,
134
+ calculateLimitingMagnitude,
135
+ apertureSliderToMm,
136
+ } from "./logic";
137
+
138
+ const container = document.getElementById("scope-container");
139
+ const canvas = document.getElementById("sky-canvas") as HTMLCanvasElement;
140
+ const ctx = canvas?.getContext("2d");
141
+ const objectsLayer = document.getElementById("objects-layer");
142
+ const apertureInput = document.getElementById("aperture") as HTMLInputElement;
143
+ const bortleInput = document.getElementById("bortle") as HTMLInputElement;
144
+ const pollutionLayer = document.getElementById("pollution-layer");
145
+ const limitMagDisplay = document.getElementById("limit-mag");
146
+ const azimuthDisplay = document.getElementById("azimuth-display");
147
+
148
+ let panAngle = 0;
149
+ let isDragging = false;
150
+ let startX = 0;
151
+ let startPan = 0;
152
+ let dragDistance = 0;
153
+
154
+ const FOV = 100;
155
+
156
+ let stars: { az: number; alt: number; mag: number }[] = [];
157
+ function initStars() {
158
+ stars = [];
159
+ for (let i = 0; i < 3000; i++) {
160
+ stars.push({
161
+ az: Math.random() * 360,
162
+ alt: Math.random() * 100,
163
+ mag: Math.pow(Math.random(), 0.8) * 15,
164
+ });
165
+ }
166
+ }
167
+
168
+ function initObjectsDOM() {
169
+ if (!objectsLayer) return;
170
+ objectsLayer.innerHTML = "";
171
+ DEEP_SPACE_OBJECTS.forEach((obj, idx) => {
172
+ const el = document.createElement("div");
173
+ el.id = `obj-${idx}`;
174
+ el.className = "scope-object";
175
+ el.dataset.mag = obj.mag.toString();
176
+ el.innerHTML = `
177
+ <div class="scope-obj-icon ${obj.color}">
178
+ <span class="iconify" data-icon="${obj.icon}"></span>
179
+ </div>
180
+ <div class="scope-obj-label">
181
+ <span class="scope-obj-name">${obj.name}</span>
182
+ <span class="scope-obj-mag">Mag ${obj.mag}</span>
183
+ </div>
184
+ `;
185
+ el.addEventListener("click", (e) => {
186
+ e.stopPropagation();
187
+ if (dragDistance < 5) openModal(obj);
188
+ });
189
+ objectsLayer.appendChild(el);
190
+ });
191
+ }
192
+
193
+ const modal = document.getElementById("info-modal");
194
+ const modalContent = document.getElementById("modal-content");
195
+ const closeModalBtn = document.getElementById("close-modal");
196
+
197
+ function openModal(obj: (typeof DEEP_SPACE_OBJECTS)[0]) {
198
+ if (!modal || !modalContent) return;
199
+ document.getElementById("modal-title")!.innerText = obj.name;
200
+ document.getElementById("modal-type")!.innerText = obj.type;
201
+ document.getElementById("modal-mag")!.innerText = obj.mag.toString();
202
+ document.getElementById("modal-az")!.innerText = `Azimuth: ${obj.az}°`;
203
+ document.getElementById("modal-alt")!.innerText = `Altitud: ${obj.alt}°`;
204
+ const iconEl = document.getElementById("modal-icon");
205
+ if (iconEl) {
206
+ iconEl.innerHTML = `<span class="iconify ${obj.color}" data-icon="${obj.icon}" style="font-size:2.5rem"></span>`;
207
+ }
208
+ const descEl = document.getElementById("modal-desc");
209
+ if (descEl) {
210
+ descEl.innerText =
211
+ obj.desc ||
212
+ `Un objeto fascinante del espacio profundo. Magnitud ${obj.mag}.`;
213
+ }
214
+ modal.classList.remove("modal-hidden");
215
+ modalContent.classList.remove("modal-scale-out");
216
+ modalContent.classList.add("modal-scale-in");
217
+ }
218
+
219
+ function closeModal() {
220
+ if (!modal || !modalContent) return;
221
+ modal.classList.add("modal-hidden");
222
+ modalContent.classList.remove("modal-scale-in");
223
+ modalContent.classList.add("modal-scale-out");
224
+ }
225
+
226
+ closeModalBtn?.addEventListener("click", closeModal);
227
+ modal?.addEventListener("click", (e) => {
228
+ if (e.target === modal) closeModal();
229
+ });
230
+
231
+ function getShortestDelta(target: number, current: number) {
232
+ const delta = target - current;
233
+ return ((delta + 540) % 360) - 180;
234
+ }
235
+
236
+ function updateDisplays(limit: number, apertureMM: number, bortle: number) {
237
+ document.getElementById("aperture-display")!.innerText = `${apertureMM}mm`;
238
+ document.getElementById("bortle-display")!.innerText = `Clase ${bortle}`;
239
+ if (limitMagDisplay) limitMagDisplay.innerText = limit.toFixed(1);
240
+ if (azimuthDisplay)
241
+ azimuthDisplay.innerText = `AZ: ${Math.round(panAngle)}°`;
242
+ if (pollutionLayer) {
243
+ const opacity = (bortle - 1) * 0.08;
244
+ pollutionLayer.style.backgroundColor = `rgba(217, 119, 6, ${opacity})`;
245
+ }
246
+ }
247
+
248
+ function renderStars(w: number, h: number, limit: number) {
249
+ if (!ctx) return;
250
+ const ppd = w / FOV;
251
+ stars.forEach((s) => {
252
+ if (s.mag > limit) return;
253
+ const delta = getShortestDelta(s.az, panAngle);
254
+ if (Math.abs(delta) >= FOV / 2 + 10) return;
255
+ const x = w / 2 + delta * ppd;
256
+ const y = h - (s.alt / 100) * h;
257
+ const margin = limit - s.mag;
258
+ const alpha = Math.min(1, margin * 0.4);
259
+ const size = Math.max(0.6, 3 - s.mag / 4);
260
+ ctx.fillStyle = `rgba(255,255,255, ${alpha})`;
261
+ ctx.beginPath();
262
+ ctx.arc(x, y, size, 0, Math.PI * 2);
263
+ ctx.fill();
264
+ });
265
+ }
266
+
267
+ function updateObjectElement(
268
+ el: HTMLElement,
269
+ obj: (typeof DEEP_SPACE_OBJECTS)[0],
270
+ limit: number,
271
+ ) {
272
+ const delta = getShortestDelta(obj.az, panAngle);
273
+ if (Math.abs(delta) >= FOV / 2 + 20) {
274
+ el.style.display = "none";
275
+ return;
276
+ }
277
+ el.style.display = "flex";
278
+ el.style.left = `${50 + (delta / (FOV / 2)) * 50}%`;
279
+ el.style.top = `${100 - obj.alt}%`;
280
+ const label = el.querySelector(".scope-obj-label") as HTMLElement | null;
281
+ label?.style.setProperty("opacity", limit - obj.mag > 2.0 ? "1" : "0");
282
+ }
283
+
284
+ function renderObjects(limit: number) {
285
+ DEEP_SPACE_OBJECTS.forEach((obj, idx) => {
286
+ const el = document.getElementById(`obj-${idx}`);
287
+ if (!el) return;
288
+ if (obj.mag > limit) {
289
+ el.style.display = "none";
290
+ return;
291
+ }
292
+ updateObjectElement(el, obj, limit);
293
+ });
294
+ }
295
+
296
+ function render() {
297
+ if (!ctx || !canvas) return;
298
+ const w = (canvas.width = canvas.offsetWidth);
299
+ const h = (canvas.height = canvas.offsetHeight);
300
+ ctx.clearRect(0, 0, w, h);
301
+ const sliderVal = parseInt(apertureInput.value);
302
+ const bortle = parseInt(bortleInput.value);
303
+ const limit = calculateLimitingMagnitude(sliderVal, bortle);
304
+ const apertureMM = apertureSliderToMm(sliderVal);
305
+ updateDisplays(limit, apertureMM, bortle);
306
+ renderStars(w, h, limit);
307
+ renderObjects(limit);
308
+ }
309
+
310
+ container?.addEventListener("mousedown", (e) => {
311
+ const target = e.target as HTMLElement;
312
+ if (target.tagName === "INPUT" || target.tagName === "BUTTON" || target.closest(".scope-controls-wrapper")) return;
313
+ isDragging = true;
314
+ startX = e.clientX;
315
+ startPan = panAngle;
316
+ dragDistance = 0;
317
+ container.style.cursor = "grabbing";
318
+ });
319
+
320
+ window.addEventListener("mousemove", (e) => {
321
+ if (!isDragging || !container) return;
322
+ const dx = e.clientX - startX;
323
+ dragDistance += Math.abs(e.movementX);
324
+ const degreesPerPixel = FOV / container.offsetWidth;
325
+ let newPan = startPan - dx * degreesPerPixel;
326
+ if (newPan < 0) newPan += 360;
327
+ if (newPan >= 360) newPan -= 360;
328
+ panAngle = newPan;
329
+ requestAnimationFrame(render);
330
+ });
331
+
332
+ window.addEventListener("mouseup", () => {
333
+ isDragging = false;
334
+ if (container) container.style.cursor = "grab";
335
+ });
336
+
337
+ container?.addEventListener("touchstart", (e) => {
338
+ const target = e.target as HTMLElement;
339
+ if (target.tagName === "INPUT" || target.closest(".scope-controls-wrapper")) return;
340
+ isDragging = true;
341
+ startX = e.touches[0]!.clientX;
342
+ startPan = panAngle;
343
+ dragDistance = 0;
344
+ });
345
+
346
+ window.addEventListener("touchmove", (e) => {
347
+ if (!isDragging || !container) return;
348
+ const dx = e.touches[0].clientX - startX;
349
+ dragDistance += Math.abs(dx);
350
+ const degreesPerPixel = FOV / container.offsetWidth;
351
+ let newPan = startPan - dx * degreesPerPixel;
352
+ if (newPan < 0) newPan += 360;
353
+ if (newPan >= 360) newPan -= 360;
354
+ panAngle = newPan;
355
+ requestAnimationFrame(render);
356
+ });
357
+
358
+ window.addEventListener("touchend", () => (isDragging = false));
359
+
360
+ const iconifyScript = document.createElement("script");
361
+ iconifyScript.src = "https://code.iconify.design/3/3.1.0/iconify.min.js";
362
+ document.head.appendChild(iconifyScript);
363
+
364
+ initStars();
365
+ initObjectsDOM();
366
+ apertureInput.addEventListener("input", () => requestAnimationFrame(render));
367
+ bortleInput.addEventListener("input", () => requestAnimationFrame(render));
368
+ window.addEventListener("resize", () => requestAnimationFrame(render));
369
+ setTimeout(render, 50);
370
+ </script>
371
+
372
+ <style is:global>
373
+ .scope-container {
374
+ --on-dark: #fff;
375
+ --planet-tint: #fef9c3;
376
+
377
+ position: relative;
378
+ width: 100%;
379
+ height: 600px;
380
+ background: var(--color-bg-deep, #000);
381
+ border-radius: 1.5rem;
382
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
383
+ overflow: hidden;
384
+ border: 1px solid rgba(100, 116, 139, 0.3);
385
+ cursor: grab;
386
+ touch-action: none;
387
+ user-select: none;
388
+ }
389
+
390
+ @media (min-width: 768px) {
391
+ .scope-container {
392
+ height: 800px;
393
+ }
394
+ }
395
+
396
+ .scope-container:active {
397
+ cursor: grabbing;
398
+ }
399
+
400
+ .world-layer {
401
+ position: absolute;
402
+ inset: 0;
403
+ width: 100%;
404
+ height: 100%;
405
+ will-change: transform;
406
+ }
407
+
408
+ .sky-canvas {
409
+ position: absolute;
410
+ inset: 0;
411
+ width: 100%;
412
+ height: 100%;
413
+ z-index: 0;
414
+ }
415
+
416
+ .objects-layer {
417
+ position: absolute;
418
+ inset: 0;
419
+ width: 100%;
420
+ height: 100%;
421
+ z-index: 10;
422
+ pointer-events: none;
423
+ }
424
+
425
+ .scope-vignette {
426
+ position: absolute;
427
+ inset: 0;
428
+ background: radial-gradient(
429
+ circle at center,
430
+ transparent 0%,
431
+ rgba(0, 0, 0, 0.5) 100%
432
+ );
433
+ pointer-events: none;
434
+ z-index: 20;
435
+ }
436
+
437
+ .scope-pollution-layer {
438
+ position: absolute;
439
+ inset: 0;
440
+ z-index: 10;
441
+ pointer-events: none;
442
+ mix-blend-mode: screen;
443
+ background: transparent;
444
+ transition: background-color 0.5s ease;
445
+ }
446
+
447
+ .scope-drag-hint {
448
+ position: absolute;
449
+ top: 1.5rem;
450
+ left: 50%;
451
+ transform: translateX(-50%);
452
+ z-index: 30;
453
+ opacity: 0.7;
454
+ pointer-events: none;
455
+ background: rgba(0, 0, 0, 0.5);
456
+ padding: 0.25rem 1rem;
457
+ border-radius: 9999px;
458
+ font-size: 0.625rem;
459
+ color: var(--on-dark);
460
+ text-transform: uppercase;
461
+ letter-spacing: 0.1em;
462
+ border: 1px solid rgba(255, 255, 255, 0.1);
463
+ backdrop-filter: blur(12px);
464
+ white-space: nowrap;
465
+ }
466
+
467
+ .hint-mobile {
468
+ display: inline;
469
+ }
470
+
471
+ .hint-desktop {
472
+ display: none;
473
+ }
474
+
475
+ @media (min-width: 768px) {
476
+ .hint-mobile {
477
+ display: none;
478
+ }
479
+ .hint-desktop {
480
+ display: inline;
481
+ }
482
+ }
483
+
484
+ .scope-object {
485
+ position: absolute;
486
+ transform: translate(-50%, -50%);
487
+ display: flex;
488
+ flex-direction: column;
489
+ align-items: center;
490
+ justify-content: center;
491
+ transition: opacity 0.3s ease;
492
+ will-change: transform;
493
+ pointer-events: auto;
494
+ cursor: pointer;
495
+ }
496
+
497
+ .scope-object:hover .scope-obj-icon {
498
+ transform: scale(1.25);
499
+ }
500
+
501
+ .scope-obj-icon {
502
+ font-size: 2rem;
503
+ transition: transform 0.3s ease;
504
+ filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.4));
505
+ }
506
+
507
+ @media (min-width: 768px) {
508
+ .scope-obj-icon {
509
+ font-size: 3rem;
510
+ }
511
+ }
512
+
513
+ .scope-obj-label {
514
+ margin-top: 0.5rem;
515
+ display: flex;
516
+ flex-direction: column;
517
+ align-items: center;
518
+ opacity: 0;
519
+ transition: opacity 0.3s ease;
520
+ }
521
+
522
+ .scope-obj-name {
523
+ font-size: 0.625rem;
524
+ font-weight: 700;
525
+ color: var(--on-dark);
526
+ background: rgba(0, 0, 0, 0.6);
527
+ padding: 0.125rem 0.5rem;
528
+ border-radius: 0.25rem;
529
+ backdrop-filter: blur(8px);
530
+ border: 1px solid rgba(255, 255, 255, 0.1);
531
+ white-space: nowrap;
532
+ }
533
+
534
+ .scope-obj-mag {
535
+ font-size: 0.5rem;
536
+ color: rgba(148, 163, 184, 0.8);
537
+ background: rgba(0, 0, 0, 0.8);
538
+ padding: 0 0.25rem;
539
+ border-radius: 0.2rem;
540
+ margin-top: 0.125rem;
541
+ }
542
+
543
+ .scope-modal {
544
+ position: absolute;
545
+ inset: 0;
546
+ z-index: 50;
547
+ display: flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ background: rgba(0, 0, 0, 0.6);
551
+ backdrop-filter: blur(4px);
552
+ transition: opacity 0.3s ease;
553
+ }
554
+
555
+ .scope-modal.modal-hidden {
556
+ opacity: 0;
557
+ pointer-events: none;
558
+ }
559
+
560
+ .scope-modal-content {
561
+ background: #0f172a;
562
+ border: 1px solid rgba(255, 255, 255, 0.2);
563
+ padding: 1.5rem;
564
+ border-radius: 1rem;
565
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8);
566
+ max-width: 22rem;
567
+ width: calc(100% - 3rem);
568
+ transition: transform 0.3s ease;
569
+ }
570
+
571
+ .modal-scale-in {
572
+ transform: scale(1);
573
+ }
574
+ .modal-scale-out {
575
+ transform: scale(0.95);
576
+ }
577
+
578
+ .modal-header {
579
+ display: flex;
580
+ justify-content: space-between;
581
+ align-items: flex-start;
582
+ margin-bottom: 1rem;
583
+ }
584
+
585
+ .modal-header-info {
586
+ display: flex;
587
+ align-items: center;
588
+ gap: 0.75rem;
589
+ }
590
+
591
+ .modal-icon {
592
+ font-size: 2.5rem;
593
+ }
594
+
595
+ .modal-title {
596
+ font-size: 1.25rem;
597
+ font-weight: 700;
598
+ color: var(--on-dark);
599
+ margin: 0;
600
+ line-height: 1;
601
+ }
602
+
603
+ .modal-type {
604
+ font-size: 0.625rem;
605
+ color: var(--text-muted, #94a3b8);
606
+ text-transform: uppercase;
607
+ letter-spacing: 0.1em;
608
+ margin: 0.25rem 0 0;
609
+ }
610
+
611
+ .modal-close {
612
+ background: none;
613
+ border: none;
614
+ color: var(--text-muted, #94a3b8);
615
+ cursor: pointer;
616
+ font-size: 1.5rem;
617
+ padding: 0;
618
+ transition: color 0.2s ease;
619
+ }
620
+
621
+ .modal-close:hover {
622
+ color: var(--on-dark);
623
+ }
624
+
625
+ .modal-body {
626
+ display: flex;
627
+ flex-direction: column;
628
+ gap: 0.75rem;
629
+ }
630
+
631
+ .modal-stat {
632
+ display: flex;
633
+ justify-content: space-between;
634
+ align-items: center;
635
+ background: rgba(255, 255, 255, 0.05);
636
+ padding: 0.75rem;
637
+ border-radius: 0.5rem;
638
+ }
639
+
640
+ .modal-stat-label {
641
+ font-size: 0.75rem;
642
+ color: var(--text-muted, #94a3b8);
643
+ text-transform: uppercase;
644
+ }
645
+
646
+ .modal-stat-value {
647
+ color: var(--on-dark);
648
+ font-weight: 700;
649
+ }
650
+
651
+ .modal-coords {
652
+ text-align: right;
653
+ }
654
+
655
+ .modal-coord {
656
+ font-size: 0.75rem;
657
+ color: var(--on-dark);
658
+ }
659
+
660
+ .modal-coord-secondary {
661
+ color: var(--text-muted, #94a3b8);
662
+ }
663
+
664
+ .modal-desc {
665
+ font-size: 0.875rem;
666
+ color: var(--text-muted, #94a3b8);
667
+ line-height: 1.6;
668
+ margin: 0;
669
+ }
670
+
671
+ .scope-controls-wrapper {
672
+ position: absolute;
673
+ bottom: 1.5rem;
674
+ left: 1.5rem;
675
+ right: 1.5rem;
676
+ z-index: 40;
677
+ display: flex;
678
+ flex-direction: column;
679
+ align-items: center;
680
+ gap: 1rem;
681
+ pointer-events: auto;
682
+ }
683
+
684
+ .scope-controls {
685
+ width: 100%;
686
+ max-width: 56rem;
687
+ background: rgba(15, 23, 42, 0.9);
688
+ backdrop-filter: blur(20px);
689
+ border: 1px solid rgba(255, 255, 255, 0.2);
690
+ border-radius: 1rem;
691
+ padding: 1rem;
692
+ box-shadow: 0 0 50px rgba(0, 0, 0, 0.5);
693
+ }
694
+
695
+ @media (min-width: 768px) {
696
+ .scope-controls {
697
+ padding: 1.5rem;
698
+ }
699
+ }
700
+
701
+ .scope-control-grid {
702
+ display: grid;
703
+ grid-template-columns: 1fr;
704
+ gap: 1.5rem;
705
+ align-items: center;
706
+ }
707
+
708
+ @media (min-width: 768px) {
709
+ .scope-control-grid {
710
+ grid-template-columns: 1fr auto 1fr;
711
+ gap: 2rem;
712
+ }
713
+ }
714
+
715
+ .scope-control-group {
716
+ display: flex;
717
+ flex-direction: column;
718
+ gap: 0.75rem;
719
+ }
720
+
721
+ .scope-control-header {
722
+ display: flex;
723
+ justify-content: space-between;
724
+ align-items: center;
725
+ }
726
+
727
+ .scope-control-label {
728
+ font-size: 0.75rem;
729
+ font-weight: 700;
730
+ text-transform: uppercase;
731
+ letter-spacing: 0.05em;
732
+ display: flex;
733
+ align-items: center;
734
+ gap: 0.5rem;
735
+ }
736
+
737
+ .scope-label-cyan {
738
+ color: var(--color-cyan, #06b6d4);
739
+ }
740
+ .scope-label-amber {
741
+ color: var(--color-amber, #f59e0b);
742
+ }
743
+
744
+ .scope-control-value {
745
+ color: var(--on-dark);
746
+ font-size: 0.875rem;
747
+ background: rgba(255, 255, 255, 0.1);
748
+ padding: 0.125rem 0.5rem;
749
+ border-radius: 0.25rem;
750
+ }
751
+
752
+ .scope-range {
753
+ width: 100%;
754
+ height: 0.375rem;
755
+ background: rgba(100, 116, 139, 0.4);
756
+ border-radius: 9999px;
757
+ appearance: none;
758
+ cursor: pointer;
759
+ outline: none;
760
+ }
761
+
762
+ .scope-range-cyan {
763
+ accent-color: var(--color-cyan, #06b6d4);
764
+ }
765
+ .scope-range-amber {
766
+ accent-color: var(--color-amber, #f59e0b);
767
+ }
768
+
769
+ .scope-limit-display {
770
+ display: flex;
771
+ flex-direction: column;
772
+ align-items: center;
773
+ justify-content: center;
774
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
775
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
776
+ padding: 0.5rem 1rem;
777
+ }
778
+
779
+ @media (min-width: 768px) {
780
+ .scope-limit-display {
781
+ border-top: none;
782
+ border-bottom: none;
783
+ border-left: 1px solid rgba(255, 255, 255, 0.1);
784
+ border-right: 1px solid rgba(255, 255, 255, 0.1);
785
+ padding: 0 2rem;
786
+ }
787
+ }
788
+
789
+ .scope-limit-label {
790
+ font-size: 0.625rem;
791
+ color: var(--text-muted, #94a3b8);
792
+ text-transform: uppercase;
793
+ letter-spacing: 0.1em;
794
+ margin-bottom: 0.25rem;
795
+ }
796
+
797
+ .scope-limit-value {
798
+ font-size: 3rem;
799
+ font-weight: 900;
800
+ color: var(--on-dark);
801
+ line-height: 1;
802
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
803
+ }
804
+
805
+ .scope-azimuth {
806
+ font-size: 0.625rem;
807
+ color: var(--color-cyan, #06b6d4);
808
+ opacity: 0.5;
809
+ margin-top: 0.5rem;
810
+ }
811
+
812
+ .scope-legend {
813
+ display: flex;
814
+ gap: 1rem;
815
+ flex-wrap: wrap;
816
+ font-size: 0.625rem;
817
+ font-weight: 700;
818
+ text-transform: uppercase;
819
+ letter-spacing: 0.05em;
820
+ color: var(--text-muted, #64748b);
821
+ background: rgba(0, 0, 0, 0.8);
822
+ padding: 0.5rem 1.5rem;
823
+ border-radius: 9999px;
824
+ backdrop-filter: blur(4px);
825
+ border: 1px solid rgba(255, 255, 255, 0.05);
826
+ }
827
+
828
+ @media (min-width: 768px) {
829
+ .scope-legend {
830
+ gap: 2rem;
831
+ }
832
+ }
833
+
834
+ .legend-item {
835
+ display: flex;
836
+ align-items: center;
837
+ gap: 0.5rem;
838
+ }
839
+
840
+ .legend-planet {
841
+ color: var(--planet-tint);
842
+ }
843
+ .legend-star {
844
+ color: var(--on-dark);
845
+ }
846
+ .legend-deep {
847
+ color: var(--color-indigo, #818cf8);
848
+ }
849
+ </style>