@jjlmoya/utils-hardware 1.28.0 → 1.30.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 +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +7 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/locale_completeness.test.ts +1 -1
  6. package/src/tests/tool_validation.test.ts +1 -1
  7. package/src/tool/mouseJitterAngleSnappingTest/bibliography.astro +14 -0
  8. package/src/tool/mouseJitterAngleSnappingTest/bibliography.ts +16 -0
  9. package/src/tool/mouseJitterAngleSnappingTest/component.astro +110 -0
  10. package/src/tool/mouseJitterAngleSnappingTest/entry.ts +29 -0
  11. package/src/tool/mouseJitterAngleSnappingTest/i18n/de.ts +289 -0
  12. package/src/tool/mouseJitterAngleSnappingTest/i18n/en.ts +289 -0
  13. package/src/tool/mouseJitterAngleSnappingTest/i18n/es.ts +289 -0
  14. package/src/tool/mouseJitterAngleSnappingTest/i18n/fr.ts +289 -0
  15. package/src/tool/mouseJitterAngleSnappingTest/i18n/id.ts +289 -0
  16. package/src/tool/mouseJitterAngleSnappingTest/i18n/it.ts +289 -0
  17. package/src/tool/mouseJitterAngleSnappingTest/i18n/ja.ts +289 -0
  18. package/src/tool/mouseJitterAngleSnappingTest/i18n/ko.ts +289 -0
  19. package/src/tool/mouseJitterAngleSnappingTest/i18n/nl.ts +289 -0
  20. package/src/tool/mouseJitterAngleSnappingTest/i18n/pl.ts +289 -0
  21. package/src/tool/mouseJitterAngleSnappingTest/i18n/pt.ts +289 -0
  22. package/src/tool/mouseJitterAngleSnappingTest/i18n/ru.ts +289 -0
  23. package/src/tool/mouseJitterAngleSnappingTest/i18n/sv.ts +289 -0
  24. package/src/tool/mouseJitterAngleSnappingTest/i18n/tr.ts +289 -0
  25. package/src/tool/mouseJitterAngleSnappingTest/i18n/zh.ts +289 -0
  26. package/src/tool/mouseJitterAngleSnappingTest/index.ts +9 -0
  27. package/src/tool/mouseJitterAngleSnappingTest/logic.ts +245 -0
  28. package/src/tool/mouseJitterAngleSnappingTest/model.ts +38 -0
  29. package/src/tool/mouseJitterAngleSnappingTest/mouse-jitter-angle-snapping-test.css +412 -0
  30. package/src/tool/mouseJitterAngleSnappingTest/render.ts +48 -0
  31. package/src/tool/mouseJitterAngleSnappingTest/seo.astro +15 -0
  32. package/src/tool/mouseJitterAngleSnappingTest/ui.ts +29 -0
  33. package/src/tool/subwooferCrossoverTest/bibliography.astro +14 -0
  34. package/src/tool/subwooferCrossoverTest/bibliography.ts +16 -0
  35. package/src/tool/subwooferCrossoverTest/component.astro +253 -0
  36. package/src/tool/subwooferCrossoverTest/entry.ts +29 -0
  37. package/src/tool/subwooferCrossoverTest/i18n/de.ts +188 -0
  38. package/src/tool/subwooferCrossoverTest/i18n/en.ts +188 -0
  39. package/src/tool/subwooferCrossoverTest/i18n/es.ts +188 -0
  40. package/src/tool/subwooferCrossoverTest/i18n/fr.ts +188 -0
  41. package/src/tool/subwooferCrossoverTest/i18n/id.ts +188 -0
  42. package/src/tool/subwooferCrossoverTest/i18n/it.ts +188 -0
  43. package/src/tool/subwooferCrossoverTest/i18n/ja.ts +188 -0
  44. package/src/tool/subwooferCrossoverTest/i18n/ko.ts +188 -0
  45. package/src/tool/subwooferCrossoverTest/i18n/nl.ts +188 -0
  46. package/src/tool/subwooferCrossoverTest/i18n/pl.ts +188 -0
  47. package/src/tool/subwooferCrossoverTest/i18n/pt.ts +188 -0
  48. package/src/tool/subwooferCrossoverTest/i18n/ru.ts +188 -0
  49. package/src/tool/subwooferCrossoverTest/i18n/sv.ts +188 -0
  50. package/src/tool/subwooferCrossoverTest/i18n/tr.ts +188 -0
  51. package/src/tool/subwooferCrossoverTest/i18n/zh.ts +188 -0
  52. package/src/tool/subwooferCrossoverTest/index.ts +11 -0
  53. package/src/tool/subwooferCrossoverTest/logic.ts +30 -0
  54. package/src/tool/subwooferCrossoverTest/seo.astro +15 -0
  55. package/src/tool/subwooferCrossoverTest/subwoofer-crossover-test.css +282 -0
  56. package/src/tool/subwooferCrossoverTest/ui.ts +20 -0
  57. package/src/tools.ts +3 -1
@@ -0,0 +1,245 @@
1
+ import { MAX_LOG, MAX_POINTS, WINDOW_SIZE, type Elements, type Labels, type PointSample, type TraceEvent, type TraceState } from './model';
2
+ import { drawGrid, drawNodes, drawTrace } from './render';
3
+
4
+ export class MouseJitterAngleSnappingTester {
5
+ private readonly points: PointSample[] = [];
6
+ private readonly events: TraceEvent[] = [];
7
+ private ctx: CanvasRenderingContext2D | null = null;
8
+ private raf = 0;
9
+ private dirty = true;
10
+ private active = false;
11
+ private nextPointFresh = true;
12
+ private sensitivity = 50;
13
+ private metrics = { jitter: 0, snapping: 0, straightness: 0, deviation: 0 };
14
+
15
+ constructor(
16
+ private readonly elements: Elements,
17
+ private readonly labels: Labels,
18
+ ) {
19
+ this.ctx = elements.canvas?.getContext('2d') ?? null;
20
+ this.sensitivity = Number(elements.sensitivity?.value ?? 50);
21
+ this.bind();
22
+ this.resize();
23
+ this.render();
24
+ }
25
+
26
+ private bind() {
27
+ window.addEventListener('resize', () => this.resize());
28
+ this.elements.stage?.addEventListener('pointerdown', (event) => this.start(event));
29
+ this.elements.stage?.addEventListener('pointermove', (event) => this.move(event));
30
+ this.elements.stage?.addEventListener('pointerup', () => this.stop());
31
+ this.elements.stage?.addEventListener('pointerleave', () => this.stop());
32
+ this.elements.sensitivity?.addEventListener('input', () => {
33
+ this.sensitivity = Number(this.elements.sensitivity?.value ?? 50);
34
+ this.analyze();
35
+ this.dirty = true;
36
+ this.renderPanel();
37
+ });
38
+ this.elements.reset?.addEventListener('click', () => this.reset());
39
+ }
40
+
41
+ private resize() {
42
+ const canvas = this.elements.canvas;
43
+ if (!canvas) return;
44
+ const rect = canvas.getBoundingClientRect();
45
+ const ratio = window.devicePixelRatio || 1;
46
+ canvas.width = Math.max(1, Math.floor(rect.width * ratio));
47
+ canvas.height = Math.max(1, Math.floor(rect.height * ratio));
48
+ this.ctx = canvas.getContext('2d');
49
+ this.ctx?.setTransform(ratio, 0, 0, ratio, 0, 0);
50
+ this.dirty = true;
51
+ }
52
+
53
+ private getCss(name: string, fallback: string) {
54
+ return getComputedStyle(this.elements.stage ?? document.documentElement).getPropertyValue(name).trim() || fallback;
55
+ }
56
+
57
+ private addPoint(event: PointerEvent) {
58
+ const canvas = this.elements.canvas;
59
+ if (!canvas) return;
60
+ const rect = canvas.getBoundingClientRect();
61
+ this.points.push({
62
+ x: event.clientX - rect.left,
63
+ y: event.clientY - rect.top,
64
+ t: performance.now(),
65
+ fresh: this.nextPointFresh,
66
+ });
67
+ this.nextPointFresh = false;
68
+ if (this.points.length > MAX_POINTS) this.points.splice(0, this.points.length - MAX_POINTS);
69
+ this.analyze();
70
+ this.dirty = true;
71
+ }
72
+
73
+ private draw() {
74
+ const canvas = this.elements.canvas;
75
+ const ctx = this.ctx;
76
+ if (!canvas || !ctx) return;
77
+ const width = canvas.clientWidth;
78
+ const height = canvas.clientHeight;
79
+ ctx.clearRect(0, 0, width, height);
80
+ drawGrid(ctx, width, height, (name, fallback) => this.getCss(name, fallback));
81
+
82
+ if (this.points.length > 1) {
83
+ const options = { points: this.points, jitter: this.metrics.jitter, getCss: (name: string, fallback: string) => this.getCss(name, fallback) };
84
+ drawTrace(ctx, options);
85
+ drawNodes(ctx, options);
86
+ }
87
+
88
+ this.dirty = false;
89
+ }
90
+
91
+ private start(event: PointerEvent) {
92
+ this.active = true;
93
+ this.nextPointFresh = true;
94
+ this.elements.stage?.setPointerCapture(event.pointerId);
95
+ this.addPoint(event);
96
+ }
97
+
98
+ private move(event: PointerEvent) {
99
+ if (!this.active) return;
100
+ const coalesced = typeof event.getCoalescedEvents === 'function' ? event.getCoalescedEvents() : [event];
101
+ coalesced.forEach((sample) => this.addPoint(sample));
102
+ }
103
+
104
+ private stop() {
105
+ this.active = false;
106
+ }
107
+
108
+ private analyze() {
109
+ if (this.points.length < 4) {
110
+ this.resetMetrics();
111
+ return;
112
+ }
113
+
114
+ const recent = this.points.slice(-Math.max(WINDOW_SIZE, Math.round(this.sensitivity * 0.7)));
115
+ const deviations = this.getLineDeviations(recent);
116
+ const averageDeviation = deviations.reduce((sum, value) => sum + value, 0) / Math.max(1, deviations.length);
117
+ const maxDeviation = Math.max(...deviations, 0);
118
+ const angles = this.getAngles(recent);
119
+ const straightness = this.getStraightness(angles);
120
+ const jitter = Math.min(100, Math.round((averageDeviation * 12 + maxDeviation * 3) * (100 / this.sensitivity)));
121
+ const snapping = Math.min(100, Math.round(straightness * 100));
122
+
123
+ this.metrics = { jitter, snapping, straightness: Math.round(straightness * 100), deviation: Math.round(averageDeviation * 10) / 10 };
124
+
125
+ this.recordEvent();
126
+ this.renderPanel();
127
+ }
128
+
129
+ private resetMetrics() {
130
+ this.metrics = { jitter: 0, snapping: 0, straightness: 0, deviation: 0 };
131
+ this.renderPanel();
132
+ }
133
+
134
+ private getLineDeviations(points: PointSample[]) {
135
+ const first = points[0];
136
+ const last = points[points.length - 1];
137
+ const dx = last.x - first.x;
138
+ const dy = last.y - first.y;
139
+ const length = Math.hypot(dx, dy);
140
+ if (length < 1) return [0];
141
+ return points.slice(1, -1).map((point) => Math.abs(dy * point.x - dx * point.y + last.x * first.y - last.y * first.x) / length);
142
+ }
143
+
144
+ private getAngles(points: PointSample[]) {
145
+ const angles: number[] = [];
146
+ for (let i = 1; i < points.length; i++) {
147
+ const dx = points[i].x - points[i - 1].x;
148
+ const dy = points[i].y - points[i - 1].y;
149
+ if (Math.hypot(dx, dy) >= 1.5) angles.push(Math.atan2(dy, dx));
150
+ }
151
+ return angles;
152
+ }
153
+
154
+ private getStraightness(angles: number[]) {
155
+ if (angles.length < 5) return 0;
156
+ const snapped = angles.filter((angle) => {
157
+ const degrees = Math.abs((angle * 180) / Math.PI);
158
+ const axis = Math.min(degrees % 90, 90 - (degrees % 90));
159
+ return axis <= 2.2;
160
+ }).length;
161
+ return snapped / angles.length;
162
+ }
163
+
164
+ private recordEvent() {
165
+ if (this.points.length % 18 !== 0) return;
166
+ let state: TraceState = 'clean';
167
+ let value = this.labels.cleanEvent;
168
+ if (this.metrics.jitter > 58 && this.metrics.snapping > 58) {
169
+ state = 'jitter';
170
+ value = this.labels.combinedEvent;
171
+ } else if (this.metrics.jitter > 58) {
172
+ state = 'jitter';
173
+ value = `${this.labels.jitterEvent} ${this.metrics.deviation}${this.labels.pxUnit}`;
174
+ } else if (this.metrics.snapping > 70) {
175
+ state = 'snapping';
176
+ value = `${this.labels.snappingEvent} ${this.metrics.snapping}${this.labels.percentUnit}`;
177
+ }
178
+ this.events.unshift({ state, value });
179
+ this.events.length = Math.min(this.events.length, MAX_LOG);
180
+ }
181
+
182
+ private render() {
183
+ if (this.dirty) this.draw();
184
+ this.raf = window.requestAnimationFrame(() => this.render());
185
+ }
186
+
187
+ private setText(element: HTMLElement | null, value: string) {
188
+ if (element) element.textContent = value;
189
+ }
190
+
191
+ private renderPanel() {
192
+ this.setText(this.elements.samples, String(this.points.length));
193
+ this.setText(this.elements.jitterScore, `${this.metrics.jitter}${this.labels.percentUnit}`);
194
+ this.setText(this.elements.snappingScore, `${this.metrics.snapping}${this.labels.percentUnit}`);
195
+ this.setText(this.elements.straightness, `${this.metrics.straightness}${this.labels.percentUnit}`);
196
+ this.setText(this.elements.deviation, `${this.metrics.deviation}${this.labels.pxUnit}`);
197
+ this.setText(this.elements.sensitivityValue, String(this.sensitivity));
198
+ this.renderStatus();
199
+ this.renderLog();
200
+ }
201
+
202
+ private renderStatus() {
203
+ const status = this.elements.status;
204
+ if (!status) return;
205
+ let text = this.labels.statusIdle;
206
+ let state = 'idle';
207
+ if (this.points.length > 8) {
208
+ const hasJitter = this.metrics.jitter > 58;
209
+ const hasSnapping = this.metrics.snapping > 70;
210
+ if (hasJitter && hasSnapping) {
211
+ text = this.labels.statusMixed;
212
+ state = 'mixed';
213
+ } else if (hasJitter) {
214
+ text = this.labels.statusJitter;
215
+ state = 'jitter';
216
+ } else if (hasSnapping) {
217
+ text = this.labels.statusSnapping;
218
+ state = 'snapping';
219
+ } else {
220
+ text = this.labels.statusHealthy;
221
+ state = 'healthy';
222
+ }
223
+ }
224
+ status.textContent = text;
225
+ status.dataset.state = state;
226
+ }
227
+
228
+ private renderLog() {
229
+ const log = this.elements.log;
230
+ if (!log) return;
231
+ if (this.events.length === 0) {
232
+ log.innerHTML = `<li class="mjas-empty">${this.labels.emptyLog}</li>`;
233
+ return;
234
+ }
235
+ log.innerHTML = this.events.map((event) => `<li class="${event.state}"><span>${event.value}</span></li>`).join('');
236
+ }
237
+
238
+ private reset() {
239
+ this.points.length = 0;
240
+ this.events.length = 0;
241
+ this.metrics = { jitter: 0, snapping: 0, straightness: 0, deviation: 0 };
242
+ this.dirty = true;
243
+ this.renderPanel();
244
+ }
245
+ }
@@ -0,0 +1,38 @@
1
+ export type TraceState = 'clean' | 'jitter' | 'snapping';
2
+
3
+ export interface PointSample { x: number; y: number; t: number; fresh?: boolean; }
4
+ export interface TraceEvent { state: TraceState; value: string; }
5
+
6
+ export interface Elements {
7
+ stage: HTMLElement | null;
8
+ canvas: HTMLCanvasElement | null;
9
+ samples: HTMLElement | null;
10
+ jitterScore: HTMLElement | null;
11
+ snappingScore: HTMLElement | null;
12
+ straightness: HTMLElement | null;
13
+ deviation: HTMLElement | null;
14
+ status: HTMLElement | null;
15
+ sensitivity: HTMLInputElement | null;
16
+ sensitivityValue: HTMLElement | null;
17
+ reset: HTMLElement | null;
18
+ log: HTMLElement | null;
19
+ }
20
+
21
+ export interface Labels {
22
+ statusIdle: string;
23
+ statusHealthy: string;
24
+ statusJitter: string;
25
+ statusSnapping: string;
26
+ statusMixed: string;
27
+ emptyLog: string;
28
+ jitterEvent: string;
29
+ snappingEvent: string;
30
+ combinedEvent: string;
31
+ cleanEvent: string;
32
+ pxUnit: string;
33
+ percentUnit: string;
34
+ }
35
+
36
+ export const MAX_POINTS = 2400;
37
+ export const MAX_LOG = 9;
38
+ export const WINDOW_SIZE = 32;
@@ -0,0 +1,412 @@
1
+ .mjas-root {
2
+ box-sizing: border-box;
3
+ width: 100%;
4
+ max-width: 100%;
5
+ min-width: 0;
6
+ }
7
+
8
+ .mjas-root *,
9
+ .mjas-root *::before,
10
+ .mjas-root *::after {
11
+ box-sizing: inherit;
12
+ }
13
+
14
+ .mjas-shell {
15
+ display: grid;
16
+ grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr);
17
+ gap: 1rem;
18
+ width: 100%;
19
+ min-width: 0;
20
+ padding: clamp(0.8rem, 2.5vw, 1.1rem);
21
+ border: 1px solid rgba(12, 74, 110, 0.2);
22
+ border-radius: 8px;
23
+ background:
24
+ linear-gradient(135deg, rgba(241, 245, 249, 0.98), rgba(224, 242, 254, 0.9)),
25
+ repeating-linear-gradient(135deg, rgba(14, 116, 144, 0.07) 0 1px, transparent 1px 18px);
26
+ color: #0f172a;
27
+ box-shadow: 0 20px 60px rgba(15, 23, 42, 0.13);
28
+ }
29
+
30
+ .theme-dark .mjas-shell {
31
+ border-color: rgba(125, 211, 252, 0.25);
32
+ background:
33
+ linear-gradient(135deg, rgba(2, 6, 23, 0.96), rgba(12, 74, 110, 0.76)),
34
+ repeating-linear-gradient(135deg, rgba(103, 232, 249, 0.08) 0 1px, transparent 1px 18px);
35
+ color: #e0f2fe;
36
+ }
37
+
38
+ .mjas-stage {
39
+ --mjas-canvas-bg: #f8fafc;
40
+ --mjas-grid: rgba(14, 116, 144, 0.16);
41
+ --mjas-line: #0e7490;
42
+ --mjas-node: #ca8a04;
43
+ --mjas-hot-node: #e11d48;
44
+
45
+ position: relative;
46
+ min-height: 520px;
47
+ overflow: hidden;
48
+ border: 1px solid rgba(14, 116, 144, 0.24);
49
+ border-radius: 8px;
50
+ background: #f8fafc;
51
+ cursor: crosshair;
52
+ touch-action: none;
53
+ }
54
+
55
+ .theme-dark .mjas-stage {
56
+ --mjas-canvas-bg: #030712;
57
+ --mjas-grid: rgba(186, 230, 253, 0.12);
58
+ --mjas-line: #67e8f9;
59
+ --mjas-node: #facc15;
60
+ --mjas-hot-node: #f43f5e;
61
+ }
62
+
63
+ .mjas-canvas {
64
+ display: block;
65
+ width: 100%;
66
+ height: 100%;
67
+ min-height: 520px;
68
+ }
69
+
70
+ .mjas-canvas-copy {
71
+ position: absolute;
72
+ inset: auto 1rem 1rem;
73
+ display: grid;
74
+ gap: 0.35rem;
75
+ max-width: 34rem;
76
+ pointer-events: none;
77
+ }
78
+
79
+ .mjas-canvas-copy span {
80
+ display: inline-flex;
81
+ align-items: center;
82
+ gap: 0.4rem;
83
+ width: fit-content;
84
+ padding: 0.35rem 0.55rem;
85
+ border: 1px solid rgba(14, 116, 144, 0.22);
86
+ border-radius: 999px;
87
+ background: rgba(255, 255, 255, 0.86);
88
+ color: #0f5f76;
89
+ font-size: 0.78rem;
90
+ font-weight: 900;
91
+ text-transform: uppercase;
92
+ }
93
+
94
+ .mjas-canvas-copy svg {
95
+ width: 1rem;
96
+ height: 1rem;
97
+ }
98
+
99
+ .mjas-canvas-copy strong {
100
+ color: #0f172a;
101
+ font-size: clamp(1.3rem, 4.5vw, 2.2rem);
102
+ line-height: 1.08;
103
+ text-shadow: 0 2px 18px rgba(255, 255, 255, 0.78);
104
+ }
105
+
106
+ .mjas-canvas-copy small {
107
+ color: #334155;
108
+ font-size: 0.95rem;
109
+ font-weight: 700;
110
+ line-height: 1.35;
111
+ }
112
+
113
+ .theme-dark .mjas-canvas-copy span {
114
+ border-color: rgba(125, 211, 252, 0.24);
115
+ background: rgba(8, 47, 73, 0.72);
116
+ color: #bae6fd;
117
+ }
118
+
119
+ .theme-dark .mjas-canvas-copy strong {
120
+ color: #f8fafc;
121
+ text-shadow: 0 2px 18px rgba(0, 0, 0, 0.7);
122
+ }
123
+
124
+ .theme-dark .mjas-canvas-copy small {
125
+ color: #cbd5e1;
126
+ }
127
+
128
+ .mjas-console {
129
+ display: grid;
130
+ gap: 0.75rem;
131
+ align-content: start;
132
+ min-width: 0;
133
+ }
134
+
135
+ .mjas-status,
136
+ .mjas-score-grid > div,
137
+ .mjas-readouts > div,
138
+ .mjas-sensitivity,
139
+ .mjas-log {
140
+ border: 1px solid rgba(14, 116, 144, 0.18);
141
+ border-radius: 8px;
142
+ background: rgba(255, 255, 255, 0.75);
143
+ }
144
+
145
+ .theme-dark .mjas-status,
146
+ .theme-dark .mjas-score-grid > div,
147
+ .theme-dark .mjas-readouts > div,
148
+ .theme-dark .mjas-sensitivity,
149
+ .theme-dark .mjas-log {
150
+ border-color: rgba(125, 211, 252, 0.16);
151
+ background: rgba(15, 23, 42, 0.58);
152
+ }
153
+
154
+ .mjas-status {
155
+ display: grid;
156
+ grid-template-rows: auto minmax(4.4rem, auto) minmax(2.6rem, auto);
157
+ gap: 0.2rem;
158
+ padding: 1rem;
159
+ }
160
+
161
+ .mjas-status span,
162
+ .mjas-score-grid span,
163
+ .mjas-readouts span,
164
+ .mjas-sensitivity span,
165
+ .mjas-log-head {
166
+ color: #475569;
167
+ font-size: 0.76rem;
168
+ font-weight: 900;
169
+ text-transform: uppercase;
170
+ }
171
+
172
+ .theme-dark .mjas-status span,
173
+ .theme-dark .mjas-score-grid span,
174
+ .theme-dark .mjas-readouts span,
175
+ .theme-dark .mjas-sensitivity span,
176
+ .theme-dark .mjas-log-head {
177
+ color: #bae6fd;
178
+ }
179
+
180
+ .mjas-status strong {
181
+ font-size: clamp(2.6rem, 9vw, 4.6rem);
182
+ line-height: 0.95;
183
+ }
184
+
185
+ .mjas-status small {
186
+ display: block;
187
+ min-height: 2.6rem;
188
+ color: #334155;
189
+ font-weight: 800;
190
+ line-height: 1.3;
191
+ overflow-wrap: anywhere;
192
+ }
193
+
194
+ .mjas-status small[data-state="healthy"] {
195
+ color: #047857;
196
+ }
197
+
198
+ .mjas-status small[data-state="jitter"],
199
+ .mjas-status small[data-state="mixed"] {
200
+ color: #be123c;
201
+ }
202
+
203
+ .mjas-status small[data-state="snapping"] {
204
+ color: #b45309;
205
+ }
206
+
207
+ .theme-dark .mjas-status small {
208
+ color: #dbeafe;
209
+ }
210
+
211
+ .theme-dark .mjas-status small[data-state="healthy"] {
212
+ color: #86efac;
213
+ }
214
+
215
+ .theme-dark .mjas-status small[data-state="jitter"],
216
+ .theme-dark .mjas-status small[data-state="mixed"] {
217
+ color: #fda4af;
218
+ }
219
+
220
+ .theme-dark .mjas-status small[data-state="snapping"] {
221
+ color: #fde68a;
222
+ }
223
+
224
+ .mjas-score-grid,
225
+ .mjas-readouts,
226
+ .mjas-actions {
227
+ display: grid;
228
+ gap: 0.65rem;
229
+ }
230
+
231
+ .mjas-score-grid,
232
+ .mjas-readouts {
233
+ grid-template-columns: repeat(2, minmax(0, 1fr));
234
+ }
235
+
236
+ .mjas-score-grid > div,
237
+ .mjas-readouts > div {
238
+ display: grid;
239
+ grid-template-rows: minmax(1.9rem, auto) minmax(2.35rem, auto);
240
+ gap: 0.2rem;
241
+ min-width: 0;
242
+ padding: 0.85rem;
243
+ }
244
+
245
+ .mjas-score-grid strong,
246
+ .mjas-readouts strong {
247
+ display: flex;
248
+ align-items: end;
249
+ min-height: 2.35rem;
250
+ overflow-wrap: anywhere;
251
+ font-size: clamp(1.35rem, 5vw, 2rem);
252
+ line-height: 1.05;
253
+ }
254
+
255
+ .mjas-sensitivity {
256
+ display: grid;
257
+ grid-template-rows: minmax(1.2rem, auto) minmax(1.5rem, auto) auto;
258
+ gap: 0.55rem;
259
+ padding: 0.85rem;
260
+ }
261
+
262
+ .mjas-sensitivity b {
263
+ display: grid;
264
+ grid-template-columns: auto 1fr auto;
265
+ gap: 0.5rem;
266
+ align-items: center;
267
+ color: #0369a1;
268
+ font-style: normal;
269
+ }
270
+
271
+ .mjas-sensitivity em {
272
+ color: #64748b;
273
+ font-style: normal;
274
+ font-size: 0.78rem;
275
+ }
276
+
277
+ .theme-dark .mjas-sensitivity b {
278
+ color: #67e8f9;
279
+ }
280
+
281
+ .theme-dark .mjas-sensitivity em {
282
+ color: #cbd5e1;
283
+ }
284
+
285
+ .mjas-sensitivity input {
286
+ width: 100%;
287
+ accent-color: #0ea5e9;
288
+ }
289
+
290
+ .mjas-actions button {
291
+ display: inline-flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ gap: 0.4rem;
295
+ min-width: 0;
296
+ border: 0;
297
+ border-radius: 8px;
298
+ background: #0f172a;
299
+ color: #fff;
300
+ cursor: pointer;
301
+ font: inherit;
302
+ font-weight: 900;
303
+ padding: 0.7rem 0.65rem;
304
+ }
305
+
306
+ .mjas-actions svg {
307
+ flex: 0 0 auto;
308
+ width: 1rem;
309
+ height: 1rem;
310
+ }
311
+
312
+ .theme-dark .mjas-actions button {
313
+ background: #e0f2fe;
314
+ color: #082f49;
315
+ }
316
+
317
+ .mjas-shift {
318
+ margin: 0;
319
+ min-height: 4.35rem;
320
+ padding: 0.75rem 0.85rem;
321
+ border: 1px solid rgba(202, 138, 4, 0.24);
322
+ border-radius: 8px;
323
+ background: rgba(254, 249, 195, 0.82);
324
+ color: #713f12;
325
+ font-size: 0.9rem;
326
+ font-weight: 750;
327
+ line-height: 1.35;
328
+ }
329
+
330
+ .theme-dark .mjas-shift {
331
+ border-color: rgba(250, 204, 21, 0.22);
332
+ background: rgba(113, 63, 18, 0.4);
333
+ color: #fef3c7;
334
+ }
335
+
336
+ .mjas-log {
337
+ display: grid;
338
+ grid-auto-rows: minmax(2.2rem, auto);
339
+ gap: 0.45rem;
340
+ min-height: 8.8rem;
341
+ height: 13rem;
342
+ max-height: 13rem;
343
+ overflow: auto;
344
+ margin: 0;
345
+ padding: 0.75rem;
346
+ list-style: none;
347
+ }
348
+
349
+ .mjas-log li,
350
+ .mjas-empty {
351
+ padding: 0.55rem 0.65rem;
352
+ border-radius: 8px;
353
+ background: rgba(224, 242, 254, 0.78);
354
+ color: #0f172a;
355
+ font-size: 0.86rem;
356
+ font-weight: 800;
357
+ overflow-wrap: anywhere;
358
+ }
359
+
360
+ .mjas-log li.jitter {
361
+ background: rgba(255, 228, 230, 0.9);
362
+ color: #9f1239;
363
+ }
364
+
365
+ .mjas-log li.snapping {
366
+ background: rgba(254, 243, 199, 0.9);
367
+ color: #92400e;
368
+ }
369
+
370
+ .theme-dark .mjas-log li,
371
+ .theme-dark .mjas-empty {
372
+ background: rgba(8, 47, 73, 0.72);
373
+ color: #dbeafe;
374
+ }
375
+
376
+ .theme-dark .mjas-log li.jitter {
377
+ background: rgba(136, 19, 55, 0.58);
378
+ color: #ffe4e6;
379
+ }
380
+
381
+ .theme-dark .mjas-log li.snapping {
382
+ background: rgba(120, 53, 15, 0.58);
383
+ color: #fef3c7;
384
+ }
385
+
386
+ @media (max-width: 880px) {
387
+ .mjas-shell {
388
+ grid-template-columns: 1fr;
389
+ }
390
+
391
+ .mjas-stage,
392
+ .mjas-canvas {
393
+ min-height: 430px;
394
+ }
395
+ }
396
+
397
+ @media (max-width: 520px) {
398
+ .mjas-shell {
399
+ padding: 0.7rem;
400
+ }
401
+
402
+ .mjas-score-grid,
403
+ .mjas-readouts,
404
+ .mjas-actions {
405
+ grid-template-columns: 1fr;
406
+ }
407
+
408
+ .mjas-stage,
409
+ .mjas-canvas {
410
+ min-height: 360px;
411
+ }
412
+ }