Initialer Laravel Commit für BetiX
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

This commit is contained in:
2026-04-04 18:01:50 +02:00
commit 0280278978
374 changed files with 65210 additions and 0 deletions

View File

@@ -0,0 +1,761 @@
<script setup lang="ts">
import { Head, router } from '@inertiajs/vue3';
import { ref } from 'vue';
import CasinoAdminLayout from '@/layouts/admin/CasinoAdminLayout.vue';
import {
ArrowLeft, Flag, Hash, Clock, CheckCircle2, XCircle,
Ban, MessageSquareOff, AlertTriangle, History,
Calendar, Mail, Shield, Gavel, Loader2, ChevronRight,
UserRound, ShieldAlert, Star, Crown, ArrowRight
} from 'lucide-vue-next';
interface Restriction {
id: number;
type: 'chat_ban' | 'account_ban';
reason: string | null;
active: boolean;
starts_at: string | null;
ends_at: string | null;
created_at: string;
}
interface UserProfile {
id: number;
username: string;
email: string;
avatar: string | null;
avatar_url: string | null;
role: string;
vip_level: number;
is_banned: boolean;
created_at: string;
restrictions?: Restriction[];
}
interface ContextMsg {
id: string | number;
message: string;
user: { id: number; username: string };
created_at: string;
}
interface Report {
id: number;
reporter_id: number;
message_id: string;
message_text: string;
sender_id: number | null;
sender_username: string | null;
reason: string | null;
context_messages: ContextMsg[] | null;
status: 'pending' | 'reviewed' | 'dismissed';
admin_note: string | null;
created_at: string;
}
const props = defineProps<{
report: Report;
senderUser: UserProfile | null;
reporterUser: UserProfile | null;
flash?: string | null;
}>();
// ── Restriction management ────────────────────────────────────
const extendHours = ref<Record<number, number>>({});
const flashMsg = ref(props.flash || '');
function liftRestriction(id: number) {
router.post(`/admin/restrictions/${id}/lift`, {}, {
preserveScroll: true,
onSuccess: () => { flashMsg.value = 'Sperre wurde aufgehoben.'; },
});
}
function extendRestriction(id: number) {
const h = extendHours.value[id];
if (!h || h < 1) return;
router.post(`/admin/restrictions/${id}/extend`, { hours: h }, {
preserveScroll: true,
onSuccess: () => { flashMsg.value = `Sperre um ${h}h verlängert.`; extendHours.value[id] = 0; },
});
}
// ── Punishment ────────────────────────────────────────────────
const punishType = ref<'chat_ban' | 'account_ban'>('chat_ban');
const punishReason = ref('');
const punishHours = ref<number | null>(null);
const submitting = ref(false);
const chatTemplates = [
{ label: '1 Tag', hours: 24, reason: 'Spam' },
{ label: '3 Tage', hours: 72, reason: 'Beleidigung' },
{ label: '7 Tage', hours: 168, reason: 'Belästigung' },
{ label: '30 Tage', hours: 720, reason: 'Schwerer Verstoß' },
{ label: 'Permanent',hours: null, reason: 'Dauerhafter Ausschluss' },
];
const banTemplates = [
{ label: '1 Tag', hours: 24, reason: 'Betrug / Scam' },
{ label: '7 Tage', hours: 168, reason: 'Schwerer Verstoß' },
{ label: '30 Tage', hours: 720, reason: 'Wiederholte Verstöße' },
{ label: 'Permanent',hours: null, reason: 'Dauerhafter Ausschluss' },
];
function applyTemplate(reason: string, hours: number | null) {
punishReason.value = reason;
punishHours.value = hours;
}
function submitPunish() {
if (!punishReason.value || submitting.value) return;
submitting.value = true;
router.post(`/admin/reports/chat/${props.report.id}/punish`, {
type: punishType.value, reason: punishReason.value, hours: punishHours.value,
}, {
preserveScroll: true,
onSuccess: () => { flashMsg.value = 'Strafe erfolgreich verhängt!'; },
onFinish: () => { submitting.value = false; },
});
}
function updateStatus(status: string) {
router.post(`/admin/reports/chat/${props.report.id}`, { status }, { preserveScroll: true });
}
// ── Helpers ───────────────────────────────────────────────────
const statusMeta: Record<string, { color: string; bg: string; border: string; label: string }> = {
pending: { color: '#f59e0b', bg: 'rgba(245,158,11,0.12)', border: 'rgba(245,158,11,0.35)', label: 'Ausstehend' },
reviewed: { color: '#22c55e', bg: 'rgba(34,197,94,0.12)', border: 'rgba(34,197,94,0.35)', label: 'Bearbeitet' },
dismissed: { color: '#6b7280', bg: 'rgba(107,114,128,0.1)', border: 'rgba(107,114,128,0.3)', label: 'Abgelehnt' },
};
const reasonLabels: Record<string, string> = {
spam:'Spam', beleidigung:'Beleidigung', belaestigung:'Belästigung', betrug:'Betrug', sonstiges:'Sonstiges',
};
function rl(r: string | null) { return r ? (reasonLabels[r] ?? r) : null; }
function avUrl(u: UserProfile | null) { return u?.avatar_url || u?.avatar || null; }
function ini(name?: string | null) { return (name || '?')[0].toUpperCase(); }
function fmt(d: string | null) {
if (!d) return '';
return new Date(d).toLocaleString('de-DE', { day:'2-digit', month:'2-digit', year:'2-digit', hour:'2-digit', minute:'2-digit' });
}
function fmtDur(h: number | null) {
if (h === null) return 'Permanent';
if (h < 24) return `${h}h`;
return `${h/24}d`;
}
function timeLeft(d: string | null) {
if (!d) return 'Permanent';
const ms = new Date(d).getTime() - Date.now();
if (ms <= 0) return 'Abgelaufen';
const days = Math.floor(ms / 86400000);
const hrs = Math.floor((ms % 86400000) / 3600000);
return days > 0 ? `${days}T ${hrs}h` : `${hrs}h`;
}
function activeR(u: UserProfile | null) {
return (u?.restrictions ?? []).filter(r => r.active && (!r.ends_at || new Date(r.ends_at) > new Date()));
}
function allR(u: UserProfile | null) { return u?.restrictions ?? []; }
</script>
<template>
<CasinoAdminLayout>
<Head :title="`Case #${report.id}`" />
<template #title>
<div class="pt">
<a href="/admin/reports/chat" class="back"><ArrowLeft :size="14" /> Chat Reports</a>
<span class="ptdiv">/</span>
<span class="pt-case"><Hash :size="12" />{{ report.id }}</span>
<span class="status-chip" :style="{ color: statusMeta[report.status].color, background: statusMeta[report.status].bg, borderColor: statusMeta[report.status].border }">
{{ statusMeta[report.status].label }}
</span>
</div>
</template>
<!-- Flash banner -->
<transition name="fade">
<div v-if="flashMsg" class="flash">
<CheckCircle2 :size="15" />
<span>{{ flashMsg }}</span>
<button @click="flashMsg = ''"><XCircle :size="14" /></button>
</div>
</transition>
<!-- Case summary bar -->
<div class="summary-bar">
<div class="sb-item">
<span class="sb-label">Melder</span>
<span class="sb-val blue">@{{ reporterUser?.username || '' }}</span>
</div>
<ArrowRight :size="14" class="sb-arrow" />
<div class="sb-item">
<span class="sb-label">Gemeldet</span>
<span class="sb-val pink">@{{ senderUser?.username || report.sender_username || '' }}</span>
</div>
<div class="sb-sep" />
<div class="sb-item" v-if="rl(report.reason)">
<span class="sb-label">Grund</span>
<span class="sb-val"><Flag :size="11" /> {{ rl(report.reason) }}</span>
</div>
<div class="sb-item">
<span class="sb-label">Datum</span>
<span class="sb-val">{{ fmt(report.created_at) }}</span>
</div>
<div class="sb-item">
<span class="sb-label">Msg-ID</span>
<span class="sb-val mono">#{{ report.message_id }}</span>
</div>
<!-- Quick status -->
<div class="sb-status-btns">
<button :class="['ssb', { active: report.status === 'reviewed' }]" @click="updateStatus('reviewed')">
<CheckCircle2 :size="12" /> Erledigt
</button>
<button :class="['ssb dismiss', { active: report.status === 'dismissed' }]" @click="updateStatus('dismissed')">
<XCircle :size="12" /> Ablehnen
</button>
</div>
</div>
<!-- Main grid -->
<div class="main-grid">
<!-- LEFT col -->
<div class="col-left">
<!-- Reporter -->
<div class="user-card blue-top">
<div class="uc-label blue"><UserRound :size="10" /> Melder</div>
<div class="uc-row">
<div class="uc-av blue">
<img v-if="avUrl(reporterUser)" :src="avUrl(reporterUser)!" />
<span v-else>{{ ini(reporterUser?.username) }}</span>
</div>
<div class="uc-info">
<div class="uc-name">@{{ reporterUser?.username || '' }}</div>
<div class="uc-email"><Mail :size="9" /> {{ reporterUser?.email || '' }}</div>
<div class="uc-since"><Calendar :size="9" /> seit {{ reporterUser?.created_at ? new Date(reporterUser.created_at).toLocaleDateString('de-DE') : '' }}</div>
</div>
<a v-if="reporterUser" :href="`/admin/users/${reporterUser.id}`" class="uc-open" title="Profil öffnen"><ChevronRight :size="14" /></a>
</div>
<div class="uc-badges">
<span class="badge role"><Crown :size="8" /> {{ reporterUser?.role || 'user' }}</span>
<span v-if="reporterUser?.vip_level && reporterUser.vip_level > 0" class="badge vip"><Star :size="8" /> VIP {{ reporterUser.vip_level }}</span>
<span v-if="reporterUser?.is_banned" class="badge banned"><Ban :size="8" /> Gebannt</span>
<span v-if="activeR(reporterUser).some(r => r.type==='chat_ban')" class="badge cbanned"><MessageSquareOff :size="8" /> Chat-Bann</span>
</div>
<!-- Mini restriction history -->
<div v-if="allR(reporterUser).length" class="mini-hist">
<div class="mh-title"><History :size="9" /> Historie <span class="mh-count">{{ allR(reporterUser).length }}</span></div>
<div v-for="r in allR(reporterUser).slice(0,3)" :key="r.id" class="mh-row" :class="{ active: r.active }">
<span class="mh-type" :class="r.type==='chat_ban'?'chat':'ban'">{{ r.type==='chat_ban'?'Chat':'Acc' }}</span>
<span class="mh-reason">{{ r.reason || '' }}</span>
<span v-if="r.active" class="mh-live"></span>
</div>
</div>
</div>
<!-- Divider -->
<div class="reported-divider"><Flag :size="11" class="df" /> hat gemeldet</div>
<!-- Reported / Sender -->
<div class="user-card pink-top">
<div class="uc-label pink"><ShieldAlert :size="10" /> Gemeldet</div>
<div class="uc-row">
<div class="uc-av pink">
<img v-if="avUrl(senderUser)" :src="avUrl(senderUser)!" />
<span v-else>{{ ini(senderUser?.username ?? report.sender_username) }}</span>
</div>
<div class="uc-info">
<div class="uc-name">@{{ senderUser?.username || report.sender_username || '' }}</div>
<div class="uc-email"><Mail :size="9" /> {{ senderUser?.email || `ID: ${report.sender_id || ''}` }}</div>
<div v-if="senderUser" class="uc-since"><Calendar :size="9" /> seit {{ new Date(senderUser.created_at).toLocaleDateString('de-DE') }}</div>
</div>
<a v-if="senderUser" :href="`/admin/users/${senderUser.id}`" class="uc-open" title="Profil öffnen"><ChevronRight :size="14" /></a>
</div>
<div class="uc-badges">
<span v-if="senderUser" class="badge role"><Crown :size="8" /> {{ senderUser.role }}</span>
<span v-if="senderUser?.vip_level && senderUser.vip_level > 0" class="badge vip"><Star :size="8" /> VIP {{ senderUser.vip_level }}</span>
<span v-if="senderUser?.is_banned" class="badge banned"><Ban :size="8" /> Gebannt</span>
<span v-if="activeR(senderUser).some(r => r.type==='chat_ban')" class="badge cbanned"><MessageSquareOff :size="8" /> Chat-Bann</span>
</div>
<!-- Active restrictions alert -->
<div v-if="activeR(senderUser).length" class="active-alert">
<AlertTriangle :size="13" />
<div>
<div class="aa-title">{{ activeR(senderUser).length }} aktive Sperre(n)</div>
<div class="aa-list">
<div v-for="r in activeR(senderUser)" :key="r.id" class="aa-row">
<span class="mh-type" :class="r.type==='chat_ban'?'chat':'ban'">{{ r.type==='chat_ban'?'Chat-Bann':'Acc-Bann' }}</span>
<span class="aa-until">{{ r.ends_at ? timeLeft(r.ends_at) : 'Permanent' }}</span>
</div>
</div>
</div>
</div>
<!-- Full restriction history with lift/extend -->
<div v-if="allR(senderUser).length" class="restrict-hist">
<div class="mh-title"><History :size="9" /> Sperr-Historie <span class="mh-count">{{ allR(senderUser).length }}</span></div>
<div v-for="r in allR(senderUser)" :key="r.id" class="rh-item" :class="{ active: r.active }">
<div class="rh-row">
<span class="mh-type" :class="r.type==='chat_ban'?'chat':'ban'">{{ r.type==='chat_ban'?'Chat':'Acc' }}</span>
<span class="rh-reason">{{ r.reason || '' }}</span>
<span v-if="r.active" class="mh-live">AKTIV</span>
<span class="rh-date">{{ fmt(r.created_at) }}</span>
</div>
<div v-if="r.ends_at || r.active" class="rh-until">
<Clock :size="9" />
{{ r.ends_at ? fmt(r.ends_at) : 'Permanent' }}
<span v-if="r.active && r.ends_at" class="rh-left">{{ timeLeft(r.ends_at) }}</span>
</div>
<div v-if="r.active" class="rh-actions">
<button class="rha lift" @click="liftRestriction(r.id)">
<CheckCircle2 :size="10" /> Aufheben
</button>
<div class="rha-extend">
<input v-model.number="extendHours[r.id]" type="number" min="1" placeholder="Std." class="rha-input" />
<button class="rha extend" @click="extendRestriction(r.id)">
<Clock :size="10" /> Verlängern
</button>
</div>
</div>
</div>
</div>
<div v-else class="no-hist">Keine Sperr-Historie</div>
</div>
</div>
<!-- CENTER: Chat timeline -->
<div class="col-center">
<div class="ct-head">
<MessageSquareOff :size="15" class="ct-icon" />
<span>Chat-Kontext</span>
<span class="ct-sub">{{ (report.context_messages?.length ?? 0) + 1 }} Nachrichten</span>
</div>
<div class="timeline">
<div v-if="!report.context_messages?.length && !report.message_text" class="tl-empty">
Kein Kontext gespeichert.
</div>
<!-- Context messages -->
<div
v-for="cm in report.context_messages"
:key="cm.id"
class="tl-msg"
:class="{
'by-sender': cm.user?.id === report.sender_id,
'by-reporter': cm.user?.id === report.reporter_id,
}"
>
<div class="tl-av" :class="{ 'av-s': cm.user?.id === report.sender_id, 'av-r': cm.user?.id === report.reporter_id }">
{{ ini(cm.user?.username) }}
</div>
<div class="tl-body">
<div class="tl-meta">
<span class="tl-user">@{{ cm.user?.username || '?' }}</span>
<span class="tl-ts">{{ fmt(cm.created_at) }}</span>
</div>
<div class="tl-text">{{ cm.message }}</div>
</div>
</div>
<!-- Reported message -->
<div class="tl-msg reported">
<div class="rep-flag"><Flag :size="10" /></div>
<div class="tl-av av-s av-rep">{{ ini(report.sender_username) }}</div>
<div class="tl-body">
<div class="tl-meta">
<span class="tl-user rep-user">@{{ report.sender_username || '?' }}</span>
<span class="rep-badge"><Flag :size="9" /> Gemeldet</span>
<span v-if="rl(report.reason)" class="reason-badge">{{ rl(report.reason) }}</span>
<span class="tl-ts">{{ fmt(report.created_at) }}</span>
</div>
<div class="tl-text rep-text">{{ report.message_text }}</div>
</div>
</div>
</div>
</div>
<!-- RIGHT: Punishment -->
<div class="col-right">
<div class="pun-head">
<Gavel :size="15" class="pun-icon" />
<span>Strafe verhängen</span>
<span class="ct-sub" v-if="senderUser">@{{ senderUser.username }}</span>
<span class="ct-sub warn" v-else>Kein Nutzer verknüpft</span>
</div>
<div class="pun-form" :class="{ locked: !senderUser }">
<!-- Type toggle -->
<div class="type-row">
<button :class="['type-btn', { on: punishType==='chat_ban' }]" @click="punishType='chat_ban'">
<MessageSquareOff :size="12" /> Chat-Bann
</button>
<button :class="['type-btn ban', { on: punishType==='account_ban' }]" @click="punishType='account_ban'">
<Ban :size="12" /> Account-Bann
</button>
</div>
<!-- Templates -->
<div class="tpl-block">
<div class="tpl-label">Schnell-Vorlagen</div>
<div class="tpl-list">
<button
v-for="t in (punishType==='chat_ban' ? chatTemplates : banTemplates)"
:key="t.label"
class="tpl-btn"
:class="{ on: punishReason===t.reason && punishHours===t.hours, ban: punishType==='account_ban' }"
@click="applyTemplate(t.reason, t.hours)"
>
<span class="tpl-name">{{ t.label }}</span>
<span class="tpl-dur">{{ fmtDur(t.hours) }}</span>
<span class="tpl-reason">{{ t.reason }}</span>
</button>
</div>
</div>
<div class="or-line"><span>oder anpassen</span></div>
<!-- Custom -->
<div class="custom-block">
<label class="fl">
<span><Clock :size="10" /> Dauer (Stunden)</span>
<input v-model.number="punishHours" type="number" min="1" placeholder="leer = permanent" class="fi" />
</label>
<label class="fl">
<span><Shield :size="10" /> Grund</span>
<textarea v-model="punishReason" rows="2" placeholder="Begründung..." class="fi ta"></textarea>
</label>
<!-- Preview -->
<div v-if="punishReason" class="pun-preview">
<span class="pp-type" :class="punishType==='account_ban'?'ban':'chat'">
{{ punishType==='chat_ban' ? 'Chat-Bann' : 'Account-Bann' }}
</span>
<span class="pp-dur">{{ fmtDur(punishHours) }}</span>
<span v-if="punishHours" class="pp-until">
bis {{ new Date(Date.now() + (punishHours??0)*3600000).toLocaleDateString('de-DE') }}
</span>
</div>
<button
class="pun-btn"
:class="{ ban: punishType==='account_ban' }"
:disabled="!punishReason || !senderUser || submitting"
@click="submitPunish"
>
<Loader2 v-if="submitting" :size="13" class="spin" />
<Ban v-else-if="punishType==='account_ban'" :size="13" />
<MessageSquareOff v-else :size="13" />
{{ punishType==='chat_ban' ? 'Chat-Bann verhängen' : 'Account sperren' }}
</button>
</div>
</div>
</div>
</div><!-- /main-grid -->
</CasinoAdminLayout>
</template>
<style scoped>
/* ─ Page title ─ */
.pt { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.back { display:flex; align-items:center; gap:4px; color:#555; font-size:12px; text-decoration:none; }
.back:hover { color:#ccc; }
.ptdiv { color:#2a2a2a; }
.pt-case { display:flex; align-items:center; gap:3px; font-weight:800; color:#fff; font-size:14px; }
.status-chip {
font-size:10px; font-weight:800; padding:3px 10px; border-radius:20px;
border:1px solid; text-transform:uppercase; letter-spacing:.5px;
}
/* ─ Flash ─ */
.flash {
display:flex; align-items:center; gap:8px; margin-bottom:16px;
background:rgba(34,197,94,0.1); border:1px solid rgba(34,197,94,0.3);
color:#22c55e; padding:10px 14px; border-radius:10px; font-size:13px; font-weight:600;
}
.flash button { margin-left:auto; background:none; border:none; color:inherit; cursor:pointer; display:flex; }
.fade-enter-active,.fade-leave-active { transition:opacity .3s; }
.fade-enter-from,.fade-leave-to { opacity:0; }
/* ─ Summary bar ─ */
.summary-bar {
display:flex; align-items:center; gap:0; flex-wrap:wrap;
background:#111113; border:1px solid #1e1e21; border-radius:12px;
padding:12px 18px; margin-bottom:18px; gap:16px;
}
.sb-item { display:flex; flex-direction:column; gap:2px; }
.sb-label { font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:.6px; color:#444; }
.sb-val { font-size:12px; font-weight:700; color:#ccc; display:flex; align-items:center; gap:4px; }
.sb-val.blue { color:#60a5fa; }
.sb-val.pink { color:#f472b6; }
.sb-val.mono { font-family:monospace; color:#666; font-size:11px; }
.sb-arrow { color:#333; flex-shrink:0; }
.sb-sep { width:1px; height:28px; background:#1e1e21; }
.sb-status-btns { margin-left:auto; display:flex; gap:6px; }
.ssb {
display:flex; align-items:center; gap:5px; padding:6px 12px;
border-radius:7px; border:1px solid #252528; background:#161618;
color:#555; font-size:11px; font-weight:700; cursor:pointer; transition:.15s;
}
.ssb:hover { border-color:#3a3a3f; color:#aaa; }
.ssb.active,.ssb:hover.active { border-color:rgba(34,197,94,0.4); background:rgba(34,197,94,0.08); color:#22c55e; }
.ssb.dismiss.active { border-color:rgba(107,114,128,0.4); background:rgba(107,114,128,0.08); color:#6b7280; }
/* ─ Main grid ─ */
.main-grid {
display:grid;
grid-template-columns: 270px 1fr 290px;
gap:16px;
align-items:start;
}
@media(max-width:1300px){ .main-grid { grid-template-columns: 250px 1fr 270px; } }
@media(max-width:1000px){ .main-grid { grid-template-columns:1fr; } }
/* ─ User cards ─ */
.col-left { display:flex; flex-direction:column; gap:8px; }
.user-card {
background:#111113; border:1px solid #1e1e21; border-radius:12px; padding:14px;
display:flex; flex-direction:column; gap:9px; position:relative;
}
.blue-top { border-top:2px solid #3b82f6; }
.pink-top { border-top:2px solid #df006a; }
.uc-label {
font-size:9px; font-weight:800; text-transform:uppercase; letter-spacing:.6px;
display:flex; align-items:center; gap:3px; padding:2px 8px; border-radius:20px;
width:fit-content; border:1px solid;
}
.uc-label.blue { color:#3b82f6; background:rgba(59,130,246,.08); border-color:rgba(59,130,246,.2); }
.uc-label.pink { color:#df006a; background:rgba(223,0,106,.08); border-color:rgba(223,0,106,.2); }
.uc-row { display:flex; align-items:center; gap:10px; }
.uc-av {
width:40px; height:40px; border-radius:10px; flex-shrink:0;
display:flex; align-items:center; justify-content:center;
font-size:16px; font-weight:900; overflow:hidden;
}
.uc-av img { width:100%; height:100%; object-fit:cover; }
.uc-av.blue { background:rgba(59,130,246,.15); color:#3b82f6; }
.uc-av.pink { background:rgba(223,0,106,.15); color:#df006a; }
.uc-info { flex:1; min-width:0; }
.uc-name { font-size:13px; font-weight:800; color:#e0e0e0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.uc-email, .uc-since { display:flex; align-items:center; gap:4px; font-size:10px; color:#555; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.uc-open {
color:#666; background:#161618; border:1px solid #252528; border-radius:7px;
padding:5px; display:flex; cursor:pointer; text-decoration:none; transition:.15s; flex-shrink:0;
}
.uc-open:hover { color:#ccc; border-color:#3a3a3f; }
.uc-badges { display:flex; gap:5px; flex-wrap:wrap; }
.badge {
display:flex; align-items:center; gap:3px;
font-size:9px; font-weight:800; padding:2px 6px; border-radius:5px; border:1px solid;
text-transform:uppercase;
}
.badge.role { color:#555; border-color:#252528; background:#161618; }
.badge.vip { color:#fcd34d; border-color:rgba(252,211,77,.25); background:rgba(252,211,77,.06); }
.badge.banned { color:#ef4444; border-color:rgba(239,68,68,.3); background:rgba(239,68,68,.08); }
.badge.cbanned { color:#f97316; border-color:rgba(249,115,22,.3); background:rgba(249,115,22,.08); }
/* Mini history (reporter) */
.mini-hist { border-top:1px solid #1a1a1c; padding-top:8px; }
.mh-title { display:flex; align-items:center; gap:4px; font-size:9px; font-weight:700; text-transform:uppercase; color:#444; letter-spacing:.5px; margin-bottom:5px; }
.mh-count { margin-left:auto; background:#1a1a1c; border:1px solid #252528; border-radius:8px; padding:0 5px; color:#666; font-size:9px; }
.mh-row { display:flex; align-items:center; gap:6px; padding:4px 7px; border-radius:5px; background:#0e0e10; border:1px solid #1a1a1c; margin-bottom:3px; font-size:10px; }
.mh-row.active { border-color:rgba(239,68,68,.2); background:rgba(239,68,68,.04); }
.mh-type { font-size:9px; font-weight:800; padding:1px 5px; border-radius:3px; }
.mh-type.chat { color:#f97316; background:rgba(249,115,22,.1); }
.mh-type.ban { color:#ef4444; background:rgba(239,68,68,.1); }
.mh-reason { flex:1; color:#666; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.mh-live { color:#ef4444; font-size:12px; }
/* Active alert */
.active-alert {
display:flex; align-items:flex-start; gap:8px;
background:rgba(239,68,68,.06); border:1px solid rgba(239,68,68,.2);
border-radius:8px; padding:8px 10px; color:#ef4444; font-size:12px; font-weight:600;
}
.aa-title { font-size:11px; font-weight:700; color:#ef4444; margin-bottom:4px; }
.aa-list { display:flex; flex-direction:column; gap:3px; }
.aa-row { display:flex; align-items:center; justify-content:space-between; gap:8px; }
.aa-until { font-size:10px; color:#888; font-weight:500; }
/* Full restriction history (sender) */
.restrict-hist { border-top:1px solid #1a1a1c; padding-top:8px; }
.rh-item {
border:1px solid #1a1a1c; border-radius:8px; padding:7px 9px;
margin-bottom:5px; display:flex; flex-direction:column; gap:4px;
background:#0e0e10;
}
.rh-item.active { border-color:rgba(239,68,68,.25); background:rgba(239,68,68,.03); }
.rh-row { display:flex; align-items:center; gap:6px; font-size:11px; }
.rh-reason { flex:1; color:#777; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.rh-date { color:#444; font-size:10px; white-space:nowrap; }
.rh-until { display:flex; align-items:center; gap:4px; font-size:10px; color:#555; }
.rh-left { color:#f97316; font-weight:700; margin-left:4px; }
.rh-actions { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.rha {
display:flex; align-items:center; gap:3px; font-size:10px; font-weight:700;
padding:3px 9px; border-radius:5px; border:1px solid; cursor:pointer; transition:.15s;
}
.rha.lift { color:#22c55e; border-color:rgba(34,197,94,.3); background:rgba(34,197,94,.07); }
.rha.lift:hover { background:rgba(34,197,94,.14); }
.rha.extend { color:#f59e0b; border-color:rgba(245,158,11,.3); background:rgba(245,158,11,.07); }
.rha.extend:hover { background:rgba(245,158,11,.14); }
.rha-extend { display:flex; align-items:center; gap:4px; }
.rha-input {
width:56px; padding:3px 7px; background:#0d0d0f;
border:1px solid #252528; color:#ccc; border-radius:5px; font-size:10px;
}
.no-hist { font-size:11px; color:#333; font-style:italic; border-top:1px solid #1a1a1c; padding-top:8px; }
/* Divider */
.reported-divider {
display:flex; align-items:center; justify-content:center; gap:6px;
color:#3a3a3f; font-size:11px; font-weight:700;
}
.df { color:#df006a; }
/* ─ Chat timeline ─ */
.col-center { min-width:0; }
.ct-head {
display:flex; align-items:center; gap:7px; margin-bottom:12px;
font-size:13px; font-weight:800; color:#e0e0e0;
}
.ct-icon { color:#df006a; }
.ct-sub { font-size:11px; font-weight:400; color:#555; margin-left:2px; }
.ct-sub.warn { color:#ef4444; }
.timeline {
background:#0c0c0e; border:1px solid #1a1a1c; border-radius:12px; overflow:hidden;
}
.tl-empty { padding:40px; text-align:center; color:#3a3a3f; font-size:13px; }
.tl-msg {
display:flex; align-items:flex-start; gap:10px;
padding:10px 14px; border-bottom:1px solid #111113; transition:background .1s;
}
.tl-msg:last-child { border-bottom:none; }
.tl-msg:hover { background:rgba(255,255,255,.015); }
.tl-msg.by-sender { background:rgba(223,0,106,.03); }
.tl-msg.by-reporter { background:rgba(59,130,246,.03); }
.tl-av {
width:28px; height:28px; border-radius:7px; flex-shrink:0;
display:flex; align-items:center; justify-content:center;
font-size:11px; font-weight:800; background:#1a1a1c; color:#555;
}
.tl-av.av-s { background:rgba(223,0,106,.15); color:#df006a; }
.tl-av.av-r { background:rgba(59,130,246,.15); color:#3b82f6; }
.tl-av.av-rep { box-shadow:0 0 0 2px rgba(223,0,106,.45); }
.tl-body { flex:1; min-width:0; }
.tl-meta { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:2px; }
.tl-user { font-size:11px; font-weight:700; color:#888; }
.tl-ts { font-size:10px; color:#3a3a3f; margin-left:auto; }
.tl-text { font-size:13px; color:#bbb; line-height:1.45; word-break:break-word; }
/* Reported msg */
.tl-msg.reported {
background:rgba(223,0,106,.06) !important;
border-left:3px solid #df006a;
padding-left:11px;
position:relative;
}
.rep-flag {
position:absolute; left:-1px; top:8px;
width:18px; height:18px; background:#df006a; color:#fff;
border-radius:50%; display:flex; align-items:center; justify-content:center;
}
.rep-user { color:#f472b6 !important; }
.rep-badge {
display:flex; align-items:center; gap:3px;
font-size:9px; font-weight:800; color:#df006a;
background:rgba(223,0,106,.1); border:1px solid rgba(223,0,106,.25);
padding:1px 6px; border-radius:4px; text-transform:uppercase;
}
.reason-badge {
font-size:9px; font-weight:700; color:#f59e0b;
background:rgba(245,158,11,.1); border:1px solid rgba(245,158,11,.25);
padding:1px 6px; border-radius:4px;
}
.rep-text { color:#fff !important; font-weight:500; }
/* ─ Punishment panel ─ */
.col-right { min-width:0; }
.pun-head {
display:flex; align-items:center; gap:7px; margin-bottom:14px;
font-size:13px; font-weight:800; color:#e0e0e0;
}
.pun-icon { color:#f59e0b; }
.pun-form { display:flex; flex-direction:column; gap:14px; }
.pun-form.locked { opacity:.45; pointer-events:none; }
.type-row { display:flex; gap:8px; }
.type-btn {
flex:1; display:flex; align-items:center; justify-content:center; gap:5px;
font-size:12px; font-weight:700; padding:9px; border-radius:9px;
border:1px solid #252528; background:#111113; color:#555; cursor:pointer; transition:.15s;
}
.type-btn.on { border-color:rgba(249,115,22,.4); background:rgba(249,115,22,.1); color:#f97316; }
.type-btn.ban.on { border-color:rgba(239,68,68,.4); background:rgba(239,68,68,.1); color:#ef4444; }
.tpl-block { display:flex; flex-direction:column; gap:6px; }
.tpl-label { font-size:10px; font-weight:700; text-transform:uppercase; color:#444; letter-spacing:.5px; }
.tpl-list { display:flex; flex-direction:column; gap:4px; }
.tpl-btn {
display:grid; grid-template-columns:auto 1fr auto;
align-items:center; gap:6px; padding:8px 11px;
border-radius:8px; border:1px solid #1a1a1c; background:#0e0e10;
cursor:pointer; transition:.15s; text-align:left;
}
.tpl-btn:hover { border-color:#252528; background:#111113; }
.tpl-btn.on { border-color:rgba(249,115,22,.35); background:rgba(249,115,22,.07); }
.tpl-btn.ban.on { border-color:rgba(239,68,68,.35); background:rgba(239,68,68,.07); }
.tpl-name { font-size:12px; font-weight:700; color:#ccc; }
.tpl-dur { font-size:10px; font-weight:700; color:#666; background:#161618; border:1px solid #252528; padding:1px 7px; border-radius:5px; justify-self:end; }
.tpl-reason { grid-column:1/-1; font-size:10px; color:#555; }
.or-line {
text-align:center; color:#2a2a2a; font-size:10px; font-weight:600;
position:relative;
}
.or-line span { position:relative; z-index:1; background:#0f0f11; padding:0 10px; }
.or-line::before { content:''; position:absolute; top:50%; left:0; right:0; height:1px; background:#1a1a1c; }
.custom-block { display:flex; flex-direction:column; gap:10px; }
.fl { display:flex; flex-direction:column; gap:5px; font-size:11px; font-weight:600; color:#555; }
.fl span { display:flex; align-items:center; gap:4px; }
.fi {
background:#0c0c0e; border:1px solid #1e1e21; color:#ccc;
padding:8px 11px; border-radius:8px; font-size:13px; width:100%; box-sizing:border-box;
}
.fi:focus { outline:none; border-color:rgba(223,0,106,.3); }
.ta { resize:none; }
.pun-preview {
display:flex; align-items:center; gap:8px; flex-wrap:wrap;
background:#0c0c0e; border:1px solid #1e1e21; border-radius:8px; padding:8px 11px; font-size:11px;
}
.pp-type { font-weight:800; padding:2px 8px; border-radius:5px; }
.pp-type.chat { color:#f97316; background:rgba(249,115,22,.1); }
.pp-type.ban { color:#ef4444; background:rgba(239,68,68,.1); }
.pp-dur { font-weight:700; color:#888; }
.pp-until { color:#555; font-size:10px; margin-left:auto; }
.pun-btn {
display:flex; align-items:center; justify-content:center; gap:7px;
width:100%; padding:11px; border-radius:10px; border:none; cursor:pointer;
font-size:13px; font-weight:800; color:#fff; transition:.2s;
background:linear-gradient(135deg,#f97316,#ea580c);
}
.pun-btn.ban { background:linear-gradient(135deg,#ef4444,#dc2626); }
.pun-btn:hover:not(:disabled) { filter:brightness(1.1); }
.pun-btn:disabled { opacity:.4; cursor:not-allowed; }
.spin { animation:spin .8s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
</style>