// landing.jsx — nokit marketing page

const { useState, useEffect, useRef, useMemo } = React;

// ── Icons ────────────────────────────────────────────────────────
const Ic = ({ d, sw = 1.6, fill, size = 14 }) => (
  <svg
    className="ico"
    width={size}
    height={size}
    viewBox="0 0 24 24"
    fill={fill || "none"}
    stroke={fill ? "none" : "currentColor"}
    strokeWidth={sw}
    strokeLinecap="round"
    strokeLinejoin="round"
  >
    {typeof d === "string" ? <path d={d} /> : d}
  </svg>
);
const I = {
  arrow: <Ic d="M5 12h14M13 6l6 6-6 6" />,
  arrow2: <Ic d="M2 12h20M17 7l5 5-5 5" sw={1.4} />,
  git: (
    <Ic d="M12 2A10 10 0 0 0 8.84 21.5c.5.09.66-.22.66-.48v-1.7c-2.78.6-3.36-1.34-3.36-1.34-.46-1.17-1.11-1.48-1.11-1.48-.91-.62.07-.6.07-.6 1 .07 1.53 1.04 1.53 1.04.89 1.52 2.34 1.08 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.1.39-2 1.03-2.7-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.6 9.6 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.6 1.03 2.7 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85V21c0 .27.16.58.67.48A10 10 0 0 0 12 2z" />
  ),
  term: <Ic d="M4 6l4 6-4 6M11 18h9" />,
  logs: <Ic d="M4 6h16M4 12h10M4 18h16M19 12l2 2-2 2" />,
  users: (
    <Ic d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
  ),
  map: <Ic d="M9 4l-6 2v14l6-2 6 2 6-2V4l-6 2-6-2zM9 4v14M15 6v14" />,
  sliders: (
    <Ic d="M4 21v-7M4 10V3M12 21v-9M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6" />
  ),
  pulse: <Ic d="M3 12h4l3-9 4 18 3-9h4" />,
  plug: <Ic d="M9 2v6M15 2v6M5 8h14v3a7 7 0 0 1-14 0V8zM12 18v4" />,
  code: <Ic d="M16 18l6-6-6-6M8 6l-6 6 6 6M14 4l-4 16" />,
  clock: <Ic d="M12 6v6l4 2M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z" />,
  shield: <Ic d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />,
  hook: (
    <Ic d="M9 4v6a3 3 0 0 0 6 0V6.5a2.5 2.5 0 1 1 5 0V14a8 8 0 0 1-16 0V8" />
  ),
  audit: <Ic d="M21 4H7v16h14zM3 4v16M21 8H7M21 12H7M21 16H7" />,
  docker: (
    <Ic
      d="M3 12h2v2H3zM6 12h2v2H6zM9 12h2v2H9zM12 12h2v2h-2zM6 9h2v2H6zM9 9h2v2H9zM12 9h2v2h-2zM12 6h2v2h-2zM3 14a6 6 0 0 0 6 6h2a8 8 0 0 0 7-4h2a3 3 0 0 0 3-3v-1l-2 1h-3"
      sw={1.2}
    />
  ),
  box: <Ic d="M21 8 12 3 3 8v8l9 5 9-5zM3 8l9 5 9-5M12 13v8" />,
  globe: (
    <Ic d="M12 22a10 10 0 1 1 0-20 10 10 0 0 1 0 20zM2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
  ),
  check: <Ic d="M5 13l4 4L19 7" sw={2.2} />,
  copy: (
    <Ic d="M9 9h12v12H9zM5 15H3a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v2" />
  ),
  ext: <Ic d="M15 3h6v6M10 14L21 3M21 14v7H3V3h7" />,
  search: (
    <Ic d="M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3" sw={1.8} />
  ),
  file: (
    <Ic d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6" />
  ),
  folder: (
    <Ic d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
  ),
  bolt: <Ic d="M13 2L3 14h8l-1 8 11-13h-9z" />,
  cpu: (
    <Ic d="M4 4h16v16H4zM9 9h6v6H9zM4 9h1M4 15h1M19 9h1M19 15h1M9 4v1M15 4v1M9 19v1M15 19v1" />
  ),
  ram: (
    <Ic d="M3 8h18v9H3zM7 12v2M11 12v2M15 12v2M19 12v2M5 19v1M9 19v1M13 19v1M17 19v1M19 19v1" />
  ),
};

// ── Hero terminal — typing + autocomplete + log stream ────────────
const RCON_CMDS = [
  { name: "mp_warmuptime", val: "15", ty: "replicated", kind: "cvar" },
  { name: "mp_warmup_end", val: "—", ty: "cmd", kind: "cmd" },
  { name: "mp_warmup_pausetimer", val: "0", ty: "replicated", kind: "cvar" },
  {
    name: "mp_warmup_offline_enabled",
    val: "0",
    ty: "replicated",
    kind: "cvar",
  },
];
const HERO_TARGET = "mp_warmup_end";

const SAMPLE_LOGS = [
  () => ({ t: ts(), tag: "rcon", cls: "", txt: "> status" }),
  () => ({
    t: ts(),
    tag: "srv",
    cls: "dim",
    txt: "hostname: nokit · fra1.mm  ·  map: de_mirage  ·  players: 10/12  ·  pubkey: 8f12..ed94",
  }),
  () => ({ t: ts(), tag: "kill", cls: "err", txt: kill() }),
  () => ({ t: ts(), tag: "chat", cls: "info", txt: chat() }),
  () => ({
    t: ts(),
    tag: "round",
    cls: "ok",
    txt: 'World triggered "Round_Start"',
  }),
  () => ({ t: ts(), tag: "kill", cls: "err", txt: kill() }),
  () => ({ t: ts(), tag: "conn", cls: "warn", txt: connect() }),
  () => ({
    t: ts(),
    tag: "plug",
    cls: "info",
    txt: '[matchzy] live config "mr12_competitive.cfg" applied',
  }),
  () => ({ t: ts(), tag: "kill", cls: "err", txt: kill() }),
  () => ({
    t: ts(),
    tag: "round",
    cls: "ok",
    txt: 'Team "CT" triggered "SFUI_Notice_Bomb_Defused" (CT "3") (T "1")',
  }),
];

function ts() {
  const d = new Date();
  return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
function pad(n) {
  return n < 10 ? "0" + n : "" + n;
}
const NAMES = [
  "m0use_",
  "NEKO",
  "zywoo•",
  "twist",
  "olof",
  "ferr",
  "bymas",
  "jL",
];
const WEAPONS = [
  "ak47",
  "m4a1_silencer",
  "awp",
  "deagle",
  "usp_silencer",
  "famas",
  "glock",
];
function pick(arr) {
  return arr[Math.floor(Math.random() * arr.length)];
}
function kill() {
  const a = pick(NAMES),
    b = pick(NAMES.filter((n) => n !== pick(NAMES)));
  const w = pick(WEAPONS);
  const hs = Math.random() > 0.6 ? " (headshot)" : "";
  return `"${a}<${rand(2, 9)}><STEAM_1:${rand(0, 1)}:${rand(10000000, 99999999)}><CT>" killed "${b}<${rand(2, 9)}><STEAM_1:${rand(0, 1)}:${rand(10000000, 99999999)}><T>" with "${w}"${hs}`;
}
function chat() {
  const sayings = [
    "gg",
    "rotate B",
    "one toxic at long",
    "info one mid",
    "eco round",
    "save save save",
    "nt",
    "force buy?",
    "flash for me",
    "gh",
  ];
  return `"${pick(NAMES)}<${rand(2, 9)}><STEAM_1:${rand(0, 1)}:${rand(10000000, 99999999)}><CT>" say "${pick(sayings)}"`;
}
function connect() {
  return `"${pick(NAMES)}<${rand(2, 9)}><STEAM_1:${rand(0, 1)}:${rand(10000000, 99999999)}><>" connected, address "${rand(20, 250)}.${rand(0, 255)}.${rand(0, 255)}.${rand(20, 250)}:27015"`;
}
function rand(a, b) {
  return Math.floor(Math.random() * (b - a + 1)) + a;
}

function syntaxColor(txt) {
  const parts = [];
  let i = 0,
    key = 0,
    last = 0;
  const push = (cls, s) =>
    parts.push(
      cls ? (
        <span key={key++} className={cls}>
          {s}
        </span>
      ) : (
        s
      ),
    );
  const re =
    /"([^"]*)"|(-?\d+(?:\.\d+)?)|((?:mp_|sv_|host_|matchzy_|tv_|cl_|bot_)[a-z_]+)/g;
  let m;
  while ((m = re.exec(txt))) {
    if (m.index > last) push(null, txt.slice(last, m.index));
    if (m[1] !== undefined) push("str", `"${m[1]}"`);
    else if (m[2] !== undefined) push("num", m[2]);
    else if (m[3] !== undefined) push("cv", m[3]);
    last = re.lastIndex;
  }
  if (last < txt.length) push(null, txt.slice(last));
  return parts.map((p, i) =>
    typeof p === "string" ? (
      <React.Fragment key={"t" + i}>{p}</React.Fragment>
    ) : (
      p
    ),
  );
}

function HeroTerminal() {
  const [typed, setTyped] = useState("");
  const [showAc, setShowAc] = useState(false);
  const [acIdx, setAcIdx] = useState(0);
  const [phase, setPhase] = useState("idle"); // idle | typing | submitted
  const [lines, setLines] = useState(() =>
    SAMPLE_LOGS.slice(0, 6).map((f) => f()),
  );
  const streamRef = useRef(null);

  // typing animation
  useEffect(() => {
    let cancelled = false;
    let timeouts = [];
    const tx = (fn, ms) => {
      const t = setTimeout(() => !cancelled && fn(), ms);
      timeouts.push(t);
    };

    const cycle = () => {
      setTyped("");
      setShowAc(false);
      setAcIdx(0);
      setPhase("typing");
      // type the target char by char
      for (let i = 1; i <= HERO_TARGET.length; i++) {
        tx(
          () => {
            setTyped(HERO_TARGET.slice(0, i));
            // show autocomplete after a few chars
            if (i >= 3 && i < HERO_TARGET.length - 1) {
              setShowAc(true);
              // arrow down to highlight mp_warmup_end (index 1 after 3+ chars filter to mp_warmup_*)
              if (i === 9) setAcIdx(1);
            } else if (i >= HERO_TARGET.length - 1) {
              setShowAc(false);
            }
          },
          70 * i + rand(0, 60),
        );
      }
      // submit
      tx(
        () => {
          setPhase("submitted");
          // push command + responses
          const t = ts();
          setLines((ls) => [
            ...ls,
            { t, tag: "rcon", cls: "", txt: "> " + HERO_TARGET },
            {
              t,
              tag: "srv",
              cls: "dim",
              txt: "[matchzy] warmup ended. live in 5…",
            },
            {
              t,
              tag: "world",
              cls: "ok",
              txt: 'World triggered "Round_Start"',
            },
          ]);
          setTyped("");
        },
        70 * HERO_TARGET.length + 800,
      );
      // wait then loop
      tx(cycle, 70 * HERO_TARGET.length + 6500);
    };
    tx(cycle, 1200);
    return () => {
      cancelled = true;
      timeouts.forEach(clearTimeout);
    };
  }, []);

  // stream new log lines into the feed
  useEffect(() => {
    const i = setInterval(() => {
      setLines((ls) => [
        ...ls.slice(-30),
        SAMPLE_LOGS[Math.floor(Math.random() * SAMPLE_LOGS.length)](),
      ]);
    }, 1300);
    return () => clearInterval(i);
  }, []);

  useEffect(() => {
    if (streamRef.current)
      streamRef.current.scrollTop = streamRef.current.scrollHeight;
  }, [lines]);

  const acFiltered = useMemo(() => {
    if (!typed) return RCON_CMDS;
    return RCON_CMDS.filter((c) => c.name.startsWith(typed));
  }, [typed]);

  return (
    <div className="hero-term-wrap">
      <div className="hero-term">
        <div className="chrome">
          <div className="dots">
            <i />
            <i />
            <i />
          </div>
          <span className="title">rcon — fra1.mm · 5.39.42.118:27015</span>
          <span className="pill">
            <i className="pulse" /> live · srcds tail
          </span>
        </div>
        <div className="body">
          <div className="stream" ref={streamRef}>
            {lines.map((l, i) => (
              <div key={`${l.t}-${i}`} className="ln">
                <span className="ts">{l.t}</span>
                <span className={"tag " + (l.cls || "")}>
                  {l.tag.padEnd(5, " ")}
                </span>
                {syntaxColor(l.txt)}
              </div>
            ))}
          </div>

          {showAc && acFiltered.length > 0 && (
            <div className="ac-pop">
              {acFiltered.map((c, i) => (
                <div
                  key={c.name}
                  className={"ac-row " + (i === acIdx ? "on" : "")}
                >
                  <span className="name">
                    <b>{c.name.slice(0, typed.length)}</b>
                    {c.name.slice(typed.length)}
                  </span>
                  <span className="val">= {c.val}</span>
                  <span className="ty">{c.ty}</span>
                </div>
              ))}
            </div>
          )}

          <div className="input">
            <span className="prompt">rcon&nbsp;❯</span>
            <span className="typed">{typed}</span>
            <span className="cursor" />
          </div>
        </div>
      </div>
    </div>
  );
}

// ── Feature grid data ───────────────────────────────────────────
const FEATURES = [
  {
    ico: I.term,
    title: "RCON Console",
    tag: "⌘K",
    desc: "Full RCON with inline autocomplete sourced from cvarlist, command history, and saved macros.",
  },
  {
    ico: I.logs,
    title: "Live Log Streaming",
    tag: "SSE",
    desc: "Tail srcds_logaddress over SSE with grep, category tabs, and persistent search history.",
  },
  {
    ico: I.users,
    title: "Player Management",
    tag: "",
    desc: "Live roster with ping, country, K/D, kick with reason, ban with expiry, and a SteamID watchlist.",
  },
  {
    ico: I.map,
    title: "Map Control",
    tag: "",
    desc: "Standard map pool, workshop ID input, collection sync, mapcycle editor, and one-click changelevel.",
  },
  {
    ico: I.sliders,
    title: "CVAR Presets",
    tag: "",
    desc: "Grouped chip presets for match, practice, retake, and LAN. Apply ad-hoc or persist to server.cfg.",
  },
  {
    ico: I.pulse,
    title: "Server Stats",
    tag: "",
    desc: "CPU, RAM, tickrate, uptime, and player-count sparklines pulled from the stats command every 4s.",
  },
  {
    ico: I.plug,
    title: "Plugin Management",
    tag: "CS#",
    desc: "Install, enable, configure, and update CounterStrikeSharp and Metamod plugins from the browser.",
  },
  {
    ico: I.code,
    title: "Config Editor",
    tag: "",
    desc: 'Monaco-style editor for mounted .cfg files with syntax highlighting and "save & exec via rcon".',
  },
  {
    ico: I.clock,
    title: "Scheduled Commands",
    tag: "cron",
    desc: "Nightly restarts, warmup resets, workshop syncs, and webhook hooks. Cron-compatible.",
  },
  {
    ico: I.shield,
    title: "Multi-Admin · Roles",
    tag: "",
    desc: "Owner / admin / moderator roles with per-capability matrix. MFA-aware. SteamID or email auth.",
  },
  {
    ico: I.hook,
    title: "Discord Webhooks",
    tag: "",
    desc: "Match start/end, server down, plugin error, and ban events forwarded to your channel.",
  },
  {
    ico: I.audit,
    title: "Audit Log",
    tag: "",
    desc: "Every kick, ban, config edit, plugin install, and signin recorded with 90-day retention.",
  },
];

// ── How it works ───────────────────────────────────────────────
const STEPS = [
  {
    n: "01",
    title: "Deploy alongside your CS2 container",
    desc: "A single 14 MB Go binary, distributed as an OCI image. Talks to srcds over loopback RCON — no extra ports exposed.",
    mock: (
      <div className="mock">
        <span className="com"># docker-compose.yml</span>
        {"\n"}
        <span className="key">services:</span>
        {"\n"}
        {"  "}
        <span className="key">cs2:</span>
        <span className="com"> # your srcds</span>
        {"\n"}
        {"  "}
        <span className="key">nokit:</span>
        {"\n"}
        {"    "}
        <span className="key">image:</span>{" "}
        <span className="str">ghcr.io/nokit/nokit</span>
        {"\n"}
        {"    "}
        <span className="key">depends_on:</span> [
        <span className="str">cs2</span>]
      </div>
    ),
  },
  {
    n: "02",
    title: "Add your server in the UI",
    desc: "Open the panel on :8080, enter your server's ip:port and rcon_password. nokit pings, verifies, and remembers it.",
    mock: (
      <div className="mock">
        <span className="ac">srv ❯</span> add fra1.mm{" "}
        <span className="str">5.39.42.118:27015</span>
        {"\n"}
        <span className="com"> ✓ connected · v1.40.6.3 · tickrate</span>{" "}
        <span className="num">127.8</span>
        {"\n"}
        <span className="com"> ✓ rcon authenticated · </span>
        <span className="ac">live</span>
        {"\n"}
        <span className="com"> ✓ logaddress wired · </span>
        <span className="ac">streaming</span>
      </div>
    ),
  },
  {
    n: "03",
    title: "Manage everything from the browser",
    desc: "Console, players, maps, plugins, configs, scheduling — all in one panel. No SSH for the day-to-day stuff again.",
    mock: (
      <div className="mock">
        <span className="ac">https://</span>panel.your-server.io{"\n"}
        <span className="com"> · 10 panels · ⌘K everywhere</span>
        {"\n"}
        <span className="com"> · 1.4k log lines/min</span>
        {"\n"}
        <span className="com"> · 32 plugins · 14 maps · </span>
        <span className="ac">9</span> <span className="com">players</span>
      </div>
    ),
  },
];

// ── Mock plugin browser (spotlight) ─────────────────────────────
function MockBrowser() {
  return (
    <div className="mock-browser">
      <div className="head">
        <span className="ico" style={{ width: 14 }}>
          {I.plug}
        </span>
        <span style={{ color: "var(--fg)" }}>Plugins</span>
        <span style={{ color: "var(--fg-3)" }}>· browse · 327 available</span>
      </div>
      <div className="layout">
        <div className="rail">
          <div className="g">Categories</div>
          <div className="it on">
            all<span className="ct">327</span>
          </div>
          <div className="it">
            admin tools<span className="ct">41</span>
          </div>
          <div className="it">
            competitive<span className="ct">28</span>
          </div>
          <div className="it">
            fun<span className="ct">62</span>
          </div>
          <div className="it">
            utilities<span className="ct">88</span>
          </div>
          <div className="it">
            maps<span className="ct">19</span>
          </div>
          <div className="it">
            gamemodes<span className="ct">24</span>
          </div>
        </div>
        <div className="list">
          <div className="search">
            <div className="input">
              <span className="ico" style={{ width: 11 }}>
                {I.search}
              </span>
              <span style={{ color: "var(--fg-2)" }}>retakes</span>
            </div>
            <span
              className="kbd"
              style={{
                fontFamily: "var(--mono)",
                fontSize: 10,
                color: "var(--fg-3)",
              }}
            >
              /
            </span>
          </div>
          {[
            {
              ic: "C",
              t: "CS2-RetakesAllocator",
              v: "2.1.0",
              a: "@b3none",
              m: "↓ 47.4k · updated 3d ago · competitive",
              installed: false,
            },
            {
              ic: "M",
              t: "MatchZy",
              v: "0.8.7",
              a: "@shobhit-pathak",
              m: "↓ 92.1k · updated 3d ago · competitive",
              installed: true,
            },
            {
              ic: "W",
              t: "WeaponPaints",
              v: "4.0",
              a: "@Nereziel",
              m: "↓ 88.0k · updated 5d ago · fun",
              installed: true,
            },
            {
              ic: "S",
              t: "Surf Timer",
              v: "3.0.4",
              a: "@kitsunelab",
              m: "↓ 21.6k · updated 4d ago · gamemodes",
              installed: false,
            },
            {
              ic: "A",
              t: "AccessChecker",
              v: "1.1.0",
              a: "@d00mfist",
              m: "↓ 24.8k · updated 5d ago · admin",
              installed: false,
            },
          ].map((r) => (
            <div key={r.t} className="row">
              <div className="ic">{r.ic}</div>
              <div>
                <div className="t">
                  {r.t} <span className="ver">v{r.v}</span>
                </div>
                <div className="m">
                  {r.a} · {r.m}
                </div>
              </div>
              <span className={"b " + (r.installed ? "installed" : "")}>
                {r.installed ? "installed" : "install"}
              </span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── Stats card + live log column ────────────────────────────────
function genSeries(seed, n, base, amp) {
  const out = [];
  let v = base;
  for (let i = 0; i < n; i++) {
    seed = (seed * 9301 + 49297) % 233280;
    v += (seed / 233280 - 0.5) * amp;
    v = Math.max(0, v);
    out.push(v);
  }
  return out;
}
function Spark({ data, color, fill = true, h = 32 }) {
  const w = 200;
  const max = Math.max(...data),
    min = Math.min(...data);
  const rng = max - min || 1;
  const pts = data.map(
    (v, i) =>
      `${(i / (data.length - 1)) * w},${(h - ((v - min) / rng) * (h - 4) - 2).toFixed(1)}`,
  );
  const dLine = `M ${pts.join(" L ")}`;
  const dArea = `${dLine} L ${w},${h} L 0,${h} Z`;
  const gid = React.useId();
  return (
    <svg className="spark" viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none">
      {fill && (
        <>
          <defs>
            <linearGradient id={gid} x1="0" x2="0" y1="0" y2="1">
              <stop offset="0%" stopColor={color} stopOpacity="0.32" />
              <stop offset="100%" stopColor={color} stopOpacity="0" />
            </linearGradient>
          </defs>
          <path d={dArea} fill={`url(#${gid})`} />
        </>
      )}
      <path
        d={dLine}
        fill="none"
        stroke={color}
        strokeWidth="1.4"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function StatsLogsSplit() {
  const players = useMemo(
    () => genSeries(7, 36, 8, 5).map((v) => Math.max(0, Math.min(12, v))),
    [],
  );
  const cpu = useMemo(
    () => genSeries(11, 36, 18, 6).map((v) => Math.max(2, Math.min(70, v))),
    [],
  );
  const ram = useMemo(
    () => genSeries(13, 36, 38, 4).map((v) => Math.max(20, Math.min(70, v))),
    [],
  );
  const tick = useMemo(
    () =>
      genSeries(17, 36, 128, 1.4).map((v) => Math.max(110, Math.min(130, v))),
    [],
  );

  const [lines, setLines] = useState(() =>
    SAMPLE_LOGS.slice(0, 8).map((f) => f()),
  );
  useEffect(() => {
    const i = setInterval(() => {
      setLines((ls) => [
        ...ls.slice(-18),
        SAMPLE_LOGS[Math.floor(Math.random() * SAMPLE_LOGS.length)](),
      ]);
    }, 1600);
    return () => clearInterval(i);
  }, []);

  return (
    <div className="split">
      <div className="split-card">
        <div className="h">
          <span className="t">Dashboard</span>
          <span>· fra1.mm</span>
          <span className="grow" />
          <span className="pill">
            <span
              style={{
                display: "inline-block",
                width: 5,
                height: 5,
                borderRadius: "50%",
                background: "var(--ac)",
                boxShadow: "0 0 0 3px var(--ac-soft)",
                marginRight: 4,
              }}
            />
            live
          </span>
        </div>
        <div className="stat-grid">
          <div className="stat-cell">
            <div className="lbl">Players</div>
            <div className="v">
              9<span className="u">/ 12</span>
              <span className="delta">+2</span>
            </div>
            <Spark data={players} color="var(--ac)" />
            <div className="foot">peak 12 · avg 6.4</div>
          </div>
          <div className="stat-cell">
            <div className="lbl">Tick rate</div>
            <div className="v">
              127.8<span className="u">hz</span>
            </div>
            <Spark data={tick} color="#f4c98a" />
            <div className="foot">target 128 · jitter 0.4</div>
          </div>
          <div className="stat-cell">
            <div className="lbl">CPU</div>
            <div className="v">
              22<span className="u">%</span>
            </div>
            <Spark data={cpu} color="#8acff7" />
            <div className="foot">4-core · 0.88 load avg</div>
          </div>
          <div className="stat-cell">
            <div className="lbl">Memory</div>
            <div className="v">
              2.1<span className="u">/ 8 GB</span>
            </div>
            <Spark data={ram} color="#c6a6ff" />
            <div className="foot">26% resident</div>
          </div>
        </div>
      </div>

      <div className="split-card">
        <div className="h">
          <span className="t">Live Logs</span>
          <span>· SSE · 1.4k lines/min</span>
          <span className="grow" />
          <span className="pill">
            <span
              style={{
                display: "inline-block",
                width: 5,
                height: 5,
                borderRadius: "50%",
                background: "var(--ac)",
                boxShadow: "0 0 0 3px var(--ac-soft)",
                marginRight: 4,
              }}
            />
            streaming
          </span>
        </div>
        <div className="log-stream">
          {lines.map((l, i) => (
            <div key={`${l.t}-${i}`} className="ln">
              <span className="ts">{l.t}</span>
              <span className={"tag " + (l.tag || "")}>
                {(l.tag || "").padEnd(5)}
              </span>
              {syntaxColor(l.txt)}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ── Compose snippet with copy ─────────────────────────────────
const COMPOSE = `services:
  cs2:
    image: joedwards32/cs2:latest
    ports: ["27015:27015/udp"]
    volumes: ["./cs2:/home/steam/cs2"]

  nokit:
    image: ghcr.io/nokit/nokit:latest
    ports: ["8080:8080"]
    volumes: ["./nokit:/data"]
    environment:
      NOKIT_ADMIN: m0use@local
    depends_on: [cs2]`;

function colorYaml(s) {
  const out = [];
  let k = 0;
  s.split("\n").forEach((line, i) => {
    if (i > 0) out.push("\n");
    const cm = line.match(/^(\s*)(#.*)$/);
    if (cm) {
      out.push(
        cm[1],
        <span key={k++} className="com">
          {cm[2]}
        </span>,
      );
      return;
    }
    const m = line.match(/^(\s*)([\w-]+)(:)(\s*)(.*)$/);
    if (m) {
      out.push(
        m[1],
        <span key={k++} className="key">
          {m[2]}
        </span>,
        m[3],
        m[4],
      );
      let rest = m[5];
      if (!rest) return;
      // numbers, strings, arrays
      const r2 = rest.match(/^(\[.*\])$|^(".*")$|^('.*')$|^(\d+)$|^(.+)$/);
      if (rest.startsWith("["))
        out.push(
          <span key={k++} className="num">
            {rest}
          </span>,
        );
      else if (rest.startsWith('"') || rest.startsWith("'"))
        out.push(
          <span key={k++} className="str">
            {rest}
          </span>,
        );
      else if (/^\d/.test(rest))
        out.push(
          <span key={k++} className="num">
            {rest}
          </span>,
        );
      else
        out.push(
          <span key={k++} className="lo">
            {rest}
          </span>,
        );
    } else {
      out.push(line);
    }
  });
  return out.map((p, i) =>
    typeof p === "string" ? (
      <React.Fragment key={"s" + i}>{p}</React.Fragment>
    ) : (
      p
    ),
  );
}

function ComposeBlock() {
  const [copied, setCopied] = useState(false);
  const lines = COMPOSE.split("\n");
  return (
    <div className="compose">
      <div className="head">
        <span className="ico" style={{ width: 13, color: "var(--fg-2)" }}>
          {I.file}
        </span>
        <span className="file">docker-compose.yml</span>
        <span className="lang">· yaml · 13 lines</span>
        <span className="grow" />
        <button
          className={"copy " + (copied ? "copied" : "")}
          onClick={() => {
            navigator.clipboard?.writeText(COMPOSE);
            setCopied(true);
            setTimeout(() => setCopied(false), 1600);
          }}
        >
          {copied ? I.check : I.copy} {copied ? "copied" : "copy"}
        </button>
      </div>
      <div className="code">
        <div className="gutter">
          {lines.map((_, i) => (
            <div key={i}>{i + 1}</div>
          ))}
        </div>
        <div className="body">{colorYaml(COMPOSE)}</div>
      </div>
    </div>
  );
}

// ── Page ─────────────────────────────────────────────────────────
function App() {
  return (
    <div className="wrap">
      {/* Nav */}
      <nav className="nav">
        <div className="container row">
          <a className="brand" href="/">
            <span className="mark">n</span>
            <span className="name">nokit</span>
            <span className="ver">v0.1.0-rc.1</span>
          </a>
          <div className="links">
            <a href="#features">Features</a>
            <a href="#how">How it works</a>
            <a href="#plugins">Plugins</a>
            <a href="#preview">Preview</a>
            <a href="#install">Install</a>
          </div>
          <div className="actions">
            <a className="btn ghost" href="#">
              <span className="ico">{I.git}</span>GitHub{" "}
              {/* <span className="mono fg3" style={{ fontSize: 11 }}>
                · 1.4k
              </span>*/}
            </a>
            <a className="btn primary" href="/demo">
              <span className="ico">{I.arrow}</span>Live Demo
            </a>
          </div>
        </div>
      </nav>

      {/* Hero */}
      <section className="hero">
        <div className="container">
          <span className="eyebrow ac">
            <span className="dot" />
            v0.1.0-rc.1 · MIT-licensed · self-hosted
          </span>
          <h1>
            Your CS2 server.
            <br />
            Your panel. <span className="ac">No cloud required.</span>
          </h1>
          <p className="sub">
            nokit is a self-hosted web admin for Counter-Strike 2 dedicated
            servers. RCON, live logs, players, plugins, configs — in one panel.
            Runs in Docker on the same box as srcds. No accounts, no telemetry,
            no SaaS.
          </p>
          <div className="ctas">
            <a className="btn primary lg" href="#install">
              Get started{I.arrow}
            </a>
            <a className="btn lg" href="https://github.com/codevski/nokit">
              <span className="ico">{I.git}</span>View on GitHub
            </a>
          </div>
          <div className="meta">
            <span>
              <span className="ok">●</span> works with srcds 1.40+
            </span>
            <span className="pip" />
            <span>14 MB single binary</span>
            <span className="pip" />
            <span>OCI · systemd · bare-metal</span>
            <span className="pip" />
            <span>
              <a href="/demo" className="ac">
                live demo →
              </a>
            </span>
          </div>

          <HeroTerminal />
        </div>
      </section>

      {/* Feature grid */}
      <section id="features">
        <div className="container">
          <div className="sec-hd">
            <span className="eyebrow">
              <span className="dot" />
              Everything you need
            </span>
            <h2>Your server, in a browser tab.</h2>
            <p>
              Everything you reach for over SSH - consoles, configs, players,
              plugins - without leaving the browser.
            </p>
          </div>
          <div className="f-grid">
            {FEATURES.map((f) => (
              <div key={f.title} className="f-cell">
                <div className="f-ico">{f.ico}</div>
                <h3>
                  {f.title}
                  {f.tag && <span className="tag">{f.tag}</span>}
                </h3>
                <p>{f.desc}</p>
              </div>
            ))}
          </div>
        </div>
      </section>

      {/* How it works */}
      <section id="how" className="tight">
        <div className="container">
          <div className="sec-hd">
            <span className="eyebrow">
              <span className="dot" />
              How it works
            </span>
            <h2>Three steps. One evening.</h2>
            <p>
              nokit doesn't ask for cloud creds, a Steam tenant, or a Discord
              OAuth dance. It runs next to your server and talks to it the way
              you would.
            </p>
          </div>
          <div className="how">
            <div className="how-step">
              <div className="step-n">
                <b>01</b>
                <span>Deploy</span>
              </div>
              <h3>{STEPS[0].title}</h3>
              <p>{STEPS[0].desc}</p>
              {STEPS[0].mock}
            </div>
            <div className="how-arrow">
              <svg viewBox="0 0 28 18">
                <path
                  d="M2 9h22M19 4l6 5-6 5"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.4"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
            </div>
            <div className="how-step">
              <div className="step-n">
                <b>02</b>
                <span>Connect</span>
              </div>
              <h3>{STEPS[1].title}</h3>
              <p>{STEPS[1].desc}</p>
              {STEPS[1].mock}
            </div>
            <div className="how-arrow">
              <svg viewBox="0 0 28 18">
                <path
                  d="M2 9h22M19 4l6 5-6 5"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.4"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
            </div>
            <div className="how-step">
              <div className="step-n">
                <b>03</b>
                <span>Run it</span>
              </div>
              <h3>{STEPS[2].title}</h3>
              <p>{STEPS[2].desc}</p>
              {STEPS[2].mock}
            </div>
          </div>
        </div>
      </section>

      {/* Plugin spotlight */}
      <section id="plugins">
        <div className="container">
          <div className="spot">
            <div className="copy">
              <span className="eyebrow ac">
                <span className="dot" />
                CounterStrikeSharp
              </span>
              <h2>Browse, install, configure CS# plugins — without SSH.</h2>
              <p>
                The CounterStrikeSharp ecosystem is great but the installation
                flow is hostile: clone, unzip, drop into{" "}
                <span className="mono ac">
                  addons/counterstrikesharp/plugins/
                </span>
                , restart, hope.
              </p>
              <p>
                nokit indexes the public registry, shows you what's there, and
                installs with one click. Configs hot-reload. Logs filter per
                plugin. Updates are a badge, not a chore.
              </p>
              <ul>
                {[
                  "327 plugins indexed — searchable, filterable, with installed badges",
                  "JSON config editor inline — schema preview, hot reload on save",
                  '"View logs" filters the live SSE stream to one plugin',
                  "Update arrows surface what's out of date, all at once",
                ].map((t) => (
                  <li key={t}>
                    <span className="ck">{I.check}</span>
                    <span>{t}</span>
                  </li>
                ))}
              </ul>
              <div style={{ display: "flex", gap: 10 }}>
                <a className="btn primary" href="/demo">
                  Explore the plugins panel{I.arrow}
                </a>
                <a className="btn" href="#">
                  <span className="ico">{I.ext}</span>CounterStrikeSharp docs
                </a>
              </div>
            </div>
            <div>
              <MockBrowser />
            </div>
          </div>
        </div>
      </section>

      {/* Stats + Logs preview */}
      <section id="preview">
        <div className="container">
          <div className="sec-hd">
            <span className="eyebrow">
              <span className="dot" />
              Always on
            </span>
            <h2>Server stats and the log tail, side by side.</h2>
            <p>
              Pulled live from <span className="mono">stats</span> and{" "}
              <span className="mono">logaddress_add</span>. Both panels stream
              over SSE, both keep working through container restarts.
            </p>
          </div>
          <StatsLogsSplit />
        </div>
      </section>

      {/* Quick start */}
      <section id="install" className="tight">
        <div className="container">
          <div className="sec-hd">
            <span className="eyebrow">
              <span className="dot" />
              Quick start
            </span>
            <h2>One file. Four lines. Live in a minute.</h2>
            <p>
              Drop this next to your existing CS2 compose. nokit picks the
              server up automatically when the container appears on the same
              network.
            </p>
          </div>
          <div className="quick">
            <ComposeBlock />
            <div className="quick-foot">
              <span className="mono">
                <span className="ac">$</span> docker compose up -d
              </span>
              <span className="pip" />
              <a href="#">→ full install guide</a>
              <span className="pip" />
              <a href="#">→ bare-metal / systemd</a>
              <span className="pip" />
              <a href="#">→ reverse-proxy + TLS</a>
            </div>
          </div>
        </div>
      </section>

      {/* Footer */}
      <footer className="foot">
        <div className="container row">
          <div className="brand-min">
            <span className="mark">n</span>
            <span className="mono">nokit</span>
          </div>
          <div className="meta">
            <a href="https://github.com/codevski/nokit">
              <span
                className="ico"
                style={{ width: 13, verticalAlign: "-2px" }}
              >
                {I.git}
              </span>{" "}
              github
            </a>
            <span className="pip" />
            <a href="#">docs</a>
            <span className="pip" />
            <a href="#">changelog</a>
            <span className="pip" />
            <a href="#">discord</a>
            <span className="pip" />
            <span>MIT</span>
            <span className="pip" />
            <span>v0.1.0-rc.1</span>
          </div>
          <div className="right">
            built for <span className="ac">homelab operators</span>.
          </div>
        </div>
      </footer>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
