| @@ -5,63 +5,52 @@ | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| <title>fm-rds-tx</title> | <title>fm-rds-tx</title> | ||||
| <style> | <style> | ||||
| @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Archivo+Black&display=swap'); | |||||
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap'); | |||||
| :root { | :root { | ||||
| --bg: #0a0a0c; | |||||
| --bg-2: #0f1015; | |||||
| --surface: #111116; | |||||
| --surface2: #18181e; | |||||
| --surface3: #1f2028; | |||||
| --border: #2a2a35; | |||||
| --border-strong: #3a3a49; | |||||
| --text: #d4d4dc; | |||||
| --text-dim: #8b8b99; | |||||
| --text-muted: #666674; | |||||
| --accent: #ff3b30; | |||||
| --accent-glow: #ff3b3044; | |||||
| --green: #30d158; | |||||
| --green-glow: #30d15844; | |||||
| --amber: #ff9f0a; | |||||
| --amber-glow: #ff9f0a44; | |||||
| --blue: #0a84ff; | |||||
| --blue-glow: #0a84ff33; | |||||
| --bg: #f7f8fb; | |||||
| --bg-2: #eef0f3; | |||||
| --surface: #ffffff; | |||||
| --surface2: #f4f6f8; | |||||
| --surface3: #e8ecf1; | |||||
| --border: #d7dcdf; | |||||
| --border-strong: #bcc5ce; | |||||
| --text: #111724; | |||||
| --text-dim: #4c5a6a; | |||||
| --text-muted: #6b7683; | |||||
| --accent: #1f4d9d; | |||||
| --accent-soft: rgba(31, 77, 157, 0.08); | |||||
| --green: #0d944a; | |||||
| --green-soft: rgba(13, 148, 74, 0.1); | |||||
| --amber: #b7791f; | |||||
| --amber-soft: rgba(183, 121, 31, 0.12); | |||||
| --red: #b03030; | |||||
| --red-soft: rgba(176, 48, 48, 0.1); | |||||
| --mono: 'JetBrains Mono', monospace; | --mono: 'JetBrains Mono', monospace; | ||||
| --display: 'Archivo Black', sans-serif; | |||||
| --display: 'Inter', sans-serif; | |||||
| --radius: 8px; | --radius: 8px; | ||||
| --shadow: 0 10px 30px rgba(0,0,0,.25); | |||||
| --shadow: 0 8px 24px rgba(15,23,42,0.08); | |||||
| } | } | ||||
| * { box-sizing: border-box; margin: 0; padding: 0; } | * { box-sizing: border-box; margin: 0; padding: 0; } | ||||
| html { color-scheme: dark; } | |||||
| html { color-scheme: light; } | |||||
| body { | body { | ||||
| background: | |||||
| radial-gradient(circle at top right, rgba(10,132,255,.06), transparent 28%), | |||||
| radial-gradient(circle at top left, rgba(255,59,48,.06), transparent 30%), | |||||
| var(--bg); | |||||
| background: linear-gradient(180deg, #fbfcfe 0%, var(--bg) 100%); | |||||
| color: var(--text); | color: var(--text); | ||||
| font-family: var(--mono); | |||||
| font-size: 13px; | |||||
| font-family: var(--display); | |||||
| font-size: 14px; | |||||
| line-height: 1.5; | line-height: 1.5; | ||||
| min-height: 100vh; | min-height: 100vh; | ||||
| overflow-x: hidden; | overflow-x: hidden; | ||||
| } | } | ||||
| body::before { | |||||
| content: ''; | |||||
| position: fixed; inset: 0; | |||||
| background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.015) 2px, rgba(255,255,255,0.015) 4px); | |||||
| pointer-events: none; | |||||
| z-index: 1000; | |||||
| } | |||||
| button, input { font: inherit; } | button, input { font: inherit; } | ||||
| button { user-select: none; } | button { user-select: none; } | ||||
| .app { | .app { | ||||
| max-width: 1120px; | |||||
| max-width: 1200px; | |||||
| margin: 0 auto; | margin: 0 auto; | ||||
| padding: 18px; | |||||
| padding: 24px; | |||||
| } | } | ||||
| .header { | .header { | ||||
| @@ -69,9 +58,9 @@ button { user-select: none; } | |||||
| align-items: flex-start; | align-items: flex-start; | ||||
| justify-content: space-between; | justify-content: space-between; | ||||
| gap: 18px; | gap: 18px; | ||||
| padding: 8px 0 22px; | |||||
| padding: 4px 0 20px; | |||||
| border-bottom: 1px solid var(--border); | border-bottom: 1px solid var(--border); | ||||
| margin-bottom: 18px; | |||||
| margin-bottom: 20px; | |||||
| } | } | ||||
| .header-main { | .header-main { | ||||
| display: flex; | display: flex; | ||||
| @@ -80,11 +69,15 @@ button { user-select: none; } | |||||
| } | } | ||||
| .header h1 { | .header h1 { | ||||
| font-family: var(--display); | font-family: var(--display); | ||||
| font-size: 24px; | |||||
| letter-spacing: 2px; | |||||
| text-transform: uppercase; | |||||
| color: var(--accent); | |||||
| text-shadow: 0 0 20px var(--accent-glow), 0 0 40px var(--accent-glow); | |||||
| font-size: 28px; | |||||
| font-weight: 800; | |||||
| letter-spacing: -0.03em; | |||||
| text-transform: none; | |||||
| color: var(--text); | |||||
| } | |||||
| .header-note { | |||||
| font-size: 13px; | |||||
| color: var(--text-muted); | |||||
| } | } | ||||
| .header-sub { | .header-sub { | ||||
| display: flex; | display: flex; | ||||
| @@ -99,11 +92,11 @@ button { user-select: none; } | |||||
| padding: 0 10px; | padding: 0 10px; | ||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-radius: 999px; | border-radius: 999px; | ||||
| background: rgba(255,255,255,0.02); | |||||
| background: var(--surface2); | |||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| font-size: 11px; | font-size: 11px; | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: .8px; | |||||
| letter-spacing: .08em; | |||||
| } | } | ||||
| .badge strong { | .badge strong { | ||||
| color: var(--text); | color: var(--text); | ||||
| @@ -127,50 +120,56 @@ button { user-select: none; } | |||||
| } | } | ||||
| .led.on-green { | .led.on-green { | ||||
| background: var(--green); | background: var(--green); | ||||
| box-shadow: 0 0 10px rgba(46, 125, 50, 0.45); | |||||
| box-shadow: 0 0 0 3px rgba(13,148,74,0.16); | |||||
| } | } | ||||
| .led.on-red { | .led.on-red { | ||||
| background: var(--accent); | |||||
| box-shadow: 0 0 10px rgba(180, 35, 24, 0.45); | |||||
| background: var(--red); | |||||
| box-shadow: 0 0 0 3px rgba(176,48,48,0.14); | |||||
| } | } | ||||
| .led.on-amber { | .led.on-amber { | ||||
| background: var(--amber); | background: var(--amber); | ||||
| box-shadow: 0 0 10px rgba(178, 106, 0, 0.45); | |||||
| box-shadow: 0 0 0 3px rgba(183,121,31,0.14); | |||||
| } | } | ||||
| .led.on-blue { | .led.on-blue { | ||||
| background: var(--blue); | |||||
| box-shadow: 0 0 10px rgba(41, 98, 179, 0.45); | |||||
| background: var(--accent); | |||||
| box-shadow: 0 0 0 3px rgba(31,77,157,0.14); | |||||
| } | } | ||||
| .status-text { | .status-text { | ||||
| font-size: 10px; | font-size: 10px; | ||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1.2px; | |||||
| letter-spacing: .08em; | |||||
| } | } | ||||
| .tab-bar { | .tab-bar { | ||||
| display: flex; | display: flex; | ||||
| gap: 10px; | |||||
| gap: 6px; | |||||
| border-bottom: 1px solid var(--border); | border-bottom: 1px solid var(--border); | ||||
| padding-bottom: 10px; | |||||
| margin: 0 -24px 18px; | |||||
| padding-bottom: 0; | |||||
| margin: 0 0 20px; | |||||
| flex-wrap: wrap; | flex-wrap: wrap; | ||||
| position: sticky; | |||||
| top: 0; | |||||
| background: rgba(247,248,251,.92); | |||||
| backdrop-filter: blur(8px); | |||||
| z-index: 10; | |||||
| } | } | ||||
| .tab-btn { | .tab-btn { | ||||
| border: 1px solid var(--border); | |||||
| border-radius: var(--radius); | |||||
| border: none; | |||||
| border-bottom: 3px solid transparent; | |||||
| border-radius: 0; | |||||
| background: transparent; | background: transparent; | ||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| font-size: 12px; | |||||
| font-weight: 600; | |||||
| padding: 8px 18px; | |||||
| font-size: 13px; | |||||
| font-weight: 700; | |||||
| padding: 12px 16px 11px; | |||||
| cursor: pointer; | cursor: pointer; | ||||
| transition: all .16s ease; | |||||
| transition: color .16s ease, border-color .16s ease, background-color .16s ease; | |||||
| } | } | ||||
| .tab-btn.active { | .tab-btn.active { | ||||
| border-color: var(--accent); | |||||
| background: var(--surface); | |||||
| border-bottom-color: var(--accent); | |||||
| background: transparent; | |||||
| color: var(--accent); | color: var(--accent); | ||||
| } | } | ||||
| .tab-btn:focus-visible { | .tab-btn:focus-visible { | ||||
| @@ -212,10 +211,6 @@ button { user-select: none; } | |||||
| .hero { | .hero { | ||||
| padding: 16px; | padding: 16px; | ||||
| } | } | ||||
| @keyframes blinkSoft { | |||||
| 0%, 100% { opacity: 1; } | |||||
| 50% { opacity: .55; } | |||||
| } | |||||
| .tx-bar { | .tx-bar { | ||||
| position: relative; | position: relative; | ||||
| @@ -234,16 +229,16 @@ button { user-select: none; } | |||||
| .freq-display-label { | .freq-display-label { | ||||
| font-size: 10px; | font-size: 10px; | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1.4px; | |||||
| letter-spacing: .08em; | |||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| } | } | ||||
| .freq-display { | .freq-display { | ||||
| font-family: var(--display); | font-family: var(--display); | ||||
| font-size: 38px; | |||||
| font-size: 40px; | |||||
| color: var(--text); | color: var(--text); | ||||
| letter-spacing: 0.6px; | |||||
| letter-spacing: -0.04em; | |||||
| line-height: 1; | line-height: 1; | ||||
| font-weight: 600; | |||||
| font-weight: 800; | |||||
| } | } | ||||
| .freq-display .unit { | .freq-display .unit { | ||||
| font-family: var(--mono); | font-family: var(--mono); | ||||
| @@ -259,7 +254,7 @@ button { user-select: none; } | |||||
| font-size: 11px; | font-size: 11px; | ||||
| color: var(--text-muted); | color: var(--text-muted); | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1px; | |||||
| letter-spacing: .08em; | |||||
| } | } | ||||
| .freq-note-item { | .freq-note-item { | ||||
| display: inline-flex; | display: inline-flex; | ||||
| @@ -287,7 +282,7 @@ button { user-select: none; } | |||||
| font-size: 12px; | font-size: 12px; | ||||
| font-weight: 700; | font-weight: 700; | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1px; | |||||
| letter-spacing: .08em; | |||||
| transition: all .16s ease; | transition: all .16s ease; | ||||
| } | } | ||||
| .tx-btn:hover, .ghost-btn:hover, .apply-btn:hover, .preset-btn:hover, .danger-btn:hover { | .tx-btn:hover, .ghost-btn:hover, .apply-btn:hover, .preset-btn:hover, .danger-btn:hover { | ||||
| @@ -299,18 +294,18 @@ button { user-select: none; } | |||||
| cursor: not-allowed; | cursor: not-allowed; | ||||
| transform: none; | transform: none; | ||||
| } | } | ||||
| .tx-btn.start { border-color: var(--green); color: var(--green); } | |||||
| .tx-btn.start:hover:not(:disabled) { background: rgba(48,209,88,.1); } | |||||
| .tx-btn.stop { border-color: var(--accent); color: var(--accent); } | |||||
| .tx-btn.stop:hover:not(:disabled) { background: rgba(255,59,48,.1); } | |||||
| .tx-btn.start { background: var(--green); color: #fff; border-color: transparent; } | |||||
| .tx-btn.start:hover:not(:disabled) { background: #0b7f40; } | |||||
| .tx-btn.stop { background: #fff; color: var(--red); border-color: rgba(176,48,48,.35); } | |||||
| .tx-btn.stop:hover:not(:disabled) { background: var(--red-soft); } | |||||
| .ghost-btn { color: var(--text-dim); } | .ghost-btn { color: var(--text-dim); } | ||||
| .danger-btn { | .danger-btn { | ||||
| border-color: rgba(255,59,48,.45); | |||||
| color: var(--accent); | |||||
| background: rgba(255,59,48,.04); | |||||
| border-color: rgba(176,48,48,.35); | |||||
| color: var(--red); | |||||
| background: var(--red-soft); | |||||
| } | } | ||||
| .danger-btn:hover:not(:disabled) { | .danger-btn:hover:not(:disabled) { | ||||
| background: rgba(255,59,48,.12); | |||||
| background: rgba(176,48,48,.16); | |||||
| } | } | ||||
| .tx-state-wrap { | .tx-state-wrap { | ||||
| @@ -325,10 +320,10 @@ button { user-select: none; } | |||||
| letter-spacing: 2px; | letter-spacing: 2px; | ||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| } | } | ||||
| .tx-state.running { color: var(--green); animation: blinkSoft 2s ease-in-out infinite; } | |||||
| .tx-state.running { color: var(--green); } | |||||
| .tx-state.idle, .tx-state.stopped { color: var(--text-dim); } | .tx-state.idle, .tx-state.stopped { color: var(--text-dim); } | ||||
| .tx-state.starting, .tx-state.stopping, .tx-state.working { color: var(--amber); animation: blinkSoft 1.1s ease-in-out infinite; } | |||||
| .tx-state.error { color: var(--accent); } | |||||
| .tx-state.starting, .tx-state.stopping, .tx-state.working { color: var(--amber); } | |||||
| .tx-state.error { color: var(--red); } | |||||
| .status-hint { | .status-hint { | ||||
| font-size: 10px; | font-size: 10px; | ||||
| color: var(--text-muted); | color: var(--text-muted); | ||||
| @@ -347,12 +342,12 @@ button { user-select: none; } | |||||
| padding: 12px; | padding: 12px; | ||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-radius: var(--radius); | border-radius: var(--radius); | ||||
| background: var(--bg-2); | |||||
| background: var(--surface2); | |||||
| } | } | ||||
| .quick-item .label { | .quick-item .label { | ||||
| font-size: 9px; | font-size: 9px; | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1.4px; | |||||
| letter-spacing: .08em; | |||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| margin-bottom: 6px; | margin-bottom: 6px; | ||||
| } | } | ||||
| @@ -377,7 +372,7 @@ button { user-select: none; } | |||||
| padding: 12px; | padding: 12px; | ||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-radius: var(--radius); | border-radius: var(--radius); | ||||
| background: var(--bg-2); | |||||
| background: var(--surface2); | |||||
| } | } | ||||
| .signal-head { | .signal-head { | ||||
| display: flex; | display: flex; | ||||
| @@ -390,7 +385,7 @@ button { user-select: none; } | |||||
| font-size: 10px; | font-size: 10px; | ||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1.2px; | |||||
| letter-spacing: .08em; | |||||
| } | } | ||||
| .signal-value { | .signal-value { | ||||
| font-size: 11px; | font-size: 11px; | ||||
| @@ -401,7 +396,7 @@ button { user-select: none; } | |||||
| width: 100%; | width: 100%; | ||||
| height: 10px; | height: 10px; | ||||
| border-radius: 999px; | border-radius: 999px; | ||||
| background: #171821; | |||||
| background: #e7ebf0; | |||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| overflow: hidden; | overflow: hidden; | ||||
| } | } | ||||
| @@ -409,21 +404,21 @@ button { user-select: none; } | |||||
| height: 100%; | height: 100%; | ||||
| width: 0%; | width: 0%; | ||||
| transition: width .25s ease, background-color .25s ease; | transition: width .25s ease, background-color .25s ease; | ||||
| background: linear-gradient(90deg, var(--green), #5cff90); | |||||
| background: linear-gradient(90deg, var(--green), #3dbd75); | |||||
| } | } | ||||
| .meter-fill.warn { | .meter-fill.warn { | ||||
| background: linear-gradient(90deg, var(--amber), #ffc45b); | |||||
| background: linear-gradient(90deg, var(--amber), #d9a14a); | |||||
| } | } | ||||
| .meter-fill.err { | .meter-fill.err { | ||||
| background: linear-gradient(90deg, var(--accent), #ff6b63); | |||||
| background: linear-gradient(90deg, var(--red), #d85c5c); | |||||
| } | } | ||||
| .spark { | .spark { | ||||
| width: 100%; | width: 100%; | ||||
| height: 34px; | height: 34px; | ||||
| margin-top: 10px; | margin-top: 10px; | ||||
| border-radius: 6px; | border-radius: 6px; | ||||
| background: rgba(255,255,255,0.01); | |||||
| border: 1px solid rgba(255,255,255,0.03); | |||||
| background: #f7f9fb; | |||||
| border: 1px solid #e3e8ee; | |||||
| } | } | ||||
| .spark path.line { | .spark path.line { | ||||
| fill: none; | fill: none; | ||||
| @@ -494,7 +489,7 @@ button { user-select: none; } | |||||
| justify-content: space-between; | justify-content: space-between; | ||||
| gap: 12px; | gap: 12px; | ||||
| padding: 7px 0; | padding: 7px 0; | ||||
| border-bottom: 1px solid #1a1a22; | |||||
| border-bottom: 1px solid #e6eaef; | |||||
| } | } | ||||
| .shortcut-line:last-child { border-bottom: none; } | .shortcut-line:last-child { border-bottom: none; } | ||||
| .shortcut-line .name { font-size: 11px; color: var(--text-dim); } | .shortcut-line .name { font-size: 11px; color: var(--text-dim); } | ||||
| @@ -509,7 +504,7 @@ button { user-select: none; } | |||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-bottom-width: 2px; | border-bottom-width: 2px; | ||||
| border-radius: 6px; | border-radius: 6px; | ||||
| background: var(--bg-2); | |||||
| background: var(--surface2); | |||||
| font-size: 10px; | font-size: 10px; | ||||
| color: var(--text); | color: var(--text); | ||||
| text-align: center; | text-align: center; | ||||
| @@ -525,13 +520,13 @@ button { user-select: none; } | |||||
| min-height: 34px; | min-height: 34px; | ||||
| padding: 0 12px; | padding: 0 12px; | ||||
| font-size: 11px; | font-size: 11px; | ||||
| letter-spacing: .8px; | |||||
| letter-spacing: .08em; | |||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| } | } | ||||
| .preset-btn.active { | .preset-btn.active { | ||||
| border-color: var(--blue); | |||||
| color: var(--blue); | |||||
| background: rgba(10,132,255,.08); | |||||
| border-color: var(--accent); | |||||
| color: var(--accent); | |||||
| background: var(--accent-soft); | |||||
| } | } | ||||
| .preset-btn.rds { | .preset-btn.rds { | ||||
| text-transform: none; | text-transform: none; | ||||
| @@ -543,7 +538,7 @@ button { user-select: none; } | |||||
| align-items: center; | align-items: center; | ||||
| gap: 12px; | gap: 12px; | ||||
| padding: 10px 0; | padding: 10px 0; | ||||
| border-bottom: 1px solid #1a1a22; | |||||
| border-bottom: 1px solid #e6eaef; | |||||
| } | } | ||||
| .ctrl-row:last-child { border-bottom: none; } | .ctrl-row:last-child { border-bottom: none; } | ||||
| .ctrl-label-wrap { | .ctrl-label-wrap { | ||||
| @@ -593,7 +588,7 @@ input[type="range"]::-webkit-slider-thumb:hover { | |||||
| transform: scale(1.06); | transform: scale(1.06); | ||||
| } | } | ||||
| input[type="number"], input[type="text"] { | input[type="number"], input[type="text"] { | ||||
| background: var(--bg); | |||||
| background: #fff; | |||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-radius: 6px; | border-radius: 6px; | ||||
| color: var(--text); | color: var(--text); | ||||
| @@ -608,16 +603,16 @@ input[type="number"] { | |||||
| input[type="text"] { width: 100%; } | input[type="text"] { width: 100%; } | ||||
| input:focus { | input:focus { | ||||
| border-color: var(--accent); | border-color: var(--accent); | ||||
| box-shadow: 0 0 0 3px rgba(255,59,48,.12); | |||||
| box-shadow: 0 0 0 3px rgba(31,77,157,.12); | |||||
| } | } | ||||
| input.input-dirty { | input.input-dirty { | ||||
| border-color: var(--amber); | border-color: var(--amber); | ||||
| box-shadow: 0 0 0 3px rgba(255,159,10,.08); | |||||
| box-shadow: 0 0 0 3px rgba(183,121,31,.08); | |||||
| } | } | ||||
| input.input-error { | input.input-error { | ||||
| border-color: var(--accent); | |||||
| box-shadow: 0 0 0 3px rgba(255,59,48,.14); | |||||
| background: rgba(255,59,48,.04); | |||||
| border-color: var(--red); | |||||
| box-shadow: 0 0 0 3px rgba(176,48,48,.14); | |||||
| background: rgba(176,48,48,.04); | |||||
| } | } | ||||
| .val-display { | .val-display { | ||||
| min-width: 64px; | min-width: 64px; | ||||
| @@ -711,10 +706,10 @@ input.input-error { | |||||
| } | } | ||||
| .rds-input { | .rds-input { | ||||
| width: 100%; | width: 100%; | ||||
| background: var(--bg); | |||||
| background: #fff; | |||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-radius: 6px; | border-radius: 6px; | ||||
| color: var(--green); | |||||
| color: var(--accent); | |||||
| font-family: var(--mono); | font-family: var(--mono); | ||||
| font-size: 15px; | font-size: 15px; | ||||
| font-weight: 700; | font-weight: 700; | ||||
| @@ -765,7 +760,7 @@ input.input-error { | |||||
| .sidebar-title { | .sidebar-title { | ||||
| font-size: 11px; | font-size: 11px; | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1.4px; | |||||
| letter-spacing: .08em; | |||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| margin-bottom: 10px; | margin-bottom: 10px; | ||||
| } | } | ||||
| @@ -779,7 +774,7 @@ input.input-error { | |||||
| font-size: 10px; | font-size: 10px; | ||||
| color: var(--text-muted); | color: var(--text-muted); | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1px; | |||||
| letter-spacing: .08em; | |||||
| } | } | ||||
| .kv .v { | .kv .v { | ||||
| font-size: 12px; | font-size: 12px; | ||||
| @@ -813,7 +808,7 @@ input.input-error { | |||||
| .health-trend-label { | .health-trend-label { | ||||
| font-size: 10px; | font-size: 10px; | ||||
| text-transform: uppercase; | text-transform: uppercase; | ||||
| letter-spacing: 1px; | |||||
| letter-spacing: .08em; | |||||
| color: var(--text-muted); | color: var(--text-muted); | ||||
| margin-bottom: 6px; | margin-bottom: 6px; | ||||
| } | } | ||||
| @@ -823,7 +818,7 @@ input.input-error { | |||||
| padding: 10px; | padding: 10px; | ||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-radius: 6px; | border-radius: 6px; | ||||
| background: var(--surface1); | |||||
| background: #fbfcfd; | |||||
| font-size: 11px; | font-size: 11px; | ||||
| max-height: 180px; | max-height: 180px; | ||||
| overflow-y: auto; | overflow-y: auto; | ||||
| @@ -902,11 +897,11 @@ input.input-error { | |||||
| } | } | ||||
| .log { | .log { | ||||
| background: var(--bg); | |||||
| background: #fbfcfd; | |||||
| border: 1px solid var(--border); | border: 1px solid var(--border); | ||||
| border-radius: 6px; | border-radius: 6px; | ||||
| padding: 10px; | padding: 10px; | ||||
| font-size: 10px; | |||||
| font-size: 11px; | |||||
| color: var(--text-dim); | color: var(--text-dim); | ||||
| max-height: 220px; | max-height: 220px; | ||||
| overflow-y: auto; | overflow-y: auto; | ||||
| @@ -995,7 +990,8 @@ input.input-error { | |||||
| <div class="app"> | <div class="app"> | ||||
| <div class="header"> | <div class="header"> | ||||
| <div class="header-main"> | <div class="header-main"> | ||||
| <h1>FM-RDS-TX</h1> | |||||
| <h1>FM-RDS-TX Control Plane</h1> | |||||
| <div class="header-note">Overview first, controls second, diagnostics when needed.</div> | |||||
| <div class="header-sub"> | <div class="header-sub"> | ||||
| <div class="badge"><span>Backend</span><strong id="badge-backend">--</strong></div> | <div class="badge"><span>Backend</span><strong id="badge-backend">--</strong></div> | ||||
| <div class="badge"><span>Mode</span><strong id="badge-mode">Control Plane</strong></div> | <div class="badge"><span>Mode</span><strong id="badge-mode">Control Plane</strong></div> | ||||
| @@ -1011,9 +1007,9 @@ input.input-error { | |||||
| <div class="tab-bar"> | <div class="tab-bar"> | ||||
| <button class="tab-btn active" data-tab="overview" type="button">Overview</button> | <button class="tab-btn active" data-tab="overview" type="button">Overview</button> | ||||
| <button class="tab-btn" data-tab="control" type="button">Control</button> | |||||
| <button class="tab-btn" data-tab="diagnostics" type="button">Diagnostics</button> | |||||
| <button class="tab-btn" data-tab="activity" type="button">Activity</button> | |||||
| <button class="tab-btn" data-tab="control" type="button">Transmission Control</button> | |||||
| <button class="tab-btn" data-tab="diagnostics" type="button">Diagnostics & Health</button> | |||||
| <button class="tab-btn" data-tab="activity" type="button">Activity & Logs</button> | |||||
| </div> | </div> | ||||
| <div class="tab-panels"> | <div class="tab-panels"> | ||||
| @@ -1099,16 +1095,24 @@ input.input-error { | |||||
| <div class="stack"> | <div class="stack"> | ||||
| <div class="card sidebar-card"> | <div class="card sidebar-card"> | ||||
| <div class="sidebar-section"> | <div class="sidebar-section"> | ||||
| <div class="sidebar-title">System Snapshot</div> | |||||
| <div class="sidebar-title">Runtime Snapshot</div> | |||||
| <div class="kv"> | <div class="kv"> | ||||
| <div class="k">Backend</div><div class="v" id="info-backend">--</div> | <div class="k">Backend</div><div class="v" id="info-backend">--</div> | ||||
| <div class="k">Frequency</div><div class="v" id="info-freq">--</div> | <div class="k">Frequency</div><div class="v" id="info-freq">--</div> | ||||
| <div class="k">Pre-emphasis</div><div class="v" id="info-preemph">--</div> | |||||
| <div class="k">FM Mod</div><div class="v" id="info-fmmod">--</div> | |||||
| <div class="k">Runtime Age</div><div class="v" id="info-runtime-age">--</div> | |||||
| <div class="k">Last Alert</div><div class="v" id="info-last-alert">--</div> | |||||
| <div class="k">Live Config</div><div class="v" id="info-live">--</div> | <div class="k">Live Config</div><div class="v" id="info-live">--</div> | ||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| <div class="sidebar-section"> | |||||
| <div class="sidebar-title">Signal Notes</div> | |||||
| <div class="section-note">Overview stays compact: primary state here, deep diagnostics in the dedicated tab.</div> | |||||
| <div class="kv"> | |||||
| <div class="k">Pre-emphasis</div><div class="v" id="info-preemph">--</div> | |||||
| <div class="k">FM Mod</div><div class="v" id="info-fmmod">--</div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| @@ -1249,7 +1253,7 @@ input.input-error { | |||||
| <span class="chevron">▼</span> | <span class="chevron">▼</span> | ||||
| </div> | </div> | ||||
| <div class="panel-body"> | <div class="panel-body"> | ||||
| <div class="section-note">Fast control, as long as you're not typing in an input field.</div> | |||||
| <div class="section-note">Fast control reference. Shortcuts stay out of the main operator path.</div> | |||||
| <div class="shortcuts-grid"> | <div class="shortcuts-grid"> | ||||
| <div> | <div> | ||||
| <div class="shortcut-line"><span class="name">Start TX</span><span class="keys"><span class="kbd">t</span></span></div> | <div class="shortcut-line"><span class="name">Start TX</span><span class="keys"><span class="kbd">t</span></span></div> | ||||
| @@ -2097,6 +2101,8 @@ function render() { | |||||
| updateText('info-freq', fmtFreq(cfg.fm?.frequencyMHz)); | updateText('info-freq', fmtFreq(cfg.fm?.frequencyMHz)); | ||||
| updateText('info-preemph', cfg.fm?.preEmphasisTauUS ? `${cfg.fm.preEmphasisTauUS} µs` : 'Off'); | updateText('info-preemph', cfg.fm?.preEmphasisTauUS ? `${cfg.fm.preEmphasisTauUS} µs` : 'Off'); | ||||
| updateText('info-fmmod', fmtBool(cfg.fm?.fmModulationEnabled)); | updateText('info-fmmod', fmtBool(cfg.fm?.fmModulationEnabled)); | ||||
| updateText('info-runtime-age', ageString(state.server.lastRuntimeAt)); | |||||
| updateText('info-last-alert', engine.runtimeAlert ? engine.runtimeAlert : (engine.lastError ? engine.lastError : 'None')); | |||||
| updateText('info-live', engine.state ? `${String(engine.state).toUpperCase()} / ${state.server.runtimeOk ? 'runtime ok' : 'runtime pending'}` : (state.server.configOk ? 'config only' : '--')); | updateText('info-live', engine.state ? `${String(engine.state).toUpperCase()} / ${state.server.runtimeOk ? 'runtime ok' : 'runtime pending'}` : (state.server.configOk ? 'config only' : '--')); | ||||
| updateHealth(engine, driver, audioStream); | updateHealth(engine, driver, audioStream); | ||||
| @@ -2486,6 +2492,16 @@ function log(message, type = '') { | |||||
| logEl.scrollTop = logEl.scrollHeight; | logEl.scrollTop = logEl.scrollHeight; | ||||
| } | } | ||||
| function bindTabs() { | |||||
| const buttons = Array.from(document.querySelectorAll('.tab-btn[data-tab]')); | |||||
| const panels = Array.from(document.querySelectorAll('.tab-panel[data-tab-panel]')); | |||||
| const activate = (tab) => { | |||||
| buttons.forEach((btn) => btn.classList.toggle('active', btn.dataset.tab === tab)); | |||||
| panels.forEach((panel) => panel.classList.toggle('active', panel.dataset.tabPanel === tab)); | |||||
| }; | |||||
| buttons.forEach((btn) => btn.addEventListener('click', () => activate(btn.dataset.tab))); | |||||
| } | |||||
| function bindPanels() { | function bindPanels() { | ||||
| document.querySelectorAll('[data-panel]').forEach((head) => { | document.querySelectorAll('[data-panel]').forEach((head) => { | ||||
| head.addEventListener('click', () => { | head.addEventListener('click', () => { | ||||
| @@ -2645,6 +2661,7 @@ function bindKeyboardShortcuts() { | |||||
| } | } | ||||
| async function init() { | async function init() { | ||||
| bindTabs(); | |||||
| bindPanels(); | bindPanels(); | ||||
| bindInputs(); | bindInputs(); | ||||
| bindResponsiveBehavior(); | bindResponsiveBehavior(); | ||||