@jjlmoya/utils-home 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 (77) hide show
  1. package/package.json +62 -0
  2. package/src/category/i18n/en.ts +24 -0
  3. package/src/category/i18n/es.ts +24 -0
  4. package/src/category/i18n/fr.ts +24 -0
  5. package/src/category/index.ts +12 -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 +11 -0
  10. package/src/env.d.ts +5 -0
  11. package/src/index.ts +26 -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 +19 -0
  17. package/src/tests/locale_completeness.test.ts +42 -0
  18. package/src/tests/mocks/astro_mock.js +2 -0
  19. package/src/tests/no_h1_in_components.test.ts +48 -0
  20. package/src/tests/seo_length.test.ts +22 -0
  21. package/src/tests/tool_validation.test.ts +17 -0
  22. package/src/tool/dewPointCalculator/bibliography.astro +14 -0
  23. package/src/tool/dewPointCalculator/component.astro +443 -0
  24. package/src/tool/dewPointCalculator/i18n/en.ts +183 -0
  25. package/src/tool/dewPointCalculator/i18n/es.ts +183 -0
  26. package/src/tool/dewPointCalculator/i18n/fr.ts +183 -0
  27. package/src/tool/dewPointCalculator/index.ts +34 -0
  28. package/src/tool/dewPointCalculator/logic.ts +16 -0
  29. package/src/tool/dewPointCalculator/seo.astro +14 -0
  30. package/src/tool/dewPointCalculator/ui.ts +13 -0
  31. package/src/tool/ledSavingCalculator/bibliography.astro +14 -0
  32. package/src/tool/ledSavingCalculator/component.astro +520 -0
  33. package/src/tool/ledSavingCalculator/i18n/en.ts +217 -0
  34. package/src/tool/ledSavingCalculator/i18n/es.ts +217 -0
  35. package/src/tool/ledSavingCalculator/i18n/fr.ts +217 -0
  36. package/src/tool/ledSavingCalculator/index.ts +34 -0
  37. package/src/tool/ledSavingCalculator/logic.ts +31 -0
  38. package/src/tool/ledSavingCalculator/seo.astro +14 -0
  39. package/src/tool/ledSavingCalculator/ui.ts +32 -0
  40. package/src/tool/projectorCalculator/bibliography.astro +14 -0
  41. package/src/tool/projectorCalculator/component.astro +569 -0
  42. package/src/tool/projectorCalculator/i18n/en.ts +181 -0
  43. package/src/tool/projectorCalculator/i18n/es.ts +181 -0
  44. package/src/tool/projectorCalculator/i18n/fr.ts +181 -0
  45. package/src/tool/projectorCalculator/index.ts +34 -0
  46. package/src/tool/projectorCalculator/logic.ts +21 -0
  47. package/src/tool/projectorCalculator/seo.astro +14 -0
  48. package/src/tool/projectorCalculator/ui.ts +16 -0
  49. package/src/tool/qrGenerator/bibliography.astro +14 -0
  50. package/src/tool/qrGenerator/component.astro +499 -0
  51. package/src/tool/qrGenerator/i18n/en.ts +233 -0
  52. package/src/tool/qrGenerator/i18n/es.ts +233 -0
  53. package/src/tool/qrGenerator/i18n/fr.ts +233 -0
  54. package/src/tool/qrGenerator/index.ts +34 -0
  55. package/src/tool/qrGenerator/logic.ts +27 -0
  56. package/src/tool/qrGenerator/seo.astro +14 -0
  57. package/src/tool/qrGenerator/ui.ts +23 -0
  58. package/src/tool/solarCalculator/bibliography.astro +14 -0
  59. package/src/tool/solarCalculator/component.astro +532 -0
  60. package/src/tool/solarCalculator/i18n/en.ts +176 -0
  61. package/src/tool/solarCalculator/i18n/es.ts +176 -0
  62. package/src/tool/solarCalculator/i18n/fr.ts +176 -0
  63. package/src/tool/solarCalculator/index.ts +34 -0
  64. package/src/tool/solarCalculator/logic.ts +31 -0
  65. package/src/tool/solarCalculator/seo.astro +14 -0
  66. package/src/tool/solarCalculator/ui.ts +11 -0
  67. package/src/tool/tariffComparator/bibliography.astro +14 -0
  68. package/src/tool/tariffComparator/component.astro +595 -0
  69. package/src/tool/tariffComparator/i18n/en.ts +192 -0
  70. package/src/tool/tariffComparator/i18n/es.ts +192 -0
  71. package/src/tool/tariffComparator/i18n/fr.ts +192 -0
  72. package/src/tool/tariffComparator/index.ts +34 -0
  73. package/src/tool/tariffComparator/logic.ts +47 -0
  74. package/src/tool/tariffComparator/seo.astro +14 -0
  75. package/src/tool/tariffComparator/ui.ts +25 -0
  76. package/src/tools.ts +9 -0
  77. package/src/types.ts +72 -0
@@ -0,0 +1,34 @@
1
+ import type { HomeToolEntry, ToolLocaleContent, ToolDefinition } from '../../types';
2
+ import LedSavingCalculatorComponent from './component.astro';
3
+ import LedSavingCalculatorSEO from './seo.astro';
4
+ import LedSavingCalculatorBibliography from './bibliography.astro';
5
+
6
+ import type { LedSavingCalculatorUI } from './ui';
7
+
8
+ export type LedSavingCalculatorLocaleContent = ToolLocaleContent<LedSavingCalculatorUI>;
9
+
10
+ import { content as es } from './i18n/es';
11
+ import { content as en } from './i18n/en';
12
+ import { content as fr } from './i18n/fr';
13
+
14
+ export const ledSavingCalculator: HomeToolEntry<LedSavingCalculatorUI> = {
15
+ id: 'led-saving-calculator',
16
+ icons: {
17
+ bg: 'mdi:lightbulb-on',
18
+ fg: 'mdi:lightning-bolt',
19
+ },
20
+ i18n: {
21
+ es: async () => es,
22
+ en: async () => en,
23
+ fr: async () => fr,
24
+ },
25
+ };
26
+
27
+ export { LedSavingCalculatorComponent, LedSavingCalculatorSEO, LedSavingCalculatorBibliography };
28
+
29
+ export const LED_SAVING_CALCULATOR_TOOL: ToolDefinition = {
30
+ entry: ledSavingCalculator,
31
+ Component: LedSavingCalculatorComponent,
32
+ SEOComponent: LedSavingCalculatorSEO,
33
+ BibliographyComponent: LedSavingCalculatorBibliography,
34
+ };
@@ -0,0 +1,31 @@
1
+ export interface LedSavingInput {
2
+ numBulbs: number;
3
+ oldWatts: number;
4
+ ledWatts: number;
5
+ hoursPerDay: number;
6
+ pricePerKwh: number;
7
+ }
8
+
9
+ export interface LedSavingResult {
10
+ annualKwh: number;
11
+ annualEuro: number;
12
+ monthlyEuro: number;
13
+ co2Kg: number;
14
+ efficiency: number;
15
+ }
16
+
17
+ export function calculateLedSaving(input: LedSavingInput): LedSavingResult {
18
+ const { numBulbs, oldWatts, ledWatts, hoursPerDay, pricePerKwh } = input;
19
+ const oldAnnualKwh = (numBulbs * oldWatts * hoursPerDay * 365) / 1000;
20
+ const ledAnnualKwh = (numBulbs * ledWatts * hoursPerDay * 365) / 1000;
21
+ const annualKwh = oldAnnualKwh - ledAnnualKwh;
22
+ const annualEuro = annualKwh * pricePerKwh;
23
+ const monthlyEuro = annualEuro / 12;
24
+ const co2Kg = annualKwh * 0.25;
25
+ const efficiency = oldWatts > 0 ? Math.round(((oldWatts - ledWatts) / oldWatts) * 100) : 0;
26
+ return { annualKwh, annualEuro, monthlyEuro, co2Kg, efficiency };
27
+ }
28
+
29
+ export function fmt2(n: number): string {
30
+ return n.toFixed(2);
31
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { ledSavingCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await ledSavingCalculator.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+ <SEORenderer content={{ locale: locale as string, sections: content.seo }} />
@@ -0,0 +1,32 @@
1
+ export interface LedSavingCalculatorUI extends Record<string, string> {
2
+ sectionTitle: string;
3
+ labelBulbs: string;
4
+ unitBulbs: string;
5
+ labelType: string;
6
+ labelHours: string;
7
+ labelPrice: string;
8
+ unitPrice: string;
9
+ resultBadge: string;
10
+ labelAnnual: string;
11
+ labelMonthly: string;
12
+ labelEfficiency: string;
13
+ labelCo2: string;
14
+ unitLess: string;
15
+ currencySign: string;
16
+ btnInc60Title: string;
17
+ btnInc60Sub: string;
18
+ btnInc40Title: string;
19
+ btnInc40Sub: string;
20
+ btnInc100Title: string;
21
+ btnInc100Sub: string;
22
+ btnHalo50Title: string;
23
+ btnHalo50Sub: string;
24
+ usageNever: string;
25
+ usageLow: string;
26
+ usageNormal: string;
27
+ usageModerate: string;
28
+ usageHeavy: string;
29
+ usagePro: string;
30
+ usageVeryHeavy: string;
31
+ usageAlways: string;
32
+ }
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { projectorCalculator } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'es' } = Astro.props;
11
+ const content = await projectorCalculator.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,569 @@
1
+ ---
2
+ import type { ProjectorCalculatorUI } from './ui';
3
+ import { calculateProjection, cmToM } from './logic';
4
+
5
+ interface Props {
6
+ ui?: Record<string, unknown>;
7
+ }
8
+
9
+ const { ui = {} } = Astro.props;
10
+ const pUI = ui as ProjectorCalculatorUI;
11
+ const initial = calculateProjection(100, 16 / 9, 1.5);
12
+ ---
13
+
14
+ <div class="proj-wrapper">
15
+ <div class="proj-card">
16
+
17
+ <div class="proj-left">
18
+ <div class="proj-header">
19
+ <h3 class="proj-title">{pUI.configTitle}</h3>
20
+ <p class="proj-subtitle">{pUI.configSubtitle}</p>
21
+ </div>
22
+
23
+ <div class="proj-field">
24
+ <div class="proj-field-header">
25
+ <label class="proj-label" for="proj-diagonal">{pUI.labelDiagonal}</label>
26
+ <span class="proj-unit">{pUI.labelDiagonalUnit}</span>
27
+ </div>
28
+ <input type="range" id="proj-diagonal-slider" min="30" max="300" value="100" step="1" class="proj-slider" />
29
+ <div class="proj-number-row">
30
+ <input type="number" id="proj-diagonal" value="100" min="30" max="300" class="proj-number-input" />
31
+ <span class="proj-number-unit">"</span>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="proj-field">
36
+ <label class="proj-label">{pUI.labelFormat}</label>
37
+ <div class="proj-ratio-grid">
38
+ <button class="proj-ratio-btn proj-ratio-active" data-ratio="1.7778">
39
+ <span class="proj-ratio-icon ratio-169"></span>
40
+ 16:9
41
+ <span class="proj-ratio-sub">{pUI.ratio169Sub}</span>
42
+ </button>
43
+ <button class="proj-ratio-btn" data-ratio="2.3333">
44
+ <span class="proj-ratio-icon ratio-219"></span>
45
+ 21:9
46
+ <span class="proj-ratio-sub">{pUI.ratio219Sub}</span>
47
+ </button>
48
+ <button class="proj-ratio-btn" data-ratio="1.3333">
49
+ <span class="proj-ratio-icon ratio-43"></span>
50
+ 4:3
51
+ <span class="proj-ratio-sub">{pUI.ratio43Sub}</span>
52
+ </button>
53
+ </div>
54
+ </div>
55
+
56
+ <div class="proj-field">
57
+ <div class="proj-field-header">
58
+ <label class="proj-label" for="proj-throw">{pUI.labelThrowRatio}</label>
59
+ <div class="proj-hint-wrap">
60
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="proj-hint-icon"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
61
+ <div class="proj-hint-tooltip">{pUI.throwRatioHint}</div>
62
+ </div>
63
+ </div>
64
+ <div class="proj-throw-row">
65
+ <input type="number" id="proj-throw" value="1.50" step="0.01" min="0.1" class="proj-throw-input" />
66
+ <span class="proj-throw-unit">: 1</span>
67
+ </div>
68
+ </div>
69
+
70
+ <div class="proj-results">
71
+ <div class="proj-results-grid">
72
+ <div>
73
+ <p class="proj-res-label">{pUI.labelWidth}</p>
74
+ <p id="proj-display-width" class="proj-res-val">{cmToM(initial.widthCm)} m</p>
75
+ </div>
76
+ <div>
77
+ <p class="proj-res-label">{pUI.labelHeight}</p>
78
+ <p id="proj-display-height" class="proj-res-val">{cmToM(initial.heightCm)} m</p>
79
+ </div>
80
+ </div>
81
+ <div class="proj-results-distance">
82
+ <p class="proj-res-label">{pUI.labelDistance}</p>
83
+ <p id="proj-display-distance" class="proj-distance-val">{cmToM(initial.distanceCm)} m</p>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="proj-right">
89
+ <div class="proj-viz-dots"></div>
90
+ <div class="proj-sim-badge">
91
+ <span class="proj-sim-dot"></span>
92
+ <span>{pUI.simulationBadge}</span>
93
+ </div>
94
+ <svg id="proj-viz" viewBox="0 0 800 600" preserveAspectRatio="xMidYMid meet" class="proj-svg">
95
+ <defs>
96
+ <linearGradient id="proj-beam-grad" x1="1" y1="0.5" x2="0" y2="0.5">
97
+ <stop offset="0%" stop-color="#06b6d4" stop-opacity="0.5"></stop>
98
+ <stop offset="100%" stop-color="#06b6d4" stop-opacity="0"></stop>
99
+ </linearGradient>
100
+ <filter id="proj-glow">
101
+ <feGaussianBlur stdDeviation="4" result="coloredBlur"></feGaussianBlur>
102
+ <feMerge>
103
+ <feMergeNode in="coloredBlur"></feMergeNode>
104
+ <feMergeNode in="SourceGraphic"></feMergeNode>
105
+ </feMerge>
106
+ </filter>
107
+ </defs>
108
+ <line x1="50" y1="550" x2="750" y2="550" stroke="#334155" stroke-width="2"></line>
109
+ <g id="proj-screen-group">
110
+ <line x1="60" y1="550" x2="60" y2="100" stroke="#475569" stroke-width="4" stroke-linecap="round"></line>
111
+ <line id="proj-viz-screen" x1="60" y1="200" x2="60" y2="400" stroke="#e2e8f0" stroke-width="6" stroke-linecap="round"></line>
112
+ </g>
113
+ <path id="proj-viz-beam" d="M400,300 L60,250 L60,350 Z" fill="url(#proj-beam-grad)" filter="url(#proj-glow)"></path>
114
+ <g id="proj-viz-projector" transform="translate(400, 300)">
115
+ <line x1="0" y1="-300" x2="0" y2="-20" stroke="#475569" stroke-width="2"></line>
116
+ <rect x="-30" y="-20" width="60" height="40" rx="4" fill="#1e293b" stroke="#475569" stroke-width="2"></rect>
117
+ <circle cx="-30" cy="0" r="10" fill="#06b6d4"></circle>
118
+ <circle cx="-30" cy="0" r="4" fill="#cffafe" class="proj-lens-pulse"></circle>
119
+ </g>
120
+ <text id="proj-viz-dist-text" x="230" y="540" fill="#94a3b8" font-size="14" text-anchor="middle">--m</text>
121
+ <line x1="60" y1="520" x2="750" y2="520" stroke="#334155" stroke-width="1" stroke-dasharray="4 4"></line>
122
+ </svg>
123
+ </div>
124
+
125
+ </div>
126
+ </div>
127
+
128
+ <script>
129
+ import { calculateProjection, cmToM } from './logic';
130
+
131
+ let currentRatio = 16 / 9;
132
+
133
+ function setTxt(id: string, val: string) {
134
+ const el = document.getElementById(id);
135
+ if (el) el.textContent = val;
136
+ }
137
+
138
+ function updateVisuals(widthCm: number, distanceCm: number) {
139
+ const SCALE = 600 / 1000;
140
+ const screenX = 50;
141
+ const projX = Math.min(Math.max(screenX + distanceCm * SCALE, 100), 750);
142
+ const screenH = Math.min((widthCm * SCALE) / currentRatio, 200);
143
+
144
+ const projector = document.getElementById('proj-viz-projector');
145
+ const beam = document.getElementById('proj-viz-beam');
146
+ const distText = document.getElementById('proj-viz-dist-text');
147
+ const screen = document.getElementById('proj-viz-screen');
148
+
149
+ if (projector) projector.setAttribute('transform', `translate(${projX}, 300)`);
150
+ if (screen) {
151
+ screen.setAttribute('y1', String(300 - screenH / 2));
152
+ screen.setAttribute('y2', String(300 + screenH / 2));
153
+ }
154
+ if (beam) {
155
+ beam.setAttribute('d', `M${projX - 10},300 L${screenX + 10},${300 - screenH / 2} L${screenX + 10},${300 + screenH / 2} Z`);
156
+ }
157
+ if (distText) {
158
+ distText.textContent = `${(distanceCm / 100).toFixed(1)}m`;
159
+ distText.setAttribute('x', String((screenX + projX) / 2));
160
+ }
161
+ }
162
+
163
+ function calculate() {
164
+ const diagEl = document.getElementById('proj-diagonal') as HTMLInputElement | null;
165
+ const throwEl = document.getElementById('proj-throw') as HTMLInputElement | null;
166
+ if (!diagEl || !throwEl) return;
167
+ const d = parseFloat(diagEl.value);
168
+ const t = parseFloat(throwEl.value);
169
+ if (isNaN(d) || isNaN(t)) return;
170
+ const r = calculateProjection(d, currentRatio, t);
171
+ setTxt('proj-display-width', cmToM(r.widthCm) + ' m');
172
+ setTxt('proj-display-height', cmToM(r.heightCm) + ' m');
173
+ setTxt('proj-display-distance', cmToM(r.distanceCm) + ' m');
174
+ updateVisuals(r.widthCm, r.distanceCm);
175
+ }
176
+
177
+ function setActiveBtn(active: Element) {
178
+ document.querySelectorAll('.proj-ratio-btn').forEach(btn => {
179
+ btn.classList.toggle('proj-ratio-active', btn === active);
180
+ });
181
+ }
182
+
183
+ function attachRatioButtons() {
184
+ document.querySelectorAll('.proj-ratio-btn').forEach(btn => {
185
+ btn.addEventListener('click', () => {
186
+ const r = (btn as HTMLElement).dataset.ratio;
187
+ if (!r) return;
188
+ currentRatio = parseFloat(r);
189
+ setActiveBtn(btn);
190
+ calculate();
191
+ });
192
+ });
193
+ }
194
+
195
+ function init() {
196
+ const diagInput = document.getElementById('proj-diagonal') as HTMLInputElement | null;
197
+ const slider = document.getElementById('proj-diagonal-slider') as HTMLInputElement | null;
198
+ const throwInput = document.getElementById('proj-throw') as HTMLInputElement | null;
199
+ if (!diagInput || !slider || !throwInput) return;
200
+ diagInput.addEventListener('input', () => { slider.value = diagInput.value; calculate(); });
201
+ slider.addEventListener('input', () => { diagInput.value = slider.value; calculate(); });
202
+ throwInput.addEventListener('input', calculate);
203
+ attachRatioButtons();
204
+ calculate();
205
+ }
206
+
207
+ document.addEventListener('astro:page-load', init);
208
+ init();
209
+ </script>
210
+
211
+ <style>
212
+ .proj-wrapper {
213
+ --proj-p: #06b6d4;
214
+
215
+ width: 100%;
216
+ padding: 1rem 0;
217
+ }
218
+
219
+ .proj-card {
220
+ background: var(--bg-surface);
221
+ width: calc(100% - 24px);
222
+ max-width: 960px;
223
+ margin: 0 auto;
224
+ border-radius: 24px;
225
+ overflow: hidden;
226
+ display: flex;
227
+ flex-direction: column;
228
+ border: 1px solid var(--border-color);
229
+ color: var(--text-main);
230
+ }
231
+
232
+ @media (min-width: 768px) {
233
+ .proj-card {
234
+ flex-direction: row;
235
+ min-height: 580px;
236
+ }
237
+ }
238
+
239
+ .proj-left {
240
+ flex: 0 0 auto;
241
+ width: 100%;
242
+ padding: 32px;
243
+ border-bottom: 1px solid var(--border-color);
244
+ display: flex;
245
+ flex-direction: column;
246
+ gap: 24px;
247
+ }
248
+
249
+ @media (min-width: 768px) {
250
+ .proj-left {
251
+ width: 360px;
252
+ border-bottom: none;
253
+ border-right: 1px solid var(--border-color);
254
+ }
255
+ }
256
+
257
+ .proj-right {
258
+ flex: 1;
259
+ background: #020617;
260
+ position: relative;
261
+ overflow: hidden;
262
+ display: flex;
263
+ flex-direction: column;
264
+ min-height: 380px;
265
+ }
266
+
267
+ .proj-viz-dots {
268
+ position: absolute;
269
+ inset: 0;
270
+ opacity: 0.15;
271
+ pointer-events: none;
272
+ background-image: radial-gradient(#334155 1px, transparent 1px);
273
+ background-size: 30px 30px;
274
+ }
275
+
276
+ .proj-sim-badge {
277
+ position: relative;
278
+ z-index: 1;
279
+ display: flex;
280
+ align-items: center;
281
+ gap: 6px;
282
+ padding: 16px 20px 0;
283
+ font-size: 0.6rem;
284
+ font-weight: 900;
285
+ text-transform: uppercase;
286
+ letter-spacing: 0.18em;
287
+ color: #94a3b8;
288
+ }
289
+
290
+ .proj-sim-dot {
291
+ width: 7px;
292
+ height: 7px;
293
+ border-radius: 50%;
294
+ background: var(--proj-p);
295
+ animation: proj-pulse 2s infinite;
296
+ }
297
+
298
+ @keyframes proj-pulse {
299
+ 0%, 100% { opacity: 1; }
300
+ 50% { opacity: 0.3; }
301
+ }
302
+
303
+ .proj-lens-pulse {
304
+ animation: proj-pulse 2s infinite;
305
+ }
306
+
307
+ .proj-svg {
308
+ flex: 1;
309
+ width: 100%;
310
+ position: relative;
311
+ z-index: 1;
312
+ }
313
+
314
+ .proj-header {
315
+ display: flex;
316
+ flex-direction: column;
317
+ gap: 4px;
318
+ }
319
+
320
+ .proj-title {
321
+ font-size: 1.25rem;
322
+ font-weight: 900;
323
+ color: var(--text-main);
324
+ margin: 0;
325
+ }
326
+
327
+ .proj-subtitle {
328
+ font-size: 0.8125rem;
329
+ color: var(--text-muted);
330
+ margin: 0;
331
+ }
332
+
333
+ .proj-field {
334
+ display: flex;
335
+ flex-direction: column;
336
+ gap: 10px;
337
+ }
338
+
339
+ .proj-field-header {
340
+ display: flex;
341
+ justify-content: space-between;
342
+ align-items: center;
343
+ }
344
+
345
+ .proj-label {
346
+ font-size: 0.6875rem;
347
+ font-weight: 700;
348
+ text-transform: uppercase;
349
+ letter-spacing: 0.1em;
350
+ color: var(--proj-p);
351
+ }
352
+
353
+ .proj-unit {
354
+ font-size: 0.75rem;
355
+ color: var(--text-muted);
356
+ }
357
+
358
+ .proj-slider {
359
+ width: 100%;
360
+ height: 6px;
361
+ accent-color: var(--proj-p);
362
+ cursor: pointer;
363
+ border-radius: 9999px;
364
+ }
365
+
366
+ .proj-number-row {
367
+ position: relative;
368
+ display: flex;
369
+ align-items: center;
370
+ }
371
+
372
+ .proj-number-input {
373
+ width: 100%;
374
+ font-size: 3rem;
375
+ font-weight: 900;
376
+ color: var(--text-main);
377
+ background: transparent;
378
+ border: none;
379
+ border-bottom: 2px solid var(--border-color);
380
+ padding: 4px 2rem 4px 0;
381
+ outline: none;
382
+ transition: border-color 0.2s;
383
+ }
384
+
385
+ .proj-number-input:focus {
386
+ border-color: var(--proj-p);
387
+ }
388
+
389
+ .proj-number-unit {
390
+ position: absolute;
391
+ right: 4px;
392
+ bottom: 8px;
393
+ font-size: 1.5rem;
394
+ color: var(--text-muted);
395
+ font-weight: 300;
396
+ }
397
+
398
+ .proj-ratio-grid {
399
+ display: grid;
400
+ grid-template-columns: repeat(3, 1fr);
401
+ gap: 8px;
402
+ }
403
+
404
+ .proj-ratio-btn {
405
+ display: flex;
406
+ flex-direction: column;
407
+ align-items: center;
408
+ gap: 4px;
409
+ padding: 10px 8px;
410
+ border-radius: 12px;
411
+ border: 1px solid var(--border-color);
412
+ background: var(--bg-surface);
413
+ color: var(--text-muted);
414
+ font-size: 0.8125rem;
415
+ font-weight: 700;
416
+ cursor: pointer;
417
+ transition: all 0.2s;
418
+ }
419
+
420
+ .proj-ratio-btn:hover {
421
+ border-color: var(--proj-p);
422
+ color: var(--text-main);
423
+ }
424
+
425
+ .proj-ratio-active {
426
+ background: var(--proj-p);
427
+ color: #fff;
428
+ border-color: var(--proj-p);
429
+ box-shadow: 0 4px 14px rgba(6, 182, 212, 0.35);
430
+ }
431
+
432
+ .proj-ratio-icon {
433
+ display: block;
434
+ border: 2px solid currentcolor;
435
+ border-radius: 2px;
436
+ opacity: 0.6;
437
+ }
438
+
439
+ .proj-ratio-active .proj-ratio-icon {
440
+ opacity: 1;
441
+ border-color: #fff;
442
+ }
443
+
444
+ .ratio-169 {
445
+ width: 28px;
446
+ height: 16px;
447
+ }
448
+
449
+ .ratio-219 {
450
+ width: 36px;
451
+ height: 14px;
452
+ }
453
+
454
+ .ratio-43 {
455
+ width: 20px;
456
+ height: 16px;
457
+ }
458
+
459
+ .proj-ratio-sub {
460
+ font-size: 0.625rem;
461
+ font-weight: 500;
462
+ opacity: 0.75;
463
+ }
464
+
465
+ .proj-hint-wrap {
466
+ position: relative;
467
+ }
468
+
469
+ .proj-hint-icon {
470
+ width: 16px;
471
+ height: 16px;
472
+ color: var(--text-muted);
473
+ cursor: help;
474
+ }
475
+
476
+ .proj-hint-tooltip {
477
+ position: absolute;
478
+ bottom: calc(100% + 6px);
479
+ right: 0;
480
+ width: 220px;
481
+ background: #1e293b;
482
+ color: #e2e8f0;
483
+ font-size: 0.75rem;
484
+ font-weight: 400;
485
+ padding: 10px 12px;
486
+ border-radius: 10px;
487
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
488
+ opacity: 0;
489
+ pointer-events: none;
490
+ transition: opacity 0.15s;
491
+ z-index: 10;
492
+ line-height: 1.5;
493
+ }
494
+
495
+ .proj-hint-wrap:hover .proj-hint-tooltip {
496
+ opacity: 1;
497
+ }
498
+
499
+ .proj-throw-row {
500
+ position: relative;
501
+ display: flex;
502
+ align-items: center;
503
+ }
504
+
505
+ .proj-throw-input {
506
+ width: 100%;
507
+ font-size: 2.5rem;
508
+ font-weight: 900;
509
+ color: var(--text-main);
510
+ background: transparent;
511
+ border: none;
512
+ border-bottom: 2px solid var(--border-color);
513
+ padding: 4px 3rem 4px 0;
514
+ outline: none;
515
+ transition: border-color 0.2s;
516
+ }
517
+
518
+ .proj-throw-input:focus {
519
+ border-color: var(--proj-p);
520
+ }
521
+
522
+ .proj-throw-unit {
523
+ position: absolute;
524
+ right: 4px;
525
+ bottom: 10px;
526
+ font-size: 1.25rem;
527
+ color: var(--text-muted);
528
+ }
529
+
530
+ .proj-results {
531
+ background: rgba(6, 182, 212, 0.06);
532
+ border: 1px solid rgba(6, 182, 212, 0.15);
533
+ border-radius: 16px;
534
+ padding: 18px;
535
+ }
536
+
537
+ .proj-results-grid {
538
+ display: grid;
539
+ grid-template-columns: 1fr 1fr;
540
+ gap: 12px;
541
+ margin-bottom: 14px;
542
+ padding-bottom: 14px;
543
+ border-bottom: 1px solid rgba(6, 182, 212, 0.15);
544
+ }
545
+
546
+ .proj-res-label {
547
+ font-size: 0.625rem;
548
+ font-weight: 900;
549
+ text-transform: uppercase;
550
+ letter-spacing: 0.12em;
551
+ color: var(--proj-p);
552
+ margin: 0 0 4px;
553
+ }
554
+
555
+ .proj-res-val {
556
+ font-size: 1.125rem;
557
+ font-weight: 700;
558
+ color: var(--text-main);
559
+ margin: 0;
560
+ }
561
+
562
+ .proj-distance-val {
563
+ font-size: 2.5rem;
564
+ font-weight: 900;
565
+ color: var(--text-main);
566
+ margin: 0;
567
+ line-height: 1;
568
+ }
569
+ </style>