foxygit / nfo Log in
commits tags

/index.html · 85.59 KB

raw
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NFO / ANS EDITOR</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
  @font-face {
    font-family: 'IBM VGA 8x16';
    src: url('ibm_vga_8x16.woff') format('woff');
    font-weight: normal; font-style: normal;
  }
  :root {
    --bg:#0a0a0a; --border:#1a1a1a; --cyan:#00e5cc;
    --cyan-dk:#006b5e; --white:#cccccc; --dim:#3a3a3a; --dim2:#555555;
  }
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{background:var(--bg);color:var(--white);font-family:'Share Tech Mono','Courier New',monospace;font-size:13px;min-height:100vh}
  #topbar{position:sticky;top:0;z-index:100;background:#000;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;padding:7px 16px;flex-wrap:wrap}
  .logo{color:var(--cyan);font-size:13px;letter-spacing:3px;text-shadow:0 0 10px rgba(0,229,204,.35);white-space:nowrap}
  .logo em{color:var(--dim2);font-style:normal}
  .tb-btn{background:none;border:1px solid var(--cyan-dk);color:var(--cyan);font-family:inherit;font-size:11px;padding:3px 10px;cursor:pointer;letter-spacing:1px;transition:all .15s;white-space:nowrap}
  .tb-btn:hover{background:var(--cyan-dk);color:#fff}
  .tb-btn.dim{border-color:var(--dim);color:var(--dim2)}
  .tb-btn.dim:hover{border-color:var(--cyan-dk);color:var(--cyan)}
  .tb-btn.hidden{display:none}
  .tb-btn.active{border-color:var(--cyan);background:rgba(0,229,204,.08);color:var(--cyan)}
  #filename{color:var(--dim2);font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
  #width-wrap{display:none;align-items:center;gap:6px}
  #width-wrap.visible{display:flex}
  #width-wrap label{color:var(--dim2);font-size:11px}
  #width-sel{background:#000;border:1px solid var(--dim);color:var(--cyan);font-family:inherit;font-size:11px;padding:2px 6px;cursor:pointer}
  #file-input{display:none}
  #img-input{display:none}
  #dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:calc(100vh - 45px);cursor:pointer;transition:background .2s;gap:14px}
  #dropzone.over{background:rgba(0,229,204,.03)}
  .drop-box{border:1px dashed var(--cyan-dk);padding:36px 56px;text-align:center;pointer-events:none}
  .drop-title{color:var(--cyan);font-size:13px;letter-spacing:3px;margin-bottom:10px}
  .drop-sub{color:var(--dim2);font-size:11px;line-height:1.9;letter-spacing:1px}
  .deco{color:var(--dim);font-size:11px;pointer-events:none;user-select:none}
  #output-wrap{display:none;padding:16px 16px 80px;overflow:auto}
  #output-wrap.visible{display:block}
  #output-wrap.centered{display:flex !important;justify-content:center;align-items:flex-start}
  #canvas-wrap{position:relative;display:inline-block;line-height:0}
  #screen{display:block;image-rendering:-webkit-optimize-contrast;image-rendering:pixelated;cursor:crosshair}
  #screen:focus{outline:none}
  #cursor-el{position:absolute;pointer-events:none;box-sizing:border-box;border:1px solid var(--cyan);display:none;animation:cblink 1s step-end infinite}
  @keyframes cblink{50%{opacity:0}}
  #statusbar{position:fixed;bottom:0;left:0;right:0;background:#000;border-top:1px solid var(--border);padding:3px 16px;font-size:11px;color:var(--dim2);display:flex;gap:20px;flex-wrap:wrap;z-index:80}
  #statusbar b{color:var(--cyan-dk);font-weight:normal}
  ::-webkit-scrollbar{width:5px;height:5px}
  ::-webkit-scrollbar-track{background:#000}
  ::-webkit-scrollbar-thumb{background:var(--cyan-dk)}
  #editbar{display:none;position:fixed;bottom:0;left:0;right:0;z-index:90;background:#000;border-top:1px solid var(--cyan-dk);padding:5px 16px;gap:8px;align-items:center;flex-wrap:wrap}
  #editbar.visible{display:flex}
  .eb-label{color:var(--dim2);font-size:10px;letter-spacing:1px;-webkit-user-select:none;user-select:none;white-space:nowrap}
  .eb-sep{width:1px;height:16px;background:var(--dim);flex-shrink:0}
  .csw{width:16px;height:16px;display:inline-block;cursor:pointer;border:1px solid #333;flex-shrink:0;transition:transform .1s,border-color .1s;position:relative}
  .csw:hover{border-color:var(--cyan);transform:scale(1.3);z-index:1}
  .csw.sel{border:2px solid #fff !important}
  .eb-btn{background:none;border:1px solid var(--dim);color:var(--dim2);font-family:inherit;font-size:10px;padding:2px 8px;cursor:pointer;letter-spacing:1px;transition:all .15s;white-space:nowrap}
  .eb-btn:hover{border-color:var(--cyan-dk);color:var(--cyan)}
  .eb-btn.active{border-color:var(--cyan);color:var(--cyan)}

  #active-char-wrap{display:flex;align-items:center;gap:4px;cursor:pointer}
  #active-char-box{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--cyan-dk);font-family:'IBM VGA 8x16',monospace;font-size:16px;line-height:1;background:#000;color:#fff}
  #active-char-wrap:hover #active-char-box{border-color:var(--cyan)}
  #charpicker{display:none;position:fixed;z-index:200;background:#000;border:1px solid var(--cyan-dk);padding:6px;box-shadow:0 0 20px rgba(0,0,0,.9)}
  #charpicker.visible{display:block}
  #canvas-bg-picker{display:none;position:fixed;z-index:200;background:#000;border:1px solid var(--cyan-dk);padding:6px;box-shadow:0 0 20px rgba(0,0,0,.9)}
  #canvas-bg-picker.visible{display:block}
  #cbp-grid{display:grid;grid-template-columns:repeat(8,22px);gap:2px}
  #canvas-bg-swatch{width:22px;height:22px;border:1px solid var(--cyan-dk);cursor:pointer;flex-shrink:0}
  #screen.eyedropper{cursor:crosshair !important}
  #new-wrap{position:relative;display:inline-block}
  #new-menu{display:none;position:absolute;top:calc(100% + 2px);left:0;background:#000;border:1px solid var(--cyan-dk);z-index:200;flex-direction:column;min-width:100%;white-space:nowrap}
  #new-menu.visible{display:flex}
  #new-menu .tb-btn{border:none;border-bottom:1px solid var(--border);text-align:left;width:100%;padding:4px 12px}
  #new-menu .tb-btn:last-child{border-bottom:none}
  #na-text{width:100%;height:130px;background:#000;border:1px solid var(--border);color:#aaa;font-family:'Share Tech Mono','Courier New',monospace;font-size:11px;padding:5px;resize:vertical;box-sizing:border-box;margin:8px 0}
  #export-wrap{position:relative;display:none}
  #export-wrap.visible{display:inline-block}
  #export-menu{display:none;position:absolute;top:calc(100% + 2px);left:0;background:#000;border:1px solid var(--cyan-dk);z-index:200;flex-direction:column;min-width:100%}
  #export-menu.visible{display:flex}
  #export-menu .tb-btn{border:none;border-bottom:1px solid var(--border);text-align:left;width:100%;padding:4px 12px}
  #export-menu .tb-btn:last-child{border-bottom:none}
  #canvas-bg-swatch:hover{border-color:var(--cyan)}
  /* Tab bar */
  #tabbar{position:sticky;top:0;z-index:95;background:#000;border-bottom:1px solid var(--border);display:flex;align-items:stretch;overflow-x:auto;flex-shrink:0}
  .tab{background:none;border:none;border-right:1px solid var(--border);color:var(--dim2);font-family:inherit;font-size:11px;padding:4px 10px 4px 12px;cursor:pointer;letter-spacing:1px;white-space:nowrap;display:flex;align-items:center;gap:5px;flex-shrink:0}
  .tab:hover{background:rgba(255,255,255,.03);color:var(--white)}
  .tab.active{background:rgba(0,229,204,.05);color:var(--cyan);border-bottom:2px solid var(--cyan)}
  .tab-x{opacity:.4;font-size:13px;line-height:1;cursor:pointer;padding:0 2px;-webkit-user-select:none;user-select:none}
  .tab-x:hover{opacity:1}
  #tab-add{background:none;border:none;color:var(--dim2);font-family:inherit;font-size:16px;line-height:1;padding:0 12px;cursor:pointer}
  #tab-add:hover{color:var(--cyan)}
  #cp-grid{display:grid;grid-template-columns:repeat(32,18px);gap:1px}
  .cpc{width:18px;height:18px;display:flex;align-items:center;justify-content:center;font-family:'IBM VGA 8x16',monospace;font-size:13px;line-height:1;cursor:pointer;color:#aaa;border:1px solid transparent;-webkit-user-select:none;user-select:none}
  .cpc:hover{background:var(--cyan-dk);color:#fff}
  .cpc.sel{border-color:var(--cyan);color:var(--cyan)}
  #new-dlg,#new-ansi-dlg{display:none;position:fixed;z-index:200;top:50%;left:50%;transform:translate(-50%,-50%);background:#0a0a0a;border:1px solid var(--cyan-dk);padding:24px;min-width:260px}
  #new-dlg.visible,#new-ansi-dlg.visible{display:block}
  #new-dlg h3,#new-ansi-dlg h3{color:var(--cyan);font-size:11px;letter-spacing:2px;margin-bottom:16px}
  .dlg-row{display:flex;align-items:center;gap:10px;margin-bottom:10px}
  .dlg-row label{color:var(--dim2);font-size:11px;width:70px}
  .dlg-row input{background:#000;border:1px solid var(--dim);color:var(--cyan);font-family:inherit;font-size:11px;padding:3px 8px;width:90px}
  .dlg-btns{display:flex;gap:8px;margin-top:16px;justify-content:flex-end}
  #overlay{display:none;position:fixed;inset:0;z-index:150;background:rgba(0,0,0,.6)}
  #overlay.visible{display:block}
  #help-overlay{display:none;position:fixed;inset:0;z-index:300;background:rgba(0,0,0,.82);align-items:center;justify-content:center}
  #help-overlay.visible{display:flex}
  #help-box{background:#0a0a0a;border:1px solid var(--cyan-dk);padding:28px 32px;max-width:580px;width:90%;max-height:90vh;overflow-y:auto;box-shadow:0 0 40px rgba(0,229,204,.08)}
  #help-box h2{color:var(--cyan);font-size:11px;letter-spacing:4px;margin-bottom:20px;text-align:center}
  .help-section{margin-bottom:18px}
  .help-section h3{color:var(--dim2);font-size:10px;letter-spacing:2px;margin-bottom:8px;border-bottom:1px solid var(--border);padding-bottom:4px}
  .help-row{display:flex;gap:12px;margin-bottom:5px;align-items:baseline}
  .help-key{color:var(--cyan);font-size:11px;min-width:160px;flex-shrink:0}
  .help-key kbd{background:#111;border:1px solid var(--cyan-dk);padding:1px 5px;border-radius:2px;font-family:inherit;font-size:10px}
  .help-desc{color:var(--white);font-size:11px;opacity:.75}
  #help-close{display:block;margin:20px auto 0;background:none;border:1px solid var(--cyan-dk);color:var(--cyan);font-family:inherit;font-size:11px;padding:4px 20px;cursor:pointer;letter-spacing:1px}
  #help-close:hover{background:var(--cyan-dk)}
  #help-titlebar{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}
  #help-titlebar h2{margin-bottom:0}
  #os-toggle{display:flex;border:1px solid var(--cyan-dk);font-size:10px}
  .os-btn{background:none;color:var(--dim2);border:none;padding:3px 10px;font-family:inherit;cursor:pointer;letter-spacing:1px}
  .os-btn.active{background:var(--cyan-dk);color:#fff}
  .color-btn{width:28px;height:18px;cursor:pointer;border:1px solid var(--cyan-dk);flex-shrink:0;transition:border-color .1s}
  .color-btn:hover{border-color:var(--cyan)}
  #color256-picker{display:none;position:fixed;z-index:200;background:#0a0a0a;border:1px solid var(--cyan-dk);padding:8px;box-shadow:0 0 24px rgba(0,0,0,.95)}
  #color256-picker.visible{display:block}
  #c256-title{color:var(--cyan);font-size:10px;letter-spacing:2px;margin-bottom:5px}
  .c256-lbl{color:var(--dim2);font-size:9px;letter-spacing:1px;margin:5px 0 2px}
  #c256-sys-grid{display:grid;grid-template-columns:repeat(16,16px);gap:1px;margin-bottom:4px}
  #c256-cube-grid{display:flex;gap:4px;margin-bottom:4px}
  .c256-plane{display:grid;grid-template-columns:repeat(6,9px);gap:1px}
  #c256-gray-grid{display:grid;grid-template-columns:repeat(24,12px);gap:1px}
  .c256c{cursor:pointer;border:1px solid transparent;box-sizing:border-box;position:relative}
  .c256c:hover{border-color:#fff;transform:scale(1.3);z-index:1}
  .c256c.sel{border:2px solid #fff !important}
  #c256-transp{display:none;margin-top:6px}
  #c256-transp-btn{color:var(--dim2);font-size:10px;cursor:pointer;border:1px solid var(--dim);padding:2px 10px;letter-spacing:1px;display:inline-block}
  #c256-transp-btn:hover{color:var(--cyan);border-color:var(--cyan-dk)}
  #c256-transp-btn.sel{color:var(--cyan);border-color:var(--cyan)}
  /* Image convert dialog */
  #img-dlg{display:none;position:fixed;z-index:200;top:50%;left:50%;transform:translate(-50%,-50%);background:#0a0a0a;border:1px solid var(--cyan-dk);padding:24px;min-width:280px}
  #img-dlg.visible{display:block}
  #img-dlg h3{color:var(--cyan);font-size:11px;letter-spacing:2px;margin-bottom:16px}
  .dlg-check{display:flex;align-items:center;gap:8px;margin-bottom:10px}
  .dlg-check label{color:var(--dim2);font-size:11px}
  .dlg-check input[type=checkbox]{accent-color:var(--cyan);cursor:pointer}
  #img-preview-wrap{margin:12px 0;text-align:center;min-height:40px}
  #img-preview{max-width:100%;max-height:200px;image-rendering:pixelated}
</style>
</head>
<body>

<div id="topbar">
  <div class="logo">NFO<em>::</em>ANS<em>::</em>EDITOR</div>
  <button type="button" class="tb-btn" onclick="openWithPicker()" title="Ctrl+O">[ OPEN ]</button>
  <div id="new-wrap">
    <button type="button" class="tb-btn dim" onclick="toggleNewMenu()" title="Ctrl+N">[ NEW ▾ ]</button>
    <div id="new-menu">
      <button type="button" class="tb-btn dim" onclick="showNewDlg();hideNewMenu()">[ EMPTY ]</button>
      <button type="button" class="tb-btn dim" onclick="newFromClipboard()">[ FROM CLIPBOARD ]</button>
      <button type="button" class="tb-btn dim" onclick="showNewAnsiDlg();hideNewMenu()">[ FROM ANSI CODE ]</button>
    </div>
  </div>
  <button type="button" class="tb-btn dim" onclick="document.getElementById('img-input').click()" title="Konvertera bild till ANSI">[ IMAGE → ANS ]</button>
  <input type="file" id="file-input" accept=".nfo,.ans,.txt,.diz,.asc,.ansi">
  <input type="file" id="img-input" accept="image/*" title="Välj bild att konvertera till ANSI">
  <div id="filename">drop .nfo / .ans anywhere</div>
  <div id="width-wrap">
    <label>cols:</label>
    <select id="width-sel" onchange="rerender()">
      <option value="80">80</option>
      <option value="160">160</option>
      <option value="0">auto</option>
    </select>
  </div>
  <button type="button" id="save-btn" class="tb-btn dim" onclick="saveANS()" title="Ctrl+S">[ SAVE ]</button>
  <div id="export-wrap">
    <button type="button" id="export-btn" class="tb-btn dim" onclick="toggleExportMenu()">[ EXPORT ▾ ]</button>
    <div id="export-menu">
      <button type="button" class="tb-btn dim" onclick="exportANS();hideExportMenu()">[ .ANS ]</button>
      <button type="button" class="tb-btn dim" onclick="exportNFO();hideExportMenu()">[ .NFO plain ]</button>
      <button type="button" class="tb-btn dim" onclick="exportHTML();hideExportMenu()">[ .HTML ]</button>
    </div>
  </div>
  <button type="button" id="edit-btn" class="tb-btn dim hidden" onclick="toggleEdit()" title="Esc (exit edit)">[ EDIT ]</button>
  <button type="button" class="tb-btn dim" onclick="showHelp()" title="?">[ ? ]</button>
</div>

<div id="tabbar"></div>

<div id="help-overlay" onclick="hideHelp()">
  <div id="help-box" onclick="event.stopPropagation()">
    <div id="help-titlebar">
      <h2>KEYBOARD SHORTCUTS</h2>
      <div id="os-toggle">
        <button type="button" id="os-win" class="os-btn active" onclick="setOsMode('win')">WIN</button>
        <button type="button" id="os-mac" class="os-btn" onclick="setOsMode('mac')">MAC</button>
      </div>
    </div>

    <div class="help-section">
      <h3>GLOBAL</h3>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>O</kbd></span><span class="help-desc">Öppna fil</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>N</kbd></span><span class="help-desc">Ny canvas</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>S</kbd></span><span class="help-desc">Exportera ANS</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>S</kbd></span><span class="help-desc">Exportera HTML</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>Z</kbd></span><span class="help-desc">Ångra</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>Y</kbd></span><span class="help-desc">Gör om</span></div>
    </div>

    <div class="help-section">
      <h3>EDIT-LÄGE</h3>
      <div class="help-row"><span class="help-key"><kbd>Esc</kbd></span><span class="help-desc">Avsluta edit-läge</span></div>
      <div class="help-row"><span class="help-key"><kbd>Tab</kbd></span><span class="help-desc">Växla TEXT ↔ DRAW</span></div>
      <div class="help-row"><span class="help-key"><kbd>F2</kbd></span><span class="help-desc">Öppna teckenpanel</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>Delete</kbd></span><span class="help-desc">Radera aktuell rad</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>Insert</kbd></span><span class="help-desc">Infoga tom rad</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>+</kbd></span><span class="help-desc">Zooma in</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>-</kbd></span><span class="help-desc">Zooma ut</span></div>
    </div>

    <div class="help-section">
      <h3>TEXT-LÄGE</h3>
      <div class="help-row"><span class="help-key"><kbd>Piltangenter</kbd></span><span class="help-desc">Flytta markör</span></div>
      <div class="help-row"><span class="help-key"><kbd>Home</kbd> / <kbd>End</kbd></span><span class="help-desc">Radstart / radslut</span></div>
      <div class="help-row"><span class="help-key"><kbd>Enter</kbd></span><span class="help-desc">Ny rad</span></div>
      <div class="help-row"><span class="help-key"><kbd>Backspace</kbd></span><span class="help-desc">Radera tecken till vänster</span></div>
      <div class="help-row"><span class="help-key"><kbd>Delete</kbd></span><span class="help-desc">Radera tecken vid markör</span></div>
      <div class="help-row"><span class="help-key">Valfritt tecken</span><span class="help-desc">Skriv med aktiv FG/BG-färg</span></div>
    </div>

    <div class="help-section">
      <h3>MARKERING &amp; KLIPPBORD</h3>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>C</kbd></span><span class="help-desc">Kopiera markering (till app + systemklippbord)</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>X</kbd></span><span class="help-desc">Klipp ut markering</span></div>
      <div class="help-row"><span class="help-key"><kbd>Ctrl</kbd> + <kbd>V</kbd></span><span class="help-desc">Klistra in — även extern ASCII-art från systemklippbord</span></div>
    </div>

    <div class="help-section">
      <h3>DRAW-LÄGE</h3>
      <div class="help-row"><span class="help-key">Klick</span><span class="help-desc">Måla cell med aktivt tecken</span></div>
      <div class="help-row"><span class="help-key">Klick + drag</span><span class="help-desc">Måla längs drag</span></div>
      <div class="help-row"><span class="help-key"><kbd>F2</kbd></span><span class="help-desc">Välj aktivt tecken</span></div>
    </div>

    <button type="button" id="help-close" onclick="hideHelp()">[ STÄNG ]</button>
  </div>
</div>

<div id="editbar">
  <button type="button" class="eb-btn active" id="mode-text-btn" onclick="setMode('text')" title="Tab">[ TEXT ]</button>
  <button type="button" class="eb-btn" id="mode-draw-btn" onclick="setMode('draw')" title="Tab">[ DRAW ]</button>
  <button type="button" class="eb-btn" id="mode-sel-btn" onclick="setMode('select')" title="Markera område">[ SEL ]</button>
  <button type="button" class="eb-btn" id="pick-btn" onclick="toggleEyedropper()" title="Högerklick på canvas plockar också färg">[ PICK ]</button>
  <div class="eb-sep"></div>
  <div id="active-char-wrap" onclick="toggleCharPicker(event)" title="F2 — pick character">
    <span class="eb-label">CHAR:</span>
    <div id="active-char-box">&#x2588;</div>
  </div>
  <div class="eb-sep"></div>
  <span class="eb-label">FG:</span>
  <div id="cp-fg-box" class="color-btn" onclick="openColorPicker('fg',event)" title="Teckenfärg (klicka för att byta)"></div>
  <div class="eb-sep"></div>
  <span class="eb-label">BG:</span>
  <div id="cp-bg-box" class="color-btn" onclick="openColorPicker('bg',event)" title="Bakgrundsfärg (klicka för att byta)"></div>
  <div class="eb-sep"></div>
  <span class="eb-label">YTA:</span>
  <div id="canvas-bg-swatch" title="Arbetsytans bakgrundsfärg — klicka för att byta" onclick="toggleCanvasBgPicker(event)"></div>
  <div class="eb-sep"></div>
  <button type="button" class="eb-btn" onclick="deleteRow()" title="Ctrl+Delete">[ DEL ROW ]</button>
  <button type="button" class="eb-btn" onclick="insertRow()" title="Ctrl+Insert">[ INS ROW ]</button>
  <button type="button" class="eb-btn" id="center-btn" onclick="toggleCenter()">[ CENTER ]</button>
  <button type="button" class="eb-btn" onclick="adjFont(1)" title="Ctrl++">[ A+ ]</button>
  <button type="button" class="eb-btn" onclick="adjFont(-1)" title="Ctrl+-">[ A- ]</button>
  <div class="eb-sep"></div>
  <button type="button" class="eb-btn" onclick="undo()" title="Ctrl+Z">[ &#x21A9; UNDO ]</button>
  <button type="button" class="eb-btn" onclick="redo()" title="Ctrl+Y / Ctrl+Shift+Z">[ &#x21AA; REDO ]</button>
  <div class="eb-sep"></div>
  <span class="eb-label" id="cur-pos">0,0</span>
</div>

<div id="charpicker"><div id="cp-grid"></div></div>
<div id="canvas-bg-picker"><div id="cbp-grid"></div></div>
<div id="color256-picker">
  <div id="c256-title">FG</div>
  <div class="c256-lbl">SYSTEM</div>
  <div id="c256-sys-grid"></div>
  <div class="c256-lbl">FÄRGKUB</div>
  <div id="c256-cube-grid"></div>
  <div class="c256-lbl">GRÅSKALA</div>
  <div id="c256-gray-grid"></div>
  <div id="c256-transp"><span id="c256-transp-btn" onmousedown="event.preventDefault()" onclick="setActiveBg(-1);hideColorPicker()">[ INGEN BG ]</span></div>
</div>

<div id="overlay" onclick="hideActiveDialog()"></div>
<div id="new-dlg">
  <h3>NEW CANVAS</h3>
  <div class="dlg-row"><label>Columns</label><input type="number" id="dlg-cols" value="80" min="20" max="320"></div>
  <div class="dlg-row"><label>Rows</label><input type="number" id="dlg-rows" value="25" min="5" max="300"></div>
  <div class="dlg-btns">
    <button type="button" class="eb-btn" onclick="hideNewDlg()">[ CANCEL ]</button>
    <button class="eb-btn active" onclick="createNewCanvas()">[ CREATE ]</button>
  </div>
</div>

<div id="new-ansi-dlg">
  <h3>NEW FROM ANSI CODE</h3>
  <div class="dlg-row"><label>Cols</label><input type="number" id="na-cols" value="80" min="20" max="320"></div>
  <textarea id="na-text" placeholder="Klistra in ANSI-kod (ESC-sekvenser) här..."></textarea>
  <div class="dlg-btns">
    <button type="button" class="eb-btn" onclick="hideNewAnsiDlg()">[ CANCEL ]</button>
    <button class="eb-btn active" onclick="createFromAnsiCode()">[ CREATE ]</button>
  </div>
</div>

<div id="img-dlg">
  <h3>BILD → ANSI</h3>
  <div class="dlg-row"><label>Kolonner</label><input type="number" id="img-cols" value="80" min="10" max="320" oninput="imgAutoRows()"></div>
  <div class="dlg-row"><label>Rader</label><input type="number" id="img-rows" value="40" min="5" max="300"></div>
  <div class="dlg-check"><input type="checkbox" id="img-ascii" onchange="imgAsciiToggle()"><label for="img-ascii">ASCII-tecken  <span id="img-ascii-ramp" style="color:var(--dim2);font-size:10px;letter-spacing:1px"> .:-=+*#%@</span></label></div>
  <div class="dlg-check" id="img-half-row"><input type="checkbox" id="img-half" checked><label for="img-half">Halvblock ▀ (dubbel vertikal upplösning)</label></div>
  <div class="dlg-check"><input type="checkbox" id="img-256"><label for="img-256">256 färger <span style="color:var(--dim2);font-size:10px">(bättre färgnoggrannhet)</span></label></div>
  <div class="dlg-check"><input type="checkbox" id="img-dither" checked><label for="img-dither">Dithering (bättre färgövergångar)</label></div>
  <div class="dlg-check"><input type="checkbox" id="img-aspect" checked onchange="imgAutoRows()"><label for="img-aspect">Bevara proportioner</label></div>
  <div id="img-preview-wrap"></div>
  <div class="dlg-btns">
    <button type="button" class="eb-btn" onclick="hideImgDlg()">[ AVBRYT ]</button>
    <button type="button" class="eb-btn active" onclick="doImgConvert()">[ KONVERTERA ]</button>
  </div>
</div>

<div id="dropzone" onclick="document.getElementById('file-input').click()">
  <div class="deco">\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/</div>
  <div class="drop-box">
    <div class="drop-title">DROP FILE HERE</div>
    <div class="drop-sub">.nfo &nbsp;.ans &nbsp;.diz &nbsp;.asc &nbsp;.txt<br>CP437 + ANSI color + cursor forward<br><br>click to browse &nbsp;·&nbsp; or [ NEW ] for blank canvas</div>
  </div>
  <div class="deco">\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/</div>
</div>

<div id="output-wrap">
  <div id="canvas-wrap">
    <canvas id="screen" tabindex="0"></canvas>
    <div id="cursor-el"></div>
  </div>
</div>

<div id="statusbar">
  <div>file: <b id="s-name">—</b></div>
  <div>size: <b id="s-size">—</b></div>
  <div>type: <b id="s-type">—</b></div>
  <div>dims: <b id="s-dims">—</b></div>
  <div>encoding: <b>CP437</b></div>
</div>

<script>
'use strict';

const CP437 = new Uint32Array([
  0,0x263A,0x263B,0x2665,0x2666,0x2663,0x2660,0x2022,0x25D8,0x25CB,0x25D9,0x2642,0x2640,0x266A,0x266B,0x263C,
  0x25BA,0x25C4,0x2195,0x203C,0xB6,0xA7,0x25AC,0x21A8,0x2191,0x2193,0x2192,0x2190,0x221F,0x2194,0x25B2,0x25BC,
  0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27,0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,
  0x30,0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B,0x3C,0x3D,0x3E,0x3F,
  0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,
  0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,
  0x60,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D,0x6E,0x6F,
  0x70,0x71,0x72,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C,0x7D,0x7E,0x2302,
  0xC7,0xFC,0xE9,0xE2,0xE4,0xE0,0xE5,0xE7,0xEA,0xEB,0xE8,0xEF,0xEE,0xEC,0xC4,0xC5,
  0xC9,0xE6,0xC6,0xF4,0xF6,0xF2,0xFB,0xF9,0xFF,0xD6,0xDC,0xA2,0xA3,0xA5,0x20A7,0x192,
  0xE1,0xED,0xF3,0xFA,0xF1,0xD1,0xAA,0xBA,0xBF,0x2310,0xAC,0xBD,0xBC,0xA1,0xAB,0xBB,
  0x2591,0x2592,0x2593,0x2502,0x2524,0x2561,0x2562,0x2556,0x2555,0x2563,0x2551,0x2557,
  0x255D,0x255C,0x255B,0x2510,0x2514,0x2534,0x252C,0x251C,0x2500,0x253C,0x255E,0x255F,
  0x255A,0x2554,0x2569,0x2566,0x2560,0x2550,0x256C,0x2567,0x2568,0x2564,0x2565,0x2559,
  0x2558,0x2552,0x2553,0x256B,0x256A,0x2518,0x250C,0x2588,0x2584,0x258C,0x2590,0x2580,
  0x3B1,0xDF,0x393,0x3C0,0x3A3,0x3C3,0xB5,0x3C4,0x3A6,0x398,0x3A9,0x3B4,0x221E,0x3C6,
  0x3B5,0x2229,0x2261,0xB1,0x2265,0x2264,0x2320,0x2321,0xF7,0x2248,0xB0,0x2219,0xB7,
  0x221A,0x207F,0xB2,0x25A0,0xA0
]);

const CP437_REV = new Map();
for (let i = 1; i < 256; i++) CP437_REV.set(CP437[i], i);
CP437_REV.set(0x20, 0x20);

const TRANSP_BG = 'repeating-conic-gradient(#888 0% 25%,#fff 0% 50%) 0/8px 8px';
const PALETTE = (() => {
  const h = n => n.toString(16).padStart(2,'0');
  const p = [
    '#000000','#aa0000','#00aa00','#aa5500',
    '#0000aa','#aa00aa','#00aaaa','#aaaaaa',
    '#555555','#ff5555','#55ff55','#ffff55',
    '#5555ff','#ff55ff','#55ffff','#ffffff'
  ];
  const lvl = [0,95,135,175,215,255];
  for (let r=0;r<6;r++) for (let g=0;g<6;g++) for (let b=0;b<6;b++)
    p.push(`#${h(lvl[r])}${h(lvl[g])}${h(lvl[b])}`);
  for (let i=0;i<24;i++) { const v=8+i*10; p.push(`#${h(v)}${h(v)}${h(v)}`); }
  return p;
})();

// ── Data model ───────────────────────────────────────────────────────────────
let sparseMap = new Map();
let lastBytes = null, lastFile = null;
let _saveTimer = null;
let canvasCols = 80, canvasRows = 25;
let canvasBg = '#000000';

// ── Canvas metrics ───────────────────────────────────────────────────────────
const DPR = window.devicePixelRatio || 1;
let CHAR_W = 8, CHAR_H = 16, fontScale = 1;

document.fonts.ready.then(() => {
  const off = document.createElement('canvas');
  const octx = off.getContext('2d');
  octx.font = '16px "IBM VGA 8x16"';
  CHAR_W = Math.round(octx.measureText('\u2588').width) || 8;
  if (sparseMap.size > 0) drawAll();
});

// ── Edit state ───────────────────────────────────────────────────────────────
let editMode = false, drawMode = 'text';
let curRow = 0, curCol = 0;
let activeFg = 7, activeBg = -1;
let activeChar = '\u2588';
let isPainting = false, isCentered = false;
let eyedropperActive = false;
let isErasing = false;
let isMoving = false;
let moveOffsetR = 0, moveOffsetC = 0;
let moveSelCells = null;
let moveOrigR1 = 0, moveOrigC1 = 0, moveOrigR2 = 0, moveOrigC2 = 0;
let undoStack = [], redoStack = [];
let selActive = false, selDragging = false;
let selR1 = 0, selC1 = 0, selR2 = 0, selC2 = 0;
let clipboard = null;
let clipboardText = null;
let lastFileHandle = null;

// ── Tabs ──────────────────────────────────────────────────────────────────────
function newTabState() {
  return { name:'untitled', sparseMap:new Map(), lastFile:null, lastBytes:null,
           lastFileHandle:null, canvasCols:80, canvasRows:25, canvasBg:'#000000',
           undoStack:[], redoStack:[], isCentered:false };
}
let tabs = [newTabState()];
let activeTab = 0;

updateColorPreview();
document.getElementById('canvas-bg-swatch').style.background = canvasBg;
renderTabs();
if (!window.showOpenFilePicker) {
  const sb = document.getElementById('save-btn');
  sb.disabled = true;
  sb.title = 'Save function only supported by Chrome and Edge at the moment';
  sb.style.opacity = '0.35';
  sb.style.cursor = 'not-allowed';
}

// ── Canvas rendering ─────────────────────────────────────────────────────────
const canvas = document.getElementById('screen');
let ctx = null;

function cw() { return Math.round(fontScale * CHAR_W); }
function ch() { return Math.round(fontScale * CHAR_H); }

function getBounds(map) {
  let maxRow = 0, maxCol = 0;
  for (const key of map.keys()) {
    const r = Math.floor(key / 100000), c = key % 100000;
    if (r > maxRow) maxRow = r;
    if (c > maxCol) maxCol = c;
  }
  return { maxRow, maxCol };
}

function drawAll() {
  const { maxRow, maxCol } = getBounds(sparseMap);
  const rows = Math.max(maxRow + 1, canvasRows);
  const cols = Math.max(maxCol + 1, canvasCols);
  const W = cols * cw(), H = rows * ch();

  canvas.style.width  = W + 'px';
  canvas.style.height = H + 'px';
  canvas.width  = W * DPR;
  canvas.height = H * DPR;

  ctx = canvas.getContext('2d');
  ctx.scale(DPR, DPR);
  ctx.fillStyle = canvasBg;
  ctx.fillRect(0, 0, W, H);
  ctx.font = `${fontScale * CHAR_H}px "IBM VGA 8x16"`;
  ctx.textBaseline = 'top';

  for (const [key, cell] of sparseMap) {
    const row = Math.floor(key / 100000), col = key % 100000;
    const x = col * cw(), y = row * ch();
    if (cell.bg >= 0) {
      ctx.fillStyle = PALETTE[cell.bg];
      ctx.fillRect(x, y, cw(), ch());
    }
    ctx.fillStyle = PALETTE[cell.fg] || PALETTE[7];
    ctx.fillText(cell.ch, x, y);
  }
  drawSelection();
  if (isMoving && moveSelCells) {
    const dstR = Math.min(selR1, selR2), dstC = Math.min(selC1, selC2);
    ctx.fillStyle = canvasBg;
    ctx.fillRect(moveOrigC1 * cw(), moveOrigR1 * ch(),
      (moveOrigC2 - moveOrigC1 + 1) * cw(), (moveOrigR2 - moveOrigR1 + 1) * ch());
    ctx.font = `${fontScale * CHAR_H}px "IBM VGA 8x16"`;
    ctx.textBaseline = 'top';
    for (const [key, cell] of moveSelCells) {
      const dr = Math.floor(key / 100000), dc = key % 100000;
      const x = (dstC + dc) * cw(), y = (dstR + dr) * ch();
      if (cell.bg >= 0) { ctx.fillStyle = PALETTE[cell.bg]; ctx.fillRect(x, y, cw(), ch()); }
      ctx.fillStyle = PALETTE[cell.fg] || PALETTE[7];
      ctx.fillText(cell.ch, x, y);
    }
  }
  updateCursor();
}

function drawCell(row, col, cell) {
  if (!ctx) return;
  const x = col * cw(), y = row * ch();
  ctx.fillStyle = canvasBg;
  ctx.fillRect(x, y, cw(), ch());
  if (!cell) return;
  if (cell.bg >= 0) {
    ctx.fillStyle = PALETTE[cell.bg];
    ctx.fillRect(x, y, cw(), ch());
  }
  ctx.font = `${fontScale * CHAR_H}px "IBM VGA 8x16"`;
  ctx.textBaseline = 'top';
  ctx.fillStyle = PALETTE[cell.fg] || PALETTE[7];
  ctx.fillText(cell.ch, x, y);
}

function updateCursor() {
  const el = document.getElementById('cursor-el');
  if (!editMode || drawMode !== 'text') { el.style.display = 'none'; return; }
  el.style.display = 'block';
  el.style.width  = cw() + 'px';
  el.style.height = ch() + 'px';
  el.style.left   = (curCol * cw()) + 'px';
  el.style.top    = (curRow * ch()) + 'px';
  document.getElementById('cur-pos').textContent = curRow + ',' + curCol;
}

// ── ANSI parser ───────────────────────────────────────────────────────────────
function parse(bytes, cols) {
  sparseMap = new Map();
  let end = bytes.length;
  for (let i = Math.max(0, end - 256); i < end - 5; i++) {
    if (bytes[i]===0x53&&bytes[i+1]===0x41&&bytes[i+2]===0x55&&
        bytes[i+3]===0x43&&bytes[i+4]===0x45) {
      end = i;
      if (end > 0 && bytes[end-1] === 0x1a) end--;
      break;
    }
  }
  let row = 0, col = 0, fg = 7, bg = -1, bold = false;
  const COLS = cols || 80;
  let wrapPending = false;
  let i = 0;
  while (i < end) {
    const b = bytes[i];
    if (b === 0x1b && i+1 < end && bytes[i+1] === 0x5b) {
      i += 2;
      let seq = '';
      while (i < end && !(bytes[i] >= 0x41 && bytes[i] <= 0x7a))
        seq += String.fromCharCode(bytes[i++]);
      const cmd = i < end ? String.fromCharCode(bytes[i++]) : '';
      const parts = seq.split(';');
      const n0 = parts[0]===''||parts[0]==null ? 0 : parseInt(parts[0],10)||0;
      if (cmd==='C') { col += n0||1; }
      else if (cmd==='m') {
        for (const n of parts.map(p=>p===''?0:parseInt(p,10)||0)) {
          if (n===0)             { fg=7; bg=-1; bold=false; }
          else if (n===1)        bold=true;
          else if (n===22)       bold=false;
          else if (n>=30&&n<=37) fg=n-30;
          else if (n===39)       fg=7;
          else if (n>=40&&n<=47) bg=n-40;
          else if (n===49)       bg=-1;
          else if (n>=90&&n<=97) fg=n-90+8;
          else if (n>=100&&n<=107) bg=n-100+8;
        }
      } else if (cmd==='A') { row=Math.max(0,row-(n0||1)); }
      else if (cmd==='B') { row+=n0||1; }
      else if (cmd==='D') { col=Math.max(0,col-(n0||1)); }
      else if (cmd==='H'||cmd==='f') {
        row=Math.max(0,parseInt(parts[0]||'1',10)-1);
        col=Math.max(0,parseInt(parts[1]||'1',10)-1);
      } else if (cmd==='J') { if (n0===2) { sparseMap.clear(); row=0; col=0; } }
      continue;
    }
    if (b===0x0d) { col=0; wrapPending=false; i++; continue; }
    if (b===0x0a) { if (!wrapPending) row++; wrapPending=false; col=0; i++; continue; }
    if (b===0x09) { col=(Math.floor(col/8)+1)*8; i++; continue; }
    if (wrapPending) { row++; col=0; wrapPending=false; }
    const fgIdx = bold ? Math.min(fg+8,15) : fg;
    sparseMap.set(row*100000+col, { ch: String.fromCodePoint(CP437[b]), fg:fgIdx, bg });
    col++;
    if (COLS>0 && col>=COLS) { col=0; wrapPending=true; }
    i++;
  }
}

// ── File loading ──────────────────────────────────────────────────────────────
function processFile(file) {
  const reader = new FileReader();
  reader.onload = e => {
    lastBytes = new Uint8Array(e.target.result);
    lastFile = file;
    doRender(parseInt(document.getElementById('width-sel').value, 10));
  };
  reader.readAsArrayBuffer(file);
}

function doRender(cols) {
  canvasCols = cols || 80;
  parse(lastBytes, cols);
  const { maxRow: mr } = getBounds(sparseMap);
  canvasRows = mr + 1;
  drawAll();
  const { maxRow, maxCol } = getBounds(sparseMap);
  const hasColor = [...sparseMap.values()].some(c => c.fg !== 7 || c.bg >= 0);
  showOutput();
  document.getElementById('filename').textContent = lastFile.name;
  document.getElementById('s-name').textContent = lastFile.name;
  document.getElementById('s-size').textContent = fmt(lastFile.size);
  document.getElementById('s-type').textContent = hasColor ? 'ANSI color' : 'plain CP437';
  document.getElementById('s-dims').textContent = `${maxCol+1}×${maxRow+1}`;
  document.getElementById('width-wrap').classList.add('visible');
  tabs[activeTab].name = lastFile.name;
  renderTabs();
}

function rerender() {
  if (!lastBytes) return;
  doRender(parseInt(document.getElementById('width-sel').value, 10));
}

function showOutput() {
  document.getElementById('output-wrap').classList.add('visible');
  document.getElementById('dropzone').style.display = 'none';
  document.getElementById('export-wrap').classList.add('visible');
  document.getElementById('edit-btn').classList.remove('hidden');
}

function fmt(n) {
  if (n < 1024) return n + ' B';
  if (n < 1048576) return (n/1024).toFixed(1) + ' KB';
  return (n/1048576).toFixed(2) + ' MB';
}

// ── Edit mode ─────────────────────────────────────────────────────────────────
function toggleEdit() {
  editMode = !editMode;
  const editbar = document.getElementById('editbar');
  const btn = document.getElementById('edit-btn');
  const sb  = document.getElementById('statusbar');
  editbar.classList.toggle('visible', editMode);
  btn.classList.toggle('active', editMode);
  btn.textContent = editMode ? '[ EDITING ]' : '[ EDIT ]';
  if (editMode) {
    canvas.focus();
    requestAnimationFrame(() => { sb.style.bottom = editbar.offsetHeight + 'px'; });
    updateCursor();
  } else {
    sb.style.bottom = '';
    document.getElementById('cursor-el').style.display = 'none';
    hideCharPicker();
    eyedropperActive = false;
    canvas.classList.remove('eyedropper');
    document.getElementById('pick-btn').classList.remove('active');
  }
}

function setMode(m) {
  drawMode = m;
  eyedropperActive = false;
  canvas.classList.remove('eyedropper');
  document.getElementById('pick-btn').classList.remove('active');
  document.getElementById('mode-text-btn').classList.toggle('active', m === 'text');
  document.getElementById('mode-draw-btn').classList.toggle('active', m === 'draw');
  document.getElementById('mode-sel-btn').classList.toggle('active', m === 'select');
  canvas.style.cursor = (m === 'draw' || m === 'select') ? 'crosshair' : 'text';
  if (m !== 'select') { selActive = false; if (ctx) drawAll(); }
  updateCursor();
}

function toggleEyedropper() {
  eyedropperActive = !eyedropperActive;
  canvas.classList.toggle('eyedropper', eyedropperActive);
  document.getElementById('pick-btn').classList.toggle('active', eyedropperActive);
}

function pickColorAt(row, col) {
  const cell = sparseMap.get(row * 100000 + col);
  if (!cell) return;
  setActiveFg(cell.fg);
  if (cell.bg >= 0) setActiveBg(cell.bg);
}

// ── 256-color picker ──────────────────────────────────────────────────────────
let _colorPickerMode = 'fg';

(function buildColorPicker256() {
  function makeCell(i, size) {
    const c = document.createElement('div');
    c.className = 'c256c';
    c.style.cssText = `background:${PALETTE[i]};width:${size}px;height:${size}px`;
    c.title = `#${i}  ${PALETTE[i]}`;
    c.dataset.idx = i;
    c.addEventListener('mousedown', e => e.preventDefault());
    c.addEventListener('click', () => {
      if (_colorPickerMode === 'fg') setActiveFg(i);
      else setActiveBg(i);
      hideColorPicker();
    });
    return c;
  }
  const sys  = document.getElementById('c256-sys-grid');
  const cube = document.getElementById('c256-cube-grid');
  const gray = document.getElementById('c256-gray-grid');
  for (let i=0; i<16; i++) sys.appendChild(makeCell(i, 16));
  for (let r=0; r<6; r++) {
    const plane = document.createElement('div');
    plane.className = 'c256-plane';
    for (let g=0; g<6; g++)
      for (let b=0; b<6; b++)
        plane.appendChild(makeCell(16 + r*36 + g*6 + b, 9));
    cube.appendChild(plane);
  }
  for (let i=232; i<256; i++) gray.appendChild(makeCell(i, 12));
})();

function updateColorPicker256Selection() {
  document.querySelectorAll('.c256c').forEach(c =>
    c.classList.toggle('sel',
      _colorPickerMode==='fg' ? +c.dataset.idx===activeFg : +c.dataset.idx===activeBg));
  const tb = document.getElementById('c256-transp-btn');
  if (tb) tb.classList.toggle('sel', _colorPickerMode==='bg' && activeBg===-1);
}

function openColorPicker(mode, e) {
  e.stopPropagation();
  const picker = document.getElementById('color256-picker');
  if (picker.classList.contains('visible') && _colorPickerMode===mode) {
    hideColorPicker(); return;
  }
  _colorPickerMode = mode;
  document.getElementById('c256-title').textContent = mode === 'fg' ? 'FG — TECKENFÄRG' : 'BG — BAKGRUNDSFÄRG';
  document.getElementById('c256-transp').style.display = mode === 'bg' ? 'block' : 'none';
  updateColorPicker256Selection();
  const ebH = document.getElementById('editbar').offsetHeight;
  picker.style.bottom = (ebH + 4) + 'px';
  const btn = mode === 'fg'
    ? document.getElementById('cp-fg-box')
    : document.getElementById('cp-bg-box');
  const rect = btn.getBoundingClientRect();
  const pickerW = 374 + 16;
  const left = Math.min(rect.left, window.innerWidth - pickerW - 8);
  picker.style.left = Math.max(0, left) + 'px';
  picker.classList.add('visible');
}
function hideColorPicker() {
  document.getElementById('color256-picker').classList.remove('visible');
}
document.addEventListener('click', e => {
  const p = document.getElementById('color256-picker');
  if (!p.contains(e.target) &&
      !document.getElementById('cp-fg-box').contains(e.target) &&
      !document.getElementById('cp-bg-box').contains(e.target))
    hideColorPicker();
});

function updateColorPreview() {
  document.getElementById('cp-fg-box').style.background = PALETTE[activeFg];
  document.getElementById('cp-bg-box').style.background =
    activeBg >= 0 ? PALETTE[activeBg] : TRANSP_BG;
}
function setActiveFg(i) {
  activeFg = i;
  updateColorPreview();
}
function setActiveBg(i) {
  activeBg = i;
  updateColorPreview();
}

// ── Char picker ───────────────────────────────────────────────────────────────
(function buildCharPicker() {
  const grid = document.getElementById('cp-grid');
  for (let i = 1; i < 256; i++) {
    const cell = document.createElement('div');
    cell.className = 'cpc';
    cell.textContent = String.fromCodePoint(CP437[i]);
    cell.dataset.idx = i;
    cell.title = '#' + i + ' 0x' + i.toString(16).toUpperCase().padStart(2,'0');
    cell.addEventListener('click', () => {
      activeChar = String.fromCodePoint(CP437[i]);
      document.getElementById('active-char-box').textContent = activeChar;
      document.querySelectorAll('.cpc').forEach(c => c.classList.toggle('sel', c === cell));
      hideCharPicker();
    });
    grid.appendChild(cell);
  }
})();

function toggleCharPicker(e) {
  e.stopPropagation();
  const picker = document.getElementById('charpicker');
  if (picker.classList.contains('visible')) { hideCharPicker(); return; }
  const ebH = document.getElementById('editbar').offsetHeight;
  picker.style.bottom = (ebH + 4) + 'px';
  picker.style.left = '16px';
  picker.classList.add('visible');
}
function hideCharPicker() {
  document.getElementById('charpicker').classList.remove('visible');
}

// ── Canvas background picker ──────────────────────────────────────────────────
(function buildCanvasBgPicker() {
  const grid = document.getElementById('cbp-grid');
  PALETTE.slice(0, 16).forEach((color, i) => {
    const s = document.createElement('div');
    s.className = 'csw';
    s.style.background = color;
    s.title = color;
    s.addEventListener('mousedown', e => { e.preventDefault(); setCanvasBg(color); });
    grid.appendChild(s);
  });
})();

function toggleCanvasBgPicker(e) {
  e.stopPropagation();
  const picker = document.getElementById('canvas-bg-picker');
  if (picker.classList.contains('visible')) { hideCanvasBgPicker(); return; }
  const ebH = document.getElementById('editbar').offsetHeight;
  picker.style.bottom = (ebH + 4) + 'px';
  const rect = document.getElementById('canvas-bg-swatch').getBoundingClientRect();
  picker.style.left = rect.left + 'px';
  picker.classList.add('visible');
}
function hideCanvasBgPicker() {
  document.getElementById('canvas-bg-picker').classList.remove('visible');
}
function setCanvasBg(color) {
  canvasBg = color;
  document.getElementById('canvas-bg-swatch').style.background = color;
  hideCanvasBgPicker();
  if (ctx) drawAll();
  scheduleSessionSave();
}
document.addEventListener('click', e => {
  if (!document.getElementById('canvas-bg-picker').contains(e.target) &&
      !document.getElementById('canvas-bg-swatch').contains(e.target))
    hideCanvasBgPicker();
});
document.addEventListener('click', e => {
  if (!document.getElementById('charpicker').contains(e.target) &&
      !document.getElementById('active-char-wrap').contains(e.target))
    hideCharPicker();
});

// ── Mouse on canvas ───────────────────────────────────────────────────────────
function cellFromEvent(e) {
  const rect = canvas.getBoundingClientRect();
  return {
    row: Math.max(0, Math.floor((e.clientY - rect.top)  / ch())),
    col: Math.max(0, Math.floor((e.clientX - rect.left) / cw()))
  };
}

function eraseCell(row, col) {
  const key = row * 100000 + col;
  if (!sparseMap.has(key)) return;
  sparseMap.delete(key);
  drawCell(row, col, null);
}

canvas.addEventListener('contextmenu', e => {
  if (!editMode) return;
  e.preventDefault();
});

canvas.addEventListener('mousedown', e => {
  if (!editMode) return;
  e.preventDefault();
  canvas.focus();
  const { row, col } = cellFromEvent(e);
  curRow = row; curCol = col;
  if (e.button === 2) {
    if (drawMode === 'draw') {
      pushUndo();
      isErasing = true;
      eraseCell(row, col);
    } else {
      pickColorAt(row, col);
    }
    return;
  }
  if (eyedropperActive) {
    pickColorAt(row, col);
    eyedropperActive = false;
    canvas.classList.remove('eyedropper');
    document.getElementById('pick-btn').classList.remove('active');
    return;
  }
  if (drawMode === 'select') {
    if (selActive) {
      const r1=Math.min(selR1,selR2),r2=Math.max(selR1,selR2);
      const c1=Math.min(selC1,selC2),c2=Math.max(selC1,selC2);
      if (row>=r1&&row<=r2&&col>=c1&&col<=c2) {
        pushUndo();
        isMoving=true;
        moveOrigR1=r1; moveOrigR2=r2; moveOrigC1=c1; moveOrigC2=c2;
        moveOffsetR=row-r1; moveOffsetC=col-c1;
        moveSelCells=new Map();
        for (let r=r1;r<=r2;r++) for (let c=c1;c<=c2;c++) {
          const cell=sparseMap.get(r*100000+c);
          if (cell) moveSelCells.set((r-r1)*100000+(c-c1),{...cell});
        }
        return;
      }
    }
    selActive=true; selDragging=true;
    selR1=selR2=row; selC1=selC2=col;
    drawAll();
  } else if (drawMode === 'draw') {
    pushUndo();
    isPainting = true;
    paintCell(row, col);
  } else {
    updateCursor();
  }
});

canvas.addEventListener('mousemove', e => {
  if (!editMode) return;
  const { row, col } = cellFromEvent(e);
  if (isMoving) {
    const h=moveOrigR2-moveOrigR1, w=moveOrigC2-moveOrigC1;
    selR1=row-moveOffsetR; selR2=selR1+h;
    selC1=col-moveOffsetC; selC2=selC1+w;
    drawAll(); return;
  }
  if (drawMode === 'select' && selDragging) {
    selR2 = row; selC2 = col; drawAll(); return;
  }
  if (isErasing && drawMode === 'draw') { eraseCell(row, col); return; }
  if (!isPainting || drawMode !== 'draw') return;
  paintCell(row, col);
});

function commitMove() {
  if (!isMoving) return;
  isMoving = false;
  const dstR=Math.min(selR1,selR2), dstC=Math.min(selC1,selC2);
  for (let r=moveOrigR1;r<=moveOrigR2;r++)
    for (let c=moveOrigC1;c<=moveOrigC2;c++)
      sparseMap.delete(r*100000+c);
  for (const [key,cell] of moveSelCells) {
    const dr=Math.floor(key/100000), dc=key%100000;
    sparseMap.set((dstR+dr)*100000+(dstC+dc),{...cell});
  }
  moveSelCells=null;
  drawAll();
}
canvas.addEventListener('mouseup',    () => { commitMove(); isPainting = false; selDragging = false; isErasing = false; });
canvas.addEventListener('mouseleave', () => { commitMove(); isPainting = false; selDragging = false; isErasing = false; });

function paintCell(row, col) {
  const key = row * 100000 + col;
  const cell = { ch: activeChar, fg: activeFg, bg: activeBg };
  const existing = sparseMap.get(key);
  if (existing && existing.ch===cell.ch && existing.fg===cell.fg && existing.bg===cell.bg) return;
  sparseMap.set(key, cell);
  drawCell(row, col, cell);
}

// ── Keyboard ──────────────────────────────────────────────────────────────────
canvas.addEventListener('keydown', e => {
  if (!editMode) return;
  const ctrl = e.ctrlKey || e.metaKey;

  // ── Shortcuts available in both modes ────────────────────────────────────
  if (ctrl && (e.key==='z'||e.key==='Z'))           { e.preventDefault(); undo(); return; }
  if (ctrl && (e.key==='y'||e.key==='Y'))           { e.preventDefault(); redo(); return; }
  if (ctrl && (e.key==='c'||e.key==='C'))           { e.preventDefault(); copySelection(); return; }
  if (ctrl && (e.key==='x'||e.key==='X'))           { e.preventDefault(); cutSelection(); return; }
  if (ctrl && (e.key==='v'||e.key==='V'))           { e.preventDefault(); pasteClipboard(); return; }
  if (e.key==='Escape') {
    if (selActive) { e.preventDefault(); selActive=false; drawAll(); return; }
    e.preventDefault(); toggleEdit(); return;
  }
  if (e.key==='Tab')                                { e.preventDefault(); setMode(drawMode==='text'?'draw':'text'); return; }
  if (ctrl && e.key==='Delete')                     { e.preventDefault(); deleteRow(); return; }
  if (ctrl && e.key==='Insert')                     { e.preventDefault(); insertRow(); return; }
  if (ctrl && (e.key==='='||e.key==='+'))           { e.preventDefault(); adjFont(1); return; }
  if (ctrl && e.key==='-')                          { e.preventDefault(); adjFont(-1); return; }
  if (e.key==='F2')                                 { e.preventDefault(); toggleCharPicker({stopPropagation:()=>{}}); return; }
  if (e.metaKey && e.key==='Backspace')             { e.preventDefault(); if(drawMode==='text'){pushUndo();sparseMap.delete(curRow*100000+curCol);drawCell(curRow,curCol,null);updateCursor();} return; }

  if (ctrl) return;

  // ── Text mode navigation & editing ───────────────────────────────────────
  if (drawMode !== 'text') return;
  e.preventDefault();
  switch (e.key) {
    case 'ArrowRight': curCol++; break;
    case 'ArrowLeft':  curCol = Math.max(0, curCol-1); break;
    case 'ArrowDown':  curRow++; break;
    case 'ArrowUp':    curRow = Math.max(0, curRow-1); break;
    case 'Home':       curCol = 0; break;
    case 'End': {
      let mx = 0;
      for (const k of sparseMap.keys())
        if (Math.floor(k/100000)===curRow) mx = Math.max(mx, k%100000);
      curCol = mx + 1;
      break;
    }
    case 'Enter': curRow++; curCol = 0; break;
    case 'Backspace':
      if (curCol > 0) {
        pushUndo();
        curCol--;
        sparseMap.delete(curRow*100000+curCol);
        drawCell(curRow, curCol, null);
      }
      break;
    case 'Delete':
      pushUndo();
      sparseMap.delete(curRow*100000+curCol);
      drawCell(curRow, curCol, null);
      break;
    default:
      if (e.key.length === 1) {
        pushUndo();
        const cell = { ch: e.key, fg: activeFg, bg: activeBg };
        sparseMap.set(curRow*100000+curCol, cell);
        drawCell(curRow, curCol, cell);
        curCol++;
      }
  }
  updateCursor();
});

// ── Global shortcuts (work outside canvas too) ────────────────────────────
document.addEventListener('keydown', e => {
  if (e.key === 'Escape') {
    if (document.getElementById('help-overlay').classList.contains('visible')) { hideHelp(); return; }
    if (document.getElementById('img-dlg').classList.contains('visible'))      { hideImgDlg(); return; }
    if (document.getElementById('new-dlg').classList.contains('visible'))      { hideNewDlg(); return; }
    if (document.getElementById('new-ansi-dlg').classList.contains('visible')) { hideNewAnsiDlg(); return; }
    hideNewMenu();
  }
  // ⌘+Backspace = Delete, works even when canvas lost focus
  if (e.metaKey && e.key === 'Backspace' && editMode && drawMode === 'text') {
    e.preventDefault();
    pushUndo();
    sparseMap.delete(curRow * 100000 + curCol);
    drawCell(curRow, curCol, null);
    updateCursor();
    return;
  }
  if (document.activeElement === canvas) return; // handled above
  const ctrl = e.ctrlKey || e.metaKey;
  if (!ctrl) return;
  if (e.key==='z'||e.key==='Z') { e.preventDefault(); undo(); }
  if (e.key==='y'||e.key==='Y') { e.preventDefault(); redo(); }
  if (e.key==='c'||e.key==='C') { e.preventDefault(); copySelection(); }
  if (e.key==='x'||e.key==='X') { e.preventDefault(); cutSelection(); }
  if (e.key==='v'||e.key==='V') { e.preventDefault(); pasteClipboard(); }
  if (e.key==='o'||e.key==='O') { e.preventDefault(); openWithPicker(); }
  if (e.key==='n'||e.key==='N') { e.preventDefault(); toggleNewMenu(); }
  if (e.key==='s'&&!e.shiftKey) { e.preventDefault(); saveANS(); }
  if (e.key==='s'&& e.shiftKey) { e.preventDefault(); exportHTML(); }
});

// ── Undo / redo ───────────────────────────────────────────────────────────────
function cloneMap(m) {
  const c = new Map();
  for (const [k,v] of m) c.set(k, {...v});
  return c;
}
function pushUndo() {
  undoStack.push(cloneMap(sparseMap));
  if (undoStack.length > 50) undoStack.shift();
  redoStack = [];
  scheduleSessionSave();
}
function undo() {
  if (!undoStack.length) return;
  redoStack.push(cloneMap(sparseMap));
  sparseMap = undoStack.pop();
  drawAll();
}
function redo() {
  if (!redoStack.length) return;
  undoStack.push(cloneMap(sparseMap));
  sparseMap = redoStack.pop();
  drawAll();
}

// ── Selection & clipboard ─────────────────────────────────────────────────────
function drawSelection() {
  if (!selActive || !ctx) return;
  const r1=Math.min(selR1,selR2), r2=Math.max(selR1,selR2);
  const c1=Math.min(selC1,selC2), c2=Math.max(selC1,selC2);
  ctx.fillStyle = 'rgba(0,229,204,0.18)';
  ctx.fillRect(c1*cw(), r1*ch(), (c2-c1+1)*cw(), (r2-r1+1)*ch());
  ctx.strokeStyle = 'rgba(0,229,204,0.85)';
  ctx.lineWidth = 1;
  ctx.strokeRect(c1*cw()+0.5, r1*ch()+0.5, (c2-c1+1)*cw()-1, (r2-r1+1)*ch()-1);
}
function copySelection() {
  if (!selActive) return;
  const r1=Math.min(selR1,selR2), r2=Math.max(selR1,selR2);
  const c1=Math.min(selC1,selC2), c2=Math.max(selC1,selC2);
  const cells = new Map();
  const lines = [];
  for (let r=r1; r<=r2; r++) {
    let line = '';
    for (let c=c1; c<=c2; c++) {
      const cell = sparseMap.get(r*100000+c);
      if (cell) { cells.set((r-r1)*100000+(c-c1), {...cell}); line += cell.ch; }
      else line += ' ';
    }
    lines.push(line);
  }
  clipboard = { cells, rows:r2-r1+1, cols:c2-c1+1 };
  clipboardText = lines.join('\n');
  navigator.clipboard.writeText(clipboardText).catch(()=>{});
}
function cutSelection() {
  if (!selActive) return;
  copySelection();
  pushUndo();
  const r1=Math.min(selR1,selR2), r2=Math.max(selR1,selR2);
  const c1=Math.min(selC1,selC2), c2=Math.max(selC1,selC2);
  for (let r=r1; r<=r2; r++)
    for (let c=c1; c<=c2; c++)
      sparseMap.delete(r*100000+c);
  selActive = false;
  drawAll();
}
async function pasteClipboard() {
  if (!editMode) return;
  let sysText = null;
  try { sysText = await navigator.clipboard.readText(); } catch(e) {}

  if (sysText !== null && sysText === clipboardText && clipboard) {
    // intern kopia matchar systemet → klistra med färger
    pushUndo();
    for (const [key, cell] of clipboard.cells) {
      const dr = Math.floor(key/100000), dc = key%100000;
      sparseMap.set((curRow+dr)*100000+(curCol+dc), {...cell});
    }
    drawAll();
  } else if (sysText !== null && sysText !== '') {
    // extern text (t.ex. ASCII-art) → klistra rad för rad
    pushUndo();
    const lines = sysText.split('\n');
    const startCol = curCol;
    for (let li = 0; li < lines.length; li++) {
      const line = lines[li].replace(/\r$/, '');
      for (let ci = 0; ci < line.length; ci++) {
        const ch = line[ci];
        sparseMap.set((curRow+li)*100000+(startCol+ci), { ch, fg: activeFg, bg: activeBg });
      }
    }
    drawAll();
  } else if (clipboard) {
    // systemklippbord tomt, använd intern kopia
    pushUndo();
    for (const [key, cell] of clipboard.cells) {
      const dr = Math.floor(key/100000), dc = key%100000;
      sparseMap.set((curRow+dr)*100000+(curCol+dc), {...cell});
    }
    drawAll();
  }
}

// ── Tabs ──────────────────────────────────────────────────────────────────────
function saveCurrentTab() {
  if (activeTab < 0 || activeTab >= tabs.length) return;
  const t = tabs[activeTab];
  t.name = lastFile ? lastFile.name : t.name;
  t.sparseMap=sparseMap; t.lastFile=lastFile; t.lastBytes=lastBytes; t.lastFileHandle=lastFileHandle;
  t.canvasCols=canvasCols; t.canvasRows=canvasRows; t.canvasBg=canvasBg;
  t.undoStack=undoStack; t.redoStack=redoStack; t.isCentered=isCentered;
}
function renderTabs() {
  const bar = document.getElementById('tabbar');
  bar.innerHTML = '';
  tabs.forEach((t, i) => {
    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'tab' + (i===activeTab ? ' active' : '');
    btn.dataset.tabIdx = i;
    const nm = document.createElement('span');
    nm.textContent = t.name;
    btn.appendChild(nm);
    if (tabs.length > 1) {
      const x = document.createElement('span');
      x.className = 'tab-x';
      x.textContent = '×';
      x.dataset.closeIdx = i;
      btn.appendChild(x);
    }
    bar.appendChild(btn);
  });
  const add = document.createElement('button');
  add.type='button'; add.id='tab-add'; add.textContent='+'; add.title='Ny flik';
  add.dataset.tabAdd = '1';
  bar.appendChild(add);
  bar.style.top = document.getElementById('topbar').offsetHeight + 'px';
  scheduleSessionSave();
}
function switchTab(idx) {
  if (idx === activeTab) return;
  saveCurrentTab();
  if (editMode) toggleEdit();
  activeTab = idx;
  const t = tabs[idx];
  sparseMap=t.sparseMap; lastFile=t.lastFile; lastBytes=t.lastBytes; lastFileHandle=t.lastFileHandle||null;
  canvasCols=t.canvasCols; canvasRows=t.canvasRows; canvasBg=t.canvasBg;
  undoStack=t.undoStack; redoStack=t.redoStack; isCentered=t.isCentered;
  curRow=0; curCol=0; selActive=false;
  document.getElementById('canvas-bg-swatch').style.background = canvasBg;
  document.getElementById('output-wrap').classList.toggle('centered', isCentered);
  document.getElementById('center-btn').classList.toggle('active', isCentered);
  if (sparseMap.size > 0) {
    showOutput();
    drawAll();
    document.getElementById('filename').textContent = t.name;
    document.getElementById('s-name').textContent = t.name;
    document.getElementById('width-wrap').classList.add('visible');
  } else {
    document.getElementById('output-wrap').classList.remove('visible');
    document.getElementById('dropzone').style.display = '';
    document.getElementById('export-wrap').classList.remove('visible');
    document.getElementById('edit-btn').classList.add('hidden');
    document.getElementById('filename').textContent = 'drop .nfo / .ans anywhere';
    document.getElementById('width-wrap').classList.remove('visible');
  }
  renderTabs();
}
function addTab() {
  saveCurrentTab();
  if (editMode) toggleEdit();
  tabs.push(newTabState());
  activeTab = tabs.length - 1;
  const t = tabs[activeTab];
  sparseMap=t.sparseMap; lastFile=null; lastBytes=null; lastFileHandle=null;
  canvasCols=80; canvasRows=25; canvasBg='#000000'; isCentered=false;
  undoStack=[]; redoStack=[]; curRow=0; curCol=0; selActive=false;
  document.getElementById('canvas-bg-swatch').style.background = canvasBg;
  document.getElementById('output-wrap').classList.remove('centered','visible');
  document.getElementById('center-btn').classList.remove('active');
  document.getElementById('dropzone').style.display = '';
  document.getElementById('export-wrap').classList.remove('visible');
  document.getElementById('edit-btn').classList.add('hidden');
  document.getElementById('filename').textContent = 'drop .nfo / .ans anywhere';
  document.getElementById('width-wrap').classList.remove('visible');
  renderTabs();
}
function closeTab(idx) {
  if (tabs.length === 1) return;
  const wasActive = idx === activeTab;
  tabs.splice(idx, 1);
  if (wasActive) {
    activeTab = -1;
    switchTab(Math.min(idx, tabs.length-1));
  } else {
    if (idx < activeTab) activeTab--;
    renderTabs();
  }
}

// ── New canvas ────────────────────────────────────────────────────────────────
function showHelp() {
  document.getElementById('help-overlay').classList.add('visible');
}
function hideHelp() {
  document.getElementById('help-overlay').classList.remove('visible');
}
function setOsMode(os) {
  document.getElementById('os-win').classList.toggle('active', os === 'win');
  document.getElementById('os-mac').classList.toggle('active', os === 'mac');
  document.querySelectorAll('#help-box .help-key kbd').forEach(kbd => {
    if (kbd.textContent === 'Ctrl' || kbd.textContent === '⌘')
      kbd.textContent = os === 'mac' ? '⌘' : 'Ctrl';
  });
}

function showNewDlg() {
  document.getElementById('overlay').classList.add('visible');
  document.getElementById('new-dlg').classList.add('visible');
}
function hideActiveDialog() {
  if (document.getElementById('img-dlg').classList.contains('visible'))      { hideImgDlg(); return; }
  if (document.getElementById('new-ansi-dlg').classList.contains('visible')) { hideNewAnsiDlg(); return; }
  hideNewDlg();
}
function hideNewDlg() {
  document.getElementById('overlay').classList.remove('visible');
  document.getElementById('new-dlg').classList.remove('visible');
}
function createNewCanvas() {
  const cols = parseInt(document.getElementById('dlg-cols').value,10) || 80;
  const rows = parseInt(document.getElementById('dlg-rows').value,10) || 25;
  hideNewDlg();
  sparseMap = new Map();
  canvasCols = cols; canvasRows = rows;
  lastFile = null; lastBytes = null;
  undoStack = []; redoStack = [];
  drawAll();
  showOutput();
  document.getElementById('filename').textContent = 'untitled.ans';
  document.getElementById('s-name').textContent = 'untitled.ans';
  document.getElementById('s-size').textContent = '—';
  document.getElementById('s-type').textContent = 'new';
  document.getElementById('s-dims').textContent = cols + '×' + rows;
  document.getElementById('width-wrap').classList.add('visible');
  tabs[activeTab].name = 'untitled.ans';
  renderTabs();
  if (!editMode) toggleEdit();
  curRow = 0; curCol = 0;
  updateCursor();
}

// ── New menu ──────────────────────────────────────────────────────────────────
function toggleNewMenu() {
  document.getElementById('new-menu').classList.toggle('visible');
}
function hideNewMenu() {
  document.getElementById('new-menu').classList.remove('visible');
}
document.addEventListener('click', e => {
  if (!document.getElementById('new-wrap').contains(e.target)) hideNewMenu();
});

function textToCP437Bytes(text) {
  // Unescape common escape-sequence notations (\x1b, \033) from source/doc pastes
  text = text.replace(/\\x1b/gi, '\x1b').replace(/\\033/g, '\x1b');
  const out = [];
  for (const ch of text) {
    const cp = ch.codePointAt(0);
    if (cp < 128) {
      out.push(cp);
    } else {
      const b = CP437_REV.get(cp);
      out.push(b !== undefined ? b : 0x3F);
    }
  }
  return new Uint8Array(out);
}

function _loadFromBytes(bytes, cols, name) {
  sparseMap = new Map();
  canvasCols = cols;
  parse(bytes, cols);
  const { maxRow: mr } = getBounds(sparseMap);
  canvasRows = Math.max(mr + 1, 5);
  lastFile = null; lastBytes = bytes;
  undoStack = []; redoStack = [];
  drawAll();
  showOutput();
  document.getElementById('filename').textContent = name;
  document.getElementById('s-name').textContent = name;
  document.getElementById('s-size').textContent = fmt(bytes.length);
  const hasColor = [...sparseMap.values()].some(c => c.fg !== 7 || c.bg >= 0);
  document.getElementById('s-type').textContent = hasColor ? 'ANSI color' : 'plain';
  const { maxRow, maxCol } = getBounds(sparseMap);
  document.getElementById('s-dims').textContent = `${maxCol+1}×${maxRow+1}`;
  document.getElementById('width-wrap').classList.add('visible');
  tabs[activeTab].name = name;
  renderTabs();
  if (!editMode) toggleEdit();
  curRow = 0; curCol = 0;
  updateCursor();
}

async function newFromClipboard() {
  // Read clipboard BEFORE hiding the menu so the user-gesture context is intact
  let text = '';
  try { text = await navigator.clipboard.readText(); } catch(e) {}
  hideNewMenu();
  if (!text.trim()) {
    showNewAnsiDlg();
    return;
  }
  const stripped = text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '');
  let maxLen = 0;
  stripped.split('\n').forEach(l => maxLen = Math.max(maxLen, l.replace(/\r$/, '').length));
  const cols = Math.min(Math.max(maxLen || 80, 40), 320);
  _loadFromBytes(textToCP437Bytes(text), cols, 'clipboard.ans');
}

function showNewAnsiDlg() {
  document.getElementById('overlay').classList.add('visible');
  document.getElementById('new-ansi-dlg').classList.add('visible');
  document.getElementById('na-text').focus();
}
function hideNewAnsiDlg() {
  document.getElementById('overlay').classList.remove('visible');
  document.getElementById('new-ansi-dlg').classList.remove('visible');
}
function createFromAnsiCode() {
  const cols = parseInt(document.getElementById('na-cols').value, 10) || 80;
  const text = document.getElementById('na-text').value;
  hideNewAnsiDlg();
  if (!text.trim()) return;
  _loadFromBytes(textToCP437Bytes(text), cols, 'ansi.ans');
}

// ── Center / font size ────────────────────────────────────────────────────────
function deleteRow() {
  pushUndo();
  const newMap = new Map();
  for (const [k, v] of sparseMap) {
    const r = Math.floor(k / 100000), c = k % 100000;
    if (r === curRow) continue;
    newMap.set((r < curRow ? r : r - 1) * 100000 + c, v);
  }
  sparseMap = newMap;
  curRow = Math.max(0, curRow - (curRow > 0 ? 0 : 0));
  drawAll();
}

function insertRow() {
  pushUndo();
  const newMap = new Map();
  for (const [k, v] of sparseMap) {
    const r = Math.floor(k / 100000), c = k % 100000;
    newMap.set((r < curRow ? r : r + 1) * 100000 + c, v);
  }
  sparseMap = newMap;
  drawAll();
  updateCursor();
}

function toggleCenter() {
  isCentered = !isCentered;
  document.getElementById('output-wrap').classList.toggle('centered', isCentered);
  document.getElementById('center-btn').classList.toggle('active', isCentered);
  scheduleSessionSave();
}
function adjFont(delta) {
  const steps = [0.5,0.75,1,1.25,1.5,2,2.5,3];
  let idx = steps.findIndex(s => Math.abs(s - fontScale) < 0.01);
  if (idx < 0) idx = 2;
  idx = Math.max(0, Math.min(steps.length-1, idx + delta));
  fontScale = steps[idx];
  if (sparseMap.size > 0 || canvasRows > 0) drawAll();
}

// ── Export HTML ───────────────────────────────────────────────────────────────
function buildHtml(map) {
  if (map.size === 0) return '';
  const { maxRow, maxCol } = getBounds(map);
  const lines = [];
  for (let r = 0; r <= maxRow; r++) {
    let rowMax = -1;
    for (const k of map.keys())
      if (Math.floor(k/100000)===r) rowMax = Math.max(rowMax, k%100000);
    if (rowMax < 0) { lines.push(''); continue; }
    let html = '', curFg = null, curBg = null, spanOpen = false;
    for (let c = 0; c <= rowMax; c++) {
      let ch2 = ' ', fgI = 7, bgI = -1;
      const cell = map.get(r*100000+c);
      if (cell) { ch2=cell.ch; fgI=cell.fg; bgI=cell.bg; }
      const needStyle = fgI!==7||bgI>=0;
      if (fgI!==curFg||bgI!==curBg) {
        if (spanOpen) { html+='</span>'; spanOpen=false; }
        if (needStyle) {
          let st=`color:${PALETTE[fgI]};`;
          if (bgI>=0) st+=`background:${PALETTE[bgI]};`;
          html+=`<span style="${st}">`; spanOpen=true;
        }
        curFg=fgI; curBg=bgI;
      }
      if (ch2==='&') html+='&amp;';
      else if (ch2==='<') html+='&lt;';
      else if (ch2==='>') html+='&gt;';
      else html+=ch2;
    }
    if (spanOpen) html+='</span>';
    lines.push(html);
  }
  return lines.join('\n');
}

function exportHTML() {
  const content = buildHtml(sparseMap);
  const fname = lastFile ? lastFile.name.replace(/\.[^.]+$/,'') : 'untitled';
  const fs = Math.round(fontScale*16)+'px';
  const centered = isCentered ? 'display:flex;justify-content:center' : '';
  const blob = new Blob([`<!DOCTYPE html><html><head><meta charset="UTF-8"><title>${fname}</title>
<style>*{margin:0;padding:0}body{background:#000;padding:20px}
#w{${centered}}#s{display:inline-block;font-family:"Courier New",monospace;font-size:${fs};line-height:1;white-space:pre;color:#aaaaaa}
</style></head><body><div id="w"><div id="s">${content}</div></div></body></html>`],
    {type:'text/html'});
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = fname + '.html';
  a.click();
}

// ── Export menu ───────────────────────────────────────────────────────────────
function toggleExportMenu() {
  document.getElementById('export-menu').classList.toggle('visible');
}
function hideExportMenu() {
  document.getElementById('export-menu').classList.remove('visible');
}
document.addEventListener('click', e => {
  if (!document.getElementById('export-wrap').contains(e.target))
    hideExportMenu();
});

// ── Export NFO (plain text, no ANSI) ─────────────────────────────────────────
function exportNFO() {
  if (sparseMap.size === 0) return;
  const { maxRow } = getBounds(sparseMap);
  const rowMaxCol = new Array(maxRow + 1).fill(-1);
  for (const k of sparseMap.keys()) {
    const r = Math.floor(k / 100000), c = k % 100000;
    if (c > rowMaxCol[r]) rowMaxCol[r] = c;
  }
  const bytes = [];
  for (let r = 0; r <= maxRow; r++) {
    const lastC = rowMaxCol[r];
    if (lastC < 0) { bytes.push(0x0d, 0x0a); continue; }
    for (let c = 0; c <= lastC; c++) {
      const cell = sparseMap.get(r * 100000 + c);
      bytes.push(CP437_REV.get((cell ? cell.ch : ' ').codePointAt(0)) ?? 0x20);
    }
    if (r < maxRow) bytes.push(0x0d, 0x0a);
  }
  const fname = lastFile ? lastFile.name.replace(/\.[^.]+$/, '') : 'untitled';
  const a = document.createElement('a');
  a.href = URL.createObjectURL(new Blob([new Uint8Array(bytes)], {type: 'application/octet-stream'}));
  a.download = fname + '.nfo';
  a.click();
}

// ── Export ANS ────────────────────────────────────────────────────────────────
function buildANSBytes() {
  if (sparseMap.size === 0) return null;
  const { maxRow } = getBounds(sparseMap);
  const rowMaxCol = new Array(maxRow+1).fill(-1);
  for (const k of sparseMap.keys()) {
    const r=Math.floor(k/100000), c=k%100000;
    if (c>rowMaxCol[r]) rowMaxCol[r]=c;
  }
  const bytes = [];
  function pushStr(s) { for (let i=0;i<s.length;i++) bytes.push(s.charCodeAt(i)); }
  function emitColor(fg,bg) {
    pushStr('\x1b[0m');
    if (fg <= 7)       { if (fg!==7) pushStr(`\x1b[${30+fg}m`); }
    else if (fg <= 15) { pushStr(`\x1b[1;${30+fg-8}m`); }
    else               { pushStr(`\x1b[38;5;${fg}m`); }
    if (bg >= 0) {
      if (bg <= 7)       pushStr(`\x1b[${40+bg}m`);
      else if (bg <= 15) pushStr(`\x1b[${100+(bg-8)}m`);
      else               pushStr(`\x1b[48;5;${bg}m`);
    }
  }
  let curFg=7, curBg=-1;
  emitColor(7,-1);
  for (let r=0;r<=maxRow;r++) {
    const lastC=rowMaxCol[r];
    if (lastC<0) { pushStr('\r\n'); continue; }
    for (let c=0;c<=lastC;c++) {
      const cell=sparseMap.get(r*100000+c);
      const fg=cell?cell.fg:7, bg=cell?cell.bg:-1, ch2=cell?cell.ch:' ';
      if (fg!==curFg||bg!==curBg) { emitColor(fg,bg); curFg=fg; curBg=bg; }
      bytes.push(CP437_REV.get(ch2.codePointAt(0)) ?? 0x20);
    }
    if (r<maxRow) pushStr('\r\n');
  }
  return new Uint8Array(bytes);
}

function exportANS() {
  const bytes = buildANSBytes();
  if (!bytes) return;
  const fname = lastFile ? lastFile.name.replace(/\.[^.]+$/,'') : 'untitled';
  const a = document.createElement('a');
  a.href = URL.createObjectURL(new Blob([bytes],{type:'application/octet-stream'}));
  a.download = fname + '.ans';
  a.click();
}

async function openWithPicker() {
  if (!window.showOpenFilePicker) {
    document.getElementById('file-input').click();
    return;
  }
  try {
    const [handle] = await window.showOpenFilePicker({
      types: [{ description: 'ANS / NFO / TXT', accept: { 'application/octet-stream': ['.nfo','.ans','.diz','.asc','.txt','.ansi'] } }],
      multiple: false
    });
    lastFileHandle = handle;
    processFile(await handle.getFile());
  } catch(e) { /* avbröts */ }
}

function flashSaved() {
  const el = document.getElementById('filename');
  const orig = el.textContent;
  el.textContent = '✓ SAVED';
  el.style.color = 'var(--cyan)';
  setTimeout(() => { el.textContent = orig; el.style.color = ''; }, 1500);
}

async function saveANS() {
  const bytes = buildANSBytes();
  if (!bytes) return;
  if (lastFileHandle) {
    try {
      const w = await lastFileHandle.createWritable();
      await w.write(bytes);
      await w.close();
      flashSaved();
      return;
    } catch(e) { /* tillstånd nekat, fortsätt till dialog */ }
  }
  if (window.showSaveFilePicker) {
    try {
      const fname = lastFile ? lastFile.name : 'untitled.ans';
      const handle = await window.showSaveFilePicker({
        suggestedName: fname,
        types: [{ description: 'ANS file', accept: { 'application/octet-stream': ['.ans'] } }]
      });
      lastFileHandle = handle;
      const w = await handle.createWritable();
      await w.write(bytes);
      await w.close();
      flashSaved();
      return;
    } catch(e) { /* avbröts */ }
  }
  exportANS();
}

// ── Image → ANSI ─────────────────────────────────────────────────────────────
let imgSrc = null;

document.getElementById('img-input').addEventListener('change', function () {
  if (!this.files[0]) return;
  const reader = new FileReader();
  reader.onload = ev => {
    const img = new Image();
    img.onload = () => {
      imgSrc = img;
      imgAutoRows();
      // Show tiny preview
      const prev = document.getElementById('img-preview-wrap');
      prev.innerHTML = '';
      const thumb = document.createElement('canvas');
      const scale = Math.min(240 / img.width, 80 / img.height, 1);
      thumb.width  = Math.round(img.width  * scale);
      thumb.height = Math.round(img.height * scale);
      thumb.id = 'img-preview';
      thumb.getContext('2d').drawImage(img, 0, 0, thumb.width, thumb.height);
      prev.appendChild(thumb);
      showImgDlg();
    };
    img.src = ev.target.result;
  };
  reader.readAsDataURL(this.files[0]);
  this.value = '';
});

const ASCII_RAMP = " .:-=+*#%@";

function imgAsciiToggle() {
  const ascii = document.getElementById('img-ascii').checked;
  const halfRow = document.getElementById('img-half-row');
  halfRow.style.opacity = ascii ? '0.35' : '';
  document.getElementById('img-half').disabled = ascii;
}

function imgAutoRows() {
  if (!imgSrc) return;
  if (!document.getElementById('img-aspect').checked) return;
  const cols = parseInt(document.getElementById('img-cols').value, 10) || 80;
  const half = document.getElementById('img-half').checked;
  const aspect = imgSrc.width / imgSrc.height;
  // char cell is CHAR_W × CHAR_H; half-block = CHAR_W × (CHAR_H/2) effective pixel
  const charAspect = half ? CHAR_W / (CHAR_H / 2) : CHAR_W / CHAR_H;
  const rows = Math.max(1, Math.round(cols / aspect / charAspect));
  document.getElementById('img-rows').value = rows;
}

function showImgDlg() {
  document.getElementById('overlay').classList.add('visible');
  document.getElementById('img-dlg').classList.add('visible');
}
function hideImgDlg() {
  document.getElementById('overlay').classList.remove('visible');
  document.getElementById('img-dlg').classList.remove('visible');
}

function doImgConvert() {
  if (!imgSrc) return;
  const cols   = parseInt(document.getElementById('img-cols').value,   10) || 80;
  const rows   = parseInt(document.getElementById('img-rows').value,   10) || 40;
  const ascii  = document.getElementById('img-ascii').checked;
  const half   = !ascii && document.getElementById('img-half').checked;
  const use256 = document.getElementById('img-256').checked;
  const dither = document.getElementById('img-dither').checked;
  hideImgDlg();

  const srcH = half ? rows * 2 : rows;
  const off  = document.createElement('canvas');
  off.width  = cols;
  off.height = srcH;
  const oc   = off.getContext('2d');
  oc.drawImage(imgSrc, 0, 0, cols, srcH);
  const raw  = oc.getImageData(0, 0, cols, srcH).data;
  // Mutable float copy for dithering
  const pix  = new Float32Array(raw);

  const palRGB = PALETTE.map(h => [
    parseInt(h.slice(1,3),16), parseInt(h.slice(3,5),16), parseInt(h.slice(5,7),16)
  ]);

  const colorLimit = use256 ? 256 : 16;
  function nearest(r,g,b) {
    let best=0, bestD=Infinity;
    for (let i=0;i<colorLimit;i++) {
      const [pr,pg,pb]=palRGB[i];
      const d=(r-pr)**2+(g-pg)**2+(b-pb)**2;
      if (d<bestD) { bestD=d; best=i; }
    }
    return best;
  }
  function getP(x,y) {
    const i=(y*cols+x)*4;
    return [Math.max(0,Math.min(255,pix[i])),Math.max(0,Math.min(255,pix[i+1])),Math.max(0,Math.min(255,pix[i+2]))];
  }
  function addErr(x,y,er,eg,eb,f) {
    if (x<0||x>=cols||y<0||y>=srcH) return;
    const i=(y*cols+x)*4; pix[i]+=er*f; pix[i+1]+=eg*f; pix[i+2]+=eb*f;
  }

  pushUndo();
  sparseMap = new Map();

  for (let row=0; row<rows; row++) {
    for (let col=0; col<cols; col++) {
      if (ascii) {
        const [r,g,b] = getP(col, row);
        const fg = nearest(r,g,b);
        if (dither) {
          const [pr,pg,pb]=palRGB[fg]; const er=r-pr,eg=g-pg,eb=b-pb;
          addErr(col+1,row,  er,eg,eb,7/16); addErr(col-1,row+1,er,eg,eb,3/16);
          addErr(col,  row+1,er,eg,eb,5/16); addErr(col+1,row+1,er,eg,eb,1/16);
        }
        const lum = (0.299*r + 0.587*g + 0.114*b) / 255;
        const ch  = ASCII_RAMP[Math.round(lum * (ASCII_RAMP.length - 1))];
        if (ch !== ' ') sparseMap.set(row*100000+col, { ch, fg, bg:-1 });
      } else if (half) {
        const [r1,g1,b1] = getP(col, row*2);
        const [r2,g2,b2] = getP(col, row*2+1);
        const fg = nearest(r1,g1,b1);
        const bg = nearest(r2,g2,b2);
        if (dither) {
          const [p1r,p1g,p1b]=palRGB[fg]; const e1r=r1-p1r,e1g=g1-p1g,e1b=b1-p1b;
          addErr(col+1,row*2,  e1r,e1g,e1b,7/16); addErr(col-1,row*2+1,e1r,e1g,e1b,3/16);
          addErr(col,  row*2+1,e1r,e1g,e1b,5/16); addErr(col+1,row*2+1,e1r,e1g,e1b,1/16);
          const [p2r,p2g,p2b]=palRGB[bg];  const e2r=r2-p2r,e2g=g2-p2g,e2b=b2-p2b;
          addErr(col+1,row*2+2,e2r,e2g,e2b,7/16); addErr(col-1,row*2+3,e2r,e2g,e2b,3/16);
          addErr(col,  row*2+3,e2r,e2g,e2b,5/16); addErr(col+1,row*2+3,e2r,e2g,e2b,1/16);
        }
        sparseMap.set(row*100000+col, { ch:'\u2580', fg, bg }); // ▀
      } else {
        const [r,g,b] = getP(col, row);
        const fg = nearest(r,g,b);
        if (dither) {
          const [pr,pg,pb]=palRGB[fg]; const er=r-pr,eg=g-pg,eb=b-pb;
          addErr(col+1,row,  er,eg,eb,7/16); addErr(col-1,row+1,er,eg,eb,3/16);
          addErr(col,  row+1,er,eg,eb,5/16); addErr(col+1,row+1,er,eg,eb,1/16);
        }
        sparseMap.set(row*100000+col, { ch:'\u2588', fg, bg:-1 }); // █
      }
    }
  }

  canvasCols = cols; canvasRows = rows;
  lastFile = null; lastBytes = null;
  drawAll();
  showOutput();
  document.getElementById('filename').textContent = 'image-convert.ans';
  document.getElementById('s-name').textContent   = 'image-convert.ans';
  document.getElementById('s-size').textContent   = '—';
  document.getElementById('s-type').textContent   = 'ANSI color';
  document.getElementById('s-dims').textContent   = `${cols}×${rows}`;
  tabs[activeTab].name = 'image-convert.ans';
  renderTabs();
  if (!editMode) toggleEdit();
}

// ── Session persistence ───────────────────────────────────────────────────────
function u8ToB64(arr) {
  let s = '';
  for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]);
  return btoa(s);
}
function b64ToU8(s) {
  const b = atob(s), a = new Uint8Array(b.length);
  for (let i = 0; i < b.length; i++) a[i] = b.charCodeAt(i);
  return a;
}

function scheduleSessionSave() {
  clearTimeout(_saveTimer);
  _saveTimer = setTimeout(saveSession, 1000);
}

function saveSession() {
  saveCurrentTab();
  try {
    const data = {
      activeTab,
      tabs: tabs.map(t => ({
        name: t.name,
        sparseMap: [...t.sparseMap.entries()],
        lastBytes: t.lastBytes ? u8ToB64(t.lastBytes) : null,
        canvasCols: t.canvasCols,
        canvasRows: t.canvasRows,
        canvasBg: t.canvasBg,
        isCentered: t.isCentered,
      }))
    };
    localStorage.setItem('nfoed_session', JSON.stringify(data));
  } catch(e) {
    console.warn('Session save failed:', e);
  }
}

function loadSession() {
  try {
    const raw = localStorage.getItem('nfoed_session');
    if (!raw) return false;
    const data = JSON.parse(raw);
    if (!data.tabs || !data.tabs.length) return false;
    tabs = data.tabs.map(t => ({
      name: t.name,
      sparseMap: new Map(t.sparseMap),
      lastFile: null,
      lastBytes: t.lastBytes ? b64ToU8(t.lastBytes) : null,
      canvasCols: t.canvasCols || 80,
      canvasRows: t.canvasRows || 25,
      canvasBg: t.canvasBg || '#000000',
      isCentered: t.isCentered || false,
      undoStack: [],
      redoStack: [],
    }));
    activeTab = Math.min(data.activeTab || 0, tabs.length - 1);
    const t = tabs[activeTab];
    sparseMap = t.sparseMap;
    lastFile = null;
    lastBytes = t.lastBytes;
    canvasCols = t.canvasCols;
    canvasRows = t.canvasRows;
    canvasBg = t.canvasBg;
    isCentered = t.isCentered;
    undoStack = []; redoStack = [];
    document.getElementById('canvas-bg-swatch').style.background = canvasBg;
    document.getElementById('output-wrap').classList.toggle('centered', isCentered);
    document.getElementById('center-btn').classList.toggle('active', isCentered);
    if (sparseMap.size > 0) {
      showOutput();
      document.getElementById('filename').textContent = t.name;
      document.getElementById('s-name').textContent = t.name;
      document.getElementById('width-wrap').classList.add('visible');
      const { maxRow, maxCol } = getBounds(sparseMap);
      document.getElementById('s-dims').textContent = `${maxCol+1}×${maxRow+1}`;
      document.getElementById('s-type').textContent = lastBytes ? 'restored' : 'new';
      document.getElementById('s-size').textContent = lastBytes ? fmt(lastBytes.length) : '—';
      document.fonts.ready.then(() => drawAll());
    }
    renderTabs();
    return true;
  } catch(e) {
    console.warn('Session load failed:', e);
    return false;
  }
}

// ── Tab bar delegation ────────────────────────────────────────────────────────
document.getElementById('tabbar').addEventListener('click', e => {
  const closeBtn = e.target.closest('[data-close-idx]');
  if (closeBtn) { e.stopPropagation(); closeTab(parseInt(closeBtn.dataset.closeIdx)); return; }
  if (e.target.closest('[data-tab-add]')) { addTab(); return; }
  const tabBtn = e.target.closest('[data-tab-idx]');
  if (tabBtn) switchTab(parseInt(tabBtn.dataset.tabIdx));
});

// ── Drop / file events ────────────────────────────────────────────────────────
const dz = document.getElementById('dropzone');
dz.addEventListener('dragover', e=>{e.preventDefault();dz.classList.add('over')});
dz.addEventListener('dragleave', ()=>dz.classList.remove('over'));
dz.addEventListener('drop', e=>{e.preventDefault();dz.classList.remove('over');const f=e.dataTransfer.files[0];if(f){lastFileHandle=null;processFile(f);}});
document.getElementById('file-input').addEventListener('change', function(){if(this.files[0]){lastFileHandle=null;processFile(this.files[0]);}});
document.addEventListener('dragover', e=>e.preventDefault());
document.addEventListener('drop', e=>{e.preventDefault();const f=e.dataTransfer.files[0];if(f){lastFileHandle=null;processFile(f);}});

loadSession();
</script>
</body>
</html>