// ── Fake terminal: click the Environment card's icon ──────────
(function () {
  const glyph = document.querySelector('.skill-card-icon .fa-terminal');
  if (!glyph) return;
  const icon = glyph.closest('.skill-card-icon');

  const HOST = 'gade.gg';
  const KERNEL = '6.6-goat';
  const T0 = performance.now();

  // Current user: su can change it (root's password leaked in auth.log),
  // exit pops back. Root passes every permission check and may write.
  const USERS = {
    root: ['root'],
    nikolaj: ['home', 'nikolaj'],
    guest: ['home', 'guest'],
    pepsi: ['home', 'pepsi'],
  };
  const PASSWORDS = { root: 'Pepsi2022' };
  let user = 'guest';
  let home = USERS.guest;
  const userStack = [];
  let pendingSu = null;   // user being su'd to while the password prompt is up

  // Filesystem: dirs are { children }, files are strings — or functions
  // returning a string, for live content (.bash_history). locked dirs
  // deny everyone but root and their owner. Only root may write.
  const dir = (children) => ({ children });
  const locked = (owner, children) => ({ children, locked: owner });
  const DENIED = { denied: true };   // a path tried to pass through a locked dir
  const isFile = (n) => typeof n === 'string' || typeof n === 'function';
  const fileBody = (n) => (typeof n === 'function' ? n() : n);
  const allowed = (n) => !n.locked || user === 'root' || user === n.locked;
  const GOAT = `                 %%%%%%%%%%%%%%
              %%%%%%%%%%%%%%%%%%%%
            %%%%          %%%%%%%%%%%
          %%%           %%%%%%%%%%%%%%%
        %%             %%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%
                    %%%%%%%%%%%%%%%%%%%%%%%
                      %%%%%%%%%%%%%%%%%%%%%%
                     %%%%%%%%%%%%%%%%%%%%%%%%%
                  %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
              %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
           %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%   %%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%          %%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%
`;
  const BIN = '\u007fELF��������';
  const FS = dir({
    bin: dir({ cat: BIN, echo: BIN, gsh: BIN, ls: BIN, nano: BIN, rm: BIN, sh: BIN }),
    etc: dir({
      hostname: 'gade.gg\n',
      hosts:
        '127.0.0.1  localhost\n' +
        '10.0.0.1   bulbasaur\n' +
        '10.0.0.6   charizard\n' +
        '10.0.0.29  nidoran-f\n' +
        '10.0.0.32  nidoran-m\n' +
        '10.0.0.58  growlithe\n' +
        '10.0.0.63  abra\n' +
        '10.0.0.64  kadabra\n' +
        '10.0.0.133 eevee\n' +
        '10.0.0.150 mewtwo\n' +
        '10.0.0.151 mew\n',
      motd: "Welcome to gade.gg.\nType 'help' for commands.\n",
      passwd:
        'root:x:0:0:root:/root:/bin/sh\n' +
        'nikolaj:x:1000:1000:Nikolaj Gade:/home/nikolaj:/bin/fish\n' +
        'guest:x:1001:1001:guest:/home/guest:/bin/gsh\n' +
        'pepsi:x:1002:1002:Pepsi Max Lime Gade:/home/pepsi:/usr/bin/meow\n',
    }),
    home: dir({
      guest: dir({
        'readme.txt': 'Nothing here yet. Have a look around.\n',
        '.bash_history': () => (history.length ? history.join('\n') + '\n' : ''),
        '.bashrc': "# ~/.bashrc\nPS1='\\u@\\h:\\w\\$ '\nalias sl=ls\n",
      }),
      nikolaj: locked('nikolaj', {
        'array-props.pdf': '%PDF-1.7\n%����\n',
      }),
      pepsi: dir({
        '.plan':
          'nap.\n' +
          'nap.\n' +
          'knock something off the table.\n' +
          'nap.\n',
      }),
    }),
    proc: dir({
      uptime: () => {
        const s = (performance.now() - T0) / 1000;
        return s.toFixed(2) + ' ' + (s * 4).toFixed(2) + '\n';
      },
      version:
        'Linux version ' + KERNEL + ' (rats@gade.gg) (gcc (GCC) 14.2.1) #1 SMP PREEMPT\n',
    }),
    root: locked('root', {
      goat: GOAT,
      '.bash_history': 'rm -rf /\n',
    }),
    usr: dir({
      bin: dir({ meow: BIN }),
      share: dir({
        centvrion: dir({ 'salve.cent': '// Hello, world\nDIC("SALVE MVNDE")\n' }),
      }),
    }),
    var: dir({
      log: dir({
        'auth.log':
          'Aug  9 03:12:44 gade sshd[812]: Failed password for root from 10.0.0.150 port 51434 ssh2\n' +
          'Aug  9 03:12:51 gade sshd[815]: Failed password for invalid user Pepsi2022 from 10.0.0.150 port 51438 ssh2\n' +
          'Aug  9 03:13:02 gade sshd[819]: Accepted password for root from 10.0.0.150 port 51442 ssh2\n',
      }),
    }),
  });

  let cwd = [...USERS.guest];

  function resolvePath(path) {
    if (path === '~') path = '/' + home.join('/');
    else if (path.startsWith('~/')) path = '/' + home.join('/') + path.slice(1);
    const segs = path.startsWith('/') ? [] : [...cwd];
    for (const part of path.split('/')) {
      if (part === '' || part === '.') continue;
      if (part === '..') segs.pop();
      else segs.push(part);
    }
    return segs;
  }

  function nodeAt(segs) {
    let node = FS;
    for (const s of segs) {
      if (!allowed(node)) return DENIED;
      if (!node.children || !Object.prototype.hasOwnProperty.call(node.children, s)) return null;
      node = node.children[s];
    }
    return node;
  }

  function displayPath(segs) {
    const p = '/' + segs.join('/');
    const h = '/' + home.join('/');
    if (p === h) return '~';
    if (p.startsWith(h + '/')) return '~' + p.slice(h.length);
    return p;
  }

  const promptText = () =>
    user + '@' + HOST + ':' + displayPath(cwd) + (user === 'root' ? '#' : '$');

  let dialog = null;
  let screen = null;
  let out = null;
  let input = null;
  let promptEl = null;
  let termTitle = null;

  function becomeUser(u) {
    user = u;
    home = USERS[u];
    // The quiet flag a future achievements system reads
    if (u === 'root') {
      try { localStorage.setItem('termRoot', '1'); } catch (e) {}
      if (window.achv) window.achv.unlock('hacker');
    }
    promptEl.textContent = promptText();
    termTitle.textContent = u + '@' + HOST;
  }

  // Real history: ↑ recalls it, ~/.bash_history reads it, localStorage
  // keeps it. Seeded so a first visit finds a previous guest's session.
  const HIST_SEED = ['ls', 'cd /', 'ls', 'cat etc/motd', 'cd'];
  const HIST_MAX = 200;
  let history = HIST_SEED;
  try { history = JSON.parse(localStorage.getItem('termHistory')) || HIST_SEED; } catch (e) {}
  if (!Array.isArray(history)) history = HIST_SEED;
  history = history.slice(-HIST_MAX).map(String);
  let hIdx = history.length;
  let draft = '';
  function saveHistory() {
    try { localStorage.setItem('termHistory', JSON.stringify(history)); } catch (e) {}
  }

  // parts: [{ text, cls? }] — one scrollback line
  function line(parts) {
    const el = document.createElement('div');
    el.className = 'term-line';
    for (const part of parts) {
      if (part.cls) {
        const span = document.createElement('span');
        span.className = part.cls;
        span.textContent = part.text;
        el.appendChild(span);
      } else {
        el.appendChild(document.createTextNode(part.text));
      }
    }
    out.appendChild(el);
  }
  const print = (text) => line([{ text }]);

  // hidden commands work but stay out of help and tab completion —
  // finding them is the point
  const ALIASES = { sl: 'ls' };   // honoring ~/.bashrc
  const COMMANDS = {
    cat: { usage: 'cat <file>', desc: 'print a file', run(args) {
      for (const t of args) {
        const node = nodeAt(resolvePath(t));
        if (node === null) print('cat: ' + t + ': No such file or directory');
        else if (node === DENIED) print('cat: ' + t + ': Permission denied');
        else if (isFile(node)) {
          const body = fileBody(node).replace(/\n$/, '');
          if (body) print(body);
        } else print('cat: ' + t + ': Is a directory');
      }
    } },
    cd: { usage: 'cd [dir]', desc: 'change directory', run(args) {
      const target = args[0] || '~';
      const segs = resolvePath(target);
      const node = nodeAt(segs);
      if (node === null) print('cd: no such file or directory: ' + target);
      else if (node === DENIED || !allowed(node)) print('cd: permission denied: ' + target);
      else if (isFile(node)) print('cd: not a directory: ' + target);
      else {
        cwd = segs;
        promptEl.textContent = promptText();
      }
    } },
    clear: { usage: 'clear', desc: 'clear the screen', run() { out.textContent = ''; } },
    echo: { usage: 'echo [text]', desc: 'print text', run(args) { print(args.join(' ')); } },
    exit: { usage: 'exit', desc: 'close the terminal', run() {
      if (userStack.length) becomeUser(userStack.pop());
      else dialog.close();
    } },
    help: { usage: 'help', desc: 'list commands', run() {
      const names = Object.keys(COMMANDS).filter((n) => !COMMANDS[n].hidden).sort();
      const pad = Math.max(...names.map((n) => COMMANDS[n].usage.length)) + 2;
      names.forEach((n) => print('  ' + COMMANDS[n].usage.padEnd(pad) + COMMANDS[n].desc));
    } },
    history: { hidden: true, usage: 'history', desc: '', run() {
      history.forEach((h, i) => print(String(i + 1).padStart(5) + '  ' + h));
    } },
    ls: { usage: 'ls [-a] [dir]', desc: 'list directory contents', run(args) {
      const all = args.some((a) => /^-[a-zA-Z]*a/.test(a));
      const paths = args.filter((a) => !a.startsWith('-'));
      const target = paths[0] || '.';
      const node = nodeAt(resolvePath(target));
      if (node === null) { print("ls: cannot access '" + target + "': No such file or directory"); return; }
      if (node === DENIED || !allowed(node)) { print("ls: cannot open directory '" + target + "': Permission denied"); return; }
      if (isFile(node)) { print(target); return; }
      let names = Object.keys(node.children).sort();
      names = all ? ['.', '..', ...names] : names.filter((n) => !n.startsWith('.'));
      if (!names.length) return;
      const parts = [];
      names.forEach((n, i) => {
        if (i) parts.push({ text: '  ' });
        const child = n === '.' || n === '..' ? FS : node.children[n];
        parts.push(isFile(child) ? { text: n } : { text: n, cls: 'term-dir' });
      });
      line(parts);
    } },
    meow: { hidden: true, usage: 'meow', desc: '', run() { print('meow'); } },
    nano: { usage: 'nano [file]', desc: 'edit a file', run(args) {
      const target = args[0];
      let name = 'New Buffer';
      let content = '';
      let segs = null;
      if (target) {
        segs = resolvePath(target);
        const node = nodeAt(segs);
        if (node === DENIED) { print('nano: ' + target + ': Permission denied'); return; }
        if (node !== null && !isFile(node)) { print('nano: ' + target + ': Is a directory'); return; }
        if (node !== null) content = fileBody(node);
        name = segs[segs.length - 1] || target;
      }
      openNano(name, content, segs);
    } },
    pwd: { usage: 'pwd', desc: 'print working directory', run() { print('/' + cwd.join('/')); } },
    rm: { usage: 'rm [-rf] <file>', desc: 'remove files', run(args) {
      const recursive = args.some((a) => /^-[a-zA-Z]*r/i.test(a) || a === '--recursive');
      const targets = args.filter((a) => !a.startsWith('-'));
      if (!targets.length) { print('rm: missing operand'); return; }
      for (const t of targets) {
        const segs = resolvePath(t);
        // The one write the filesystem allows: rm -rf / is the real
        // delete-website flow, confirmation dialog and all
        if (recursive && segs.length === 0) {
          const dlg = document.getElementById('deleteDialog');
          if (dlg) {
            dlg.showModal();
            // Chromium: once a nested modal has closed, the input under it
            // still gets key events but no longer inserts text — only a
            // fresh input element types again
            dlg.addEventListener('close', () => {
              const fresh = input.cloneNode();
              input.replaceWith(fresh);
              input = fresh;
              wireInput(fresh);
              fresh.focus();
            }, { once: true });
            return;
          }
        }
        const node = nodeAt(segs);
        if (node === null) print("rm: cannot remove '" + t + "': No such file or directory");
        else if (node === DENIED) print("rm: cannot remove '" + t + "': Permission denied");
        else if (!isFile(node) && !recursive) print("rm: cannot remove '" + t + "': Is a directory");
        else if (user !== 'root') print("rm: cannot remove '" + t + "': Permission denied");
        else {
          const parent = nodeAt(segs.slice(0, -1));
          delete parent.children[segs[segs.length - 1]];
        }
      }
    } },
    roll: { usage: 'roll [NdS+M]', desc: 'roll dice', run(args) {
      const spec = (args[0] || 'd20').toLowerCase();
      const m = /^(\d*)d(\d+)([+-]\d+)?$/.exec(spec);
      if (!m) { print('roll: usage: roll [NdS+M], e.g. roll 2d6+1'); return; }
      const n = Math.min(parseInt(m[1] || '1', 10) || 1, 100);
      const sides = parseInt(m[2], 10);
      const mod = parseInt(m[3] || '0', 10);
      const rolls = [];
      for (let i = 0; i < n; i++) rolls.push(1 + Math.floor(Math.random() * sides));
      const total = rolls.reduce((a, r) => a + r, 0) + mod;
      if (n === 1 && !mod) { print(spec + ': ' + total); return; }
      let breakdown = rolls.join(' + ');
      if (mod) breakdown += mod > 0 ? ' + ' + mod : ' - ' + -mod;
      print(spec + ': ' + breakdown + ' = ' + total);
    } },
    su: { hidden: true, usage: 'su [user]', desc: '', run(args) {
      const target = args.filter((a) => !a.startsWith('-'))[0] || 'root';
      if (!(target in USERS)) { print('su: user ' + target + ' does not exist'); return; }
      if (user === 'root') {
        userStack.push(user);
        becomeUser(target);
        return;
      }
      pendingSu = target;
      promptEl.textContent = 'Password:';
      input.type = 'password';
    } },
    sudo: { hidden: true, usage: 'sudo', desc: '', run(args) {
      if (user !== 'root') {
        print(user + ' is not in the sudoers file.  This incident will be reported.');
        return;
      }
      if (!args.length) return;
      const cmd = COMMANDS[ALIASES[args[0]] || args[0]];
      if (cmd) cmd.run(args.slice(1));
      else print('sudo: ' + args[0] + ': command not found');
    } },
    uname: { hidden: true, usage: 'uname', desc: '', run(args) {
      if (args.includes('-a')) print('Linux ' + HOST + ' ' + KERNEL + ' #1 SMP PREEMPT x86_64 GNU/Linux');
      else if (args.includes('-r')) print(KERNEL);
      else print('Linux');
    } },
    whoami: { usage: 'whoami', desc: 'print user name', run() { print(user); } },
  };

  function runLine(raw) {
    line([{ text: promptText() + ' ', cls: 'term-ps1' }, { text: raw }]);
    const trimmed = raw.trim();
    if (trimmed) {
      if (trimmed !== history[history.length - 1]) {
        history.push(trimmed);
        if (history.length > HIST_MAX) history.shift();
        saveHistory();
      }
      const tokens = trimmed.split(/\s+/);
      const cmd = COMMANDS[ALIASES[tokens[0]] || tokens[0]];
      if (cmd) cmd.run(tokens.slice(1));
      else print('gsh: ' + tokens[0] + ': command not found');
    }
    hIdx = history.length;
    draft = '';
    screen.scrollTop = screen.scrollHeight;
  }

  // Tab: complete command names (first word) or paths (arguments) to the
  // longest shared prefix; a stuck tab with several matches lists them
  function complete() {
    const v = input.value;
    const token = /(\S*)$/.exec(v)[1];
    const before = v.slice(0, v.length - token.length);
    let base = '';
    let candidates;
    if (before.trim() === '') {
      candidates = Object.keys(COMMANDS)
        .filter((c) => !COMMANDS[c].hidden && c.startsWith(token))
        .map((c) => c + ' ');
    } else {
      const slash = token.lastIndexOf('/');
      base = slash >= 0 ? token.slice(0, slash + 1) : '';
      const prefix = token.slice(slash + 1);
      const node = nodeAt(resolvePath(base || '.'));
      if (!node || node === DENIED || !allowed(node) || isFile(node)) return;
      candidates = Object.keys(node.children)
        .filter((n) => n.startsWith(prefix) && (prefix.startsWith('.') || !n.startsWith('.')))
        .map((n) => n + (isFile(node.children[n]) ? ' ' : '/'));
    }
    if (!candidates.length) return;
    let lcp = candidates[0];
    for (const c of candidates) {
      while (!c.startsWith(lcp)) lcp = lcp.slice(0, -1);
    }
    if (candidates.length === 1) {
      input.value = before + base + candidates[0];
    } else if (lcp && base + lcp !== token) {
      input.value = before + base + lcp;
    } else {
      line(candidates.map((c, i) => ({ text: (i ? '  ' : '') + c.trimEnd() })));
      screen.scrollTop = screen.scrollHeight;
    }
  }

  // ── nano: the one installed editor. Editing works; saving is another
  // matter (the filesystem is read-only, and it says so like nano would)
  let nano = null;
  let nanoTitleName = null;
  let nanoTitleMod = null;
  let nanoEdit = null;
  let nanoStatus = null;
  let nanoKeys = null;
  let nanoName = '';
  let nanoSegs = null;
  let nanoOriginal = '';
  let nanoPrompting = false;
  const NANO_EDIT_KEYS = [['^X', 'Exit'], ['^O', 'Write Out']];

  function setNanoKeys(pairs) {
    nanoKeys.textContent = '';
    pairs.forEach(([key, label]) => {
      const k = document.createElement('span');
      k.className = 'nano-key';
      k.textContent = key;
      nanoKeys.appendChild(k);
      nanoKeys.appendChild(document.createTextNode(' ' + label + '   '));
    });
  }

  function setNanoStatus(msg) {
    nanoStatus.textContent = '';
    if (msg) {
      const s = document.createElement('span');
      s.textContent = msg;
      nanoStatus.appendChild(s);
    }
  }

  function updateNanoTitle() {
    nanoTitleName.textContent = nanoName;
    nanoTitleMod.textContent = nanoEdit.value === nanoOriginal ? '' : 'Modified';
  }

  function buildNano() {
    nano = document.createElement('div');
    nano.className = 'nano';
    const title = document.createElement('div');
    title.className = 'nano-bar nano-title';
    const version = document.createElement('span');
    version.textContent = 'GNU nano 8.1';
    nanoTitleName = document.createElement('span');
    nanoTitleMod = document.createElement('span');
    title.append(version, nanoTitleName, nanoTitleMod);
    nanoEdit = document.createElement('textarea');
    nanoEdit.className = 'nano-edit';
    nanoEdit.spellcheck = false;
    nanoEdit.setAttribute('aria-label', 'Editor');
    nanoStatus = document.createElement('div');
    nanoStatus.className = 'nano-status';
    nanoKeys = document.createElement('div');
    nanoKeys.className = 'nano-keys';
    nano.append(title, nanoEdit, nanoStatus, nanoKeys);
    nanoEdit.addEventListener('keydown', nanoKeydown);
    nanoEdit.addEventListener('input', updateNanoTitle);
  }

  function openNano(name, content, segs) {
    if (!nano) buildNano();
    nanoName = name;
    nanoSegs = segs;
    nanoOriginal = content;
    nanoEdit.value = content;
    nanoPrompting = false;
    setNanoStatus('');
    setNanoKeys(NANO_EDIT_KEYS);
    updateNanoTitle();
    screen.classList.add('nano-open');
    screen.appendChild(nano);
    nanoEdit.focus();
    nanoEdit.setSelectionRange(0, 0);
  }

  function closeNano() {
    nanoPrompting = false;
    screen.classList.remove('nano-open');
    nano.remove();
    input.focus();
    screen.scrollTop = screen.scrollHeight;
  }

  function nanoAskExit() {
    nanoPrompting = true;
    setNanoStatus('Save modified buffer?');
    setNanoKeys([['Y', 'Yes'], ['N', 'No'], ['^C', 'Cancel']]);
  }

  function nanoEndPrompt(msg) {
    nanoPrompting = false;
    setNanoStatus(msg);
    setNanoKeys(NANO_EDIT_KEYS);
  }

  // Writing works for root and no one else; sets its own status line
  function nanoSave() {
    if (user !== 'root') {
      setNanoStatus('[ Error writing ' + nanoName + ': Permission denied ]');
      return false;
    }
    if (!nanoSegs || !nanoSegs.length) {
      setNanoStatus('[ Cancelled ]');
      return false;
    }
    const parent = nodeAt(nanoSegs.slice(0, -1));
    if (!parent || parent === DENIED || isFile(parent)) {
      setNanoStatus('[ Directory does not exist ]');
      return false;
    }
    parent.children[nanoSegs[nanoSegs.length - 1]] = nanoEdit.value;
    nanoOriginal = nanoEdit.value;
    updateNanoTitle();
    const n = nanoEdit.value ? nanoEdit.value.replace(/\n$/, '').split('\n').length : 0;
    setNanoStatus('[ Wrote ' + n + ' line' + (n === 1 ? '' : 's') + ' ]');
    return true;
  }

  function nanoKeydown(e) {
    if (nanoPrompting) {
      e.preventDefault();
      const k = e.key.toLowerCase();
      if (k === 'y') {
        nanoPrompting = false;
        setNanoKeys(NANO_EDIT_KEYS);
        if (nanoSave()) closeNano();
      } else if (k === 'n') closeNano();
      else if ((k === 'c' && e.ctrlKey) || e.key === 'Escape') nanoEndPrompt('[ Cancelled ]');
      return;
    }
    if (e.ctrlKey && !e.altKey && !e.metaKey) {
      const k = e.key.toLowerCase();
      if (k === 'x') {
        e.preventDefault();
        if (nanoEdit.value !== nanoOriginal) nanoAskExit();
        else closeNano();
      } else if (k === 'o' || k === 's') {
        e.preventDefault();
        nanoSave();
      }
      return;   // other ctrl combos (copy, paste, undo) keep their defaults
    }
    if (e.key === 'Escape') {
      e.preventDefault();
      if (nanoEdit.value !== nanoOriginal) nanoAskExit();
      else closeNano();
    } else if (e.key === 'Tab') {
      e.preventDefault();
      nanoEdit.setRangeText('\t', nanoEdit.selectionStart, nanoEdit.selectionEnd, 'end');
      updateNanoTitle();
    }
  }

  function build() {
    dialog = document.createElement('dialog');
    dialog.className = 'term-dialog';
    dialog.setAttribute('aria-label', 'Terminal');

    const bar = document.createElement('div');
    bar.className = 'term-titlebar';
    termTitle = document.createElement('span');
    termTitle.className = 'term-title';
    termTitle.textContent = user + '@' + HOST;
    bar.appendChild(termTitle);

    screen = document.createElement('div');
    screen.className = 'term-screen';
    out = document.createElement('div');
    out.className = 'term-out';
    out.setAttribute('role', 'log');
    const row = document.createElement('div');
    row.className = 'term-inputrow';
    promptEl = document.createElement('span');
    promptEl.className = 'term-prompt';
    promptEl.setAttribute('aria-hidden', 'true');
    promptEl.textContent = promptText();
    input = document.createElement('input');
    input.className = 'term-input';
    input.type = 'text';
    input.autocapitalize = 'off';
    input.autocomplete = 'off';
    input.spellcheck = false;
    input.setAttribute('aria-label', 'Command input');
    row.append(promptEl, input);
    screen.append(out, row);
    dialog.append(bar, screen);
    document.body.appendChild(dialog);

    const motd = nodeAt(resolvePath('/etc/motd'));
    if (typeof motd === 'string') print(motd.replace(/\n$/, ''));

    wireInput(input);
    // Click focuses the input, but not when selecting scrollback text
    screen.addEventListener('click', () => {
      const sel = window.getSelection();
      if (!sel || sel.isCollapsed) input.focus();
    });
    // Click on the backdrop closes, like the delete dialog
    dialog.addEventListener('click', (e) => {
      if (e.target === dialog) dialog.close();
    });
    // While nano is up, Escape belongs to nano, not the dialog
    dialog.addEventListener('cancel', (e) => {
      if (screen.classList.contains('nano-open')) e.preventDefault();
    });
  }

  function wireInput(el) {
    el.addEventListener('keydown', (e) => {
      if (e.ctrlKey && !e.altKey && !e.metaKey) {
        const k = e.key.toLowerCase();
        if (k === 'c') {
          e.preventDefault();
          if (pendingSu) {
            pendingSu = null;
            input.type = 'text';
            line([{ text: 'Password:^C' }]);
            promptEl.textContent = promptText();
          } else {
            line([{ text: promptText() + ' ', cls: 'term-ps1' }, { text: input.value + '^C' }]);
          }
          input.value = '';
          hIdx = history.length;
          draft = '';
          screen.scrollTop = screen.scrollHeight;
        } else if (k === 'd') {
          e.preventDefault();
          if (pendingSu) {
            pendingSu = null;
            input.type = 'text';
            input.value = '';
            line([{ text: 'Password:' }]);
            promptEl.textContent = promptText();
          } else if (input.value === '') COMMANDS.exit.run();
        } else if (k === 'l') {
          e.preventDefault();
          out.textContent = '';
        }
        return;   // unhandled ctrl combos (paste, …) keep their defaults
      }
      if (e.key === 'Enter') {
        // preventDefault kills the trailing keypress: if a command opens a
        // modal (rm -rf /), the keypress would land on its autofocused
        // button and click it before the dialog is ever seen
        e.preventDefault();
        const raw = input.value;
        input.value = '';
        if (pendingSu) {
          // password attempts stay out of scrollback and history
          const target = pendingSu;
          pendingSu = null;
          input.type = 'text';
          line([{ text: 'Password:' }]);
          if (raw === PASSWORDS[target]) {
            userStack.push(user);
            becomeUser(target);
          } else {
            print('su: Authentication failure');
            promptEl.textContent = promptText();
          }
          screen.scrollTop = screen.scrollHeight;
          return;
        }
        runLine(raw);
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        if (pendingSu) return;
        if (hIdx > 0) {
          if (hIdx === history.length) draft = input.value;
          hIdx--;
          input.value = history[hIdx];
          input.setSelectionRange(input.value.length, input.value.length);
        }
      } else if (e.key === 'ArrowDown') {
        e.preventDefault();
        if (pendingSu) return;
        if (hIdx < history.length) {
          hIdx++;
          input.value = hIdx === history.length ? draft : history[hIdx];
          input.setSelectionRange(input.value.length, input.value.length);
        }
      } else if (e.key === 'Tab') {
        e.preventDefault();
        if (!pendingSu) complete();
      }
    });
  }

  function open() {
    if (!dialog) build();   // lazy: no trace in the DOM until first opened
    if (dialog.open) return;
    dialog.showModal();
    if (screen.classList.contains('nano-open')) nanoEdit.focus();
    else input.focus();
    screen.scrollTop = screen.scrollHeight;
  }

  icon.addEventListener('click', open);
})();
