/* SPDX-FileCopyrightText: 2026 Stan Grams 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