:root {
    --bg: #0f1115;
    --panel: #171a21;
    --text: #e8eaed;
    --muted: #9aa0a6;
    --accent: #4f8cff;
    --danger: #e05252;
    --ok: #3ccb7f;
    --warn: #e0b752;
    --border: #262a33;
}

* { box-sizing: border-box; }

/* Custom scrollbars everywhere (rail list, admin tables, page body, etc.) instead of each
   browser/OS's default bar, which looks out of place against this dark theme. */
html {
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
}
::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}
::-webkit-scrollbar-track {
    background: transparent;
}
::-webkit-scrollbar-thumb {
    background-color: var(--border);
    border-radius: 999px;
    border: 2px solid var(--bg);
    background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
    background-color: var(--muted);
    background-clip: padding-box;
}

body {
    margin: 0;
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
}

a { color: var(--accent); }

header.site-header {
    display: flex;
    align-items: center;
    gap: 1.4rem;
    padding: 0.85rem 1.75rem;
    background: rgba(23, 26, 33, 0.85);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 10;
    flex-wrap: wrap;
}

header.site-header h1 {
    font-size: 1.25rem;
    margin: 0;
}

.site-brand {
    font-weight: 800;
    letter-spacing: -0.02em;
    font-size: 1.05rem;
    white-space: nowrap;
    text-decoration: none;
    color: var(--text);
    flex-shrink: 0;
}
.site-brand-accent { color: var(--accent); }

.site-user { color: var(--muted); font-size: 0.82rem; white-space: nowrap; }
.site-logout {
    color: var(--muted) !important;
    text-decoration: none;
    white-space: nowrap;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    border-radius: 8px;
    flex-shrink: 0;
}
.site-logout:hover { color: var(--text) !important; background: rgba(255, 255, 255, 0.08); }
.site-logout svg { display: block; }

.filters {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
    flex: 1;
}

.filters select, .filters input {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.4rem 0.6rem;
}

.header-search {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.45rem 0.7rem;
    font-size: 0.85rem;
    min-width: 0;
    flex: 1;
}
.header-search:focus { outline: none; border-color: var(--accent); }

.fav-toggle {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--muted);
    font-size: 0.85rem;
    cursor: pointer;
    white-space: nowrap;
}
.fav-toggle input { accent-color: var(--accent); width: 15px; height: 15px; cursor: pointer; }

/* Hidden on desktop — the filter selects stay inline in the header there (plenty of room).
   Shown only under the mobile breakpoint below, where they'd otherwise stack into several full
   rows and push the actual channel grid off the first screenful. */
.filters-toggle {
    display: none;
    align-items: center;
    gap: 0.4rem;
    background: var(--panel);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.45rem 0.8rem;
    font-size: 0.82rem;
    cursor: pointer;
    flex-shrink: 0;
}
.filters-toggle svg { width: 15px; height: 15px; flex-shrink: 0; }
.filters-toggle[aria-expanded="true"] { border-color: var(--accent); color: var(--accent); }
.filters-count {
    display: none;
    align-items: center;
    justify-content: center;
    min-width: 1.1rem;
    height: 1.1rem;
    padding: 0 0.3rem;
    border-radius: 999px;
    background: var(--accent);
    color: #fff;
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1;
}
.filters-count.show { display: inline-flex; }

@media (max-width: 720px) {
    header.site-header { gap: 0.6rem; }
    .header-search { flex-basis: 100%; order: 2; }
    .filters-toggle { display: inline-flex; order: 3; }
    .site-user { display: none; }
    /* Collapsed by default — .open (toggled by app.js) is what actually reveals it, as a
       full-width panel that drops below the search bar and toggle button instead of the old
       "every control stacked on its own row" layout eating the whole first screenful. */
    .filters {
        display: none;
        flex-basis: 100%;
        order: 5;
        flex-direction: column;
        gap: 0.6rem;
        padding-top: 0.7rem;
        margin-top: 0.1rem;
        border-top: 1px solid var(--border);
    }
    .filters.open { display: flex; }
    .filters select { width: 100%; }
}

.channel-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 1rem;
    padding: 1.5rem;
}

.channel-card {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 0.75rem;
    text-decoration: none;
    color: var(--text);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    position: relative;
    transition: transform 0.1s ease;
}

.channel-card:hover { transform: translateY(-2px); border-color: var(--accent); }

.channel-card img {
    width: 64px;
    height: 64px;
    object-fit: contain;
}

.channel-card .name {
    font-size: 0.85rem;
    text-align: center;
}

.badge {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
}
.badge.working { background: var(--ok); }
.badge.dead { background: var(--danger); }
.badge.geo_blocked, .badge.http_blocked, .badge.cors_blocked { background: var(--warn); }
.badge.unknown { background: var(--muted); }

.fav-star {
    position: absolute;
    top: 6px;
    left: 6px;
    background: none;
    border: none;
    color: var(--muted);
    font-size: 1.1rem;
    cursor: pointer;
}
.fav-star.active { color: var(--warn); }

.empty-state {
    padding: 3rem;
    text-align: center;
    color: var(--muted);
}

/* Watch page */
.watch-body {
    background:
        radial-gradient(ellipse 80% 60% at 50% 0%, rgba(79, 140, 255, 0.08), transparent),
        var(--bg);
    min-height: 100vh;
}

.watch-header { gap: 0.9rem; }

.back-link {
    color: var(--muted);
    text-decoration: none;
    font-size: 0.85rem;
    white-space: nowrap;
}
.back-link:hover { color: var(--text); }

.watch-title {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-width: 0;
    flex: 1;
}

/* Narrow viewports: the title was shrinking to near-nothing fighting brand/back-link/user for
   room on one line — give it a guaranteed full-width row instead of letting flexbox squeeze it. */
@media (max-width: 640px) {
    .watch-header .watch-title { flex-basis: 100%; order: 3; }
    .watch-header .site-user { display: none; }
}

.watch-logo {
    width: 28px;
    height: 28px;
    object-fit: contain;
    border-radius: 4px;
}

.watch-title h1 {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.watch-main {
    max-width: 1400px;
    margin: 0 auto;
    padding: 2rem 1.5rem;
}

.watch-layout {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.watch-player-col { min-width: 0; }

/* Rail: horizontal scroll strip below the player by default (also the mobile/narrow layout). */
.watch-rail {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    min-width: 0;
}

.rail-search {
    background: var(--panel);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.55rem 0.8rem;
    font-size: 0.85rem;
    flex-shrink: 0;
}
.rail-search:focus { outline: none; border-color: var(--accent); }

.rail-filters {
    display: flex;
    gap: 0.5rem;
    flex-shrink: 0;
}
.rail-filter-select {
    flex: 1;
    min-width: 0;
    background: var(--panel);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 0.5rem 0.5rem;
    font-size: 0.78rem;
}
.rail-filter-select:focus { outline: none; border-color: var(--accent); }

.rail-list {
    display: flex;
    flex-direction: row;
    gap: 0.6rem;
    overflow-x: auto;
    overflow-y: hidden;
    padding-bottom: 0.4rem;
}

.rail-item {
    flex: 0 0 auto;
    width: 108px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    padding: 0.6rem 0.5rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    text-decoration: none;
    color: var(--text);
    text-align: center;
    position: relative;
    transition: border-color 0.15s ease, transform 0.15s ease;
}
.rail-item:hover { border-color: var(--accent); transform: translateY(-2px); }
.rail-item.active { border-color: var(--accent); background: rgba(79, 140, 255, 0.1); }
.rail-item img {
    width: 44px;
    height: 44px;
    object-fit: contain;
}
.rail-item .rail-item-name {
    font-size: 0.72rem;
    line-height: 1.25;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
}
.rail-item .badge { top: 4px; right: 4px; }

/* Wide viewports: rail becomes a sticky sidebar next to the player instead of a strip below it. */
@media (min-width: 960px) {
    /* Height is pinned to the player's actual rendered height by JS (watch-rail.js's
       syncRailHeight()), not flexbox stretch — the rail's own content (however many channels)
       must never be able to inflate the row past the player's height. align-items stays at the
       flex-start default so the player column keeps its own natural, aspect-ratio-driven size. */
    .watch-layout { flex-direction: row; }
    .watch-player-col { flex: 1 1 auto; }
    .watch-rail {
        flex: 0 0 300px;
        position: sticky;
        top: 1.25rem;
        max-height: calc(100vh - 2.5rem);
    }
    .rail-list {
        flex: 1 1 auto;
        min-height: 0;
        flex-direction: column;
        overflow-x: hidden;
        overflow-y: auto;
    }
    .rail-item {
        width: auto;
        flex-direction: row;
        text-align: left;
        justify-content: flex-start;
    }
    .rail-item img { width: 32px; height: 32px; flex-shrink: 0; }
    .rail-item .rail-item-name { -webkit-line-clamp: 1; font-size: 0.8rem; }
    .rail-item .badge { top: 50%; transform: translateY(-50%); right: 6px; }
    .rail-item:hover { transform: none; }
}

.player-wrap {
    position: relative;
    background: #000;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 0 0 1px var(--border), 0 20px 60px -20px rgba(0, 0, 0, 0.8);
}

/* CSS-only fullscreen fallback for platforms where Element.requestFullscreen() on a plain div
   isn't reliable (iOS Safari, notably, for the YouTube-in-an-iframe case) — see player.js's
   enterCssFullscreen(). Fixed positioning + a high z-index visually fills the viewport the same
   way real fullscreen would, without depending on the browser actually supporting the API for
   this element. !important overrides the <video> tag's own inline width/height/aspect-ratio. */
.player-wrap.css-fullscreen {
    position: fixed;
    inset: 0;
    z-index: 1000;
    width: 100vw !important;
    height: 100vh !important;
    border-radius: 0;
}
.player-wrap.css-fullscreen video,
.player-wrap.css-fullscreen #youtube-wrap {
    width: 100% !important;
    height: 100% !important;
    aspect-ratio: unset !important;
}
/* Prevents the page behind from scrolling while the CSS fallback is active — real fullscreen
   does this automatically, so this just matches that same expectation. */
html.css-fullscreen-lock, html.css-fullscreen-lock body {
    overflow: hidden;
}

.live-badge {
    position: absolute;
    top: 14px;
    left: 14px;
    z-index: 5;
    display: flex;
    align-items: center;
    gap: 0.35rem;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(4px);
    color: #ff5c5c;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    padding: 0.3rem 0.6rem;
    border-radius: 999px;
    pointer-events: none;
}

.player-overlay {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.9rem;
    text-align: center;
    padding: 2rem;
    background: rgba(10, 11, 15, 0.82);
    backdrop-filter: blur(2px);
    color: var(--text);
}
.player-overlay.show { display: flex; }
.player-overlay.error { color: #ffb3b3; }
.player-overlay p { margin: 0; max-width: 26rem; font-size: 0.9rem; line-height: 1.5; }
/* Tappable in every non-error state (see player.js's overlay click -> togglePlay()) — mainly
   for when autoplay is blocked (iOS Safari far more aggressively than desktop) and playback
   needs an explicit tap to start; harmless before that too, since togglePlay() no-ops safely
   until the adapter exists. Error states aren't tappable — there's nothing to start. */
.player-overlay:not(.error) { cursor: pointer; }
.player-overlay.tap-to-play { color: var(--accent); }

.player-overlay-spinner {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: 3px solid rgba(255, 255, 255, 0.15);
    border-top-color: var(--accent);
    animation: spin 0.8s linear infinite;
}
.player-overlay.error .player-overlay-spinner { display: none; }
/* Idle: waiting for the admin to pick something to preview (admin/lineup.php) — same overlay,
   just without implying something is actively loading. */
.player-overlay.idle .player-overlay-spinner { display: none; }

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* One shared control bar for both playback backends (HLS via video.js and manually-added
   YouTube channels via the IFrame Player API) — see player.js's adapter pattern. video.js's own
   native control bar is disabled (controls:false) so both backends look and behave identically
   instead of YouTube getting a custom bar while HLS keeps video.js's default skin. The capture
   layer sits on top of whichever backend is active so clicks/right-clicks land on our own
   (same-origin) element — for YouTube specifically that's what keeps its right-click "Copy video
   URL"/"Copy embed code" menu from ever being reachable. */
#youtube-wrap { position: relative; }
/* The YouTube IFrame API defaults its iframe to a fixed 640x390px box, unlike the HLS <video>
   element which fills the player via width:100%/aspect-ratio — without this it renders small
   and top-left-anchored instead of matching the rest of the player chrome. YT.Player() replaces
   the #yt-mount div in place with the iframe itself (same id, not a nested child), so this rule
   targets #yt-mount directly rather than "#yt-mount iframe". */
#yt-mount {
    position: absolute;
    inset: 0;
    width: 100% !important;
    height: 100% !important;
    display: block;
    border: 0;
}
.player-capture {
    position: absolute;
    inset: 0;
    z-index: 2;
    cursor: pointer;
    background: transparent;
    align-items: center;
    justify-content: center;
}
/* YouTube shows its own title card + a suggested-video thumbnail over the video whenever
   playback is paused, even with controls hidden — there's no player-var to suppress that. An
   opaque backdrop is the only way to actually hide it, not just the control bar; applying the
   same backdrop when an HLS stream is paused keeps the two backends looking identical. */
.player-capture.paused {
    display: flex;
    background: rgba(10, 11, 15, 0.92);
}
.player-paused-badge {
    display: none;
    width: 64px;
    height: 64px;
    border-radius: 50%;
    align-items: center;
    justify-content: center;
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
}
.player-capture.paused .player-paused-badge { display: flex; }
.player-paused-badge svg { width: 26px; height: 26px; margin-left: 3px; }
.player-controls {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    /* Above .player-overlay (z-index 4) — the overlay covers the whole player box (loading
       spinner, error message, "no working stream" text, etc.), and being under it meant its
       higher stacking order won every hit-test in the same region: Next/Previous/Play/Mute all
       silently did nothing any time the overlay was showing, not just during a genuine error —
       the loading spinner blocked them too, just less noticeably since that state is normally
       brief. Controls need to stay usable (e.g. skip to the next channel) precisely when
       playback has failed, so this always wins regardless of overlay state. */
    z-index: 6;
    display: flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.6rem 0.7rem;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
    opacity: 0;
    transition: opacity 0.15s ease;
    pointer-events: none;
}
.player-wrap:hover .player-controls, .player-controls:focus-within { opacity: 1; }
/* Touch devices have no persistent hover state to reveal the bar with in the first place —
   opacity stayed 0 forever there, leaving every control (Next/Previous included) invisible even
   though the buttons underneath were technically clickable. (hover: none) is the standard,
   reliable way to detect "this is a touch-primary device," unlike a viewport-width media query
   (a phone in landscape, or a small browser window on desktop, both have narrow viewports but
   very different input models). */
@media (hover: none) {
    .player-controls { opacity: 1; }
}
/* Forced on by player.js whenever playback has failed — the moment a viewer most needs
   Next/Previous, regardless of whether their mouse happens to be hovering the player right now
   (desktop) or the bar's default visibility (touch, already covered above, but kept here too
   for the same "never hidden during an error" guarantee on every device). */
.player-controls.force-visible { opacity: 1; }
.player-controls .ctl-btn {
    pointer-events: auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    flex-shrink: 0;
    padding: 0;
    background: transparent;
    border: none;
    border-radius: 8px;
    color: rgba(255, 255, 255, 0.88);
    cursor: pointer;
    transition: color 0.15s ease, background 0.15s ease, transform 0.1s ease;
}
.player-controls .ctl-btn:hover { background: rgba(79, 140, 255, 0.18); color: var(--accent); }
.player-controls .ctl-btn:active { transform: scale(0.9); }
.player-controls .ctl-btn svg { width: 18px; height: 18px; }
.player-controls .ctl-spacer { flex: 1; }

/* Custom-styled volume slider — a bare <input type=range> (even with accent-color) looks like a
   generic OS widget; this fills the track up to the thumb with the same accent color the control
   buttons highlight to on hover, so the bar and the icons read as one consistent, deliberate UI
   instead of two different design languages. --volume-percent is kept in sync with the input's
   value from player.js (native range inputs have no CSS way to read their own value). */
.player-controls .ctl-volume {
    pointer-events: auto;
    -webkit-appearance: none;
    appearance: none;
    width: 5.5rem;
    height: 4px;
    border-radius: 999px;
    outline: none;
    cursor: pointer;
    background: linear-gradient(
        to right,
        var(--accent) 0%,
        var(--accent) var(--volume-percent, 100%),
        rgba(255, 255, 255, 0.22) var(--volume-percent, 100%),
        rgba(255, 255, 255, 0.22) 100%
    );
}
.player-controls .ctl-volume::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 13px;
    height: 13px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.player-controls .ctl-volume:hover::-webkit-slider-thumb,
.player-controls .ctl-volume:active::-webkit-slider-thumb {
    transform: scale(1.2);
    box-shadow: 0 0 0 6px rgba(79, 140, 255, 0.25);
}
.player-controls .ctl-volume::-moz-range-track {
    height: 4px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.22);
}
.player-controls .ctl-volume::-moz-range-progress {
    height: 4px;
    border-radius: 999px;
    background: var(--accent);
}
.player-controls .ctl-volume::-moz-range-thumb {
    width: 13px;
    height: 13px;
    border-radius: 50%;
    background: #fff;
    border: none;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
    transition: transform 0.12s ease;
}
.player-controls .ctl-volume:hover::-moz-range-thumb,
.player-controls .ctl-volume:active::-moz-range-thumb {
    transform: scale(1.2);
}

/* Skin video.js controls to match the site's dark theme instead of its default look. */
.video-js .vjs-control-bar {
    background: linear-gradient(0deg, rgba(0, 0, 0, 0.75), transparent);
}
/* video.js shows its own generic error modal automatically on a native media error,
   independent of the controls:false option — #player-overlay already covers every error
   case with its own channel-aware message (see player.js's fail()/showOverlay() calls), so
   video.js's copy is pure redundant noise that was overlapping the custom overlay/LIVE badge. */
.video-js .vjs-error-display { display: none !important; }
.video-js .vjs-play-progress, .video-js .vjs-volume-level { background: var(--accent); }
.video-js .vjs-big-play-button {
    background: rgba(15, 17, 21, 0.6);
    border-color: rgba(255, 255, 255, 0.35);
}
.video-js:hover .vjs-big-play-button, .video-js .vjs-big-play-button:focus {
    background: var(--accent);
    border-color: var(--accent);
}

/* Admin */
body.admin {
    font-family: system-ui, sans-serif;
    background:
        radial-gradient(ellipse 70% 40% at 50% 0%, rgba(79, 140, 255, 0.06), transparent),
        var(--bg);
    min-height: 100vh;
}

.admin-nav {
    display: flex;
    gap: 1.4rem;
    align-items: center;
    padding: 0.85rem 1.75rem;
    background: rgba(23, 26, 33, 0.85);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 10;
}
.admin-brand {
    font-weight: 800;
    letter-spacing: -0.02em;
    font-size: 1.05rem;
    margin-right: 0.5rem;
    white-space: nowrap;
}
.admin-brand-accent { color: var(--accent); }
.admin-nav a {
    color: var(--muted);
    text-decoration: none;
    font-size: 0.88rem;
    font-weight: 500;
    padding: 0.4rem 0.1rem;
    border-bottom: 2px solid transparent;
    transition: color 0.15s ease, border-color 0.15s ease;
}
.admin-nav a:hover { color: var(--text); }
.admin-nav a.active { color: var(--text); border-bottom-color: var(--accent); }
.admin-nav .spacer { flex: 1; }
.admin-user { color: var(--muted); font-size: 0.82rem; }
.admin-logout { color: var(--muted) !important; }
.admin-main { padding: 2rem 1.75rem 3rem; max-width: 1140px; margin: 0 auto; }
.admin-main h1 { font-size: 1.5rem; letter-spacing: -0.01em; margin-bottom: 1.25rem; }
.admin-main h2 { font-size: 1.1rem; margin: 2rem 0 1rem; }

.stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
    gap: 0.9rem;
    margin-bottom: 2rem;
}
.stat {
    position: relative;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 1.1rem 1rem;
    text-align: center;
    overflow: hidden;
    transition: transform 0.15s ease, border-color 0.15s ease;
}
.stat::before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 3px;
    background: linear-gradient(90deg, var(--accent), transparent);
    opacity: 0.7;
}
.stat:hover { transform: translateY(-2px); border-color: var(--border); }
.stat-value { display: block; font-size: 1.7rem; font-weight: 800; letter-spacing: -0.02em; }
.stat-label { color: var(--muted); font-size: 0.76rem; text-transform: uppercase; letter-spacing: 0.04em; }

.stat-live::before { background: linear-gradient(90deg, var(--danger), transparent); opacity: 0.8; }
.stat-live-dot {
    display: inline-block;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--danger);
    margin-right: 0.4rem;
    vertical-align: middle;
    animation: stat-live-pulse 1.6s ease-in-out infinite;
}
@keyframes stat-live-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.35; }
}

.dash-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.9rem;
    color: var(--muted);
    font-size: 0.82rem;
    margin: -0.6rem 0 1.25rem;
}
.dash-meta strong { color: var(--text); font-weight: 600; }

.dash-actions { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 2rem; }
.pill-link {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    background: var(--panel);
    border: 1px solid var(--border);
    color: var(--text);
    text-decoration: none;
    font-size: 0.85rem;
    padding: 0.55rem 1.1rem;
    border-radius: 999px;
    transition: border-color 0.15s ease, transform 0.15s ease, background 0.15s ease;
}
.pill-link:hover { border-color: var(--accent); background: rgba(79, 140, 255, 0.08); transform: translateY(-1px); }

/* Joins an input and its submit button into one visually unified control (e.g. the per-row
   "set device limit" / "set password" mini-forms) instead of two separate elements floating
   next to each other with a gap. */
.input-group {
    display: inline-flex;
    align-items: stretch;
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow: hidden;
    background: var(--bg);
    transition: border-color 0.15s ease;
}
.input-group:focus-within { border-color: var(--accent); }
.input-group input, .input-group select {
    border: none;
    background: transparent;
    color: var(--text);
    padding: 0.4rem 0.65rem;
    font-size: 0.82rem;
    min-width: 0;
}
.input-group input:focus, .input-group select:focus { outline: none; }
.input-group button {
    border-radius: 0;
    padding: 0.4rem 0.85rem;
    font-size: 0.8rem;
    flex-shrink: 0;
}
/* Overrides .input-group button's text-oriented padding for an icon-only button joined to an
   input (e.g. "set device limit") — square instead of wide, height driven by the input's own
   line-height via align-items:stretch on the parent .input-group. */
.input-group .icon-btn {
    width: 2.3rem;
    height: auto;
    padding: 0;
}

/* Lets a table wider than its column budget (many columns, long URLs, several action buttons)
   scroll horizontally in its own box instead of every column being squeezed unreadably thin —
   which is what was forcing the action buttons onto their own cramped stacked rows. */
.table-scroll {
    overflow-x: auto;
    margin-bottom: 1.5rem;
    border-radius: 10px;
}
.table-scroll .admin-table { margin-bottom: 0; }

.admin-table {
    width: 100%;
    min-width: 640px;
    border-collapse: separate;
    border-spacing: 0;
    margin-bottom: 1.5rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    overflow: hidden;
}
.admin-table th {
    text-align: left;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--muted);
    font-weight: 600;
    padding: 0.7rem 0.8rem;
    background: rgba(255, 255, 255, 0.02);
    border-bottom: 1px solid var(--border);
}
.admin-table td {
    border-bottom: 1px solid var(--border);
    padding: 0.85rem 0.8rem;
    text-align: left;
    font-size: 0.85rem;
    vertical-align: middle;
}
.admin-table tbody tr:last-child td { border-bottom: none; }
.admin-table tbody tr:hover { background: rgba(255, 255, 255, 0.02); }
/* No display:inline-block here — it was forcing every <td class="truncate"> out of table-cell
   layout (its own display value), breaking normal column/row alignment. A <span class="truncate">
   still needs inline-block for max-width/ellipsis to apply at all (plain inline elements ignore
   both), so that's scoped to the span-specific rule below instead of the shared one. */
.admin-table .truncate { max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.admin-table span.truncate { display: inline-block; }
.admin-table .small { color: var(--muted); font-size: 0.75rem; }

/* Small pill status label — replaces bare "Yes"/"Public"/"m3u_url" text with something that
   actually reads as a status at a glance instead of blending into every other line of text. */
.tag {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.2rem 0.6rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1.6;
    white-space: nowrap;
    background: rgba(255, 255, 255, 0.05);
    color: var(--muted);
    border: 1px solid var(--border);
}
.tag-ok { background: rgba(60, 203, 127, 0.12); color: var(--ok); border-color: rgba(60, 203, 127, 0.3); }
.tag-warn { background: rgba(224, 183, 82, 0.12); color: var(--warn); border-color: rgba(224, 183, 82, 0.3); }
.tag-danger { background: rgba(224, 82, 82, 0.12); color: var(--danger); border-color: rgba(224, 82, 82, 0.3); }
/* Row (not column) so this cell is normally single-line, same as every other cell in the row —
   stacking the tag under the name made that one cell taller than its neighbors, so row height
   varied from row to row depending on name length and the borders no longer lined up evenly. */
.name-cell { display: flex; flex-direction: row; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
/* flex-wrap: nowrap (not wrap) is deliberate — a wrapping flex container reports only its
   widest single child as its minimum content width to the table layout algorithm, so the
   Actions column would get shrunk down to fit one button and the rest would wrap onto their
   own stacked rows (which is what "not nice" looked like). Forcing one row means table
   auto-layout reserves the actions column's *actual* full width instead, and .table-scroll
   picks up the slack with a horizontal scrollbar if the table as a whole doesn't fit. */
.admin-table .actions { display: flex; gap: 0.4rem; flex-wrap: nowrap; white-space: nowrap; }
.admin-table .actions form { margin-right: 0; flex-shrink: 0; }
.admin-table .actions button {
    padding: 0.4rem 0.7rem;
    font-size: 0.78rem;
    flex-shrink: 0;
}

.error { color: var(--danger); }
.success { color: var(--ok); }
.danger { background: var(--danger); color: white; border: none; padding: 0.35rem 0.7rem; border-radius: 6px; cursor: pointer; }
.danger:hover { filter: brightness(1.1); }

/* Icon-only table action buttons (Import now, Delete, Preview, etc.) — square instead of the
   wider text-button padding, with the icon's SVG stroke set to currentColor so it follows the
   button's own text color automatically. The action's name lives in the title attribute
   (native browser tooltip) since there's no visible label anymore. */
.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.1rem;
    height: 2.1rem;
    padding: 0;
    flex-shrink: 0;
}
.icon-btn svg { width: 16px; height: 16px; }

.add-source-form {
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
    background: var(--panel);
    border: 1px solid var(--border);
    padding: 1.5rem;
    border-radius: 10px;
}
.filter-form {
    display: flex;
    gap: 1.1rem 1.25rem;
    flex-wrap: wrap;
    align-items: end;
    background: var(--panel);
    border: 1px solid var(--border);
    padding: 1.5rem;
    border-radius: 10px;
}
/* A form-row groups fields that belong on the same visual line. Forms with a mode-dependent
   field (M3U URL vs. upload, logo URL vs. upload) put the simple fields in one row and the
   mode-dependent field + submit button in a second row below — always, deterministically, not
   just when the first row happens to fill up and push the rest down by luck. */
.add-source-form .form-row {
    display: flex;
    gap: 1.1rem 1.25rem;
    flex-wrap: wrap;
    align-items: end;
    width: 100%;
}
.add-source-form label, .filter-form label {
    display: flex;
    flex-direction: column;
    font-size: 0.78rem;
    color: var(--muted);
    gap: 0.4rem;
    flex: 1 1 160px;
    min-width: 160px;
}
.add-source-form input, .add-source-form select, .filter-form input, .filter-form select {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.55rem 0.7rem;
    height: 2.6rem;
    transition: border-color 0.15s ease;
}
/* input[type=file] renders its own "Choose file" button with browser-controlled intrinsic
   sizing that padding alone doesn't normalize — without an explicit height, swapping the URL
   field for the file field changed the whole row's height. line-height centers the button's
   text within that fixed height the same way padding centers text in the other inputs. */
.add-source-form input[type="file"] {
    padding: 0.35rem 0.5rem;
    line-height: 1.7rem;
}
.add-source-form button, .filter-form button {
    flex: 0 0 auto;
}

/* A checkbox option next to bordered text inputs looked like a stray afterthought — give it
   the same bordered-box visual weight as every other field instead of floating unboxed. */
.checkbox-field {
    flex-direction: row !important;
    align-items: center;
    gap: 0.6rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.6rem 0.7rem;
    justify-content: flex-start;
}
.checkbox-field input[type="checkbox"] {
    width: 17px;
    height: 17px;
    accent-color: var(--accent);
    flex-shrink: 0;
}
.add-source-form input:focus, .add-source-form select:focus, .filter-form input:focus, .filter-form select:focus {
    outline: none;
    border-color: var(--accent);
}
button {
    background: var(--accent);
    color: white;
    border: none;
    border-radius: 6px;
    padding: 0.45rem 0.9rem;
    cursor: pointer;
    font-size: 0.85rem;
    transition: filter 0.15s ease, transform 0.1s ease;
}
button:hover { filter: brightness(1.12); }
button:active { transform: translateY(1px); }

.pagination { display: flex; gap: 0.35rem; flex-wrap: wrap; }
.pagination a {
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: 6px;
    text-decoration: none;
    color: var(--muted);
    font-size: 0.82rem;
    transition: border-color 0.15s ease, color 0.15s ease;
}
.pagination a:hover { color: var(--text); border-color: var(--accent); }
.pagination a.active { background: var(--accent); color: white; border-color: var(--accent); }
.pagination-ellipsis { padding: 0.35rem 0.3rem; color: var(--muted); font-size: 0.82rem; }

body.admin-login {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    background:
        radial-gradient(ellipse 60% 50% at 50% 0%, rgba(79, 140, 255, 0.08), transparent),
        var(--bg);
    color: var(--text);
    font-family: system-ui, sans-serif;
}
.login-box {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 2.25rem 2rem;
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    width: 300px;
    box-shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.6);
}
.login-box h1 { font-size: 1.3rem; letter-spacing: -0.01em; }
.login-box label { display: flex; flex-direction: column; gap: 0.25rem; font-size: 0.85rem; }
.login-box input {
    background: var(--bg);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 0.5rem;
    transition: border-color 0.15s ease;
}
.login-box input:focus { outline: none; border-color: var(--accent); }
