corrected all bugs

This commit is contained in:
2026-01-11 15:33:44 +01:00
parent 46ee44c6c9
commit 9837657dd9
10 changed files with 992 additions and 497 deletions

View File

@@ -112,13 +112,13 @@
<div class="row">
<WebSwitch status={status?.webswitch} />
<PowerGenius status={status?.power_genius} />
<TunerGenius status={status?.tuner_genius} />
<TunerGenius status={status?.tuner_genius} flexradio={status?.flexradio} />
</div>
<div class="row">
<AntennaGenius status={status?.antenna_genius} />
<Ultrabeam status={status?.ultrabeam} />
<RotatorGenius status={status?.rotator_genius} />
<Ultrabeam status={status?.ultrabeam} flexradio={status?.flexradio} />
<RotatorGenius status={status?.rotator_genius} ultrabeam={status?.ultrabeam} />
</div>
</div>
</main>
@@ -292,4 +292,4 @@
flex-wrap: wrap;
}
}
</style>
</style>

View File

@@ -7,15 +7,6 @@
$: powerReflected = status?.power_reflected || 0;
$: swr = status?.swr || 1.0;
// Debug logging
$: if (status) {
console.log('PowerGenius status update:', {
powerForward: status.power_forward,
swr: status.swr,
state: status.state,
connected: status.connected
});
}
$: voltage = status?.voltage || 0;
$: vdd = status?.vdd || 0;
$: current = status?.current || 0;

View File

@@ -2,31 +2,66 @@
import { api } from '../lib/api.js';
export let status;
export let ultrabeam = null;
let heading = 0;
let heading = null; // Start with null instead of 0
let connected = false;
// Update heading only if we have a valid value from status
// Get Ultrabeam direction mode: 0=Normal, 1=180°, 2=Bi-Dir
$: ultrabeamDirection = ultrabeam?.direction ?? 0;
// Update heading with detailed logging to debug
$: if (status?.heading !== undefined && status?.heading !== null) {
heading = status.heading;
const newHeading = status.heading;
const oldHeading = heading;
console.log(`RotatorGenius heading update: ${oldHeading} -> ${newHeading}`);
if (heading === null) {
// First time: accept any value
heading = newHeading;
console.log(` ✓ First load, set to ${heading}°`);
} else if (newHeading === 0 && heading > 10 && heading < 350) {
// Ignore sudden jump to 0 from middle range (glitch)
console.log(` ✗ IGNORED glitch jump from ${heading}° to 0°`);
} else {
// Normal update
heading = newHeading;
console.log(` ✓ Updated to ${heading}°`);
}
}
// Display heading: use cached value or 0 if never set
$: displayHeading = heading !== null ? heading : 0;
$: connected = status?.connected || false;
let targetHeading = 0;
let hasTarget = false;
// Clear target when we reach it (within 5 degrees)
$: if (hasTarget && heading !== null) {
const diff = Math.abs(heading - targetHeading);
const wrappedDiff = Math.min(diff, 360 - diff);
if (wrappedDiff < 5) {
hasTarget = false;
}
}
async function goToHeading() {
if (targetHeading < 0 || targetHeading > 359) {
alert('Heading must be between 0 and 359');
// Removed alert popup - check console for errors
return;
}
try {
hasTarget = true; // Mark that we have a target
// Subtract 10 degrees to compensate for rotator momentum
const adjustedHeading = (targetHeading - 10 + 360) % 360;
await api.rotator.setHeading(adjustedHeading);
} catch (err) {
console.error('Failed to set heading:', err);
alert('Failed to rotate');
hasTarget = false;
// Removed alert popup - check console for errors
}
}
@@ -89,7 +124,12 @@
<div class="heading-controls-row">
<div class="heading-display-compact">
<div class="heading-label">CURRENT HEADING</div>
<div class="heading-value">{heading}°</div>
<div class="heading-value">
{displayHeading}°
{#if hasTarget}
<span class="target-indicator">{targetHeading}°</span>
{/if}
</div>
</div>
<div class="controls-compact">
@@ -107,7 +147,12 @@
<!-- Map with Beam -->
<div class="map-container">
<svg viewBox="0 0 300 300" class="map-svg clickable-compass" on:click={handleCompassClick}>
<svg viewBox="0 0 300 300" class="map-svg clickable-compass"
on:click={handleCompassClick}
on:keydown={(e) => e.key === 'Enter' && handleCompassClick(e)}
role="button"
tabindex="0"
aria-label="Click to rotate antenna to direction">
<defs>
<!-- Gradient for beam -->
<radialGradient id="beamGradient">
@@ -126,30 +171,139 @@
<!-- Rotated group for beam -->
<g transform="translate(150, 150)">
<!-- Beam (rotates with heading) -->
<g transform="rotate({heading})">
<!-- Beam sector (±15° = 30° total beamwidth) -->
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130}
A 130,130 0 0,1 {Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130} Z"
fill="url(#beamGradient)"
opacity="0.85"/>
<!-- Beam outline -->
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
<!-- Direction arrow -->
<g transform="translate(0, -110)">
<polygon points="0,-20 -8,5 0,0 8,5"
fill="#4fc3f7"
stroke="#0288d1"
stroke-width="2"
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
<!-- Physical antenna direction indicator (only in 180° or Bi-Dir mode) -->
{#if ultrabeamDirection === 1 || ultrabeamDirection === 2}
<g transform="rotate({displayHeading})">
<!-- Gray dashed line showing physical antenna direction -->
<line x1="0" y1="0" x2="0" y2="-125"
stroke="rgba(255, 255, 255, 0.3)"
stroke-width="2"
stroke-dasharray="5,5"
opacity="0.6"/>
<!-- Small triangle at end to show physical direction -->
<g transform="translate(0, -125)">
<polygon points="0,-8 -5,5 5,5"
fill="rgba(255, 255, 255, 0.4)"
stroke="rgba(255, 255, 255, 0.5)"
stroke-width="1"/>
</g>
</g>
{/if}
<!-- Beam (rotates with heading) -->
<g transform="rotate({displayHeading})">
<!-- NORMAL MODE (0): Forward beam only -->
{#if ultrabeamDirection === 0}
<!-- Beam sector (±15° = 30° total beamwidth) -->
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130}
A 130,130 0 0,1 {Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130} Z"
fill="url(#beamGradient)"
opacity="0.85"/>
<!-- Beam outline -->
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
<!-- Direction arrow -->
<g transform="translate(0, -110)">
<polygon points="0,-20 -8,5 0,0 8,5"
fill="#4fc3f7"
stroke="#0288d1"
stroke-width="2"
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
</g>
{/if}
<!-- 180° MODE (1): Backward beam only -->
{#if ultrabeamDirection === 1}
<!-- Beam sector pointing BACKWARD (180° opposite) -->
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{Math.cos(15 * Math.PI/180) * 130}
A 130,130 0 0,0 {Math.sin(15 * Math.PI/180) * 130},{Math.cos(15 * Math.PI/180) * 130} Z"
fill="url(#beamGradient)"
opacity="0.85"/>
<!-- Beam outline -->
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.9"/>
<!-- Direction arrow pointing BACKWARD -->
<g transform="translate(0, 110)">
<polygon points="0,20 -8,-5 0,0 8,-5"
fill="#4fc3f7"
stroke="#0288d1"
stroke-width="2"
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
</g>
{/if}
<!-- BI-DIRECTIONAL MODE (2): Both forward AND backward beams -->
{#if ultrabeamDirection === 2}
<!-- Forward beam -->
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130}
A 130,130 0 0,1 {Math.sin(15 * Math.PI/180) * 130},{-Math.cos(15 * Math.PI/180) * 130} Z"
fill="url(#beamGradient)"
opacity="0.7"/>
<!-- Backward beam -->
<path d="M 0,0 L {-Math.sin(15 * Math.PI/180) * 130},{Math.cos(15 * Math.PI/180) * 130}
A 130,130 0 0,0 {Math.sin(15 * Math.PI/180) * 130},{Math.cos(15 * Math.PI/180) * 130} Z"
fill="url(#beamGradient)"
opacity="0.7"/>
<!-- Beam outlines -->
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.8"/>
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={-Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.8"/>
<line x1="0" y1="0" x2={-Math.sin(15 * Math.PI/180) * 130} y2={Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.8"/>
<line x1="0" y1="0" x2={Math.sin(15 * Math.PI/180) * 130} y2={Math.cos(15 * Math.PI/180) * 130}
stroke="#4fc3f7" stroke-width="2" opacity="0.8"/>
<!-- Direction arrows (both directions) -->
<g transform="translate(0, -110)">
<polygon points="0,-20 -8,5 0,0 8,5"
fill="#4fc3f7"
stroke="#0288d1"
stroke-width="2"
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
</g>
<g transform="translate(0, 110)">
<polygon points="0,20 -8,-5 0,0 8,-5"
fill="#4fc3f7"
stroke="#0288d1"
stroke-width="2"
style="filter: drop-shadow(0 0 10px rgba(79, 195, 247, 1))"/>
</g>
{/if}
</g>
<!-- Target arrow (if we have a target) -->
{#if hasTarget}
<g transform="rotate({targetHeading})">
<line x1="0" y1="0" x2="0" y2="-120"
stroke="#ffc107"
stroke-width="3"
stroke-dasharray="8,4"
opacity="0.9"/>
<g transform="translate(0, -120)">
<polygon points="0,-15 -10,10 0,5 10,10"
fill="#ffc107"
stroke="#ff9800"
stroke-width="2"
style="filter: drop-shadow(0 0 10px rgba(255, 193, 7, 0.8))">
<animate attributeName="opacity" values="0.8;1;0.8" dur="1s" repeatCount="indefinite"/>
</polygon>
</g>
</g>
{/if}
<!-- Center dot (your QTH - JN36dg) -->
<circle cx="0" cy="0" r="5" fill="#f44336" stroke="#fff" stroke-width="2">
<animate attributeName="r" values="5;7;5" dur="2s" repeatCount="indefinite"/>
@@ -175,6 +329,24 @@
</svg>
</div>
<!-- Legend (only show in 180° or Bi-Dir mode) -->
{#if ultrabeamDirection === 1 || ultrabeamDirection === 2}
<div class="map-legend">
<div class="legend-item">
<svg width="30" height="20" viewBox="0 0 30 20">
<line x1="5" y1="10" x2="25" y2="10" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-dasharray="3,3"/>
</svg>
<span>Physical antenna</span>
</div>
<div class="legend-item">
<svg width="30" height="20" viewBox="0 0 30 20">
<line x1="5" y1="10" x2="25" y2="10" stroke="#4fc3f7" stroke-width="2"/>
</svg>
<span>Radiation pattern</span>
</div>
</div>
{/if}
<!-- Go To Heading -->
</div>
</div>
@@ -226,14 +398,6 @@
gap: 10px;
}
/* Heading Display */
.heading-display {
text-align: center;
padding: 12px;
background: rgba(79, 195, 247, 0.1);
border-radius: 6px;
border: 1px solid rgba(79, 195, 247, 0.3);
}
.heading-controls-row {
display: flex;
@@ -259,45 +423,39 @@
.btn-mini {
width: 36px;
height: 36px;
border: none;
border: 2px solid rgba(79, 195, 247, 0.3);
border-radius: 6px;
font-size: 20px;
font-weight: bold;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
background: rgba(79, 195, 247, 0.08);
}
.btn-mini.ccw {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
.btn-mini:hover {
border-color: rgba(79, 195, 247, 0.6);
color: rgba(255, 255, 255, 0.9);
background: rgba(79, 195, 247, 0.15);
transform: translateY(-1px);
box-shadow: 0 0 15px rgba(79, 195, 247, 0.3);
}
.btn-mini.ccw:hover {
transform: rotate(-15deg) scale(1.05);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-mini.stop {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.btn-mini.stop:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(245, 87, 108, 0.4);
}
.btn-mini.cw {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
transform: translateY(-1px) rotate(-5deg);
}
.btn-mini.cw:hover {
transform: rotate(15deg) scale(1.05);
box-shadow: 0 4px 12px rgba(79, 172, 254, 0.4);
transform: translateY(-1px) rotate(5deg);
}
.btn-mini.stop:hover {
border-color: #f44336;
color: #f44336;
background: rgba(244, 67, 54, 0.15);
}
.heading-label {
@@ -315,6 +473,20 @@
text-shadow: 0 0 20px rgba(79, 195, 247, 0.5);
}
.target-indicator {
font-size: 24px;
font-weight: 400;
color: #ffc107;
margin-left: 20px;
text-shadow: 0 0 15px rgba(255, 193, 7, 0.6);
animation: targetPulse 1s ease-in-out infinite;
}
@keyframes targetPulse {
0%, 100% { opacity: 0.8; }
50% { opacity: 1; }
}
/* Map */
.map-container {
display: flex;
@@ -324,6 +496,24 @@
border-radius: 8px;
}
.map-legend {
display: flex;
gap: 20px;
justify-content: center;
padding: 8px;
margin-top: 8px;
background: rgba(10, 22, 40, 0.4);
border-radius: 6px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.map-svg {
width: 100%;
max-width: 300px;
@@ -352,70 +542,4 @@
font-weight: 600;
}
/* Go To Heading */
.goto-container {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
}
.go-btn {
padding: 10px 24px;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
cursor: pointer;
border: none;
background: linear-gradient(135deg, var(--accent-cyan), #0288d1);
color: #000;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(79, 195, 247, 0.4);
}
.go-btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(79, 195, 247, 0.5);
}
/* Controls */
.controls {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 8px;
}
.control-btn {
padding: 12px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
cursor: pointer;
border: 1px solid var(--border-color);
background: var(--bg-tertiary);
color: var(--text-primary);
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.control-btn:hover {
border-color: var(--accent-cyan);
transform: translateY(-1px);
}
.control-btn.stop {
background: linear-gradient(135deg, #f44336, #d32f2f);
border-color: #f44336;
color: white;
box-shadow: 0 4px 12px rgba(244, 67, 54, 0.4);
}
.control-btn.stop:hover {
box-shadow: 0 6px 16px rgba(244, 67, 54, 0.5);
}
</style>
</style>

View File

@@ -2,6 +2,7 @@
import { api } from '../lib/api.js';
export let status;
export let flexradio = null;
$: connected = status?.connected || false;
$: frequency = status?.frequency || 0;
@@ -13,6 +14,21 @@
$: elementLengths = status?.element_lengths || [];
$: firmwareVersion = status ? `${status.firmware_major}.${status.firmware_minor}` : '0.0';
// FlexRadio interlock
$: interlockConnected = flexradio?.connected || false;
$: interlockState = flexradio?.interlock_state || null;
$: interlockColor = getInterlockColor(interlockState);
function getInterlockColor(state) {
switch(state) {
case 'READY': return '#4caf50';
case 'NOT_READY': return '#f44336';
case 'PTT_REQUESTED': return '#ffc107';
case 'TRANSMITTING': return '#ff9800';
default: return 'rgba(255, 255, 255, 0.3)';
}
}
// Band names mapping - VL2.3 covers 6M to 40M only
// Band 0=6M, 1=10M, 2=12M, 3=15M, 4=17M, 5=20M, 6=30M, 7=40M
const bandNames = [
@@ -102,7 +118,7 @@
await api.ultrabeam.setAutoTrack(autoTrackEnabled, autoTrackThreshold);
} catch (err) {
console.error('Failed to update auto-track:', err);
alert('Failed to update auto-track settings');
// Removed alert popup - check console for errors
}
}
@@ -114,7 +130,7 @@
await api.ultrabeam.retract();
} catch (err) {
console.error('Failed to retract:', err);
alert('Failed to retract');
// Removed alert popup - check console for errors
}
}
@@ -122,11 +138,11 @@
try {
const newLength = elementLengths[selectedElement] + elementAdjustment;
// TODO: Add API call when backend supports it
alert(`Would adjust element ${selectedElement} by ${elementAdjustment}mm to ${newLength}mm`);
// Removed alert popup - check console for errors
elementAdjustment = 0;
} catch (err) {
console.error('Failed to adjust element:', err);
alert('Failed to adjust element');
// Removed alert popup - check console for errors
}
}
@@ -137,7 +153,17 @@
<div class="card">
<div class="card-header">
<h2>Ultrabeam VL2.3</h2>
<span class="status-dot" class:disconnected={!connected}></span>
<div class="header-right">
{#if interlockConnected && interlockState}
<div class="interlock-badge" style="border-color: {interlockColor}; color: {interlockColor}">
{interlockState === 'READY' ? '🔓 TX OK' :
interlockState === 'NOT_READY' ? '🔒 TX Block' :
interlockState === 'PTT_REQUESTED' ? '⏳ PTT' :
interlockState === 'TRANSMITTING' ? '📡 TX' : '❓'}
</div>
{/if}
<span class="status-dot" class:disconnected={!connected}></span>
</div>
</div>
<div class="metrics">
@@ -176,30 +202,31 @@
{/each}
</select>
</div>
<div class="direction-buttons">
<button
class="dir-btn normal"
class:active={targetDirection === 0}
on:click={() => { targetDirection = 0; setDirection(); }}
>
Normal
</button>
<button
class="dir-btn rotate180"
class:active={targetDirection === 1}
on:click={() => { targetDirection = 1; setDirection(); }}
>
180°
</button>
<button
class="dir-btn bidir"
class:active={targetDirection === 2}
on:click={() => { targetDirection = 2; setDirection(); }}
>
Bi-Dir
</button>
</div>
</div>
<!-- Direction buttons on separate line -->
<div class="direction-buttons">
<button
class="dir-btn"
class:active={targetDirection === 0}
on:click={() => { targetDirection = 0; setDirection(); }}
>
Normal
</button>
<button
class="dir-btn"
class:active={targetDirection === 1}
on:click={() => { targetDirection = 1; setDirection(); }}
>
180°
</button>
<button
class="dir-btn"
class:active={targetDirection === 2}
on:click={() => { targetDirection = 2; setDirection(); }}
>
Bi-Dir
</button>
</div>
</div>
@@ -310,6 +337,24 @@
border-bottom: 2px solid rgba(79, 195, 247, 0.3);
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.interlock-badge {
padding: 4px 10px;
border-radius: 12px;
border: 2px solid;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
background: rgba(0, 0, 0, 0.3);
transition: all 0.2s;
}
h2 {
margin: 0;
font-size: 20px;
@@ -447,267 +492,76 @@
.direction-buttons {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-top: 12px;
gap: 12px;
margin-top: 16px;
}
.dir-btn {
padding: 14px 20px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: 700;
text-transform: uppercase;
cursor: pointer;
transition: all 0.3s;
color: white;
letter-spacing: 0.5px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.dir-btn.normal {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.dir-btn.normal:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
}
.dir-btn.normal.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
box-shadow: 0 0 25px rgba(102, 126, 234, 0.8), 0 6px 20px rgba(102, 126, 234, 0.5);
transform: translateY(-2px);
}
.dir-btn.rotate180 {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.dir-btn.rotate180:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(245, 87, 108, 0.5);
}
.dir-btn.rotate180.active {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
box-shadow: 0 0 25px rgba(245, 87, 108, 0.8), 0 6px 20px rgba(245, 87, 108, 0.5);
transform: translateY(-2px);
}
.dir-btn.bidir {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.dir-btn.bidir:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(79, 172, 254, 0.5);
}
.dir-btn.bidir.active {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
box-shadow: 0 0 25px rgba(79, 172, 254, 0.8), 0 6px 20px rgba(79, 172, 254, 0.5);
transform: translateY(-2px);
}
.freq-control {
display: grid;
grid-template-columns: 2fr 1fr auto;
gap: 12px;
align-items: end;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.input-group label {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
text-transform: uppercase;
letter-spacing: 0.5px;
}
input[type="number"],
select {
background: rgba(15, 23, 42, 0.8);
border: 1px solid rgba(79, 195, 247, 0.3);
padding: 12px 16px;
border: 2px solid rgba(79, 195, 247, 0.3);
border-radius: 8px;
padding: 10px 12px;
color: #fff;
font-size: 16px;
transition: all 0.2s;
}
input[type="number"]:focus,
select:focus {
outline: none;
border-color: #4fc3f7;
box-shadow: 0 0 12px rgba(79, 195, 247, 0.3);
}
/* Buttons */
button {
padding: 12px 20px;
border-radius: 8px;
border: none;
font-size: 13px;
font-weight: 600;
font-size: 14px;
text-transform: uppercase;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
justify-content: center;
color: rgba(255, 255, 255, 0.7);
background: rgba(79, 195, 247, 0.08);
letter-spacing: 0.5px;
}
.btn-primary {
background: linear-gradient(135deg, #4fc3f7 0%, #03a9f4 100%);
color: #fff;
box-shadow: 0 4px 16px rgba(79, 195, 247, 0.4);
.dir-btn:hover {
border-color: rgba(79, 195, 247, 0.6);
color: rgba(255, 255, 255, 0.9);
background: rgba(79, 195, 247, 0.15);
transform: translateY(-1px);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(79, 195, 247, 0.6);
}
.btn-danger {
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
color: #fff;
box-shadow: 0 4px 16px rgba(244, 67, 54, 0.4);
width: 100%;
}
.btn-danger:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(244, 67, 54, 0.6);
}
.btn-caution {
background: linear-gradient(135deg, #ffa726 0%, #fb8c00 100%);
color: #fff;
box-shadow: 0 4px 16px rgba(255, 167, 38, 0.4);
width: 100%;
}
.btn-caution:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 167, 38, 0.6);
}
.btn-toggle {
background: rgba(79, 195, 247, 0.1);
.dir-btn.active {
border-color: #4fc3f7;
color: #4fc3f7;
border: 1px solid rgba(79, 195, 247, 0.3);
padding: 6px 12px;
font-size: 12px;
}
.btn-toggle.active {
background: rgba(79, 195, 247, 0.2);
box-shadow: 0 0 20px rgba(79, 195, 247, 0.4);
font-weight: 700;
}
.icon {
font-size: 16px;
}
/* Progress */
/* Progress Section */
.progress-section {
background: rgba(15, 23, 42, 0.4);
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(255, 193, 7, 0.3);
background: rgba(79, 195, 247, 0.1);
padding: 16px;
border-radius: 8px;
border: 2px solid rgba(79, 195, 247, 0.3);
margin-top: 16px;
}
.progress-section h3 {
margin: 0 0 12px 0;
font-size: 14px;
color: #4fc3f7;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.progress-bar {
width: 100%;
height: 24px;
background: rgba(15, 23, 42, 0.8);
border-radius: 12px;
height: 20px;
background: rgba(15, 23, 42, 0.6);
border-radius: 10px;
overflow: hidden;
margin: 12px 0;
border: 1px solid rgba(79, 195, 247, 0.3);
position: relative;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4fc3f7 0%, #66bb6a 100%);
background: linear-gradient(90deg, #4fc3f7, #03a9f4);
transition: width 0.3s ease;
box-shadow: 0 0 12px rgba(79, 195, 247, 0.6);
border-radius: 10px;
}
.progress-text {
text-align: center;
color: #4fc3f7;
font-weight: 600;
}
/* Elements */
.elements-section {
background: rgba(15, 23, 42, 0.4);
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(79, 195, 247, 0.2);
}
.elements-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 12px;
}
.element-item {
background: rgba(15, 23, 42, 0.6);
padding: 12px;
border-radius: 8px;
border: 1px solid rgba(79, 195, 247, 0.2);
text-align: center;
}
.element-label {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 4px;
}
.element-value {
font-size: 16px;
font-weight: 600;
color: #66bb6a;
}
/* Calibration */
.calibration-section {
background: rgba(255, 152, 0, 0.05);
padding: 12px;
border-radius: 12px;
border: 1px solid rgba(255, 152, 0, 0.3);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.calibration-controls {
display: flex;
flex-direction: column;
gap: 10px;
}
.warning-text {
margin: 0;
padding: 12px;
background: rgba(255, 152, 0, 0.1);
border-radius: 8px;
border-left: 3px solid #ffa726;
color: #ffa726;
font-size: 13px;
color: rgba(255, 255, 255, 0.8);
margin-top: 8px;
}
.actions {
@@ -715,13 +569,4 @@
gap: 12px;
}
@media (max-width: 768px) {
.freq-control {
grid-template-columns: 1fr;
}
.elements-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
}
</style>