Files
trx-rs/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
T
sjgandClaude Opus 5 a19633e81f
CI / lint (push) Successful in 2m21s
CI / test (push) Successful in 8m37s
CI / frontend (push) Successful in 4m45s
CI / reuse (push) Successful in 5s
[fix](trx-frontend-http): rebuild the background decode panel around one list
The panel listed the same bookmarks twice: a status card at the top naming
the selected ones and what each was doing, and a checklist below naming all
of them with the checkboxes.  Neither list said which row in the other it
meant, so choosing what to decode and reading what was being decoded were two
passes over the same names.  The checklist itself sat squeezed against the
right-hand edge, because the row it lives in sets `align-items: flex-end` and
an inline `flex-direction: column` turned that into "push everything right".

It is one list now.  A row carries its checkbox, its name, its frequency,
mode and decoder, and its own state, so picking and watching happen in the
same place.  What the rig can hear moves up beside the switch, where it
explains why a selected bookmark reads out of span, and the selection adds
itself up under the list.  The states lose the ✓/△/✗ they carried next to a
dot that already said as much, and say what they mean: "Out of span",
"Scheduler has it", "Nobody listening", each with the reason on hover.  The
empty list now says which of the two reasons it is empty for, and what to do.

Save was live from the moment the panel opened, with nothing to save; it now
offers itself only when something has changed.

Two races behind it, both of which left the panel useless rather than ugly:

  * The rig was whatever the panel was handed at load.  Loading before the rig
    list arrived handed it null, and the next telling only came when the
    operator switched rigs — so the panel stayed empty and silent.  Every rig
    list refresh now passes the rig on, and both panels ignore one they have.

  * The settings panels are wired once the session is up, but their modules
    import asynchronously and the wiring skipped whichever had not arrived.
    A panel that missed it kept dead buttons for the rest of the session: no
    filter, no Select All, no Save.  Wiring runs again when the modules land.

Closes #52

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SyX26FCpMQxiBoC7r5K1A7
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-08-07 19:54:36 +02:00

6391 lines
187 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;
}
/* Square icon buttons in the top bar: an icon with no box of its own has no
size to draw at, so the button gives it one. */
.header-bar-btn.header-audio-btn,
.header-bar-btn.header-share-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,
.header-share-btn svg {
width: 100%;
height: 100%;
}
/* The link icon is drawn in strokes, not fills, and wants room around it. */
.header-share-btn svg {
padding: 1px;
}
.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; }
/* Only the bottom nav is short of room. */
.tab-label-short { display: none; }
.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);
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.
It keeps both anchors and shrinks inside them, rather than dropping `left`
and being sized by shrink-to-fit: the anchored box is a size the browser
already knows, and an absolutely positioned, backdrop-filtered, composited
box being asked to size itself from its content is the shape of thing that
renders as nothing on engines other than the one it was written against. */
.map-overlay-panel.filters-hidden {
width: fit-content;
margin-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);
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. Satellites, 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="satellites"],
.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; }
/* A grid column sizes to its content by default, so one row wider than the
phone — the mode picker, six buttons across — dragged the whole tray out
with it and left the rest to be found by scrolling sideways. `minmax(0,
1fr)` lets the column be as narrow as the screen and the rows wrap inside
it. */
.controls-tray {
width: 100%;
min-width: 0;
grid-template-columns: minmax(0, 1fr);
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; }
.tab[data-tab="satellites"], .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);
}
/* The bottom nav divides the bar between its five destinations, so a tab is
about a fifth of the screen and the label has to live inside that. Sharing
the width evenly beats sizing each tab to its own label, which gave
"Digital modes" the same room as "Map" and clipped it mid-word. */
.tab-bar-nav .tab {
flex: 1 1 0;
min-width: 0;
padding-left: 0.15rem;
padding-right: 0.15rem;
}
.tab .tab-label, .tab[data-tab] .tab-label {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 0.75rem;
}
/* The long ones, sized against the screen rather than at a fixed size.
A tab is a fifth of the viewport, so what fits in it is a function of the
viewport — and only roughly of the font size, since how wide a platform
renders a word varies by ten per cent or more between machines. A fixed
0.6rem fit here with ten pixels to spare and clipped in CI.
(These rules also used to sit above the one that sets the size for all
labels — identical specificity, later in the file, so nothing was ever
shortened.) */
.tab[data-tab="bookmarks"] .tab-label,
.tab[data-tab="statistics"] .tab-label {
font-size: clamp(0.46rem, 2.2vw, 0.62rem);
}
/* "Digital modes" does not fit at any size worth reading, so the nav shows
the short form and the button carries the full one as its label. */
.tab[data-tab="digital-modes"] .tab-label { display: none; }
.tab[data-tab="digital-modes"] .tab-label-short {
display: block;
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: clamp(0.46rem, 2.2vw, 0.62rem);
}
.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;
}
/* The rig's name is the widest thing in the bar and it does not need to be:
139 px of a 338 px bar went on it, and everything else was pushed into the
overflow menu to make room. Enough for a short name, ellipsised past that,
with the full name still in the menu it opens. */
.header-rig-switch select,
.header-style-pick select {
width: auto;
min-width: 0;
max-width: 6.5rem;
padding-right: 0.2rem;
font-size: 0.8rem;
text-overflow: ellipsis;
}
.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;
}
/* These are given the width of their column below; with padding on a
content box that is the column's width *plus* the padding, which is what
pushed the page itself sideways by a dozen pixels. */
.wavelength-display,
.sig-strength-display,
.jog-step,
.jog-mult {
box-sizing: border-box;
width: 100%;
}
.sig-strength-display {
min-width: 0;
font-size: 1.05rem;
}
.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-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 control strip ────────────────────────────────────────────
Two clusters of controls under the plot: the receiver's bandwidth on the
left, the display's levels on the right. Everything in here is one height
and one shape — a row whose controls each size themselves reads as a pile
rather than a strip.
The container wraps; a cluster does not. A cluster that cannot fit drops
whole to the next line and starts it left-aligned, rather than splitting and
leaving the cluster beside it floating at a height of its own, which is what
the two rows did to each other between 1100 and 1400 px. */
#spectrum-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
padding: 3px 4px 0;
gap: 0.35rem 0.5rem;
font-size: 0.78rem;
color: var(--text-muted);
}
#spectrum-bw-row,
#spectrum-level-row {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 0.3rem;
}
/* Takes the slack, so the clusters reach the ends of a wide strip without
`space-between` opening a quarter-width hole in the middle of it. */
.spectrum-controls-spacer {
flex: 1 1 1.5rem;
min-width: 0;
}
/* A field is one box: name, value and unit share a border, so a number can
never be read apart from the unit it is in. */
.spectrum-field {
display: inline-flex;
align-items: center;
gap: 0.3rem;
/* Border-box on both field and button, or the button's own border and
padding add to the height and the two end up a couple of pixels apart —
which is the pile-of-controls look this is meant to end. */
box-sizing: border-box;
height: 1.7rem;
padding: 0 0.4rem 0 0.5rem;
border: 1px solid var(--border-light);
border-radius: 6px;
background: var(--input-bg);
white-space: nowrap;
font-size: 0.75rem;
color: var(--text-muted);
}
.spectrum-field:focus-within {
border-color: color-mix(in srgb, var(--accent-green) 45%, var(--border-light));
}
.spectrum-field-name {
font-size: 0.72rem;
color: var(--text-muted);
}
/* Unit and readout hold a fixed, tabular slot: the row must not twitch as a
value goes from 1.0 to 0.9. */
.spectrum-field-unit,
.spectrum-field-value {
font-size: 0.7rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
min-width: 1.7rem;
}
.spectrum-field-value {
color: var(--text);
text-align: right;
}
.spectrum-field input[type="number"] {
width: 3.2rem;
padding: 0;
border: 0;
background: transparent;
color: var(--text);
font-size: 0.78rem;
text-align: right;
font-variant-numeric: tabular-nums;
}
.spectrum-field input[type="number"]:focus {
outline: none;
}
#spectrum-bw-input {
width: 3.6rem;
}
/* The select carries `status-input` for the sake of other layouts, which gives
it a border and a background of its own; inside a field that reads as a box
drawn inside a box. */
#spectrum-controls .spectrum-field select {
width: auto;
height: 1.35rem;
min-height: 0;
padding: 0 0.1rem;
border: 0;
border-radius: 0;
background: transparent;
color: var(--text);
font-size: 0.74rem;
}
.spectrum-field input[type="range"] {
width: 5.5rem;
height: 1.2rem;
cursor: pointer;
}
.spectrum-btn {
box-sizing: border-box;
height: 1.7rem;
min-height: 0;
padding: 0 0.6rem;
border-radius: 6px;
font-size: 0.73rem;
font-weight: 600;
white-space: nowrap;
}
/* Fingers need more than a 27 px target; a mouse does not, and the strip has
no width to spare on a desktop. */
@media (hover: none) and (pointer: coarse) {
.spectrum-field,
.spectrum-btn {
height: 2.4rem;
}
}
/* Narrow screens: the same fields, stacked. This sits after the rules it
overrides — `#spectrum-bw-row` in both places has identical specificity, so
whichever comes last wins, and from inside the earlier media query the
narrow rules silently lost. */
@media (max-width: 640px) {
/* Narrow: the clusters stack, and a field takes the width so its name and
its value sit at opposite ends of a line the thumb can hit. */
#spectrum-controls {
flex-direction: column;
align-items: stretch;
gap: 0.45rem;
padding-top: 0.45rem;
}
#spectrum-bw-row,
#spectrum-level-row {
flex-wrap: wrap;
gap: 0.35rem 0.4rem;
}
.spectrum-controls-spacer {
display: none;
}
#spectrum-controls .spectrum-field {
flex: 1 1 100%;
justify-content: space-between;
}
#spectrum-controls .spectrum-field input[type="number"],
#spectrum-controls .spectrum-field input[type="range"],
#spectrum-controls .spectrum-field select {
flex: 1 1 auto;
min-width: 3rem;
}
#spectrum-controls .spectrum-btn {
flex: 1 1 auto;
}
}
.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.
`nowrap` is the whole point of it. The base rule wraps, and a *column*
that wraps starts a second column when its items are taller than it is —
which put the transmit controls at x=400 on a 390 px screen, off the side
of a tray 354 px wide, reachable only by a horizontal scroll with nothing
to say it was there. */
.controls-row {
flex-direction: column;
flex-wrap: nowrap;
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-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;
}
/* ── "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-intro {
margin: 0 0 0.9rem;
max-width: 62ch;
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.45;
}
/* The switch and what the rig can hear, on one line: the second is the reason
a selected bookmark may not be decoding, so it belongs beside the first. */
.bgd-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-bottom: 0.9rem;
}
.bgd-enable {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
font-weight: 600;
color: var(--text);
cursor: pointer;
}
.bgd-span {
color: var(--text-muted);
font-size: 0.82rem;
}
.bgd-span[data-tone="warn"] {
color: var(--text-warn, #b7791f);
}
/* The filter and the bulk buttons sit on the list they act on, and stay on it
when the panel narrows. */
.bgd-list-toolbar {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.bgd-list-toolbar .bgd-checklist-filter {
flex: 1 1 auto;
min-width: 0;
}
.bgd-checklist {
max-height: 22rem;
overflow-y: auto;
border: 1px solid var(--border-light);
border-radius: 0.4rem;
background: var(--bg-secondary);
}
/* One row per bookmark: pick it here, and read what it is doing here too. */
.bgd-checklist-row {
display: grid;
grid-template-columns: auto minmax(6rem, 1fr) auto auto;
align-items: center;
gap: 0.55rem;
padding: 0.5rem 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.is-selected {
background: color-mix(in srgb, var(--accent-green) 8%, transparent);
}
.bgd-checklist-row input[type="checkbox"] {
flex-shrink: 0;
}
.bgd-checklist-name {
font-weight: 600;
color: var(--text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.bgd-checklist-meta {
display: inline-flex;
align-items: baseline;
gap: 0.4rem;
color: var(--text-muted);
font-size: 0.78rem;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.bgd-checklist-mode {
color: var(--text-muted);
opacity: 0.85;
}
/* A dot and a word. The dot carries the state, so the word can stay a word. */
.bgd-state {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 0.35rem;
min-width: 9.5rem;
color: var(--text-muted);
font-size: 0.76rem;
white-space: nowrap;
}
.bgd-state-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: currentcolor;
opacity: 0.55;
flex-shrink: 0;
}
.bgd-state[data-state="active"] {
color: var(--accent-green);
font-weight: 600;
}
.bgd-state[data-state="active"] .bgd-state-dot {
opacity: 1;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-green) 22%, transparent);
}
.bgd-state[data-state="missing_bookmark"],
.bgd-state[data-state="no_supported_decoders"] {
color: var(--color-error, #c0392b);
}
.bgd-state[data-state="unselected"] {
min-width: 0;
}
.bgd-checklist-empty {
padding: 1rem 0.75rem;
text-align: center;
color: var(--text-muted);
font-size: 0.85rem;
line-height: 1.5;
}
/* What the selection adds up to, under the list it describes. */
.bgd-summary {
margin-top: 0.5rem;
color: var(--text-muted);
font-size: 0.8rem;
}
/* ── Select all / none buttons ─────────────────────────────────────── */
.bgd-select-actions {
display: flex;
flex-shrink: 0;
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);
}
@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 {
flex-direction: column;
align-items: stretch;
}
.bgd-add-row button {
width: 100%;
}
.interleave-ring-label {
max-width: 8rem;
}
/* Two lines rather than four columns: the name and its state stay legible
when there is no room for the frequency beside them. */
.bgd-checklist-row {
grid-template-columns: auto minmax(0, 1fr);
row-gap: 0.2rem;
}
.bgd-checklist-meta,
.bgd-state {
grid-column: 2;
justify-content: flex-start;
min-width: 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; }
/* Wrapping a narrow tray's columns onto more lines is right while the row runs
left to right. Below the phone breakpoint the row is a column, and wrap there
means "start another column" — which is how the transmit controls ended up
beside the tray instead of under it, off the side of the screen. This rule
outranks the phone one (two classes to its one) and sits later in the file,
so it has to exclude itself rather than be overridden. */
@media (min-width: 521px) {
@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; }