// Packet Bandit Lab — TryHackMe Writeups Screen (Enhanced)

const WRITEUPS = [
  {
    id: 'mrrobot', title: 'Mr Robot', category: 'Privilege Escalation', badgeColor: 'purple',
    theme: { c1: '#ef4444', c2: '#22c55e', label: 'fsociety' },
    difficulty: 2, cats: ['web','privesc'],
    desc: 'Chain robots.txt disclosure, WordPress username enumeration, Hydra brute forcing, theme editor RCE, MD5 cracking, and SUID nmap 3.81 to recover all three keys.',
    tags: ['robots.txt','WordPress','hydra','MD5','SUID nmap'],
    file: 'mrrobot.html',
  },
  {
    id: 'wonderland', title: 'Wonderland', category: 'Privilege Escalation', badgeColor: 'purple',
    theme: { c1: '#ec4899', c2: '#8b5cf6', label: 'rabbit-hole' },
    difficulty: 2, cats: ['web','privesc'],
    desc: 'Follow the white rabbit into hidden SSH credentials, then chain Python library hijacking, PATH hijacking, and Perl cap_setuid capabilities to reach root.',
    tags: ['nmap','curl','SSH','Python hijack','capabilities'],
    file: 'wonderland.md',
  },
  {
    id: 'tomghost', title: 'Tomghost', category: 'Privilege Escalation', badgeColor: 'purple',
    theme: { c1: '#22d3ee', c2: '#f97316', label: 'ghostcat' },
    difficulty: 1, cats: ['web','privesc'],
    desc: 'Exploit exposed AJP on Tomcat 9.0.30 to read WEB-INF/web.xml, pivot through leaked SSH credentials, then turn sudo zip into a root command execution primitive.',
    tags: ['AJP','Tomcat','Ghostcat','SSH','sudo zip'],
    file: 'tomghost.md',
  },
  {
    id: 'takeover', title: 'TakeOver', category: 'Subdomain Takeover', badgeColor: 'orange',
    theme: { c1: '#f97316', c2: '#fbbf24', label: 'recon' },
    difficulty: 1, cats: ['subdomain-takeover','web'],
    desc: 'Discover a forgotten support subdomain via TLS certificate SAN inspection, identify a dangling AWS S3 endpoint, and validate a subdomain takeover condition.',
    tags: ['ffuf','openssl','AWS S3','recon'],
    file: 'takeover.md',
  },
  {
    id: 'compiled', title: 'Compiled', category: 'Reverse Engineering', badgeColor: 'orange',
    theme: { c1: '#f59e0b', c2: '#10b981', label: 'binary' },
    difficulty: 1, cats: ['reverse-engineering'],
    desc: 'Recover the correct password from an ELF binary using static analysis — tracing scanf format strings, strcmp control flow, and .rodata resolution without ever running the binary.',
    tags: ['objdump','strings','nm','ELF','x86-64'],
    file: 'compiled.md',
  },
  {
    id: 'wgel', title: 'Wgel', category: 'Privilege Escalation', badgeColor: 'purple',
    theme: { c1: '#a855f7', c2: '#38bdf8', label: 'exfil' },
    difficulty: 1, cats: ['web','privesc'],
    desc: 'Enumerate a deceptive default web page to uncover an exposed SSH private key, then abuse a privileged wget binary to exfiltrate root-owned files via HTTP POST body injection.',
    tags: ['nmap','gobuster','SSH keys','sudo','wget'],
    file: 'wgel.md',
  },
  {
    id: 'epoch', title: 'Epoch', category: 'Web', badgeColor: 'blue',
    theme: { c1: '#38bdf8', c2: '#818cf8', label: 'unix-time' },
    difficulty: 1, cats: ['web'],
    desc: 'Exploit an unsanitized epoch-to-UTC converter to achieve OS command injection via bash -c, read server-side Go source to confirm the vulnerability, and extract the flag.',
    tags: ['curl','command injection','bash','Go'],
    file: 'epoch.md',
  },
  {
    id: 'nmap', title: 'Intermediate Nmap', category: 'Network', badgeColor: 'green',
    theme: { c1: '#4ade80', c2: '#38bdf8', label: 'scan' },
    difficulty: 2, cats: ['network'],
    desc: 'Perform full-port scanning with service detection to discover credentials leaked on a non-standard high port, pivot into SSH access, and locate a world-readable flag.',
    tags: ['nmap','ssh','sshpass','banner analysis'],
    file: 'nmap.md',
  },
  {
    id: 'passcode', title: 'PassCode', category: 'Web3', badgeColor: 'web3',
    theme: { c1: '#a78bfa', c2: '#22d3ee', label: 'evm' },
    difficulty: 1, cats: ['web3'],
    desc: "Read a deployed smart contract's supposedly private storage slots directly via eth_getStorageAt JSON-RPC — no wallet, no transaction — and recover the flag.",
    tags: ['curl','JSON-RPC','Solidity','EVM storage'],
    file: 'passcode.md',
  },
  {
    id: 'neighbour', title: 'Neighbour', category: 'Web', badgeColor: 'blue',
    theme: { c1: '#38bdf8', c2: '#f472b6', label: 'idor' },
    difficulty: 1, cats: ['web'],
    desc: 'Use guest credentials leaked in source, trace the post-login redirect to profile.php?user=guest, then exploit an IDOR by swapping the user reference to admin.',
    tags: ['curl','IDOR','PHP sessions','broken access control'],
    file: 'neighbour.md',
  },
  {
    id: 'anonymous', title: 'Anonymous', category: 'Privilege Escalation', badgeColor: 'purple',
    theme: { c1: '#00ff5a', c2: '#dc2626', label: 'mask' },
    difficulty: 2, cats: ['privesc','network'],
    desc: 'Enumerate SMB and anonymous FTP shares to discover a writable cron-driven script, hijack it for a reverse shell as user, then escalate to root via a SUID env binary.',
    tags: ['nmap','smbclient','FTP','cron','SUID env'],
    file: 'anonymous.md',
  },
  {
    id: 'gamingserver', title: 'GamingServer', category: 'Privilege Escalation', badgeColor: 'purple',
    theme: { c1: '#8b5cf6', c2: '#22d3ee', label: 'arcade' },
    difficulty: 1, cats: ['privesc','web'],
    desc: 'Enumerate a hidden web directory to recover an encrypted SSH RSA key, crack the weak passphrase with john, then escalate to root via a privileged LXD container that mounts the host filesystem.',
    tags: ['gobuster','ssh2john','john','LXD','privesc'],
    file: 'gamingserver.md',
  },
  {
    id: 'ignite', title: 'Ignite', category: 'Web Application', badgeColor: 'orange',
    theme: { c1: '#f97316', c2: '#fbbf24', label: 'fuel-cms' },
    difficulty: 1, cats: ['web','privesc'],
    desc: 'Exploit an unpatched Fuel CMS 1.4 installation via CVE-2018-16763 filter-parameter injection for RCE as www-data, extract hardcoded database credentials, and escalate to root through password reuse and a pty.fork() TTY bypass.',
    tags: ['nmap','CVE-2018-16763','Fuel CMS','RCE','credential reuse','pty.fork()'],
    file: 'ignite.html',
  },
  {
    id: 'chatbot', title: 'Chatbot', category: 'AI / API Enumeration', badgeColor: 'blue',
    theme: { c1: '#22d3ee', c2: '#a855f7', label: 'evil-gpt' },
    difficulty: 1, cats: ['ai','web'],
    desc: "Bypass the Evil-GPT v2 chatbot by ignoring it entirely — a full TCP scan exposes Ollama's default API on port 11434, then /api/tags and /api/show leak the model's system prompt with the flag baked in. No prompt injection required.",
    tags: ['nmap','Ollama','/api/show','LLM','prompt leak'],
    file: 'chatbot.html',
  },
  {
    id: 'lofi', title: 'Lo-Fi', category: 'LFI / Path Traversal', badgeColor: 'blue',
    theme: { c1: '#f1e6cf', c2: '#a78bfa', label: 'traverse' },
    difficulty: 1, cats: ['web'],
    desc: "The site loads each 'album' through ?page= and hands it straight to PHP include(). Swap the filename for ../../../../../flag.txt and the filesystem itself plays back the flag — no auth, no fuzzing, just dots and slashes past a depth-shallow blacklist.",
    tags: ['LFI','path traversal','PHP include','blacklist bypass','curl'],
    file: 'lofi.html',
  },
  {
    id: 'bricks', title: 'Bricks Heist', category: 'Web / Forensics', badgeColor: 'orange',
    theme: { c1: '#dc2626', c2: '#f59e0b', label: 'bricks-heist' },
    difficulty: 1, cats: ['web'],
    desc: "CVE-2024-25600 turns a leaked WordPress nonce into PHP eval(). Post-exploitation chases a miner masquerading as nm-inet-dialog, a hex-then-base64-twice wallet in inet.conf, and a LockBit-attributed Bitcoin trail. Written as a failure audit — the clean path is short; mine wasn't.",
    tags: ['CVE-2024-25600','WordPress','Bricks Builder','RCE','miner forensics','OFAC','LockBit'],
    file: 'bricks.html',
  },
  {
    id: 'vectara', title: 'Vectara', category: 'AI Security', badgeColor: 'blue',
    theme: { c1: '#22d3ee', c2: '#a855f7', label: 'oracle-9' },
    difficulty: 1, cats: ['ai','web'],
    desc: "Eight-task AI Odyssey CTF aboard the EPOCH-1 generation ship: prompt-inject a relay satellite, poison a model registry, cross-reference corrupted nav data, exploit an agentic medical-bay assistant, and finish with stored XSS + cookie clobbering against a bot-reviewed workflow. Maps cleanly to the OWASP LLM Top 10.",
    tags: ['prompt injection','OWASP LLM Top 10','AI supply chain','data poisoning','agentic AI','stored XSS','cookie clobbering'],
    file: 'vectara.html',
  },
];

const FILTERS = [
  { label: 'all', value: 'all' },
  { label: 'web', value: 'web' },
  { label: 'privesc', value: 'privesc' },
  { label: 'network', value: 'network' },
  { label: 'rev-eng', value: 'reverse-engineering' },
  { label: 'web3', value: 'web3' },
  { label: 'ai', value: 'ai' },
  { label: 'subdomain', value: 'subdomain-takeover' },
];

// Helper — hex to rgb tuple string for rgba(). Accepts #rgb, #rrggbb, #rrggbbaa.
// Falls back to orange (249,115,22) if the input isn't a recognizable hex.
const FALLBACK_RGB = '249,115,22';
const hexRgb = (hex) => {
  if (typeof hex !== 'string') return FALLBACK_RGB;
  let h = hex.trim().replace(/^#/, '');
  if (h.length === 3 || h.length === 4) h = h.split('').slice(0, 3).map(c => c + c).join('');
  if (h.length !== 6 && h.length !== 8) return FALLBACK_RGB;
  if (!/^[0-9a-fA-F]{6,8}$/.test(h)) return FALLBACK_RGB;
  const r = parseInt(h.slice(0, 2), 16);
  const g = parseInt(h.slice(2, 4), 16);
  const b = parseInt(h.slice(4, 6), 16);
  return `${r},${g},${b}`;
};

const DiffDots = ({ level, accent = '#f97316' }) => {
  const accentRgb = hexRgb(accent);
  return (
    <div style={{ display: 'flex', gap: 3, alignItems: 'center' }}>
      <span style={{ fontFamily: 'ui-monospace,monospace', fontSize: '0.6rem', color: '#6b7280', marginRight: 4, letterSpacing: '0.08em', textTransform: 'uppercase' }}>diff</span>
      {[1,2,3].map(i => (
        <span key={i} style={{
          width: 7, height: 7, borderRadius: '50%',
          background: i <= level ? accent : 'rgba(148,163,184,0.2)',
          boxShadow: i <= level ? `0 0 6px rgba(${accentRgb},0.67)` : 'none',
          display: 'inline-block', transition: 'all 0.2s',
        }} />
      ))}
    </div>
  );
};

const WriteupCard = ({ writeup, index }) => {
  const [hovered, setHovered] = React.useState(false);
  const { ref, visible } = useScrollReveal(0.1);

  const navigate = () => { window.location.href = `/writeups/${writeup.id}.html`; };

  const theme = writeup.theme || { c1: '#f97316', c2: '#38bdf8', label: 'room' };
  const c1 = theme.c1, c2 = theme.c2;
  const c1rgb = hexRgb(c1), c2rgb = hexRgb(c2);

  return (
    <div ref={ref}
      role="button"
      tabIndex={0}
      onClick={navigate}
      onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); navigate(); } }}
      onMouseEnter={() => setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      style={{
        opacity: visible ? 1 : 0,
        transform: visible ? (hovered ? 'translateY(-4px)' : 'translateY(0)') : 'translateY(28px)',
        background: hovered
          ? `linear-gradient(135deg, rgba(${c1rgb},0.08), rgba(${c2rgb},0.04) 60%, rgba(15,23,42,0.95))`
          : 'rgba(2,6,23,0.85)',
        border: `1px solid ${hovered ? `rgba(${c1rgb},0.42)` : 'rgba(148,163,184,0.14)'}`,
        borderRadius: '0.85rem',
        overflow: 'hidden',
        cursor: 'pointer',
        boxShadow: hovered
          ? `0 18px 50px rgba(0,0,0,0.55), 0 0 0 1px rgba(${c1rgb},0.18), 0 0 32px rgba(${c1rgb},0.18)`
          : '0 6px 22px rgba(0,0,0,0.35)',
        backdropFilter: 'blur(16px)',
        WebkitBackdropFilter: 'blur(16px)',
        transition: `opacity 0.7s cubic-bezier(0.16,1,0.3,1) ${index * 0.06}s, transform 0.4s cubic-bezier(0.16,1,0.3,1), background 0.25s, border-color 0.25s, box-shadow 0.25s`,
        display: 'flex', flexDirection: 'column',
        position: 'relative',
      }}>

      {/* Top accent strip (theme gradient) */}
      <div style={{
        height: '2px',
        background: `linear-gradient(90deg, ${c1}, ${c2})`,
        opacity: hovered ? 1 : 0.55,
        transition: 'opacity 0.25s',
      }} />

      {/* Theme sigil — top right corner glow */}
      <div style={{
        position: 'absolute', top: 0, right: 0, width: 120, height: 120,
        background: `radial-gradient(circle at top right, rgba(${c1rgb},0.18), transparent 60%)`,
        opacity: hovered ? 1 : 0.5,
        transition: 'opacity 0.3s',
        pointerEvents: 'none',
      }} />

      {/* Terminal chrome bar */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: '0.4rem',
        padding: '0.5rem 0.9rem',
        background: hovered ? `rgba(${c1rgb},0.08)` : 'rgba(15,23,42,0.75)',
        borderBottom: `1px solid ${hovered ? `rgba(${c1rgb},0.22)` : 'rgba(148,163,184,0.08)'}`,
        transition: 'all 0.25s',
        position: 'relative', zIndex: 1,
      }}>
        <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#ef4444', opacity: 0.75 }} />
        <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#f59e0b', opacity: 0.75 }} />
        <span style={{ width: 8, height: 8, borderRadius: '50%', background: '#4ade80', opacity: 0.75 }} />
        <span style={{ flex: 1 }} />
        <span style={{
          fontFamily: 'ui-monospace,monospace', fontSize: '0.62rem',
          color: c1, opacity: hovered ? 0.95 : 0.6,
          letterSpacing: '0.16em', textTransform: 'uppercase',
          transition: 'opacity 0.25s',
        }}>
          ◇ {theme.label}
        </span>
      </div>

      <div style={{ padding: '1.05rem 1.1rem 1rem', display: 'flex', flexDirection: 'column', gap: '0.75rem', flex: 1, position: 'relative', zIndex: 1 }}>

        {/* Prompt + title */}
        <div>
          <div style={{ fontFamily: 'ui-monospace,monospace', fontSize: '0.66rem', color: '#6b7280', marginBottom: '0.3rem' }}>
            <span style={{ color: '#4ade80' }}>root@thm</span>
            <span style={{ color: '#9ca3af' }}>:</span>
            <span style={{ color: '#38bdf8' }}>~/rooms</span>
            <span style={{ color: '#9ca3af' }}>$</span>
            <span style={{ color: '#e5e7eb' }}> cat {writeup.file}</span>
          </div>
          <div style={{
            fontSize: '1.12rem', fontWeight: 700,
            backgroundImage: hovered
              ? `linear-gradient(135deg, ${c1}, ${c2})`
              : 'linear-gradient(135deg, #e5e7eb, #e5e7eb)',
            WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text',
            lineHeight: 1.25,
            letterSpacing: '-0.01em',
          }}>
            {writeup.title}
          </div>
          <div style={{
            display: 'inline-flex', alignItems: 'center', gap: '0.35rem',
            marginTop: '0.4rem',
            fontFamily: 'ui-monospace,monospace', fontSize: '0.62rem',
            color: c1, opacity: 0.85,
            padding: '0.15rem 0.5rem',
            background: `rgba(${c1rgb},0.08)`,
            border: `1px solid rgba(${c1rgb},0.22)`,
            borderRadius: '4px',
            letterSpacing: '0.1em', textTransform: 'uppercase',
          }}>
            {writeup.category}
          </div>
        </div>

        {/* Description */}
        <div style={{ fontSize: '0.85rem', color: '#94a3b8', lineHeight: 1.6, flex: 1 }}>
          {writeup.desc}
        </div>

        {/* Footer */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          paddingTop: '0.65rem', borderTop: '1px solid rgba(148,163,184,0.08)',
          gap: '0.5rem',
        }}>
          <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
            {writeup.tags.slice(0,3).map(t => <Tag key={t}>{t}</Tag>)}
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexShrink: 0 }}>
            <DiffDots level={writeup.difficulty} accent={c1} />
            <span style={{
              color: hovered ? c1 : '#6b7280', fontSize: '1.15rem',
              transition: 'color 0.2s, transform 0.2s',
              transform: hovered ? 'translateX(4px)' : 'none', display: 'inline-block',
              textShadow: hovered ? `0 0 8px ${c1}aa` : 'none',
            }}>→</span>
          </div>
        </div>
      </div>

      {/* Hover glow bar at bottom — theme gradient */}
      <div style={{
        height: '2px',
        background: `linear-gradient(90deg, ${c1}, ${c2})`,
        transform: hovered ? 'scaleX(1)' : 'scaleX(0)',
        transformOrigin: 'left',
        transition: 'transform 0.35s cubic-bezier(0.16,1,0.3,1)',
      }} />
    </div>
  );
};

// Per-room display code shown inside each room-node diamond.
const ROOM_CODES = {
  mrrobot: 'MR', wonderland: 'WL', tomghost: 'TG', takeover: 'TO',
  compiled: 'ELF', chatbot: 'AI', wgel: 'WG', epoch: 'EP',
  passcode: 'EVM', neighbour: 'IDR', nmap: 'NM', anonymous: 'AN',
  gamingserver: 'GS', ignite: 'IG', bricks: 'BH',
  lofi: 'LFI', vectara: 'VEC',
};

// Four sectors that partition the room-map stage into meaningful quadrants.
// Each room is assigned to a sector based on its primary category (cats[0]),
// then positioned inside the sector's bounds. Sector labels on the map are
// clickable and filter the visible rooms to the sector's categories.
const SECTORS = [
  {
    id: 'entry',
    label: 'entry deck',
    blurb: 'Recon — port sweeps, name resolution, dangling subdomains.',
    color: '#4ade80',
    rgb: '74,222,128',
    cats: ['network', 'subdomain-takeover'],
    bounds: { x: [12, 36], y: [22, 40] },
    labelPos: { left: '4%', top: '4%' },
  },
  {
    id: 'exploit',
    label: 'exploit wing',
    blurb: 'Application bugs — RCE, IDOR, command injection, prompt leaks.',
    color: '#38bdf8',
    rgb: '56,189,248',
    cats: ['web', 'ai'],
    bounds: { x: [54, 92], y: [22, 42] },
    labelPos: { right: '4%', top: '4%' },
  },
  {
    id: 'lab',
    label: 'lab bench',
    blurb: 'Static analysis — ELF reversing, contract storage, model APIs.',
    color: '#f59e0b',
    rgb: '245,158,11',
    cats: ['reverse-engineering', 'web3'],
    bounds: { x: [12, 36], y: [60, 80] },
    labelPos: { left: '4%', bottom: '4%' },
  },
  {
    id: 'root',
    label: 'loot vault',
    blurb: 'Privilege escalation — SUID, capabilities, container escapes.',
    color: '#a855f7',
    rgb: '168,85,247',
    cats: ['privesc'],
    bounds: { x: [54, 92], y: [60, 80] },
    labelPos: { right: '4%', bottom: '4%' },
  },
];

// Primary-cat → sector lookup.
const sectorForRoom = (room) =>
  SECTORS.find(s => s.cats.includes(room.cats[0])) || SECTORS[1];

// Pack each sector's rooms into a roughly square grid inside its bounds.
const buildRoomLayout = (writeups) => {
  const layout = {};
  const grouped = {};
  for (const w of writeups) {
    const s = sectorForRoom(w);
    (grouped[s.id] = grouped[s.id] || []).push(w);
  }
  for (const sector of SECTORS) {
    const rooms = grouped[sector.id] || [];
    const cols = Math.max(1, Math.ceil(Math.sqrt(rooms.length)));
    const rows = Math.max(1, Math.ceil(rooms.length / cols));
    const [xMin, xMax] = sector.bounds.x;
    const [yMin, yMax] = sector.bounds.y;
    const colStep = cols > 1 ? (xMax - xMin) / (cols - 1) : 0;
    const rowStep = rows > 1 ? (yMax - yMin) / (rows - 1) : 0;
    rooms.forEach((room, i) => {
      const col = i % cols;
      const row = Math.floor(i / cols);
      layout[room.id] = {
        x: cols > 1 ? xMin + col * colStep : (xMin + xMax) / 2,
        y: rows > 1 ? yMin + row * rowStep : (yMin + yMax) / 2,
        code: ROOM_CODES[room.id] || room.title.slice(0, 2).toUpperCase(),
        sectorId: sector.id,
      };
    });
  }
  return layout;
};

// Match a room against the current filter. Filter is either 'all', a sector
// id prefixed with 'sector:', or a single category value.
const roomMatchesFilter = (room, filter) => {
  if (filter === 'all') return true;
  if (filter.startsWith('sector:')) {
    const sector = SECTORS.find(s => s.id === filter.slice(7));
    return sector ? sector.cats.some(c => room.cats.includes(c)) : false;
  }
  return room.cats.includes(filter);
};

const SECTOR_META = {
  web: { label: 'web', color: '#38bdf8' },
  privesc: { label: 'privesc', color: '#a855f7' },
  network: { label: 'network', color: '#4ade80' },
  'reverse-engineering': { label: 'rev', color: '#f59e0b' },
  web3: { label: 'web3', color: '#a78bfa' },
  ai: { label: 'ai', color: '#22d3ee' },
  'subdomain-takeover': { label: 'subdomain', color: '#f97316' },
};

const RoomMap = ({ writeups, filter, setFilter }) => {
  const layout = React.useMemo(() => buildRoomLayout(writeups), [writeups]);
  const rooms = writeups.map(w => ({ ...w, ...layout[w.id] }));
  const visibleRooms = rooms.filter(room => roomMatchesFilter(room, filter));

  const [selectedId, setSelectedId] = React.useState(rooms[0]?.id);
  const [scanning, setScanning] = React.useState(false);
  const scanTimer = React.useRef(null);

  React.useEffect(() => {
    if (!visibleRooms.length) return;
    if (!visibleRooms.some(room => room.id === selectedId)) {
      setSelectedId(visibleRooms[0].id);
    }
  }, [filter]);

  React.useEffect(() => () => clearInterval(scanTimer.current), []);

  const selected = rooms.find(room => room.id === selectedId) || rooms[0];

  // Per-sector chain: connect rooms within each sector in the order they
  // appear in WRITEUPS. Produces 4 visual clusters instead of one arbitrary
  // chain through the whole array.
  const sectorChains = React.useMemo(() => {
    const out = [];
    for (const sector of SECTORS) {
      const inSector = rooms.filter(r => r.sectorId === sector.id);
      for (let i = 0; i < inSector.length - 1; i++) {
        out.push({ from: inSector[i], to: inSector[i + 1], color: sector.color });
      }
    }
    return out;
  }, [rooms]);

  // Highlighted "related rooms" lines for the selected node — pick up to 5
  // rooms with overlapping cats so the user sees who's in the same family.
  const relatedRooms = selected
    ? rooms.filter(room => room.id !== selected.id && room.cats.some(cat => selected.cats.includes(cat))).slice(0, 5)
    : [];

  const startScan = () => {
    const scanRooms = visibleRooms.length ? visibleRooms : rooms;
    if (!scanRooms.length) return;
    clearInterval(scanTimer.current);
    let i = 0;
    setScanning(true);
    setSelectedId(scanRooms[0].id);
    scanTimer.current = setInterval(() => {
      i += 1;
      if (i >= scanRooms.length) {
        clearInterval(scanTimer.current);
        scanTimer.current = null;
        setScanning(false);
        return;
      }
      setSelectedId(scanRooms[i].id);
    }, 320);
  };

  const openSelected = () => {
    if (selected) window.location.href = `/writeups/${selected.id}.html`;
  };

  const sectorCounts = Object.keys(SECTOR_META).map(value => ({
    value,
    ...SECTOR_META[value],
    count: writeups.filter(room => room.cats.includes(value)).length,
    rgb: hexRgb(SECTOR_META[value].color),
  }));

  const sectorCountsById = React.useMemo(() => {
    const out = {};
    for (const sector of SECTORS) {
      out[sector.id] = writeups.filter(w => sector.cats.some(c => w.cats.includes(c))).length;
    }
    return out;
  }, [writeups]);

  return (
    <section className="writeup-room-map" aria-label="Writeup room map">
      <div className="room-map-stage">
        {SECTORS.map(sector => {
          const sectorFilter = `sector:${sector.id}`;
          const isActive = filter === sectorFilter;
          return (
            <button
              key={sector.id}
              type="button"
              className={`room-sector-label ${isActive ? 'is-active' : ''}`}
              style={{
                ...sector.labelPos,
                '--sector-color': sector.color,
                '--sector-rgb': sector.rgb,
              }}
              onClick={() => setFilter(isActive ? 'all' : sectorFilter)}
              aria-pressed={isActive}
              aria-label={`Filter to ${sector.label}: ${sector.blurb}`}
              title={sector.blurb}
            >
              <span className="room-sector-label__count">{sectorCountsById[sector.id]}</span>
              <span className="room-sector-label__text">{sector.label}</span>
            </button>
          );
        })}

        <svg className="room-map-svg" viewBox="0 0 100 100" aria-hidden="true" focusable="false">
          {sectorChains.map(({ from, to, color }) => (
            <path
              key={`${from.id}-${to.id}`}
              className="room-map-link"
              style={{ stroke: `rgba(${hexRgb(color)},0.22)` }}
              d={`M ${from.x} ${from.y} C ${(from.x + to.x) / 2} ${from.y}, ${(from.x + to.x) / 2} ${to.y}, ${to.x} ${to.y}`}
            />
          ))}
          {selected && relatedRooms.map((room) => (
            <path
              key={`selected-${room.id}`}
              className="room-map-link is-active"
              style={{ '--room-color': selected.theme.c1 }}
              d={`M ${selected.x} ${selected.y} C ${(selected.x + room.x) / 2} ${selected.y - 14}, ${(selected.x + room.x) / 2} ${room.y + 14}, ${room.x} ${room.y}`}
            />
          ))}
        </svg>

        {rooms.map((room) => {
          const matches = roomMatchesFilter(room, filter);
          const isSelected = selected?.id === room.id;
          return (
            <button
              key={room.id}
              type="button"
              className={`room-node ${isSelected ? 'is-selected' : ''} ${matches ? '' : 'is-dim'} ${scanning && isSelected ? 'is-scanning' : ''}`}
              style={{
                '--room-x': `${room.x}%`,
                '--room-y': `${room.y}%`,
                '--room-color': room.theme.c1,
                '--room-rgb': hexRgb(room.theme.c1),
              }}
              disabled={!matches}
              onClick={() => setSelectedId(room.id)}
              aria-label={`${room.title} writeup`}
            >
              <span>{room.code}</span>
            </button>
          );
        })}
      </div>

      <aside className="room-map-panel">
        <div className="room-map-panel-card">
          <SectionHeading accent>Room map</SectionHeading>
          <h2 className="room-map-title" style={{ color: selected?.theme.c1 }}>{selected?.title}</h2>
          <div className="room-map-meta">
            {selected?.category}
          </div>
          <div style={{ marginTop: '0.7rem', display: 'flex', gap: 5, flexWrap: 'wrap' }}>
            {selected?.tags.slice(0, 4).map(tag => <Tag key={tag}>{tag}</Tag>)}
          </div>
          <div style={{ marginTop: '0.85rem' }}>
            <DiffDots level={selected?.difficulty || 1} accent={selected?.theme.c1 || '#f97316'} />
          </div>
          <p className="room-map-meta">
            {selected?.desc}
          </p>
          <div className="room-map-actions">
            <button className="room-map-button primary" onClick={openSelected}>Open room</button>
            <button className="room-map-button" onClick={startScan}>{scanning ? 'Scanning' : 'Scan sector'}</button>
          </div>
        </div>

        <div className="room-map-panel-card">
          <SectionHeading>Categories</SectionHeading>
          <div className="room-sector-grid">
            <button
              className={`room-sector ${filter === 'all' ? 'is-active' : ''}`}
              onClick={() => setFilter('all')}
              style={{ '--sector-color': '#f97316', '--sector-rgb': '249,115,22' }}
            >
              <span>all</span><span>{writeups.length}</span>
            </button>
            {sectorCounts.map(sector => (
              <button
                key={sector.value}
                className={`room-sector ${filter === sector.value ? 'is-active' : ''}`}
                onClick={() => setFilter(sector.value)}
                style={{ '--sector-color': sector.color, '--sector-rgb': sector.rgb }}
              >
                <span>{sector.label}</span><span>{sector.count}</span>
              </button>
            ))}
          </div>
        </div>
      </aside>
    </section>
  );
};

const WriteupsScreen = ({ onNavigate }) => {
  const [filter, setFilter] = React.useState('all');
  const [heroVisible, setHeroVisible] = React.useState(false);

  React.useEffect(() => {
    const t = setTimeout(() => setHeroVisible(true), 60);
    return () => clearTimeout(t);
  }, []);

  const visible = WRITEUPS.filter(w => roomMatchesFilter(w, filter));

  const filterLabel = filter.startsWith('sector:')
    ? (SECTORS.find(s => s.id === filter.slice(7))?.label.replace(/\s+/g, '-') || filter)
    : filter;
  const { displayed: cmdLine } = useTypewriter(`ls ~/rooms --filter=${filterLabel} --sort=date`, 28, 300);

  // Category counts for the stats strip
  const counts = {
    web: WRITEUPS.filter(w => w.cats.includes('web')).length,
    privesc: WRITEUPS.filter(w => w.cats.includes('privesc')).length,
    network: WRITEUPS.filter(w => w.cats.includes('network')).length,
    web3: WRITEUPS.filter(w => w.cats.includes('web3')).length,
  };

  return (
    <div style={{ position: 'relative', zIndex: 1, padding: '3rem 0 5rem' }}>

      {/* Back */}
      <a href="#" onClick={e => { e.preventDefault(); onNavigate('home'); }}
        style={{
          display: 'inline-flex', alignItems: 'center', gap: '0.4rem',
          color: '#38bdf8', fontSize: '0.78rem',
          fontFamily: 'ui-monospace,monospace',
          padding: '0.32rem 0.75rem', border: '1px solid rgba(56,189,248,0.2)',
          borderRadius: '4px', textDecoration: 'none', marginBottom: '2.5rem',
          background: 'rgba(56,189,248,0.05)',
          transition: 'all 0.15s',
        }}>
        ← cd ..
      </a>

      {/* Hero */}
      <div style={{
        opacity: heroVisible ? 1 : 0,
        transform: heroVisible ? 'translateY(0)' : 'translateY(20px)',
        transition: 'all 0.8s cubic-bezier(0.16,1,0.3,1)',
        marginBottom: '2.5rem',
        position: 'relative',
      }}>
        {/* Decorative atomic sigil — top right behind title */}
        <div aria-hidden="true" style={{
          position: 'absolute', top: '-1rem', right: '-1rem',
          width: 180, height: 180, opacity: 0.18, pointerEvents: 'none',
        }}>
          <svg viewBox="0 0 200 200" style={{ width: '100%', height: '100%' }}>
            <ellipse cx="100" cy="100" rx="88" ry="32" fill="none" stroke="#f97316" strokeWidth="0.8" transform="rotate(0 100 100)" />
            <ellipse cx="100" cy="100" rx="88" ry="32" fill="none" stroke="#38bdf8" strokeWidth="0.8" transform="rotate(60 100 100)" />
            <ellipse cx="100" cy="100" rx="88" ry="32" fill="none" stroke="#8b5cf6" strokeWidth="0.8" transform="rotate(120 100 100)" />
            <circle cx="100" cy="100" r="5" fill="#f97316" opacity="0.7" />
          </svg>
        </div>

        <Badge color="purple" style={{ marginBottom: '1rem', display: 'inline-block' }}>TryHackMe</Badge>
        <h1 style={{
          fontSize: 'clamp(2.2rem,5.5vw,3.9rem)', fontWeight: 800, lineHeight: 1.05,
          background: 'linear-gradient(135deg, #f97316 0%, #ec4899 30%, #38bdf8 65%, #8b5cf6 100%)',
          WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text',
          margin: '0.75rem 0 1.15rem',
          letterSpacing: '-0.025em',
        }}>
          Challenge Writeups
        </h1>
        <p style={{ color: '#94a3b8', fontSize: '1.02rem', maxWidth: 580, margin: '0 0 1.5rem', lineHeight: 1.7 }}>
          Documented solutions, methodology breakdowns, and lessons learned from TryHackMe rooms —
          each one themed to the personality of the room, focused on understanding the <em>why</em> behind each technique.
        </p>
        <div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
          <BtnPrimary onClick={() => { window.location.href = '/tryhackme-learning.html'; }}>Browse learning rooms →</BtnPrimary>
          <BtnSecondary onClick={() => onNavigate('home')}>← Home</BtnSecondary>
        </div>
      </div>

      {/* Stats strip */}
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: '0.5rem',
        marginBottom: '1.5rem',
      }}>
        {[
          { n: WRITEUPS.length, label: 'rooms', c: '#f97316' },
          { n: counts.web, label: 'web', c: '#38bdf8' },
          { n: counts.privesc, label: 'privesc', c: '#a855f7' },
          { n: counts.network, label: 'network', c: '#4ade80' },
          { n: counts.web3, label: 'web3', c: '#a78bfa' },
        ].map(s => (
          <div key={s.label} style={{
            padding: '0.55rem 0.7rem',
            background: 'rgba(2,6,23,0.7)',
            border: '1px solid rgba(148,163,184,0.1)',
            borderRadius: '0.5rem',
            display: 'flex', alignItems: 'baseline', gap: '0.45rem',
            position: 'relative', overflow: 'hidden',
          }}>
            <div style={{
              position: 'absolute', inset: 'auto 0 0 0', height: '1px',
              background: `linear-gradient(90deg, transparent, ${s.c}66, transparent)`,
            }} />
            <span style={{
              fontFamily: 'ui-monospace,monospace', fontSize: '1.2rem', fontWeight: 800,
              color: s.c, fontVariantNumeric: 'tabular-nums',
              textShadow: `0 0 10px ${s.c}55`,
            }}>{s.n}</span>
            <span style={{
              fontFamily: 'ui-monospace,monospace', fontSize: '0.66rem',
              color: '#6b7280', letterSpacing: '0.12em', textTransform: 'uppercase',
            }}>{s.label}</span>
          </div>
        ))}
      </div>

      <RoomMap writeups={WRITEUPS} filter={filter} setFilter={setFilter} />

      {/* Terminal command bar */}
      <div style={{
        fontFamily: 'ui-monospace,monospace', fontSize: '0.78rem',
        background: 'rgba(2,6,23,0.85)', border: '1px solid rgba(74,222,128,0.22)',
        borderRadius: '0.5rem', padding: '0.65rem 1rem',
        marginBottom: '1.5rem', backdropFilter: 'blur(8px)',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.5rem',
        boxShadow: 'inset 0 0 30px rgba(74,222,128,0.04)',
      }}>
        <div>
          <span style={{ color: '#4ade80' }}>$</span>{' '}
          <span style={{ color: '#e5e7eb' }}>{cmdLine}</span>
          <span style={{ color: '#f97316', animation: 'blink 1s step-end infinite' }}>█</span>
        </div>
        <div style={{ display: 'flex', gap: '1.25rem' }}>
          <span style={{ color: '#9ca3af' }}>
            <span style={{ color: '#4ade80' }}>●</span> {visible.length} results
          </span>
          <span style={{ color: '#6b7280' }}>{WRITEUPS.length} total</span>
        </div>
      </div>

      {/* Filter bar */}
      <div style={{ display: 'flex', gap: '0.4rem', marginBottom: '2rem', flexWrap: 'wrap', alignItems: 'center' }}>
        <span style={{ fontFamily: 'ui-monospace,monospace', fontSize: '0.66rem', color: '#6b7280', marginRight: '0.25rem', textTransform: 'uppercase', letterSpacing: '0.12em' }}>filter:</span>
        {FILTERS.map(f => (
          <button key={f.value} onClick={() => setFilter(f.value)} style={{
            fontFamily: 'ui-monospace,monospace',
            fontSize: '0.74rem', fontWeight: 600, padding: '0.28rem 0.7rem',
            borderRadius: '999px', border: '1px solid', cursor: 'pointer',
            background: filter === f.value
              ? 'linear-gradient(135deg, rgba(249,115,22,0.18), rgba(249,115,22,0.06))'
              : 'rgba(15,23,42,0.5)',
            borderColor: filter === f.value ? 'rgba(249,115,22,0.5)' : 'rgba(148,163,184,0.15)',
            color: filter === f.value ? '#f97316' : '#9ca3af',
            transition: 'all 0.15s',
            letterSpacing: '0.02em',
          }}>
            {filter === f.value ? '▸ ' : ''}{f.label}
          </button>
        ))}
      </div>

      <SectionRule label={`published (${visible.length})`} />

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(290px,1fr))', gap: '1.15rem' }}>
        {visible.map((w, i) => (
          <WriteupCard key={w.id} writeup={w} index={i} />
        ))}
        {visible.length === 0 && (
          <div style={{ gridColumn: '1/-1', textAlign: 'center', padding: '3rem', color: '#9ca3af',
            fontFamily: 'ui-monospace,monospace', fontSize: '0.85rem' }}>
            <span style={{ color: '#ef4444' }}>ERROR:</span> No writeups match filter <span style={{ color: '#f97316' }}>"{filter}"</span>
          </div>
        )}
        {/* Coming soon */}
        {filter === 'all' && (
          <div style={{
            background: 'rgba(2,6,23,0.5)', border: '1px dashed rgba(148,163,184,0.18)',
            borderRadius: '0.85rem', overflow: 'hidden', opacity: 0.5,
          }}>
            <div style={{ height: '2px', background: 'rgba(148,163,184,0.15)' }} />
            <div style={{
              display: 'flex', alignItems: 'center', gap: '0.4rem',
              padding: '0.5rem 0.9rem',
              background: 'rgba(15,23,42,0.6)',
              borderBottom: '1px dashed rgba(148,163,184,0.08)',
            }}>
              <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'rgba(148,163,184,0.3)' }} />
              <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'rgba(148,163,184,0.3)' }} />
              <span style={{ width: 8, height: 8, borderRadius: '50%', background: 'rgba(148,163,184,0.3)' }} />
              <span style={{ flex: 1 }} />
              <span style={{ fontFamily: 'ui-monospace,monospace', fontSize: '0.62rem', color: '#6b7280', letterSpacing: '0.12em', textTransform: 'uppercase' }}>◇ coming_soon</span>
            </div>
            <div style={{ padding: '1.05rem 1.1rem' }}>
              <div style={{ fontFamily: 'ui-monospace,monospace', fontSize: '0.66rem', color: '#4b5563', marginBottom: '0.5rem' }}>
                <span style={{ color: '#374151' }}>root@thm:~/rooms$</span> cat ???.md
              </div>
              <div style={{ fontSize: '1.08rem', fontWeight: 700, color: '#6b7280' }}>Next writeup...</div>
              <div style={{ fontSize: '0.83rem', color: '#4b5563', marginTop: '0.4rem' }}>More rooms and writeups will appear here as they are completed.</div>
            </div>
          </div>
        )}
      </div>

      <PageFooter />
    </div>
  );
};

Object.assign(window, { WriteupsScreen, WRITEUPS, FILTERS });
