@jjlmoya/utils-hardware 1.22.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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/mouseScrollTest/mouse-scroll-test.css +23 -0
  8. package/src/tool/stereoAudioTest/bibliography.astro +14 -0
  9. package/src/tool/stereoAudioTest/bibliography.ts +16 -0
  10. package/src/tool/stereoAudioTest/component.astro +251 -0
  11. package/src/tool/stereoAudioTest/entry.ts +29 -0
  12. package/src/tool/stereoAudioTest/i18n/de.ts +229 -0
  13. package/src/tool/stereoAudioTest/i18n/en.ts +229 -0
  14. package/src/tool/stereoAudioTest/i18n/es.ts +229 -0
  15. package/src/tool/stereoAudioTest/i18n/fr.ts +229 -0
  16. package/src/tool/stereoAudioTest/i18n/id.ts +229 -0
  17. package/src/tool/stereoAudioTest/i18n/it.ts +229 -0
  18. package/src/tool/stereoAudioTest/i18n/ja.ts +229 -0
  19. package/src/tool/stereoAudioTest/i18n/ko.ts +229 -0
  20. package/src/tool/stereoAudioTest/i18n/nl.ts +229 -0
  21. package/src/tool/stereoAudioTest/i18n/pl.ts +229 -0
  22. package/src/tool/stereoAudioTest/i18n/pt.ts +229 -0
  23. package/src/tool/stereoAudioTest/i18n/ru.ts +229 -0
  24. package/src/tool/stereoAudioTest/i18n/sv.ts +229 -0
  25. package/src/tool/stereoAudioTest/i18n/tr.ts +229 -0
  26. package/src/tool/stereoAudioTest/i18n/zh.ts +229 -0
  27. package/src/tool/stereoAudioTest/index.ts +11 -0
  28. package/src/tool/stereoAudioTest/logic.ts +22 -0
  29. package/src/tool/stereoAudioTest/seo.astro +15 -0
  30. package/src/tool/stereoAudioTest/stereo-audio-test.css +411 -0
  31. package/src/tool/stereoAudioTest/ui.ts +24 -0
  32. package/src/tools.ts +2 -1
@@ -0,0 +1,229 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { StereoAudioTestUI } from '../ui';
4
+ import { bibliography } from '../bibliography';
5
+
6
+ const slug = 'stereo-audio-test';
7
+ const title = '立体声音频测试';
8
+ const description = '使用浏览器生成的测试音检查左右扬声器声道、立体声平衡、接线方向和耳机声像定位。';
9
+
10
+ const faqData = [
11
+ {
12
+ question: '如何在线测试左右扬声器?',
13
+ answer: '从低音量开始,按左,然后按右。左声道音调应仅从左扬声器或左耳罩发出,右声道音调应仅从右侧发出。使用中置确认两侧均衡播放。',
14
+ },
15
+ {
16
+ question: '为什么在左或右测试期间两个扬声器都播放?',
17
+ answer: '某些设备、操作系统、蓝牙扬声器、单声道模式、辅助功能设置或音频增强功能会将立体声降混为单声道。检查系统平衡、单声道音频设置、电缆接线和应用程序特定的音频效果。',
18
+ },
19
+ {
20
+ question: '这能诊断扬声器电缆接反吗?',
21
+ answer: '能。如果左按钮从右扬声器播放,右按钮从左扬声器播放,则输出路径在计算机、电缆、放大器、扬声器或耳机的某处反转。',
22
+ },
23
+ {
24
+ question: '正弦波音调对扬声器和耳机安全吗?',
25
+ answer: '中等音量的短正弦波音调通常是安全的。避免高音量、长时间使用或非常高的频率,尤其是在使用耳机、小型笔记本电脑扬声器或未知放大器时。',
26
+ },
27
+ {
28
+ question: '浏览器会影响立体声测试吗?',
29
+ answer: '该工具使用 Web Audio API 的 StereoPannerNode。在现代浏览器中可靠,但最终输出仍取决于操作系统路由、驱动程序、蓝牙编解码器、外部 DAC 和扬声器接线。',
30
+ },
31
+ ];
32
+
33
+ const howToData = [
34
+ {
35
+ name: '设置较低的起始音量',
36
+ text: '在播放任何测试音之前,降低系统音量并使用工具音量滑块。',
37
+ },
38
+ {
39
+ name: '测试每一侧',
40
+ text: '按左和右。每个音调应清晰隔离到匹配的扬声器或耳机侧。',
41
+ },
42
+ {
43
+ name: '检查中置平衡',
44
+ text: '按中置。音调应在两个扬声器之间居中,不强烈偏向一侧。',
45
+ },
46
+ {
47
+ name: '使用扫描',
48
+ text: '运行扫描以听到立体声场中的移动,发现断音、接线反转或成像不均。',
49
+ },
50
+ ];
51
+
52
+ const faqSchema: WithContext<FAQPage> = {
53
+ '@context': 'https://schema.org',
54
+ '@type': 'FAQPage',
55
+ mainEntity: faqData.map((item) => ({
56
+ '@type': 'Question',
57
+ name: item.question,
58
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
59
+ })),
60
+ };
61
+
62
+ const howToSchema: WithContext<HowTo> = {
63
+ '@context': 'https://schema.org',
64
+ '@type': 'HowTo',
65
+ name: title,
66
+ description,
67
+ step: howToData.map((step, i) => ({
68
+ '@type': 'HowToStep',
69
+ position: i + 1,
70
+ name: step.name,
71
+ text: step.text,
72
+ })),
73
+ };
74
+
75
+ const appSchema: WithContext<SoftwareApplication> = {
76
+ '@context': 'https://schema.org',
77
+ '@type': 'SoftwareApplication',
78
+ name: title,
79
+ description,
80
+ applicationCategory: 'UtilityApplication',
81
+ operatingSystem: 'All',
82
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
83
+ inLanguage: 'zh',
84
+ };
85
+
86
+ export const content: ToolLocaleContent<StereoAudioTestUI> = {
87
+ slug,
88
+ title,
89
+ description,
90
+ faq: faqData,
91
+ bibliography,
92
+ howTo: howToData,
93
+ schemas: [faqSchema, howToSchema, appSchema],
94
+ seo: [
95
+ {
96
+ type: 'title',
97
+ text: '左右扬声器在线测试',
98
+ level: 2,
99
+ },
100
+ {
101
+ type: 'paragraph',
102
+ html: '使用此在线立体声音频测试来检查您的左右扬声器、耳机、耳塞、回音壁、DAC、放大器或监听音箱是否正确路由。该工具通过左声道、右声道、双声道或移动的立体声扫描播放浏览器生成的测试音,让您无需安装音频软件即可快速检测声道反转、单声道输出、扬声器弱音、平衡问题和接线错误。',
103
+ },
104
+ {
105
+ type: 'paragraph',
106
+ html: '正确的立体声设置保持方向感:左测试音应仅从左扬声器或左耳罩发出,右测试音应仅从右侧发出,中置音应在两侧之间均衡平衡。如果声音出现在错误的一侧、两侧同时出现或一侧明显更响,问题通常出在输出设置、单声道辅助功能模式、电缆接线、蓝牙路由、扬声器摆放或音频增强软件上。',
107
+ },
108
+ {
109
+ type: 'table',
110
+ headers: ['按钮', '您应听到的声音', '用于诊断'],
111
+ rows: [
112
+ ['左', '仅从左扬声器、左耳机驱动器或左耳塞发出音调。', '电缆接反、扬声器摆错位置、单声道输出或声道重映射。'],
113
+ ['右', '仅从右扬声器、右耳机驱动器或右耳塞发出音调。', '输出交换、适配器接线错误或改变声道顺序的驱动效果。'],
114
+ ['中置', '音调出现在中间,因为两个声道均衡播放。', '系统平衡偏移、一个扬声器弱音、耳塞网罩堵塞或放大器增益不均。'],
115
+ ['扫描', '音调在立体声图像中从一侧平滑移动到另一侧。', '断音、蓝牙链接不稳定、相位问题、虚拟环绕声伪像或成像不均。'],
116
+ ],
117
+ },
118
+ {
119
+ type: 'title',
120
+ text: '此测试发现的最常见立体声问题',
121
+ level: 3,
122
+ },
123
+ {
124
+ type: 'list',
125
+ items: [
126
+ '左右声道反转:左按钮在右侧播放,或右按钮在左侧播放。',
127
+ '立体声塌陷为单声道:左、右和中置从两个扬声器听起来几乎相同。',
128
+ '一侧较安静:即使系统平衡滑块看起来居中,中置音频也偏向一个扬声器。',
129
+ '耳机接线或佩戴错误:游戏脚步声、音乐声像平移和视频通话在空间上感觉混乱。',
130
+ '蓝牙或USB音频被处理:回音壁、底座、耳机和电视模式可能会降混或虚拟化信号。',
131
+ '扬声器摆放误导:扬声器太靠近墙壁、被家具遮挡或距离更远可能导致中置图像偏移。',
132
+ ],
133
+ },
134
+ {
135
+ type: 'title',
136
+ text: '如何修复左右音频反转',
137
+ level: 3,
138
+ },
139
+ {
140
+ type: 'list',
141
+ items: [
142
+ '对于有线扬声器,交换放大器、音频接口、DAC或计算机输出端的左右插头。',
143
+ '对于无源扬声器,确认左扬声器连接到放大器的左端子,右扬声器连接到右端子。',
144
+ '对于耳机,检查佩戴方向是否正确,并在不使用适配器、延长线或分线器的情况下测试。',
145
+ '在Windows或macOS上,检查输出平衡并在辅助功能或声音设置中禁用单声道音频。',
146
+ '对于蓝牙扬声器和回音壁,测试期间禁用虚拟环绕声、派对模式、双音频、房间校正或电视声音模式。',
147
+ '对于音频接口、DAW和混音器,检查通道路由、声像控制、监听混音设置以及制造商提供的任何软件混音器。',
148
+ ],
149
+ },
150
+ {
151
+ type: 'title',
152
+ text: '如何解读中置扬声器测试',
153
+ level: 3,
154
+ },
155
+ {
156
+ type: 'paragraph',
157
+ html: '中置音调在普通双声道设置中并不是一个独立的物理中置扬声器。它是均匀发送到左右声道的相同信号。在耳机中应在头部中央感觉居中;在扬声器上应在两者之间形成幻象中心。如果偏向左侧或右侧,请检查系统平衡、扬声器距离、扬声器角度、音量旋钮、放大器微调、耳塞贴合度、驱动器格栅中的灰尘以及扬声器是否部分被遮挡或损坏。',
158
+ },
159
+ {
160
+ type: 'table',
161
+ headers: ['发生了什么', '可能的原因', '下一步'],
162
+ rows: [
163
+ ['左从两侧播放', '单声道音频、降混或空间音频处理。', '禁用单声道输出和虚拟环绕声,然后重新测试。'],
164
+ ['左从右侧播放', '播放链中某处声道被交换。', '交换电缆或更改驱动程序、混音器或放大器中的通道路由。'],
165
+ ['中置在一侧更响', '平衡、摆放、驱动器损坏、耳朵贴合度或扬声器格栅堵塞。', '与其他耳机或扬声器对比较,并检查物理摆放。'],
166
+ ['扫描跳跃或消失', '蓝牙不稳定、音频增强伪像或电缆/连接器故障。', '用有线输出或其他电缆测试,以隔离薄弱环节。'],
167
+ ],
168
+ },
169
+ {
170
+ type: 'title',
171
+ text: '耳机和耳塞左右测试',
172
+ level: 3,
173
+ },
174
+ {
175
+ type: 'paragraph',
176
+ html: '对于耳机和耳塞,左右声道测试在游戏、音频编辑、观看电影或加入通话前特别有用。正常戴上耳机,从低音量开始,按左和右,确认每个音调到达正确的耳朵。如果两侧听起来相同,您的设备可能正在使用单声道音频。如果一侧沉闷或较安静,清洁耳塞网罩,重新安装耳塞头,检查电缆,并与其他输出设备比较。',
177
+ },
178
+ {
179
+ type: 'title',
180
+ text: '安全音频测试提示',
181
+ level: 3,
182
+ },
183
+ {
184
+ type: 'list',
185
+ items: [
186
+ '从低系统音量开始,尤其是在耳机上。',
187
+ '仅在需要持续声音进行电缆追踪、摆放或平衡调整时使用循环直到停止。',
188
+ '在小型扬声器上保持测试音简短;连续正弦波可能很快变得不适。',
189
+ '避免在小型笔记本电脑扬声器和未知放大器上使用最大增益。',
190
+ '在确认音量安全之前,不要将耳机戴在耳朵上。',
191
+ '更换电缆或设置后,按左、右、中置、扫描的顺序重复测试。',
192
+ '对于录音室或家庭影院校准,将此快速测试与声压计或接收器校准程序结合使用。',
193
+ ],
194
+ },
195
+ {
196
+ type: 'title',
197
+ text: '快速诊断',
198
+ level: 3,
199
+ },
200
+ {
201
+ type: 'paragraph',
202
+ html: '如果左右反转,首先检查物理接线,因为这是台式扬声器、放大器和音频接口最快的修复方法。如果两个按钮都从两侧播放,寻找单声道输出、空间音频、虚拟环绕声或故意将立体声降混的设备。如果中置偏离中心但左右路由正确,问题通常是平衡、摆放、耳机贴合度、堵塞的格栅或扬声器输出不均。',
203
+ },
204
+ ],
205
+ ui: {
206
+ left: '左',
207
+ center: '中置',
208
+ right: '右',
209
+ sweep: '扫描',
210
+ stop: '停止',
211
+ volume: '音量',
212
+ duration: '时长',
213
+ infiniteMode: '循环至停止',
214
+ infiniteModeHint: '保持任意声道持续播放以进行连续测试。',
215
+ secondsUnit: '秒',
216
+ hertzUnit: '赫兹',
217
+ tone: '音调',
218
+ balance: '平衡',
219
+ activeIdle: '就绪',
220
+ activeLeft: '正在播放左声道',
221
+ activeCenter: '正在播放中置音调',
222
+ activeRight: '正在播放右声道',
223
+ activeSweep: '正在扫描立体声场',
224
+ safety: '从低音量开始。测试音通过耳机、放大器和小型笔记本电脑扬声器可能会很响。',
225
+ leftSpeaker: '左扬声器',
226
+ rightSpeaker: '右扬声器',
227
+ centerLine: '立体声场',
228
+ },
229
+ };
@@ -0,0 +1,11 @@
1
+ import { stereoAudioTest } from './entry';
2
+ import type { ToolDefinition } from '../../types';
3
+
4
+ export * from './entry';
5
+
6
+ export const STEREO_AUDIO_TEST_TOOL: ToolDefinition = {
7
+ entry: stereoAudioTest,
8
+ Component: () => import('./component.astro'),
9
+ SEOComponent: () => import('./seo.astro'),
10
+ BibliographyComponent: () => import('./bibliography.astro'),
11
+ };
@@ -0,0 +1,22 @@
1
+ export type StereoChannel = 'left' | 'center' | 'right' | 'sweep';
2
+
3
+ export interface StereoPanPoint {
4
+ label: StereoChannel;
5
+ pan: number;
6
+ frequency: number;
7
+ }
8
+
9
+ export const stereoPanPoints: StereoPanPoint[] = [
10
+ { label: 'left', pan: -1, frequency: 440 },
11
+ { label: 'center', pan: 0, frequency: 523.25 },
12
+ { label: 'right', pan: 1, frequency: 659.25 },
13
+ { label: 'sweep', pan: 0, frequency: 587.33 },
14
+ ];
15
+
16
+ export function getStereoBalance(leftLevel: number, rightLevel: number): number {
17
+ const left = Math.max(0, leftLevel);
18
+ const right = Math.max(0, rightLevel);
19
+ const total = left + right;
20
+ if (total === 0) return 0;
21
+ return (right - left) / total;
22
+ }
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { stereoAudioTest } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await stereoAudioTest.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
@@ -0,0 +1,411 @@
1
+ .sat-root {
2
+ --sat-ink: #172033;
3
+ --sat-muted: #667085;
4
+ --sat-card: #fff;
5
+ --sat-soft: #f3f6fb;
6
+ --sat-line: #d9e1ee;
7
+ --sat-left: #2563eb;
8
+ --sat-right: #e11d48;
9
+ --sat-center: #0f9f7a;
10
+ --sat-shadow: rgb(23, 32, 51, 0.1);
11
+ --sat-pan: 0;
12
+
13
+ color: var(--sat-ink);
14
+ }
15
+
16
+ .theme-dark .sat-root {
17
+ --sat-ink: #f8fafc;
18
+ --sat-muted: #aab4c5;
19
+ --sat-card: #161d29;
20
+ --sat-soft: #101722;
21
+ --sat-line: #334155;
22
+ --sat-left: #60a5fa;
23
+ --sat-right: #fb7185;
24
+ --sat-center: #34d399;
25
+ --sat-shadow: rgb(0, 0, 0, 0.3);
26
+ }
27
+
28
+ .sat-panel {
29
+ display: grid;
30
+ gap: 0.75rem;
31
+ width: 100%;
32
+ padding: 0.75rem;
33
+ border: 1px solid var(--sat-line);
34
+ border-radius: 8px;
35
+ background: var(--sat-card);
36
+ box-shadow: 0 1rem 2.5rem var(--sat-shadow);
37
+ }
38
+
39
+ .sat-stage {
40
+ position: relative;
41
+ display: grid;
42
+ grid-template-columns: minmax(0, 1fr) minmax(7rem, 0.75fr) minmax(0, 1fr);
43
+ gap: 0.5rem;
44
+ align-items: center;
45
+ min-height: 15rem;
46
+ padding: 0.75rem;
47
+ border: 1px solid var(--sat-line);
48
+ border-radius: 8px;
49
+ background: var(--sat-soft);
50
+ overflow: hidden;
51
+ }
52
+
53
+ .sat-speaker {
54
+ position: relative;
55
+ z-index: 1;
56
+ display: grid;
57
+ place-items: center;
58
+ min-height: 8rem;
59
+ min-width: 0;
60
+ }
61
+
62
+ .sat-speaker span {
63
+ position: relative;
64
+ width: min(8rem, 28vw);
65
+ aspect-ratio: 1;
66
+ border: 1px solid var(--sat-line);
67
+ border-radius: 50%;
68
+ background: var(--sat-card);
69
+ box-shadow: inset 0 0 0 0.8rem var(--sat-soft), 0 0.6rem 1.5rem var(--sat-shadow);
70
+ }
71
+
72
+ .sat-speaker span::before,
73
+ .sat-speaker span::after {
74
+ position: absolute;
75
+ inset: 27%;
76
+ border: 1px solid var(--sat-line);
77
+ border-radius: inherit;
78
+ content: "";
79
+ }
80
+
81
+ .sat-speaker span::after {
82
+ inset: 42%;
83
+ background: currentcolor;
84
+ }
85
+
86
+ .sat-left {
87
+ color: var(--sat-left);
88
+ }
89
+
90
+ .sat-right {
91
+ color: var(--sat-right);
92
+ }
93
+
94
+ .sat-speaker strong {
95
+ position: absolute;
96
+ top: calc(50% + min(4.45rem, 15.8vw));
97
+ left: 50%;
98
+ width: max-content;
99
+ max-width: 100%;
100
+ color: var(--sat-muted);
101
+ font-size: 0.78rem;
102
+ font-weight: 900;
103
+ text-align: center;
104
+ transform: translateX(-50%);
105
+ }
106
+
107
+ .sat-orbit {
108
+ position: relative;
109
+ display: grid;
110
+ place-items: center;
111
+ align-self: center;
112
+ min-width: 0;
113
+ min-height: 8rem;
114
+ }
115
+
116
+ .sat-orbit::before {
117
+ position: absolute;
118
+ z-index: 0;
119
+ left: -55%;
120
+ right: -55%;
121
+ top: 50%;
122
+ height: 0.35rem;
123
+ border-radius: 8px;
124
+ background: linear-gradient(90deg, var(--sat-left), var(--sat-center), var(--sat-right));
125
+ content: "";
126
+ transform: translateY(-50%);
127
+ }
128
+
129
+ .sat-orbit i {
130
+ position: relative;
131
+ z-index: 2;
132
+ width: 3rem;
133
+ aspect-ratio: 1;
134
+ border: 0.45rem solid var(--sat-card);
135
+ border-radius: 50%;
136
+ background: var(--sat-center);
137
+ box-shadow: 0 0.5rem 1.4rem var(--sat-shadow);
138
+ transform: translateX(calc(var(--sat-pan) * 6rem));
139
+ transition: transform 90ms linear, background 160ms ease;
140
+ }
141
+
142
+ .sat-orbit b {
143
+ position: absolute;
144
+ bottom: 1.2rem;
145
+ color: var(--sat-muted);
146
+ font-size: 0.68rem;
147
+ font-weight: 900;
148
+ }
149
+
150
+ .sat-controls {
151
+ display: grid;
152
+ gap: 0.7rem;
153
+ align-content: start;
154
+ padding: 0.7rem;
155
+ border: 1px solid var(--sat-line);
156
+ border-radius: 8px;
157
+ background: var(--sat-soft);
158
+ }
159
+
160
+ .sat-buttons {
161
+ display: grid;
162
+ grid-template-columns: repeat(2, minmax(0, 1fr));
163
+ gap: 0.45rem;
164
+ }
165
+
166
+ .sat-buttons button {
167
+ display: inline-grid;
168
+ grid-auto-flow: column;
169
+ place-content: center;
170
+ align-items: center;
171
+ gap: 0.35rem;
172
+ min-height: 2.8rem;
173
+ border: 1px solid var(--sat-line);
174
+ border-radius: 8px;
175
+ background: var(--sat-card);
176
+ color: var(--sat-ink);
177
+ font: inherit;
178
+ font-weight: 900;
179
+ cursor: pointer;
180
+ transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
181
+ }
182
+
183
+ .sat-buttons svg {
184
+ width: 1rem;
185
+ height: 1rem;
186
+ }
187
+
188
+ .sat-buttons button:hover {
189
+ border-color: var(--sat-center);
190
+ background: color-mix(in srgb, var(--sat-center) 14%, var(--sat-card));
191
+ transform: translateY(-1px);
192
+ }
193
+
194
+ .sat-buttons button:last-child {
195
+ grid-column: 1 / -1;
196
+ background: var(--sat-ink);
197
+ color: var(--sat-card);
198
+ }
199
+
200
+ .sat-sliders,
201
+ .sat-readout,
202
+ .sat-loop {
203
+ display: grid;
204
+ gap: 0.55rem;
205
+ padding: 0.7rem;
206
+ border: 1px solid var(--sat-line);
207
+ border-radius: 8px;
208
+ background: var(--sat-card);
209
+ }
210
+
211
+ .sat-control-row {
212
+ display: grid;
213
+ grid-template-columns: minmax(0, 1fr) auto;
214
+ align-items: center;
215
+ gap: 0.45rem 0.7rem;
216
+ min-height: 4.4rem;
217
+ padding: 0.55rem;
218
+ border: 1px solid var(--sat-line);
219
+ border-radius: 8px;
220
+ background: var(--sat-soft);
221
+ color: var(--sat-muted);
222
+ font-size: 0.72rem;
223
+ font-weight: 850;
224
+ }
225
+
226
+ .sat-control-row input {
227
+ grid-column: 1 / -1;
228
+ width: 100%;
229
+ height: 0.55rem;
230
+ border-radius: 8px;
231
+ appearance: none;
232
+ background:
233
+ linear-gradient(90deg, var(--sat-left), var(--sat-center), var(--sat-right)),
234
+ var(--sat-line);
235
+ cursor: pointer;
236
+ }
237
+
238
+ .sat-control-row.is-disabled {
239
+ opacity: 0.48;
240
+ }
241
+
242
+ .sat-control-row.is-disabled input {
243
+ cursor: not-allowed;
244
+ filter: grayscale(1);
245
+ }
246
+
247
+ .sat-control-row input::-webkit-slider-thumb {
248
+ width: 1.25rem;
249
+ height: 1.25rem;
250
+ border: 0.22rem solid var(--sat-card);
251
+ border-radius: 50%;
252
+ appearance: none;
253
+ background: var(--sat-ink);
254
+ box-shadow: 0 0.35rem 0.85rem var(--sat-shadow);
255
+ }
256
+
257
+ .sat-control-row input::-moz-range-thumb {
258
+ width: 0.9rem;
259
+ height: 0.9rem;
260
+ border: 0.22rem solid var(--sat-card);
261
+ border-radius: 50%;
262
+ background: var(--sat-ink);
263
+ box-shadow: 0 0.35rem 0.85rem var(--sat-shadow);
264
+ }
265
+
266
+ .sat-sliders strong,
267
+ .sat-readout strong {
268
+ color: var(--sat-ink);
269
+ text-align: right;
270
+ font-size: 0.95rem;
271
+ font-weight: 950;
272
+ }
273
+
274
+ .sat-loop {
275
+ grid-template-columns: auto minmax(0, 1fr);
276
+ align-items: center;
277
+ cursor: pointer;
278
+ }
279
+
280
+ .sat-loop input {
281
+ width: 2.65rem;
282
+ height: 1.45rem;
283
+ border: 1px solid var(--sat-line);
284
+ border-radius: 999px;
285
+ appearance: none;
286
+ background: var(--sat-line);
287
+ cursor: pointer;
288
+ transition: background 160ms ease;
289
+ }
290
+
291
+ .sat-loop input::before {
292
+ display: block;
293
+ width: 1.05rem;
294
+ height: 1.05rem;
295
+ margin: 0.13rem;
296
+ border-radius: 50%;
297
+ background: var(--sat-card);
298
+ box-shadow: 0 0.25rem 0.6rem var(--sat-shadow);
299
+ content: "";
300
+ transition: transform 160ms ease;
301
+ }
302
+
303
+ .sat-loop input:checked {
304
+ background: var(--sat-center);
305
+ }
306
+
307
+ .sat-loop input:checked::before {
308
+ transform: translateX(1.16rem);
309
+ }
310
+
311
+ .sat-loop span {
312
+ display: grid;
313
+ gap: 0.12rem;
314
+ }
315
+
316
+ .sat-loop strong {
317
+ color: var(--sat-ink);
318
+ font-size: 0.86rem;
319
+ font-weight: 950;
320
+ }
321
+
322
+ .sat-loop em {
323
+ color: var(--sat-muted);
324
+ font-size: 0.72rem;
325
+ font-style: normal;
326
+ font-weight: 750;
327
+ line-height: 1.25;
328
+ }
329
+
330
+ .sat-readout {
331
+ grid-template-columns: minmax(4.6rem, 0.55fr) minmax(0, 1fr);
332
+ align-items: center;
333
+ }
334
+
335
+ .sat-readout span {
336
+ color: var(--sat-muted);
337
+ font-size: 0.72rem;
338
+ font-weight: 850;
339
+ }
340
+
341
+ .sat-readout meter {
342
+ grid-column: 1 / -1;
343
+ width: 100%;
344
+ height: 0.7rem;
345
+ }
346
+
347
+ .sat-controls p {
348
+ margin: 0;
349
+ padding: 0.75rem;
350
+ border: 1px solid color-mix(in srgb, var(--sat-center) 35%, var(--sat-line));
351
+ border-radius: 8px;
352
+ background: color-mix(in srgb, var(--sat-center) 10%, var(--sat-card));
353
+ color: var(--sat-ink);
354
+ font-size: 0.85rem;
355
+ font-weight: 750;
356
+ line-height: 1.35;
357
+ }
358
+
359
+ @media (min-width: 860px) {
360
+ .sat-panel {
361
+ grid-template-columns: minmax(0, 1.25fr) minmax(19rem, 0.75fr);
362
+ align-items: stretch;
363
+ }
364
+
365
+ .sat-stage {
366
+ min-height: 22rem;
367
+ }
368
+ }
369
+
370
+ @media (max-width: 560px) {
371
+ .sat-stage {
372
+ grid-template-columns: minmax(0, 1fr) minmax(5.5rem, 0.7fr) minmax(0, 1fr);
373
+ gap: 0.35rem;
374
+ }
375
+
376
+ .sat-orbit {
377
+ min-height: 7rem;
378
+ }
379
+
380
+ .sat-orbit i {
381
+ width: 2.35rem;
382
+ transform: translateX(calc(var(--sat-pan) * 3.75rem));
383
+ }
384
+
385
+ .sat-speaker span {
386
+ width: min(5.2rem, 28vw);
387
+ box-shadow: inset 0 0 0 0.55rem var(--sat-soft), 0 0.6rem 1.5rem var(--sat-shadow);
388
+ }
389
+
390
+ .sat-speaker strong {
391
+ top: calc(50% + min(3.05rem, 16vw));
392
+ font-size: 0.68rem;
393
+ }
394
+
395
+ .sat-control-row,
396
+ .sat-readout {
397
+ grid-template-columns: 1fr;
398
+ }
399
+
400
+ .sat-sliders strong,
401
+ .sat-readout strong {
402
+ text-align: left;
403
+ }
404
+ }
405
+
406
+ @media (prefers-reduced-motion: reduce) {
407
+ .sat-orbit i,
408
+ .sat-buttons button {
409
+ transition: none;
410
+ }
411
+ }