@jjlmoya/utils-drones 1.23.0 → 1.24.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 (35) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -0
  3. package/src/entries.ts +4 -0
  4. package/src/tests/locale_completeness.test.ts +2 -2
  5. package/src/tests/tool_validation.test.ts +2 -2
  6. package/src/tool/fpv-drone-thrust-to-weight-ratio/bibliography.astro +6 -0
  7. package/src/tool/fpv-drone-thrust-to-weight-ratio/bibliography.ts +18 -0
  8. package/src/tool/fpv-drone-thrust-to-weight-ratio/component.astro +203 -0
  9. package/src/tool/fpv-drone-thrust-to-weight-ratio/controller.ts +240 -0
  10. package/src/tool/fpv-drone-thrust-to-weight-ratio/dom-views.ts +148 -0
  11. package/src/tool/fpv-drone-thrust-to-weight-ratio/entry.ts +27 -0
  12. package/src/tool/fpv-drone-thrust-to-weight-ratio/evaluator.ts +30 -0
  13. package/src/tool/fpv-drone-thrust-to-weight-ratio/fpv-drone-thrust-to-weight-ratio.css +404 -0
  14. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/de.ts +208 -0
  15. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/en.ts +208 -0
  16. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/es.ts +208 -0
  17. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/fr.ts +208 -0
  18. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/id.ts +208 -0
  19. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/it.ts +208 -0
  20. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ja.ts +208 -0
  21. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ko.ts +208 -0
  22. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/nl.ts +208 -0
  23. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pl.ts +208 -0
  24. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pt.ts +208 -0
  25. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ru.ts +208 -0
  26. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/sv.ts +208 -0
  27. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/tr.ts +208 -0
  28. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/zh.ts +208 -0
  29. package/src/tool/fpv-drone-thrust-to-weight-ratio/index.ts +10 -0
  30. package/src/tool/fpv-drone-thrust-to-weight-ratio/logic.test.ts +93 -0
  31. package/src/tool/fpv-drone-thrust-to-weight-ratio/logic.ts +147 -0
  32. package/src/tool/fpv-drone-thrust-to-weight-ratio/seo.astro +16 -0
  33. package/src/tool/fpv-drone-thrust-to-weight-ratio/storage.ts +19 -0
  34. package/src/tool/fpv-drone-thrust-to-weight-ratio/ui.ts +58 -0
  35. package/src/tools.ts +3 -0
@@ -0,0 +1,148 @@
1
+ import type { DroneTwrInputs, DroneTwrResults, AgilityTier } from './logic';
2
+ import type { AgilityEvaluation } from './evaluator';
3
+ import type { FpvDroneThrustToWeightRatioUI } from './ui';
4
+
5
+ export interface DOMResultElements {
6
+ twrRatio: HTMLElement;
7
+ hoverThrottle: HTMLElement;
8
+ currentThrust: HTMLElement;
9
+ instantGForce: HTMLElement;
10
+ zeroToHundred: HTMLElement;
11
+ recommendedCamAngle: HTMLElement;
12
+ windResistance: HTMLElement;
13
+ totalMaxThrust: HTMLElement;
14
+ maxPitchAngle: HTMLElement;
15
+ tpaSetting?: HTMLElement;
16
+ dynamicIdleSetting?: HTMLElement;
17
+ propwashRisk?: HTMLElement;
18
+ agilityBadge: HTMLElement;
19
+ statusTitle: HTMLElement;
20
+ statusDesc: HTMLElement;
21
+ svgContainer: HTMLElement;
22
+ stickPercentBadge?: HTMLElement;
23
+ }
24
+
25
+ export function getTierColor(tier: AgilityTier): string {
26
+ if (tier === 'underpowered') return '#ef4444';
27
+ if (tier === 'cinematic') return '#0284c7';
28
+ if (tier === 'freestyle') return '#10b981';
29
+ if (tier === 'acro_pro') return '#8b5cf6';
30
+ return '#f59e0b';
31
+ }
32
+
33
+ function renderDroneVectors(stickPercent: number, tierColor: string, camAngle: number): string {
34
+ const vHeight = Math.max(10, (stickPercent / 100) * 60);
35
+ const opacity = Math.min(1, Math.max(0.25, stickPercent / 100));
36
+ const tiltAngle = (camAngle - 20) * 0.4;
37
+ return `<g transform="translate(130, 115) rotate(${tiltAngle})">
38
+ <polygon points="0,-18 55,${-18 - camAngle * 0.6} 55,${-18 + camAngle * 0.6}" fill="var(--n-primary, #38bdf8)" fill-opacity="0.08" stroke="var(--n-primary, #38bdf8)" stroke-dasharray="2,2" stroke-width="1"/>
39
+ <line x1="-80" y1="0" x2="80" y2="0" stroke="var(--n-svg-stroke, #334155)" stroke-width="4.5" stroke-linecap="round"/>
40
+ <line x1="-35" y1="0" x2="0" y2="-18" stroke="var(--n-primary, #38bdf8)" stroke-width="3"/>
41
+ <circle cx="0" cy="-18" r="8" fill="var(--n-primary, #38bdf8)" fill-opacity="0.35" stroke="var(--n-primary, #38bdf8)" stroke-width="2"/>
42
+ <rect x="-24" y="-8" width="48" height="16" rx="4" fill="var(--n-svg-card, #0f172a)" stroke="var(--n-svg-stroke, #334155)" stroke-width="2"/>
43
+ <circle cx="0" cy="0" r="4" fill="${tierColor}"/>
44
+
45
+ <g transform="translate(-80, 0)">
46
+ <ellipse cx="0" cy="-3" rx="20" ry="4" fill="${tierColor}" fill-opacity="0.2" stroke="${tierColor}" stroke-width="1"/>
47
+ <rect x="-10" y="-4" width="20" height="8" rx="2" fill="var(--n-svg-inner, #1e293b)" stroke="${tierColor}" stroke-width="1.5"/>
48
+ <line x1="0" y1="-4" x2="0" y2="${-4 - vHeight}" stroke="${tierColor}" stroke-width="3.5" stroke-linecap="round" opacity="${opacity}"/>
49
+ <polygon points="-5,${-4 - vHeight + 5} 5,${-4 - vHeight + 5} 0,${-4 - vHeight}" fill="${tierColor}" opacity="${opacity}"/>
50
+ </g>
51
+
52
+ <g transform="translate(80, 0)">
53
+ <ellipse cx="0" cy="-3" rx="20" ry="4" fill="${tierColor}" fill-opacity="0.2" stroke="${tierColor}" stroke-width="1"/>
54
+ <rect x="-10" y="-4" width="20" height="8" rx="2" fill="var(--n-svg-inner, #1e293b)" stroke="${tierColor}" stroke-width="1.5"/>
55
+ <line x1="0" y1="-4" x2="0" y2="${-4 - vHeight}" stroke="${tierColor}" stroke-width="3.5" stroke-linecap="round" opacity="${opacity}"/>
56
+ <polygon points="-5,${-4 - vHeight + 5} 5,${-4 - vHeight + 5} 0,${-4 - vHeight}" fill="${tierColor}" opacity="${opacity}"/>
57
+ </g>
58
+ </g>`;
59
+ }
60
+
61
+ function renderDroneStage(inputs: DroneTwrInputs, results: DroneTwrResults, ui: FpvDroneThrustToWeightRatioUI): string {
62
+ const tierColor = getTierColor(results.agilityTier);
63
+ const vectorsHtml = renderDroneVectors(inputs.throttleStickPercent, tierColor, results.recommendedCamAngleDeg);
64
+ return `<g transform="translate(15, 15)">
65
+ <rect x="0" y="0" width="250" height="180" rx="14" fill="var(--n-svg-card, #0f172a)" stroke="var(--n-svg-stroke, #334155)" stroke-width="2" />
66
+ <text x="15" y="24" fill="var(--n-text-muted, #94a3b8)" font-size="10" font-weight="700" letter-spacing="0.5">${ui.hudVectorPowerLabel.toUpperCase()}</text>
67
+ <text x="235" y="24" fill="${tierColor}" font-size="11" font-weight="800" text-anchor="end">${results.instantGForce.toFixed(2)}G</text>
68
+ ${vectorsHtml}
69
+ <text x="125" y="152" fill="var(--n-text, #f8fafc)" font-size="15" font-weight="900" text-anchor="middle">${results.currentThrustGrams.toLocaleString()} g</text>
70
+ <text x="125" y="168" fill="var(--n-text-muted, #94a3b8)" font-size="9" font-weight="600" text-anchor="middle">STICK @ ${inputs.throttleStickPercent}% | CAM ${results.recommendedCamAngleDeg}°</text>
71
+ </g>`;
72
+ }
73
+
74
+ function renderCurvePoints(results: DroneTwrResults, inputs: DroneTwrInputs): string {
75
+ const hoverX = 25 + (results.hoverThrottlePercent / 100) * 190;
76
+ const hoverY = 145 - Math.pow(results.hoverThrottlePercent / 100, 1.8) * 105;
77
+ const stickX = 25 + (inputs.throttleStickPercent / 100) * 190;
78
+ const stickY = 145 - Math.pow(inputs.throttleStickPercent / 100, 1.8) * 105;
79
+ return `<g>
80
+ <rect x="${25 + 0.25 * 190}" y="40" width="${0.35 * 190}" height="105" fill="var(--n-primary, #38bdf8)" fill-opacity="0.06"/>
81
+ <rect x="${25 + 0.70 * 190}" y="40" width="${0.30 * 190}" height="105" fill="#f59e0b" fill-opacity="0.08"/>
82
+ <line x1="${hoverX}" y1="40" x2="${hoverX}" y2="145" stroke="#38bdf8" stroke-width="1.5" stroke-dasharray="3,3" opacity="0.7"/>
83
+ <circle cx="${hoverX}" cy="${hoverY}" r="4.5" fill="#38bdf8" stroke="#fff" stroke-width="1.5"/>
84
+ <circle cx="${stickX}" cy="${stickY}" r="6" fill="#f59e0b" stroke="#fff" stroke-width="2"/>
85
+ <circle cx="${stickX}" cy="${stickY}" r="11" fill="none" stroke="#f59e0b" stroke-width="1.5" opacity="0.4"/>
86
+ </g>`;
87
+ }
88
+
89
+ function renderCurveStage(inputs: DroneTwrInputs, results: DroneTwrResults, ui: FpvDroneThrustToWeightRatioUI): string {
90
+ const pointsHtml = renderCurvePoints(results, inputs);
91
+ return `<g transform="translate(280, 15)">
92
+ <rect x="0" y="0" width="265" height="180" rx="14" fill="var(--n-svg-card, #0f172a)" stroke="var(--n-svg-stroke, #334155)" stroke-width="2" />
93
+ <text x="15" y="24" fill="var(--n-text-muted, #94a3b8)" font-size="10" font-weight="700" letter-spacing="0.5">${ui.hudThrustCurveTitle.toUpperCase()}</text>
94
+ <path d="M 25 145 L 215 145" stroke="var(--n-svg-stroke, #334155)" stroke-width="1.5" />
95
+ <path d="M 25 40 L 25 145" stroke="var(--n-svg-stroke, #334155)" stroke-width="1.5" />
96
+ <path d="M 25 145 Q 120 135 215 40" fill="none" stroke="var(--n-primary, #38bdf8)" stroke-width="3.5" stroke-linecap="round" />
97
+ ${pointsHtml}
98
+ <text x="25" y="165" fill="var(--n-text-muted, #94a3b8)" font-size="8" font-weight="700">0% IDLE</text>
99
+ <text x="120" y="165" fill="#38bdf8" font-size="8" font-weight="700" text-anchor="middle">HOVER ${results.hoverThrottlePercent}%</text>
100
+ <text x="215" y="165" fill="#f59e0b" font-size="8" font-weight="700" text-anchor="end">PUNCH ${results.twrRatio.toFixed(1)}:1</text>
101
+ </g>`;
102
+ }
103
+
104
+ export function renderTwrSVG(inputs: DroneTwrInputs, results: DroneTwrResults, ui: FpvDroneThrustToWeightRatioUI): string {
105
+ const droneSvg = renderDroneStage(inputs, results, ui);
106
+ const curveSvg = renderCurveStage(inputs, results, ui);
107
+ return `<svg viewBox="0 0 560 210" width="100%" height="210" xmlns="http://www.w3.org/2000/svg">
108
+ ${droneSvg}
109
+ ${curveSvg}
110
+ </svg>`;
111
+ }
112
+
113
+ function updateCoreTelemetry(elements: DOMResultElements, results: DroneTwrResults): void {
114
+ if (elements.twrRatio) elements.twrRatio.textContent = `${results.twrRatio.toFixed(2)} : 1`;
115
+ if (elements.hoverThrottle) elements.hoverThrottle.textContent = `${results.hoverThrottlePercent.toFixed(1)} %`;
116
+ if (elements.currentThrust) elements.currentThrust.textContent = `${results.currentThrustGrams.toLocaleString()} g`;
117
+ if (elements.instantGForce) elements.instantGForce.textContent = `${results.instantGForce.toFixed(2)} G`;
118
+ }
119
+
120
+ function updateFlightDynamics(elements: DOMResultElements, results: DroneTwrResults): void {
121
+ if (elements.zeroToHundred) elements.zeroToHundred.textContent = `${results.zeroToHundredTimeSec.toFixed(2)} s`;
122
+ if (elements.recommendedCamAngle) elements.recommendedCamAngle.textContent = `${results.recommendedCamAngleDeg}°`;
123
+ if (elements.windResistance) elements.windResistance.textContent = `${results.windResistanceKmh} km/h`;
124
+ if (elements.totalMaxThrust) elements.totalMaxThrust.textContent = `${results.totalMaxThrustGrams.toLocaleString()} g`;
125
+ if (elements.maxPitchAngle) elements.maxPitchAngle.textContent = `${results.maxPitchAngleDeg}°`;
126
+ if (elements.stickPercentBadge) elements.stickPercentBadge.textContent = `${results.currentThrottlePercent}%`;
127
+ }
128
+
129
+ function updateTuningMetrics(elements: DOMResultElements, results: DroneTwrResults): void {
130
+ if (elements.tpaSetting) elements.tpaSetting.textContent = results.tuneAdvice.tpaSetting;
131
+ if (elements.dynamicIdleSetting) elements.dynamicIdleSetting.textContent = results.tuneAdvice.dynamicIdleSetting;
132
+ if (elements.propwashRisk) elements.propwashRisk.textContent = results.tuneAdvice.propwashRisk;
133
+ }
134
+
135
+ export function updateDOMMetrics(elements: DOMResultElements, results: DroneTwrResults): void {
136
+ updateCoreTelemetry(elements, results);
137
+ updateFlightDynamics(elements, results);
138
+ updateTuningMetrics(elements, results);
139
+ }
140
+
141
+ export function updateDOMSafety(elements: DOMResultElements, evalData: AgilityEvaluation): void {
142
+ if (elements.agilityBadge) {
143
+ elements.agilityBadge.className = `sc-safety-badge ${evalData.badgeClass}`;
144
+ elements.agilityBadge.textContent = evalData.title;
145
+ }
146
+ if (elements.statusTitle) elements.statusTitle.textContent = evalData.title;
147
+ if (elements.statusDesc) elements.statusDesc.textContent = evalData.description;
148
+ }
@@ -0,0 +1,27 @@
1
+ import type { DronesToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { FpvDroneThrustToWeightRatioUI } from './ui';
3
+
4
+ export type { FpvDroneThrustToWeightRatioUI };
5
+ export type FpvDroneThrustToWeightRatioLocaleContent = ToolLocaleContent<FpvDroneThrustToWeightRatioUI>;
6
+
7
+ export const fpvDroneThrustToWeightRatio: DronesToolEntry<FpvDroneThrustToWeightRatioUI> = {
8
+ id: 'fpv-drone-thrust-to-weight-ratio',
9
+ icons: { bg: 'mdi:quadcopter', fg: 'mdi:gauge' },
10
+ i18n: {
11
+ es: () => import('./i18n/es').then((m) => m.content),
12
+ en: () => import('./i18n/en').then((m) => m.content),
13
+ fr: () => import('./i18n/fr').then((m) => m.content),
14
+ de: () => import('./i18n/de').then((m) => m.content),
15
+ it: () => import('./i18n/it').then((m) => m.content),
16
+ pt: () => import('./i18n/pt').then((m) => m.content),
17
+ nl: () => import('./i18n/nl').then((m) => m.content),
18
+ pl: () => import('./i18n/pl').then((m) => m.content),
19
+ ru: () => import('./i18n/ru').then((m) => m.content),
20
+ ja: () => import('./i18n/ja').then((m) => m.content),
21
+ ko: () => import('./i18n/ko').then((m) => m.content),
22
+ zh: () => import('./i18n/zh').then((m) => m.content),
23
+ tr: () => import('./i18n/tr').then((m) => m.content),
24
+ sv: () => import('./i18n/sv').then((m) => m.content),
25
+ id: () => import('./i18n/id').then((m) => m.content),
26
+ },
27
+ };
@@ -0,0 +1,30 @@
1
+ import type { AgilityTier } from './logic';
2
+ import type { FpvDroneThrustToWeightRatioUI } from './ui';
3
+
4
+ export interface AgilityEvaluation {
5
+ tier: AgilityTier;
6
+ badgeClass: string;
7
+ title: string;
8
+ description: string;
9
+ }
10
+
11
+ const TIER_CONFIG: Record<AgilityTier, { badgeClass: string; titleKey: keyof FpvDroneThrustToWeightRatioUI; descKey: keyof FpvDroneThrustToWeightRatioUI }> = {
12
+ underpowered: { badgeClass: 'status-danger', titleKey: 'tierUnderpoweredTitle', descKey: 'tierUnderpoweredDesc' },
13
+ cinematic: { badgeClass: 'status-info', titleKey: 'tierCinematicTitle', descKey: 'tierCinematicDesc' },
14
+ freestyle: { badgeClass: 'status-optimal', titleKey: 'tierFreestyleTitle', descKey: 'tierFreestyleDesc' },
15
+ acro_pro: { badgeClass: 'status-accent', titleKey: 'tierAcroProTitle', descKey: 'tierAcroProDesc' },
16
+ racing_extreme: { badgeClass: 'status-warning', titleKey: 'tierRacingExtremeTitle', descKey: 'tierRacingExtremeDesc' },
17
+ };
18
+
19
+ export function evaluateAgilityPresentation(
20
+ tier: AgilityTier,
21
+ ui: FpvDroneThrustToWeightRatioUI
22
+ ): AgilityEvaluation {
23
+ const cfg = TIER_CONFIG[tier] || TIER_CONFIG.freestyle;
24
+ return {
25
+ tier,
26
+ badgeClass: cfg.badgeClass,
27
+ title: ui[cfg.titleKey],
28
+ description: ui[cfg.descKey],
29
+ };
30
+ }
@@ -0,0 +1,404 @@
1
+ :root {
2
+ --n-bg: #f8fafc;
3
+ --n-surface: #fff;
4
+ --n-card-bg: rgba(255, 255, 255, 0.95);
5
+ --n-border: #cbd5e1;
6
+ --n-text: #0f172a;
7
+ --n-text-muted: #475569;
8
+ --n-primary: #0284c7;
9
+ --n-primary-hover: #0369a1;
10
+ --n-accent: #8b5cf6;
11
+ --n-accent-cyan: #06b6d4;
12
+ --n-success: #10b981;
13
+ --n-warning: #f59e0b;
14
+ --n-danger: #ef4444;
15
+ --n-info: #0284c7;
16
+ --n-svg-card: #f1f5f9;
17
+ --n-svg-inner: #fff;
18
+ --n-svg-stroke: #cbd5e1;
19
+ --n-radius: 16px;
20
+ --n-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.08);
21
+ }
22
+
23
+ .theme-dark {
24
+ --n-bg: #070b12;
25
+ --n-surface: #0f172a;
26
+ --n-card-bg: rgba(15, 23, 42, 0.92);
27
+ --n-border: #1e293b;
28
+ --n-text: #f8fafc;
29
+ --n-text-muted: #94a3b8;
30
+ --n-primary: #38bdf8;
31
+ --n-primary-hover: #7dd3fc;
32
+ --n-accent: #a78bfa;
33
+ --n-accent-cyan: #22d3ee;
34
+ --n-success: #34d399;
35
+ --n-warning: #fbbf24;
36
+ --n-danger: #f87171;
37
+ --n-info: #38bdf8;
38
+ --n-svg-card: #0b1120;
39
+ --n-svg-inner: #1e293b;
40
+ --n-svg-stroke: #334155;
41
+ --n-shadow: 0 14px 35px -5px rgba(0, 0, 0, 0.6);
42
+ }
43
+
44
+ .sc-fpv-cockpit {
45
+ display: grid;
46
+ grid-template-columns: 1fr;
47
+ gap: 2rem;
48
+ background-color: var(--n-card-bg);
49
+ border: 1px solid var(--n-border);
50
+ border-radius: var(--n-radius);
51
+ padding: 2rem;
52
+ box-shadow: var(--n-shadow);
53
+ backdrop-filter: blur(14px);
54
+ color: var(--n-text);
55
+ margin-bottom: 2rem;
56
+ }
57
+
58
+ @media (min-width: 1024px) {
59
+ .sc-fpv-cockpit {
60
+ grid-template-columns: 380px 1fr;
61
+ }
62
+ }
63
+
64
+ .sc-cockpit-sidebar {
65
+ display: flex;
66
+ flex-direction: column;
67
+ gap: 1.5rem;
68
+ }
69
+
70
+ .sc-cockpit-title {
71
+ font-size: 1.05rem;
72
+ font-weight: 700;
73
+ color: var(--n-text);
74
+ margin-bottom: 0.8rem;
75
+ letter-spacing: -0.01em;
76
+ }
77
+
78
+ .sc-preset-group {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ gap: 0.5rem;
82
+ }
83
+
84
+ .sc-preset-chip {
85
+ background-color: var(--n-surface);
86
+ border: 1px solid var(--n-border);
87
+ color: var(--n-text);
88
+ border-radius: 8px;
89
+ padding: 0.45rem 0.85rem;
90
+ font-size: 0.85rem;
91
+ font-weight: 600;
92
+ cursor: pointer;
93
+ transition: all 0.2s ease;
94
+ }
95
+
96
+ .sc-preset-chip:hover {
97
+ border-color: var(--n-primary);
98
+ color: var(--n-primary);
99
+ }
100
+
101
+ .sc-preset-chip.active {
102
+ background-color: var(--n-primary);
103
+ border-color: var(--n-primary);
104
+ color: #fff;
105
+ box-shadow: 0 0 14px rgba(56, 189, 248, 0.35);
106
+ }
107
+
108
+ .sc-field-group {
109
+ display: flex;
110
+ flex-direction: column;
111
+ gap: 0.4rem;
112
+ }
113
+
114
+ .sc-field-header {
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: space-between;
118
+ }
119
+
120
+ .sc-field-header label {
121
+ font-size: 0.85rem;
122
+ font-weight: 600;
123
+ color: var(--n-text-muted);
124
+ }
125
+
126
+ .sc-input {
127
+ width: 100%;
128
+ background-color: var(--n-surface);
129
+ border: 1px solid var(--n-border);
130
+ border-radius: 8px;
131
+ padding: 0.55rem 0.8rem;
132
+ color: var(--n-text);
133
+ font-size: 0.95rem;
134
+ outline: none;
135
+ transition: border-color 0.2s ease;
136
+ }
137
+
138
+ .sc-input:focus {
139
+ border-color: var(--n-primary);
140
+ }
141
+
142
+ .sc-slider {
143
+ width: 100%;
144
+ accent-color: var(--n-primary);
145
+ cursor: pointer;
146
+ margin-top: 0.2rem;
147
+ }
148
+
149
+ .sc-throttle-box {
150
+ background-color: var(--n-surface);
151
+ border: 1px solid var(--n-border);
152
+ border-radius: 12px;
153
+ padding: 1rem;
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 0.6rem;
157
+ border-left: 3px solid var(--n-accent-cyan);
158
+ }
159
+
160
+ .sc-throttle-label {
161
+ font-size: 0.9rem;
162
+ font-weight: 700;
163
+ color: var(--n-text);
164
+ }
165
+
166
+ .sc-throttle-badge {
167
+ background-color: var(--n-accent-cyan);
168
+ color: #000;
169
+ font-weight: 800;
170
+ font-size: 0.85rem;
171
+ padding: 0.15rem 0.55rem;
172
+ border-radius: 6px;
173
+ }
174
+
175
+ .sc-throttle-slider {
176
+ accent-color: var(--n-accent-cyan);
177
+ height: 6px;
178
+ }
179
+
180
+ .sc-snap-group {
181
+ display: grid;
182
+ grid-template-columns: repeat(4, 1fr);
183
+ gap: 0.35rem;
184
+ margin-top: 0.2rem;
185
+ }
186
+
187
+ .sc-snap-btn {
188
+ background-color: var(--n-bg);
189
+ border: 1px solid var(--n-border);
190
+ border-radius: 6px;
191
+ padding: 0.3rem 0.2rem;
192
+ font-size: 0.72rem;
193
+ font-weight: 700;
194
+ color: var(--n-text-muted);
195
+ cursor: pointer;
196
+ transition: all 0.2s ease;
197
+ text-align: center;
198
+ }
199
+
200
+ .sc-snap-btn:hover {
201
+ border-color: var(--n-accent-cyan);
202
+ color: var(--n-accent-cyan);
203
+ }
204
+
205
+ .sc-custom-select {
206
+ position: relative;
207
+ width: 100%;
208
+ }
209
+
210
+ .sc-select-trigger {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: space-between;
214
+ background-color: var(--n-surface);
215
+ border: 1px solid var(--n-border);
216
+ border-radius: 8px;
217
+ padding: 0.6rem 0.8rem;
218
+ color: var(--n-text);
219
+ font-size: 0.95rem;
220
+ cursor: pointer;
221
+ user-select: none;
222
+ }
223
+
224
+ .sc-select-trigger svg {
225
+ transition: transform 0.2s ease;
226
+ fill: var(--n-text-muted);
227
+ }
228
+
229
+ .sc-custom-select.open .sc-select-trigger svg {
230
+ transform: rotate(180deg);
231
+ }
232
+
233
+ .sc-select-dropdown {
234
+ display: none;
235
+ position: absolute;
236
+ top: calc(100% + 4px);
237
+ left: 0;
238
+ right: 0;
239
+ background-color: var(--n-surface);
240
+ border: 1px solid var(--n-border);
241
+ border-radius: 8px;
242
+ box-shadow: var(--n-shadow);
243
+ z-index: 50;
244
+ overflow: hidden;
245
+ }
246
+
247
+ .sc-custom-select.open .sc-select-dropdown {
248
+ display: block;
249
+ }
250
+
251
+ .sc-select-option {
252
+ padding: 0.6rem 0.8rem;
253
+ font-size: 0.9rem;
254
+ color: var(--n-text);
255
+ cursor: pointer;
256
+ transition: background-color 0.2s ease;
257
+ }
258
+
259
+ .sc-select-option:hover,
260
+ .sc-select-option.active {
261
+ background-color: var(--n-primary);
262
+ color: #fff;
263
+ }
264
+
265
+ .sc-cockpit-telemetry {
266
+ display: flex;
267
+ flex-direction: column;
268
+ gap: 1.5rem;
269
+ }
270
+
271
+ .sc-telemetry-topbar {
272
+ display: flex;
273
+ align-items: center;
274
+ justify-content: space-between;
275
+ gap: 1rem;
276
+ }
277
+
278
+ .sc-safety-badge {
279
+ padding: 0.45rem 1.1rem;
280
+ border-radius: 20px;
281
+ font-weight: 700;
282
+ font-size: 0.85rem;
283
+ text-transform: uppercase;
284
+ letter-spacing: 0.04em;
285
+ }
286
+
287
+ .status-optimal {
288
+ background-color: rgba(16, 185, 129, 0.15);
289
+ color: var(--n-success);
290
+ border: 1px solid var(--n-success);
291
+ }
292
+
293
+ .status-info {
294
+ background-color: rgba(2, 132, 199, 0.15);
295
+ color: var(--n-info);
296
+ border: 1px solid var(--n-info);
297
+ }
298
+
299
+ .status-accent {
300
+ background-color: rgba(139, 92, 246, 0.15);
301
+ color: var(--n-accent);
302
+ border: 1px solid var(--n-accent);
303
+ }
304
+
305
+ .status-warning {
306
+ background-color: rgba(245, 158, 11, 0.15);
307
+ color: var(--n-warning);
308
+ border: 1px solid var(--n-warning);
309
+ }
310
+
311
+ .status-danger {
312
+ background-color: rgba(239, 68, 68, 0.15);
313
+ color: var(--n-danger);
314
+ border: 1px solid var(--n-danger);
315
+ }
316
+
317
+ .sc-hud-wrapper {
318
+ background-color: var(--n-surface);
319
+ border: 1px solid var(--n-border);
320
+ border-radius: 12px;
321
+ padding: 1rem;
322
+ display: flex;
323
+ justify-content: center;
324
+ align-items: center;
325
+ }
326
+
327
+ .sc-hud-metrics-grid {
328
+ display: grid;
329
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
330
+ gap: 1rem;
331
+ }
332
+
333
+ .sc-hud-card {
334
+ background-color: var(--n-surface);
335
+ border: 1px solid var(--n-border);
336
+ border-radius: 12px;
337
+ padding: 1rem;
338
+ display: flex;
339
+ flex-direction: column;
340
+ gap: 0.35rem;
341
+ }
342
+
343
+ .sc-hud-card.highlight-cyan {
344
+ border-color: var(--n-accent-cyan);
345
+ }
346
+
347
+ .sc-hud-card.highlight-purple {
348
+ border-color: var(--n-accent);
349
+ }
350
+
351
+ .sc-hud-label {
352
+ font-size: 0.8rem;
353
+ color: var(--n-text-muted);
354
+ font-weight: 600;
355
+ }
356
+
357
+ .sc-hud-val {
358
+ font-size: 1.35rem;
359
+ font-weight: 800;
360
+ color: var(--n-text);
361
+ }
362
+
363
+ .sc-tuning-grid {
364
+ display: grid;
365
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
366
+ gap: 1rem;
367
+ }
368
+
369
+ .sc-tuning-card {
370
+ background-color: var(--n-surface);
371
+ border: 1px solid var(--n-border);
372
+ border-radius: 12px;
373
+ padding: 0.85rem 1rem;
374
+ display: flex;
375
+ flex-direction: column;
376
+ gap: 0.3rem;
377
+ border-top: 2px solid var(--n-primary);
378
+ }
379
+
380
+ .sc-tuning-val {
381
+ font-size: 1.05rem;
382
+ font-weight: 800;
383
+ color: var(--n-primary);
384
+ }
385
+
386
+ .sc-flight-status-card {
387
+ background-color: var(--n-surface);
388
+ border: 1px solid var(--n-border);
389
+ border-radius: 12px;
390
+ padding: 1.2rem;
391
+ }
392
+
393
+ .sc-flight-status-card h4 {
394
+ font-size: 1rem;
395
+ font-weight: 700;
396
+ margin-bottom: 0.4rem;
397
+ color: var(--n-text);
398
+ }
399
+
400
+ .sc-flight-status-card p {
401
+ font-size: 0.9rem;
402
+ color: var(--n-text-muted);
403
+ line-height: 1.5;
404
+ }