:root {
    --bg-color: #0f172a;
    --card-bg: rgba(30, 41, 59, 0.7);
    --accent-color: #38bdf8;
    --text-primary: #f8fafc;
    --text-secondary: #94a3b8;
    --tx-active: #ef4444;
    --glass-border: rgba(255, 255, 255, 0.1);
    /* Control boundary: >=3:1 non-text contrast against the dark surfaces
       (WCAG 2.1 SC 1.4.11) for form fields and buttons. */
    --control-border: #94a3b8;
}

* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

/* Visually hidden but available to screen readers (for input labels). */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

/* Skip link: off-screen until focused, then slides into view. */
.skip-link {
    position: fixed;
    top: -4rem;
    left: 0.75rem;
    z-index: 2000;
    padding: 0.5rem 1rem;
    background: var(--accent-color);
    color: var(--bg-color);
    font-weight: 700;
    font-size: 0.85rem;
    text-decoration: none;
    border-radius: 0 0 8px 8px;
    transition: top 0.15s ease;
}
.skip-link:focus {
    top: 0;
}

body {
    background: var(--bg-color);
    color: var(--text-primary);
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    line-height: 1.5;
    min-height: 100vh;
    padding: 2rem;
}

/* Decorative background gradient on a fixed pseudo-element instead of on body:
   axe can't composite text contrast over a CSS gradient, so keeping body a
   solid colour (and painting the gradient behind) lets contrast be measured. */
body::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background: radial-gradient(circle at top right, #1e293b, #0f172a);
}

.container {
    max-width: 1200px;
    margin: 0 auto;
}

header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 3.5rem;
    padding: 1.5rem 2rem;
    /* The blur was removed here on purpose. This header contains .status-dot,
     * whose pulse-glow animation never stops, and animating anything inside a
     * backdrop-filter region forces the whole region to be re-rasterized and
     * re-blurred every frame. The 0.4-alpha background over a dark body is
     * visually equivalent without it. If the glass look is ever restored here,
     * the pulse must move out of this subtree first. */
    background: rgba(30, 41, 59, 0.4);
    border: 1px solid var(--glass-border);
    border-radius: 20px;
    position: relative;
    z-index: 50;
    box-shadow: 0 4px 20px -5px rgba(0, 0, 0, 0.3);
}

.logo-section {
    display: flex;
    align-items: center;
    gap: 1.5rem;
}

.logo-section img {
    height: 65px;
    width: auto;
    filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.3));
    transition: transform 0.3s ease;
}

.logo-section img:hover {
    transform: scale(1.05);
}

h1 {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.025em;
}

.status-badge {
    background: rgba(15, 23, 42, 0.6);
    padding: 0.6rem 1.25rem;
    border-radius: 12px;
    border: 1px solid rgba(255, 255, 255, 0.05);
    font-size: 0.8rem;
    font-family: 'JetBrains Mono', 'Fira Code', monospace;
    display: flex;
    align-items: center;
    gap: 1rem;
    color: var(--text-secondary);
}


/* Pulsing "live" dot.
 *
 * A never-ending keyframe animation is enough on its own to hold the compositor
 * at 60fps indefinitely: the page can never reach a true idle state, which is
 * what Chrome's GPU process was showing. So the pulse no longer runs forever --
 * it is retriggered briefly (see .status-dot.pulse in index.php) each time a
 * poll actually brings new data, then settles to a static glow. An idle
 * reflector therefore costs no frames at all, and an active one still reads as
 * live because the dot flashes on each update.
 *
 * Only opacity and transform are animated, so the pulse never triggers a
 * paint, and the header no longer carries a backdrop-filter (see the note on
 * the header rule) so there is no blurred backdrop to re-blur.
 */
.status-dot {
    width: 6px;
    height: 6px;
    background: #10b981;
    border-radius: 50%;
    box-shadow: 0 0 12px #10b981;
}

/* One-shot flash, retriggered by removing/re-adding the class. */
.status-dot.pulse {
    animation: pulse-glow 600ms ease-out 1;
}

@keyframes pulse-glow {
    0%   { opacity: 0.45; transform: scale(1); }
    35%  { opacity: 1;    transform: scale(1.35); }
    100% { opacity: 1;    transform: scale(1); }
}

.callsign-container {
    position: relative;
    display: inline-block;
}

.callsign-tooltip {
    position: absolute;
    top: 120%;
    left: 50%;
    transform: translateY(-10px) translateX(-50%);
    background: rgba(15, 23, 42, 0.95);
    /* No backdrop-filter here. The background is already 95% opaque, so a blur
       behind it is visually imperceptible, but each of the 66 tooltips on a full
       last-heard table cost the GPU a real blur layer. Dropped deliberately:
       zero visual change, 66 fewer layers. */
    border: 1px solid var(--glass-border);
    padding: 0.6rem 1rem;
    border-radius: 10px;
    z-index: 1000;
    width: max-content;
    max-width: 250px;
    pointer-events: none;
    opacity: 0;
    visibility: hidden;
    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
    box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
    color: #fff;
    font-size: 0.75rem;
    line-height: 1.4;
    text-align: center;
}

.callsign-container:hover .callsign-tooltip {
    opacity: 1;
    visibility: visible;
    transform: translateY(0) translateX(-50%);
}

.callsign-tooltip::after {
    content: '';
    position: absolute;
    bottom: 100%;
    left: 50%;
    margin-left: -5px;
    border-width: 5px;
    border-style: solid;
    border-color: transparent transparent var(--glass-border) transparent;
}
.grid {
    display: grid;
    grid-template-columns: 1fr 300px;
    gap: 2rem;
}

/* Grid items default to min-width:auto, which lets a nowrap table's intrinsic
 * width blow the 1fr track past the viewport on mobile. min-width:0 lets the
 * track shrink so the inner .table-wrapper / overflow:auto can scroll instead. */
.grid > * {
    min-width: 0;
}

@media (max-width: 900px) {
    .grid {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 600px) {
    body {
        padding: 1rem;
    }
    header {
        flex-direction: column;
        align-items: stretch;
        padding: 1.25rem;
        gap: 1.25rem;
        border-radius: 12px;
    }
    .logo-section {
        flex-direction: column;
        align-items: flex-start;
        gap: 1rem;
    }
    .logo-section img {
        height: 40px;
    }
    .status-badge {
        justify-content: space-between;
        flex-wrap: wrap;
    }
    .card {
        padding: 1rem;
    }
    .card-title {
        flex-wrap: wrap;
        gap: 0.5rem;
    }
    .table-wrapper {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        margin: 0 -1rem; /* Pulls the scrollbar slightly to the edge of the card */
        padding: 0 1rem;
    }
    th, td {
        white-space: nowrap;
        padding-right: 1rem;
    }
}

.card {
    background: var(--card-bg);
    backdrop-filter: blur(12px);
    border: 1px solid var(--glass-border);
    border-radius: 16px;
    padding: 1.5rem;
    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}

.card-title {
    font-size: 1.125rem;
    font-weight: 600;
    margin-bottom: 1.25rem;
    color: var(--accent-color);
    display: flex;
    justify-content: space-between;
}

table {
    width: 100%;
    border-collapse: collapse;
}

th {
    text-align: left;
    color: var(--text-secondary);
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    padding: 0 0.75rem 1rem 0;
}

td {
    padding: 0.75rem 0.75rem 0.75rem 0;
    border-top: 1px solid var(--glass-border);
    font-size: 0.875rem;
}

/* The active-transmission row carries a 3px mode-coloured bar as border-left on
 * the <tr>. Global `td` padding-left is 0, so that bar sat flush against the
 * first cell's content -- the mode badge (YSF/M17) butted straight into it with
 * a 0px gap, which read as visual tension and cramped alignment.
 *
 * Indent just the first cell of that row so the badge clears the bar. Scoped to
 * .tx-active-row on purpose: adding padding to every td would shift all columns
 * right and re-introduce the horizontal clipping the aside tables were tuned to
 * avoid. */
.tx-active-row > td:first-child {
    padding-left: 0.5rem;
}

/* The aside tables sit in a narrow column; trim their horizontal padding so
   all columns fit without being clipped. */
aside th,
aside td {
    padding-right: 0.4rem;
}

/* Active-transmission row. This one only ever exists while a transmission is
 * running, so a loop here is not a permanent GPU tax the way the status dot was
 * -- but it is still bounded and paused when the tab is hidden. */
.tx-active-row {
    font-weight: 600;
    animation: pulse 1.6s infinite;
    will-change: opacity;
}

@keyframes pulse {
    0% {
        opacity: 1;
    }

    50% {
        opacity: 0.7;
    }

    100% {
        opacity: 1;
    }
}

footer {
    margin-top: 2rem;
    text-align: center;
    color: var(--text-secondary);
    font-size: 0.75rem;
}

.callsign {
    font-family: 'Roboto Mono', monospace;
    color: var(--accent-color);
}

.badge {
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 0.75rem;
    background: rgba(56, 189, 248, 0.2);
}

.badge-p25 {
    background: rgba(255, 204, 51, 0.2);
    color: #FFCC33;
}

.badge-nxdn {
    background: rgba(51, 204, 255, 0.2);
    color: #33CCFF;
}

.badge-ysf {
    background: rgba(255, 138, 61, 0.2);
    color: #FF8A3D;
}

.badge-m17 {
    background: rgba(179, 128, 255, 0.2);
    color: #B380FF;
}

.badge-dmr {
    background: rgba(51, 153, 255, 0.2);
    color: #3399FF;
}
/* Reflector Switcher */
.switcher-dropdown {
    position: relative;
    display: inline-block;
}
.switcher-content {
    display: none;
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 200px;
    background: rgba(15, 23, 42, 0.98);
    backdrop-filter: blur(12px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    border-radius: 8px;
    z-index: 1000;
    margin-top: 0;
    padding: 0.5rem 0;
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
}
.switcher-dropdown:hover .switcher-content {
    display: block;
}
.switcher-content a {
    color: var(--text-secondary);
    padding: 0.75rem 1rem;
    text-decoration: none;
    display: block;
    font-size: 0.875rem;
    transition: all 0.2s;
}
.switcher-content a:hover {
    background: rgba(255, 255, 255, 0.05);
    color: var(--accent-primary);
}
.switcher-content a.active {
    color: var(--accent-primary);
    font-weight: 600;
}

/* --- Hidden-tab animation pause ------------------------------------------
 * Infinite CSS animations keep ticking on every frame even when the tab is in
 * the background. The compositor usually skips presenting those frames, but the
 * animation still drives style recalc, and inside a backdrop-filter region it
 * can still force backdrop work. index.php sets html.is-idle on
 * visibilitychange, so the tx-active-row loop stops while the tab is hidden and
 * resumes with the data on return.
 */
html.is-idle .tx-active-row {
    animation-play-state: paused;
}

/* Respect the OS-level motion preference outright. */
@media (prefers-reduced-motion: reduce) {
    .status-dot,
    .tx-active-row {
        animation: none;
    }
}
