// plugins.jsx — extended plugin management
// Overrides PluginsPanel from panels-c.jsx (loaded after it in index.html).

// ── Extended fixtures ────────────────────────────────────────────
const PL_INSTALLED = [
  {
    id: 'css', name: 'CounterStrikeSharp', author: 'roflmuffin',
    ver: '1.0.305', status: 'ok', update: null, category: 'core',
    desc: 'C# scripting host for CS2 dedicated servers. Loads .NET 8 plugin assemblies and exposes the gameserver event bus, RCON, and cvar APIs to managed code.',
    essential: true, lastUpdated: '12 May', enabled: true,
    configFile: 'core.json',
    configBody:
`{
  "PublicChatTrigger": ["!", "/"],
  "SilentChatTrigger": [".", "*"],
  "FollowCS2ServerGuidelines": true,
  "PluginHotReloadEnabled": true,
  "PluginsAutoload": true,
  "ServerLanguage": "en"
}`,
  },
  {
    id: 'mm', name: 'Metamod:Source', author: 'AlliedModders',
    ver: '2.0.0-12', status: 'ok', update: null, category: 'core',
    desc: 'Source engine plugin loader. Required by CounterStrikeSharp.',
    essential: true, lastUpdated: '08 May', enabled: true,
    configFile: 'metaplugins.ini',
    configBody:
`; auto-managed by nokit · do not edit
addons/metamod/bin/server`,
  },
  {
    id: 'matchzy', name: 'MatchZy', author: 'shobhit-pathak',
    ver: '0.8.7', status: 'ok', update: '0.8.8', category: 'competitive',
    desc: 'Pug, scrim, and tournament match management with knife rounds, tactical pauses, and per-round demos.',
    lastUpdated: '3d ago', enabled: true,
    configFile: 'MatchZy.json',
    configBody:
`{
  "MinimumReadyRequired": 5,
  "ChatPrefix": "[\\u0010MatchZy\\u0001]",
  "KickWhenNoMatchLoaded": false,
  "EnableDamageReport": true,
  "EnableDemoUploader": true,
  "EnableStatsLogging": true,
  "DemoPath": "matchzy_demos",
  "DemoNameFormat": "matchzy_{DATE}_{MATCH_ID}_{MAP}",
  "ResetCvarsOnSeriesEnd": true
}`,
  },
  {
    id: 'mov', name: 'KitsuneLab.MovementApi', author: 'kitsunelab',
    ver: '1.4.0', status: 'ok', update: null, category: 'utilities',
    desc: 'Movement hook API used by surf, kz, and bhop plugins. Provides tick-accurate per-player movement callbacks.',
    lastUpdated: '11d ago', enabled: true,
    configFile: 'MovementApi.json',
    configBody: `{ "EnableLogging": false, "MaxHookDepth": 4 }`,
  },
  {
    id: 'paints', name: 'WeaponPaints', author: 'Nereziel',
    ver: '4.0', status: 'warn', update: null, category: 'fun',
    desc: 'Skin / glove / knife chooser. Persists selection per SteamID via MySQL.',
    lastUpdated: '5d ago', enabled: true,
    configFile: 'WeaponPaints.json',
    configBody:
`{
  "DatabaseHost": "127.0.0.1",
  "DatabasePort": 3306,
  "DatabaseUser": "weapon",
  "DatabasePassword": "***",
  "DatabaseName": "weapons",
  "Commands": ["ws", "knife", "gloves"],
  "Website": "https://nokit.local/skins"
}`,
  },
  {
    id: 'sa', name: 'CS2-SimpleAdmin', author: 'daffyy',
    ver: '1.6.1', status: 'ok', update: null, category: 'admin',
    desc: 'In-game admin commands — kick, ban, gag, mute, freeze, slay. Compatible with SourceMod admins.cfg.',
    lastUpdated: '1mo ago', enabled: true,
    configFile: 'admins.json',
    configBody:
`[
  { "steamid": "STEAM_1:0:48211904", "name": "m0use@",   "flags": "abcdfijkz" },
  { "steamid": "STEAM_1:1:24010332", "name": "jaeger",   "flags": "abcdjkz" },
  { "steamid": "STEAM_1:0:11241003", "name": "kira",     "flags": "abj" }
]`,
  },
  {
    id: 'cadm', name: 'CallAdmin', author: 'rumblefrog',
    ver: '0.6.2', status: 'off', update: null, category: 'admin',
    desc: 'In-game report → Discord webhook with player context attached.',
    lastUpdated: '2mo ago', enabled: false,
    configFile: 'CallAdmin.json',
    configBody: `{ "Webhook": "", "Cooldown": 60, "EnableSteamLookup": true }`,
  },
  {
    id: 'gotv', name: 'GotvFix', author: 'whisper',
    ver: '0.3.0', status: 'err', update: null, category: 'utilities',
    desc: 'TCP relay shim for GOTV — works around Valve\'s GOTV2 broken multicast on Linux 6.x kernels.',
    lastUpdated: '14d ago', enabled: false,
    configFile: 'GotvFix.json',
    configBody: `{ "RelayPort": 27020, "ForceTCP": true, "MaxClients": 32 }`,
    error: 'bind(0.0.0.0:27020) → EADDRINUSE — gotv relay disabled',
  },
];

// ── Registry: 24 plugins across 6 categories ────────────────────
const REGISTRY = [
  // Admin tools
  { id: 'access',  name: 'AccessChecker',           author: 'd00mfist',     ver: '1.1.0', category: 'admin',
    desc: 'Steam group + flag-based access control. Verifies via Steam Web API on connect with 5-minute cache.',
    installs: '24.8k', updated: '5d ago', featured: false, tags: ['groups', 'auth'] },
  { id: 'voteman', name: 'VoteManager',             author: 'rrrfffrrr',    ver: '2.0.1', category: 'admin',
    desc: 'Veto, map vote, mute vote, kick vote. Configurable thresholds and player-side menus.',
    installs: '18.2k', updated: '2w ago', featured: false, tags: ['voting'] },
  { id: 'spec',    name: 'SpectatorTools',          author: 'b3none',       ver: '0.9.4', category: 'admin',
    desc: 'Force spectate, freeze, no-clip, and god-mode admin tools.',
    installs: '9.7k',  updated: '1mo ago', featured: false, tags: ['debug'] },
  { id: 'sshield',  name: 'NetShield',              author: 'kitsunelab',   ver: '0.5.0', category: 'admin',
    desc: 'IP throttling and connection rate-limit at the engine level. Drops attackers before Valve\'s auth handshake.',
    installs: '6.1k',  updated: '8d ago', featured: false, tags: ['ddos', 'security'] },

  // Competitive
  { id: 'retakes', name: 'CS2-RetakesAllocator',    author: 'b3none',       ver: '2.1.0', category: 'competitive',
    desc: 'Full retakes implementation — bombsite rotation, automatic bomb planter, weapon allocator with savings.',
    installs: '47.4k', updated: '3d ago', featured: true, tags: ['retakes', 'rotation'] },
  { id: 'mzy',     name: 'MatchZy',                 author: 'shobhit-pathak',ver: '0.8.7', category: 'competitive',
    desc: 'Pug, scrim & tournament match management with knife round, pauses, demos.',
    installs: '92.1k', updated: '3d ago', installed: true, featured: true, tags: ['match', 'pug'] },
  { id: 'pug',     name: 'PUG-Setup',               author: 'splewis',      ver: '1.4.2', category: 'competitive',
    desc: 'Captain-pick public match flow. Auto-balance, ready-up, and per-server stats.',
    installs: '12.4k', updated: '2mo ago', featured: false, tags: ['pug'] },
  { id: 'anticht', name: 'Anti-Bhop',               author: 'whisper',      ver: '1.0.0', category: 'competitive',
    desc: 'Tickrate-aware bhop limiter — competitive safe. Lets one-tick hops through, kills sustained scripts.',
    installs: '8.3k',  updated: '10d ago', featured: false, tags: ['anticheat'] },

  // Fun
  { id: 'wpnt',    name: 'WeaponPaints',            author: 'Nereziel',     ver: '4.0',   category: 'fun',
    desc: 'Skin / glove / knife chooser with web profile editor. MySQL-backed.',
    installs: '88.0k', updated: '5d ago', installed: true, featured: true, tags: ['cosmetic', 'skins'] },
  { id: 'rzang',   name: 'Rainbow Zone',            author: 'lostgirl',     ver: '0.4.0', category: 'fun',
    desc: 'Trail effects, particle hats, kill camera VFX. Toggleable per-player.',
    installs: '4.2k',  updated: '2w ago', featured: false, tags: ['vfx'] },
  { id: 'econ',    name: 'EconBet',                 author: 'lostgirl',     ver: '0.8.1', category: 'fun',
    desc: 'Round betting on outcomes with persistent in-game currency. Hooks economy events.',
    installs: '2.1k',  updated: '1mo ago', featured: false, tags: ['minigame'] },
  { id: 'pong',    name: 'KillSounds',              author: 'b3none',       ver: '1.2.0', category: 'fun',
    desc: 'Player-selectable kill / headshot / round-win sounds. Comes with a curated pack.',
    installs: '14.8k', updated: '21d ago', featured: false, tags: ['audio'] },

  // Utilities
  { id: 'autoup',  name: 'CSS-AutoUpdater',         author: 'roflmuffin',   ver: '0.4.2', category: 'utilities',
    desc: 'Hot-reload plugins from disk. Development-only — strongly recommend disabling in production.',
    installs: '11.0k', updated: '4d ago', featured: false, tags: ['dev'] },
  { id: 'mvapi',   name: 'KitsuneLab.MovementApi',  author: 'kitsunelab',   ver: '1.4.0', category: 'utilities',
    desc: 'Movement hook API for surf/kz/bhop plugins. Required dependency for KitsuneLab plugins.',
    installs: '8.5k',  updated: '11d ago', installed: true, featured: false, tags: ['api', 'lib'] },
  { id: 'discord', name: 'DiscordBridge',           author: 'rumblefrog',   ver: '1.1.4', category: 'utilities',
    desc: 'Two-way chat relay between in-game and Discord. Embeds, mentions, and player tags.',
    installs: '6.4k',  updated: '14d ago', featured: false, tags: ['chat'] },
  { id: 'metrics', name: 'Prometheus Exporter',     author: 'jaeger',       ver: '0.3.0', category: 'utilities',
    desc: 'Exposes server metrics — players, tick variance, RCON latency — on a /metrics endpoint.',
    installs: '4.9k',  updated: '7d ago', featured: false, tags: ['monitoring'] },

  // Maps
  { id: 'rotscm',  name: 'AutoMapCycle',            author: 'b3none',       ver: '1.0.5', category: 'maps',
    desc: 'Smart map rotation — weighted by player count, last-played, and time of day.',
    installs: '7.3k',  updated: '18d ago', featured: false, tags: ['rotation'] },
  { id: 'wsync',   name: 'WorkshopSync',            author: 'roflmuffin',   ver: '2.2.0', category: 'maps',
    desc: 'Subscribes the server to a Steam workshop collection and keeps it synced on a schedule.',
    installs: '5.0k',  updated: '1mo ago', featured: false, tags: ['workshop'] },
  { id: 'mvotes',  name: 'MapVote',                 author: 'kus',          ver: '3.1.0', category: 'maps',
    desc: 'End-of-match map vote with nominations and runoff. Workshop-aware.',
    installs: '13.8k', updated: '21d ago', featured: false, tags: ['voting'] },

  // Gamemodes
  { id: 'surf',    name: 'Surf Timer',              author: 'kitsunelab',   ver: '3.0.4', category: 'gamemode',
    desc: 'Per-map records, leaderboards, and per-zone splits with a TAS-quality replay bot.',
    installs: '21.6k', updated: '4d ago', featured: true, tags: ['surf'] },
  { id: 'kz',      name: 'KZ Timer',                author: 'kzclimb',      ver: '5.2.1', category: 'gamemode',
    desc: 'Climb timer with GlobalAPI submission, runs-leaderboard, and noclip checkpoints.',
    installs: '9.4k',  updated: '12d ago', featured: false, tags: ['kz', 'climb'] },
  { id: 'dm',      name: 'Deathmatch',              author: 'rumblefrog',   ver: '1.8.0', category: 'gamemode',
    desc: 'Aim-map deathmatch with rifle / pistol / awp filters, hitsounds, and respawn HUD.',
    installs: '17.7k', updated: '9d ago', featured: false, tags: ['dm'] },
  { id: 'gungame', name: 'GunGame',                 author: 'splewis',      ver: '2.0.0', category: 'gamemode',
    desc: 'Per-kill weapon progression. Last weapon is knife. Win triggers map change.',
    installs: '6.8k',  updated: '2mo ago', featured: false, tags: ['gungame'] },
  { id: 'jb',      name: 'JailBreak',               author: 'd00mfist',     ver: '4.0.0', category: 'gamemode',
    desc: 'Full JailBreak mode — warden mechanics, last-request menus, day events, and rebel HUD.',
    installs: '3.3k',  updated: '1mo ago', featured: false, tags: ['jailbreak'] },
];

// inject "installed" flag from PL_INSTALLED
{
  const installedIds = new Set(PL_INSTALLED.map((p) => p.id === 'mm' ? null : p.id).filter(Boolean));
  REGISTRY.forEach((r) => {
    if (r.installed === undefined) {
      // also mark by name for matching
      r.installed = !!PL_INSTALLED.find((i) => i.name.toLowerCase() === r.name.toLowerCase());
    }
  });
}

const CATEGORIES = [
  { id: 'all',         label: 'All',          ico: I.dash,    count: REGISTRY.length },
  { id: 'admin',       label: 'Admin tools',  ico: I.shield,  count: REGISTRY.filter(r => r.category === 'admin').length },
  { id: 'competitive', label: 'Competitive',  ico: I.flag,    count: REGISTRY.filter(r => r.category === 'competitive').length },
  { id: 'fun',         label: 'Fun',          ico: I.bolt,    count: REGISTRY.filter(r => r.category === 'fun').length },
  { id: 'utilities',   label: 'Utilities',    ico: I.cog,     count: REGISTRY.filter(r => r.category === 'utilities').length },
  { id: 'maps',        label: 'Maps',         ico: I.map,     count: REGISTRY.filter(r => r.category === 'maps').length },
  { id: 'gamemode',    label: 'Gamemodes',    ico: I.sliders, count: REGISTRY.filter(r => r.category === 'gamemode').length },
];

const SORTS = ['Trending', 'Most installed', 'Recently updated', 'Name'];

// Changelog generator — deterministic per plugin id so screenshots are stable
function changelogFor(p) {
  const major = p.ver.split('.').map(Number);
  const minor = (major[1] || 0);
  const patch = (major[2] || 0);
  return [
    { v: p.ver, d: p.updated || p.lastUpdated || '5d ago', notes: [
      'Compatibility with CounterStrikeSharp 1.0.305.',
      'Fix occasional crash on map change when a player is mid-buy.',
      'Add `' + (p.id || p.name).toLowerCase().replace(/[^a-z]/g, '_') + '_debug` cvar for tracing event hooks.',
    ], latest: true },
    { v: `${major[0]}.${minor}.${Math.max(0, patch - 1)}`, d: '3w ago', notes: [
      'Performance: reduce per-tick allocations by 60%.',
      'Bumped minimum Metamod requirement to 2.0.0-12.',
    ] },
    { v: `${major[0]}.${Math.max(0, minor - 1)}.0`, d: '2mo ago', notes: [
      'Initial public release after 6 months of internal use on fra1.mm.',
      'BREAKING: `' + (p.id || p.name).toLowerCase() + '_path` renamed to `data_path`.',
    ] },
  ];
}

// Config schema preview — generic but plausible
function configSchemaFor(p) {
  const base = [
    { k: 'Enabled',          t: 'bool', d: 'true',    desc: 'Master switch — disables plugin without unloading.' },
    { k: 'ChatPrefix',       t: 'str',  d: '"[nokit]"', desc: 'Prefix shown on plugin chat output. Supports CS2 color codes.' },
    { k: 'AdminFlags',       t: 'str',  d: '"abcdz"', desc: 'Required admin flags to invoke admin commands.' },
    { k: 'DatabaseHost',     t: 'str',  d: '"127.0.0.1"', desc: 'MySQL host. Use "" to disable persistence.' },
    { k: 'CacheTtlSeconds',  t: 'int',  d: '300',     desc: 'How long to cache lookups before refreshing.' },
    { k: 'LogLevel',         t: 'enum', d: '"info"',  desc: 'One of: error, warn, info, debug, trace.' },
  ];
  if (p.category === 'competitive') base.push(
    { k: 'KnifeRound',     t: 'bool', d: 'true',  desc: 'Run a knife round before going live to pick sides.' },
    { k: 'MaxPauses',      t: 'int',  d: '4',     desc: 'Maximum tactical pauses per team per half.' },
  );
  if (p.category === 'fun') base.push(
    { k: 'CooldownSeconds', t: 'int',  d: '60',    desc: 'Per-player cooldown between command uses.' },
  );
  return base;
}

// Long-form description per plugin — falls back to a richer version of desc
function longDescFor(p) {
  return [
    p.desc,
    `Installs into addons/counterstrikesharp/plugins/${p.id || p.name}/ and registers its commands and event hooks via the CS# host. ` +
    `Config lives in the same directory and hot-reloads when ${p.author} hands you a new build — no map change required.`,
    `Heavily used in production by nokit's reference servers — fra1.mm, sea1.scrim — and a long tail of community servers. ` +
    `MIT-licensed; pull requests welcome on GitHub.`,
  ];
}

// ── Plugins panel (overrides) ─────────────────────────────────────
function PluginsPanel() {
  const [tab, setTab] = React.useState('installed');
  const [detail, setDetail] = React.useState(null);
  const [filterPlugin, setFilterPlugin] = React.useState(null); // for "view logs" filter
  const updates = PL_INSTALLED.filter((p) => p.update).length;
  const errors  = PL_INSTALLED.filter((p) => p.status === 'err').length;
  const warns   = PL_INSTALLED.filter((p) => p.status === 'warn').length;

  // ESC to close modal
  React.useEffect(() => {
    if (!detail) return undefined;
    const k = (e) => e.key === 'Escape' && setDetail(null);
    window.addEventListener('keydown', k);
    return () => window.removeEventListener('keydown', k);
  }, [detail]);

  return (
    <div>
      <div className="panel-head">
        <div>
          <h1>Plugins</h1>
          <div className="sub">
            {PL_INSTALLED.length} installed · {updates} update{updates === 1 ? '' : 's'} available
            {errors > 0 && <> · <span className="ac" style={{ color: 'var(--err)' }}>{errors} error</span></>}
            {warns > 0 && <> · <span style={{ color: 'var(--warn)' }}>{warns} warning</span></>}
            <> · CS# 1.0.305 + Metamod 2.0.0-12</>
          </div>
        </div>
        <div className="tools">
          <button className="btn"><span className="ico">{I.reload}</span>Reload all</button>
          <button className="btn">
            <span className="ico">{I.download}</span>Update all ({updates})
          </button>
          <button className="btn primary" onClick={() => setTab('browse')}>
            <span className="ico">{I.plus}</span>Browse registry
          </button>
        </div>
      </div>

      <div className="tabs">
        <button className={tab === 'installed' ? 'on' : ''} onClick={() => setTab('installed')}>
          Installed<span className="badge">{PL_INSTALLED.length}</span>
        </button>
        <button className={tab === 'browse' ? 'on' : ''} onClick={() => setTab('browse')}>
          Browse<span className="badge">{REGISTRY.length}+</span>
        </button>
        <button className={tab === 'errors' ? 'on' : ''} onClick={() => setTab('errors')}>
          Error log{errors > 0 && <span className="badge err">{errors}</span>}
        </button>
      </div>

      <div className="body">
        {tab === 'installed' && <InstalledView onDetail={(p) => setDetail({ kind: 'installed', plugin: p })} onViewLogs={setFilterPlugin} filterPlugin={filterPlugin} />}
        {tab === 'browse'    && <BrowseView    onDetail={(p) => setDetail({ kind: 'browse',    plugin: p })} />}
        {tab === 'errors'    && <ExtendedErrorLog />}
      </div>

      {detail && <PluginDetail data={detail} onClose={() => setDetail(null)} />}
    </div>
  );
}

// ── Installed view ───────────────────────────────────────────────
function InstalledView({ onDetail, onViewLogs, filterPlugin }) {
  const [expanded, setExpanded] = React.useState(null); // plugin id whose config editor is open
  const [enabled, setEnabled] = React.useState(() => {
    const m = {};
    PL_INSTALLED.forEach((p) => { m[p.id] = p.enabled !== false; });
    return m;
  });
  const [q, setQ] = React.useState('');

  const rows = PL_INSTALLED.filter((p) =>
    !q || (p.name + p.author + p.desc).toLowerCase().includes(q.toLowerCase())
  );

  return (
    <div>
      <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
        <div className="input" style={{ flex: 1, maxWidth: 480 }}>
          <span className="ico">{I.search}</span>
          <input placeholder="filter installed — name, author, tag" value={q} onChange={(e) => setQ(e.target.value)} />
        </div>
        <div className="seg">
          <button className="on">all</button>
          <button>enabled</button>
          <button>disabled</button>
          <button>has update</button>
        </div>
        <div className="grow" />
        <button className="btn"><span className="ico">{I.upload}</span>Install from .dll</button>
      </div>

      {filterPlugin && (
        <div className="card" style={{ marginBottom: 12, padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10, background: 'var(--ac-soft)', borderColor: 'var(--ac-line)' }}>
          <span className="ico ac">{I.logs}</span>
          <span className="m" style={{ color: 'var(--fg)', fontSize: 12 }}>
            Live logs filtered to <strong>{filterPlugin}</strong> — jump to <a className="ac">Live Logs panel ↗</a> for the full view.
          </span>
          <div className="grow" />
          <button className="btn sm ghost" onClick={() => onViewLogs(null)}>clear</button>
        </div>
      )}

      <div className="card" style={{ padding: 0 }}>
        {rows.map((p, i) => (
          <div key={p.id}>
            <div className={"pl-row " + (expanded === p.id ? 'on' : '')}>
              <div className={"pl-ico " + (p.category === 'core' ? 'ac' : '')}>{p.name[0]}</div>
              <div className="pl-body" onClick={() => onDetail(p)} style={{ cursor: 'default' }}>
                <div className="pl-title">
                  <span>{p.name}</span>
                  <span className="ver">v{p.ver}</span>
                  <StatusBadge p={p} />
                  {p.update && <span className="badge warn">update → {p.update}</span>}
                  {p.essential && <span className="badge ac">core</span>}
                </div>
                <div className="pl-meta">
                  <span className="author">@{p.author}</span>
                  <span className="dot-sep">·</span>
                  <span>updated {p.lastUpdated}</span>
                  <span className="dot-sep">·</span>
                  <span>{p.category}</span>
                  <span className="dot-sep">·</span>
                  <span className="fg2">/addons/cs_sharp/plugins/{p.id}</span>
                </div>
                <div className="pl-desc">{p.desc}</div>
                {p.error && (
                  <div className="m" style={{ marginTop: 8, padding: '6px 8px', background: 'var(--err-soft)', border: '1px solid transparent', borderRadius: 6, color: 'var(--err)', fontSize: 11.5 }}>
                    [ERR] {p.error}
                  </div>
                )}
              </div>
              <div className="pl-acts" style={{ alignItems: 'center', gap: 8 }}>
                <Toggle on={enabled[p.id]} onClick={() => setEnabled({ ...enabled, [p.id]: !enabled[p.id] })} />
                <button className={"btn sm " + (expanded === p.id ? 'primary' : '')}
                        onClick={() => setExpanded(expanded === p.id ? null : p.id)}>
                  <span className="ico">{I.cog}</span>{expanded === p.id ? 'Editing' : 'Edit config'}
                </button>
                <button className="btn sm" onClick={() => onViewLogs(p.name)}>
                  <span className="ico">{I.logs}</span>View logs
                </button>
                <button className="btn sm ghost" onClick={() => onDetail(p)}>
                  <span className="ico">{I.out}</span>Details
                </button>
                <button className="btn sm danger" disabled={p.essential} title={p.essential ? 'Core plugin — cannot remove' : 'Uninstall'}>
                  <span className="ico">{I.trash}</span>
                </button>
              </div>
            </div>

            {expanded === p.id && (
              <div className="pl-expand">
                <div>
                  <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
                    <span className="ico fg3">{I.file}</span>
                    <span className="m" style={{ fontSize: 12 }}>{p.configFile}</span>
                    <span className="hint m">· {p.configBody.split('\n').length} lines · changes apply on plugin reload</span>
                    <div className="grow" />
                    <button className="btn sm ghost">format</button>
                    <button className="btn sm">revert</button>
                    <button className="btn sm primary">save &amp; reload</button>
                  </div>
                  <div className="editor" style={{ border: '1px solid var(--br)', borderRadius: 8, overflow: 'hidden', maxHeight: 260 }}>
                    <div className="gutter">
                      {p.configBody.split('\n').map((_, j) => <div key={j}>{j + 1}</div>)}
                    </div>
                    <div className="code" style={{ whiteSpace: 'pre' }}>
                      {colorJson(p.configBody)}
                    </div>
                  </div>
                </div>
                <div>
                  <h3 style={{ fontSize: 10, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--fg-3)', margin: '0 0 8px' }}>Quick info</h3>
                  <dl className="kv" style={{ fontSize: 11.5 }}>
                    <dt>status</dt> <dd><StatusBadge p={p} /></dd>
                    <dt>commands</dt><dd>{p.id === 'matchzy' ? '!ready, !pause, !knife, !restart' : p.id === 'sa' ? '!kick, !ban, !gag' : p.id === 'paints' ? '!ws, !knife, !gloves' : '—'}</dd>
                    <dt>hooks</dt> <dd className="fg2">OnRoundStart, OnPlayerSpawn, OnMapStart</dd>
                    <dt>memory</dt><dd>14.2 MB resident</dd>
                    <dt>uptime</dt><dd>11h 02m</dd>
                  </dl>
                  <hr className="hr" style={{ margin: '12px 0' }} />
                  <div style={{ display: 'grid', gap: 6 }}>
                    <button className="btn sm" style={{ justifyContent: 'flex-start' }}>
                      <span className="ico">{I.reload}</span>Reload plugin only
                    </button>
                    <button className="btn sm" style={{ justifyContent: 'flex-start' }} onClick={() => onViewLogs(p.name)}>
                      <span className="ico">{I.logs}</span>View its log output
                    </button>
                    <button className="btn sm" style={{ justifyContent: 'flex-start' }}>
                      <span className="ico">{I.git}</span>Source on GitHub
                    </button>
                  </div>
                </div>
              </div>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}

function StatusBadge({ p }) {
  if (p.status === 'ok')   return <span className="badge ok">enabled</span>;
  if (p.status === 'warn') return <span className="badge warn">warn</span>;
  if (p.status === 'err')  return <span className="badge err">failed</span>;
  if (p.status === 'off')  return <span className="badge">disabled</span>;
  return null;
}

function Toggle({ on, onClick }) {
  return (
    <button onClick={onClick} style={{
      position: 'relative', width: 30, height: 18, borderRadius: 999,
      background: on ? 'var(--ac)' : 'var(--bg-4)',
      boxShadow: on ? '0 0 10px var(--ac-soft)' : 'inset 0 0 0 1px var(--br-1)',
      transition: 'all .15s',
    }}>
      <i style={{
        position: 'absolute', top: 2, left: on ? 14 : 2,
        width: 14, height: 14, borderRadius: '50%',
        background: on ? '#0a0a0b' : 'var(--fg-2)',
        transition: 'left .15s',
      }} />
    </button>
  );
}

// json syntax colorer
function colorJson(s) {
  // very lightweight regex pass — keys, strings, numbers, bools
  const re = /("(?:\\.|[^"])*")(\s*:)|(\b(?:true|false|null)\b)|(\b-?\d+(?:\.\d+)?\b)|("(?:\\.|[^"])*")|(\/\/.*$|;.*$|^\s*#.*$)/gm;
  const parts = []; let last = 0, k = 0, m;
  const push = (cls, t) => parts.push(<span key={k++} className={cls}>{t}</span>);
  while ((m = re.exec(s))) {
    if (m.index > last) push('', s.slice(last, m.index));
    if (m[1] !== undefined) { push('cv', m[1]); push('', m[2]); }
    else if (m[3]) push('key', m[3]);
    else if (m[4]) push('num', m[4]);
    else if (m[5]) push('str', m[5]);
    else if (m[6]) push('com', m[6]);
    last = re.lastIndex;
  }
  if (last < s.length) push('', s.slice(last));
  return <>{parts}</>;
}

// ── Browse view ──────────────────────────────────────────────────
function BrowseView({ onDetail }) {
  const [cat, setCat] = React.useState('all');
  const [q, setQ] = React.useState('');
  const [sort, setSort] = React.useState('Trending');

  const filtered = React.useMemo(() => {
    let rows = REGISTRY.slice();
    if (cat !== 'all') rows = rows.filter((r) => r.category === cat);
    if (q) {
      const Q = q.toLowerCase();
      rows = rows.filter((r) =>
        r.name.toLowerCase().includes(Q) ||
        r.author.toLowerCase().includes(Q) ||
        r.desc.toLowerCase().includes(Q) ||
        (r.tags || []).some((t) => t.includes(Q))
      );
    }
    if (sort === 'Most installed') rows.sort((a, b) => parseFloat(b.installs) - parseFloat(a.installs));
    else if (sort === 'Recently updated') rows.sort((a, b) => updatedRank(b.updated) - updatedRank(a.updated));
    else if (sort === 'Name') rows.sort((a, b) => a.name.localeCompare(b.name));
    return rows;
  }, [cat, q, sort]);

  const featured = REGISTRY.filter((r) => r.featured);

  return (
    <div className="plug-layout">
      <aside className="plug-rail">
        <div className="group">Categories</div>
        {CATEGORIES.map((c) => (
          <div key={c.id} className={"cat " + (cat === c.id ? 'on' : '')} onClick={() => setCat(c.id)}>
            <span className="ico">{c.ico}</span>
            <span>{c.label}</span>
            <span className="ct">{c.count}</span>
          </div>
        ))}
        <hr />
        <div className="group">Filters</div>
        <div className="cat"><span className="chk" /><span>Has screenshots</span></div>
        <div className="cat"><span className="chk on"><span style={{ fontSize: 9 }}>✓</span></span><span>CS2 compatible</span></div>
        <div className="cat"><span className="chk" /><span>MIT / Apache</span></div>
        <div className="cat"><span className="chk" /><span>No DB required</span></div>
        <hr />
        <div className="group">Sources</div>
        <div className="cat on"><span className="ico">{I.git}</span><span>Registry</span><span className="ct">327</span></div>
        <div className="cat"><span className="ico">{I.link}</span><span>Custom git</span><span className="ct">3</span></div>
        <div className="cat"><span className="ico">{I.folder}</span><span>Local .dll</span></div>
      </aside>

      <div className="plug-list">
        <div className="plug-list-head">
          <div className="input" style={{ flex: 1, maxWidth: 460 }}>
            <span className="ico">{I.search}</span>
            <input placeholder="search 327 plugins — name, author, tag" value={q} onChange={(e) => setQ(e.target.value)} />
            <kbd>/</kbd>
          </div>
          <div className="grow" />
          <span className="hint m">sort by</span>
          <div className="seg">
            {SORTS.map((s) => (
              <button key={s} className={sort === s ? 'on' : ''} onClick={() => setSort(s)}>{s.toLowerCase()}</button>
            ))}
          </div>
        </div>

        {cat === 'all' && !q && (
          <>
            <div className="div-lbl" style={{ margin: '12px 16px 6px' }}><span>Featured</span></div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 10, padding: '0 16px 12px' }}>
              {featured.map((r) => (
                <FeaturedCard key={r.id} r={r} onDetail={onDetail} />
              ))}
            </div>
            <div className="div-lbl" style={{ margin: '6px 16px 0' }}><span>All plugins · {REGISTRY.length}</span></div>
          </>
        )}

        {filtered.map((r) => (
          <BrowseRow key={r.id} r={r} onDetail={onDetail} />
        ))}
        {filtered.length === 0 && (
          <div style={{ padding: 48, textAlign: 'center', color: 'var(--fg-3)' }}>
            <div className="m" style={{ fontSize: 12 }}>no plugins match "{q}" in {CATEGORIES.find(c => c.id === cat)?.label}.</div>
          </div>
        )}
      </div>
    </div>
  );
}
function updatedRank(s) {
  if (!s) return 0;
  const m = s.match(/(\d+)\s*(d|w|mo|h)/);
  if (!m) return 9999;
  const n = +m[1]; const u = m[2];
  return -(n * ({ h: 1/24, d: 1, w: 7, mo: 30 }[u] || 1));
}

function FeaturedCard({ r, onDetail }) {
  return (
    <div className="card" style={{ background: 'var(--bg-3)', cursor: 'default' }} onClick={() => onDetail(r)}>
      <div style={{ padding: 14 }}>
        <div className="row" style={{ alignItems: 'flex-start' }}>
          <div className="pl-ico" style={{ width: 32, height: 32 }}>{r.name[0]}</div>
          <div style={{ minWidth: 0, flex: 1 }}>
            <div style={{ fontWeight: 500, fontSize: 13 }}>{r.name}</div>
            <div className="m fg3" style={{ fontSize: 10.5, marginTop: 2 }}>@{r.author} · v{r.ver}</div>
          </div>
          {r.installed
            ? <span className="badge ok">installed</span>
            : <span className="badge ac">featured</span>}
        </div>
        <div className="pl-desc" style={{ marginTop: 8 }}>{r.desc}</div>
        <div className="row" style={{ marginTop: 10, fontSize: 10.5, fontFamily: 'var(--mono)', color: 'var(--fg-3)' }}>
          <span>↓ {r.installs}</span>
          <span>·</span>
          <span>{r.updated}</span>
          <div className="grow" />
          {!r.installed && <button className="btn sm primary" onClick={(e) => { e.stopPropagation(); onDetail(r); }}>install</button>}
        </div>
      </div>
    </div>
  );
}

function BrowseRow({ r, onDetail }) {
  return (
    <div className="pl-row" onClick={() => onDetail(r)}>
      <div className="pl-ico">{r.name[0]}</div>
      <div className="pl-body">
        <div className="pl-title">
          <span>{r.name}</span>
          <span className="ver">v{r.ver}</span>
          {r.installed && <span className="badge ok">installed</span>}
          {r.featured && !r.installed && <span className="badge ac">featured</span>}
        </div>
        <div className="pl-meta">
          <span className="author">@{r.author}</span>
          <span className="dot-sep">·</span>
          <span>↓ {r.installs}</span>
          <span className="dot-sep">·</span>
          <span>updated {r.updated}</span>
          <span className="dot-sep">·</span>
          <span>{CATEGORIES.find(c => c.id === r.category)?.label || r.category}</span>
          {(r.tags || []).slice(0, 2).map((t) => (
            <span key={t} className="badge" style={{ height: 16, fontSize: 9, padding: '0 5px' }}>#{t}</span>
          ))}
        </div>
        <div className="pl-desc">{r.desc}</div>
      </div>
      <div className="pl-acts">
        {r.installed
          ? <button className="btn sm" disabled style={{ opacity: 0.6 }}>installed</button>
          : <button className="btn sm primary" onClick={(e) => { e.stopPropagation(); onDetail(r); }}>install</button>}
        <button className="btn sm ghost" onClick={(e) => { e.stopPropagation(); onDetail(r); }}>
          <span className="ico">{I.out}</span>
        </button>
      </div>
    </div>
  );
}

// ── Detail modal ─────────────────────────────────────────────────
function PluginDetail({ data, onClose }) {
  const p = data.plugin;
  const isInstalled = data.kind === 'installed' || p.installed;
  const [pane, setPane] = React.useState('overview');

  return (
    <div className="modal-scrim" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <header className="mh">
          <div className={"pl-ico " + (p.category === 'core' ? 'ac' : '')}>{p.name[0]}</div>
          <div style={{ minWidth: 0 }}>
            <h2>
              {p.name}
              <span className="ver">v{p.ver}</span>
              {p.featured && <span className="badge ac">featured</span>}
              {p.essential && <span className="badge ac">core</span>}
              {isInstalled && <span className="badge ok">installed</span>}
            </h2>
            <div className="sub">
              <span>@{p.author}</span>
              <span>·</span>
              <span>{CATEGORIES.find(c => c.id === p.category)?.label || p.category}</span>
              <span>·</span>
              <span>↓ {p.installs || '—'}</span>
              <span>·</span>
              <span>updated {p.updated || p.lastUpdated}</span>
            </div>
          </div>
          <div className="mh-acts">
            {isInstalled
              ? <>
                  <button className="btn"><span className="ico">{I.cog}</span>Edit config</button>
                  <button className="btn"><span className="ico">{I.logs}</span>View logs</button>
                  <button className="btn danger"><span className="ico">{I.trash}</span>Uninstall</button>
                </>
              : <>
                  <button className="btn"><span className="ico">{I.git}</span>Source</button>
                  <button className="btn primary"><span className="ico">{I.download}</span>Install · v{p.ver}</button>
                </>}
            <button className="mh-close" onClick={onClose}>{I.x}</button>
          </div>
        </header>

        <div className="mt">
          {[
            ['overview',    'Overview'],
            ['screenshots', 'Screenshots'],
            ['config',      'Configuration'],
            ['changelog',   'Changelog'],
          ].map(([k, l]) => (
            <button key={k} className={pane === k ? 'on' : ''} onClick={() => setPane(k)}>{l}</button>
          ))}
        </div>

        <div className="mb">
          {pane === 'overview'    && <DetailOverview p={p} />}
          {pane === 'screenshots' && <DetailScreenshots p={p} />}
          {pane === 'config'      && <DetailConfig p={p} />}
          {pane === 'changelog'   && <DetailChangelog p={p} />}
        </div>
      </div>
    </div>
  );
}

function DetailOverview({ p }) {
  const paragraphs = longDescFor(p);
  return (
    <div>
      <h3>About</h3>
      {paragraphs.map((t, i) => <p key={i}>{t}</p>)}

      <h3>Tags</h3>
      <div className="row" style={{ gap: 6, flexWrap: 'wrap' }}>
        {(p.tags || ['plugin', p.category]).concat(['cs2', 'cs#']).map((t) => (
          <span key={t} className="chip" style={{ height: 24 }}>#{t}</span>
        ))}
      </div>

      <h3>Stats</h3>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10 }}>
        <Stat label="Installs"   value={p.installs || '—'} foot="lifetime" />
        <Stat label="Servers"    value={p.installs ? `${(parseFloat(p.installs) * 0.3).toFixed(1)}k` : '—'} foot="active 30d" />
        <Stat label="Stars"      value={p.installs ? `${(parseFloat(p.installs) * 0.05).toFixed(1)}k` : '—'} foot="GitHub" />
        <Stat label="Released"   value={p.updated || p.lastUpdated} foot="latest version" />
      </div>

      <h3>Requirements</h3>
      <div className="card" style={{ padding: 0 }}>
        <div className="kv" style={{ padding: 12, gridTemplateColumns: '160px 1fr', fontSize: 12 }}>
          <dt>CounterStrikeSharp</dt><dd>≥ 1.0.300 <span className="badge ok" style={{ marginLeft: 6 }}>installed</span></dd>
          <dt>Metamod:Source</dt>    <dd>≥ 2.0.0-12 <span className="badge ok" style={{ marginLeft: 6 }}>installed</span></dd>
          <dt>Server tickrate</dt>   <dd>64 or 128 hz</dd>
          <dt>Database</dt>          <dd>{p.category === 'fun' || p.id === 'wpnt' ? 'MySQL ≥ 5.7' : 'none'}</dd>
          <dt>Disk</dt>              <dd className="m">~ 14 MB</dd>
        </div>
      </div>

      <h3>Repository</h3>
      <div className="card" style={{ padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10 }}>
        <span className="ico fg2">{I.git}</span>
        <span className="m" style={{ fontSize: 12 }}>github.com/{p.author}/{p.name.replace(/\s+/g, '-')}</span>
        <div className="grow" />
        <span className="m fg3" style={{ fontSize: 11 }}>main · a14ce82 · 3d ago</span>
        <button className="btn sm ghost"><span className="ico">{I.copy}</span></button>
      </div>
    </div>
  );
}

function DetailScreenshots({ p }) {
  return (
    <div>
      <h3>Screenshots</h3>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
        {[
          { l: '01 · in-game HUD',        mock: 'hud' },
          { l: '02 · admin chat menu',    mock: 'menu' },
          { l: '03 · config wizard',      mock: 'cfg' },
          { l: '04 · stats / leaderboard',mock: 'board' },
        ].map((s, i) => (
          <div key={i} className="shot" data-label={s.l}>
            <span className="shot-lbl">{s.l}</span>
            <ScreenshotMock kind={s.mock} name={p.name} />
          </div>
        ))}
      </div>
      <p className="hint m" style={{ marginTop: 12, fontSize: 11, color: 'var(--fg-3)' }}>
        // placeholders — screenshots will be pulled from the plugin's README when its repo is indexed.
      </p>
    </div>
  );
}

function ScreenshotMock({ kind, name }) {
  if (kind === 'hud') {
    return (
      <div className="shot-mock">
        <div className="row" style={{ marginBottom: 4 }}>
          <span style={{ color: 'var(--ac)' }}>[{name}]</span>
          <span className="fg3" style={{ marginLeft: 'auto' }}>round 8/24</span>
        </div>
        <div style={{ flex: 1, display: 'flex', alignItems: 'flex-end', gap: 2 }}>
          {[24, 32, 18, 41, 28, 36, 22, 30, 38].map((h, i) => (
            <div key={i} style={{ flex: 1, height: h, background: 'var(--ac-soft)', border: '1px solid var(--ac-line)' }} />
          ))}
        </div>
        <div className="row" style={{ marginTop: 6, color: 'var(--fg-2)' }}>
          <span>CT 4</span><span style={{ marginLeft: 'auto' }}>T 3</span>
        </div>
      </div>
    );
  }
  if (kind === 'menu') {
    return (
      <div className="shot-mock">
        <div style={{ color: 'var(--ac)', marginBottom: 4 }}>! {name} menu</div>
        {['1. enable plugin', '2. reload config', '3. open admin panel', '4. view stats', '5. exit'].map((l, i) => (
          <div key={i} style={{ color: i === 1 ? 'var(--ac)' : 'var(--fg-2)', padding: '1px 0' }}>{l}</div>
        ))}
      </div>
    );
  }
  if (kind === 'cfg') {
    return (
      <div className="shot-mock">
        <div style={{ color: 'var(--ac)' }}># config — {name}.json</div>
        <div style={{ marginTop: 4 }}>
          <div><span style={{ color: '#c6c1f5' }}>"Enabled"</span>: <span style={{ color: '#8acff7' }}>true</span>,</div>
          <div><span style={{ color: '#c6c1f5' }}>"ChatPrefix"</span>: <span style={{ color: '#f4c98a' }}>"[nokit]"</span>,</div>
          <div><span style={{ color: '#c6c1f5' }}>"MaxRetries"</span>: <span style={{ color: '#8acff7' }}>4</span></div>
        </div>
      </div>
    );
  }
  // board
  return (
    <div className="shot-mock">
      <div style={{ color: 'var(--ac)', marginBottom: 4 }}>top 5 — last 30d</div>
      {[['m0use_', '1.31'], ['zywoo•', '1.95'], ['NEKO', '1.42'], ['twist', '1.18'], ['ferr', '0.94']].map((r, i) => (
        <div key={i} className="row" style={{ padding: '1px 0' }}>
          <span className="fg3">{i + 1}.</span>
          <span style={{ color: 'var(--fg)', marginLeft: 6 }}>{r[0]}</span>
          <span style={{ marginLeft: 'auto', color: 'var(--fg-2)' }}>{r[1]}</span>
        </div>
      ))}
    </div>
  );
}

function DetailConfig({ p }) {
  const schema = configSchemaFor(p);
  return (
    <div>
      <h3>Configuration schema</h3>
      <p className="hint m" style={{ fontSize: 11.5, marginBottom: 12, color: 'var(--fg-2)' }}>
        // generated from <span className="fg">{p.name}.json.schema</span> · {schema.length} keys
      </p>
      <div className="cfg-schema">
        <div className="cfg-schema-head">
          <span>Key</span>
          <span>Type</span>
          <span>Default</span>
          <span>Description</span>
        </div>
        {schema.map((r) => (
          <div key={r.k} className="cfg-row">
            <span className="k">{r.k}</span>
            <span className={"t " + r.t}>{r.t}</span>
            <span className="d">{r.d}</span>
            <span className="desc">{r.desc}</span>
          </div>
        ))}
      </div>

      <h3>Example config</h3>
      <div className="editor" style={{ border: '1px solid var(--br)', borderRadius: 8, overflow: 'hidden' }}>
        <div className="gutter">
          {Array.from({ length: 11 }).map((_, i) => <div key={i}>{i + 1}</div>)}
        </div>
        <div className="code" style={{ whiteSpace: 'pre' }}>
          {colorJson(`{
  "Enabled": true,
  "ChatPrefix": "[\\u0010${p.name}\\u0001]",
  "AdminFlags": "abcdz",
  "DatabaseHost": "127.0.0.1",
  "CacheTtlSeconds": 300,
  "LogLevel": "info"${p.category === 'competitive' ? ',\n  "KnifeRound": true,\n  "MaxPauses": 4' : ''}${p.category === 'fun' ? ',\n  "CooldownSeconds": 60' : ''}
}`)}
        </div>
      </div>
      <p className="hint m" style={{ fontSize: 11, marginTop: 10, color: 'var(--fg-3)' }}>
        // changes hot-reload on save · no map change needed
      </p>
    </div>
  );
}

function DetailChangelog({ p }) {
  const entries = changelogFor(p);
  return (
    <div>
      <h3>Release history</h3>
      <div className="cl">
        {entries.map((e, i) => (
          <div key={i} className={"entry " + (e.latest ? 'latest' : '')}>
            <div>
              <span className="v">v{e.v}</span>
              <span className="d">{e.d}</span>
              {e.latest && <span className="badge ac">latest</span>}
              {i === entries.length - 1 && <span className="badge">first public</span>}
            </div>
            <ul>
              {e.notes.map((n, j) => <li key={j}>{n}</li>)}
            </ul>
          </div>
        ))}
      </div>

      <h3>Subscribe</h3>
      <div className="card" style={{ padding: '10px 14px', display: 'flex', alignItems: 'center', gap: 10 }}>
        <span className="ico fg2">{I.bolt}</span>
        <span style={{ fontSize: 12, color: 'var(--fg-1)' }}>Notify me on release</span>
        <div className="grow" />
        <div className="seg">
          <button className="on">channel: stable</button>
          <button>beta</button>
        </div>
        <button className="btn sm">subscribe</button>
      </div>
    </div>
  );
}

// ── Extended error log ───────────────────────────────────────────
function ExtendedErrorLog() {
  const lvl = (l) => l === 'err' ? 'err' : l === 'warn' ? 'warn' : 'info';
  const lines = [
    ...PLUGIN_ERRORS,
    { ts: '11:08:31', plug: 'CounterStrikeSharp', level: 'info', msg: 'plugin host started · 8 plugins loaded in 412ms' },
    { ts: '10:55:14', plug: 'MatchZy',            level: 'info', msg: 'autoload: warmup.cfg → live.cfg transition configured' },
    { ts: '10:42:11', plug: 'WeaponPaints',       level: 'info', msg: 'mysql: connected to 127.0.0.1:3306 (db=weapons, latency 1.4ms)' },
    { ts: '10:39:02', plug: 'GotvFix',            level: 'warn', msg: 'falling back to TCP relay — kernel multicast probe failed' },
    { ts: '10:38:54', plug: 'GotvFix',            level: 'err',  msg: 'unable to subscribe to GotvFix.RelayPort (27020) — retrying in 30s' },
  ];
  return (
    <div>
      <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
        <div className="input" style={{ flex: 1, maxWidth: 420 }}>
          <span className="ico">{I.search}</span>
          <input placeholder="search plugin output — message, plugin name, level" />
        </div>
        <div className="seg">
          <button className="on">all</button>
          <button>errors</button>
          <button>warnings</button>
          <button>info</button>
        </div>
        <div className="grow" />
        <button className="btn"><span className="ico">{I.download}</span>Export</button>
      </div>
      <div className="term" style={{ height: 380 }}>
        <div className="out">
          {lines.map((e, i) => (
            <div key={i} className="ln">
              <span className="ts">2026-05-21 {e.ts}</span>
              <span className={lvl(e.level)}>[{e.level.toUpperCase().padEnd(4, ' ')}]</span>
              <span className="cv"> {e.plug.padEnd(22, ' ')}</span>
              <span> — {e.msg}</span>
            </div>
          ))}
          {Array.from({ length: 12 }).map((_, i) => <div key={'b' + i} className="ln dim">{' '}</div>)}
        </div>
        <div className="input-row">
          <span className="prompt fg3">plugin-log&nbsp;❯</span>
          <span className="m fg3" style={{ flex: 1, fontSize: 11.5 }}>last 24h · 1 err · 2 warn · 12 info · {lines.length} lines</span>
          <button className="btn sm">clear</button>
        </div>
      </div>
    </div>
  );
}

// override the panels-c.jsx export
Object.assign(window, { PluginsPanel });
