<?php
require_once __DIR__ . '/includes/auth.php';
require_once __DIR__ . '/includes/config.php';
require_once __DIR__ . '/includes/header.php';
require_once __DIR__ . '/includes/sidebar.php';
?>

<?php
// Proste helpery do zliczania wartości bez wysypywania całej strony
function safeCount(PDO $pdo, string $sql, array $params = []): int {
    try {
        $stmt = $pdo->prepare($sql);
        foreach ($params as $k => $v) {
            $stmt->bindValue($k, $v);
        }
        $stmt->execute();
        return (int)$stmt->fetchColumn();
    } catch (Throwable $e) {
        // W dashboard pokazujemy 0 zamiast 500 jeśli SQL się nie powiedzie
        return 0;
    }
}

// Liczniki dashboardu
$liveMatches     = safeCount($pdo, "SELECT COUNT(*) FROM matches WHERE LOWER(Status) = 'live'");
$plannedMatches  = safeCount($pdo, "SELECT COUNT(*) FROM matches WHERE LOWER(Status) = 'planned'");
$activeUsers     = safeCount($pdo, "SELECT COUNT(*) FROM users WHERE (disabled IS NULL OR disabled = 0)");

// Placeholder na zgłoszenia BOK – brak tabeli w projekcie, więc ustawiamy 0
$supportTickets  = 0;
?>

<div class="admin-content">
    <style>
        .admin-page-title {
            font-size: 24px;
            font-weight: 600;
            color: #23282d;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 2px solid #0073aa;
        }

        .admin-dashboard-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            margin-top: 30px;
        }

        .admin-stat-card {
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 4px;
            padding: 20px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            transition: all 0.3s;
        }

        .admin-stat-card:hover {
            box-shadow: 0 3px 8px rgba(0,0,0,0.15);
            transform: translateY(-2px);
        }

        .admin-stat-title {
            font-size: 13px;
            color: #666;
            text-transform: uppercase;
            letter-spacing: 0.5px;
            margin-bottom: 10px;
        }

        .admin-stat-value {
            font-size: 32px;
            font-weight: 700;
            color: #0073aa;
            margin-bottom: 5px;
        }

        .admin-stat-description {
            font-size: 13px;
            color: #888;
        }

        .admin-welcome-box {
            background: #fff;
            border-left: 4px solid #0073aa;
            padding: 20px;
            margin-bottom: 30px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }

        .admin-welcome-box h2 {
            margin: 0 0 10px 0;
            color: #23282d;
            font-size: 20px;
        }

        .admin-welcome-box p {
            margin: 0;
            color: #666;
            line-height: 1.6;
        }

        ul.admin-feature-list {
            padding-left: 20px;
        }

        .admin-split-panel {
            margin-top: 30px;
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }

        /* Dashboard: stała wysokość obu paneli + scroll w środku */
        .admin-split-panel .admin-panel-box {
            height: 1000px;
            overflow: hidden;
            display: flex;
            flex-direction: column;
        }

        @media (max-width: 1100px) {
            .admin-split-panel {
                grid-template-columns: 1fr;
            }
        }

        .admin-panel-box {
            background: #fff;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            padding: 16px;
        }

        .admin-panel-box h3 {
            margin: 0 0 12px 0;
            color: #23282d;
            font-size: 16px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        }

        .admin-muted {
            color: #666;
            font-size: 12px;
        }

        .admin-form-row {
            display: grid;
            grid-template-columns: 1fr;
            gap: 10px;
            margin-bottom: 12px;
        }

        .admin-form-row input[type="text"],
        .admin-form-row textarea {
            width: 100%;
            min-height: 38px;
            max-height: 500px;
            border: 1px solid #ccd0d4;
            border-radius: 4px;
            padding: 10px;
            font-size: 14px;
            outline: none;
        }

        .admin-form-row textarea {
            min-height: 90px;
            resize: vertical;
        }

        .admin-task-filebar {
            display: grid;
            grid-template-columns: auto 1fr auto;
            align-items: center;
            gap: 10px;
            padding: 2px 0 0 0;
            min-width: 0;
        }

        .admin-task-filebtn {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            height: 34px;
            padding: 0 12px;
            border: 1px solid #d0d7de;
            border-radius: 10px;
            background: #f6f8fa;
            color: #24292f;
            font-size: 13px;
            font-weight: 700;
            cursor: pointer;
            user-select: none;
        }

        .admin-task-filebtn:hover {
            background: #eef2f6;
        }

        .admin-task-filename {
            font-size: 13px;
            color: #444;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .admin-task-filehint {
            white-space: nowrap;
        }

        .admin-task-files-list {
            display: grid;
            gap: 6px;
            margin-top: 2px;
        }

        .admin-task-file-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 8px;
            border: 1px solid #d0d7de;
            border-radius: 8px;
            background: #f6f8fa;
            padding: 6px 8px;
            font-size: 12px;
            color: #333;
            min-width: 0;
        }

        .admin-task-file-item-name {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            min-width: 0;
            flex: 1;
        }

        .admin-task-file-item-remove {
            border: 0;
            background: transparent;
            color: #b32d2e;
            cursor: pointer;
            font-size: 14px;
            font-weight: 700;
            line-height: 1;
            padding: 0 2px;
            flex: 0 0 auto;
        }

        .admin-task-file-item-remove:hover {
            opacity: 0.8;
        }

        @media (max-width: 560px) {
            .admin-task-filebar {
                grid-template-columns: 1fr;
                align-items: flex-start;
                gap: 6px;
            }
            .admin-task-filebtn {
                width: 100%;
            }
            .admin-task-filehint {
                white-space: normal;
            }
        }

        .admin-btn {
            height: 35px;
            appearance: none;
            border: 0;
            border-radius: 4px;
            padding: 10px 12px;
            background: #0073aa;
            color: #fff;
            font-weight: 600;
            cursor: pointer;
        }

        .admin-btn:hover {
            filter: brightness(1.03);
        }

        .admin-btn:active {
            transform: translateY(1px);
        }

        .admin-btn:disabled {
            opacity: 0.7;
            cursor: not-allowed;
        }

        .admin-btn-small {
            height: 30px;
            padding: 8px 10px;
            font-size: 12px;
            border-radius: 6px;
        }

        .admin-btn-secondary {
            background: #6c757d;
        }

        .admin-btn-danger {
            background: #b32d2e;
        }

        .admin-btn-success {
            background: #1e7e34;
        }

        .admin-list {
            margin-top: 14px;
            border-top: 1px solid #eee;
            padding-top: 14px;
            max-height: none;
            overflow-y: auto;
            flex: 1;
            min-height: 0;
        }

        .admin-empty {
            padding: 16px;
            border: 1px dashed rgba(0,0,0,0.18);
            border-radius: 10px;
            background: rgba(255,255,255,0.6);
            color: #666;
            text-align: center;
            font-size: 13px;
        }

        .admin-item {
            border: 1px solid #eee;
            border-radius: 4px;
            padding: 10px;
            margin-bottom: 10px;
            background: #fafafa;
        }

        .admin-item.todo {
            border-color: rgba(179, 45, 46, 0.35);
            background: rgba(179, 45, 46, 0.06);
        }

        .admin-item.done {
            border-color: rgba(30, 126, 52, 0.35);
            background: rgba(30, 126, 52, 0.06);
        }

        .admin-item-header {
            display: flex;
            align-items: flex-start;
            justify-content: space-between;
            gap: 10px;
            flex-wrap: wrap;
        }

        .admin-item-badges {
            display: flex;
            gap: 8px;
            align-items: center;
            flex-wrap: wrap;
            justify-content: flex-end;
            flex: 0 0 auto;
        }

        .admin-badge {
            font-size: 11px;
            font-weight: 700;
            padding: 4px 8px;
            border-radius: 999px;
            border: 1px solid rgba(0,0,0,0.08);
            background: #fff;
            color: #23282d;
            white-space: nowrap;
        }

        .admin-badge.todo {
            background: rgba(179, 45, 46, 0.10);
            color: #b32d2e;
            border-color: rgba(179, 45, 46, 0.25);
        }

        .admin-badge.done {
            background: rgba(30, 126, 52, 0.12);
            color: #1e7e34;
            border-color: rgba(30, 126, 52, 0.25);
        }

        .admin-task-actions {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
            margin-top: 10px;
        }

        .admin-task-comments-wrap {
            display: grid;
            gap: 8px;
        }

        .admin-task-comments-list {
            max-height: 340px;
            overflow: auto;
            border: 1px solid #e6e6e6;
            border-radius: 8px;
            background: #fafafa;
            padding: 8px;
            display: grid;
            gap: 8px;
        }

        .admin-task-comment-item {
            border: 1px solid #e1e4e8;
            background: #fff;
            border-radius: 8px;
            padding: 8px;
            display: grid;
            gap: 6px;
        }

        .admin-task-comment-head {
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 8px;
            font-size: 12px;
            color: #666;
        }

        .admin-task-comment-text {
            font-size: 13px;
            color: #333;
            white-space: pre-wrap;
            word-break: break-word;
            overflow-wrap: anywhere;
        }

        .admin-task-comments-form {
            display: grid;
            gap: 8px;
        }

        .admin-task-comments-form textarea {
            width: 100%;
            min-height: 86px;
            border: 1px solid #ccd0d4;
            border-radius: 8px;
            padding: 10px;
            outline: none;
            resize: vertical;
            font-size: 13px;
        }

        .admin-modal-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,0.55);
            display: none;
            align-items: center;
            justify-content: center;
            padding: 18px;
            z-index: 99999;
            backdrop-filter: blur(2px);
        }

        .admin-modal {
            width: min(820px, 100%);
            background: #fff;
            border-radius: 14px;
            border: 1px solid rgba(0,0,0,0.10);
            box-shadow: 0 20px 60px rgba(0,0,0,0.28);
            overflow: hidden;
            animation: adminModalIn 0.14s ease-out;
        }

        @keyframes adminModalIn {
            from {
                transform: translateY(6px) scale(0.992);
                opacity: 0.85;
            }
            to {
                transform: translateY(0) scale(1);
                opacity: 1;
            }
        }

        /* Nicer scrollbars (where supported) */
        .admin-list,
        .admin-chat-messages,
        .admin-mention-box {
            scrollbar-width: thin;
            scrollbar-color: rgba(0,0,0,0.28) transparent;
        }

        .admin-list::-webkit-scrollbar,
        .admin-chat-messages::-webkit-scrollbar,
        .admin-mention-box::-webkit-scrollbar {
            width: 12px;
        }

        .admin-list::-webkit-scrollbar-thumb,
        .admin-chat-messages::-webkit-scrollbar-thumb,
        .admin-mention-box::-webkit-scrollbar-thumb {
            background-color: rgba(0,0,0,0.25);
            border-radius: 999px;
            border: 3px solid rgba(255,255,255,0.85);
            background-clip: padding-box;
        }

        .admin-list::-webkit-scrollbar-thumb:hover,
        .admin-chat-messages::-webkit-scrollbar-thumb:hover,
        .admin-mention-box::-webkit-scrollbar-thumb:hover {
            background-color: rgba(0,0,0,0.35);
        }

        .admin-chat-compose textarea,
        .admin-modal-body textarea {
            scrollbar-width: thin;
        }

        .admin-chat-compose textarea {
            height: 35px;
            min-height: 35px;
            scrollbar-color: rgba(255,255,255,0.26) transparent;
        }

        .admin-chat-compose textarea::-webkit-scrollbar {
            width: 12px;
        }

        .admin-chat-compose textarea::-webkit-scrollbar-thumb {
            background-color: rgba(255,255,255,0.20);
            border-radius: 999px;
            border: 3px solid rgba(31,35,41,1);
            background-clip: padding-box;
        }

        .admin-chat-compose textarea::-webkit-scrollbar-thumb:hover {
            background-color: rgba(255,255,255,0.32);
        }

        .admin-modal-body textarea {
            scrollbar-color: rgba(0,0,0,0.28) transparent;
        }

        .admin-modal-body textarea::-webkit-scrollbar {
            width: 12px;
        }

        .admin-modal-body textarea::-webkit-scrollbar-thumb {
            background-color: rgba(0,0,0,0.22);
            border-radius: 999px;
            border: 3px solid rgba(255,255,255,1);
            background-clip: padding-box;
        }

        .admin-modal-body textarea::-webkit-scrollbar-thumb:hover {
            background-color: rgba(0,0,0,0.32);
        }

        .admin-modal-header {
            padding: 14px 16px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
            background: linear-gradient(180deg, #ffffff 0%, #f7f9fb 100%);
            border-bottom: 1px solid rgba(0,0,0,0.08);
        }

        .admin-modal-title {
            margin: 0;
            font-size: 15px;
            font-weight: 800;
            color: #23282d;
        }

        .admin-modal-close {
            width: 34px;
            height: 34px;
            border-radius: 10px;
            border: 1px solid rgba(0,0,0,0.10);
            background: #fff;
            cursor: pointer;
            font-size: 18px;
            line-height: 1;
            font-weight: 800;
            color: #23282d;
        }

        .admin-modal-close:hover {
            background: #f2f4f7;
        }

        .admin-modal-body {
            padding: 16px;
            display: grid;
            gap: 10px;
        }

        .admin-modal-body label {
            font-size: 12px;
            font-weight: 800;
            color: #23282d;
        }

        .admin-modal-body input[type="text"],
        .admin-modal-body textarea {
            width: 100%;
            border: 1px solid #ccd0d4;
            border-radius: 10px;
            padding: 10px 12px;
            font-size: 14px;
            outline: none;
        }

        .admin-modal-body textarea {
            min-height: 160px;
            max-height: 500px;
            overflow: auto;
            resize: vertical;
            white-space: pre-wrap;
        }

        .admin-modal-footer {
            padding: 14px 16px;
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            border-top: 1px solid rgba(0,0,0,0.08);
            background: #fafbfc;
        }

        .admin-confirm-msg {
            font-size: 13px;
            color: #333;
            line-height: 1.5;
        }

        .admin-confirm-danger {
            display: grid;
            gap: 8px;
            padding: 12px;
            border-radius: 12px;
            border: 1px solid rgba(179, 45, 46, 0.20);
            background: rgba(179, 45, 46, 0.06);
        }

        .admin-task-edit {
            margin-top: 10px;
            padding: 10px;
            border: 1px dashed rgba(0,0,0,0.18);
            border-radius: 8px;
            background: rgba(255,255,255,0.65);
        }

        .admin-task-edit input[type="text"],
        .admin-task-edit textarea {
            width: 100%;
            border: 1px solid #ccd0d4;
            border-radius: 8px;
            padding: 10px;
            font-size: 14px;
            outline: none;
        }

        .admin-task-edit textarea {
            min-height: 90px;
            max-height: 300px;
            overflow: auto;
            resize: vertical;
        }

        .admin-task-edit input[type="file"] {
            width: 100%;
            font-size: 13px;
            color: #333;
        }

        .admin-task-edit input[type="file"]::file-selector-button {
            height: 34px;
            padding: 0 12px;
            border: 1px solid #d0d7de;
            border-radius: 10px;
            background: #f6f8fa;
            color: #24292f;
            font-size: 13px;
            font-weight: 700;
            cursor: pointer;
            margin-right: 10px;
        }

        .admin-task-edit input[type="file"]::file-selector-button:hover {
            background: #eef2f6;
        }

        .admin-task-edit input[type="file"]::-webkit-file-upload-button {
            height: 34px;
            padding: 0 12px;
            border: 1px solid #d0d7de;
            border-radius: 10px;
            background: #f6f8fa;
            color: #24292f;
            font-size: 13px;
            font-weight: 700;
            cursor: pointer;
            margin-right: 10px;
        }

        .admin-task-edit .row {
            display: grid;
            gap: 8px;
        }

        .admin-task-edit .admin-muted {
            font-size: 12px;
        }

        .admin-item-title {
            font-weight: 700;
            color: #23282d;
            margin-bottom: 4px;
            overflow-wrap: anywhere;
            word-break: break-word;
            flex: 1 1 260px;
            min-width: 0;
        }

        .admin-item-meta {
            color: #777;
            font-size: 12px;
            margin-bottom: 8px;
        }

        .admin-item-desc {
            color: #333;
            font-size: 13px;
            white-space: pre-wrap;
            overflow-wrap: anywhere;
            word-break: break-word;
        }

        .admin-chat-box {
            display: grid;
            grid-template-rows: auto auto 1fr auto;
            gap: 10px;
            height: 1000px;
        }

        .admin-chat-messages {
            border: 1px solid #e6e6e6;
            border-radius: 10px;
            background: linear-gradient(180deg, #fbfbfb 0%, #f5f7fa 100%);
            overflow: auto;
            min-height: 0;
            padding: 12px;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        /* Empty state (no messages yet) */
        .admin-chat-messages:empty {
            justify-content: center;
            align-items: center;
        }

        .admin-chat-messages:empty::before {
            content: "Brak wiadomości.\A Napisz pierwszą wiadomość lub dodaj załącznik.";
            white-space: pre-line;
            text-align: center;
            color: #6b7280;
            font-size: 13px;
            line-height: 1.35;
            padding: 16px 14px;
            border-radius: 14px;
            border: 1px dashed rgba(0,0,0,0.18);
            background: rgba(255,255,255,0.72);
            box-shadow: 0 8px 22px rgba(0,0,0,0.06);
            max-width: 420px;
        }

        .admin-chat-message {
            max-width: 86%;
            border: 1px solid #e9ecef;
            border-radius: 12px;
            padding: 10px 12px;
            background: #fff;
            box-shadow: 0 1px 2px rgba(0,0,0,0.06);
            position: relative;
            overflow-wrap: anywhere;
            word-break: break-word;
        }

        .admin-chat-message.me {
            align-self: flex-end;
            background: #eaf6ff;
            border-color: #cfe8f7;
        }

        .admin-chat-message.hearted {
            border-color: rgba(179, 45, 46, 0.35);
            box-shadow: 0 0 0 1px rgba(179, 45, 46, 0.08);
        }

        .admin-chat-heart-pin {
            margin-top: 6px;
            display: inline-flex;
            align-items: center;
            gap: 6px;
            font-size: 12px;
            color: #b32d2e;
            font-weight: 700;
        }

        /* Typing bubble (single) */
        .admin-chat-message.typing {
            align-self: flex-start;
            max-width: 72%;
            background: rgba(255,255,255,0.92);
            border-color: rgba(0,0,0,0.06);
            box-shadow: 0 1px 2px rgba(0,0,0,0.05);
        }

        .admin-chat-typing-row {
            display: flex;
            align-items: center;
            gap: 10px;
            padding-top: 2px;
        }

        .admin-chat-typing-name {
            font-weight: 700;
            font-size: 13px;
            color: #23282d;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            max-width: 220px;
        }

        .admin-chat-typing-dots {
            display: inline-flex;
            align-items: center;
            gap: 6px;
        }

        .admin-chat-typing-dots span {
            width: 7px;
            height: 7px;
            border-radius: 50%;
            background: rgba(0,0,0,0.28);
            display: inline-block;
            animation: adminTypingDot 1.05s infinite ease-in-out;
        }

        .admin-chat-typing-dots span:nth-child(2) { animation-delay: 0.15s; }
        .admin-chat-typing-dots span:nth-child(3) { animation-delay: 0.30s; }

        @keyframes adminTypingDot {
            0%, 80%, 100% { transform: translateY(0); opacity: 0.35; }
            40% { transform: translateY(-4px); opacity: 0.95; }
        }

        .admin-chat-header {
            display: flex;
            align-items: baseline;
            justify-content: space-between;
            gap: 10px;
        }

        .admin-chat-user {
            font-weight: 700;
            color: #23282d;
            font-size: 13px;
        }

        .admin-chat-time {
            color: #777;
            font-size: 12px;
            white-space: nowrap;
        }

        .admin-chat-text {
            margin-top: 6px;
            color: #333;
            font-size: calc(13px + 0.2rem);
            white-space: pre-wrap;
            word-break: break-word;
            overflow-wrap: anywhere;
            line-height: 1.45;
        }

        .admin-chat-link {
            color: #0073aa;
            text-decoration: underline;
            word-break: break-all;
        }

        .admin-chat-link:hover {
            color: #005f8d;
        }

        .admin-chat-compose {
            display: flex;
            align-items: flex-start;
            gap: 10px;
            border: 1px solid #2d333b;
            border-radius: 14px;
            background: #1f2329;
            padding: 10px;
            box-shadow: 0 1px 2px rgba(0,0,0,0.16);
            --chat-ctl-h: 56px;
        }

        .admin-chat-compose:focus-within {
            border-color: #0073aa;
            box-shadow: 0 0 0 4px rgba(0,115,170,0.10);
        }

        .admin-chat-icon-btn {
            width: var(--chat-ctl-h);
            height: var(--chat-ctl-h);
            flex: 0 0 var(--chat-ctl-h);
            align-self: flex-start;
            border: 1px solid rgba(255,255,255,0.10);
            border-radius: 12px;
            background: rgba(255,255,255,0.06);
            color: rgba(255,255,255,0.92);
            font-size: 22px;
            line-height: 1;
            font-weight: 700;
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            user-select: none;
        }

        .admin-chat-icon-btn:hover {
            background: rgba(255,255,255,0.10);
        }

        .admin-chat-icon-btn:active {
            transform: translateY(1px);
        }

        .admin-chat-inputwrap {
            flex: 1;
            display: grid;
            grid-template-columns: 1fr;
            gap: 6px;
            min-width: 0;
            align-self: stretch;
            position: relative;
        }

        .admin-chat-compose .admin-btn {
            height: var(--chat-ctl-h);
            box-sizing: border-box;
            border: 1px solid rgba(255,255,255,0.10);
            border-radius: 12px;
            padding: 0 16px;
            display: inline-flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            box-shadow: 0 1px 1px rgba(0,0,0,0.14);
        }

        .admin-chat-send-btn {
            flex: 0 0 auto;
            align-self: flex-start;
        }

        .admin-chat-compose textarea {
            box-sizing: border-box;
            min-height: var(--chat-ctl-h);
            max-height: 150px;
            height: var(--chat-ctl-h);
            padding: 9px 10px;
            outline: 0;
            border: 1px solid rgba(255,255,255,0.10);
            border-radius: 12px;
            background: rgba(255,255,255,0.04);
            color: rgba(255,255,255,0.92);
            box-shadow: inset 0 1px 2px rgba(0,0,0,0.18);
            transition: border-color 220ms ease, box-shadow 220ms ease, background 220ms ease;
            font-size: calc(14px + 0.2rem);
            line-height: 1.35;
            overflow-y: hidden;
        }

        .admin-chat-compose textarea::placeholder {
            color: rgba(255,255,255,0.55);
        }

        /* Scrollbar: visually inset ~2px from right edge (WebKit/Blink) */
        .admin-chat-compose textarea::-webkit-scrollbar {
            width: 12px;
        }

        .admin-chat-compose textarea::-webkit-scrollbar-track {
            background: transparent;
        }

        .admin-chat-compose textarea::-webkit-scrollbar-thumb {
            background-color: rgba(255,255,255,0.28);
            border-radius: 999px;
            border: 2px solid transparent;
            background-clip: content-box;
        }

        .admin-chat-compose textarea::-webkit-scrollbar-thumb:hover {
            background-color: rgba(255,255,255,0.38);
        }

        .admin-chat-compose textarea:focus {
            border-color: rgba(0,115,170,0.85);
            background: rgba(255,255,255,0.06);
            box-shadow: 0 0 0 3px rgba(0,115,170,0.12);
        }

        @keyframes adminPulseRedBorder {
            0% {
                border-color: rgba(255, 77, 77, 0.25);
                box-shadow: 0 0 0 0 rgba(255, 77, 77, 0.0);
            }
            35% {
                border-color: rgba(255, 77, 77, 0.95);
                box-shadow: 0 0 0 5px rgba(255, 77, 77, 0.18);
            }
            100% {
                border-color: rgba(255, 77, 77, 0.25);
                box-shadow: 0 0 0 0 rgba(255, 77, 77, 0.0);
            }
        }

        .admin-chat-compose textarea.admin-pulse-red,
        .admin-chat-inline-textarea.admin-pulse-red {
            animation: adminPulseRedBorder 1s ease-out;
        }

        .admin-chat-inputwrap > textarea {
            resize: none;
        }

        .admin-chat-replybar {
            border: 1px solid rgba(255,255,255,0.10);
            border-radius: 10px;
            background: rgba(255,255,255,0.06);
            padding: 8px 10px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            border-left: 4px solid #0073aa;
            min-width: 0;
        }

        .admin-chat-replybar > div {
            min-width: 0;
            flex: 1 1 auto;
        }

        .admin-chat-replybar strong {
            display: block;
            font-size: 12px;
            color: rgba(255,255,255,0.92);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .admin-chat-replybar span {
            display: block;
            font-size: 12px;
            color: rgba(255,255,255,0.70);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            max-width: 100%;
        }

        .admin-chat-replybar button {
            border: 0;
            background: transparent;
            color: rgba(255,255,255,0.85);
            cursor: pointer;
            font-weight: 700;
            font-size: 18px;
            line-height: 1;
            flex: 0 0 auto;
        }

        .admin-chat-filechip {
            display: grid;
            grid-template-columns: 1fr auto auto;
            align-items: center;
            gap: 10px;
            padding: 6px 10px;
            border: 1px solid rgba(255,255,255,0.10);
            border-radius: 12px;
            background: rgba(255,255,255,0.04);
            min-width: 0;
        }

        .admin-chat-filechip-name {
            font-size: 12px;
            color: rgba(255,255,255,0.85);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .admin-chat-filechip-remove {
            width: 26px;
            height: 26px;
            border-radius: 8px;
            border: 1px solid rgba(255,255,255,0.10);
            background: rgba(255,255,255,0.06);
            color: rgba(255,255,255,0.90);
            cursor: pointer;
            font-size: 16px;
            line-height: 1;
        }

        .admin-chat-filechip-remove:hover {
            background: rgba(255,255,255,0.10);
        }

        /* legacy file picker styles removed from layout; keeping mention box below input */

        @media (max-width: 560px) {
            .admin-chat-compose {
                flex-wrap: wrap;
            }
            .admin-chat-send-btn {
                width: 100%;
            }
            .admin-chat-icon-btn {
                width: 100%;
                flex: 1 1 auto;
            }
        }

        .admin-chat-actions {
            margin-top: 8px;
            display: flex;
            gap: 10px;
            opacity: 0;
            transition: opacity 0.15s ease;
        }

        .admin-chat-message:hover .admin-chat-actions {
            opacity: 1;
        }

        .admin-chat-action-btn {
            border: 0;
            background: transparent;
            color: #0073aa;
            font-size: 12px;
            cursor: pointer;
            padding: 0;
        }

        .admin-chat-action-btn:hover {
            text-decoration: underline;
        }

        .admin-chat-action-btn.active-heart {
            color: #b32d2e;
            font-weight: 700;
        }

        .admin-chat-inline-edit {
            margin-top: 6px;
        }

        .admin-chat-inline-edit textarea {
            width: 100%;
            min-height: 90px;
            max-height: 240px;
            resize: vertical;
            border: 1px solid #ccd0d4;
            border-radius: 10px;
            padding: 10px 12px;
            font-size: calc(14px + 0.2rem);
            line-height: 1.35;
            outline: none;
        }

        .admin-chat-inline-edit-actions {
            margin-top: 8px;
            display: flex;
            gap: 10px;
            justify-content: flex-end;
        }

        /* WhatsApp-like inline edit (minimal visual change) */
        .admin-chat-message.editing .admin-chat-actions {
            opacity: 1;
        }

        .admin-chat-inline-textarea {
            width: 100%;
            resize: none;
            border: 0;
            outline: none;
            background: transparent;
            color: inherit;
            font: inherit;
            line-height: inherit;
            padding: 0;
            margin: 0;
            overflow: hidden;
            transition: box-shadow 220ms ease;
        }

        .admin-chat-inline-toolbar {
            margin-top: 6px;
            display: flex;
            justify-content: flex-end;
            gap: 12px;
        }

        .admin-chat-inline-filebar {
            margin-top: 6px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 10px;
            font-size: 12px;
            color: #6e7781;
        }

        .admin-chat-inline-filebar .name {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .admin-chat-inline-filebar .actions {
            display: flex;
            gap: 12px;
            flex: 0 0 auto;
        }

        .admin-chat-inline-toolbtn {
            border: 0;
            background: transparent;
            padding: 0;
            cursor: pointer;
            font-size: 12px;
            color: #0073aa;
        }

        .admin-chat-inline-toolbtn:hover {
            text-decoration: underline;
        }

        .admin-chat-inline-error {
            margin-top: 6px;
            font-size: 12px;
            color: #b32d2e;
            display: none;
        }

        .admin-chat-reply-quote {
            margin-top: 8px;
            border-left: 3px solid #0073aa;
            padding: 6px 10px;
            background: rgba(255,255,255,0.7);
            border-radius: 10px;
            font-size: 13px;
        }

        .admin-chat-reply-quote .admin-muted {
            margin-bottom: 4px;
        }

        .admin-chat-message.recalled .admin-chat-text-main {
            font-style: italic;
            opacity: 0.85;
        }

        .admin-chat-attachment {
            margin-top: 8px;
        }

        .admin-chat-attachment img {
            display: block;
            width: min(360px, 100%);
            max-width: 100%;
            height: auto;
            max-height: 240px;
            object-fit: contain;
            border-radius: 6px;
            border: 1px solid #e6e6e6;
            box-shadow: 0 1px 2px rgba(0,0,0,0.06);
            cursor: pointer;
        }

        .admin-chat-attachment img:hover {
            transform: translateY(-1px);
            transition: transform 0.12s ease;
        }

        .admin-mention {
            color: #0073aa;
            font-weight: 700;
        }

        .admin-mention-box {
            border: 1px solid #e6e6e6;
            border-radius: 6px;
            background: #fff;
            overflow: auto;
            max-height: 160px;
            box-shadow: 0 8px 20px rgba(0,0,0,0.10);
        }

        .admin-mention-item {
            padding: 8px 10px;
            cursor: pointer;
            font-size: 13px;
            border-bottom: 1px solid #f1f1f1;
        }

        .admin-mention-item:last-child {
            border-bottom: 0;
        }

        .admin-mention-item.active {
            background: #f1f7fb;
        }

        .admin-status {
            margin-top: 8px;
            font-size: 12px;
            color: #666;
        }

        .admin-charhint {
            margin-top: -6px;
            margin-bottom: 4px;
            font-size: 12px;
            color: #6e7781;
            text-align: right;
            user-select: none;
        }

        .admin-charhint.warn {
            color: #b54708;
            font-weight: 600;
        }

        .admin-charhint.over {
            color: #b32d2e;
            font-weight: 700;
        }
    </style>

    <h1 class="admin-page-title">Dashboard</h1>

    <div class="admin-welcome-box">
        <h2>👋 Witaj w panelu administracyjnym, <?php echo htmlspecialchars($_SESSION['username']); ?>!</h2>
        <p>Zarządzaj swoją platformą togethere.cloud. Wybierz opcję z menu po lewej stronie.</p>
    </div>

    <div class="admin-dashboard-grid">
        <div class="admin-stat-card">
            <div class="admin-stat-title">Trwające mecze</div>
            <div class="admin-stat-value"><?php echo $liveMatches; ?></div>
            <div class="admin-stat-description">Aktualnie w trakcie</div>
        </div>

        <div class="admin-stat-card">
            <div class="admin-stat-title">Zaplanowane mecze</div>
            <div class="admin-stat-value"><?php echo $plannedMatches; ?></div>
            <div class="admin-stat-description">Nadchodzące spotkania</div>
        </div>

        <div class="admin-stat-card">
            <div class="admin-stat-title">Aktywni użytkownicy</div>
            <div class="admin-stat-value"><?php echo $activeUsers; ?></div>
            <div class="admin-stat-description">Zarejestrowani użytkownicy</div>
        </div>

        <div class="admin-stat-card">
            <div class="admin-stat-title">Zgłoszenia BOK</div>
            <div class="admin-stat-value"><?php echo $supportTickets; ?></div>
            <div class="admin-stat-description">Oczekujące zgłoszenia</div>
        </div>
    </div>

    <div class="admin-split-panel">
        <div class="admin-panel-box">
            <h3>
                <span>KEEP (notatki / taski)</span>
                <span class="admin-muted" id="adminTasksStatus">—</span>
            </h3>

            <form id="adminTaskForm" class="admin-form-row" enctype="multipart/form-data">
                <input type="text" name="title" id="adminTaskTitle" placeholder="Tytuł (np. 'Do zrobienia dziś')" required maxlength="100" />
                <div class="admin-charhint" id="adminTaskTitleHint" aria-live="polite"></div>
                <textarea name="description" id="adminTaskDescription" placeholder="Opis / ważne informacje..." maxlength="1000"></textarea>
                <div class="admin-charhint" id="adminTaskDescriptionHint" aria-live="polite"></div>
                <div class="admin-task-filebar">
                    <label class="admin-task-filebtn" for="adminTaskFile" title="Dodaj pliki">Wybierz pliki</label>
                    <span class="admin-task-filename" id="adminTaskFileName">Nie wybrano plików</span>
                    <span class="admin-task-filehint admin-muted">zapis do DB • LONGBLOB</span>
                    <input type="file" name="files[]" id="adminTaskFile" multiple style="display:none;" />
                </div>
                <div class="admin-task-files-list" id="adminTaskFilesList"></div>
                <button class="admin-btn" type="submit" id="adminTaskSubmit">Dodaj</button>
            </form>

            <div class="admin-list" id="adminTasksList"></div>
            <div class="admin-status" id="adminTasksError" style="display:none;color:#b32d2e;"></div>
            <div class="admin-muted" style="margin-top:10px;">
                Uwaga: pliki zapisują się w bazie (LONGBLOB). Dla bardzo dużych plików trzeba podnieść limity PHP: upload_max_filesize i post_max_size.
            </div>
        </div>

        <div class="admin-panel-box admin-chat-box">
            <h3>
                <span>Czat (stała historia)</span>
                <span class="admin-muted" id="adminChatStatus">Ładowanie…</span>
            </h3>

            <form id="adminChatForm" class="admin-chat-compose" enctype="multipart/form-data">
                <button type="button" class="admin-chat-icon-btn" id="adminChatAttachBtn" aria-label="Dodaj załącznik" title="Dodaj załącznik">+</button>
                <input type="file" id="adminChatFile" name="file" accept="image/*,application/pdf,text/plain" style="display:none;" />

                <div class="admin-chat-inputwrap">
                    <div id="adminChatReplyBar" class="admin-chat-replybar" style="display:none;">
                        <div>
                            <strong id="adminChatReplyTitle">Odpowiadasz…</strong>
                            <span id="adminChatReplySnippet"></span>
                        </div>
                        <button type="button" id="adminChatReplyCancel" aria-label="Anuluj odpowiedź">×</button>
                    </div>

                    <textarea id="adminChatInput" placeholder="Napisz wiadomość…" maxlength="1500"></textarea>
                    <div id="adminMentionBox" class="admin-mention-box" style="display:none;"></div>

                    <div class="admin-chat-filechip" id="adminChatFileChip" style="display:none;">
                        <span class="admin-chat-filechip-name" id="adminChatFileChipName">Załącznik</span>
                        <span class="admin-muted" style="white-space:nowrap;">max 5MB • jpg/png/gif/webp/pdf/txt</span>
                        <button type="button" class="admin-chat-filechip-remove" id="adminChatFileChipRemove" aria-label="Usuń załącznik" title="Usuń">×</button>
                    </div>
                </div>

                <button class="admin-btn admin-chat-send-btn" type="submit" id="adminChatSend">Wyślij</button>
            </form>

            <div class="admin-chat-messages" id="adminChatMessages" aria-label="Czat"></div>
            <div class="admin-status" id="adminChatError" style="display:none;color:#b32d2e;"></div>
        </div>
    </div>

    <div style="margin-top: 40px; padding: 20px; background: #fff; border: 1px solid #ddd; border-radius: 4px;">
        <h3 style="margin-top: 0; color: #23282d;">🚀 Funkcjonalność w przygotowaniu</h3>
        <p style="color: #666; line-height: 1.6;">
            Panel administracyjny jest w fazie rozwoju. Wkrótce dodamy pełne funkcjonalności zarządzania:
        </p>
        <ul style="color: #666; line-height: 1.8;" class="admin-feature-list">
            <li>Zarządzanie meczami i turniejami</li>
            <li>Administracja użytkownikami</li>
            <li>System ligowy</li>
            <li>Obsługa zgłoszeń BOK</li>
            <li>Statystyki i raporty</li>
        </ul>
    </div>

    <script>
        (function () {
            const CHAT_URL = '/api/admin_chat_messages.php';
            const CHAT_FILE_URL = '/api/admin_chat_file.php';
            const TASKS_URL = '/api/admin_tasks.php';
            const ADMINS_URL = '/api/admin_admins.php';
            const TYPING_URL = '/api/admin_chat_typing.php';

            const CURRENT_USERNAME = <?php echo json_encode($_SESSION['username'] ?? ''); ?>;

            const chatBox = document.getElementById('adminChatMessages');
            const chatStatus = document.getElementById('adminChatStatus');
            const chatError = document.getElementById('adminChatError');
            const chatForm = document.getElementById('adminChatForm');
            const chatInput = document.getElementById('adminChatInput');
            const chatSend = document.getElementById('adminChatSend');
            const chatAttachBtn = document.getElementById('adminChatAttachBtn');
            const chatFile = document.getElementById('adminChatFile');
            const chatFileChip = document.getElementById('adminChatFileChip');
            const chatFileChipName = document.getElementById('adminChatFileChipName');
            const chatFileChipRemove = document.getElementById('adminChatFileChipRemove');
            const mentionBox = document.getElementById('adminMentionBox');
            const replyBar = document.getElementById('adminChatReplyBar');
            const replyTitle = document.getElementById('adminChatReplyTitle');
            const replySnippet = document.getElementById('adminChatReplySnippet');
            const replyCancel = document.getElementById('adminChatReplyCancel');

            const tasksStatus = document.getElementById('adminTasksStatus');
            const tasksError = document.getElementById('adminTasksError');
            const tasksList = document.getElementById('adminTasksList');
            const taskForm = document.getElementById('adminTaskForm');
            const taskSubmit = document.getElementById('adminTaskSubmit');
            const taskFile = document.getElementById('adminTaskFile');
            const taskFileName = document.getElementById('adminTaskFileName');
            const taskFilesList = document.getElementById('adminTaskFilesList');

            const taskTitleInput = document.getElementById('adminTaskTitle');
            const taskDescInput = document.getElementById('adminTaskDescription');
            const taskTitleHint = document.getElementById('adminTaskTitleHint');
            const taskDescHint = document.getElementById('adminTaskDescriptionHint');
            const chatInputHint = document.getElementById('adminChatInputHint');

            const LIMITS = {
                chatMessage: 1500,
                taskTitle: 100,
                taskDescription: 1000,
                taskComment: 2000
            };

            const TASK_ATTACHMENTS = {
                maxCount: 10,
                maxFileBytes: 20 * 1024 * 1024
            };

            function bytesToMbText(bytes) {
                return (bytes / 1024 / 1024).toFixed(1) + ' MB';
            }

            function validateTaskFiles(filesArr) {
                const files = Array.isArray(filesArr) ? filesArr : [];

                if (files.length > TASK_ATTACHMENTS.maxCount) {
                    return 'Maksymalnie ' + String(TASK_ATTACHMENTS.maxCount) + ' załączników';
                }

                for (const f of files) {
                    const size = f && typeof f.size === 'number' ? f.size : 0;
                    if (size > TASK_ATTACHMENTS.maxFileBytes) {
                        return 'Plik "' + (f.name || 'załącznik') + '" ma ' + bytesToMbText(size) + ', a limit to 20 MB';
                    }
                }

                return null;
            }

            function updateInputFiles(inputEl, filesArr) {
                if (!inputEl) return;
                if (typeof DataTransfer === 'undefined') return;
                const dt = new DataTransfer();
                (filesArr || []).forEach(function (file) {
                    dt.items.add(file);
                });
                inputEl.files = dt.files;
            }

            function makeFilesController(inputEl, labelEl, listEl, emptyText) {
                const state = {
                    files: []
                };

                function keyOf(file) {
                    return [file.name || '', file.size || 0, file.lastModified || 0].join('::');
                }

                function render() {
                    if (labelEl) {
                        if (state.files.length === 0) {
                            labelEl.textContent = emptyText;
                        } else {
                            labelEl.textContent = 'Wybrano (' + state.files.length + '): ' + state.files.map(function (f) {
                                return f.name;
                            }).join(', ');
                        }
                    }

                    if (!listEl) return;
                    listEl.innerHTML = '';

                    state.files.forEach(function (file, idx) {
                        const row = document.createElement('div');
                        row.className = 'admin-task-file-item';

                        const name = document.createElement('span');
                        name.className = 'admin-task-file-item-name';
                        name.textContent = file.name + ' (' + bytesToMbText(file.size) + ')';

                        const removeBtn = document.createElement('button');
                        removeBtn.type = 'button';
                        removeBtn.className = 'admin-task-file-item-remove';
                        removeBtn.setAttribute('aria-label', 'Usuń plik');
                        removeBtn.textContent = '×';
                        removeBtn.addEventListener('click', function () {
                            state.files.splice(idx, 1);
                            updateInputFiles(inputEl, state.files);
                            render();
                        });

                        row.appendChild(name);
                        row.appendChild(removeBtn);
                        listEl.appendChild(row);
                    });
                }

                function handleInputChange() {
                    const picked = inputEl && inputEl.files ? Array.from(inputEl.files) : [];
                    if (picked.length === 0) {
                        render();
                        return;
                    }

                    const existing = new Set(state.files.map(keyOf));
                    const merged = state.files.slice();
                    picked.forEach(function (f) {
                        const k = keyOf(f);
                        if (!existing.has(k)) {
                            merged.push(f);
                            existing.add(k);
                        }
                    });

                    const err = validateTaskFiles(merged);
                    if (err) {
                        showError(tasksError, err);
                        updateInputFiles(inputEl, state.files);
                        render();
                        return;
                    }

                    clearError(tasksError);
                    state.files = merged;
                    updateInputFiles(inputEl, state.files);
                    render();
                }

                if (inputEl) {
                    inputEl.addEventListener('change', handleInputChange);
                }

                return {
                    getFiles: function () { return state.files.slice(); },
                    clear: function () {
                        state.files = [];
                        if (inputEl) inputEl.value = '';
                        updateInputFiles(inputEl, state.files);
                        render();
                    },
                    render: render,
                    setFiles: function (filesArr) {
                        state.files = Array.isArray(filesArr) ? filesArr.slice() : [];
                        updateInputFiles(inputEl, state.files);
                        render();
                    }
                };
            }

            function pulseRedBorder(inputEl) {
                if (!inputEl) return;
                try {
                    inputEl.classList.remove('admin-pulse-red');
                    void inputEl.offsetWidth; // restart animation
                    inputEl.classList.add('admin-pulse-red');

                    if (inputEl._pulseRedTimer) clearTimeout(inputEl._pulseRedTimer);
                    inputEl._pulseRedTimer = setTimeout(function () {
                        inputEl.classList.remove('admin-pulse-red');
                    }, 1000);
                } catch (e) {
                }
            }

            function wireCharHint(inputEl, hintEl, maxLen) {
                if (!inputEl || !hintEl) return { update: function () { } };

                function update() {
                    const val = (inputEl.value || '').toString();
                    const len = val.length;
                    const remaining = maxLen - len;

                    hintEl.classList.toggle('over', remaining < 0);
                    hintEl.classList.toggle('warn', remaining >= 0 && remaining <= Math.max(20, Math.floor(maxLen * 0.1)));

                    if (remaining < 0) {
                        hintEl.textContent = 'Za dużo znaków: ' + String(len) + '/' + String(maxLen) + ' (usuń ' + String(Math.abs(remaining)) + ')';
                        return;
                    }
                    if (remaining <= Math.max(20, Math.floor(maxLen * 0.1))) {
                        hintEl.textContent = 'Uwaga: kończy się miejsce — ' + String(len) + '/' + String(maxLen);
                        return;
                    }
                    hintEl.textContent = String(len) + '/' + String(maxLen);
                }

                inputEl.addEventListener('input', update);
                update();
                return { update };
            }

            const taskTitleCounter = wireCharHint(taskTitleInput, taskTitleHint, LIMITS.taskTitle);
            const taskDescCounter = wireCharHint(taskDescInput, taskDescHint, LIMITS.taskDescription);
            const chatCounter = wireCharHint(chatInput, chatInputHint, LIMITS.chatMessage);
            const taskCreateFilesCtrl = makeFilesController(taskFile, taskFileName, taskFilesList, 'Nie wybrano plików');
            taskCreateFilesCtrl.render();

            let chatOldestId = null;
            let chatNewestId = null;
            let chatLoadingOlder = false;
            let chatHasMore = true;
            let chatPollingTimer = null;
            let chatEditsPollingTimer = null;
            let typingPollingTimer = null;
            let chatBaseStatus = 'OK';

            let chatSending = false;
            let chatLastEditsPollAt = Math.max(0, Math.floor(Date.now() / 1000) - 10);

            let replyTo = null;
            let admins = [];
            let mentionActiveIndex = 0;
            let mentionVisible = false;

            const audioNewMessage = new Audio('/sounds/newMessage.wav');
            const audioTyping = new Audio('/sounds/typing.wav');
            audioNewMessage.preload = 'auto';
            audioTyping.preload = 'auto';
            audioTyping.volume = 0.4;
            let typingStopTimer = null;
            let lastTypingPingAt = 0;

            // Typing bubble state (single bubble, newest starter wins)
            let typingBubbleEl = null;
            let typingBubbleUserId = null;
            let typingState = new Map(); // userId -> { username, seenAtMs }
            let typingSuppressUntil = new Map(); // userId -> ms

            function tryPlay(audio) {
                try {
                    audio.currentTime = 0;
                    const p = audio.play();
                    if (p && typeof p.catch === 'function') p.catch(function () { });
                } catch (e) { }
            }

            function playTypingSoundFor3s() {
                tryPlay(audioTyping);
                if (typingStopTimer) clearTimeout(typingStopTimer);
                typingStopTimer = setTimeout(function () {
                    try {
                        audioTyping.pause();
                        audioTyping.currentTime = 0;
                    } catch (e) { }
                }, 3000);
            }

            function renderTypingBubble(username) {
                const wrap = document.createElement('div');
                wrap.className = 'admin-chat-message typing';
                wrap.dataset.id = 'typing';

                const row = document.createElement('div');
                row.className = 'admin-chat-typing-row';

                const name = document.createElement('div');
                name.className = 'admin-chat-typing-name';
                name.textContent = username || 'admin';

                const dots = document.createElement('div');
                dots.className = 'admin-chat-typing-dots';
                dots.innerHTML = '<span></span><span></span><span></span>';

                row.appendChild(name);
                row.appendChild(dots);
                wrap.appendChild(row);
                return wrap;
            }

            function hideTypingBubble() {
                if (typingBubbleEl && typingBubbleEl.parentNode) {
                    typingBubbleEl.parentNode.removeChild(typingBubbleEl);
                }
                typingBubbleEl = null;
                typingBubbleUserId = null;
            }

            function showTypingBubbleFor(userId, username) {
                if (!userId) return;
                if (typingBubbleUserId === userId && typingBubbleEl) {
                    const nameEl = typingBubbleEl.querySelector('.admin-chat-typing-name');
                    if (nameEl) nameEl.textContent = username || 'admin';
                    return;
                }

                hideTypingBubble();
                typingBubbleUserId = userId;
                typingBubbleEl = renderTypingBubble(username);

                const stick = isNearTop(chatBox);
                chatBox.insertBefore(typingBubbleEl, chatBox.firstChild);
                if (stick) chatBox.scrollTop = 0;
            }

            function ensureTypingBubbleAtTop() {
                if (!typingBubbleEl) return;
                if (typingBubbleEl.parentNode !== chatBox) return;
                if (chatBox.firstElementChild !== typingBubbleEl) {
                    chatBox.insertBefore(typingBubbleEl, chatBox.firstChild);
                }
            }

            function refreshTypingBubbleFromState() {
                if (!typingState || typingState.size === 0) {
                    hideTypingBubble();
                    return;
                }

                // pick newest starter
                let pickId = null;
                let pickName = null;
                let pickSeen = -1;
                typingState.forEach(function (v, k) {
                    if (v && typeof v.seenAtMs === 'number' && v.seenAtMs > pickSeen) {
                        pickSeen = v.seenAtMs;
                        pickId = k;
                        pickName = v.username;
                    }
                });

                if (!pickId) {
                    hideTypingBubble();
                    return;
                }
                showTypingBubbleFor(pickId, pickName);
                ensureTypingBubbleAtTop();
            }

            function showError(el, msg) {
                el.style.display = 'block';
                el.textContent = msg;
            }

            function clearError(el) {
                el.style.display = 'none';
                el.textContent = '';
            }

            // Modal helpers (task edit + nicer confirms)
            let modalOverlay = null;
            let modalTitleEl = null;
            let modalBodyEl = null;
            let modalFooterEl = null;
            let modalState = null;

            function ensureModal() {
                if (modalOverlay) return;
                modalOverlay = document.createElement('div');
                modalOverlay.className = 'admin-modal-overlay';

                const card = document.createElement('div');
                card.className = 'admin-modal';
                card.setAttribute('role', 'dialog');
                card.setAttribute('aria-modal', 'true');

                const header = document.createElement('div');
                header.className = 'admin-modal-header';

                modalTitleEl = document.createElement('h3');
                modalTitleEl.className = 'admin-modal-title';

                const closeBtn = document.createElement('button');
                closeBtn.type = 'button';
                closeBtn.className = 'admin-modal-close';
                closeBtn.setAttribute('aria-label', 'Zamknij');
                closeBtn.textContent = '×';
                closeBtn.addEventListener('click', function () {
                    if (modalState && typeof modalState.onCancel === 'function') {
                        modalState.onCancel();
                    } else {
                        closeModal();
                    }
                });

                header.appendChild(modalTitleEl);
                header.appendChild(closeBtn);

                modalBodyEl = document.createElement('div');
                modalBodyEl.className = 'admin-modal-body';

                modalFooterEl = document.createElement('div');
                modalFooterEl.className = 'admin-modal-footer';

                card.appendChild(header);
                card.appendChild(modalBodyEl);
                card.appendChild(modalFooterEl);
                modalOverlay.appendChild(card);
                document.body.appendChild(modalOverlay);

                modalOverlay.addEventListener('mousedown', function (e) {
                    if (e.target !== modalOverlay) return;
                    if (modalState && modalState.locked) return;
                    if (modalState && typeof modalState.onCancel === 'function') {
                        modalState.onCancel();
                    } else {
                        closeModal();
                    }
                });

                document.addEventListener('keydown', function (e) {
                    if (!modalOverlay || modalOverlay.style.display !== 'flex') return;
                    if (e.key === 'Escape') {
                        if (modalState && modalState.locked) return;
                        if (modalState && typeof modalState.onCancel === 'function') {
                            modalState.onCancel();
                        } else {
                            closeModal();
                        }
                    }
                });
            }

            function openModal(opts) {
                ensureModal();
                modalState = {
                    locked: !!(opts && opts.locked),
                    onCancel: (opts && typeof opts.onCancel === 'function') ? opts.onCancel : null
                };
                modalTitleEl.textContent = (opts && opts.title) ? String(opts.title) : '';
                modalBodyEl.innerHTML = '';
                modalFooterEl.innerHTML = '';

                if (opts && opts.bodyEl) {
                    modalBodyEl.appendChild(opts.bodyEl);
                }

                const buttons = (opts && Array.isArray(opts.buttons)) ? opts.buttons : [];
                buttons.forEach(function (b) {
                    const btn = document.createElement('button');
                    btn.type = 'button';
                    btn.className = b.className || 'admin-btn';
                    btn.textContent = b.text || 'OK';
                    if (b.disabled) btn.disabled = true;
                    btn.addEventListener('click', function () {
                        if (typeof b.onClick === 'function') b.onClick(btn);
                    });
                    modalFooterEl.appendChild(btn);
                });

                modalOverlay.style.display = 'flex';
                setTimeout(function () {
                    try {
                        const focusEl = (opts && opts.initialFocusEl) ? opts.initialFocusEl : null;
                        if (focusEl && typeof focusEl.focus === 'function') {
                            focusEl.focus();
                        } else {
                            const first = modalBodyEl.querySelector('input, textarea, button, select, a[href]');
                            if (first && typeof first.focus === 'function') first.focus();
                        }
                    } catch (e) { }
                }, 0);
            }

            function closeModal() {
                if (!modalOverlay) return;
                modalOverlay.style.display = 'none';
                modalState = null;
                if (modalBodyEl) modalBodyEl.innerHTML = '';
                if (modalFooterEl) modalFooterEl.innerHTML = '';
            }

            function confirmModal(opts) {
                return new Promise(function (resolve) {
                    const danger = document.createElement('div');
                    danger.className = 'admin-confirm-danger';

                    const msg = document.createElement('div');
                    msg.className = 'admin-confirm-msg';
                    msg.textContent = (opts && opts.message) ? String(opts.message) : 'Czy na pewno?';
                    danger.appendChild(msg);

                    const onCancel = function () {
                        closeModal();
                        resolve(false);
                    };

                    openModal({
                        title: (opts && opts.title) ? String(opts.title) : 'Potwierdź',
                        bodyEl: danger,
                        onCancel: onCancel,
                        buttons: [
                            {
                                text: (opts && opts.cancelText) ? String(opts.cancelText) : 'Anuluj',
                                className: 'admin-btn admin-btn-secondary',
                                onClick: onCancel
                            },
                            {
                                text: (opts && opts.confirmText) ? String(opts.confirmText) : 'Usuń',
                                className: (opts && opts.confirmClassName) ? String(opts.confirmClassName) : 'admin-btn admin-btn-danger',
                                onClick: function () {
                                    closeModal();
                                    resolve(true);
                                }
                            }
                        ]
                    });
                });
            }

            // Chat textarea auto-grow (min=button height, max=150px)
            const CHAT_TEXTAREA_MAX_PX = 150;
            function autosizeChatInput() {
                if (!chatInput) return;
                try {
                    const btnH = chatAttachBtn ? chatAttachBtn.getBoundingClientRect().height : 42;
                    const minPx = Math.max(36, Math.round(btnH || 42));

                    chatInput.style.height = minPx + 'px';
                    chatInput.style.overflowY = 'hidden';

                    chatInput.style.height = 'auto';
                    const scrollH = chatInput.scrollHeight || minPx;
                    const nextH = Math.max(minPx, Math.min(scrollH, CHAT_TEXTAREA_MAX_PX));
                    chatInput.style.height = nextH + 'px';
                    chatInput.style.overflowY = (scrollH > CHAT_TEXTAREA_MAX_PX) ? 'auto' : 'hidden';
                } catch (e) { }
            }

            function isNearBottom(container, px = 60) {
                return (container.scrollHeight - (container.scrollTop + container.clientHeight)) < px;
            }

            function isNearTop(container, px = 60) {
                return (container.scrollTop || 0) < px;
            }

            function formatTime(ts) {
                return ts ? ts.replace('T', ' ').slice(0, 19) : '';
            }

            function isEdited(msg) {
                const u = (msg && msg.updated_at) ? String(msg.updated_at) : '';
                const c = (msg && msg.created_at) ? String(msg.created_at) : '';
                return !!u && u !== c;
            }

            function isHeartedMessage(msg) {
                return !!(msg && (msg.is_hearted === true || intOrZero(msg.is_hearted) === 1));
            }

            function getHeartPinText(msg) {
                const by = (msg && msg.hearted_by_username) ? String(msg.hearted_by_username) : '';
                return by ? ('❤️ Przypięte przez ' + by) : '❤️ Przypięte';
            }

            function getHeartButtonText(msg) {
                return isHeartedMessage(msg) ? 'Odepnij ❤️' : 'Przypnij ❤️';
            }

            function getMessageRenderSignature(msg) {
                const message = (msg && msg.message) ? String(msg.message) : '';
                const hasFile = !!(msg && msg.has_file);
                const fileName = (msg && msg.file_name) ? String(msg.file_name) : '';
                const fileMime = (msg && msg.file_mime) ? String(msg.file_mime) : '';
                const fileSize = (msg && msg.file_size != null) ? String(msg.file_size) : '';
                const updatedAt = (msg && msg.updated_at) ? String(msg.updated_at) : '';
                const isHearted = isHeartedMessage(msg) ? '1' : '0';
                const heartedBy = (msg && msg.hearted_by_username) ? String(msg.hearted_by_username) : '';
                const heartedAt = (msg && msg.hearted_at) ? String(msg.hearted_at) : '';
                return [message, hasFile ? '1' : '0', fileName, fileMime, fileSize, updatedAt, isHearted, heartedBy, heartedAt].join('|');
            }

            function updateMessageInDom(msg) {
                if (!msg || !msg.id) return false;
                const messageId = intOrZero(msg.id);
                if (!messageId) return false;
                const el = chatBox.querySelector('.admin-chat-message[data-id="' + String(messageId) + '"]');
                if (!el) return false;

                const incomingUpdatedAt = msg.updated_at ? String(msg.updated_at) : '';
                const currentUpdatedAt = el.dataset && el.dataset.updatedAt ? String(el.dataset.updatedAt) : '';
                if (incomingUpdatedAt && currentUpdatedAt && incomingUpdatedAt === currentUpdatedAt) {
                    return false;
                }

                const incomingSig = getMessageRenderSignature(msg);
                const currentSig = el.dataset && el.dataset.renderSig ? String(el.dataset.renderSig) : '';
                if (incomingSig && currentSig && incomingSig === currentSig) {
                    return false;
                }

                function isPreviewableImageAttachment(m) {
                    const mime = ((m && m.file_mime) ? String(m.file_mime) : '').toLowerCase();
                    if (mime === 'image/jpeg' || mime === 'image/jpg' || mime === 'image/png' || mime === 'image/gif') return true;
                    const name = ((m && m.file_name) ? String(m.file_name) : '').toLowerCase();
                    return /\.(jpe?g|png|gif)$/.test(name);
                }

                // Don't overwrite while the local user is editing
                if (el.dataset.editing === '1') return false;

                const recalledText = 'Wiadomość cofnięta';
                const isRecalled = ((msg.message || '').toString() === recalledText);
                el.classList.toggle('recalled', isRecalled);
                const isHearted = isHeartedMessage(msg);
                el.classList.toggle('hearted', isHearted);

                let heartPinEl = el.querySelector('.admin-chat-heart-pin');
                if (isHearted) {
                    if (!heartPinEl) {
                        heartPinEl = document.createElement('div');
                        heartPinEl.className = 'admin-chat-heart-pin';
                        const headerEl = el.querySelector('.admin-chat-header');
                        const insertAfterHeader = headerEl && headerEl.nextSibling ? headerEl.nextSibling : null;
                        if (insertAfterHeader) {
                            el.insertBefore(heartPinEl, insertAfterHeader);
                        } else {
                            el.appendChild(heartPinEl);
                        }
                    }
                    heartPinEl.textContent = getHeartPinText(msg);
                } else if (heartPinEl) {
                    heartPinEl.remove();
                }

                let textEl = el.querySelector('.admin-chat-text-main');
                const wantsText = ((msg.message || '').toString() !== '');
                if (!textEl && wantsText) {
                    textEl = document.createElement('div');
                    textEl.className = 'admin-chat-text admin-chat-text-main';
                    const existingAtt = el.querySelector('.admin-chat-attachment');
                    const actionsEl = el.querySelector('.admin-chat-actions');
                    const insertBefore = existingAtt || actionsEl || null;
                    el.insertBefore(textEl, insertBefore);
                }
                if (textEl && wantsText) {
                    textEl.innerHTML = '';
                    textEl.appendChild(renderTextWithMentions(msg.message || ''));
                }

                const timeEl = el.querySelector('.admin-chat-time');
                if (timeEl) {
                    const base = formatTime(msg.created_at);
                    timeEl.textContent = base + (isEdited(msg) ? ' • edytowano' : '');
                }

                // Attachment update
                const actionsEl = el.querySelector('.admin-chat-actions');
                const existingAtt = el.querySelector('.admin-chat-attachment');
                if (msg.has_file) {
                    const newAtt = document.createElement('div');
                    newAtt.className = 'admin-chat-attachment';

                    const fileUrl = CHAT_FILE_URL + '?id=' + encodeURIComponent(msg.id);
                    if (isPreviewableImageAttachment(msg)) {
                        const a = document.createElement('a');
                        a.href = fileUrl;
                        a.download = msg.file_name ? String(msg.file_name) : 'zalacznik';
                        const img = document.createElement('img');
                        img.alt = msg.file_name ? ('Załącznik: ' + msg.file_name) : 'Załącznik';
                        img.src = fileUrl + '&inline=1';
                        a.appendChild(img);
                        newAtt.appendChild(a);
                    } else {
                        const a = document.createElement('a');
                        a.href = fileUrl;
                        a.download = msg.file_name ? String(msg.file_name) : 'zalacznik';
                        a.textContent = 'Pobierz plik' + (msg.file_name ? (': ' + msg.file_name) : '');
                        newAtt.appendChild(a);
                    }

                    if (existingAtt) {
                        existingAtt.replaceWith(newAtt);
                    } else {
                        if (actionsEl) {
                            el.insertBefore(newAtt, actionsEl);
                        } else {
                            el.appendChild(newAtt);
                        }
                    }
                } else {
                    if (existingAtt) existingAtt.remove();
                }

                // Action buttons state
                const editBtn = el.querySelector('.admin-chat-action-btn[data-action="edit"]');
                const recallBtn = el.querySelector('.admin-chat-action-btn[data-action="recall"]');
                const heartBtn = el.querySelector('.admin-chat-action-btn[data-action="heart"]');
                if (editBtn) editBtn.style.display = isRecalled ? 'none' : '';
                if (recallBtn) recallBtn.style.display = isRecalled ? 'none' : '';
                if (heartBtn) {
                    heartBtn.textContent = getHeartButtonText(msg);
                    heartBtn.classList.toggle('active-heart', isHearted);
                }

                el.dataset.updatedAt = msg.updated_at ? String(msg.updated_at) : '';
                el.dataset.renderSig = incomingSig;
                return true;
            }

            function renderMentionsOnly(text) {
                const frag = document.createDocumentFragment();
                const s = (text || '').toString();
                const re = /(@[A-Za-z0-9._-]+)/g;
                let last = 0;
                let m;
                while ((m = re.exec(s)) !== null) {
                    if (m.index > last) {
                        frag.appendChild(document.createTextNode(s.slice(last, m.index)));
                    }
                    const span = document.createElement('span');
                    span.className = 'admin-mention';
                    span.textContent = m[1];
                    frag.appendChild(span);
                    last = m.index + m[1].length;
                }
                if (last < s.length) {
                    frag.appendChild(document.createTextNode(s.slice(last)));
                }
                return frag;
            }

            function renderTextWithMentions(text) {
                const frag = document.createDocumentFragment();
                const s = (text || '').toString();
                const urlRe = /(https?:\/\/[^\s<>"'`]+|www\.[^\s<>"'`]+)/gi;

                function splitTrailingPunctuation(urlText) {
                    let clean = urlText;
                    let trailing = '';
                    while (clean && /[.,!?;:)]$/.test(clean)) {
                        trailing = clean.slice(-1) + trailing;
                        clean = clean.slice(0, -1);
                    }
                    return { clean, trailing };
                }

                let lastIndex = 0;
                let match;
                while ((match = urlRe.exec(s)) !== null) {
                    const start = match.index;
                    const raw = match[0] || '';

                    if (start > lastIndex) {
                        frag.appendChild(renderMentionsOnly(s.slice(lastIndex, start)));
                    }

                    const parts = splitTrailingPunctuation(raw);
                    const linkText = parts.clean;
                    const trailing = parts.trailing;

                    if (linkText) {
                        const href = /^https?:\/\//i.test(linkText) ? linkText : ('https://' + linkText);
                        const a = document.createElement('a');
                        a.className = 'admin-chat-link';
                        a.href = href;
                        a.target = '_blank';
                        a.rel = 'noopener noreferrer';
                        a.textContent = linkText;
                        frag.appendChild(a);
                    }

                    if (trailing) {
                        frag.appendChild(document.createTextNode(trailing));
                    }

                    lastIndex = start + raw.length;
                }

                if (lastIndex < s.length) {
                    frag.appendChild(renderMentionsOnly(s.slice(lastIndex)));
                }

                return frag;
            }

            function setReplyToMessage(msg) {
                replyTo = msg && msg.id ? msg.id : null;
                if (!replyTo) {
                    replyBar.style.display = 'none';
                    return;
                }
                const username = (msg && msg.username) ? String(msg.username) : 'admin';
                replyTitle.textContent = 'Odpowiadasz do: ' + username;

                const hasAttachment = !!(msg && msg.has_file);
                if (hasAttachment) {
                    replySnippet.textContent = 'odpowiedź na wiadomość ' + username;
                } else {
                    const snipRaw = (msg && msg.message ? String(msg.message) : '').replace(/\s+/g, ' ').trim();
                    if (!snipRaw) {
                        replySnippet.textContent = '(bez tekstu)';
                    } else {
                        replySnippet.textContent = snipRaw.length > 150 ? (snipRaw.slice(0, 150) + '...') : snipRaw;
                    }
                }
                replyBar.style.display = 'flex';
                chatInput.focus();
            }

            function clearReply() {
                replyTo = null;
                replyBar.style.display = 'none';
                replyTitle.textContent = 'Odpowiadasz…';
                replySnippet.textContent = '';
            }

            function renderChatMessage(msg) {
                const wrap = document.createElement('div');
                wrap.className = 'admin-chat-message';
                wrap.dataset.id = msg.id;
                wrap.dataset.updatedAt = msg.updated_at ? String(msg.updated_at) : '';
                wrap.dataset.renderSig = getMessageRenderSignature(msg);

                const recalledText = 'Wiadomość cofnięta';
                const isRecalled = ((msg.message || '').toString() === recalledText);
                if (isRecalled) wrap.classList.add('recalled');

                if (CURRENT_USERNAME && msg.username && msg.username === CURRENT_USERNAME) {
                    wrap.classList.add('me');
                }

                const header = document.createElement('div');
                header.className = 'admin-chat-header';

                const user = document.createElement('div');
                user.className = 'admin-chat-user';
                user.textContent = msg.username || 'admin';

                const time = document.createElement('div');
                time.className = 'admin-chat-time';
                time.textContent = formatTime(msg.created_at) + (isEdited(msg) ? ' • edytowano' : '');

                header.appendChild(user);
                header.appendChild(time);
                wrap.appendChild(header);

                if (isHeartedMessage(msg)) {
                    const pin = document.createElement('div');
                    pin.className = 'admin-chat-heart-pin';
                    pin.textContent = getHeartPinText(msg);
                    wrap.appendChild(pin);
                }

                if (msg.reply_to_id && (msg.reply_username || msg.reply_message)) {
                    const quote = document.createElement('div');
                    quote.className = 'admin-chat-reply-quote';

                    const meta = document.createElement('div');
                    meta.className = 'admin-muted';
                    meta.textContent = 'Odpowiedź do: ' + (msg.reply_username || '');

                    const q = document.createElement('div');
                    q.className = 'admin-chat-text admin-chat-text-quote';
                    q.style.marginTop = '0';
                    q.textContent = (msg.reply_message || '').toString().slice(0, 220);

                    quote.appendChild(meta);
                    quote.appendChild(q);
                    wrap.appendChild(quote);
                }

                if ((msg.message || '') !== '') {
                    const text = document.createElement('div');
                    text.className = 'admin-chat-text admin-chat-text-main';
                    text.appendChild(renderTextWithMentions(msg.message));
                    wrap.appendChild(text);
                }

                function isPreviewableImageAttachment(m) {
                    const mime = ((m && m.file_mime) ? String(m.file_mime) : '').toLowerCase();
                    if (mime === 'image/jpeg' || mime === 'image/jpg' || mime === 'image/png' || mime === 'image/gif') return true;
                    const name = ((m && m.file_name) ? String(m.file_name) : '').toLowerCase();
                    return /\.(jpe?g|png|gif)$/.test(name);
                }

                if (msg.has_file) {
                    const att = document.createElement('div');
                    att.className = 'admin-chat-attachment';

                    const fileUrl = CHAT_FILE_URL + '?id=' + encodeURIComponent(msg.id);
                    if (isPreviewableImageAttachment(msg)) {
                        const a = document.createElement('a');
                        a.href = fileUrl;
                        a.download = msg.file_name ? String(msg.file_name) : 'zalacznik';
                        const img = document.createElement('img');
                        img.alt = msg.file_name ? ('Załącznik: ' + msg.file_name) : 'Załącznik';
                        img.src = fileUrl + '&inline=1';
                        a.appendChild(img);
                        att.appendChild(a);
                    } else {
                        const a = document.createElement('a');
                        a.href = fileUrl;
                        a.download = msg.file_name ? String(msg.file_name) : 'zalacznik';
                        a.textContent = 'Pobierz plik' + (msg.file_name ? (': ' + msg.file_name) : '');
                        att.appendChild(a);
                    }

                    wrap.appendChild(att);
                }

                const actions = document.createElement('div');
                actions.className = 'admin-chat-actions';

                const replyBtn = document.createElement('button');
                replyBtn.type = 'button';
                replyBtn.className = 'admin-chat-action-btn';
                replyBtn.dataset.action = 'reply';
                replyBtn.textContent = 'Odpowiedz';
                replyBtn.addEventListener('click', function () {
                    setReplyToMessage(msg);
                });
                actions.appendChild(replyBtn);

                const heartBtn = document.createElement('button');
                heartBtn.type = 'button';
                heartBtn.className = 'admin-chat-action-btn';
                heartBtn.dataset.action = 'heart';
                heartBtn.textContent = getHeartButtonText(msg);
                heartBtn.classList.toggle('active-heart', isHeartedMessage(msg));
                heartBtn.addEventListener('click', function () {
                    toggleMessageHeart(wrap, msg);
                });
                actions.appendChild(heartBtn);

                if (!isRecalled && CURRENT_USERNAME && msg.username && msg.username === CURRENT_USERNAME && (msg.message || '').toString().trim() !== '') {
                    const editBtn = document.createElement('button');
                    editBtn.type = 'button';
                    editBtn.className = 'admin-chat-action-btn';
                    editBtn.dataset.action = 'edit';
                    editBtn.textContent = 'Edytuj';
                    editBtn.addEventListener('click', function () {
                        openInlineEdit(wrap, msg);
                    });
                    actions.appendChild(editBtn);
                }

                if (!isRecalled && CURRENT_USERNAME && msg.username && msg.username === CURRENT_USERNAME) {
                    const recallBtn = document.createElement('button');
                    recallBtn.type = 'button';
                    recallBtn.className = 'admin-chat-action-btn';
                    recallBtn.dataset.action = 'recall';
                    recallBtn.textContent = 'Cofnij';
                    recallBtn.addEventListener('click', function () {
                        recallChatMessage(wrap, msg);
                    });
                    actions.appendChild(recallBtn);
                }

                wrap.appendChild(actions);

                return wrap;
            }

            let activeInlineEditId = null;

            async function toggleMessageHeart(messageEl, msg) {
                try {
                    const id = msg && msg.id ? intOrZero(msg.id) : 0;
                    if (!id) return;

                    const heartBtn = messageEl ? messageEl.querySelector('.admin-chat-action-btn[data-action="heart"]') : null;
                    if (heartBtn) heartBtn.disabled = true;

                    const res = await fetch(CHAT_URL, {
                        method: 'POST',
                        credentials: 'same-origin',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ action: 'toggle_heart', id: id })
                    });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd zmiany serduszka');

                    const updated = json.data;
                    if (updated) {
                        msg.is_hearted = !!updated.is_hearted;
                        msg.hearted_by_user_id = updated.hearted_by_user_id;
                        msg.hearted_by_username = updated.hearted_by_username;
                        msg.hearted_at = updated.hearted_at;
                        msg.updated_at = updated.updated_at;
                        updateMessageInDom(updated);
                    }
                } catch (e) {
                    showError(chatError, 'Czat: ' + (e && e.message ? e.message : 'nieznany błąd'));
                } finally {
                    const heartBtn = messageEl ? messageEl.querySelector('.admin-chat-action-btn[data-action="heart"]') : null;
                    if (heartBtn) heartBtn.disabled = false;
                }
            }

            async function recallChatMessage(messageEl, msg) {
                try {
                    const ok = await confirmModal({
                        title: 'Cofnij wiadomość',
                        message: 'Treść wiadomości i załącznik zostaną usunięte. Pozostanie tylko: "Wiadomość cofnięta". Kontynuować?',
                        confirmText: 'Cofnij',
                        confirmClassName: 'admin-btn admin-btn-danger'
                    });
                    if (!ok) return;

                    if (messageEl && messageEl.dataset && messageEl.dataset.editing === '1') {
                        closeInlineEdit(messageEl);
                    }

                    const id = msg && msg.id ? (intOrZero(msg.id)) : 0;
                    if (!id) return;

                    const res = await fetch(CHAT_URL, {
                        method: 'POST',
                        credentials: 'same-origin',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ action: 'recall', id: id })
                    });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd cofania');

                    const updated = json.data;
                    if (updated) {
                        msg.message = updated.message;
                        msg.updated_at = updated.updated_at;
                        msg.created_at = updated.created_at;
                        msg.has_file = !!updated.has_file;
                        msg.file_name = updated.file_name;
                        msg.file_mime = updated.file_mime;
                        msg.file_size = updated.file_size;
                        updateMessageInDom(updated);
                    }
                } catch (e) {
                    showError(chatError, 'Czat: ' + (e && e.message ? e.message : 'nieznany błąd'));
                }
            }

            function intOrZero(v) {
                const n = parseInt(String(v), 10);
                return Number.isFinite(n) ? n : 0;
            }

            function openInlineEdit(messageEl, msg) {
                if (!messageEl || !msg || !msg.id) return;
                const id = msg.id;

                if (activeInlineEditId && activeInlineEditId !== id) {
                    const other = chatBox.querySelector('.admin-chat-message[data-id="' + String(activeInlineEditId) + '"]');
                    if (other) closeInlineEdit(other);
                }
                activeInlineEditId = id;

                if (messageEl.dataset.editing === '1') return;
                messageEl.dataset.editing = '1';
                messageEl.classList.add('editing');

                const textEl = messageEl.querySelector('.admin-chat-text-main');
                if (!textEl) return;

                const original = (msg.message || '').toString();
                messageEl._inlineEditOriginal = original;
                messageEl._inlineEditOriginalHtml = textEl.innerHTML;

                let selectedFile = null;
                let clearExistingFile = false;

                const ta = document.createElement('textarea');
                ta.className = 'admin-chat-inline-textarea';
                ta.value = original;
                ta.maxLength = LIMITS.chatMessage;
                ta.rows = 1;

                ta.addEventListener('keydown', function (e) {
                    const isPrintable = e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
                    if (!isPrintable) return;
                    const maxLen = LIMITS.chatMessage;
                    const val = (ta.value || '').toString();
                    const selStart = typeof ta.selectionStart === 'number' ? ta.selectionStart : val.length;
                    const selEnd = typeof ta.selectionEnd === 'number' ? ta.selectionEnd : val.length;
                    const replacing = selEnd > selStart;
                    if (!replacing && val.length >= maxLen) {
                        pulseRedBorder(ta);
                    }
                });

                ta.addEventListener('paste', function () {
                    setTimeout(function () {
                        const maxLen = LIMITS.chatMessage;
                        const val = (ta.value || '').toString();
                        if (val.length >= maxLen) pulseRedBorder(ta);
                    }, 0);
                });

                const fileInput = document.createElement('input');
                fileInput.type = 'file';
                fileInput.accept = 'image/*,application/pdf,text/plain';
                fileInput.style.display = 'none';

                const toolbar = document.createElement('div');
                toolbar.className = 'admin-chat-inline-toolbar';

                const btnCancel = document.createElement('button');
                btnCancel.type = 'button';
                btnCancel.className = 'admin-chat-inline-toolbtn';
                btnCancel.textContent = 'Anuluj';

                const btnSave = document.createElement('button');
                btnSave.type = 'button';
                btnSave.className = 'admin-chat-inline-toolbtn';
                btnSave.textContent = 'Zapisz';

                toolbar.appendChild(btnCancel);
                toolbar.appendChild(btnSave);

                const err = document.createElement('div');
                err.className = 'admin-chat-inline-error';

                const filebar = document.createElement('div');
                filebar.className = 'admin-chat-inline-filebar';

                const fileName = document.createElement('div');
                fileName.className = 'name';

                const fileActions = document.createElement('div');
                fileActions.className = 'actions';

                const btnPickFile = document.createElement('button');
                btnPickFile.type = 'button';
                btnPickFile.className = 'admin-chat-inline-toolbtn';
                btnPickFile.textContent = (msg.has_file ? 'Zmień plik' : 'Dodaj plik');

                const btnRemoveFile = document.createElement('button');
                btnRemoveFile.type = 'button';
                btnRemoveFile.className = 'admin-chat-inline-toolbtn';
                btnRemoveFile.textContent = 'Usuń plik';

                fileActions.appendChild(btnPickFile);
                fileActions.appendChild(btnRemoveFile);
                filebar.appendChild(fileName);
                filebar.appendChild(fileActions);

                function refreshFileUi() {
                    const hasOrig = !!msg.has_file;
                    const hasSel = !!selectedFile;
                    const cleared = !!clearExistingFile;

                    if (hasSel) {
                        fileName.textContent = 'Nowy: ' + (selectedFile.name || 'plik');
                    } else if (hasOrig && !cleared) {
                        fileName.textContent = 'Aktualny: ' + (msg.file_name || 'załącznik');
                    } else if (hasOrig && cleared) {
                        fileName.textContent = 'Załącznik zostanie usunięty';
                    } else {
                        fileName.textContent = 'Brak załącznika';
                    }

                    btnRemoveFile.style.display = (hasOrig || hasSel) ? 'inline' : 'none';
                    btnPickFile.textContent = (hasOrig || hasSel) ? 'Zmień plik' : 'Dodaj plik';
                }

                textEl.innerHTML = '';
                textEl.appendChild(ta);
                textEl.appendChild(fileInput);
                textEl.appendChild(toolbar);
                textEl.appendChild(filebar);
                textEl.appendChild(err);

                refreshFileUi();

                btnPickFile.addEventListener('click', function () {
                    fileInput.click();
                });

                btnRemoveFile.addEventListener('click', function () {
                    selectedFile = null;
                    try { fileInput.value = ''; } catch (e) { }
                    if (msg.has_file) {
                        clearExistingFile = true;
                    }
                    refreshFileUi();
                    clearInlineError();
                });

                fileInput.addEventListener('change', function () {
                    const f = fileInput.files && fileInput.files[0] ? fileInput.files[0] : null;
                    if (!f) return;
                    selectedFile = f;
                    clearExistingFile = false;
                    refreshFileUi();
                    clearInlineError();
                });

                function autosizeInline() {
                    try {
                        ta.style.height = 'auto';
                        const h = Math.min(150, Math.max(24, ta.scrollHeight || 24));
                        ta.style.height = h + 'px';
                    } catch (e) { }
                }

                function showInlineError(message) {
                    err.style.display = 'block';
                    err.textContent = message;
                }

                function clearInlineError() {
                    err.style.display = 'none';
                    err.textContent = '';
                }

                async function save() {
                    clearInlineError();
                    const newMsg = (ta.value || '').toString();
                    const trimmed = newMsg.trim();
                    if (!trimmed) {
                        showInlineError('Wiadomość nie może być pusta');
                        return;
                    }
                    if (newMsg.length > LIMITS.chatMessage) {
                        showInlineError('Wiadomość jest zbyt długa (max ' + String(LIMITS.chatMessage) + ')');
                        return;
                    }
                    const fileChanged = !!selectedFile || !!clearExistingFile;
                    if (newMsg === original && !fileChanged) {
                        closeInlineEdit(messageEl);
                        return;
                    }

                    btnSave.disabled = true;
                    btnCancel.disabled = true;
                    btnPickFile.disabled = true;
                    btnRemoveFile.disabled = true;
                    try {
                        let res;
                        if (selectedFile || clearExistingFile) {
                            const fd = new FormData();
                            fd.append('action', 'update');
                            fd.append('id', String(id));
                            fd.append('message', newMsg);
                            if (clearExistingFile) fd.append('clear_file', '1');
                            if (selectedFile) fd.append('file', selectedFile, selectedFile.name);
                            res = await fetch(CHAT_URL, { method: 'POST', credentials: 'same-origin', body: fd });
                        } else {
                            res = await fetch(CHAT_URL, {
                                method: 'POST',
                                credentials: 'same-origin',
                                headers: { 'Content-Type': 'application/json' },
                                body: JSON.stringify({ action: 'update', id: id, message: newMsg })
                            });
                        }
                        const json = await res.json();
                        if (!json.success) throw new Error(json.error || 'Błąd edycji');

                        const updated = json.data;
                        if (updated) {
                            msg.message = updated.message;
                            msg.updated_at = updated.updated_at;
                            msg.created_at = updated.created_at;
                            msg.has_file = !!updated.has_file;
                            msg.file_name = updated.file_name;
                            msg.file_mime = updated.file_mime;
                            msg.file_size = updated.file_size;
                        }

                        closeInlineEdit(messageEl);
                        if (updated) updateMessageInDom(updated);
                    } catch (e) {
                        showInlineError('Nie udało się zapisać: ' + (e && e.message ? e.message : 'błąd'));
                    } finally {
                        btnSave.disabled = false;
                        btnCancel.disabled = false;
                        btnPickFile.disabled = false;
                        btnRemoveFile.disabled = false;
                    }
                }

                btnCancel.addEventListener('click', function () {
                    closeInlineEdit(messageEl);
                });

                btnSave.addEventListener('click', function () {
                    save();
                });

                ta.addEventListener('input', function () {
                    autosizeInline();
                    clearInlineError();
                });

                ta.addEventListener('keydown', function (e) {
                    if (e.key === 'Escape') {
                        e.preventDefault();
                        closeInlineEdit(messageEl);
                        return;
                    }
                    // Enter saves, Shift+Enter adds newline
                    if (e.key === 'Enter' && !e.shiftKey) {
                        e.preventDefault();
                        save();
                    }
                });

                setTimeout(function () {
                    try {
                        autosizeInline();
                        ta.focus();
                        ta.setSelectionRange(ta.value.length, ta.value.length);
                    } catch (e) { }
                }, 0);
            }

            function closeInlineEdit(messageEl) {
                if (!messageEl) return;
                if (messageEl.dataset.editing !== '1') return;

                const textEl = messageEl.querySelector('.admin-chat-text-main');
                if (textEl) {
                    // restore original HTML to keep bubble looking the same
                    if (typeof messageEl._inlineEditOriginalHtml === 'string') {
                        textEl.innerHTML = messageEl._inlineEditOriginalHtml;
                    } else {
                        textEl.innerHTML = '';
                        textEl.appendChild(renderTextWithMentions((messageEl._inlineEditOriginal || '').toString()));
                    }
                }

                messageEl._inlineEditOriginal = null;
                messageEl._inlineEditOriginalHtml = null;
                messageEl.dataset.editing = '0';
                messageEl.classList.remove('editing');
                activeInlineEditId = null;
            }

            function appendMessages(msgs) {
                if (!Array.isArray(msgs) || msgs.length === 0) return;

                const wasNearTop = isNearTop(chatBox);
                const ordered = msgs.slice().sort(function (a, b) {
                    return intOrZero(a && a.id) - intOrZero(b && b.id);
                });

                let hasOther = false;
                for (const m of ordered) {
                    if (!m || !m.id) continue;
                    const messageId = intOrZero(m.id);
                    if (!messageId) continue;
                    m.id = messageId;

                    // If the typer sends a message, remove their typing bubble immediately
                    if (typingBubbleUserId && m.user_id && String(m.user_id) === String(typingBubbleUserId)) {
                        typingSuppressUntil.set(String(typingBubbleUserId), Date.now() + 3500);
                        typingState.delete(String(typingBubbleUserId));
                        hideTypingBubble();
                    }

                    if (chatBox.querySelector('.admin-chat-message[data-id="' + String(messageId) + '"]')) {
                        updateMessageInDom(m);
                    } else {
                        if (m.username && CURRENT_USERNAME && m.username !== CURRENT_USERNAME) {
                            hasOther = true;
                        }
                        const messageEl = renderChatMessage(m);
                        if (typingBubbleEl && typingBubbleEl.parentNode === chatBox) {
                            if (typingBubbleEl.nextSibling) {
                                chatBox.insertBefore(messageEl, typingBubbleEl.nextSibling);
                            } else {
                                chatBox.appendChild(messageEl);
                            }
                        } else {
                            chatBox.insertBefore(messageEl, chatBox.firstChild);
                        }
                    }

                    chatNewestId = (!chatNewestId || messageId > chatNewestId) ? messageId : chatNewestId;
                    if (chatOldestId === null) chatOldestId = messageId;
                }

                if (hasOther) {
                    tryPlay(audioNewMessage);
                }

                ensureTypingBubbleAtTop();

                if (wasNearTop) {
                    chatBox.scrollTop = 0;
                }
            }

            function prependMessages(msgsOldToNew) {
                if (!Array.isArray(msgsOldToNew) || msgsOldToNew.length === 0) return;

                const prevScrollTop = chatBox.scrollTop;

                const frag = document.createDocumentFragment();
                const oldestId = intOrZero(msgsOldToNew[0] && msgsOldToNew[0].id);
                if (oldestId) chatOldestId = oldestId;
                for (const m of msgsOldToNew) {
                    const messageId = intOrZero(m && m.id);
                    if (!messageId) continue;
                    m.id = messageId;
                    frag.appendChild(renderChatMessage(m));
                    if (chatNewestId === null) chatNewestId = messageId;
                }

                chatBox.appendChild(frag);
                chatBox.scrollTop = prevScrollTop;
            }

            async function loadInitialChat() {
                clearError(chatError);
                chatStatus.textContent = 'Ładowanie…';
                try {
                    const res = await fetch(CHAT_URL, { credentials: 'same-origin' });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd API');

                    const rowsDesc = json.data || [];
                    const rows = rowsDesc.slice().reverse();

                    chatBox.innerHTML = '';
                    chatOldestId = null;
                    chatNewestId = null;
                    chatHasMore = !!json.hasMore;

                    appendMessages(rows);
                    chatBox.scrollTop = 0;

                    chatBaseStatus = 'OK';
                    chatStatus.textContent = chatBaseStatus;
                } catch (e) {
                    chatBaseStatus = 'Błąd';
                    chatStatus.textContent = chatBaseStatus;
                    showError(chatError, 'Czat: ' + (e && e.message ? e.message : 'nieznany błąd'));
                }
            }

            async function loadOlderChat() {
                if (chatLoadingOlder || !chatHasMore || !chatOldestId) return;
                chatLoadingOlder = true;
                chatStatus.textContent = 'Wczytywanie starszych…';
                try {
                    const url = CHAT_URL + '?before_id=' + encodeURIComponent(chatOldestId) + '&limit=100';
                    const res = await fetch(url, { credentials: 'same-origin' });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd API');

                    const rowsDesc = json.data || [];
                    prependMessages(rowsDesc);
                    chatHasMore = !!json.hasMore;
                    chatBaseStatus = chatHasMore ? 'OK' : 'Początek historii';
                    chatStatus.textContent = chatBaseStatus;
                } catch (e) {
                    chatBaseStatus = 'Błąd';
                    chatStatus.textContent = chatBaseStatus;
                    showError(chatError, 'Czat: ' + (e && e.message ? e.message : 'nieznany błąd'));
                } finally {
                    chatLoadingOlder = false;
                }
            }

            async function pollNewChat() {
                if (!chatNewestId) return;
                try {
                    const url = CHAT_URL + '?after_id=' + encodeURIComponent(chatNewestId) + '&limit=200';
                    const res = await fetch(url, { credentials: 'same-origin' });
                    const json = await res.json();
                    if (!json.success) return;
                    const rows = json.data || [];
                    if (rows.length > 0) {
                        clearError(chatError);
                        const shouldStickToTop = isNearTop(chatBox);
                        appendMessages(rows);
                        if (shouldStickToTop) {
                            chatBox.scrollTop = 0;
                        }
                    }
                } catch (e) {
                }
            }

            async function pollEditedChat() {
                let nextPollCursor = chatLastEditsPollAt;
                try {
                    const url = CHAT_URL + '?updated_after=' + encodeURIComponent(String(chatLastEditsPollAt)) + '&limit=200';
                    const res = await fetch(url, { credentials: 'same-origin' });
                    const json = await res.json();
                    if (!json.success) return;
                    const rows = json.data || [];
                    if (rows.length > 0) {
                        for (const r of rows) {
                            updateMessageInDom(r);
                            const ts = r && r.updated_at_ts ? intOrZero(r.updated_at_ts) : 0;
                            if (ts > nextPollCursor) nextPollCursor = ts;
                        }
                    }
                } catch (e) {
                } finally {
                    chatLastEditsPollAt = Math.max(chatLastEditsPollAt, nextPollCursor);
                }
            }

            function getMentionQuery() {
                const value = (chatInput.value || '').toString();
                const pos = chatInput.selectionStart || 0;
                const before = value.slice(0, pos);

                const at = before.lastIndexOf('@');
                if (at < 0) return null;
                if (at > 0 && !/\s/.test(before.charAt(at - 1))) return null;
                const tail = before.slice(at + 1);
                if (/\s/.test(tail)) return null;

                return { atIndex: at, query: tail };
            }

            function hideMentions() {
                mentionVisible = false;
                mentionActiveIndex = 0;
                mentionBox.style.display = 'none';
                mentionBox.innerHTML = '';
            }

            function showMentions(items) {
                if (!items || items.length === 0) {
                    hideMentions();
                    return;
                }

                mentionVisible = true;
                mentionBox.style.display = 'block';
                mentionBox.innerHTML = '';

                items.forEach(function (u, idx) {
                    const el = document.createElement('div');
                    el.className = 'admin-mention-item' + (idx === mentionActiveIndex ? ' active' : '');
                    el.textContent = '@' + u;
                    el.addEventListener('mousedown', function (e) {
                        e.preventDefault();
                        insertMention(u);
                    });
                    mentionBox.appendChild(el);
                });
            }

            function refreshMentionBox() {
                const info = getMentionQuery();
                if (!info) {
                    hideMentions();
                    return;
                }
                const q = (info.query || '').toLowerCase();
                const names = admins.map(function (a) { return a.username; });
                const filtered = names.filter(function (u) {
                    return u && u.toLowerCase().indexOf(q) === 0;
                }).slice(0, 8);
                mentionActiveIndex = 0;
                showMentions(filtered);
            }

            function insertMention(username) {
                const info = getMentionQuery();
                if (!info) return;
                const value = (chatInput.value || '').toString();
                const pos = chatInput.selectionStart || 0;
                const before = value.slice(0, info.atIndex);
                const after = value.slice(pos);
                const mentionText = '@' + username + ' ';
                chatInput.value = before + mentionText + after;
                const newPos = (before + mentionText).length;
                chatInput.setSelectionRange(newPos, newPos);
                hideMentions();
                chatInput.focus();
                chatCounter.update();
            }

            async function sendChatMessage() {
                if (chatSending) return;
                clearError(chatError);
                chatSend.disabled = true;
                if (chatAttachBtn) chatAttachBtn.disabled = true;
                chatSending = true;
                try {
                    const fd = new FormData();
                    const msg = (chatInput.value || '').toString();
                    const trimmed = msg.trim();

                    if (msg.length > LIMITS.chatMessage) {
                        throw new Error('Wiadomość jest zbyt długa (max ' + String(LIMITS.chatMessage) + ' znaków)');
                    }

                    const file = chatFile && chatFile.files && chatFile.files[0] ? chatFile.files[0] : null;

                    if (!trimmed && !file) {
                        chatSend.disabled = false;
                        if (chatAttachBtn) chatAttachBtn.disabled = false;
                        chatSending = false;
                        return;
                    }

                    fd.append('message', msg);
                    if (replyTo) fd.append('reply_to_id', String(replyTo));
                    if (file) fd.append('file', file, file.name);

                    const res = await fetch(CHAT_URL, {
                        method: 'POST',
                        credentials: 'same-origin',
                        body: fd
                    });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd zapisu');

                    const m = json.data;
                    if (m) {
                        appendMessages([m]);
                        chatBox.scrollTop = 0;
                        tryPlay(audioNewMessage);
                    }

                    chatInput.value = '';
                    autosizeChatInput();
                    chatCounter.update();
                    if (chatFile) chatFile.value = '';
                    if (chatFileChip) chatFileChip.style.display = 'none';
                    if (chatFileChipName) chatFileChipName.textContent = 'Załącznik';
                    clearReply();
                    hideMentions();
                    chatInput.focus();
                } catch (e) {
                    showError(chatError, 'Nie udało się wysłać: ' + (e && e.message ? e.message : 'nieznany błąd'));
                } finally {
                    chatSend.disabled = false;
                    if (chatAttachBtn) chatAttachBtn.disabled = false;
                    chatSending = false;
                }
            }

            async function loadTasks() {
                clearError(tasksError);
                tasksStatus.textContent = 'Ładowanie…';
                try {
                    const res = await fetch(TASKS_URL + '?limit=50', { credentials: 'same-origin' });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd API');

                    const rows = json.data || [];
                    tasksList.innerHTML = '';
                    if (rows.length === 0) {
                        const empty = document.createElement('div');
                        empty.className = 'admin-empty';
                        empty.textContent = 'Brak tasków';
                        tasksList.appendChild(empty);
                    }
                    for (const t of rows) {
                        const item = document.createElement('div');
                        item.className = 'admin-item ' + (t.is_done ? 'done' : 'todo');

                        const header = document.createElement('div');
                        header.className = 'admin-item-header';

                        const title = document.createElement('div');
                        title.className = 'admin-item-title';
                        title.textContent = t.title || '(bez tytułu)';

                        const badges = document.createElement('div');
                        badges.className = 'admin-item-badges';

                        const statusBadge = document.createElement('span');
                        statusBadge.className = 'admin-badge ' + (t.is_done ? 'done' : 'todo');
                        statusBadge.textContent = t.is_done ? 'Wykonane' : 'Do zrobienia';
                        badges.appendChild(statusBadge);

                        header.appendChild(title);
                        header.appendChild(badges);

                        const meta = document.createElement('div');
                        meta.className = 'admin-item-meta';
                        const by = t.created_by_username || 'admin';
                        const at = formatTime(t.created_at);
                        if (t.is_done) {
                            const doneBy = t.done_by_username || 'admin';
                            const doneAt = formatTime(t.done_at);
                            meta.textContent = by + ' • ' + at + ' • wykonane przez: ' + doneBy + (doneAt ? (' • ' + doneAt) : '');
                        } else {
                            meta.textContent = by + ' • ' + at;
                        }

                        const desc = document.createElement('div');
                        desc.className = 'admin-item-desc';
                        desc.textContent = t.description || '';

                        item.appendChild(header);
                        item.appendChild(meta);
                        if ((t.description || '') !== '') item.appendChild(desc);

                        const attachments = Array.isArray(t.attachments) ? t.attachments : [];
                        if (attachments.length > 0) {
                            const filesWrap = document.createElement('div');
                            filesWrap.style.marginTop = '8px';

                            for (const att of attachments) {
                                const link = document.createElement('a');
                                link.href = (att && att.download_url) ? String(att.download_url) : ('/api/admin_task_file.php?id=' + encodeURIComponent(t.id));
                                link.target = '_blank';
                                link.rel = 'noopener';
                                link.style.display = 'block';
                                link.style.marginTop = '4px';
                                link.textContent = 'Pobierz plik' + (att && att.name ? ': ' + String(att.name) : '');
                                filesWrap.appendChild(link);
                            }

                            item.appendChild(filesWrap);
                        }

                        const actions = document.createElement('div');
                        actions.className = 'admin-task-actions';

                        const btnToggle = document.createElement('button');
                        btnToggle.type = 'button';
                        btnToggle.className = 'admin-btn admin-btn-small ' + (t.is_done ? 'admin-btn-secondary' : 'admin-btn-success');
                        btnToggle.textContent = t.is_done ? 'Oznacz jako do zrobienia' : 'Oznacz jako wykonane';
                        btnToggle.addEventListener('click', function () {
                            toggleTaskDone(t.id, !t.is_done);
                        });

                        const btnEdit = document.createElement('button');
                        btnEdit.type = 'button';
                        btnEdit.className = 'admin-btn admin-btn-small admin-btn-secondary';
                        btnEdit.textContent = 'Edytuj';
                        btnEdit.addEventListener('click', function () {
                            openTaskEditor(item, t);
                        });

                        const btnComments = document.createElement('button');
                        btnComments.type = 'button';
                        btnComments.className = 'admin-btn admin-btn-small admin-btn-secondary';
                        const commentsCount = Number(t.comments_count || 0);
                        btnComments.textContent = 'Komentarze (' + String(commentsCount) + ')';
                        btnComments.addEventListener('click', function () {
                            openTaskComments(t);
                        });

                        const btnDel = document.createElement('button');
                        btnDel.type = 'button';
                        btnDel.className = 'admin-btn admin-btn-small admin-btn-danger';
                        btnDel.textContent = 'Usuń';
                        btnDel.addEventListener('click', function () {
                            deleteTask(t.id);
                        });

                        actions.appendChild(btnToggle);
                        actions.appendChild(btnComments);
                        actions.appendChild(btnEdit);
                        actions.appendChild(btnDel);
                        item.appendChild(actions);

                        tasksList.appendChild(item);
                    }

                    tasksStatus.textContent = 'OK (' + rows.length + ')';
                } catch (e) {
                    tasksStatus.textContent = 'Błąd';
                    showError(tasksError, 'Notatki: ' + (e && e.message ? e.message : 'nieznany błąd'));
                }
            }

            async function toggleTaskDone(id, isDone) {
                clearError(tasksError);
                tasksStatus.textContent = 'Zapisywanie…';
                try {
                    const res = await fetch(TASKS_URL, {
                        method: 'POST',
                        credentials: 'same-origin',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ action: 'toggle_done', id: id, is_done: !!isDone })
                    });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd');
                    await loadTasks();
                    tasksStatus.textContent = 'OK';
                } catch (e) {
                    tasksStatus.textContent = 'Błąd';
                    showError(tasksError, 'Nie udało się zmienić statusu: ' + (e && e.message ? e.message : 'nieznany błąd'));
                }
            }

            async function deleteTask(id) {
                clearError(tasksError);
                const ok = await confirmModal({
                    title: 'Usuwanie taska',
                    message: 'Czy na pewno chcesz usunąć ten task? Tej operacji nie da się cofnąć.',
                    cancelText: 'Anuluj',
                    confirmText: 'Usuń',
                    confirmClassName: 'admin-btn admin-btn-danger'
                });
                if (!ok) return;
                tasksStatus.textContent = 'Usuwanie…';
                try {
                    const res = await fetch(TASKS_URL, {
                        method: 'POST',
                        credentials: 'same-origin',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ action: 'delete', id: id })
                    });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd');
                    await loadTasks();
                    tasksStatus.textContent = 'OK';
                } catch (e) {
                    tasksStatus.textContent = 'Błąd';
                    showError(tasksError, 'Nie udało się usunąć: ' + (e && e.message ? e.message : 'nieznany błąd'));
                }
            }

            function openTaskEditor(_containerEl, task) {
                const wrap = document.createElement('div');
                wrap.className = 'admin-task-edit';

                const errorInline = document.createElement('div');
                errorInline.className = 'admin-status';
                errorInline.style.display = 'none';
                errorInline.style.color = '#b32d2e';

                const labTitle = document.createElement('label');
                labTitle.textContent = 'Tytuł';
                const inpTitle = document.createElement('input');
                inpTitle.type = 'text';
                inpTitle.value = (task.title || '').toString();
                inpTitle.placeholder = 'Tytuł';
                inpTitle.maxLength = LIMITS.taskTitle;

                const titleHint = document.createElement('div');
                titleHint.className = 'admin-charhint';

                const labDesc = document.createElement('label');
                labDesc.textContent = 'Opis';
                const taDesc = document.createElement('textarea');
                taDesc.value = (task.description || '').toString();
                taDesc.placeholder = 'Opis';
                taDesc.maxLength = LIMITS.taskDescription;

                const descHint = document.createElement('div');
                descHint.className = 'admin-charhint';

                const attachments = Array.isArray(task.attachments) ? task.attachments : [];

                const labFile = document.createElement('label');
                labFile.textContent = 'Dodaj nowe pliki (opcjonalnie)';
                const file = document.createElement('input');
                file.type = 'file';
                file.name = 'files[]';
                file.multiple = true;

                const newFilesInfo = document.createElement('div');
                newFilesInfo.className = 'admin-muted';
                newFilesInfo.style.fontSize = '12px';
                newFilesInfo.style.marginTop = '4px';
                newFilesInfo.textContent = 'Nie wybrano nowych plików';
                const newFilesList = document.createElement('div');
                newFilesList.className = 'admin-task-files-list';

                const editFilesCtrl = makeFilesController(file, newFilesInfo, newFilesList, 'Nie wybrano nowych plików');
                editFilesCtrl.render();

                const existingWrap = document.createElement('div');
                existingWrap.style.marginTop = '8px';
                const pendingDeleteIds = new Set();
                let pendingClearLegacy = false;

                function renderExistingAttachments() {
                    existingWrap.innerHTML = '';

                    const visible = attachments.filter(function (att) {
                        const isLegacy = !!(att && att.legacy);
                        const fid = (att && att.id != null) ? Number(att.id) : null;
                        if (isLegacy) return !pendingClearLegacy;
                        if (fid !== null && !Number.isNaN(fid)) return !pendingDeleteIds.has(fid);
                        return true;
                    });

                    if (visible.length === 0) {
                        const noExisting = document.createElement('div');
                        noExisting.className = 'admin-muted';
                        noExisting.textContent = 'Brak aktualnych załączników';
                        existingWrap.appendChild(noExisting);
                        return;
                    }

                    const existingTitle = document.createElement('div');
                    existingTitle.className = 'admin-muted';
                    existingTitle.style.marginBottom = '6px';
                    existingTitle.textContent = 'Aktualne załączniki';
                    existingWrap.appendChild(existingTitle);

                    visible.forEach(function (att) {
                        const row = document.createElement('div');
                        row.className = 'admin-task-file-item';

                        const name = document.createElement('span');
                        name.className = 'admin-task-file-item-name';
                        name.textContent = (att && att.name) ? String(att.name) : 'załącznik';

                        const removeBtn = document.createElement('button');
                        removeBtn.type = 'button';
                        removeBtn.className = 'admin-task-file-item-remove';
                        removeBtn.setAttribute('aria-label', 'Usuń załącznik');
                        removeBtn.textContent = '×';
                        removeBtn.addEventListener('click', function () {
                            if (att && att.legacy) {
                                pendingClearLegacy = true;
                            } else if (att && att.id != null) {
                                pendingDeleteIds.add(Number(att.id));
                            }
                            renderExistingAttachments();
                        });

                        row.appendChild(name);
                        row.appendChild(removeBtn);
                        existingWrap.appendChild(row);
                    });
                }

                renderExistingAttachments();

                wrap.appendChild(errorInline);
                wrap.appendChild(labTitle);
                wrap.appendChild(inpTitle);
                wrap.appendChild(titleHint);
                wrap.appendChild(labDesc);
                wrap.appendChild(taDesc);
                wrap.appendChild(descHint);
                wrap.appendChild(labFile);
                wrap.appendChild(file);
                wrap.appendChild(newFilesInfo);
                wrap.appendChild(newFilesList);
                wrap.appendChild(existingWrap);

                const modalTitleCounter = wireCharHint(inpTitle, titleHint, LIMITS.taskTitle);
                const modalDescCounter = wireCharHint(taDesc, descHint, LIMITS.taskDescription);

                const cancel = function () {
                    closeModal();
                };

                openModal({
                    title: 'Edycja taska #' + String(task.id),
                    bodyEl: wrap,
                    onCancel: cancel,
                    initialFocusEl: inpTitle,
                    buttons: [
                        {
                            text: 'Anuluj',
                            className: 'admin-btn admin-btn-secondary',
                            onClick: cancel
                        },
                        {
                            text: 'Zapisz',
                            className: 'admin-btn admin-btn-success',
                            onClick: async function (btn) {
                                errorInline.style.display = 'none';
                                errorInline.textContent = '';

                                modalTitleCounter.update();
                                modalDescCounter.update();

                                const tTitle = (inpTitle.value || '').toString();
                                const tDesc = (taDesc.value || '').toString();
                                if (tTitle.trim() === '') {
                                    errorInline.style.display = 'block';
                                    errorInline.textContent = 'Tytuł jest wymagany';
                                    return;
                                }
                                if (tTitle.length > LIMITS.taskTitle) {
                                    errorInline.style.display = 'block';
                                    errorInline.textContent = 'Tytuł jest zbyt długi (max ' + String(LIMITS.taskTitle) + ' znaków)';
                                    return;
                                }
                                if (tDesc.length > LIMITS.taskDescription) {
                                    errorInline.style.display = 'block';
                                    errorInline.textContent = 'Opis jest zbyt długi (max ' + String(LIMITS.taskDescription) + ' znaków)';
                                    return;
                                }

                                btn.disabled = true;
                                tasksStatus.textContent = 'Zapisywanie…';
                                try {
                                    const fd = new FormData();
                                    fd.append('action', 'update');
                                    fd.append('id', String(task.id));
                                    fd.append('title', tTitle);
                                    fd.append('description', tDesc);

                                    const deleteIds = Array.from(pendingDeleteIds);
                                    const clearLegacy = pendingClearLegacy;

                                    const selectedFiles = editFilesCtrl.getFiles();
                                    const validationError = validateTaskFiles(selectedFiles);
                                    if (validationError) {
                                        throw new Error(validationError);
                                    }

                                    const existingCount = attachments.length;
                                    const deleteCount = deleteIds.length + (clearLegacy ? 1 : 0);
                                    const totalAfterSave = Math.max(0, existingCount - deleteCount) + selectedFiles.length;
                                    if (totalAfterSave > TASK_ATTACHMENTS.maxCount) {
                                        throw new Error('Task może mieć maksymalnie ' + String(TASK_ATTACHMENTS.maxCount) + ' załączników');
                                    }

                                    if (clearLegacy) fd.append('clear_file', '1');
                                    deleteIds.forEach(function (id) {
                                        fd.append('delete_file_ids[]', String(id));
                                    });

                                    if (selectedFiles.length > 0) {
                                        selectedFiles.forEach(function (f) {
                                            fd.append('files[]', f, f.name);
                                        });
                                    }

                                    const res = await fetch(TASKS_URL, {
                                        method: 'POST',
                                        credentials: 'same-origin',
                                        body: fd
                                    });
                                    const json = await res.json();
                                    if (!json.success) throw new Error(json.error || 'Błąd zapisu');
                                    closeModal();
                                    await loadTasks();
                                    tasksStatus.textContent = 'OK';
                                } catch (e) {
                                    tasksStatus.textContent = 'Błąd';
                                    errorInline.style.display = 'block';
                                    errorInline.textContent = 'Nie udało się zapisać: ' + (e && e.message ? e.message : 'nieznany błąd');
                                } finally {
                                    btn.disabled = false;
                                }
                            }
                        }
                    ]
                });
            }

            function openTaskComments(task) {
                const wrap = document.createElement('div');
                wrap.className = 'admin-task-comments-wrap';

                const info = document.createElement('div');
                info.className = 'admin-muted';
                info.textContent = 'Wątek komentarzy dla taska #' + String(task.id);

                const errorInline = document.createElement('div');
                errorInline.className = 'admin-status';
                errorInline.style.display = 'none';
                errorInline.style.color = '#b32d2e';

                const list = document.createElement('div');
                list.className = 'admin-task-comments-list';

                const form = document.createElement('div');
                form.className = 'admin-task-comments-form';

                const commentInput = document.createElement('textarea');
                commentInput.maxLength = LIMITS.taskComment;
                commentInput.placeholder = 'Dodaj komentarz do tego taska…';

                const commentHint = document.createElement('div');
                commentHint.className = 'admin-charhint';

                const actions = document.createElement('div');
                actions.style.display = 'flex';
                actions.style.justifyContent = 'space-between';
                actions.style.gap = '8px';

                const refreshBtn = document.createElement('button');
                refreshBtn.type = 'button';
                refreshBtn.className = 'admin-btn admin-btn-secondary';
                refreshBtn.textContent = 'Odśwież';

                const addBtn = document.createElement('button');
                addBtn.type = 'button';
                addBtn.className = 'admin-btn admin-btn-success';
                addBtn.textContent = 'Dodaj komentarz';

                actions.appendChild(refreshBtn);
                actions.appendChild(addBtn);

                form.appendChild(commentInput);
                form.appendChild(commentHint);
                form.appendChild(actions);

                wrap.appendChild(info);
                wrap.appendChild(errorInline);
                wrap.appendChild(list);
                wrap.appendChild(form);

                const commentCounter = wireCharHint(commentInput, commentHint, LIMITS.taskComment);

                async function fetchComments() {
                    try {
                        const res = await fetch(TASKS_URL, {
                            method: 'POST',
                            credentials: 'same-origin',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ action: 'list_comments', task_id: task.id })
                        });
                        const json = await res.json();
                        if (!json.success) throw new Error(json.error || 'Błąd API');
                        return Array.isArray(json.data) ? json.data : [];
                    } catch (e) {
                        throw e;
                    }
                }

                function renderComments(rows) {
                    list.innerHTML = '';
                    if (!rows || rows.length === 0) {
                        const empty = document.createElement('div');
                        empty.className = 'admin-empty';
                        empty.textContent = 'Brak komentarzy w tym tasku';
                        list.appendChild(empty);
                        return;
                    }

                    rows.forEach(function (row) {
                        const item = document.createElement('div');
                        item.className = 'admin-task-comment-item';

                        const head = document.createElement('div');
                        head.className = 'admin-task-comment-head';

                        const who = document.createElement('strong');
                        who.textContent = (row && row.username) ? String(row.username) : 'admin';

                        const right = document.createElement('div');
                        right.style.display = 'inline-flex';
                        right.style.alignItems = 'center';
                        right.style.gap = '8px';

                        const at = document.createElement('span');
                        at.textContent = formatTime(row && row.created_at ? String(row.created_at) : '');

                        const delBtn = document.createElement('button');
                        delBtn.type = 'button';
                        delBtn.className = 'admin-btn admin-btn-small admin-btn-danger';
                        delBtn.textContent = 'Usuń';
                        delBtn.addEventListener('click', async function () {
                            errorInline.style.display = 'none';
                            try {
                                const ok = await confirmModal({
                                    title: 'Usuwanie komentarza',
                                    message: 'Usunąć ten komentarz?',
                                    cancelText: 'Anuluj',
                                    confirmText: 'Usuń',
                                    confirmClassName: 'admin-btn admin-btn-danger'
                                });
                                if (!ok) return;

                                const res = await fetch(TASKS_URL, {
                                    method: 'POST',
                                    credentials: 'same-origin',
                                    headers: { 'Content-Type': 'application/json' },
                                    body: JSON.stringify({ action: 'delete_comment', comment_id: row.id })
                                });
                                const json = await res.json();
                                if (!json.success) throw new Error(json.error || 'Nie udało się usunąć komentarza');
                                await reload();
                            } catch (e) {
                                errorInline.style.display = 'block';
                                errorInline.textContent = e && e.message ? e.message : 'Błąd usuwania komentarza';
                            }
                        });

                        right.appendChild(at);
                        right.appendChild(delBtn);
                        head.appendChild(who);
                        head.appendChild(right);

                        const text = document.createElement('div');
                        text.className = 'admin-task-comment-text';
                        text.textContent = (row && row.comment) ? String(row.comment) : '';

                        item.appendChild(head);
                        item.appendChild(text);
                        list.appendChild(item);
                    });
                }

                async function reload() {
                    const rows = await fetchComments();
                    renderComments(rows);
                }

                refreshBtn.addEventListener('click', async function () {
                    errorInline.style.display = 'none';
                    try {
                        await reload();
                    } catch (e) {
                        errorInline.style.display = 'block';
                        errorInline.textContent = e && e.message ? e.message : 'Błąd odświeżania komentarzy';
                    }
                });

                addBtn.addEventListener('click', async function () {
                    errorInline.style.display = 'none';
                    commentCounter.update();
                    const txt = (commentInput.value || '').toString();
                    if (txt.trim() === '') {
                        errorInline.style.display = 'block';
                        errorInline.textContent = 'Treść komentarza jest wymagana';
                        return;
                    }
                    if (txt.length > LIMITS.taskComment) {
                        errorInline.style.display = 'block';
                        errorInline.textContent = 'Komentarz jest zbyt długi (max ' + String(LIMITS.taskComment) + ' znaków)';
                        return;
                    }

                    addBtn.disabled = true;
                    try {
                        const res = await fetch(TASKS_URL, {
                            method: 'POST',
                            credentials: 'same-origin',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({
                                action: 'add_comment',
                                task_id: task.id,
                                comment: txt
                            })
                        });
                        const json = await res.json();
                        if (!json.success) throw new Error(json.error || 'Nie udało się dodać komentarza');
                        commentInput.value = '';
                        commentCounter.update();
                        await reload();
                        await loadTasks();
                    } catch (e) {
                        errorInline.style.display = 'block';
                        errorInline.textContent = e && e.message ? e.message : 'Błąd dodawania komentarza';
                    } finally {
                        addBtn.disabled = false;
                    }
                });

                openModal({
                    title: 'Komentarze — task #' + String(task.id),
                    bodyEl: wrap,
                    initialFocusEl: commentInput,
                    buttons: [
                        {
                            text: 'Zamknij',
                            className: 'admin-btn admin-btn-secondary',
                            onClick: function () {
                                closeModal();
                            }
                        }
                    ]
                });

                reload().catch(function (e) {
                    errorInline.style.display = 'block';
                    errorInline.textContent = e && e.message ? e.message : 'Błąd ładowania komentarzy';
                });
            }

            async function createTask(formEl) {
                clearError(tasksError);
                taskSubmit.disabled = true;
                tasksStatus.textContent = 'Zapisywanie…';
                try {
                    const tTitle = (taskTitleInput && taskTitleInput.value ? taskTitleInput.value : '').toString();
                    const tDesc = (taskDescInput && taskDescInput.value ? taskDescInput.value : '').toString();
                    taskTitleCounter.update();
                    taskDescCounter.update();

                    if (tTitle.trim() === '') {
                        throw new Error('Tytuł jest wymagany');
                    }
                    if (tTitle.length > LIMITS.taskTitle) {
                        throw new Error('Tytuł jest zbyt długi (max ' + String(LIMITS.taskTitle) + ' znaków)');
                    }
                    if (tDesc.length > LIMITS.taskDescription) {
                        throw new Error('Opis jest zbyt długi (max ' + String(LIMITS.taskDescription) + ' znaków)');
                    }

                    const selectedFiles = taskCreateFilesCtrl.getFiles();
                    const validationError = validateTaskFiles(selectedFiles);
                    if (validationError) {
                        throw new Error(validationError);
                    }

                    const fd = new FormData();
                    fd.append('title', tTitle);
                    fd.append('description', tDesc);
                    selectedFiles.forEach(function (f) {
                        fd.append('files[]', f, f.name);
                    });
                    const res = await fetch(TASKS_URL, {
                        method: 'POST',
                        credentials: 'same-origin',
                        body: fd
                    });
                    const json = await res.json();
                    if (!json.success) throw new Error(json.error || 'Błąd zapisu');

                    formEl.reset();
                    taskCreateFilesCtrl.clear();
                    taskTitleCounter.update();
                    taskDescCounter.update();
                    await loadTasks();
                    tasksStatus.textContent = 'Dodano';
                } catch (e) {
                    tasksStatus.textContent = 'Błąd';
                    showError(tasksError, 'Nie udało się dodać: ' + (e && e.message ? e.message : 'nieznany błąd'));
                } finally {
                    taskSubmit.disabled = false;
                }
            }

            async function loadAdmins() {
                try {
                    const res = await fetch(ADMINS_URL, { credentials: 'same-origin' });
                    const json = await res.json();
                    if (!json.success) return;
                    admins = json.data || [];
                } catch (e) {
                }
            }

            function pingTypingThrottled() {
                const now = Date.now();
                if (now - lastTypingPingAt < 350) return;
                lastTypingPingAt = now;
                fetch(TYPING_URL, { method: 'POST', credentials: 'same-origin' }).catch(function () { });
            }

            async function pollTyping() {
                try {
                    const res = await fetch(TYPING_URL, { credentials: 'same-origin', cache: 'no-store' });
                    const json = await res.json();
                    if (!json.success) return;
                    const rows = json.data || [];

                    const now = Date.now();
                    // Cleanup suppress list
                    typingSuppressUntil.forEach(function (until, uid) {
                        if (!until || now >= until) typingSuppressUntil.delete(uid);
                    });

                    const clientTtlMs = 3000;
                    for (const r of rows) {
                        const uid = r && r.user_id ? String(r.user_id) : '';
                        if (!uid) continue;
                        if (typingSuppressUntil.has(uid)) continue;
                        const seenAtMs = r && r.updated_at_ts ? (parseInt(r.updated_at_ts, 10) * 1000) : now;
                        if (!typingState.has(uid)) {
                            typingState.set(uid, { username: (r.username || 'admin'), seenAtMs });
                        } else {
                            const prev = typingState.get(uid);
                            if (prev) {
                                prev.seenAtMs = seenAtMs;
                                if (!prev.username && r.username) prev.username = r.username;
                            }
                        }
                    }

                    // Remove users who stopped typing (local TTL to avoid flicker)
                    Array.from(typingState.entries()).forEach(function ([uid, state]) {
                        if (!state || !state.seenAtMs) {
                            typingState.delete(uid);
                            return;
                        }
                        if (now - state.seenAtMs > clientTtlMs) typingState.delete(uid);
                    });

                    if (typingState.size > 0) {
                        // Status text remains (optional)
                        const latest = Array.from(typingState.values()).sort(function (a, b) { return (b.seenAtMs || 0) - (a.seenAtMs || 0); })[0];
                        chatStatus.textContent = (latest && latest.username) ? (latest.username + ' pisze…') : 'Ktoś pisze…';
                        playTypingSoundFor3s();
                        refreshTypingBubbleFromState();
                    } else {
                        chatStatus.textContent = chatBaseStatus;
                        hideTypingBubble();
                    }
                } catch (e) {
                }
            }

            chatBox.addEventListener('scroll', function () {
                if (isNearBottom(chatBox)) {
                    loadOlderChat();
                }
            });

            chatForm.addEventListener('submit', function (e) {
                e.preventDefault();
                if (chatSending) return;
                sendChatMessage();
            });

            replyCancel.addEventListener('click', function () {
                clearReply();
            });

            chatInput.addEventListener('keydown', function (e) {
                // @mention navigation
                if (mentionVisible) {
                    if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
                        e.preventDefault();
                        const items = Array.from(mentionBox.querySelectorAll('.admin-mention-item'));
                        if (items.length === 0) return;
                        if (e.key === 'ArrowDown') mentionActiveIndex = Math.min(items.length - 1, mentionActiveIndex + 1);
                        if (e.key === 'ArrowUp') mentionActiveIndex = Math.max(0, mentionActiveIndex - 1);
                        items.forEach(function (el, idx) {
                            el.classList.toggle('active', idx === mentionActiveIndex);
                        });
                        return;
                    }
                    if (e.key === 'Enter') {
                        const items = Array.from(mentionBox.querySelectorAll('.admin-mention-item'));
                        const active = items[mentionActiveIndex];
                        if (active) {
                            e.preventDefault();
                            const u = (active.textContent || '').replace(/^@/, '').trim();
                            if (u) insertMention(u);
                            return;
                        }
                    }
                    if (e.key === 'Escape') {
                        hideMentions();
                        return;
                    }
                }

                // Enter = send (Shift+Enter = newline)
                if (e.key === 'Enter' && !e.shiftKey) {
                    e.preventDefault();
                    if (chatSending) return;
                    sendChatMessage();
                    return;
                }

                // typing indicator for printable keys + kasowanie
                const isPrintable = e.key && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey;
                const isDeleteKey = e.key === 'Backspace' || e.key === 'Delete';
                if (isPrintable || isDeleteKey) {
                    if (isPrintable) {
                        const maxLen = LIMITS.chatMessage;
                        const val = (chatInput.value || '').toString();
                        const selStart = typeof chatInput.selectionStart === 'number' ? chatInput.selectionStart : val.length;
                        const selEnd = typeof chatInput.selectionEnd === 'number' ? chatInput.selectionEnd : val.length;
                        const replacing = selEnd > selStart;
                        if (!replacing && val.length >= maxLen) {
                            pulseRedBorder(chatInput);
                        }
                    }
                    pingTypingThrottled();
                }
            });

            chatInput.addEventListener('paste', function () {
                setTimeout(function () {
                    const maxLen = LIMITS.chatMessage;
                    const val = (chatInput.value || '').toString();
                    if (val.length >= maxLen) pulseRedBorder(chatInput);
                }, 0);
            });

            chatInput.addEventListener('input', function () {
                autosizeChatInput();
                refreshMentionBox();
                pingTypingThrottled();
                chatCounter.update();
            });

            if (chatAttachBtn && chatFile) {
                chatAttachBtn.addEventListener('click', function () {
                    chatFile.click();
                });
            }

            if (chatFile) {
                chatFile.addEventListener('change', function () {
                    const f = chatFile.files && chatFile.files[0] ? chatFile.files[0] : null;
                    if (!f) {
                        if (chatFileChip) chatFileChip.style.display = 'none';
                        if (chatFileChipName) chatFileChipName.textContent = 'Załącznik';
                        return;
                    }
                    if (chatFileChipName) chatFileChipName.textContent = f.name;
                    if (chatFileChip) chatFileChip.style.display = 'grid';
                });
            }

            if (chatFileChipRemove && chatFile) {
                chatFileChipRemove.addEventListener('click', function () {
                    chatFile.value = '';
                    if (chatFileChip) chatFileChip.style.display = 'none';
                    if (chatFileChipName) chatFileChipName.textContent = 'Załącznik';
                    chatInput.focus();
                });
            }

            taskForm.addEventListener('submit', function (e) {
                e.preventDefault();
                createTask(taskForm);
            });

            taskCreateFilesCtrl.render();

            // Init
            loadTasks();
            loadAdmins();
            autosizeChatInput();
            loadInitialChat().then(function () {
                if (chatPollingTimer) clearInterval(chatPollingTimer);
                chatPollingTimer = setInterval(pollNewChat, 1000);
                if (typingPollingTimer) clearInterval(typingPollingTimer);
                typingPollingTimer = setInterval(pollTyping, 500);
                if (chatEditsPollingTimer) clearInterval(chatEditsPollingTimer);
                chatEditsPollingTimer = setInterval(pollEditedChat, 1000);
            });
        })();
    </script>
</div>

<?php
require_once __DIR__ . '/includes/footer.php';
?>
