Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
T
sjg 09634eb851
CI / lint (pull_request) Successful in 2m21s
CI / test (pull_request) Successful in 8m7s
CI / frontend (push) Successful in 2m57s
CI / reuse (push) Successful in 3s
CI / frontend (pull_request) Successful in 3m47s
CI / reuse (pull_request) Successful in 3s
CI / lint (push) Successful in 2m16s
CI / test (push) Successful in 7m19s
[fix](trx-frontend-http): tidy up the map's filter bar
The bar explained itself in prose: "All bands visible by default" sat
between the chips and the next group, taking width the bar could not
spare and reading as a stray line of text. An "All" chip says the same
thing in a chip's width and gives the selection somewhere to be undone.

Band chips also came up dimmed at the very moment every band was on the
map -- an empty selection is no filter at all, so nothing is dimmed
until something is picked. The path toggles drop their "On"/"Off"
suffix, which cost most of a row and only repeated what their own
highlight already said; state moves to aria-pressed and the tooltip.

The rest is alignment. The rule dividing the buttons from the filters
is drawn on the button block's edge, and a centred block left it
floating as a stub beside a two-row bar; stacked, it lay down the left
of a block that sits underneath. The labels sat at their natural
widths, so each row's first control started somewhere different, and
the two pairs of phase buttons differed in width, so the groups after
them missed each other by four pixels. One gutter for every label, one
width for both pairs, and the search field moved last where it can take
the room the fixed-width groups leave.

The map layout test now covers the chips, the divider's height and the
rows' shared start.

Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-05 21:43:57 +02:00

6236 lines
180 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
SPDX-License-Identifier: GPL-2.0-or-later
*/
:root {
--bg: #070d1a;
--card-bg: #0f172a;
--input-bg: #0b1324;
--border: #22324a;
--border-light: #304766;
--text: #e7edf9;
--text-muted: #9bb0ca;
--text-heading: #c6d5ea;
--btn-bg: #16243a;
--btn-border: #3a5274;
--accent-green: #c24b1a;
--accent-yellow: #f0ad4e;
--accent-red: #e55353;
/* Healthy/on indicator. Distinct from --accent-green, which this theme
family uses as the (orange) brand accent rather than a status colour. */
--status-ok: #00d17f;
--vchan-color: #38bdf8;
--control-height: 2.6rem;
--jog-hi: #243a5b;
--jog-lo: #14233a;
--jog-shadow: rgba(0,0,0,0.3);
--jog-inset: rgba(255,255,255,0.05);
--audio-level-bg: #14263f;
--audio-level-border: #304766;
--audio-level-fill-start: #c24b1a;
--audio-level-fill-end: #f0ad4e;
--filter-bg: #13253e;
--filter-fg: #e7edf9;
--filter-border: #385577;
--wavelength-fg: #8da3be;
--spectrum-bg: #0a0f18;
--overview-plot-height: 160px;
--spectrum-plot-height: 160px;
--jog-wheel-size: 83.2px;
--header-waterfall-overlap: 0rem;
--card-base-max-width: 1280px;
--card-max-width: 1600px;
--card-bookmark-gutter: 9.5rem;
--spectrum-bookmark-side-width: 6.5rem;
--spectrum-bookmark-side-offset: 8.85rem;
--btn-hover-bg: color-mix(in srgb, var(--btn-bg) 85%, var(--text) 15%);
--btn-active-bg: color-mix(in srgb, var(--btn-bg) 75%, var(--text) 25%);
--border-hover: color-mix(in srgb, var(--border-light) 80%, var(--text) 20%);
--accent-green-hover: color-mix(in srgb, var(--accent-green) 85%, #fff 15%);
--accent-green-active: color-mix(in srgb, var(--accent-green) 75%, #fff 25%);
/* Accent tuned for text/links: mixes toward the theme's text colour,
which is always the higher-contrast direction (lighter on dark
themes, darker on light). Use for accent-coloured text; keep
--accent-green for fills, indicators and meters. */
--accent-text: color-mix(in srgb, var(--accent-green) 82%, var(--text) 18%);
/* ── Design tokens ──────────────────────────────────────────────────
Derived tokens use color-mix over themed variables, so they
re-resolve per active [data-style]/[data-theme] automatically. */
/* Elevated surface, one step lighter than --card-bg. Consumed by
.controls-tray and other raised panels. */
--surface: color-mix(in srgb, var(--card-bg) 90%, var(--text) 7%);
/* Type scale */
--fs-xs: 0.75rem;
--fs-sm: 0.85rem;
--fs-base: 1rem;
--fs-md: 1.125rem;
--fs-lg: 1.25rem;
--fs-xl: 1.5rem;
--fs-2xl: 2rem;
/* Spacing scale (4px grid) */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.25rem;
--space-6: 1.5rem;
--space-8: 2rem;
/* Corner radii */
--radius-sm: 6px;
--radius-md: 0.5rem;
--radius-lg: 1rem;
--radius-pill: 999px;
/* Motion */
--dur-fast: 100ms;
--dur-base: 180ms;
--dur-slow: 300ms;
--ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
}
[data-theme="light"] {
--bg: #edf2f7;
--card-bg: #ffffff;
--input-bg: #f7fafc;
--border: #d6deea;
--border-light: #bcc8da;
--text: #1a202c;
--text-muted: #4a5568;
--text-heading: #2d3748;
--btn-bg: #e6edf8;
--btn-border: #a9b8cf;
--accent-green: #b04317;
--accent-yellow: #b57600;
--accent-red: #cf3f3f;
/* Darker on light backgrounds: #00d17f as text/dots is barely legible there. */
--status-ok: #0a9d63;
--jog-hi: #e6edf8;
--jog-lo: #cdd9eb;
--jog-shadow: rgba(58, 79, 110, 0.18);
--jog-inset: rgba(255,255,255,0.75);
--audio-level-bg: #edf2fb;
--audio-level-border: #b8c5da;
--audio-level-fill-start: #b04317;
--audio-level-fill-end: #b57600;
--filter-bg: #eef3fb;
--filter-fg: #1f2937;
--filter-border: #b8c5da;
--wavelength-fg: #6b7280;
--spectrum-bg: #eef3fb;
}
@font-face {
font-family: 'DSEG14 Classic';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/vendor/dseg14-classic-latin-400-normal.woff2') format('woff2');
unicode-range: U+0020-007E;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: var(--fs-base);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
min-height: 100vh;
min-height: 100dvh;
box-sizing: border-box;
display: block;
background: var(--bg);
color: var(--text);
overflow-x: hidden;
overflow-y: auto;
}
.card {
width: min(100%, var(--card-base-max-width));
margin: 0 auto;
padding: 0.85rem 1.25rem 1.5rem;
background: transparent;
box-sizing: border-box;
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
overflow: visible;
}
@media (min-width: 1100px) {
.card {
width: min(
var(--card-max-width),
calc(100vw - (var(--card-bookmark-gutter) * 2))
);
}
}
/* ── Auth gate ────────────────────────────────────────────────────────
Class-based; #display toggling stays inline (driven by app.js). */
.auth-gate {
max-width: 30rem;
margin: 0 auto 0.9rem;
padding: var(--space-5) 0 var(--space-6);
text-align: center;
}
.auth-gate-head { margin-bottom: var(--space-6); }
.auth-gate-title { font-size: var(--fs-md); font-weight: 600; margin-bottom: var(--space-2); }
.auth-gate-sub { color: var(--text-muted); }
.auth-form { margin: var(--space-4) 0 0.35rem; }
.auth-input {
width: 100%;
padding: 0.65rem 0.75rem;
margin-bottom: var(--space-4);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
background: var(--input-bg);
color: var(--text);
font-size: var(--fs-base);
box-sizing: border-box;
}
.auth-submit,
.auth-guest {
width: 100%;
padding: 0.65rem 0.75rem;
border-radius: var(--radius-md);
font-size: var(--fs-base);
cursor: pointer;
box-sizing: border-box;
transition: background-color var(--dur-fast) var(--ease-standard);
}
.auth-submit {
background: var(--accent-green);
color: #fff;
border: none;
font-weight: 700;
}
.auth-submit:hover:not(:disabled) { background: var(--accent-green-hover); }
.auth-guest {
background: var(--btn-bg);
color: var(--text);
border: 1px solid var(--border-light);
font-weight: 600;
margin-top: var(--space-4);
}
.auth-guest:hover:not(:disabled) { background: var(--btn-hover-bg); }
.auth-error { color: var(--accent-red); font-size: var(--fs-sm); margin-top: var(--space-4); }
.auth-role { margin-top: var(--space-4); color: var(--text-muted); font-size: var(--fs-sm); }
.label { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 6px; display: block; }
#tab-main .label > span {
display: inline-block;
padding: 0.14rem 0.5rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-light) 68%, transparent);
background: color-mix(in srgb, var(--btn-bg) 58%, transparent);
color: var(--text-muted);
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.04em;
line-height: 1.2;
}
.status { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1.1rem 1rem; }
.controls-tray-shell {
min-width: 0;
}
.controls-tray-scroll {
width: 100%;
min-width: 0;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 0.2rem;
}
.controls-tray {
display: grid;
gap: 0.95rem;
width: max(100%, 58rem);
min-width: 0;
box-sizing: border-box;
padding: 0.95rem 1rem 1rem;
border: 1px solid color-mix(in srgb, var(--border-light) 85%, transparent);
border-radius: 1rem;
background:
linear-gradient(180deg,
color-mix(in srgb, var(--surface) 88%, transparent),
color-mix(in srgb, var(--btn-bg) 76%, transparent));
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 12%, transparent);
}
input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem; font-size: 1rem; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); color: var(--text); }
#mode { height: var(--control-height); }
#freq { font-family: 'DSEG14 Classic', monospace; font-size: 2rem; padding: 0.5rem 0.6rem; letter-spacing: 0.05em; text-align: center; }
#freq:disabled,
#center-freq:disabled {
opacity: 1;
-webkit-text-fill-color: currentColor;
}
#center-freq { color: var(--wavelength-fg); }
/* Packs left. As a grid it held a track for every column, but the WFM, SAM and
transmit columns are hidden on most rigs, so the row ended in a hole of dead
space and the power buttons' label chip floated far from the buttons. */
.controls-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1rem 1.25rem;
align-items: flex-start;
}
/* Whatever the current mode adds — WFM's deemphasis and interference meters,
SAM's carrier sync — sits on its own line under the controls every rig has,
instead of stretching the row sideways when one mode happens to be active. */
.controls-row-mode {
padding-top: 0.85rem;
border-top: 1px solid color-mix(in srgb, var(--border-light) 45%, transparent);
}
.controls-col {
min-width: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.controls-col.label-below-col {
align-items: stretch;
}
.controls-col.label-below-col .label {
order: 2;
margin-top: 0.3rem;
margin-bottom: 0;
}
.controls-col-power.label-below-col .label {
justify-content: flex-end;
}
.controls-col.label-below-col > :not(.label) {
order: 1;
}
.controls-col.label-below-col .inline,
.controls-col.label-below-col .btn-grid {
align-self: stretch;
width: 100%;
margin-top: calc((var(--jog-wheel-size) - var(--control-height)) / 2);
}
/* Mode: a segmented group sized by its content. As a full-width select it took
483px of the row to show "FM"; the modes are all three or four characters and
there are at most twelve of them, so they fit two rows in a third of that. */
.controls-col-mode {
flex: 0 1 auto;
min-width: 0;
}
/* Specific enough to beat .controls-col.label-below-col .inline, whose offset
assumes a --control-height field: the picker and the step groups are both
3.35rem, so they line up with each other beside the wheel. */
.controls-col-mode.label-below-col .inline {
width: auto;
margin-top: calc((var(--jog-wheel-size) - 3.35rem) / 2);
}
.mode-picker {
display: flex;
flex-wrap: wrap;
gap: 1px;
width: max-content;
max-width: 22rem;
border: 1px solid var(--border-light);
border-radius: 6px;
overflow: hidden;
background: var(--border-light);
}
.mode-picker button {
flex: 1 0 3.2rem;
border: none;
border-radius: 0;
min-height: 1.65rem;
height: auto;
padding: 0.32rem 0.55rem;
font-size: 0.82rem;
font-weight: 600;
background: var(--input-bg);
color: var(--text-muted);
cursor: pointer;
}
.mode-picker button:hover:not(:disabled):not(.active) {
color: var(--text);
background: var(--btn-hover-bg);
}
.mode-picker button.active {
background: var(--btn-bg);
color: var(--accent-green);
}
.mode-picker button:disabled {
opacity: 0.55;
cursor: default;
}
/* Tune step: the unit and the scale drive the wheel and the +/- beside it, so
they sit with it rather than up in the frequency row. */
.controls-col-step {
flex: 0 0 auto;
}
.step-controls-inline {
display: flex;
gap: 0.75rem;
align-items: flex-start;
margin-top: calc((var(--jog-wheel-size) - 3.35rem) / 2);
}
.step-controls-inline .label {
justify-content: flex-start;
}
.controls-col-center {
width: auto;
align-items: center;
flex: 0 0 auto;
}
.controls-col-wfm.label-below-col .label {
justify-content: flex-start;
}
.wfm-controls-inline {
gap: 0.45rem;
justify-content: flex-start;
align-items: flex-end;
flex-wrap: wrap;
}
.wfm-control {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.22rem;
color: var(--text-muted);
font-size: 0.75rem;
line-height: 1.1;
white-space: nowrap;
}
.wfm-control-label {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-muted);
}
.wfm-control .status-input {
min-width: 4.4rem;
width: auto;
height: 2.1rem;
padding: 0.28rem 0.45rem;
font-size: 0.85rem;
line-height: 1.2;
box-sizing: border-box;
}
.wfm-control input.status-input {
width: 4.25rem;
}
.wfm-gain-group {
display: flex;
align-items: flex-end;
gap: 0.35rem;
}
.wfm-inline-btn {
min-width: 3rem;
height: 2.1rem;
padding: 0 0.6rem;
font-size: 0.8rem;
line-height: 1.1;
flex-shrink: 0;
}
.wfm-st-flag-wrap {
min-width: 0;
}
.wfm-st-flag {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2.7rem;
min-height: 2.1rem;
padding: 0.28rem 0.5rem;
box-sizing: border-box;
border: 1px solid var(--border-light);
border-radius: 6px;
font-size: 0.82rem;
font-weight: 800;
line-height: 1.2;
letter-spacing: 0.04em;
background: var(--input-bg);
}
.wfm-st-flag-stereo {
color: #ff5c5c;
border-color: color-mix(in srgb, #ff5c5c 65%, var(--border-light));
background: color-mix(in srgb, #ff5c5c 14%, var(--input-bg));
}
.wfm-st-flag-mono {
color: var(--text-muted);
border-color: var(--border-light);
background: color-mix(in srgb, var(--input-bg) 92%, var(--panel-2));
}
.wfm-intf-bar-wrap {
min-width: 3.6rem;
}
.wfm-intf-bar {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-width: 3.6rem;
min-height: 2.1rem;
padding: 0;
box-sizing: border-box;
border: 1px solid var(--border-light);
border-radius: 6px;
overflow: hidden;
background: var(--input-bg);
}
.wfm-intf-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 0%;
border-radius: 5px 0 0 5px;
background: color-mix(in srgb, #4fc3f7 45%, transparent);
transition: width 0.25s ease, background-color 0.35s ease;
}
.wfm-intf-fill.wfm-intf-warn {
background: color-mix(in srgb, #ffa726 55%, transparent);
}
.wfm-intf-fill.wfm-intf-high {
background: color-mix(in srgb, #ef5350 55%, transparent);
}
.wfm-intf-val {
position: relative;
z-index: 1;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.03em;
color: var(--text-primary);
}
.controls-col-center::after {
content: "";
display: block;
min-height: 1.2rem;
margin-top: 0.3rem;
}
.controls-row .label {
margin-bottom: 6px;
margin-top: 0;
min-height: 1.2rem;
display: flex;
align-items: center;
white-space: nowrap;
}
.controls-col .inline,
.controls-col .btn-grid {
align-self: stretch;
}
.controls-col .jog-container { align-self: center; }
/* Sized by the buttons that are actually there. Three fixed tracks meant a rig
without transmit or lock kept two empty ones, leaving its label chip stranded
at the far edge of the group. */
.btn-grid {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.btn-grid button { flex: 0 1 auto; min-width: 6rem; height: var(--control-height); }
.jog-container {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.jog-wheel {
width: var(--jog-wheel-size);
height: var(--jog-wheel-size);
border-radius: 50%;
background: radial-gradient(circle at 40% 35%, var(--jog-hi), var(--jog-lo));
border: 2px solid var(--border-light);
position: relative;
cursor: grab;
flex-shrink: 0;
box-shadow: 0 2px 8px var(--jog-shadow), inset 0 1px 0 var(--jog-inset);
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.jog-indicator {
position: absolute;
width: 6.4px;
height: 16px;
background: var(--accent-green);
border-radius: 2px;
top: 6.4px;
left: 50%;
transform-origin: 50% 35.2px;
transform: translateX(-50%);
pointer-events: none;
}
.jog-btn {
width: 3.3rem;
height: 3.3rem;
font-size: 1.8rem;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
}
.jog-step {
display: flex;
border: 1px solid var(--border-light);
border-radius: 6px;
overflow: hidden;
height: 3.35rem;
flex-shrink: 0;
}
.jog-step button {
border: none;
border-right: 1px solid var(--border-light);
border-radius: 0;
height: 100%;
padding: 0 0.65rem;
font-size: 0.9rem;
background: var(--input-bg);
color: var(--text-muted);
cursor: pointer;
}
.jog-step button:last-child { border-right: none; }
.jog-step button.active {
background: var(--btn-bg);
color: var(--accent-green);
font-weight: 600;
}
.vfo-picker {
display: flex;
border: 1px solid var(--border-light);
border-radius: 6px;
overflow: hidden;
}
.vfo-picker button {
flex: 1;
border: none;
border-right: 1px solid var(--border-light);
border-radius: 0;
height: var(--control-height);
background: var(--input-bg);
color: var(--text-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.85rem;
}
.vfo-picker button:last-child { border-right: none; }
.vfo-picker button.active {
background: var(--btn-bg);
font-weight: 600;
}
.channel-scheduler-controls {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 0.6rem 0.8rem;
}
.vchan-picker {
display: flex;
flex-wrap: wrap;
gap: 4px;
flex: 1 1 18rem;
min-width: 0;
}
.scheduler-control-row {
display: flex;
align-items: flex-start;
justify-content: flex-start;
flex: 0 1 22rem;
}
.scheduler-release-wrap {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.35rem;
min-width: 14rem;
}
.scheduler-release-wrap button.active {
border-color: var(--accent-yellow);
color: var(--accent-yellow);
}
/* One row, read left to right: step through the entries, hand the rig back,
then — fenced off — the entry actually on air. */
.scheduler-action-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
}
/* The rule belongs to the current-entry block, so it leaves with it: the
block is display-toggled whenever fewer than two entries are active. */
.scheduler-action-row #scheduler-cycle-status::before {
content: "";
align-self: stretch;
width: 1px;
min-height: 1.7rem;
margin-inline: 0.3rem;
background: color-mix(in srgb, var(--border-light) 70%, transparent);
}
.scheduler-step-controls {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.scheduler-step-controls button {
min-width: 7.6rem;
}
.scheduler-release-status {
color: var(--text-muted);
font-size: 0.78rem;
text-align: left;
}
.scheduler-cycle-status {
color: var(--text-muted);
font-size: 0.75rem;
text-align: left;
opacity: 0.88;
}
.vchan-picker button {
display: inline-flex;
align-items: center;
gap: 4px;
border: 1px solid var(--border-light);
border-radius: 6px;
height: var(--control-height);
padding: 0 0.55rem;
background: var(--input-bg);
color: var(--text-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.82rem;
cursor: pointer;
}
.vchan-picker button.active {
background: var(--btn-bg);
color: var(--text);
font-weight: 600;
border-color: var(--vchan-color);
box-shadow: inset 3px 0 0 var(--vchan-color);
}
/* Applied to #freq and #spectrum-bw-input when on a virtual channel */
.vchan-ch-active {
border-color: var(--vchan-color) !important;
box-shadow: 0 0 0 1px var(--vchan-color);
}
.vchan-del {
opacity: 0.5;
font-size: 1rem;
line-height: 1;
}
.vchan-del:hover { opacity: 1; }
.vchan-add {
font-size: 1.1rem;
font-weight: 700;
padding: 0 0.6rem !important;
color: var(--text-muted);
}
.signal-measure {
display: inline-flex;
gap: 0.5rem;
align-items: center;
margin-top: 0.4rem;
}
button { padding: 0.5rem 0.9rem; border-radius: 6px; border: 1px solid var(--btn-border); background: var(--btn-bg); color: var(--text); cursor: pointer; height: var(--control-height); transition: background-color 100ms ease, border-color 100ms ease, color 100ms ease, box-shadow 100ms ease; }
button:hover:not(:disabled) { background: color-mix(in srgb, var(--btn-bg) 75%, var(--accent-green)); border-color: color-mix(in srgb, var(--btn-border) 60%, var(--accent-green)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-green) 18%, transparent); }
button:active:not(:disabled) { background: color-mix(in srgb, var(--btn-bg) 55%, var(--accent-green)); border-color: var(--accent-green); box-shadow: none; transform: translateY(1px); }
button:disabled { opacity: 0.6; cursor: not-allowed; }
.hint { color: var(--text-muted); font-size: 0.85rem; }
.inline { display: flex; gap: 0.5rem; align-items: center; }
.freq-inline {
gap: 0.35rem;
align-items: flex-start;
flex-wrap: nowrap;
}
.freq-field {
display: grid;
grid-template-rows: 3.35rem auto;
align-items: start;
}
.freq-field .label {
margin-top: 0.45rem;
margin-bottom: 0;
}
.frequency-col {
flex: 1 1 auto;
min-width: 0;
}
.center-frequency-col {
flex: 0 1 8.75rem;
min-width: 7.75rem;
}
.frequency-col input.status-input {
width: 100%;
height: 3.35rem;
box-sizing: border-box;
}
.wavelength-col {
flex: 0 0 auto;
}
.sig-strength-col {
flex: 0 0 auto;
}
.unit-col {
flex: 0 0 auto;
}
.unit-col .label {
display: flex;
justify-content: flex-end;
}
.mult-col {
flex: 0 0 auto;
}
.mult-col .label {
display: flex;
justify-content: flex-end;
}
.jog-mult {
display: inline-grid;
grid-template-columns: repeat(2, minmax(0, auto));
border: 1px solid var(--border-light);
border-radius: 6px;
overflow: hidden;
height: 3.35rem;
flex-shrink: 0;
width: auto;
}
.jog-mult button {
min-width: 3.15rem;
border: none;
border-right: 1px solid var(--border-light);
border-radius: 0;
height: 100%;
padding: 0 0.7rem;
font-size: 0.85rem;
background: var(--input-bg);
color: var(--text-muted);
cursor: pointer;
}
.jog-mult button:last-child { border-right: none; }
.jog-mult button.active {
background: var(--btn-bg);
color: var(--accent-green);
font-weight: 600;
}
.label-below-row {
display: flex;
flex-direction: column;
}
#tab-main .label > span {
text-align: center;
}
.label-below-row > .label {
order: 2;
margin-top: 0.45rem;
margin-bottom: 0;
}
.label-below-row > :not(.label) {
order: 1;
}
.wavelength-display {
min-width: 5.2rem;
height: 3.35rem;
padding: 0 0.7rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
color: var(--wavelength-fg);
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'DSEG14 Classic', monospace;
font-weight: 600;
font-size: 1.25rem;
letter-spacing: 0.03em;
white-space: nowrap;
flex-shrink: 0;
}
.sig-strength-display {
min-width: 7.5rem;
height: 3.35rem;
padding: 0 0.7rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
color: var(--wavelength-fg);
display: inline-flex;
align-items: center;
justify-content: center;
font-family: 'DSEG14 Classic', monospace;
font-weight: 600;
font-size: 1.25rem;
letter-spacing: 0.03em;
white-space: nowrap;
flex-shrink: 0;
cursor: pointer;
user-select: none;
}
.sig-unit {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.85em;
font-weight: 400;
}
small { color: var(--text-muted); }
.header {
margin-bottom: 0;
padding: 0.25rem 0 0.15rem;
position: relative;
z-index: 6;
}
.header-main {
display: inline-flex;
align-items: center;
gap: 0.9rem;
max-width: min(100%, 46rem);
padding: 0.35rem 0.8rem 0.4rem 0.45rem;
margin-left: -0.3rem;
transform: translateY(-10px);
border-radius: 0.95rem;
background: color-mix(in srgb, var(--card-bg) 92%, transparent);
box-shadow:
0 8px 20px color-mix(in srgb, var(--bg) 18%, transparent),
inset 0 1px 0 color-mix(in srgb, var(--text) 10%, transparent);
}
.header-text {
width: auto;
min-width: 0;
flex: 0 1 auto;
}
.title { font-size: 1.4rem; font-weight: 700; display: inline-flex; align-items: center; gap: 0.35rem; }
.title-link {
color: inherit;
text-decoration: none;
}
.title-link:hover {
text-decoration: underline;
text-underline-offset: 0.14em;
}
.overview-strip {
width: 100%;
margin: 0;
position: relative;
}
.signal-visual-block {
position: relative;
display: flex;
flex-direction: column;
gap: 0;
margin-bottom: 0.9rem;
}
#signal-split-control {
position: absolute;
top: 50%;
right: 0.32rem;
transform: translateY(-50%);
z-index: 9;
display: none;
flex-direction: column;
align-items: center;
gap: 0.24rem;
padding: 0.24rem 0.2rem;
border: 1px solid var(--btn-border);
border-radius: 6px;
background: var(--btn-bg);
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 6%, transparent);
}
#signal-split-slider {
writing-mode: vertical-lr;
direction: rtl;
width: 0.5rem;
height: 5.4rem;
margin: 0;
accent-color: var(--accent-green);
cursor: ns-resize;
}
#signal-split-value {
min-width: 2.4rem;
text-align: center;
font-size: 0.6rem;
font-weight: 600;
letter-spacing: 0.03em;
color: var(--text);
}
#signal-overlay-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 0;
pointer-events: none;
z-index: 4;
}
#fast-freq-marker {
display: none;
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
background: #ff1744;
opacity: 0.85;
pointer-events: none;
z-index: 5;
will-change: transform;
}
#fast-bw-left, #fast-bw-right {
display: none;
position: absolute;
top: 0;
left: 0;
height: 100%;
pointer-events: none;
z-index: 3;
will-change: transform, width;
}
#fast-bw-left {
background: linear-gradient(to right, transparent, rgba(255,23,68,0.10));
}
#fast-bw-right {
background: linear-gradient(to left, transparent, rgba(255,23,68,0.10));
}
#rds-ps-overlay {
display: none;
position: absolute;
inset: 0;
z-index: 5;
pointer-events: none;
}
.rds-ps-overlay-item {
position: absolute;
transform: translate(-50%, -50%);
pointer-events: auto;
cursor: pointer;
color: var(--text-heading);
padding: 0.34rem 0.9rem 0.28rem;
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
border-radius: 6px;
background: color-mix(in srgb, var(--card-bg) 92%, transparent);
box-shadow:
0 8px 18px color-mix(in srgb, var(--bg) 16%, transparent),
inset 0 1px 0 color-mix(in srgb, var(--text) 10%, transparent);
text-shadow: 0 1px 10px color-mix(in srgb, var(--bg) 68%, transparent);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.16rem;
text-align: center;
max-width: min(92vw, 24rem);
overflow: hidden;
text-overflow: ellipsis;
}
.rds-ps {
cursor: pointer;
}
.rds-ps-main {
display: block;
font-family: 'DSEG14 Classic', monospace;
font-size: clamp(1rem, 2.2vw, 1.45rem);
letter-spacing: 0.08em;
white-space: pre;
}
.rds-ps-gap {
color: color-mix(in srgb, currentColor 34%, var(--text-muted));
}
.rds-ps-fallback {
display: block;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: clamp(0.72rem, 1.4vw, 0.95rem);
font-weight: 700;
letter-spacing: 0.04em;
white-space: nowrap;
}
.rds-ps-meta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.4rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: clamp(0.58rem, 1.1vw, 0.78rem);
letter-spacing: 0.04em;
color: var(--text-muted);
white-space: nowrap;
}
.rds-ps-meta-text {
display: inline-block;
}
.rds-ps-flags {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.rds-flag {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.9em;
padding: 0.05rem 0.35rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
font-size: 0.92em;
font-weight: 700;
letter-spacing: 0.03em;
}
.rds-flag-active {
color: color-mix(in srgb, var(--accent-red) 30%, #fff);
background: color-mix(in srgb, var(--accent-red) 68%, transparent);
border-color: color-mix(in srgb, var(--accent-red) 46%, transparent);
box-shadow: 0 0 10px color-mix(in srgb, var(--accent-red) 28%, transparent);
}
.rds-flag-inactive {
color: var(--text-muted);
background: color-mix(in srgb, var(--card-bg) 62%, transparent);
}
#aprs-bar-overlay,
#ais-bar-overlay,
#vdes-bar-overlay,
#ft8-bar-overlay,
#cw-bar-overlay {
display: none;
position: absolute;
top: 50%;
left: 0.45rem;
right: 0.45rem;
width: auto;
max-width: none;
transform: translateY(-50%);
z-index: 5;
pointer-events: auto;
cursor: default;
user-select: text;
color: var(--text-heading);
padding: 0.22rem 0.55rem 0.24rem;
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
border-radius: 6px;
background: color-mix(in srgb, var(--card-bg) 92%, transparent);
box-shadow:
0 10px 24px color-mix(in srgb, var(--bg) 14%, transparent),
inset 0 1px 0 color-mix(in srgb, var(--text) 8%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--text) 4%, transparent);
max-height: min(58%, 9.5rem);
overflow-y: auto;
flex-direction: column;
gap: 0.12rem;
}
.aprs-bar-header {
position: sticky;
top: 0;
z-index: 1;
background: color-mix(in srgb, var(--card-bg) 95%, transparent);
display: flex;
align-items: center;
justify-content: flex-start;
gap: 0.32rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: clamp(0.6rem, 0.9vw, 0.75rem);
font-weight: 700;
letter-spacing: 0.06em;
color: var(--accent-green);
text-transform: uppercase;
padding: 0;
margin-bottom: 0.02rem;
border-bottom: 1px solid color-mix(in srgb, var(--border-light) 22%, transparent);
opacity: 0.9;
}
.aprs-bar-title {
display: inline-flex;
align-items: center;
gap: 0.1rem;
min-width: 0;
flex: 0 0 auto;
}
.aprs-bar-title-word {
display: inline-block;
}
.aprs-bar-window {
display: inline-flex;
align-items: center;
color: var(--text-muted);
font-size: 0.72em;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: none;
white-space: nowrap;
}
.aprs-bar-actions {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 0.38rem;
flex: 0 0 auto;
}
.aprs-bar-clear-wrap {
display: inline-flex;
align-items: center;
flex: 0 0 auto;
padding: 0;
align-self: center;
}
.aprs-bar-clear {
display: inline;
background: transparent;
border: none;
border-radius: 0;
padding: 0;
line-height: 1;
font-family: inherit;
font-size: 0.8em;
font-weight: 600;
letter-spacing: 0.03em;
color: var(--text-muted);
cursor: pointer;
opacity: 0.82;
text-transform: uppercase;
white-space: nowrap;
transition: opacity 120ms, color 120ms;
}
.aprs-bar-clear:hover {
opacity: 1;
color: var(--accent-green);
text-decoration: underline;
}
.aprs-bar-clear-wrap:hover {
color: inherit;
}
.aprs-bar-close {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.05rem;
height: 1.05rem;
padding: 0;
border: none;
border-radius: 999px;
background: transparent;
color: var(--text-muted);
font-family: inherit;
font-size: 0.95em;
font-weight: 700;
line-height: 1;
cursor: pointer;
opacity: 0.82;
transition: opacity 120ms, color 120ms, background-color 120ms;
}
.aprs-bar-close:hover {
opacity: 1;
color: var(--accent-green);
background: color-mix(in srgb, var(--btn-bg) 70%, transparent);
}
.aprs-bar-frame {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: clamp(0.56rem, 0.92vw, 0.7rem);
line-height: 1.12;
padding: 0.08rem 0;
border-bottom: 1px solid color-mix(in srgb, var(--border-light) 18%, transparent);
opacity: 1;
}
.aprs-bar-frame:last-child {
border-bottom: none;
}
.aprs-bar-frame-main {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-right: 0.15rem;
}
.aprs-bar-pin {
display: inline-flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--btn-bg) 74%, transparent);
border: 1px solid color-mix(in srgb, var(--border-light) 28%, transparent);
border-radius: 4px;
padding: 0;
margin-right: 0.28em;
width: 1.55em;
height: 1.55em;
font-size: 0.72em;
line-height: 1;
cursor: pointer;
vertical-align: middle;
opacity: 0.82;
transition: opacity 120ms, border-color 120ms, background-color 120ms;
}
.aprs-bar-pin:hover {
opacity: 1;
background: color-mix(in srgb, var(--btn-bg) 92%, transparent);
border-color: color-mix(in srgb, var(--accent-green) 28%, var(--border-light));
}
.aprs-bar-frame + .aprs-bar-frame {
opacity: 0.78;
}
.aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame {
opacity: 0.62;
}
.aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame {
opacity: 0.46;
}
.aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame + .aprs-bar-frame {
opacity: 0.34;
}
.aprs-bar-time {
color: color-mix(in srgb, var(--text-muted) 92%, transparent);
margin-right: 0.42em;
font-size: 0.94em;
}
.aprs-bar-call {
color: var(--accent-green);
font-weight: 600;
}
.aprs-bar-crc {
color: var(--accent-red);
margin-left: 0.3em;
}
.overview-toolbar {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.75rem;
margin-bottom: 0;
position: absolute;
top: calc(var(--header-waterfall-overlap) + 0.2rem);
right: 0.15rem;
z-index: 2;
}
.overview-label {
margin-left: auto;
color: var(--text-heading);
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.overview-control {
display: inline-flex;
align-items: center;
gap: 0.4rem;
color: var(--text-muted);
font-size: 0.8rem;
white-space: nowrap;
}
.overview-control .status-input {
width: auto;
min-width: 4.6rem;
padding-top: 0.22rem;
padding-bottom: 0.22rem;
font-size: 0.85rem;
}
#overview-canvas {
width: 100%;
height: var(--overview-plot-height);
display: block;
}
.header-left {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
gap: 0;
flex-shrink: 0;
}
/* One row, always. Controls that do not fit are moved into the overflow menu
* by ui-core rather than wrapping: wrapping made the header's height depend on
* the viewport width in a way that was not even monotonic (112px at 1440,
* 169px at 1100, 131px at 900), which is what made the bar feel unstable. */
.top-bar-actions {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
flex-wrap: nowrap;
}
/* Every control in the bar is the same height and none of them stretch. */
.top-bar-actions > * {
flex: 0 0 auto;
}
.header-bar-btn.header-audio-btn {
width: 2rem;
height: 2rem;
min-height: 0;
padding: 2px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
color: var(--text-muted);
cursor: pointer;
flex-shrink: 0;
}
.header-audio-btn svg {
width: 100%;
height: 100%;
}
.header-audio-btn.audio-active {
color: #00d17f;
border-color: #00d17f;
}
.header-bar-btn.header-rec-btn {
height: 2rem;
min-height: 0;
padding: 0 0.45rem;
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 0.04em;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
color: var(--text-muted);
cursor: pointer;
flex-shrink: 0;
}
.header-rec-btn.rec-active {
color: #ff3b30;
border-color: #ff3b30;
background: rgba(255, 59, 48, 0.12);
animation: rec-pulse 1.5s ease-in-out infinite;
}
/* ── Recorder page ──────────────────────────────────────────────────────── */
.recorder-controls-bar {
display: flex;
gap: 0.75rem;
align-items: center;
margin-bottom: 1.25rem;
}
.recorder-action-btn {
padding: 0.45rem 1rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
color: var(--text-primary);
font-size: 0.85rem;
cursor: pointer;
transition: border-color 0.15s, color 0.15s, background-color 0.15s;
}
.recorder-action-btn:hover:not(:disabled) { border-color: var(--accent-green); color: var(--accent-green); }
.recorder-action-btn:disabled { opacity: 0.4; cursor: default; }
.recorder-action-btn.recorder-stop:not(:disabled) { border-color: #ff3b30; color: #ff3b30; }
.recorder-action-btn.recorder-stop:hover:not(:disabled) { background: rgba(255,59,48,0.1); }
.recorder-status-indicator {
font-size: 0.8rem;
color: var(--text-muted);
}
.recorder-status-indicator.rec-active {
color: #ff3b30;
font-weight: 600;
animation: rec-pulse 1.5s ease-in-out infinite;
}
@keyframes rec-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.tab.rec-active .tab-icon { color: #ff3b30; }
.recorder-filter-bar {
display: flex;
gap: 0.6rem;
align-items: center;
margin-bottom: 0.6rem;
}
.recorder-filter-input { flex: 3; min-width: 16rem; }
.recorder-sort-select { flex: 1; width: auto; min-width: 7rem; max-width: 10rem; }
.recorder-section { margin-bottom: 1.5rem; }
.recorder-section-heading {
font-size: 0.85rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 0.5rem;
}
.recorder-list { font-size: 0.82rem; }
.recorder-empty { color: var(--text-muted); font-style: italic; margin: 0; }
.recorder-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
}
.recorder-table th,
.recorder-table td {
padding: 0.35rem 0.6rem;
text-align: left;
border-bottom: 1px solid var(--border-light);
}
.recorder-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.recorder-table td:first-child,
.recorder-table th:first-child { width: 100%; }
.recorder-table td:nth-child(2),
.recorder-table th:nth-child(2) { white-space: nowrap; }
.recorder-table td:last-child,
.recorder-table th:last-child { text-align: right; white-space: nowrap; }
.recorder-table .rec-file-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.4rem;
}
.recorder-table .rec-file-btn {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
margin: 0;
padding: 0.2rem 0.5rem;
height: 1.55rem;
font-size: 0.78rem;
font-family: inherit;
line-height: 1;
text-align: center;
text-decoration: none;
border: 1px solid var(--btn-border);
border-radius: 0.25rem;
background: var(--btn-bg);
color: var(--text);
cursor: pointer;
}
.recorder-table .rec-file-btn:hover { background: color-mix(in srgb, var(--btn-bg) 75%, var(--accent-green)); border-color: color-mix(in srgb, var(--btn-border) 60%, var(--accent-green)); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-green) 18%, transparent); }
.recorder-table .rec-file-btn:active { background: color-mix(in srgb, var(--btn-bg) 55%, var(--accent-green)); border-color: var(--accent-green); box-shadow: none; transform: translateY(1px); }
.recorder-table .rec-file-btn.rec-delete-btn { color: var(--accent-red); border-color: var(--accent-red); }
.recorder-table .rec-file-btn.rec-delete-btn:hover { background: color-mix(in srgb, var(--btn-bg) 75%, var(--accent-red)); border-color: var(--accent-red); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-red) 18%, transparent); }
.recorder-table tr.rec-player-row > td {
padding: 0.4rem 0.6rem 0.6rem;
background: color-mix(in srgb, var(--btn-bg) 55%, transparent);
border-top: 0;
}
.recorder-table .rec-player-audio {
display: block;
width: 100%;
height: 2.1rem;
border-radius: 0.25rem;
outline: none;
}
.recorder-page-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
padding: 0.9rem 0.2rem 0;
}
/* Inline, not stacked: the summary used to sit under the select, making this
* the only 54px control in a bar of 32px ones. */
.header-rig-switch {
display: flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.header-rig-switch select {
min-width: 8rem;
height: 2rem;
padding: 0.15rem 0.35rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
color: var(--text);
font-size: 0.85rem;
align-self: start;
}
.header-rig-switch button {
height: 2rem;
padding: 0 0.65rem;
font-size: 0.78rem;
white-space: nowrap;
}
.header-bar-btn {
height: 2rem;
padding: 0 0.65rem;
font-size: 0.78rem;
white-space: nowrap;
}
/* Theme toggle: styled to match the active theme */
#theme-toggle {
background: #1e2a3a;
color: #e7edf9;
border-color: #3a5274;
}
[data-theme="light"] #theme-toggle {
background: #dde3ed;
color: #1a2336;
border-color: #a0aec0;
}
.header-style-pick {
display: flex;
align-items: center;
}
.header-style-pick select {
height: 2rem;
padding: 0.15rem 0.35rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
color: var(--text);
font-size: 0.85rem;
}
.header-logo { height: 4.6em; width: auto; flex-shrink: 0; filter: drop-shadow(0 4px 12px rgba(0,0,0,0.35)); }
.subtitle { color: var(--text-muted); font-size: 0.95rem; }
.subtitle a { color: var(--accent-text); text-decoration: none; }
.subtitle a:hover { text-decoration: underline; }
.band-tag { display: inline-block; padding: 2px 6px; border-radius: 6px; background: var(--btn-bg); color: var(--text); font-size: 0.82rem; border: 1px solid var(--border-light); margin-left: 6px; }
.signal { display: flex; gap: 0.6rem; align-items: center; }
.signal-bar { flex: 1 1 auto; height: 12px; border-radius: 999px; background: var(--btn-bg); border: 1px solid var(--border-light); overflow: hidden; }
.signal-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent-green), var(--accent-yellow), var(--accent-red)); transition: width 300ms ease-out; }
.signal-value { font-size: 0.95rem; color: var(--text-heading); min-width: 48px; text-align: right; }
.meter { display: flex; gap: 0.6rem; align-items: center; }
.meter-bar { flex: 1 1 auto; height: 12px; border-radius: 999px; background: var(--btn-bg); border: 1px solid var(--border-light); overflow: hidden; }
.meter-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent-green), var(--accent-yellow), var(--accent-red)); transition: width 150ms ease; }
.meter-value { font-size: 0.95rem; color: var(--text-heading); min-width: 64px; text-align: right; }
#content { display: flex; flex-direction: column; gap: 1.1rem; min-height: 0; flex: 1 1 auto; overflow: visible; position: relative; }
.tab-panel { flex: 1 1 auto; min-height: 0; overflow: visible; display: flex; flex-direction: column; }
.sub-tab-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
.tab-bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.85rem;
padding: 0.45rem 0 0.7rem;
border-bottom: 1px solid color-mix(in srgb, var(--border-light) 55%, transparent);
position: relative;
z-index: 2;
}
.tab-bar-left {
display: flex;
align-items: center;
gap: 1rem;
min-width: 0;
flex: 1 1 auto;
}
.tab-bar .header-main {
display: flex;
align-items: center;
gap: 0.7rem;
max-width: min(100%, 24rem);
padding: 0;
margin-left: 0;
transform: none;
flex-shrink: 0;
border-radius: 0;
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
box-shadow: none;
}
.tab-bar .header-logo {
height: 2.6rem;
}
.tab-bar .title {
font-size: 1.05rem;
line-height: 1.1;
}
.tab-bar .subtitle {
font-size: 0.78rem;
line-height: 1.15;
}
.tab-bar-nav {
display: flex;
align-items: center;
gap: 0.2rem;
min-width: 0;
/* Never wraps. It must still be able to shrink below its content, or it
* overlaps the controls; the menus it anchors are reparented to the body on
* open, so clipping here no longer reaches them. */
flex-wrap: nowrap;
overflow: hidden;
}
.tab-bar-nav .tab { flex: 0 0 auto; }
/* Icons before scrolling: every tab already carries one, and four icons plus
* More always fit, so the strip never has to hide a destination. Applied by
* measurement (ui-core's reflowOverflow) rather than at a viewport width: how
* much fits depends on the rig name and on how wide the platform renders the
* labels, so the same width fits on one machine and clips on another.
* Bounded below 761px, where the strip becomes the bottom nav and keeps its
* labels under the icons. */
@media (min-width: 761px) {
.tab-bar-nav.nav-icons-only .tab .tab-label { display: none; }
.tab-bar-nav.nav-icons-only .tab .tab-icon,
.tab-bar-nav.nav-icons-only .tab .tab-more-icon { display: block; }
.tab-bar-nav.nav-icons-only .tab { padding: 0.45rem 0.55rem; }
}
/* Last resort, past icons: something in the bar has to absorb the shortfall,
and by default that is the tab strip — it may shrink below its content, so
its tabs keep full width and run under the controls, unreachable. The
identity block takes it instead, ellipsised: a clipped station name is still
readable, a destination hidden under the controls is not. */
.tab-bar.bar-tight .header-main {
flex-shrink: 1;
min-width: 0;
}
.tab-bar.bar-tight .header-text {
min-width: 0;
}
.tab-bar.bar-tight .title,
.tab-bar.bar-tight .subtitle {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tab-bar.bar-tight .tab-bar-nav {
flex-shrink: 0;
}
/* The selected destination is boxed, not underlined — the same treatment the
mobile bottom nav already used, so one navigation model reads the same at
every width. The transparent border is on the base so switching tabs moves
no neighbours. */
.tab {
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-md);
padding: 0.45rem 0.9rem;
color: var(--text-muted);
cursor: pointer;
font-size: 0.95rem;
height: auto;
transition: color var(--dur-fast) var(--ease-standard),
background-color var(--dur-fast) var(--ease-standard),
border-color var(--dur-fast) var(--ease-standard);
}
.tab.active {
border-color: color-mix(in srgb, var(--accent-green) 50%, var(--border-light));
background: color-mix(in srgb, var(--accent-green) 12%, transparent);
color: var(--accent-text);
font-weight: 600;
}
.tab:hover:not(.active) {
color: var(--text);
background: color-mix(in srgb, var(--btn-bg) 55%, transparent);
border-color: color-mix(in srgb, var(--border-light) 40%, transparent);
}
/* Tab icons — hidden on desktop, shown on mobile bottom nav */
.tab-icon {
display: none;
width: 20px;
height: 20px;
flex-shrink: 0;
}
.tab-label { display: block; }
.tab-more-icon { display: none; }
.about-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; }
.about-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 0.5rem; padding: 0; overflow: hidden; }
.about-card-title { display: flex; align-items: center; gap: 0.5rem; padding: 0.6rem 0.75rem; font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-heading); border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--card-bg) 50%, var(--bg)); }
.about-card-icon { width: 15px; height: 15px; flex-shrink: 0; }
.about-table { width: 100%; border-collapse: collapse; }
.about-table td { padding: 0.4rem 0.75rem; border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); font-size: 0.85rem; }
.about-table tr:last-child td { border-bottom: none; }
.about-table td:first-child { color: var(--text-muted); width: 40%; }
.about-status-on { color: var(--status-ok); }
.about-status-off { color: var(--text-muted); }
.plugin-item { padding: 0.5rem 0.6rem; border-bottom: 1px solid var(--border); color: var(--text); }
.plugin-item:last-child { border-bottom: none; }
/* ── Footer ───────────────────────────────────────────────────────────
A hairline rule closes the page the way .tab-bar opens it; the two
clusters (attribution + source pill, live status chip) sit on one
baseline-free centre line so the pills don't hang off the text. */
.footer {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-4);
flex-wrap: wrap;
margin-top: auto;
padding: var(--space-3) 0 var(--space-1);
border-top: 1px solid color-mix(in srgb, var(--border-light) 45%, transparent);
flex-shrink: 0;
}
.footer-meta {
display: flex;
align-items: center;
gap: var(--space-3);
flex-wrap: wrap;
min-width: 0;
}
.full-row { grid-column: 1 / -1; }
/* No opacity: --text-muted is already the muted step, and stacking the
two put the attribution below a readable contrast ratio. */
.copyright { color: var(--text-muted); font-size: var(--fs-xs); }
.copyright a { color: var(--accent-text); text-decoration: none; }
.copyright a:hover { text-decoration: underline; }
/* Live status: dot + text, colour driven by the data-state app.ts sets. */
.footer .hint {
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-size: var(--fs-xs);
color: var(--text-heading);
padding: 0.2rem 0.6rem;
border-radius: var(--radius-pill);
border: 1px solid color-mix(in srgb, var(--border-light) 60%, transparent);
background: color-mix(in srgb, var(--btn-bg) 55%, transparent);
font-variant-numeric: tabular-nums;
}
.footer .hint::before {
content: "";
width: 0.45rem;
height: 0.45rem;
flex-shrink: 0;
border-radius: 50%;
background: var(--text-muted);
box-shadow: 0 0 0 0.16rem color-mix(in srgb, var(--text-muted) 22%, transparent);
transition: background var(--dur-base) var(--ease-standard),
box-shadow var(--dur-base) var(--ease-standard);
}
.footer .hint[data-state="ok"]::before {
background: var(--status-ok);
box-shadow: 0 0 0 0.16rem color-mix(in srgb, var(--status-ok) 24%, transparent);
}
.footer .hint[data-state="busy"]::before {
background: var(--accent-yellow);
box-shadow: 0 0 0 0.16rem color-mix(in srgb, var(--accent-yellow) 24%, transparent);
}
.footer .hint[data-state="error"] {
color: var(--accent-red);
border-color: color-mix(in srgb, var(--accent-red) 45%, var(--border-light));
}
.footer .hint[data-state="error"]::before {
background: var(--accent-red);
box-shadow: 0 0 0 0.16rem color-mix(in srgb, var(--accent-red) 24%, transparent);
}
@media (prefers-reduced-motion: no-preference) {
.footer .hint[data-state="busy"]::before,
.footer .hint[data-state="error"]::before {
animation: trx-status-pulse 1.8s var(--ease-standard) infinite;
}
}
@keyframes trx-status-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.45; }
}
.gh-link-wrap {
display: inline-flex;
vertical-align: middle;
margin: 0 0.15rem;
}
.gh-link {
display: inline-flex;
align-items: center;
text-decoration: none;
font-size: var(--fs-xs);
gap: 0.34rem;
padding: 0.18rem 0.5rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
background: color-mix(in srgb, var(--btn-bg) 78%, transparent);
color: var(--text-heading);
line-height: 1;
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
}
.gh-link:hover {
text-decoration: none;
border-color: color-mix(in srgb, var(--accent-green) 48%, var(--border-light));
background: color-mix(in srgb, var(--btn-bg) 90%, transparent);
}
.gh-link-icon {
width: 0.95rem;
height: 0.95rem;
flex-shrink: 0;
fill: currentColor;
}
.vol-label {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.15rem;
color: var(--text-muted);
font-size: 0.82rem;
white-space: nowrap;
}
.vol-pct {
font-size: 0.75rem;
color: var(--text-muted);
line-height: 1;
}
/* Squelch: a threshold in dB, the unit the spectrum's own axis is labelled in,
with a dot showing whether the gate is passing audio right now. The old
percentage was a number with nothing on screen to relate it to. */
.sql-control {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--text-muted);
font-size: 0.82rem;
white-space: nowrap;
}
/* The row carries three unrelated things — how loud, whether there is audio at
all, and how much is arriving — so each is named and fenced from the next.
Each rule belongs to the block that follows it, so it leaves when that block
does (the squelch is absent on rigs without one). */
.audio-group {
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.audio-volume-group {
flex: 0 1 auto;
}
.audio-level-group {
flex: 1 1 12rem;
min-width: 8rem;
}
.audio-group-label {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-muted);
white-space: nowrap;
}
.audio-level-group #audio-status {
min-width: 3.4rem;
}
.audio-level-group::before,
.sql-control::before {
content: "";
align-self: stretch;
width: 1px;
min-height: 1.5rem;
margin-inline: 0.5rem 0.6rem;
background: color-mix(in srgb, var(--border-light) 65%, transparent);
display: none;
}
/* Only while the row is one line. A rule divides what sits either side of it,
so once a group wraps onto its own line the wrap is the division and the
rule would just be a mark at the start of a line. Measured against the row,
not the viewport: what fits depends on whether the rig transmits and whether
it has a squelch at all. */
#audio-row .inline {
container-type: inline-size;
container-name: audio-row;
}
@container audio-row (min-width: 57rem) {
.audio-level-group::before,
.sql-control::before {
display: block;
}
}
/* The name is the switch: one target instead of a label and a button that
said the same thing twice, with the dot carrying the state. */
.sql-toggle {
display: inline-flex;
align-items: center;
gap: 0.3rem;
height: 1.5rem;
min-height: 0;
padding: 0 0.4rem;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--text-muted);
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
}
.sql-toggle:hover {
border-color: color-mix(in srgb, var(--border-light) 60%, transparent);
color: var(--text);
}
.sql-toggle[aria-pressed="true"] {
border-color: color-mix(in srgb, var(--accent-green) 50%, var(--border-light));
background: color-mix(in srgb, var(--accent-green) 10%, transparent);
color: var(--accent-text);
}
.sql-state {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
flex-shrink: 0;
background: color-mix(in srgb, var(--text-muted) 45%, transparent);
transition: background var(--dur-fast) var(--ease-standard),
box-shadow var(--dur-fast) var(--ease-standard);
}
.sql-state[data-state="open"] {
background: var(--status-ok);
box-shadow: 0 0 0 0.14rem color-mix(in srgb, var(--status-ok) 25%, transparent);
}
.sql-state[data-state="closed"] {
background: var(--accent-yellow);
box-shadow: 0 0 0 0.14rem color-mix(in srgb, var(--accent-yellow) 22%, transparent);
}
.sql-db {
display: inline-flex;
align-items: baseline;
gap: 0.2rem;
}
.sql-db input {
width: 3.4rem;
height: 1.5rem;
min-height: 0;
padding: 0 0.3rem;
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
text-align: right;
border: 1px solid var(--border-light);
border-radius: 4px;
background: var(--input-bg);
color: var(--text);
}
.sql-db-unit {
font-size: 0.68rem;
}
.sql-control button[aria-pressed="true"] {
border-color: color-mix(in srgb, var(--accent-green) 55%, var(--border-light));
color: var(--accent-text);
}
/* The line is the control: it spans the plot at its threshold and carries its
own grip, so the level is set where the noise it has to clear is visible. */
.spectrum-squelch-line {
position: absolute;
left: 0;
right: 0;
height: 0;
/* Above the overlays that share the plot: the split control sits at the
right edge on z-index 9 and would otherwise swallow the grip's pointer. */
z-index: 10;
border-top: 1px dashed color-mix(in srgb, var(--accent-yellow) 75%, transparent);
pointer-events: none;
}
.spectrum-squelch-line[data-state="open"] {
border-top-color: color-mix(in srgb, var(--status-ok) 80%, transparent);
}
.spectrum-squelch-grip {
position: absolute;
/* Clear of the split control's column so the two never sit on top of each
other, whatever height the threshold is at. */
right: 3.6rem;
top: -0.72rem;
padding: 0.05rem 0.35rem;
border-radius: 3px;
border: 1px solid color-mix(in srgb, var(--accent-yellow) 60%, var(--border-light));
background: color-mix(in srgb, var(--card-bg) 88%, transparent);
color: var(--text);
font-size: 0.62rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.3;
cursor: ns-resize;
pointer-events: auto;
user-select: none;
touch-action: none;
}
.spectrum-squelch-line[data-state="open"] .spectrum-squelch-grip {
border-color: color-mix(in srgb, var(--status-ok) 60%, var(--border-light));
}
.spectrum-squelch-grip:focus-visible {
outline: 2px solid var(--accent-text);
outline-offset: 1px;
}
.sql-auto-btn {
font-size: 0.68rem;
padding: 0 5px;
height: 1.2rem;
min-height: 0;
line-height: 1;
border-radius: 3px;
cursor: pointer;
}
.vol-slider {
-webkit-appearance: none;
appearance: none;
width: 80px;
height: 6px;
border-radius: 3px;
background: var(--btn-bg);
border: 1px solid var(--border-light);
outline: none;
cursor: pointer;
}
.vol-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent-green);
border: none;
cursor: pointer;
}
.vol-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent-green);
border: none;
cursor: pointer;
}
#audio-level {
flex: 1 1 auto;
height: 12px;
border-radius: 999px;
background: var(--audio-level-bg);
border: 1px solid var(--audio-level-border);
overflow: hidden;
min-width: 80px;
}
#audio-level-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--audio-level-fill-start), var(--audio-level-fill-end));
transition: width 100ms ease;
}
.sub-tab-bar { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 0.75rem; overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* ── Digital modes: sidebar + panel ───────────────────────────────────
Thirteen decoders in a horizontal strip meant a scroller on anything but a
wide window, and which one was open was a single underline in a row of
thirteen. As a list down the side every decoder is visible at once, each
with the state dot it already carried, and the panel gets the full width. */
#tab-digital-modes {
display: grid;
grid-template-columns: minmax(8.5rem, 11rem) minmax(0, 1fr);
gap: 1rem;
/* The panel takes the full height of the tab, which is what the decode
lists inside it size against: FT8, FT4, FT2 and WSPR fill their panel
with flex, so a panel sized to its own content collapsed them to their
120px minimum however much room was going spare. */
align-items: stretch;
}
#tab-digital-modes > .sub-tab-bar {
/* The list keeps its own height while the panel stretches. */
align-self: start;
flex-direction: column;
align-items: stretch;
gap: 0.12rem;
margin-bottom: 0;
padding-right: 0.85rem;
border-bottom: none;
border-right: 1px solid color-mix(in srgb, var(--border) 80%, transparent);
overflow-x: hidden;
/* Stays put while a long decode list scrolls past it. */
position: sticky;
top: 0.5rem;
}
#tab-digital-modes > .sub-tab-bar .sub-tab {
display: flex;
align-items: center;
gap: 0.4rem;
width: 100%;
padding: 0.42rem 0.6rem;
border: 1px solid transparent;
border-radius: var(--radius-md);
text-align: left;
font-size: 0.88rem;
transition: color var(--dur-fast) var(--ease-standard),
background-color var(--dur-fast) var(--ease-standard),
border-color var(--dur-fast) var(--ease-standard);
}
#tab-digital-modes > .sub-tab-bar .sub-tab .decoder-state-dot {
margin-left: auto;
}
#tab-digital-modes > .sub-tab-bar .sub-tab:hover:not(.active) {
color: var(--text);
background: color-mix(in srgb, var(--btn-bg) 55%, transparent);
}
#tab-digital-modes > .sub-tab-bar .sub-tab.active {
border-color: color-mix(in srgb, var(--accent-green) 50%, var(--border-light));
background: color-mix(in srgb, var(--accent-green) 12%, transparent);
color: var(--accent-text);
}
#tab-digital-modes > .sub-tab-panel {
grid-column: 2;
grid-row: 1;
min-width: 0;
min-height: 0;
}
.sub-tab { flex-shrink: 0; background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: 0.35rem 0.75rem; color: var(--text-muted); cursor: pointer; font-size: 0.85rem; height: auto; }
.sub-tab.active { border-bottom-color: var(--accent-green); color: var(--accent-green); font-weight: 600; }
.sub-tab:hover:not(.active) { color: var(--text); }
/* ── Shortcut help overlay (F1) ─────────────────────────────────────── */
.shortcut-overlay {
position: fixed;
inset: 0;
z-index: 9600;
display: flex;
align-items: center;
justify-content: center;
padding: 1.2rem;
background: rgba(7, 13, 26, 0.82);
opacity: 1;
visibility: visible;
transition: opacity 140ms ease, visibility 140ms ease;
}
.shortcut-overlay.is-hidden {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.shortcut-overlay-card {
min-width: min(22rem, calc(100vw - 2.4rem));
max-width: min(28rem, calc(100vw - 2.4rem));
padding: 1.2rem 1.4rem;
border-radius: 0.9rem;
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
background: color-mix(in srgb, var(--card-bg) 92%, transparent);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.22);
}
.shortcut-overlay-title {
font-size: 1.05rem;
font-weight: 800;
color: var(--text-heading);
margin-bottom: 0.8rem;
text-align: center;
}
.shortcut-table {
width: 100%;
border-collapse: collapse;
}
.shortcut-table td {
padding: 0.32rem 0.4rem;
font-size: 0.85rem;
color: var(--text);
border-bottom: 1px solid color-mix(in srgb, var(--border-light) 40%, transparent);
}
.shortcut-table tr:last-child td {
border-bottom: none;
}
.shortcut-key {
width: 5rem;
text-align: right;
padding-right: 0.9rem !important;
}
.shortcut-key kbd,
.shortcut-overlay-hint kbd {
display: inline-block;
min-width: 1.6em;
padding: 0.12em 0.45em;
border: 1px solid var(--border);
border-radius: 0.3rem;
background: var(--bg);
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
text-align: center;
color: var(--text);
box-shadow: 0 1px 0 color-mix(in srgb, var(--border) 60%, transparent);
}
.shortcut-overlay-hint {
margin-top: 0.7rem;
text-align: center;
font-size: 0.75rem;
color: var(--text-muted);
}
/* Decode history loads in the corner, not over the page. It was a full-screen
scrim: the operator could not read the waterfall, the decode panels or
anything else while a large history replayed, and the replay is exactly when
there is something to watch. */
.history-progress {
position: fixed;
left: var(--space-4);
bottom: var(--space-4);
z-index: 120;
display: flex;
flex-direction: column;
gap: 0.4rem;
width: min(20rem, calc(100vw - 2rem));
padding: 0.6rem 0.75rem 0.7rem;
border: 1px solid color-mix(in srgb, var(--border-light) 70%, transparent);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--card-bg) 94%, transparent);
box-shadow: 0 10px 24px color-mix(in srgb, var(--bg) 35%, transparent);
pointer-events: none;
transition: opacity var(--dur-base) var(--ease-standard),
visibility var(--dur-base) var(--ease-standard);
}
.history-progress.is-hidden {
opacity: 0;
visibility: hidden;
}
.history-progress-text {
display: flex;
flex-direction: column;
gap: 0.1rem;
min-width: 0;
}
.history-progress-title {
font-size: var(--fs-xs);
font-weight: 700;
color: var(--text-heading);
}
.history-progress-sub {
font-size: var(--fs-xs);
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
.history-progress-track {
height: 0.28rem;
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--border-light) 45%, transparent);
overflow: hidden;
}
.history-progress-bar {
display: block;
width: 0%;
height: 100%;
border-radius: inherit;
background: var(--accent-green);
transition: width var(--dur-base) var(--ease-out);
}
/* Indeterminate while the payload is still on the wire. */
.history-progress[data-phase="fetching"] .history-progress-bar {
width: 35%;
animation: trx-history-sweep 1.1s var(--ease-standard) infinite;
}
@keyframes trx-history-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(285%); }
}
@media (max-width: 760px) {
.history-progress { bottom: calc(5.9rem + env(safe-area-inset-bottom)); }
}
.decode-history-overlay {
position: fixed;
inset: 0;
z-index: 9500;
display: flex;
align-items: center;
justify-content: center;
padding: 1.2rem;
background: rgba(7, 13, 26, 0.82);
pointer-events: none;
opacity: 1;
visibility: visible;
transition: opacity 140ms ease, visibility 140ms ease;
}
.decode-history-overlay.is-hidden {
opacity: 0;
visibility: hidden;
}
.decode-history-overlay.content-overlay {
position: absolute;
border-radius: 0 0 0.9rem 0.9rem;
}
.decode-history-overlay.content-overlay.conn-lost-fullscreen {
position: fixed;
border-radius: 0;
}
#server-lost-banner {
display: none;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.45rem 0.75rem;
background: color-mix(in srgb, var(--accent-red) 18%, var(--card-bg));
border: 1px solid color-mix(in srgb, var(--accent-red) 40%, var(--border));
border-radius: 0.4rem;
color: var(--accent-red);
font-size: 0.82rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
#server-lost-banner .banner-dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--accent-red);
animation: banner-pulse 1.5s ease-in-out infinite;
}
@keyframes banner-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
#tab-main.server-disconnected #content {
pointer-events: none;
filter: saturate(0.3) brightness(0.7);
opacity: 0.55;
transition: filter 0.4s ease, opacity 0.4s ease;
}
#tab-main.server-disconnected #server-lost-banner { display: flex; }
#tab-main:not(.server-disconnected) #content {
transition: filter 0.3s ease, opacity 0.3s ease;
}
.decode-history-overlay-card {
min-width: min(26rem, calc(100vw - 2.4rem));
max-width: min(30rem, calc(100vw - 2.4rem));
padding: 0.9rem 1rem;
border-radius: 0.9rem;
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
background: color-mix(in srgb, var(--card-bg) 88%, transparent);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.22);
text-align: center;
}
.decode-history-overlay-title {
font-size: 1rem;
font-weight: 800;
color: var(--text-heading);
}
.decode-history-overlay-sub {
margin-top: 0.24rem;
font-size: 0.82rem;
color: var(--text-muted);
}
/* Full-bleed: break the map panel out of the centred .card column so the
stage spans the whole viewport width. The negative inline margins cancel
the card's centring offset plus its side padding, whatever they are. */
#tab-map {
display: flex;
flex-direction: column;
min-height: 0;
gap: 0.85rem;
width: 100vw;
max-width: 100vw;
margin-inline: calc(50% - 50vw);
}
/* map-loading uses the shared .decode-history-overlay .content-overlay classes */
#map-stage {
position: relative;
flex: 0 1 auto;
min-height: 0;
overflow: hidden;
border-radius: 0;
border: 1px solid color-mix(in srgb, var(--border-light) 78%, transparent);
border-inline: 0;
background:
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 86%, transparent), color-mix(in srgb, var(--card-bg) 64%, transparent)),
color-mix(in srgb, var(--input-bg) 88%, transparent);
}
#aprs-map {
width: 100%;
min-height: 0;
border-radius: 0;
}
.map-qso-summary {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.9rem 1rem 1rem;
border-radius: 0.8rem;
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent);
background:
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 90%, transparent), color-mix(in srgb, var(--input-bg) 82%, transparent));
box-shadow: inset 0 1px 0 color-mix(in srgb, white 5%, transparent);
}
.map-qso-summary-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
}
.map-qso-summary-title {
font-size: 0.85rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-heading);
}
.map-qso-summary-subtitle {
margin-top: 0.18rem;
font-size: 0.78rem;
color: var(--text-muted);
}
.map-qso-summary-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
gap: 0.65rem;
}
.map-qso-summary-empty {
padding: 0.85rem 0.9rem;
border-radius: 0.75rem;
border: 1px dashed color-mix(in srgb, var(--border-light) 76%, transparent);
background: color-mix(in srgb, var(--input-bg) 84%, transparent);
color: var(--text-muted);
font-size: 0.82rem;
}
button.map-qso-card {
display: flex;
appearance: none;
-webkit-appearance: none;
width: 100%;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
min-width: 0;
margin: 0;
padding: 0.8rem 0.85rem;
box-sizing: border-box;
min-height: 0;
height: auto;
border-radius: 0.75rem;
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--card-bg) 78%, transparent);
font: inherit;
color: inherit;
line-height: inherit;
text-align: left;
cursor: pointer;
box-shadow: none;
transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
}
button.map-qso-card:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--accent-green) 38%, var(--border-light));
background: color-mix(in srgb, var(--card-bg) 70%, transparent);
box-shadow: none;
}
button.map-qso-card.is-selected {
border-color: color-mix(in srgb, var(--accent-green) 62%, var(--border-light));
background: color-mix(in srgb, var(--accent-green) 10%, var(--card-bg));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent-green) 18%, transparent);
}
button.map-qso-card:active:not(:disabled) {
transform: translateY(1px);
background: color-mix(in srgb, var(--card-bg) 66%, transparent);
border-color: color-mix(in srgb, var(--accent-green) 46%, var(--border-light));
box-shadow: none;
}
button.map-qso-card:focus-visible {
outline: 2px solid color-mix(in srgb, var(--accent-green) 52%, transparent);
outline-offset: 2px;
}
.map-qso-card-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 0.75rem;
}
.map-qso-card-rank {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.6rem;
min-height: 1.6rem;
padding: 0 0.4rem;
border-radius: 999px;
background: color-mix(in srgb, var(--accent-green) 16%, transparent);
color: var(--accent-green);
font-size: 0.75rem;
font-weight: 800;
}
.map-qso-card-distance {
flex: 0 0 auto;
font-size: 0.95rem;
font-weight: 800;
color: var(--text-heading);
white-space: nowrap;
}
.map-qso-card-body {
display: flex;
flex-direction: column;
gap: 0.24rem;
min-width: 0;
}
.map-qso-card-pair {
font-size: 0.9rem;
font-weight: 700;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.map-qso-card-meta {
display: flex;
flex-wrap: wrap;
gap: 0.35rem 0.45rem;
align-items: center;
}
.map-qso-card-pill {
display: inline-flex;
align-items: center;
min-height: 1.35rem;
padding: 0.04rem 0.42rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--input-bg) 86%, transparent);
color: var(--text-muted);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.map-qso-card-grids {
color: var(--text-muted);
font-size: 0.78rem;
}
.map-qso-card-band {
color: var(--text);
border-color: color-mix(in srgb, var(--band-color, var(--accent-green)) 40%, transparent);
background: color-mix(in srgb, var(--band-color, var(--accent-green)) 14%, transparent);
}
.map-qso-card-rx {
text-transform: none;
font-weight: 600;
color: var(--text-muted);
border-color: color-mix(in srgb, var(--accent-blue, #5b9bd5) 35%, transparent);
background: color-mix(in srgb, var(--accent-blue, #5b9bd5) 10%, transparent);
}
/* A toolbar across the top of the map rather than a panel parked in a corner:
it leaves the map itself unobscured, and the bottom-left band legend keeps
its place. Fullscreen and the filter toggle ride at its right-hand end;
only the filters themselves collapse, so the button that hides them is
still there to bring them back. */
.map-overlay-panel {
position: absolute;
top: 0.7rem;
/* Clear of Leaflet's zoom buttons, which draw over the top-left corner. */
left: 3.4rem;
right: 0.7rem;
z-index: 410;
display: flex;
flex-flow: row nowrap;
/* Stretched, not centred: the rule dividing the filters from the buttons is
drawn on the button block's edge, and it has to run the height of the bar
rather than float beside it as a stub. */
align-items: stretch;
gap: 0.5rem;
width: auto;
max-height: calc(100% - 1.4rem);
padding: 0.45rem 0.6rem;
border-radius: 0.7rem;
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--card-bg) 82%, transparent);
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.24);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
will-change: backdrop-filter;
overflow: auto;
transition: opacity 140ms ease, transform 140ms ease, visibility 140ms ease;
}
.map-overlay-filters {
display: flex;
flex: 1 1 auto;
flex-flow: row wrap;
align-items: center;
/* Rows keep their own height when the bar is taller than they are, so the
groups stay a bar's two rows rather than drifting apart to fill it. */
align-content: center;
gap: 0.35rem 0.5rem;
min-width: 0;
}
.map-overlay-filters.is-hidden {
display: none;
}
/* With the filters collapsed the bar has no reason to span the map. */
.map-overlay-panel.filters-hidden {
left: auto;
}
.map-overlay-actions {
display: flex;
flex: 0 0 auto;
/* The block spans the bar so its divider can; the buttons still sit level
with the middle of it. */
align-self: stretch;
align-items: center;
gap: 0.4rem;
}
/* Set off from the filters, but only while there are filters to set off. */
.map-overlay-panel:not(.filters-hidden) .map-overlay-actions {
padding-left: 0.5rem;
border-left: 1px solid color-mix(in srgb, var(--border-light) 55%, transparent);
}
.map-overlay-panel .map-locator-filter-group {
flex: 0 1 auto;
/* The label stays beside its control; only the bar itself wraps. */
flex-wrap: nowrap;
align-items: center;
gap: 0.35rem;
min-width: 0;
padding-right: 0.5rem;
border-right: 1px solid color-mix(in srgb, var(--border-light) 55%, transparent);
}
.map-overlay-panel .map-locator-filter-group:last-child {
padding-right: 0;
border-right: 0;
}
/* The search field takes whatever room the fixed-width groups leave, up to a
width past which a text box spanning the map reads as a mistake. */
.map-overlay-panel .map-filter-grow {
flex: 1 1 9rem;
max-width: 26rem;
}
/* One gutter for every label, so whichever group starts a row starts it in the
same place: at their natural widths the labels staggered each row's first
control by however much the label above it was wider or narrower. */
.map-overlay-panel .map-locator-filter-label {
min-width: 3.5rem;
padding-top: 0;
font-size: 0.72rem;
letter-spacing: 0.02em;
white-space: nowrap;
}
/* The two rows of the bar are led by the two pairs of phase buttons, and
SOURCE|BAND is wider than TRX|CONTACT: left to their own widths the groups
after them missed each other by four pixels. One width for both pairs. */
.map-overlay-panel .map-locator-phase-row {
flex: 0 0 auto;
min-width: 9rem;
}
.map-overlay-panel .map-history-select {
flex: 0 0 auto;
width: auto;
max-width: 8rem;
}
.map-overlay-panel .map-search-input {
flex: 1 1 6rem;
min-width: 4.5rem;
}
/* The buttons carry this as a tooltip; a sentence of it would swallow the bar. */
.map-overlay-panel .map-paths-hint {
display: none;
}
.map-overlay-panel.is-hidden {
opacity: 0;
visibility: hidden;
transform: translateY(0.25rem);
pointer-events: none;
}
.map-fullscreen-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.9rem;
padding: 0.18rem 0.65rem;
border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--card-bg) 82%, transparent);
color: var(--text);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
}
.map-fullscreen-btn:hover {
border-color: color-mix(in srgb, var(--accent-green) 34%, var(--border-light));
color: var(--text-heading);
}
.map-overlay-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.9rem;
padding: 0.18rem 0.65rem;
border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--card-bg) 82%, transparent);
color: var(--text);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
}
.map-overlay-toggle-btn:hover {
border-color: color-mix(in srgb, var(--accent-green) 34%, var(--border-light));
color: var(--text-heading);
}
.map-band-legend {
position: absolute;
left: 0.7rem;
bottom: 0.7rem;
z-index: 410;
display: flex;
flex-direction: column;
gap: 0.45rem;
width: min(14rem, calc(100% - 5.8rem));
max-height: min(40%, 18rem);
padding: 0.65rem 0.7rem;
border-radius: 0.8rem;
border: 1px solid color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--card-bg) 78%, transparent);
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
will-change: backdrop-filter;
overflow: auto;
}
.map-band-legend.is-empty {
display: none;
}
.map-band-legend-title {
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
}
.map-band-legend-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(5.4rem, 1fr));
gap: 0.35rem 0.55rem;
}
.map-band-legend-item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
min-width: 0;
}
.map-band-legend-swatch {
flex: 0 0 auto;
width: 0.78rem;
height: 0.78rem;
border-radius: 999px;
background: var(--legend-color, var(--accent-green));
box-shadow: 0 0 0 1px color-mix(in srgb, var(--legend-color, var(--accent-green)) 44%, rgba(255, 255, 255, 0.18));
}
.map-band-legend-text {
min-width: 0;
font-size: 0.75rem;
font-weight: 700;
color: var(--text);
white-space: nowrap;
}
#map-stage:fullscreen,
#map-stage:-webkit-full-screen,
#map-stage.map-fake-fullscreen {
background: var(--bg);
width: 100%;
height: 100%;
padding: 0;
box-sizing: border-box;
}
/* CSS-based fake fullscreen for browsers that block the Fullscreen API
(notably mobile Safari, which only allows fullscreen for <video>). */
#map-stage.map-fake-fullscreen {
position: fixed;
inset: 0;
z-index: 9000;
height: 100dvh;
}
body.map-fake-fullscreen-active {
overflow: hidden;
touch-action: none;
}
#map-stage:fullscreen #aprs-map,
#map-stage:-webkit-full-screen #aprs-map,
#map-stage.map-fake-fullscreen #aprs-map {
border-radius: 0;
height: 100% !important;
}
.aprs-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
.aprs-controls > button.active,
.ft8-controls > button.active,
.cw-controls > button.active {
border-color: color-mix(in srgb, var(--accent-green) 58%, var(--border-light));
color: var(--accent-green);
background: color-mix(in srgb, var(--accent-green) 10%, var(--btn-bg));
}
.aprs-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.6rem;
margin-bottom: 0.75rem;
}
.aprs-summary-card {
display: flex;
flex-direction: column;
gap: 0.18rem;
padding: 0.45rem 0.55rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: color-mix(in srgb, var(--card-bg) 84%, transparent);
}
.aprs-summary-label {
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.aprs-summary-value {
color: var(--text);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.8rem;
line-height: 1.3;
}
.aprs-filter-row {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin-bottom: 0.6rem;
}
.aprs-chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.65rem;
padding: 0.08rem 0.5rem;
border-radius: 0.4rem;
border: 1px solid var(--filter-border);
background: var(--filter-bg);
color: var(--filter-fg);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.03em;
cursor: pointer;
}
.aprs-chip.active {
color: var(--card-bg);
background: var(--accent-green);
border-color: var(--accent-green);
}
.aprs-chip:hover:not(.active) {
border-color: var(--accent-green);
color: var(--text);
}
.ais-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.6rem;
margin-bottom: 0.75rem;
}
.ais-summary-card {
display: flex;
flex-direction: column;
gap: 0.18rem;
padding: 0.45rem 0.55rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: color-mix(in srgb, var(--card-bg) 84%, transparent);
}
.ais-summary-label {
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.ais-summary-value {
color: var(--text);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 0.8rem;
line-height: 1.3;
}
#subtab-ais {
display: flex;
flex-direction: column;
}
#subtab-vdes {
display: flex;
flex-direction: column;
}
#subtab-aprs {
display: flex;
flex-direction: column;
}
#aprs-packets,
#ais-messages,
#vdes-messages { max-height: 360px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
/* Fill the panel, the way the FT8, FT4, FT2 and WSPR lists do. These were
sized by formula against the viewport — 100vh minus a guess at everything
above them — which stopped matching the moment the panel changed shape, and
left a few hundred pixels of the page empty under a scrolling list. */
#aprs-packets,
#ais-messages,
#vdes-messages,
#hf-aprs-packets {
flex: 1 1 0;
min-height: 12rem;
max-height: none;
}
/* HF APRS had no container styling at all: no scroller, no frame, no height —
its packets simply ran down the page. */
#hf-aprs-packets {
overflow-y: auto;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
/* One line per decode, opening in place — shared by the APRS and AIS lists. A frame used to be a card five lines
tall — timestamp, meta, raw information field, three buttons, a Details
panel repeating the row — so five of them filled the panel. */
.aprs-packet { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; border-bottom: 1px solid var(--border); line-height: 1.35; }
.aprs-packet:last-child { border-bottom: none; }
.decode-line {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.28rem 0.55rem;
cursor: pointer;
list-style: none;
white-space: nowrap;
}
.decode-line::-webkit-details-marker { display: none; }
.decode-line:hover {
background: color-mix(in srgb, var(--btn-bg) 45%, transparent);
}
.aprs-packet[open] > .decode-line {
background: color-mix(in srgb, var(--accent-green) 8%, transparent);
}
/* Fixed columns for what identifies a decode, so the summaries line up down
the list and the eye can run along one of them instead of hunting. Names
longer than the column ellipsise rather than pushing the rest along. */
.decode-line .aprs-call,
.decode-line .ais-call {
flex: 0 0 auto;
width: 8.5rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.decode-line .aprs-badge-type {
flex: 0 0 auto;
min-width: 5.5rem;
}
.decode-line .ais-badge-type {
flex: 0 0 auto;
min-width: 9rem;
}
/* The summary takes the leftover width and ellipsises: a frame is one line
whatever its payload, so the column of times and callsigns stays readable. */
.decode-line-summary {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text);
}
.decode-line-distance {
flex: 0 0 auto;
color: var(--text-muted);
font-size: 0.75rem;
}
.decode-expanded {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 0.1rem 0.55rem 0.55rem 2.6rem;
}
.decode-expanded-meta {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.decode-expanded-raw,
.decode-expanded-bytes {
word-break: break-all;
font-size: 0.78rem;
color: var(--text);
}
.decode-expanded-bytes {
color: var(--text-muted);
font-size: 0.72rem;
}
.aprs-packet-new {
animation: aprs-row-flash 1.2s ease;
}
.aprs-packet-crc {
opacity: 0.6;
}
.aprs-row-actions {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
}
/* Counts sit with the filters rather than in three cards of their own: they
are one short line of text, and they were taking a fifth of the panel. */
.aprs-counts {
display: inline-flex;
align-items: center;
gap: 0.6rem;
margin-left: auto;
font-size: 0.72rem;
color: var(--text-muted);
white-space: nowrap;
}
.aprs-filter-sep {
width: 1px;
align-self: stretch;
min-height: 1.2rem;
margin-inline: 0.15rem;
background: color-mix(in srgb, var(--border-light) 65%, transparent);
}
.aprs-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.2rem;
padding: 0.02rem 0.38rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-light) 78%, transparent);
background: color-mix(in srgb, var(--card-bg) 72%, transparent);
color: var(--text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.aprs-badge-type {
color: var(--text);
}
.aprs-badge-type-position {
color: #77d6a5;
border-color: color-mix(in srgb, #77d6a5 42%, transparent);
background: color-mix(in srgb, #77d6a5 12%, transparent);
}
.aprs-badge-type-message {
color: #8ec8ff;
border-color: color-mix(in srgb, #8ec8ff 42%, transparent);
background: color-mix(in srgb, #8ec8ff 12%, transparent);
}
.aprs-badge-type-weather {
color: #ffd77a;
border-color: color-mix(in srgb, #ffd77a 42%, transparent);
background: color-mix(in srgb, #ffd77a 14%, transparent);
}
.aprs-badge-type-telemetry {
color: #d4a5ff;
border-color: color-mix(in srgb, #d4a5ff 42%, transparent);
background: color-mix(in srgb, #d4a5ff 12%, transparent);
}
.aprs-badge-crc {
color: #ff9a9a;
border-color: color-mix(in srgb, #ff9a9a 42%, transparent);
background: color-mix(in srgb, #ff9a9a 10%, transparent);
}
.aprs-meta-text {
color: var(--text-muted);
font-size: 0.75rem;
}
.aprs-inline-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.7rem;
padding: 0.08rem 0.42rem;
border-radius: 999px;
border: 1px solid var(--filter-border);
background: var(--filter-bg);
color: var(--filter-fg);
font: inherit;
font-size: 0.75rem;
cursor: pointer;
}
.aprs-inline-btn:hover {
border-color: var(--accent-green);
color: var(--text);
}
.aprs-details {
width: 100%;
}
.aprs-details summary {
cursor: pointer;
color: var(--accent-green);
font-size: 0.75rem;
user-select: none;
}
.aprs-details[open] summary {
margin-bottom: 0.35rem;
}
.aprs-details-grid {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.2rem 0.65rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.aprs-detail-label {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.aprs-detail-value {
color: var(--text);
word-break: break-word;
}
.ais-message { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; border-bottom: 1px solid var(--border); line-height: 1.35; }
.ais-message > .decode-line:hover { background: color-mix(in srgb, var(--btn-bg) 45%, transparent); }
.ais-message[open] > .decode-line { background: color-mix(in srgb, var(--accent-red) 8%, transparent); }
.ais-message:last-child { border-bottom: none; }
.vdes-message { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; padding: 0.45rem 0.55rem; border-bottom: 1px solid var(--border); line-height: 1.35; }
.vdes-message:last-child { border-bottom: none; }
.aprs-call { color: var(--accent-green); font-weight: 600; }
.ais-call { color: var(--accent-red); font-weight: 600; }
.vdes-call { color: #8ec8ff; font-weight: 600; }
.aprs-time { color: var(--text-muted); margin-right: 0.5rem; }
.ais-time { color: var(--text-muted); margin-right: 0.5rem; }
.vdes-time { color: var(--text-muted); margin-right: 0.5rem; }
.ais-row-head,
.ais-row-meta,
.ais-row-detail {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
}
.ais-row-head + .ais-row-meta,
.ais-row-meta + .ais-row-detail {
margin-top: 0.2rem;
}
.ais-row-detail {
color: var(--text-muted);
font-size: 0.78rem;
}
.ais-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.2rem;
padding: 0.02rem 0.38rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-light) 78%, transparent);
background: color-mix(in srgb, var(--card-bg) 72%, transparent);
color: var(--text-muted);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.ais-badge-channel-a {
color: #77d6a5;
border-color: color-mix(in srgb, #77d6a5 42%, transparent);
background: color-mix(in srgb, #77d6a5 12%, transparent);
}
.ais-badge-channel-b {
color: #ff9a7a;
border-color: color-mix(in srgb, #ff9a7a 42%, transparent);
background: color-mix(in srgb, #ff9a7a 14%, transparent);
}
.ais-badge-type {
color: var(--text);
}
.ais-meta-text {
color: var(--text-muted);
font-size: 0.75rem;
}
.ais-pos-link {
color: var(--accent-red);
text-decoration: none;
}
.ais-pos-link:hover {
text-decoration: underline;
}
.vdes-row-head,
.vdes-row-meta,
.vdes-row-detail {
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
}
.vdes-row-head + .vdes-row-meta,
.vdes-row-meta + .vdes-row-detail {
margin-top: 0.2rem;
}
.vdes-row-detail {
color: var(--text-muted);
font-size: 0.78rem;
}
.vdes-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.2rem;
padding: 0.02rem 0.38rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, #8ec8ff 42%, transparent);
background: color-mix(in srgb, #8ec8ff 12%, transparent);
color: #8ec8ff;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.vdes-raw {
color: var(--text-muted);
font-size: 0.75rem;
word-break: break-all;
}
/* APRS symbols are drawn from the vendored hessu/aprs-symbols sprite sheets:
16x6 grids of 24px cells, indexed by `symbol code - 0x21`. The cell offset
is set inline by the APRS/map bundles; the sheet URL stays here so retina
displays can swap in the @2x variants. */
.aprs-symbol { display: inline-block; width: 24px; height: 24px; background-repeat: no-repeat; background-size: 384px 144px; vertical-align: middle; margin-right: 0.3rem; }
.aprs-symbol-marker { margin-right: 0; }
/* Holds the column open for a frame that carries no symbol. */
.aprs-symbol-empty { background-image: none; }
.aprs-symbol-primary { background-image: url('/vendor/aprs-symbols-24-0.png'); }
.aprs-symbol-alternate { background-image: url('/vendor/aprs-symbols-24-1.png'); }
.aprs-symbol-overlaid { background-image: url('/vendor/aprs-symbols-24-2.png'), url('/vendor/aprs-symbols-24-1.png'); }
@media (min-resolution: 2dppx) {
.aprs-symbol-primary { background-image: url('/vendor/aprs-symbols-24-0-2x.png'); }
.aprs-symbol-alternate { background-image: url('/vendor/aprs-symbols-24-1-2x.png'); }
.aprs-symbol-overlaid { background-image: url('/vendor/aprs-symbols-24-2-2x.png'), url('/vendor/aprs-symbols-24-1-2x.png'); }
}
/* Fallback for symbol codes outside the sprite sheets: show the raw character. */
.aprs-symbol-local { border: 1px solid var(--border); border-radius: 4px; background: var(--surface-raised); color: var(--text); font: 700 0.8rem/22px ui-monospace, monospace; text-align: center; }
.aprs-pos { color: var(--accent-green); text-decoration: none; margin-left: 0.3rem; font-size: 0.8rem; }
.aprs-pos:hover { text-decoration: underline; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: color-mix(in srgb, var(--card-bg) 84%, transparent) !important; color: var(--text) !important; box-shadow: 0 3px 14px rgba(0,0,0,0.45) !important; }
.leaflet-popup-close-button { color: var(--text-muted) !important; }
.leaflet-popup-close-button:hover { color: var(--text) !important; }
.aprs-popup { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.82rem; min-width: 12rem; max-width: 22rem; }
.aprs-popup-call { font-weight: 700; font-size: 1em; color: var(--accent-green); margin-bottom: 0.18rem; }
.aprs-popup-meta { font-size: 0.85em; color: var(--text-muted); margin-bottom: 0.3rem; }
.aprs-popup-table { border-collapse: collapse; width: 100%; margin-bottom: 0.3rem; }
.aprs-popup-table td { padding: 0.06rem 0.3rem 0.06rem 0; vertical-align: top; font-size: 0.88em; }
.aprs-popup-label { color: var(--text-muted); white-space: nowrap; padding-right: 0.5rem !important; }
.aprs-popup-info { font-size: 0.85em; color: var(--text); border-top: 1px solid var(--border-light); padding-top: 0.25rem; margin-top: 0.1rem; word-break: break-word; }
.decode-locator-tip {
min-width: 16rem;
max-width: 26rem;
max-height: min(22rem, 60vh);
overflow: auto;
padding: 0.1rem 0.05rem 0.05rem;
color: var(--text);
}
.decode-locator-tip-title {
color: var(--accent-yellow);
font-weight: 700;
font-size: 0.9rem;
letter-spacing: 0.03em;
}
.decode-locator-tip-subtitle {
margin-top: 0.1rem;
margin-bottom: 0.45rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.decode-locator-tip-row + .decode-locator-tip-row {
margin-top: 0.45rem;
padding-top: 0.4rem;
border-top: 1px solid color-mix(in srgb, var(--border-light) 70%, transparent);
}
.decode-locator-tip-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
}
.decode-locator-tip-name {
font-weight: 600;
color: var(--text-heading);
}
.decode-locator-tip-time {
flex: 0 0 auto;
font-size: 0.75rem;
color: var(--text-muted);
white-space: nowrap;
}
.decode-locator-tip-meta {
margin-top: 0.12rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.decode-locator-tip-rx {
margin-top: 0.12rem;
font-size: 0.75rem;
color: var(--accent-yellow);
opacity: 0.85;
}
.decode-locator-tip-note {
margin-top: 0.2rem;
font-size: 0.75rem;
line-height: 1.3;
color: var(--text);
word-break: break-word;
}
.aprs-radio-path { stroke-dasharray: 10 5 !important; animation: aprs-radio-path-flow 0.7s linear infinite; }
.locator-radio-path { stroke-dasharray: 12 6 !important; animation: aprs-radio-path-flow 0.7s linear infinite; }
.decode-contact-path {
stroke-dasharray: 9 6 !important;
filter: drop-shadow(0 0 3px color-mix(in srgb, currentColor 34%, transparent));
animation: aprs-radio-path-flow 0.85s linear infinite;
}
.decode-contact-distance-label {
background: transparent;
border: none;
}
.decode-contact-distance-pill {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 4.25rem;
padding: 0.16rem 0.5rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--accent-green) 40%, var(--border-light));
background: color-mix(in srgb, var(--card-bg) 88%, transparent);
color: color-mix(in srgb, var(--accent-green) 72%, var(--accent-yellow));
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.22);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.03em;
white-space: nowrap;
pointer-events: none;
}
.trx-locator-selected { stroke-opacity: 1 !important; stroke-width: 3.25px !important; filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent-green) 52%, transparent)); animation: trx-locator-breathe 1.6s ease-in-out infinite; }
.trx-receiver-marker { stroke: var(--accent-green) !important; fill: var(--accent-green) !important; }
.trx-receiver-marker-secondary { opacity: 0.55; }
.receiver-popup-active { font-size: 0.75em; background: rgba(194,75,26,0.15); color: var(--accent-green); border: 1px solid rgba(194,75,26,0.3); border-radius: 3px; padding: 0 0.25rem; margin-left: 0.3rem; vertical-align: middle; }
@keyframes aprs-radio-path-flow { to { stroke-dashoffset: -15; } }
.map-paths-static .decode-contact-path,
.map-paths-static .aprs-radio-path,
.map-paths-static .locator-radio-path {
animation: none;
filter: none;
}
@keyframes trx-locator-breathe {
0%, 100% { stroke-width: 2.4px; stroke-opacity: 0.78; filter: drop-shadow(0 0 2px color-mix(in srgb, var(--accent-green) 18%, transparent)); }
50% { stroke-width: 4.2px; stroke-opacity: 1; filter: drop-shadow(0 0 10px color-mix(in srgb, var(--accent-green) 52%, transparent)); }
}
.aprs-bar-pos { background: none; border: none; padding: 0; margin-left: 0.4em; font-family: inherit; font-size: inherit; color: var(--accent-green); cursor: pointer; }
.aprs-bar-pos:hover { text-decoration: underline; }
.aprs-byte { color: var(--accent-yellow); background: rgba(255, 214, 0, 0.12); border: 1px solid rgba(255, 214, 0, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-size: 0.78em; }
@keyframes aprs-row-flash {
0% { background: color-mix(in srgb, var(--accent-green) 14%, transparent); }
100% { background: transparent; }
}
.ft8-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
.ft8-filter {
flex: 1;
min-width: 10rem;
height: var(--control-height);
box-sizing: border-box;
background: var(--filter-bg);
color: var(--filter-fg);
border: 1px solid var(--filter-border);
border-radius: 6px;
padding: 0.45rem 0.55rem;
}
.ft8-filter::placeholder { color: color-mix(in srgb, var(--filter-fg) 55%, transparent); }
.ft8-header { display: flex; gap: 0.6rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); border-bottom: 1px solid var(--border); padding: 0 0 0.35rem 0; margin-bottom: 0.35rem; }
#ft8-messages,
#ft4-messages,
#ft2-messages,
#wspr-messages { flex: 1 1 0; min-height: 120px; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.85rem; padding: 0.35rem 0.5rem; }
.ft8-row { display: flex; gap: 0.6rem; line-height: 1.4; border-bottom: 1px solid var(--border); padding: 0.25rem 0; }
.ft8-row:last-child { border-bottom: none; }
.ft8-time { color: var(--text-muted); min-width: 4.6rem; }
.ft8-snr { color: var(--accent-yellow); min-width: 3.6rem; text-align: right; }
.ft8-dt { color: var(--text-muted); min-width: 3.6rem; text-align: right; }
.ft8-freq { color: var(--accent-green); min-width: 4.6rem; text-align: right; }
.ft8-msg { flex: 1; }
.ft8-locator { color: var(--accent-green); background: rgba(0, 209, 127, 0.12); border: 1px solid rgba(0, 209, 127, 0.25); border-radius: 4px; padding: 0 0.2rem; margin: 0 0.1rem; font-weight: 600; }
.ft8-locator[data-locator-grid] { cursor: pointer; user-select: none; }
.ft8-locator[data-locator-grid]:hover { filter: brightness(1.12); }
.ft8-locator[data-locator-grid]:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent-green) 66%, transparent); outline-offset: 1px; }
.map-locator-filters {
display: flex;
flex-direction: column;
gap: 0.3rem;
margin-bottom: 0.55rem;
}
.map-locator-filter-group {
display: flex;
align-items: flex-start;
gap: 0.5rem;
flex-wrap: wrap;
}
.map-locator-filter-label {
flex: 0 0 auto;
min-width: 4.4rem;
padding-top: 0.35rem;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--text-muted);
}
.map-locator-chip-row {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 0.45rem;
}
.map-locator-phase-row {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
gap: 0.45rem;
}
.map-locator-phase-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 1.8rem;
padding: 0.1rem 0.65rem;
border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--border-light) 82%, transparent);
background: color-mix(in srgb, var(--input-bg) 92%, transparent);
color: var(--text-muted);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.02em;
text-transform: uppercase;
cursor: pointer;
transition: border-color 120ms ease, background-color 120ms ease, color 120ms ease;
}
.map-locator-phase-btn:hover {
border-color: color-mix(in srgb, var(--accent-green) 24%, var(--border-light));
color: var(--text);
}
.map-locator-phase-btn.is-active {
border-color: var(--accent-green);
background: color-mix(in srgb, var(--accent-green) 10%, var(--input-bg));
color: var(--accent-green);
}
.map-locator-empty {
font-size: 0.75rem;
color: var(--text-muted);
opacity: 0.8;
align-self: flex-start;
padding-top: 0.2rem;
}
.map-locator-chip {
--chip-color: var(--filter-border);
display: inline-flex;
align-items: center;
gap: 0.35rem;
min-height: 1.8rem;
padding: 0.1rem 0.55rem;
border-radius: 6px;
border: 1px solid color-mix(in srgb, var(--chip-color) 52%, var(--border-light));
background: color-mix(in srgb, var(--chip-color) 8%, var(--input-bg));
color: var(--text);
cursor: pointer;
transition: border-color 120ms ease, background-color 120ms ease, opacity 120ms ease, color 120ms ease;
}
.map-locator-chip:hover {
color: var(--text-heading);
}
.map-locator-chip.is-default {
border-color: color-mix(in srgb, var(--border-light) 74%, transparent);
background: color-mix(in srgb, var(--input-bg) 94%, transparent);
color: var(--text-muted);
}
.map-locator-chip.is-inactive {
opacity: 0.62;
border-color: color-mix(in srgb, var(--border-light) 68%, transparent);
background: color-mix(in srgb, var(--input-bg) 96%, transparent);
}
/* "All" clears the selection rather than naming a band or a source, so it
borrows the phase buttons' look instead of a colour of its own — and says
in one chip's width what a line of helper text used to say in the bar. */
.map-locator-chip-all {
--chip-color: var(--border-light);
color: var(--text-muted);
}
.map-locator-chip-all.is-active {
border-color: var(--accent-green);
background: color-mix(in srgb, var(--accent-green) 10%, var(--input-bg));
color: var(--accent-green);
}
.map-locator-chip-all .map-locator-chip-text {
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.map-locator-chip-text {
font-size: 0.78rem;
font-weight: 600;
}
.map-search-input {
flex: 1 1 15rem;
width: 100%;
max-width: none;
min-height: 1.95rem;
padding: 0.3rem 0.55rem;
border-radius: 7px;
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent);
background: color-mix(in srgb, var(--card-bg) 72%, transparent);
color: var(--text);
font-size: 0.82rem;
}
.map-search-input::placeholder {
color: color-mix(in srgb, var(--text-muted) 92%, transparent);
}
.map-history-select {
flex: 1 1 10rem;
width: 100%;
max-width: 12rem;
min-height: 1.95rem;
padding: 0.3rem 0.55rem;
border-radius: 7px;
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent);
background: color-mix(in srgb, var(--card-bg) 72%, transparent);
color: var(--text);
font-size: 0.82rem;
}
.rds-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1rem; align-items: baseline; margin-bottom: 1rem; }
.rds-field { display: contents; }
.rds-label { color: var(--text-muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; }
.rds-value { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.95rem; color: var(--text); }
.rds-ps { font-family: 'DSEG14 Classic', monospace; font-size: 1.1rem; font-weight: 600; letter-spacing: 0.08em; color: var(--accent-green); }
.rds-text { white-space: normal; overflow-wrap: anywhere; line-height: 1.35; }
.rds-af-list { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.rds-af-btn {
height: 1.7rem;
padding: 0 0.55rem;
border: 1px solid var(--border-light);
border-radius: 999px;
background: var(--input-bg);
color: var(--text);
font: inherit;
cursor: pointer;
}
.rds-af-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
.rds-no-signal { color: var(--text-muted); }
.rds-decoding { color: var(--accent-green); }
.rds-raw-header { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.3rem; }
.rds-raw-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 0.3rem; }
.rds-raw-header .rds-raw-label { margin-bottom: 0; }
#rds-raw-copy-btn { height: 1.65rem; padding: 0 0.55rem; font-size: 0.75rem; }
.rds-raw { background: var(--input-bg); border: 1px solid var(--border-light); border-radius: 6px; padding: 0.5rem 0.6rem; font-size: 0.8rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; color: var(--text); white-space: pre; overflow-x: auto; margin: 0; }
.cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
.cw-config { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; }
.cw-config label { display: flex; flex-direction: column; gap: 0.2rem; color: var(--text-muted); font-size: 0.82rem; }
.cw-config input[type="number"] { width: 5rem; padding: 0.3rem 0.4rem; font-size: 0.9rem; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); color: var(--text); }
.cw-tone-picker { margin-bottom: 0.75rem; border: 1px solid var(--border-light); border-radius: 8px; background: var(--input-bg); padding: 0.5rem 0.6rem; }
.cw-tone-picker.is-auto { opacity: 0.82; }
.cw-tone-picker-head { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; margin-bottom: 0.35rem; color: var(--text-muted); font-size: 0.78rem; }
#cw-tone-waterfall { width: 100%; height: 56px; display: block; border-radius: 6px; background: linear-gradient(180deg, rgba(8, 14, 18, 0.92), rgba(18, 28, 36, 0.98)); cursor: crosshair; }
.cw-tone-picker.is-auto #cw-tone-waterfall { cursor: not-allowed; }
#cw-output { flex: 1 1 0; max-height: none; overflow-y: auto; border: 1px solid var(--border-light); border-radius: 6px; background: var(--input-bg); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.85rem; padding: 0.4rem 0.5rem; min-height: 60px; white-space: pre-wrap; word-break: break-all; }
.cw-line { line-height: 1.5; }
.cw-signal-on { width: 10px; height: 10px; border-radius: 50%; background: var(--accent-green); box-shadow: 0 0 6px var(--accent-green); flex-shrink: 0; }
.cw-signal-off { width: 10px; height: 10px; border-radius: 50%; background: var(--border-light); flex-shrink: 0; }
.cw-config .cw-auto-label { display: inline-flex; align-items: center; gap: 0.35rem; font-size: 0.82rem; color: var(--text-muted); cursor: pointer; flex-direction: row; }
.cw-auto-label input[type="checkbox"] { margin: 0; cursor: pointer; }
.cw-config input[type="number"][readonly] { opacity: 0.6; }
.cw-config input[type="number"]:disabled { opacity: 0.58; }
button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible, a:focus-visible,
canvas:focus-visible, [tabindex]:focus-visible {
outline: 2px solid var(--accent-green);
outline-offset: 2px;
}
/* ── Shared interaction primitives ───────────────────────────────────── */
.visually-hidden {
position: absolute !important;
width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}
.toast-region {
position: fixed;
z-index: 120;
right: var(--space-4);
bottom: var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-2);
width: min(28rem, calc(100vw - 2rem));
pointer-events: none;
}
.toast {
--toast-accent: var(--accent-yellow);
position: relative;
display: grid;
grid-template-columns: 1.25rem minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-3);
min-height: 3rem;
padding: 0.7rem 0.75rem;
border: 1px solid color-mix(in srgb, var(--toast-accent) 35%, var(--border-light));
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--card-bg) 96%, var(--toast-accent) 4%);
color: var(--text);
box-shadow: 0 14px 38px color-mix(in srgb, #000 38%, transparent), inset 3px 0 0 var(--toast-accent);
opacity: 0;
transform: translateY(0.65rem) scale(0.98);
transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-base) var(--ease-out);
pointer-events: auto;
}
.toast::before {
display: grid;
place-items: center;
width: 1.25rem;
height: 1.25rem;
border-radius: var(--radius-pill);
background: color-mix(in srgb, var(--toast-accent) 18%, transparent);
color: var(--toast-accent);
content: "i";
font-size: var(--fs-xs);
font-weight: 800;
}
.toast > span { min-width: 0; line-height: 1.35; }
.toast-visible { opacity: 1; transform: translateY(0); }
.toast-success { --toast-accent: var(--accent-green); }
.toast-success::before { content: "✓"; }
.toast-error { --toast-accent: var(--accent-red); }
.toast-error::before { content: "!"; }
.toast button {
width: auto;
min-height: 2rem;
height: 2rem;
padding: 0 var(--space-3);
border-color: color-mix(in srgb, var(--toast-accent) 55%, var(--btn-border));
color: var(--toast-accent);
font-size: var(--fs-sm);
font-weight: 700;
}
.ui-dialog {
max-width: none;
border: 0;
padding: 0;
border-radius: var(--radius-lg);
color: var(--text);
background: transparent;
box-shadow: 0 24px 70px color-mix(in srgb, #000 55%, transparent);
}
.ui-dialog::backdrop { background: rgba(3, 8, 18, 0.72); backdrop-filter: blur(5px); }
.ui-dialog-card {
width: min(29rem, calc(100vw - 2rem));
margin: 0;
padding: var(--space-6);
border: 1px solid var(--border-light);
border-radius: var(--radius-lg);
background: linear-gradient(145deg, color-mix(in srgb, var(--card-bg) 94%, var(--text) 6%), var(--card-bg));
box-sizing: border-box;
}
.ui-dialog-card h2 { margin: 0 0 var(--space-2); color: var(--text-heading); font-size: var(--fs-md); line-height: 1.3; }
.ui-dialog-card p { margin: 0; color: var(--text-muted); line-height: 1.55; }
.ui-dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-6); }
.ui-dialog-actions button { min-width: 6rem; }
.ui-dialog-actions .danger { background: var(--accent-red); border-color: var(--accent-red); color: #fff; font-weight: 700; }
.ui-dialog-actions .danger:hover:not(:disabled) { background: color-mix(in srgb, var(--accent-red) 82%, #fff); border-color: color-mix(in srgb, var(--accent-red) 82%, #fff); }
.form-error { flex: 1 1 100%; min-height: 1.2em; color: var(--accent-red); font-size: var(--fs-sm); font-weight: 600; }
button.is-active {
border-color: var(--accent-green);
background: color-mix(in srgb, var(--btn-bg) 82%, var(--accent-green) 18%);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-green) 32%, transparent), 0 0 0 1px color-mix(in srgb, var(--accent-green) 12%, transparent);
}
#ptt-btn.is-active { background: var(--accent-red) !important; border-color: var(--accent-red) !important; color: white !important; }
.is-busy { cursor: progress !important; opacity: 0.68; }
.operator-layout-picker {
display: inline-flex;
align-items: center;
min-height: 2rem;
padding-left: 0.6rem;
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--input-bg) 88%, transparent);
color: var(--text-muted);
font-size: var(--fs-xs);
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
overflow: visible;
}
/* The prefix made the least-used control the widest in the bar; the
* select carries its own aria-label and title instead. */
.operator-layout-picker { padding-left: 0; }
.operator-layout-picker select { max-width: 8rem; }
.operator-layout-picker select {
width: auto;
min-height: 2rem;
max-width: 9rem;
padding: 0 1.75rem 0 0.45rem;
border: 0;
background-color: transparent;
color: var(--text);
font-size: var(--fs-sm);
font-weight: 600;
text-transform: none;
cursor: pointer;
}
.operator-layout-picker:focus-within { border-color: var(--accent-green); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-green) 20%, transparent); }
.operator-layout-picker select:focus-visible { outline: 0; }
.advanced-radio-controls {
margin-top: var(--space-3);
border: 1px solid color-mix(in srgb, var(--border-light) 75%, transparent);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--input-bg) 68%, transparent);
overflow: clip;
transition: border-color var(--dur-fast) var(--ease-standard), background-color var(--dur-fast) var(--ease-standard);
}
.advanced-radio-controls:hover { border-color: var(--border-light); }
.advanced-radio-controls[open] { background: color-mix(in srgb, var(--surface) 86%, transparent); }
.advanced-radio-controls summary {
display: flex;
align-items: center;
gap: var(--space-2);
padding: 0.65rem 0.75rem;
color: var(--text-heading);
font-size: var(--fs-sm);
font-weight: 700;
cursor: pointer;
list-style: none;
user-select: none;
}
.advanced-radio-controls summary::-webkit-details-marker { display: none; }
.advanced-radio-controls summary::before {
content: "";
color: var(--accent-text);
font-size: 1.2rem;
line-height: 1;
transform: rotate(0deg);
transition: transform var(--dur-base) var(--ease-standard);
}
.advanced-radio-controls[open] summary::before { transform: rotate(90deg); }
.advanced-radio-controls[open] summary { border-bottom: 1px solid color-mix(in srgb, var(--border) 70%, transparent); }
.advanced-radio-body { display: grid; gap: var(--space-3); padding: var(--space-3); }
.advanced-radio-body > * { margin: 0; }
body[data-operator-layout="digital"] .controls-col-wfm { display: none !important; }
body[data-operator-layout="broadcast"] #tx-power-col,
body[data-operator-layout="broadcast"] #tx-meters,
body[data-operator-layout="broadcast"] #tx-limit-row,
body[data-operator-layout="broadcast"] #vfo-row,
body[data-operator-layout="broadcast"] #sam-controls-col,
body[data-operator-layout="broadcast"] #advanced-radio-controls {
display: none !important;
}
body[data-operator-layout="broadcast"] #wfm-controls-col,
body[data-operator-layout="broadcast"] #audio-row,
body[data-operator-layout="broadcast"] #spectrum-bw-row {
border-color: color-mix(in srgb, var(--accent-green) 42%, var(--border-light));
background: color-mix(in srgb, var(--accent-green) 7%, transparent);
}
body[data-operator-layout="broadcast"] #wfm-controls-col {
padding: var(--space-2) var(--space-3);
border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light));
border-radius: var(--radius-md);
}
body[data-operator-layout="broadcast"] #audio-row {
padding: var(--space-3);
border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light));
border-radius: var(--radius-md);
}
body[data-operator-layout="broadcast"] #spectrum-bw-row {
padding: var(--space-2);
border: 1px solid color-mix(in srgb, var(--accent-green) 42%, var(--border-light));
border-radius: var(--radius-sm);
}
body[data-operator-layout="broadcast"] #ais-bar-overlay,
body[data-operator-layout="broadcast"] #vdes-bar-overlay,
body[data-operator-layout="broadcast"] #ft8-bar-overlay,
body[data-operator-layout="broadcast"] #aprs-bar-overlay,
body[data-operator-layout="broadcast"] #hf-aprs-bar-overlay,
body[data-operator-layout="broadcast"] #cw-bar-overlay {
display: none !important;
}
@media (max-width: 760px) {
body[data-operator-layout="broadcast"] #wfm-controls-col { flex-basis: 100%; }
}
/* One navigation model at every width. Statistics, Recorder, Settings and
* About are occasional destinations: they live behind More rather than
* competing with the operating tabs for the row and then scrolling out of
* reach. The mobile layout already grouped them this way; the desktop strip
* now matches it, which is why the tab strip no longer needs to scroll. */
.tab-bar-nav .tab[data-tab="statistics"],
.tab-bar-nav .tab[data-tab="recorder"],
.tab-bar-nav .tab[data-tab="settings"],
.tab-bar-nav .tab[data-tab="about"] { display: none; }
.mobile-more-btn { display: flex; flex: 0 0 auto; }
.mobile-more-menu, .decoder-tab-select { display: none; }
.tab-bar-nav { position: relative; }
.mobile-more-menu {
position: absolute;
top: calc(100% + 0.4rem);
right: 0;
right: max(0.75rem, env(safe-area-inset-right));
bottom: calc(5.4rem + env(safe-area-inset-bottom));
z-index: 80;
min-width: 12rem;
padding: var(--space-2);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--card-bg) 97%, transparent);
box-shadow: 0 18px 45px color-mix(in srgb, #000 42%, transparent);
}
.mobile-more-menu.is-open { display: grid; gap: 0.2rem; animation: trx-menu-in var(--dur-base) var(--ease-out); }
.mobile-more-menu button {
justify-content: flex-start;
width: 100%;
height: 2.65rem;
padding: 0 var(--space-3);
border-color: transparent;
background: transparent;
text-align: left;
}
.mobile-more-menu button:hover { background: var(--btn-hover-bg); border-color: transparent; }
.decoder-state-dot {
display: inline-block;
width: 0.45rem;
height: 0.45rem;
margin-left: 0.4rem;
border: 1px solid color-mix(in srgb, var(--text-muted) 70%, transparent);
border-radius: 50%;
background: color-mix(in srgb, var(--text-muted) 60%, transparent);
vertical-align: middle;
}
.decoder-state-dot[data-state="active"] { background: var(--accent-green); box-shadow: 0 0 0.35rem color-mix(in srgb, var(--accent-green) 65%, transparent); }
.decoder-state-dot[data-state="error"] { background: var(--accent-red); }
@keyframes trx-menu-in { from { opacity: 0; transform: translateY(0.4rem) scale(0.98); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
}
/* ── Scrollbars ───────────────────────────────────────────────────────── */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--border-light) 70%, transparent); border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--accent-green) 55%, var(--border-light)); }
* { scrollbar-width: thin; scrollbar-color: color-mix(in srgb, var(--border-light) 70%, transparent) transparent; }
@media (max-width: 1099px) {
/* Side bookmark panels need the gutter; hide when card is full-width */
.spectrum-bookmark-side { display: none !important; }
}
@media (max-width: 900px) {
.card { width: 100%; padding-left: 0.9rem; padding-right: 0.9rem; }
}
@media (max-width: 760px) {
.card {
padding:
calc(0.7rem + env(safe-area-inset-top))
calc(0.7rem + env(safe-area-inset-right))
calc(1rem + env(safe-area-inset-bottom))
calc(0.7rem + env(safe-area-inset-left));
}
button { min-height: 2.8rem; font-size: 0.95rem; }
input.status-input, select.status-input { font-size: 1.1rem; }
:root { --header-waterfall-overlap: 0rem; }
.controls-tray-scroll { overflow-x: auto; }
.controls-tray { width: 100%; min-width: 0; padding-left: 0.85rem; padding-right: 0.85rem; }
.freq-inline { gap: 0.5rem; flex-wrap: wrap; }
.header-text { width: auto; min-width: 0; flex: 0 1 auto; }
.header-main {
max-width: 100%;
padding-right: 0.65rem;
margin-left: -0.15rem;
transform: translateY(-1px);
}
.header-left { justify-content: flex-end; }
.tab-bar { flex-wrap: wrap; }
.tab-bar-left {
width: 100%;
flex-wrap: wrap;
gap: 0.7rem;
}
.tab-bar .header-main {
max-width: 100%;
margin-left: 0;
transform: none;
}
.top-bar-actions { width: 100%; justify-content: space-between; }
.header-rig-switch { width: auto; justify-content: flex-end; }
.header-rig-switch select { min-width: 6.5rem; }
.overview-toolbar { top: calc(var(--header-waterfall-overlap) + 0.15rem); }
#signal-split-control {
top: 0.35rem;
right: 0.35rem;
transform: none;
flex-direction: row;
gap: 0.32rem;
border-radius: 6px;
padding: 0.24rem 0.32rem;
}
#signal-split-slider {
writing-mode: horizontal-tb;
direction: ltr;
width: 3.6rem;
height: 1rem;
cursor: ew-resize;
}
.controls-col-wfm { flex-basis: 100%; }
.controls-col-power { flex-basis: 100%; }
.controls-col.label-below-col .inline,
.controls-col.label-below-col .btn-grid { margin-top: 0; }
.wfm-controls-inline { flex-wrap: wrap; }
.ft8-controls { flex-wrap: wrap; }
#ft8-decode-toggle-btn, #ft4-decode-toggle-btn, #ft2-decode-toggle-btn, #wspr-decode-toggle-btn { white-space: nowrap; }
.jog-container { flex-wrap: wrap; }
.vfo-picker { flex-direction: column; }
.vfo-picker button { border-right: none; border-bottom: 1px solid var(--border-light); }
.vfo-picker button:last-child { border-bottom: none; }
}
@media (max-width: 640px) {
.card {
padding-bottom: calc(5.6rem + env(safe-area-inset-bottom));
}
.tab-bar {
gap: 0.6rem;
align-items: stretch;
margin-bottom: 1rem;
padding-bottom: 0.2rem;
border-bottom: none;
}
.tab-bar-left {
gap: 0.55rem;
align-items: stretch;
}
.tab-bar .header-main {
width: 100%;
max-width: 100%;
}
.tab-bar .header-logo {
height: 1.9rem;
}
/* Collapse verbose subtitles — the About tab has this info */
.tab-bar .subtitle { display: none; }
.tab-bar .title { font-size: 0.9rem; }
.tab-bar-nav {
position: fixed;
left: calc(0.6rem + env(safe-area-inset-left));
right: calc(0.6rem + env(safe-area-inset-right));
bottom: calc(0.55rem + env(safe-area-inset-bottom));
z-index: 30;
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0.25rem;
padding: 0.38rem;
border: 1px solid color-mix(in srgb, var(--border-light) 82%, transparent);
border-radius: 1rem;
background: color-mix(in srgb, var(--card-bg) 95%, transparent);
box-shadow:
0 12px 28px color-mix(in srgb, var(--bg) 22%, transparent),
inset 0 1px 0 color-mix(in srgb, var(--text) 10%, transparent);
overflow: visible;
}
.tab {
min-height: 3.2rem;
padding: 0.35rem 0.1rem 0.3rem;
border: 1px solid transparent;
border-radius: 0.75rem;
white-space: nowrap;
text-align: center;
font-size: 0.65rem;
font-weight: 700;
line-height: 1.1;
background: transparent;
color: var(--text-muted);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
}
.tab.active {
border-color: color-mix(in srgb, var(--accent-green) 50%, var(--border-light));
background: color-mix(in srgb, var(--btn-bg) 86%, transparent);
color: var(--text);
box-shadow: inset 0 1px 0 color-mix(in srgb, #ffffff 8%, transparent);
}
.tab-icon, .tab-more-icon { display: block; }
/* Shorten long tab labels to keep bottom nav compact */
.tab[data-tab="bookmarks"] .tab-label { font-size: 0.6rem; }
.tab[data-tab="digital-modes"] .tab-label { font-size: 0.6rem; }
.tab[data-tab="statistics"] .tab-label { font-size: 0.6rem; }
.tab[data-tab="statistics"], .tab[data-tab="recorder"],
.tab[data-tab="settings"], .tab[data-tab="about"] { display: none; }
.mobile-more-btn { display: flex; }
.mobile-more-menu {
position: fixed;
top: auto;
right: max(0.75rem, env(safe-area-inset-right));
bottom: calc(5.4rem + env(safe-area-inset-bottom));
}
.mobile-more-btn[aria-expanded="true"] {
color: var(--accent-text);
background: color-mix(in srgb, var(--accent-green) 10%, transparent);
}
.tab .tab-label, .tab[data-tab] .tab-label { font-size: 0.75rem; }
.decoder-tab-select {
display: block;
width: 100%;
min-height: 2.8rem;
margin-bottom: var(--space-3);
padding: 0 var(--space-3);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
background: color-mix(in srgb, var(--input-bg) 90%, var(--card-bg));
color: var(--text);
font-size: 1rem;
font-weight: 600;
box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 5%, transparent);
}
/* The picker replaces the sidebar here, so the panel takes the full width.
align-content keeps the two rows at their own height: the panel fills the
tab, and stretching would otherwise hand the picker a third of it. */
#tab-digital-modes {
grid-template-columns: minmax(0, 1fr);
align-content: start;
}
#tab-digital-modes > .sub-tab-bar { display: none; }
#tab-digital-modes > .sub-tab-panel { grid-column: 1; grid-row: auto; }
.toast-region { bottom: calc(5.7rem + env(safe-area-inset-bottom)); }
.top-bar-actions {
width: 100%;
justify-content: flex-end;
flex-wrap: nowrap;
gap: 0.4rem;
}
.operator-layout-picker { max-width: 100%; }
.operator-layout-picker select { max-width: 10rem; }
.header-rig-switch,
.header-style-pick {
flex: 0 0 auto;
min-width: 0;
}
.header-rig-switch select,
.header-style-pick select {
width: auto;
min-width: 0;
}
.header-bar-btn {
flex: 0 0 auto;
min-width: 0;
}
#tab-main,
#tab-digital-modes,
#tab-map,
#tab-about {
scroll-margin-top: 0.75rem;
}
.freq-inline {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.5rem;
align-items: start;
}
.frequency-col {
grid-column: 1 / -1;
}
.center-frequency-col {
grid-column: 1 / -1;
min-width: 0;
}
.wavelength-col,
.sig-strength-col,
.unit-col,
.mult-col {
min-width: 0;
}
.wavelength-display,
.sig-strength-display,
.jog-step,
.jog-mult {
width: 100%;
}
.jog-step button,
.jog-mult button {
flex: 1 1 0;
}
.signal,
.meter {
flex-wrap: wrap;
}
.signal-value,
.meter-value {
min-width: 0;
width: 100%;
text-align: left;
}
.signal-measure {
flex-wrap: wrap;
}
.signal-measure button {
flex: 1 1 8rem;
}
.signal-measure #sig-result {
width: 100%;
}
#spectrum-controls {
flex-direction: column;
align-items: stretch;
gap: 0.45rem;
padding-top: 0.45rem;
}
#spectrum-bw-row,
#spectrum-level-row {
display: flex;
flex-wrap: wrap;
gap: 0.35rem 0.4rem;
}
#spectrum-bw-label,
#spectrum-floor-label,
#spectrum-range-label,
#spectrum-peak-hold-label {
flex: 1 1 100%;
justify-content: space-between;
}
#spectrum-gamma-label {
flex: 1 1 100%;
justify-content: space-between;
}
#spectrum-gamma-input {
flex: 1 1 auto;
min-width: 3rem;
}
#spectrum-bw-input,
#spectrum-floor-input,
#spectrum-range-input,
#overview-peak-hold {
flex: 1 1 auto;
min-width: 3rem;
box-sizing: border-box;
}
#spectrum-bw-set-btn,
#spectrum-bw-auto-btn,
#spectrum-bw-sweet-btn,
#spectrum-auto-btn {
flex: 1 1 auto;
}
.spectrum-edge-shift {
width: 0.95rem;
}
.spectrum-bookmark-side .spectrum-bookmark-chip {
font-size: 0.58rem;
padding: 2px 6px;
}
.spectrum-edge-shift-left {
left: 0.2rem;
}
.spectrum-edge-shift-right {
right: 0.2rem;
}
.aprs-controls,
.ft8-controls,
.cw-controls {
flex-wrap: wrap;
align-items: stretch;
}
.ais-summary {
grid-template-columns: minmax(0, 1fr);
}
.aprs-summary {
grid-template-columns: minmax(0, 1fr);
}
#subtab-ais {
min-height: calc(100vh - 14rem);
}
#subtab-vdes {
min-height: calc(100vh - 14rem);
}
#subtab-aprs {
min-height: calc(100vh - 14rem);
}
#aprs-packets,
#ais-messages,
#vdes-messages {
min-height: 60vh;
}
.aprs-details-grid {
grid-template-columns: minmax(0, 1fr);
gap: 0.14rem;
}
.aprs-controls > button,
.ft8-controls > button,
.cw-controls > button {
flex: 1 1 auto;
}
.ft8-filter {
min-width: 0;
width: 100%;
flex-basis: 100%;
}
.map-overlay-panel {
top: 0.55rem;
left: 3.25rem;
right: 0.55rem;
flex-flow: column nowrap;
align-items: stretch;
width: auto;
max-height: min(16.5rem, calc(100% - 1.1rem));
padding: 0.6rem 0.65rem;
border-radius: 0.7rem;
}
.map-overlay-panel .map-locator-filter-group {
flex-wrap: wrap;
align-items: flex-start;
padding-right: 0;
border-right: 0;
}
/* Stacked in a column the panel scrolls, and the search field is no use at
the bottom of it: it goes back to the top, where it needs no scrolling. */
.map-overlay-panel .map-filter-grow {
order: -1;
max-width: none;
}
.map-overlay-actions {
justify-content: flex-end;
}
/* Stacked, the buttons sit under the filters rather than beside them, so the
rule that divides them has to lie across the panel, not down its left. */
.map-overlay-panel:not(.filters-hidden) .map-overlay-actions {
padding-left: 0;
padding-top: 0.5rem;
border-left: 0;
border-top: 1px solid color-mix(in srgb, var(--border-light) 55%, transparent);
}
.map-overlay-panel .map-paths-hint {
display: block;
font-size: 0.75rem;
color: var(--text-muted);
}
.map-band-legend {
left: 0.55rem;
bottom: 0.55rem;
width: min(12rem, calc(100% - 1.1rem));
max-height: min(36%, 13rem);
padding: 0.55rem 0.6rem;
border-radius: 0.7rem;
}
.map-band-legend-list {
grid-template-columns: repeat(auto-fit, minmax(4.8rem, 1fr));
}
.map-locator-filter-group {
align-items: stretch;
}
.map-qso-summary {
padding: 0.8rem 0.85rem 0.9rem;
}
.map-qso-summary-list {
grid-template-columns: 1fr;
}
.map-locator-filter-label {
width: 100%;
min-width: 0;
padding-top: 0;
}
.map-locator-empty {
padding-top: 0;
}
.rds-grid {
grid-template-columns: minmax(0, 1fr);
gap: 0.55rem;
}
.rds-field {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 0.16rem;
padding: 0.45rem 0.55rem;
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
border-radius: 0.6rem;
background: color-mix(in srgb, var(--input-bg) 84%, transparent);
}
.rds-label {
white-space: normal;
}
.rds-raw-header {
flex-wrap: wrap;
}
#rds-raw-copy-btn {
width: 100%;
}
.about-grid {
grid-template-columns: 1fr;
}
.about-table,
.about-table tbody,
.about-table tr,
.about-table td {
display: block;
width: 100%;
box-sizing: border-box;
}
.about-table tr {
padding: 0.35rem 0.75rem;
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
}
.about-table tr:last-child {
border-bottom: none;
}
.about-table td {
padding: 0.15rem 0;
border-bottom: none;
}
.about-table td:first-child {
width: 100%;
}
.footer {
flex-direction: column;
align-items: flex-start;
gap: 0.55rem;
}
/* ── Bookmark card layout ──────────────────────────────────────────── */
#bm-table-wrap { overflow-x: hidden; }
.bm-table,
.bm-table tbody { display: block; }
.bm-table thead { display: none; }
.bm-table tr {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.22rem 0.5rem;
border: 1px solid color-mix(in srgb, var(--border-light) 72%, transparent);
border-radius: 0.7rem;
padding: 0.6rem 0.7rem;
margin-bottom: 0.45rem;
background: color-mix(in srgb, var(--btn-bg) 35%, transparent);
}
.bm-table tr:hover td { background: transparent; }
.bm-table td {
display: flex;
flex-direction: column;
gap: 0.05rem;
border-bottom: none;
padding: 0.08rem 0;
font-size: 0.82rem;
word-break: break-word;
}
.bm-table td::before {
font-size: 0.62rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--text-muted);
}
.bm-col-sel { grid-column: 1 / -1; display: flex; align-items: center; padding: 0; }
.bm-col-sel::before { display: none; }
.bm-col-name { grid-column: 1 / -1; font-weight: 600; font-size: 0.9rem; }
.bm-col-name::before { content: "Bookmark"; }
.bm-col-freq::before { content: "Frequency"; }
.bm-col-mode::before { content: "Mode"; }
.bm-col-bw::before { content: "Bandwidth"; }
.bm-col-loc::before { content: "Locator"; }
.bm-col-cat::before { content: "Category"; }
.bm-col-dec::before { content: "Digital modes"; }
.bm-col-cmt { grid-column: 1 / -1; }
.bm-col-cmt::before { content: "Comment"; }
.bm-col-act { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 0.4rem; padding-top: 0.25rem; }
.bm-col-act::before { display: none; }
.bm-col-act button { flex: 1 1 auto; min-height: 2.4rem; font-size: 0.8rem; }
}
/* ── Spectrum display ─────────────────────────────────────────────────── */
#spectrum-panel {
margin-bottom: 0;
}
.spectrum-resizing,
.spectrum-resizing * {
cursor: ns-resize !important;
user-select: none !important;
}
.spectrum-wrap {
position: relative;
width: 100%;
user-select: none;
}
#spectrum-canvas {
display: block;
width: 100%;
height: var(--spectrum-plot-height);
background: var(--spectrum-bg);
border-radius: 6px 6px 0 0;
cursor: crosshair;
touch-action: none;
}
#spectrum-db-axis {
position: absolute;
top: 0;
left: 0;
width: 3rem;
height: var(--spectrum-plot-height);
pointer-events: none;
z-index: 7;
}
#spectrum-db-axis span {
position: absolute;
left: 0.22rem;
transform: translateY(-50%);
font-size: 0.62rem;
line-height: 1;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
color: var(--text-muted);
font-weight: 700;
letter-spacing: 0.01em;
text-shadow: 0 1px 2px color-mix(in srgb, var(--bg) 65%, transparent);
white-space: nowrap;
}
.spectrum-edge-shift {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 8;
width: 1.4rem;
height: 100%;
padding: 0;
border: none;
background: none;
color: color-mix(in srgb, var(--text-muted) 78%, var(--accent-yellow));
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0;
line-height: 1;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
transition: color 120ms ease, opacity 120ms ease;
opacity: 0.92;
}
.spectrum-edge-shift::before {
content: "";
width: 0;
height: 0;
border-top: 0.24rem solid transparent;
border-bottom: 0.24rem solid transparent;
}
.spectrum-edge-shift-left::before {
border-right: 0.38rem solid currentColor;
}
.spectrum-edge-shift-right::before {
border-left: 0.38rem solid currentColor;
}
.spectrum-edge-shift:hover {
color: var(--accent-yellow);
opacity: 1;
}
.spectrum-edge-shift:active {
transform: translateY(-50%);
color: var(--text-heading);
}
.spectrum-edge-shift-left {
left: 0.24rem;
}
.spectrum-edge-shift-right {
right: 0.24rem;
}
#spectrum-freq-axis {
position: relative;
height: 18px;
width: 100%;
font-size: 0.7rem;
color: var(--text-muted);
background: var(--bg-secondary);
border-radius: 0 0 6px 6px;
border: 1px solid var(--border);
border-top: none;
}
#spectrum-size-grip {
position: relative;
height: 1.1rem;
margin: 0.18rem 0 0.42rem;
cursor: ns-resize;
touch-action: none;
border-radius: 999px;
}
#spectrum-size-grip::before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 4.2rem;
height: 0.3rem;
transform: translate(-50%, -50%);
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent);
background: color-mix(in srgb, var(--btn-bg) 78%, transparent);
}
#spectrum-size-grip:hover::before {
border-color: color-mix(in srgb, var(--accent-yellow) 55%, var(--border-light));
background: color-mix(in srgb, var(--accent-yellow) 22%, var(--btn-bg));
}
#spectrum-freq-axis span {
position: absolute;
transform: translateX(-50%);
white-space: nowrap;
top: 2px;
font-weight: 700;
pointer-events: none;
}
/* ── Zoom indicator ── */
#spectrum-zoom-indicator {
display: none;
position: absolute;
top: 6px;
right: 3.5rem;
z-index: 9;
font-size: 0.68rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-weight: 700;
color: var(--text-muted);
background: color-mix(in srgb, var(--spectrum-bg) 75%, transparent);
padding: 1px 6px;
border-radius: 3px;
pointer-events: none;
}
/* ── Zoom minimap ── */
#spectrum-minimap {
display: none;
position: absolute;
bottom: 22px;
right: 3.5rem;
z-index: 9;
width: 80px;
height: 10px;
background: color-mix(in srgb, var(--spectrum-bg) 60%, transparent);
border: 1px solid color-mix(in srgb, var(--border-light) 50%, transparent);
border-radius: 3px;
pointer-events: none;
overflow: hidden;
}
.minimap-view {
position: absolute;
top: 0;
height: 100%;
background: color-mix(in srgb, var(--accent-yellow) 40%, transparent);
border-radius: 2px;
}
/* ── Full waterfall canvas ── */
#spectrum-waterfall-canvas {
display: block;
width: 100%;
height: var(--spectrum-plot-height);
background: var(--spectrum-bg);
cursor: crosshair;
touch-action: none;
}
/* ── Bandplan strip ─────────────────────────────────────────────────────────── */
#spectrum-bandplan-strip {
position: relative;
left: 0;
right: 0;
width: 100%;
height: 0;
overflow: hidden;
transition: height 80ms ease;
z-index: 5;
background: var(--bg-secondary, #121828);
border-bottom: 1px solid transparent;
}
#spectrum-bandplan-strip.bp-visible {
height: 18px;
overflow: hidden;
border-bottom-color: var(--border-light, rgba(255,255,255,0.06));
}
/* No allocations in range. The height stays reserved — collapsing it moved
every element below by 18px each time tuning left a band. */
#spectrum-bandplan-strip.bp-empty {
background: color-mix(in srgb, var(--border) 22%, transparent);
}
.bp-segment {
position: absolute;
top: 0;
height: 100%;
box-sizing: border-box;
border-right: 1px solid var(--bg-secondary, #121828);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
cursor: default;
min-width: 0;
}
.bp-segment-label {
font-size: 0.56rem;
font-weight: 600;
line-height: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 2px;
pointer-events: none;
opacity: 0.92;
}
.bp-segment[data-mode="CW"] { background: rgba(74,144,217,0.55); color: #d0e4ff; }
.bp-segment[data-mode="Phone"] { background: rgba(76,175,80,0.50); color: #d0f5d2; }
.bp-segment[data-mode="Narrow"] { background: rgba(217,74,122,0.50); color: #ffd0e4; }
.bp-segment[data-mode="FM"] { background: rgba(255,152,0,0.50); color: #fff0d0; }
.bp-segment[data-mode="All"] { background: rgba(120,120,120,0.40); color: #ddd; }
.bp-segment[data-mode="Beacon"] { background: rgba(156,39,176,0.50); color: #f0d0ff; }
.bp-segment[data-mode="Satellite"]{ background: rgba(0,188,212,0.50); color: #d0f8ff; }
.bp-band-label {
position: absolute;
top: 0;
height: 100%;
display: flex;
align-items: center;
font-size: 0.54rem;
font-weight: 700;
color: var(--text-muted);
opacity: 0.65;
pointer-events: none;
padding-left: 3px;
z-index: 1;
}
/* Legend in settings */
.bandplan-legend-grid {
display: flex;
flex-wrap: wrap;
gap: 0.6rem 1.2rem;
}
.bandplan-legend-item {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.8rem;
color: var(--text);
}
.bandplan-legend-swatch {
display: inline-block;
width: 18px;
height: 12px;
border-radius: 2px;
}
.bandplan-legend-swatch[data-mode="CW"] { background: rgba(74,144,217,0.75); }
.bandplan-legend-swatch[data-mode="Phone"] { background: rgba(76,175,80,0.70); }
.bandplan-legend-swatch[data-mode="Narrow"] { background: rgba(217,74,122,0.70); }
.bandplan-legend-swatch[data-mode="FM"] { background: rgba(255,152,0,0.70); }
.bandplan-legend-swatch[data-mode="All"] { background: rgba(120,120,120,0.55); }
.bandplan-legend-swatch[data-mode="Beacon"] { background: rgba(156,39,176,0.70); }
.bandplan-legend-swatch[data-mode="Satellite"]{ background: rgba(0,188,212,0.70); }
#spectrum-bookmark-axis {
position: absolute;
top: calc(-1 * var(--overview-plot-height));
left: 0;
right: 0;
z-index: 8;
height: 0;
overflow: hidden;
font-size: 0.68rem;
background: transparent;
transition: height 80ms ease;
}
#spectrum-bookmark-axis.bm-axis-visible {
height: 38px;
overflow: visible;
}
/* The rail sits over the top of the overview plot, so it must not swallow
drags and clicks meant for it — only the chips themselves are targets. */
#spectrum-bookmark-axis { pointer-events: none; }
#spectrum-bookmark-axis .spectrum-bookmark-chip { pointer-events: auto; }
/* Nothing bookmarked in range: the rail keeps its place but stays blank —
no caption over the plot. */
.spectrum-bookmark-chip {
position: absolute;
top: 2px;
left: 0;
will-change: transform;
cursor: pointer;
font-weight: 600;
font-size: 0.65rem;
background: var(--bm-cat-bg, var(--accent-yellow));
color: var(--bm-cat-fg, #1a202c);
border: 1px solid var(--bm-cat-bg, var(--accent-yellow));
border-radius: 3px;
padding: 2px 8px;
max-width: 130px;
overflow: hidden;
line-height: 1.4;
display: inline-flex;
align-items: flex-start;
gap: 4px;
}
.spectrum-bookmark-chip:hover {
filter: brightness(1.15);
}
.spectrum-bookmark-chip .spectrum-bookmark-name {
white-space: normal;
word-break: break-word;
line-height: 1.2;
}
.spectrum-bookmark-side {
position: absolute;
top: calc(var(--spectrum-plot-height) / 2);
transform: translateY(-50%);
z-index: 7;
width: var(--spectrum-bookmark-side-width);
display: none;
flex-direction: column;
gap: 0.28rem;
}
.spectrum-bookmark-side.bm-side-visible {
display: flex;
}
.spectrum-bookmark-side-left {
left: calc(-1 * var(--spectrum-bookmark-side-offset));
align-items: flex-end;
}
.spectrum-bookmark-side-right {
right: calc(-1 * var(--spectrum-bookmark-side-offset));
align-items: flex-start;
}
.spectrum-bookmark-side .spectrum-bookmark-chip {
position: relative;
top: auto;
left: auto;
transform: none;
max-width: 100%;
}
.spectrum-bookmark-chip-side {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.16rem;
width: 100%;
min-height: 0;
padding: 0.38rem 0.5rem 0.42rem;
border-radius: 0.55rem;
white-space: normal;
line-height: 1.1;
}
.spectrum-bookmark-side-head {
display: inline-flex;
align-items: center;
gap: 0.32rem;
min-width: 0;
width: 100%;
}
.spectrum-bookmark-chip-side .bm-icon-svg {
flex: 0 0 auto;
width: 0.5rem;
height: 0.72rem;
opacity: 0.95;
}
.spectrum-bookmark-chip-side .spectrum-bookmark-freq,
.spectrum-bookmark-chip-side .spectrum-bookmark-name {
display: block;
max-width: 100%;
min-width: 0;
}
.spectrum-bookmark-chip-side .spectrum-bookmark-freq {
font-size: 0.54rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.spectrum-bookmark-chip-side .spectrum-bookmark-name {
width: 100%;
font-size: 0.65rem;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.22;
}
.bm-icon-svg path {
fill: var(--bm-cat-fg, #1a202c);
}
#spectrum-tooltip {
display: none;
position: absolute;
pointer-events: none;
background: rgba(10,15,24,0.85);
color: #00e676;
font-size: 0.75rem;
font-family: monospace;
padding: 2px 6px;
border-radius: 4px;
border: 1px solid rgba(0,230,118,0.3);
white-space: nowrap;
z-index: 10;
}
#spectrum-controls {
display: flex;
align-items: center;
justify-content: space-between;
padding: 3px 4px 0;
gap: 0.6rem;
font-size: 0.78rem;
color: var(--text-muted);
}
#spectrum-bw-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.4rem;
}
#spectrum-bw-label {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.75rem;
color: var(--text-muted);
min-width: 0;
}
#spectrum-bw-input {
width: 4.5rem;
padding: 1px 4px;
font-size: 0.75rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--input-bg);
color: var(--text);
text-align: right;
height: 1.5rem;
}
#spectrum-bw-set-btn,
#spectrum-bw-auto-btn,
#spectrum-bw-sweet-btn {
height: 1.5rem;
min-height: 0;
padding: 0 8px;
font-size: 0.75rem;
}
#spectrum-level-row {
display: flex;
align-items: center;
gap: 0.4rem;
}
#spectrum-floor-label {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.75rem;
color: var(--text-muted);
}
#spectrum-floor-input {
width: 3.4rem;
padding: 1px 4px;
font-size: 0.75rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--input-bg);
color: var(--text);
text-align: right;
height: 1.5rem;
}
#spectrum-range-label {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.75rem;
color: var(--text-muted);
}
#spectrum-range-input {
width: 3.4rem;
padding: 1px 4px;
font-size: 0.75rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--input-bg);
color: var(--text);
text-align: right;
height: 1.5rem;
}
#spectrum-auto-btn {
height: 1.5rem;
min-height: 0;
padding: 0 8px;
font-size: 0.75rem;
}
#spectrum-gamma-label {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.75rem;
color: var(--text-muted);
}
#spectrum-gamma-input {
width: 5rem;
height: 1.5rem;
cursor: pointer;
}
#spectrum-gamma-value {
font-size: 0.75rem;
min-width: 1.6rem;
text-align: right;
}
.spectrum-hint-mouse,
.spectrum-hint-touch {
font-size: 0.68rem;
color: var(--text-muted);
text-align: right;
margin-top: 2px;
opacity: 0.6;
}
/* Show correct hint based on input device */
.spectrum-hint-touch { display: none; }
@media (hover: none) and (pointer: coarse) {
.spectrum-hint-mouse { display: none; }
.spectrum-hint-touch { display: block; }
}
/* ── Phone layout (≤ 520px) ───────────────────────────────────────────── */
@media (max-width: 520px) {
/* Single-column controls: jog first, then mode, then power */
.controls-row {
flex-direction: column;
align-items: stretch;
}
.controls-col-center {
order: -1;
align-self: center;
width: auto;
}
.controls-col-center::after { display: none; }
.controls-col.label-below-col .inline,
.controls-col.label-below-col .btn-grid { margin-top: 0; }
/* Scale frequency display to fit narrow screens */
#freq { font-size: clamp(1.3rem, 6vw, 2rem); }
/* Wider volume sliders for touch */
.vol-slider { width: 100%; flex: 1 1 auto; }
.audio-group {
flex-wrap: wrap;
width: 100%;
}
.audio-group-label { width: 100%; }
.vol-label {
flex-direction: row;
align-items: center;
gap: 0.5rem;
width: 100%;
justify-content: space-between;
}
.vol-pct { min-width: 2.4rem; text-align: right; }
.vol-slider::-webkit-slider-thumb { width: 22px; height: 22px; }
.vol-slider::-moz-range-thumb { width: 22px; height: 22px; }
#audio-row .inline {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.55rem;
align-items: stretch;
}
#rx-audio-btn,
#tx-audio-btn,
.audio-volume-group,
.audio-level-group,
#sdr-squelch-wrap {
grid-column: 1 / -1;
}
#audio-level {
min-width: 0;
height: 14px;
}
/* Spectrum control inputs and buttons: meet minimum tap-target size */
#spectrum-bw-input,
#spectrum-floor-input {
height: 2.2rem;
font-size: 0.82rem;
}
#spectrum-bw-set-btn,
#spectrum-bw-auto-btn,
#spectrum-bw-sweet-btn,
#spectrum-auto-btn {
height: 2.2rem;
min-height: 0;
padding: 0 10px;
font-size: 0.82rem;
}
}
@media (max-width: 480px) {
.toast-region {
right: max(0.65rem, env(safe-area-inset-right));
width: calc(100vw - max(1.3rem, env(safe-area-inset-left) + env(safe-area-inset-right)));
}
.toast { grid-template-columns: 1.25rem minmax(0, 1fr); }
.toast button { grid-column: 2; justify-self: start; }
.ui-dialog-card { padding: var(--space-5); }
.ui-dialog-actions { flex-direction: column-reverse; }
.ui-dialog-actions button { width: 100%; }
}
/* Preserve legibility after the component-specific compact-mobile rules. */
@media (max-width: 640px) {
button, input, select, textarea, .tab-label, .sub-tab,
.label, .hint, .subtitle, .wfm-control-label, .wfm-intf-val,
.ft8-header, .ft8-message, .aprs-packet, .ais-message, .vdes-message {
font-size: max(0.75rem, 12px);
}
}
/* Theme styles have been moved to themes.css */
/* ============================================================
Bookmarks tab
============================================================ */
.bm-toolbar {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 0.75rem 0.4rem;
flex-wrap: wrap;
}
.bm-add-btn {
white-space: nowrap;
}
#sch-entry-form .bm-form-grid {
gap: 0.75rem 0.75rem;
}
#sch-entry-form .bm-label {
gap: 0.35rem;
}
#bm-form-wrap {
position: fixed;
inset: 0;
z-index: 120;
padding: 1.25rem;
background: rgba(7, 12, 18, 0.88);
align-items: center;
justify-content: center;
}
.bm-form {
box-sizing: border-box;
background: var(--card-bg);
border: 1px solid var(--border-light);
border-radius: 0.8rem;
padding: 1rem 1.1rem;
margin: 0;
width: min(46rem, calc(100vw - 2.5rem));
max-height: min(85vh, 42rem);
overflow-x: hidden;
overflow-y: auto;
box-shadow: 0 1.2rem 2.6rem rgba(0, 0, 0, 0.38);
}
.bm-form-title {
font-weight: 700;
margin-bottom: 0.65rem;
font-size: 0.95rem;
color: var(--text-heading);
}
.bm-form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
gap: 0.5rem 0.75rem;
}
.bm-label {
display: flex;
flex-direction: column;
gap: 0.2rem;
font-size: 0.78rem;
color: var(--text-muted);
min-width: 0;
}
.bm-label input,
.bm-label select {
font-size: 0.85rem;
padding: 0.35rem 0.5rem;
}
.bm-label-wide {
grid-column: 1 / -1;
}
.bm-form-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
justify-content: flex-end;
}
.bm-save-btn,
#bm-form-cancel,
#sch-entry-form-cancel {
background: var(--accent-green);
color: #fff;
border: none;
border-radius: 0.35rem;
padding: 0.4rem 1rem;
font-weight: 600;
cursor: pointer;
}
.bm-save-btn:hover,
#bm-form-cancel:hover,
#sch-entry-form-cancel:hover {
opacity: 0.88;
}
#bm-form-cancel,
#sch-entry-form-cancel {
background: var(--btn-bg);
color: var(--text);
border: 1px solid var(--btn-border);
}
.bm-decoder-checks {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
min-height: 2.2rem;
padding: 0.5rem 0.65rem;
border: 1px solid var(--border-light);
border-radius: 0.45rem;
background: var(--input-bg);
}
.bm-decoder-check {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--text);
font-size: 0.85rem;
}
.bm-decoder-check input {
margin: 0;
}
.bm-table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
margin: 0 0 0.5rem;
}
.bm-table th {
text-align: left;
padding: 0.35rem 0.6rem;
border-bottom: 1px solid var(--border);
color: var(--text-muted);
font-weight: 600;
white-space: nowrap;
}
.bm-table td {
padding: 0.35rem 0.6rem;
border-bottom: 1px solid var(--border);
vertical-align: middle;
}
.bm-table tr:hover td {
background: var(--btn-bg);
}
.bm-col-freq,
.bm-col-bw {
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.bm-col-mode,
.bm-col-dec {
white-space: nowrap;
}
.bm-col-act {
white-space: nowrap;
}
.bm-col-act button {
font-size: 0.78rem;
padding: 0.2rem 0.5rem;
margin-right: 0.25rem;
border-radius: 0.25rem;
border: 1px solid var(--btn-border);
background: var(--btn-bg);
color: var(--text);
cursor: pointer;
}
.bm-col-act button:hover {
background: var(--border-light);
}
.bm-col-sel {
width: 1.8rem;
text-align: center;
padding-left: 0.4rem;
padding-right: 0;
}
.bm-del-btn,
#bm-del-selected-btn {
color: var(--accent-red) !important;
border-color: var(--accent-red) !important;
}
.bm-move-wrap {
display: inline-flex;
align-items: center;
gap: 0.35rem;
}
.bm-scope-badge {
display: inline-block;
font-size: 0.65rem;
font-weight: 700;
padding: 0.05rem 0.3rem;
border-radius: 0.2rem;
background: var(--btn-bg);
border: 1px solid var(--border-light);
color: var(--text-muted);
vertical-align: middle;
margin-left: 0.3rem;
}
.bm-empty {
padding: 2rem 1rem;
text-align: center;
color: var(--text-muted);
font-size: 0.9rem;
}
.bm-paginator {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.8rem;
padding: 0.9rem 0.2rem 0;
}
.bm-page-summary,
.bm-page-indicator {
color: var(--text-muted);
font-size: 0.85rem;
}
.bm-page-controls {
display: flex;
align-items: center;
gap: 0.55rem;
}
.bm-page-controls button {
min-width: 6.4rem;
}
.bm-page-controls button:disabled {
opacity: 0.45;
cursor: default;
}
/* =========================================================================
Scheduler tab
========================================================================= */
.sch-panel {
padding: 1rem;
}
.sch-toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 1.25rem;
border-radius: 0.4rem;
color: #fff;
font-weight: 600;
font-size: 0.9rem;
z-index: 999;
pointer-events: none;
}
.sch-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
align-items: flex-end;
}
.sch-label {
display: flex;
flex-direction: column;
gap: 0.3rem;
font-size: 0.85rem;
color: var(--text-muted);
font-weight: 600;
min-width: 9rem;
}
.sch-rig-select {
min-width: 10rem;
}
.sch-section {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--bg-secondary);
border-radius: 0.5rem;
border: 1px solid var(--border-light);
}
.sch-section-title {
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.sch-ts-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
margin-bottom: 1rem;
}
.sch-ts-table th,
.sch-ts-table td {
padding: 0.4rem 0.6rem;
text-align: left;
border-bottom: 1px solid var(--border-light);
}
.sch-ts-table th {
color: var(--text-muted);
font-weight: 600;
}
.sch-ts-table td:last-child {
white-space: nowrap;
}
.sch-ts-table td:last-child button {
font-size: 0.78rem;
padding: 0.18rem 0.45rem;
margin-right: 0.25rem;
}
.sch-ts-table td:last-child button:last-child {
margin-right: 0;
}
.sch-ts-table tr.sch-active {
border-left: 3px solid var(--accent);
}
.sch-ts-table tr.sch-active td:first-child {
padding-left: calc(0.6rem - 3px);
}
.sch-local-time {
color: var(--text-muted);
font-size: 0.78rem;
opacity: 0.7;
}
.sch-timeline-local-tick {
opacity: 0.6;
}
.sch-status-detail {
font-size: 0.82rem;
color: var(--accent);
font-weight: 600;
}
.sch-add-row {
align-items: flex-end;
}
.sch-actions {
display: flex;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
/* Save is a primary action — accent background, inherits all other button rules. */
.sch-save-btn {
background: var(--accent-green);
color: #fff;
border-color: var(--accent-green);
font-weight: 700;
}
.sch-save-btn.sch-dirty::after {
content: "";
display: inline-block;
width: 7px;
height: 7px;
border-radius: 50%;
background: #fff;
margin-left: 0.45rem;
vertical-align: middle;
animation: sch-dirty-pulse 1.5s ease-in-out infinite;
}
@keyframes sch-dirty-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.sch-status-card {
font-size: 0.9rem;
color: var(--text-muted);
}
.sch-extra-bm-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 1.6rem;
}
.sch-extra-bm-tag {
display: inline-flex;
align-items: center;
gap: 3px;
background: var(--btn-bg);
border: 1px solid var(--border-light);
border-radius: 4px;
padding: 0.1rem 0.4rem;
font-size: 0.8rem;
color: var(--text-muted);
}
.sch-extra-bm-rm {
cursor: pointer;
opacity: 0.6;
font-size: 1rem;
line-height: 1;
}
.sch-extra-bm-rm:hover { opacity: 1; }
/* Satellite scheduler overlay */
.sch-sat-toggle-row {
display: inline-flex;
align-items: center;
gap: 0.55rem;
min-height: var(--control-height);
color: var(--text);
font-weight: 500;
}
.sch-sat-pass-status {
font-size: 0.82rem;
}
.sch-sat-active-badge {
display: inline-block;
background: var(--accent-green, #1a7);
color: #fff;
font-size: 0.78rem;
font-weight: 600;
padding: 0.2rem 0.6rem;
border-radius: 3px;
letter-spacing: 0.03em;
}
#sch-sat-form-wrap {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
}
#sch-sat-form .bm-form-grid {
gap: 0.75rem;
}
#sch-sat-form .bm-label {
min-width: 12rem;
flex: 1 1 12rem;
}
#sch-sat-form-cancel {
cursor: pointer;
border: 1px solid var(--border);
background: var(--bg-muted);
color: var(--text);
border-radius: var(--radius);
padding: 0.55rem 1rem;
font-size: 0.85rem;
}
#sch-sat-form-cancel:hover {
background: var(--border);
}
.bgd-toggle-wrap {
min-width: 18rem;
flex: 1 1 20rem;
}
.bgd-toggle-row {
display: inline-flex;
align-items: center;
gap: 0.55rem;
min-height: var(--control-height);
color: var(--text);
font-weight: 500;
}
.bgd-add-row {
display: flex;
gap: 0.55rem;
align-items: center;
width: 100%;
}
.bgd-bookmark-pick {
flex: 1 1 28rem;
min-width: 16rem;
}
.bgd-add-row .status-input {
flex: 1 1 auto;
}
.bgd-bookmark-list {
min-height: 1.8rem;
}
.bgd-bookmark-tag {
padding-right: 0.3rem;
}
.bgd-bookmark-meta {
color: var(--text-muted);
font-size: 0.78rem;
}
.bgd-status-list {
display: grid;
gap: 0.65rem;
}
.bgd-status-row {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.65rem 0.75rem;
border: 1px solid var(--border-light);
border-radius: 0.55rem;
background: color-mix(in srgb, var(--card-bg) 74%, transparent);
}
.bgd-status-name {
font-weight: 600;
}
.bgd-status-meta {
color: var(--text-muted);
font-size: 0.82rem;
}
.bgd-status-state {
align-self: center;
white-space: nowrap;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--accent-green);
}
.bgd-status-state[data-state="out_of_span"],
.bgd-status-state[data-state="waiting_for_spectrum"],
.bgd-status-state[data-state="waiting_for_user"],
.bgd-status-state[data-state="scheduler_has_control"],
.bgd-status-state[data-state="inactive"],
.bgd-status-state[data-state="handled_by_scheduler"],
.bgd-status-state[data-state="handled_by_virtual_channel"] {
color: var(--accent-yellow);
}
.bgd-status-state[data-state="missing_bookmark"],
.bgd-status-state[data-state="no_supported_decoders"] {
color: var(--accent-red);
}
/* ── "Now Playing" status card (top of scheduler & bgd panels) ──── */
.now-playing-card {
border-left: 3px solid var(--accent-green);
padding: 0.75rem 1rem;
margin-bottom: 1.25rem;
background: var(--bg-secondary);
border-radius: 0 0.5rem 0.5rem 0;
font-size: 0.9rem;
}
/* ── 24h Timeline Bar ─────────────────────────────────────────────── */
.sch-timeline-wrap {
margin-bottom: 1rem;
overflow: hidden;
border-radius: 0.4rem;
border: 1px solid var(--border-light);
background: var(--bg-secondary);
}
.sch-timeline-wrap svg {
display: block;
width: 100%;
height: 80px;
}
.sch-timeline-seg {
cursor: pointer;
opacity: 0.82;
transition: opacity 0.12s;
}
.sch-timeline-seg:hover {
opacity: 1;
}
.sch-timeline-tick-label {
fill: var(--text-muted);
font-size: 9px;
font-family: inherit;
}
.sch-timeline-needle {
stroke: var(--accent-red);
stroke-width: 1.5;
}
.sch-timeline-needle-head {
fill: var(--accent-red);
}
/* Details toggle for entry table */
.sch-ts-details {
margin-top: 0.5rem;
}
.sch-ts-details summary {
cursor: pointer;
font-size: 0.82rem;
color: var(--text-muted);
font-weight: 600;
padding: 0.3rem 0;
user-select: none;
}
/* ── Inline Row Editing ──────────────────────────────────────────── */
.sch-inline-editing td {
padding: 0.25rem 0.4rem;
}
.sch-inline-input {
font-size: 0.8rem;
padding: 0.15rem 0.3rem;
width: 100%;
min-width: 0;
}
.sch-inline-save, .sch-inline-cancel {
font-size: 0.75rem !important;
padding: 0.15rem 0.4rem !important;
}
/* ── Drag-to-reorder ─────────────────────────────────────────────── */
.sch-drag-handle {
cursor: grab;
user-select: none;
text-align: center;
color: var(--text-muted);
font-size: 1rem;
width: 1.5rem;
padding: 0.3rem !important;
}
.sch-drag-handle:active { cursor: grabbing; }
.sch-drag-th { width: 1.5rem; }
.sch-dragging { opacity: 0.5; }
.sch-drag-over { border-top: 2px solid var(--accent); }
/* ── Extra Bookmark Chips ────────────────────────────────────────── */
.sch-extra-bm-chip {
display: inline-flex;
align-items: center;
gap: 0.2rem;
padding: 0.15rem 0.5rem;
border-radius: 1rem;
background: var(--accent);
color: #fff;
font-size: 0.78rem;
font-weight: 600;
white-space: nowrap;
}
.sch-extra-bm-chip-rm {
cursor: pointer;
font-size: 0.9rem;
opacity: 0.8;
font-weight: 700;
line-height: 1;
}
.sch-extra-bm-chip-rm:hover { opacity: 1; }
/* ── Activity Log ────────────────────────────────────────────────── */
.sch-activity-log-details {
margin-bottom: 1rem;
}
.sch-activity-log-details summary {
cursor: pointer;
font-size: 0.82rem;
color: var(--text-muted);
font-weight: 600;
padding: 0.3rem 0;
}
.sch-activity-log {
max-height: 12rem;
overflow-y: auto;
font-size: 0.78rem;
padding: 0.3rem 0;
}
.sch-log-entry {
padding: 0.15rem 0;
border-bottom: 1px solid var(--border-light);
}
.sch-log-time {
color: var(--text-muted);
}
.sch-log-action {
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
}
.sch-log-bm {
color: var(--accent);
}
.sch-log-label {
color: var(--text-muted);
font-style: italic;
}
/* ── Inline Entry Editor (replaces modal overlay) ─────────────────── */
#sch-entry-form-wrap {
position: static;
z-index: auto;
display: none;
align-items: stretch;
justify-content: stretch;
background: none;
margin-bottom: 1rem;
}
#sch-entry-form-wrap .bm-form {
max-width: 100%;
width: 100%;
border: 1px solid var(--border-light);
border-radius: 0.5rem;
background: var(--bg-secondary);
}
/* ── Interleave Progress Ring ─────────────────────────────────────── */
.interleave-ring-wrap {
display: flex;
align-items: center;
gap: 0.5rem;
}
.interleave-ring {
width: 32px;
height: 32px;
transform: rotate(-90deg);
flex-shrink: 0;
}
.interleave-ring-bg {
fill: none;
stroke: var(--border-light);
stroke-width: 3;
}
.interleave-ring-fill {
fill: none;
stroke: var(--accent-green);
stroke-width: 3;
stroke-linecap: round;
transition: stroke-dashoffset 0.9s linear;
}
.interleave-ring-text {
display: flex;
flex-direction: column;
gap: 0;
min-width: 0;
}
.interleave-ring-label {
font-size: 0.78rem;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 14rem;
}
.interleave-ring-sub {
font-size: 0.7rem;
color: var(--text-muted);
opacity: 0.88;
}
/* ── Background Decode Checkbox List ──────────────────────────────── */
.bgd-checklist-filter {
width: 100%;
margin-bottom: 0.5rem;
padding: 0.4rem 0.6rem;
font-size: 0.85rem;
background: var(--input-bg);
border: 1px solid var(--border);
border-radius: var(--radius, 4px);
color: var(--text);
}
.bgd-checklist-filter::placeholder {
color: var(--text-muted);
opacity: 0.7;
}
.bgd-checklist {
max-height: 16rem;
overflow-y: auto;
border: 1px solid var(--border-light);
border-radius: 0.4rem;
background: var(--bg-secondary);
}
.bgd-checklist-row {
display: flex;
align-items: center;
gap: 0.55rem;
padding: 0.45rem 0.65rem;
border-bottom: 1px solid var(--border-light);
cursor: pointer;
font-size: 0.85rem;
transition: background-color 0.1s;
}
.bgd-checklist-row:last-child {
border-bottom: none;
}
.bgd-checklist-row:hover {
background: color-mix(in srgb, var(--card-bg) 60%, transparent);
}
.bgd-checklist-row input[type="checkbox"] {
flex-shrink: 0;
}
.bgd-checklist-name {
font-weight: 600;
color: var(--text);
}
.bgd-checklist-meta {
color: var(--text-muted);
font-size: 0.78rem;
margin-left: auto;
white-space: nowrap;
}
.bgd-checklist-empty {
padding: 0.75rem;
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
}
/* ── Select All / Deselect All buttons ────────────────────────────── */
.bgd-select-actions {
display: flex;
gap: 0.5rem;
}
.bgd-select-btn {
background: var(--bg-secondary);
border: 1px solid var(--border-light);
border-radius: 0.3rem;
color: var(--text-muted);
font-size: 0.78rem;
font-weight: 600;
padding: 0.25rem 0.65rem;
cursor: pointer;
transition: background-color 0.15s, color 0.15s;
}
.bgd-select-btn:hover {
background: var(--card-bg);
color: var(--text);
}
/* ── SVG State Dot Badges ─────────────────────────────────────────── */
.bgd-state-dot {
width: 8px;
height: 8px;
display: inline-block;
vertical-align: middle;
margin-right: 4px;
fill: currentColor;
}
@media (max-width: 600px) {
.channel-scheduler-controls {
flex-direction: column;
align-items: stretch;
}
.vchan-picker {
flex-basis: auto;
}
.scheduler-control-row {
align-items: stretch;
flex-basis: auto;
}
.scheduler-release-wrap {
align-items: stretch;
min-width: 0;
}
.scheduler-release-status {
text-align: left;
}
.sch-row {
flex-direction: column;
}
.sch-label {
min-width: 100%;
}
.bm-paginator {
flex-direction: column;
align-items: stretch;
}
.bm-page-controls {
justify-content: space-between;
}
.bm-page-controls button {
flex: 1 1 0;
min-width: 0;
}
.bm-page-indicator,
.bm-page-summary {
text-align: center;
}
.bgd-add-row,
.bgd-status-row {
flex-direction: column;
align-items: stretch;
}
.bgd-add-row button {
width: 100%;
}
.interleave-ring-label {
max-width: 8rem;
}
.bgd-checklist-meta {
margin-left: 0;
font-size: 0.75rem;
}
}
/* ── SAT panel ──────────────────────────────────────────────────────── */
.sat-view-bar { display: flex; gap: 0; margin-bottom: 0.75rem; border-bottom: 1px solid var(--border); }
.sat-view-btn { flex-shrink: 0; background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: 0.3rem 0.9rem; color: var(--text-muted); cursor: pointer; font-size: 0.82rem; }
.sat-view-active { border-bottom-color: var(--accent-green); color: var(--accent-green); font-weight: 600; }
.sat-view-btn:hover:not(.sat-view-active) { color: var(--text); }
.sat-live-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.5rem; }
.sat-live-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 0.35rem; padding: 0.5rem 0.75rem; display: flex; flex-direction: column; gap: 0.15rem; }
.sat-live-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.04em; }
.sat-live-value { font-size: 0.9rem; font-weight: 600; }
.sat-state-idle { color: var(--text-muted); }
.sat-state-listening { color: var(--accent-green); }
.sat-state-decoding { color: #f0a020; }
.sat-history-controls { display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; flex-wrap: wrap; }
.sat-sort-select { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 0.25rem; color: var(--text); padding: 0.25rem 0.4rem; font-size: 0.82rem; }
.sat-history-header { display: grid; grid-template-columns: 7rem 5.5rem 9rem 6rem 4.5rem 1fr; gap: 0.25rem; padding: 0.25rem 0.4rem; font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; border-bottom: 1px solid var(--border); }
.sat-history-row { display: grid; grid-template-columns: 7rem 5.5rem 9rem 6rem 4.5rem 1fr; gap: 0.25rem; padding: 0.35rem 0.4rem; font-size: 0.82rem; border-bottom: 1px solid var(--border-faint, rgba(255,255,255,0.04)); }
.sat-history-row:hover { background: var(--bg-hover, rgba(255,255,255,0.02)); }
.sat-col-type { font-weight: 500; }
.sat-type-lrpt { color: #b39ddb; }
.sat-latest-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 0.4rem; padding: 0.6rem 0.75rem; }
.sat-latest-card .sat-latest-title { font-size: 0.82rem; font-weight: 600; margin-bottom: 0.25rem; }
.sat-latest-card .sat-latest-meta { font-size: 0.78rem; color: var(--text-muted); }
.sat-pred-header { display: grid; grid-template-columns: 6rem 1fr 4.5rem 5rem 6rem; gap: 0.25rem; padding: 0.25rem 0.4rem; font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.03em; border-bottom: 1px solid var(--border); }
.sat-pred-row { display: grid; grid-template-columns: 6rem 1fr 4.5rem 5rem 6rem; gap: 0.25rem; padding: 0.35rem 0.4rem; font-size: 0.82rem; border-bottom: 1px solid var(--border-faint, rgba(255,255,255,0.04)); }
.sat-pred-row:hover { background: var(--bg-hover, rgba(255,255,255,0.02)); }
.sat-pred-col-time { font-variant-numeric: tabular-nums; color: var(--text-muted); }
.sat-pred-col-sat { font-weight: 500; }
.sat-pred-col-el { font-variant-numeric: tabular-nums; text-align: right; }
.sat-pred-col-dur { font-variant-numeric: tabular-nums; color: var(--text-muted); }
.sat-pred-col-dir { color: var(--text-muted); }
.sat-pred-el-high { color: var(--accent-green); font-weight: 600; }
.sat-pred-el-mid { color: #f0a020; }
.sat-pred-el-low { color: var(--text-muted); }
.sat-pred-section-title { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); padding: 0.5rem 0.4rem 0.15rem; }
.sat-pred-header-current { grid-template-columns: 1fr 4.5rem 6rem 6rem 5.5rem 6rem; }
.sat-pred-row-current { display: grid; grid-template-columns: 1fr 4.5rem 6rem 6rem 5.5rem 6rem; gap: 0.25rem; padding: 0.35rem 0.4rem; font-size: 0.82rem; border-bottom: 1px solid var(--border-faint, rgba(255,255,255,0.04)); }
.sat-pred-row-current:hover { background: var(--bg-hover, rgba(255,255,255,0.02)); }
.sat-pred-col-countdown { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--accent-green); }
.sat-pred-current-empty { padding: 0.5rem 0.4rem; font-size: 0.8rem; color: var(--text-muted); }
@media (max-width: 600px) {
.sat-live-grid { grid-template-columns: 1fr; }
.sat-history-header, .sat-history-row { grid-template-columns: 5rem 4rem 6rem 4rem 3.5rem 1fr; font-size: 0.75rem; }
.sat-pred-header, .sat-pred-row { grid-template-columns: 5.5rem 1fr 4rem 4.5rem; font-size: 0.75rem; }
.sat-pred-header-current, .sat-pred-row-current { grid-template-columns: 1fr 4rem 5rem 5rem 4.5rem; font-size: 0.75rem; }
.sat-pred-col-dir { display: none; }
}
/* ── Statistics panel ──────────────────────────────────────────────── */
#tab-statistics {
display: flex;
flex-direction: column;
gap: 1rem;
}
.stats-controls {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: flex-end;
}
.stats-control-group {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stats-control-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.stats-select {
padding: 0.35rem 0.5rem;
border-radius: 0.5rem;
border: 1px solid var(--border-light);
background: var(--input-bg);
color: var(--text);
font: inherit;
font-size: 0.82rem;
}
.stats-counters {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 0.65rem;
}
.stats-counter-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.9rem 0.7rem;
border-radius: 0.75rem;
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent);
background:
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 90%, transparent), color-mix(in srgb, var(--input-bg) 82%, transparent));
box-shadow: inset 0 1px 0 color-mix(in srgb, white 5%, transparent);
}
.stats-counter-value {
font-size: 1.6rem;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: var(--text-heading);
line-height: 1.1;
}
.stats-counter-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
}
.stats-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.9rem 1rem 1rem;
border-radius: 0.8rem;
border: 1px solid color-mix(in srgb, var(--border-light) 76%, transparent);
background:
linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 90%, transparent), color-mix(in srgb, var(--input-bg) 82%, transparent));
box-shadow: inset 0 1px 0 color-mix(in srgb, white 5%, transparent);
}
.stats-section-head {
display: flex;
flex-direction: column;
gap: 0.18rem;
}
.stats-section-title {
font-size: 0.85rem;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-heading);
}
.stats-section-subtitle {
font-size: 0.78rem;
color: var(--text-muted);
}
.stats-bar-chart {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.stats-bar-row {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.82rem;
}
.stats-bar-label {
min-width: 5.5rem;
text-align: right;
font-weight: 600;
font-size: 0.78rem;
color: var(--text);
flex-shrink: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.stats-bar-track {
flex: 1;
height: 1.15rem;
border-radius: 0.4rem;
background: color-mix(in srgb, var(--input-bg) 84%, transparent);
overflow: hidden;
position: relative;
}
.stats-bar-fill {
height: 100%;
border-radius: 0.4rem;
min-width: 2px;
transition: width 300ms ease;
}
.stats-bar-count {
min-width: 3.2rem;
font-variant-numeric: tabular-nums;
font-size: 0.78rem;
color: var(--text-muted);
flex-shrink: 0;
}
.stats-bar-empty {
padding: 0.85rem 0.9rem;
border-radius: 0.75rem;
border: 1px dashed color-mix(in srgb, var(--border-light) 76%, transparent);
background: color-mix(in srgb, var(--input-bg) 84%, transparent);
color: var(--text-muted);
font-size: 0.82rem;
}
@media (max-width: 600px) {
#tab-statistics { padding: 0.75rem; }
.stats-counters { grid-template-columns: repeat(2, 1fr); }
.stats-counter-value { font-size: 1.3rem; }
.stats-bar-label { min-width: 4rem; font-size: 0.75rem; }
.stats-section { padding: 0.8rem 0.85rem 0.9rem; }
}
/* --- Containment for off-screen/inactive content --- */
[data-tab]:not(.active) {
contain: content;
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}
.spectrum-container, .waterfall-container {
contain: strict;
}
#tab-map:not(.active) {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
#tab-statistics:not(.active) {
content-visibility: auto;
contain-intrinsic-size: auto 400px;
}
/* --- Container queries for responsive components --- */
.controls-tray { container-type: inline-size; container-name: controls; }
.decode-history-table-wrap { container-type: inline-size; container-name: decode-table; }
@container controls (max-width: 600px) {
.controls-tray .controls-row { flex-wrap: wrap; }
}
@container decode-table (max-width: 500px) {
.decode-history-table th:nth-child(n+4),
.decode-history-table td:nth-child(n+4) { display: none; }
}
/* --- Re-enable backdrop-filter blur for users who prefer full effects --- */
@media (prefers-reduced-motion: no-preference) {
[data-effects="full"] .tab-bar {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
[data-effects="full"] .controls-tray {
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
[data-effects="full"] .freq-overlay {
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
[data-effects="full"] .shortcut-overlay-card {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
[data-effects="full"] .decode-history-overlay {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
[data-effects="full"] .mobile-bottom-nav {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
}
/* ── Panel-switch fade ────────────────────────────────────────────────
Panels are shown by clearing inline display (app.js), so this fires on
every tab/sub-tab change. Opacity-only to avoid transforming canvases
and the Leaflet map. Gated so reduced-motion users get instant switches. */
@media (prefers-reduced-motion: no-preference) {
.tab-panel,
.sub-tab-panel {
animation: trx-fade-in var(--dur-base) var(--ease-out);
}
}
@keyframes trx-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@media (max-width: 640px) {
button, input, select, textarea, .tab-label, .sub-tab,
.label, .hint, .subtitle, .wfm-control-label, .wfm-intf-val,
.ft8-header, .ft8-message, .aprs-packet, .ais-message, .vdes-message {
font-size: max(0.75rem, 12px);
}
}
/* --- Top bar overflow menu ------------------------------------------------ */
.top-bar-more { position: relative; }
.top-bar-more[hidden] { display: none; }
.top-bar-more-btn {
width: 2rem;
height: 2rem;
min-height: 2rem;
padding: 0;
font-size: 1rem;
line-height: 1;
}
.top-bar-more-menu {
position: absolute;
top: calc(100% + 0.35rem);
right: 0;
z-index: 40;
display: none;
flex-direction: column;
gap: 0.4rem;
min-width: 12rem;
padding: 0.5rem;
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
background: var(--surface);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
}
.top-bar-more-menu.is-open { display: flex; }
.top-bar-more-menu > * { width: 100%; }
.top-bar-more-menu select { width: 100%; max-width: none; }
/* Page title for destinations reached through the Tools menu. The tab strip
* cannot show which of them is active, so each states its own name.
*
* The margin is the spacing on the plain block panels; #tab-statistics is a
* flex column with its own `gap`, where the margin lands on top of that gap
* and doubles the space under the title. */
.section-heading {
margin: 0 0 var(--space-3);
font-size: 1.15rem;
font-weight: 700;
line-height: 1.2;
color: var(--text);
}
#tab-statistics > .section-heading { margin-bottom: 0; }