SIMULATOR_HTML = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>Cardisk USSD Simulator</title>
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=DM+Sans:wght@300;400;500;600&family=Syne:wght@700;800&display=swap" rel="stylesheet"/>
<style>
  :root {
    --black: #ffffff;
    --surface: #f4f6fb;
    --panel: #eef1f8;
    --border: #d6dae8;
    --accent: #0077ff;
    --accent2: #6c47ff;
    --warn: #e03e3e;
    --text: #111827;
    --muted: #7a85a3;
    --phone-bg: #e8ecf5;
    --screen-bg: #1a3a6b;
    --mono: 'Share Tech Mono', monospace;
    --sans: 'DM Sans', sans-serif;
    --display: 'Syne', sans-serif;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  body{background:var(--surface);color:var(--text);font-family:var(--sans);min-height:100vh;overflow-x:hidden}

  /* Subtle dot pattern overlay */
  body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(circle, #c8d0e8 1px, transparent 1px);background-size:28px 28px;pointer-events:none;z-index:0;opacity:.35}

  header{padding:20px 32px;display:flex;align-items:center;gap:14px;border-bottom:1px solid var(--border);background:var(--black);position:relative;z-index:1;box-shadow:0 1px 3px rgba(0,0,0,.06)}
  .logo{width:36px;height:36px;background:var(--accent);border-radius:8px;display:flex;align-items:center;justify-content:center}
  .logo svg{width:20px;height:20px;fill:#fff;stroke:none}
  header h1{font-family:var(--display);font-size:18px;font-weight:800;letter-spacing:-0.5px;color:var(--text)}
  header h1 span{color:var(--accent)}
  .header-badge{margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--accent);border:1px solid var(--accent);padding:3px 10px;border-radius:999px;letter-spacing:1px}

  main{display:grid;grid-template-columns:280px 1fr 340px;gap:0;min-height:calc(100vh - 65px);position:relative;z-index:1}

  /* LEFT PANEL — Config */
  .config-panel{border-right:1px solid var(--border);padding:24px 20px;display:flex;flex-direction:column;gap:6px;background:var(--black)}
  .config-panel h2{font-family:var(--display);font-size:11px;font-weight:700;letter-spacing:2px;color:var(--muted);text-transform:uppercase;margin-bottom:10px}
  .field{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
  .field label{font-size:11px;color:var(--muted);letter-spacing:.5px;font-weight:500}
  .field input{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:10px 12px;border-radius:8px;font-family:var(--mono);font-size:12px;outline:none;transition:border-color .2s;box-shadow:inset 0 1px 2px rgba(0,0,0,.04)}
  .field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(0,119,255,.1)}
  .field input::placeholder{color:var(--muted)}

  .session-id-row{display:flex;gap:6px;align-items:center}
  .session-id-row input{flex:1}
  .icon-btn{background:var(--surface);border:1px solid var(--border);color:var(--muted);width:36px;height:36px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0}
  .icon-btn:hover{border-color:var(--accent);color:var(--accent);background:#eef4ff}

  .divider{height:1px;background:var(--border);margin:8px 0}

  .btn-dial{background:var(--accent);color:#fff;border:none;padding:12px;border-radius:10px;font-family:var(--display);font-size:14px;font-weight:700;cursor:pointer;letter-spacing:.5px;transition:all .2s;width:100%;box-shadow:0 4px 12px rgba(0,119,255,.3)}
  .btn-dial:hover{background:#0066dd;transform:translateY(-1px);box-shadow:0 6px 16px rgba(0,119,255,.35)}
  .btn-dial:active{transform:translateY(0)}
  .btn-reset{background:transparent;color:var(--warn);border:1px solid var(--warn);padding:10px;border-radius:10px;font-family:var(--sans);font-size:13px;font-weight:500;cursor:pointer;width:100%;margin-top:8px;transition:all .2s}
  .btn-reset:hover{background:var(--warn);color:white}

  .session-info{margin-top:auto;padding:12px;background:var(--panel);border:1px solid var(--border);border-radius:10px}
  .session-info .si-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
  .si-label{font-size:10px;color:var(--muted)}
  .si-val{font-family:var(--mono);font-size:10px;color:var(--accent)}
  .status-dot{width:8px;height:8px;border-radius:50%;background:var(--muted);display:inline-block;margin-right:6px;transition:background .3s}
  .status-dot.active{background:#00b86b;box-shadow:0 0 6px #00b86b}
  .status-dot.ended{background:var(--warn)}

  /* CENTER — Phone */
  .phone-area{display:flex;align-items:center;justify-content:center;padding:32px;background:radial-gradient(ellipse at 50% 0%, #dde8ff 0%, var(--surface) 70%)}
  .phone-wrap{position:relative}
  .phone{width:280px;background:#f0f3fa;border-radius:38px;border:2px solid #c8d0e4;box-shadow:0 0 0 1px #e0e6f4, 0 40px 80px rgba(100,120,200,.18), inset 0 1px 0 rgba(255,255,255,.9);overflow:hidden;padding:10px}
  .phone-notch{width:80px;height:6px;background:#c8d0e4;border-radius:3px;margin:0 auto 10px}

  .phone-screen{background:var(--screen-bg);border-radius:26px;overflow:hidden;min-height:380px;display:flex;flex-direction:column;border:1px solid #0f2a52}
  .screen-bar{background:#112244;padding:8px 14px;display:flex;justify-content:space-between;align-items:center}
  .screen-carrier{font-family:var(--mono);font-size:9px;color:#6aadff;letter-spacing:1px}
  .screen-time{font-family:var(--mono);font-size:9px;color:#6aadff}

  .screen-content{flex:1;padding:16px;display:flex;flex-direction:column;gap:12px}
  .screen-title{font-size:9px;color:#6aadff;letter-spacing:2px;text-transform:uppercase;font-family:var(--mono)}

  .screen-text{font-family:var(--mono);font-size:12px;color:#c8e4ff;line-height:1.7;white-space:pre-wrap;flex:1;min-height:120px}
  .cursor{display:inline-block;width:6px;height:12px;background:#6aadff;animation:blink 1s steps(1) infinite;margin-left:2px;vertical-align:middle}
  @keyframes blink{0%,100%{opacity:1}50%{opacity:0}}

  .input-display{background:#0f2244;border:1px solid #1e3a6e;border-radius:8px;padding:8px 10px;font-family:var(--mono);font-size:14px;color:#6aadff;min-height:34px;letter-spacing:2px}
  .input-display.empty{color:#3a5a88;font-size:11px;letter-spacing:0}

  .phone-bottom{padding:10px 4px 6px}
  .keypad{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
  .key{background:#fff;border:1px solid #d0d8ee;border-radius:12px;padding:12px 8px;text-align:center;cursor:pointer;transition:all .12s;user-select:none;position:relative;overflow:hidden;box-shadow:0 2px 4px rgba(100,120,200,.08)}
  .key::after{content:'';position:absolute;inset:0;background:var(--accent);opacity:0;transition:opacity .15s;border-radius:12px}
  .key:active::after{opacity:.1}
  .key:hover{border-color:#a8b8e8;background:#f4f7ff;box-shadow:0 4px 8px rgba(100,120,200,.14)}
  .key .num{font-family:var(--mono);font-size:16px;color:#1a2a5a;display:block}
  .key .alpha{font-size:7px;color:var(--muted);display:block;letter-spacing:1px;margin-top:1px}
  .key.send{background:linear-gradient(135deg,#0055cc,#0077ff);border-color:#0066ee;box-shadow:0 4px 10px rgba(0,100,255,.25)}
  .key.send .num{color:#fff}
  .key.send .alpha{color:rgba(255,255,255,.6)}
  .key.del{background:#fff5f5;border-color:#f0c0c0}
  .key.del .num{color:#cc3333;font-size:14px}
  .key.star .num, .key.hash .num{font-size:18px}

  .phone-home{width:60px;height:4px;background:#c8d0e4;border-radius:2px;margin:10px auto 0}

  /* RIGHT PANEL — Logs */
  .logs-panel{border-left:1px solid var(--border);display:flex;flex-direction:column;background:var(--black)}
  .logs-header{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between}
  .logs-header h2{font-family:var(--display);font-size:11px;font-weight:700;letter-spacing:2px;color:var(--muted);text-transform:uppercase}
  .clear-btn{font-size:10px;color:var(--muted);background:none;border:1px solid var(--border);padding:3px 10px;border-radius:5px;cursor:pointer;font-family:var(--mono);transition:all .2s}
  .clear-btn:hover{color:var(--warn);border-color:var(--warn)}

  .route-tracker{padding:12px 20px;border-bottom:1px solid var(--border);min-height:44px;display:flex;align-items:center;gap:6px;flex-wrap:wrap}
  .route-crumb{font-family:var(--mono);font-size:10px;background:#eef2ff;border:1px solid #c8d0ee;padding:3px 8px;border-radius:5px;color:var(--accent2)}
  .route-arrow{color:var(--muted);font-size:10px}

  .logs-list{flex:1;overflow-y:auto;padding:12px}
  .logs-list::-webkit-scrollbar{width:4px}
  .logs-list::-webkit-scrollbar-track{background:transparent}
  .logs-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}

  .log-entry{margin-bottom:12px;border:1px solid var(--border);border-radius:10px;overflow:hidden;animation:slideIn .2s ease;background:#fff}
  @keyframes slideIn{from{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:translateY(0)}}
  .log-header{padding:8px 12px;background:var(--panel);display:flex;align-items:center;gap:8px;font-size:10px;font-family:var(--mono)}
  .log-badge{padding:2px 7px;border-radius:4px;font-size:9px;letter-spacing:1px;font-weight:600}
  .log-badge.req{background:#eeeaff;color:var(--accent2)}
  .log-badge.res{background:#e6f4ee;color:#007a45}
  .log-badge.err{background:#fdeaea;color:var(--warn)}
  .log-type-badge{padding:2px 7px;border-radius:4px;font-size:9px;background:transparent;border:1px solid var(--border);color:var(--muted)}
  .log-time{margin-left:auto;color:var(--muted);font-size:9px}
  .log-body{padding:10px 12px;display:flex;flex-direction:column;gap:8px}
  .log-section{font-size:9px;color:var(--muted);letter-spacing:1px;text-transform:uppercase;margin-bottom:2px}
  .log-xml{font-family:var(--mono);font-size:10px;color:#445;background:#f8f9fc;padding:8px;border-radius:6px;white-space:pre-wrap;word-break:break-all;border:1px solid var(--border)}
  .log-xml .xml-tag{color:#6c47ff}
  .log-xml .xml-val{color:#007a45}
  .log-xml .xml-end{color:#cc3333}

  .empty-logs{display:flex;align-items:center;justify-content:center;height:100%;flex-direction:column;gap:8px;color:var(--muted)}
  .empty-logs svg{opacity:.25;width:40px;height:40px}
  .empty-logs p{font-size:12px;font-family:var(--mono)}

  .field input.invalid{border-color:#e03e3e!important;background:#fff8f8;box-shadow:0 0 0 3px rgba(224,62,62,.1)!important}
  .url-hint{font-size:10px;color:#e03e3e;font-family:var(--mono);margin-top:4px;display:none}
  .url-hint.show{display:block}
  @keyframes ripple{to{transform:scale(3);opacity:0}}
  .ripple-effect{position:absolute;border-radius:50%;background:var(--accent);animation:ripple .4s ease-out;pointer-events:none;width:40px;height:40px;margin:-20px;opacity:.15}
</style>
</head>
<body>

<header>
  <div class="logo">
    <svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
  </div>
  <h1>Cardisk <span>USSD</span> Simulator</h1>
  <div class="header-badge">v1.0</div>
</header>

<main>
  <!-- CONFIG -->
  <aside class="config-panel">
    <h2>Connection</h2>

    <div class="field">
      <label>ENDPOINT URL</label>
      <input id="cfg-url" value="http://localhost:8000/" placeholder="http://localhost:8000/" oninput="validateUrl(this)"/>
      <span class="url-hint" id="url-hint">⚠ Invalid URL format</span>
    </div>
    <div class="field">
      <label>MSISDN (Phone)</label>
      <input id="cfg-msisdn" value="254712345678" placeholder="254712345678"/>
    </div>
    <div class="field">
      <label>SHORT CODE</label>
      <input id="cfg-shortcode" value="*123#" placeholder="*123#"/>
    </div>
    <div class="field">
      <label>SESSION ID</label>
      <div class="session-id-row">
        <input id="cfg-session" placeholder="Auto-generated"/>
        <button class="icon-btn" onclick="generateSession()" title="Generate">
          <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M1 4v6h6M23 20v-6h-6"/><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10M23 14l-4.64 4.36A9 9 0 0 1 3.51 15"/></svg>
        </button>
      </div>
    </div>

    <div class="divider"></div>

    <button class="btn-dial" onclick="dialUSSD()">📞 Dial</button>
    <button class="btn-reset" onclick="resetAll()">↺ Reset Session</button>

    <div style="height:1px;background:var(--border);margin-top:16px"></div>

    <div class="session-info" style="margin-top:16px">
      <div class="si-row">
        <span class="si-label"><span class="status-dot" id="status-dot"></span>STATUS</span>
        <span class="si-val" id="si-status">IDLE</span>
      </div>
      <div class="si-row">
        <span class="si-label">SESSION</span>
        <span class="si-val" id="si-session">—</span>
      </div>
      <div class="si-row">
        <span class="si-label">TYPE</span>
        <span class="si-val" id="si-type">—</span>
      </div>
      <div class="si-row">
        <span class="si-label">STEP</span>
        <span class="si-val" id="si-step">0</span>
      </div>
    </div>
  </aside>

  <!-- PHONE -->
  <div class="phone-area">
    <div class="phone-wrap">
      <div class="phone">
        <div class="phone-notch"></div>
        <div class="phone-screen">
          <div class="screen-bar">
            <span class="screen-carrier" id="screen-carrier">NO SIGNAL</span>
            <span class="screen-time" id="screen-clock">00:00</span>
          </div>
          <div class="screen-content">
            <div class="screen-title">USSD SERVICE</div>
            <div class="screen-text" id="screen-text">Dial to connect...</div>
            <div class="input-display empty" id="input-display">enter option...</div>
          </div>
        </div>
        <div class="phone-bottom">
          <div class="keypad" id="keypad"></div>
        </div>
        <div class="phone-home"></div>
      </div>
    </div>
  </div>

  <!-- LOGS -->
  <aside class="logs-panel">
    <div class="logs-header">
      <h2>Session Logs</h2>
      <button class="clear-btn" onclick="clearLogs()">clear</button>
    </div>
    <div class="route-tracker" id="route-tracker">
      <span style="font-size:11px;color:var(--muted);font-family:var(--mono)">route breadcrumb appears here...</span>
    </div>
    <div class="logs-list" id="logs-list">
      <div class="empty-logs" id="empty-state">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 9h8M8 13h5M3 7c0-1.1.9-2 2-2h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/></svg>
        <p>no logs yet — dial to start</p>
      </div>
    </div>
  </aside>
</main>

<script>
const state = {
  sessionType: 1,   // 1=new, 2=continue
  input: '',
  sessionId: '',
  step: 0,
  active: false,
  ended: false,
  breadcrumbs: []
};

// ── Clock ──────────────────────────────────────────────
function updateClock() {
  const now = new Date();
  document.getElementById('screen-clock').textContent =
    String(now.getHours()).padStart(2,'0') + ':' + String(now.getMinutes()).padStart(2,'0');
}
updateClock();
setInterval(updateClock, 30000);

// ── URL validator ──────────────────────────────────────
function validateUrl(input) {
  const hint = document.getElementById('url-hint');
  try {
    const u = new URL(input.value.trim());
    if (!['http:', 'https:'].includes(u.protocol)) throw new Error();
    input.classList.remove('invalid');
    hint.classList.remove('show');
  } catch(_) {
    input.classList.add('invalid');
    hint.textContent = '⚠ Must start with http:// or https://';
    hint.classList.add('show');
  }
}

// ── Session ID generator ───────────────────────────────
function generateSession() {
  const id = 'SIM-' + Math.random().toString(36).substr(2,9).toUpperCase();
  document.getElementById('cfg-session').value = id;
  return id;
}

// ── Keypad builder ─────────────────────────────────────
const KEYS = [
  ['1',''], ['2','ABC'], ['3','DEF'],
  ['4','GHI'], ['5','JKL'], ['6','MNO'],
  ['7','PQRS'], ['8','TUV'], ['9','WXYZ'],
  ['*',''], ['0','+'], ['#','SEND']
];

function buildKeypad() {
  const kp = document.getElementById('keypad');
  kp.innerHTML = '';
  KEYS.forEach(([num, alpha]) => {
    const key = document.createElement('div');
    key.className = 'key' +
      (num === '#' ? ' send' : '') +
      (num === '*' ? ' star' : '') +
      (alpha === '' && num !== '*' && num !== '#' ? '' : '');
    key.innerHTML = `<span class="num">${num}</span><span class="alpha">${alpha}</span>`;
    key.addEventListener('click', (e) => {
      addRipple(e, key);
      handleKey(num);
    });
    kp.appendChild(key);
  });

  // Add backspace key visually replacing last key slot style
  // Actually keep it simple — long press * or add del functionality via UI
}

function addRipple(e, el) {
  const r = document.createElement('div');
  r.className = 'ripple-effect';
  const rect = el.getBoundingClientRect();
  r.style.left = (e.clientX - rect.left) + 'px';
  r.style.top = (e.clientY - rect.top) + 'px';
  el.appendChild(r);
  setTimeout(() => r.remove(), 400);
}

function handleKey(k) {
  if (k === '#') {
    sendInput();
  } else if (k === '*') {
    // backspace
    state.input = state.input.slice(0, -1);
    updateInputDisplay();
  } else {
    if (state.ended) return;
    state.input += k;
    updateInputDisplay();
  }
}

function updateInputDisplay() {
  const el = document.getElementById('input-display');
  if (state.input) {
    el.textContent = state.input;
    el.classList.remove('empty');
  } else {
    el.textContent = state.ended ? 'session ended' : 'enter option...';
    el.classList.add('empty');
  }
}

// ── XML builder ────────────────────────────────────────
function buildXML(type, msisdn, sessionId, msg) {
  return `<?xml version="1.0" encoding="UTF-8"?>\\n<ussd>\\n  <type>${type}</type>\\n  <msisdn>${msisdn}</msisdn>\\n  <sessionid>${sessionId}</sessionid>\\n  <msg>${escapeXml(msg)}</msg>\\n</ussd>`;
}

function escapeXml(str) {
  return String(str)
    .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
    .replace(/"/g,'&quot;').replace(/'/g,'&apos;');
}

// ── XML parser ─────────────────────────────────────────
function parseResponse(xmlStr) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(xmlStr, 'text/xml');
  const type = doc.querySelector('type')?.textContent?.trim() || '2';
  const msg = doc.querySelector('msg')?.textContent?.trim() || xmlStr;
  return { type, msg, isEnd: type === '3' };
}

// ── Dial ───────────────────────────────────────────────
async function dialUSSD() {
  state.sessionId = document.getElementById('cfg-session').value.trim() || generateSession();
  document.getElementById('cfg-session').value = state.sessionId;
  state.sessionType = 1;
  state.input = document.getElementById('cfg-shortcode').value.trim();
  state.step = 0;
  state.active = true;
  state.ended = false;
  state.breadcrumbs = ['DIAL'];
  updateBreadcrumbs();
  updateStatusUI();
  setScreenText('Connecting...\\u2588');
  await doRequest();
}

// ── Send on # press ────────────────────────────────────
async function sendInput() {
  if (!state.active || state.ended) {
    setScreenText('Dial first ↙');
    return;
  }
  await doRequest();
}

// ── Core request ───────────────────────────────────────
async function doRequest() {
  const url = document.getElementById('cfg-url').value.trim();
  const msisdn = document.getElementById('cfg-msisdn').value.trim();
  const payload = buildXML(state.sessionType, msisdn, state.sessionId, state.input);
  const timestamp = new Date().toLocaleTimeString();

  // Validate URL before fetching
  try { new URL(url); } catch(_) {
    setScreenText(`Invalid URL:\\n"${url}"\\n\\nExample:\\nhttp://localhost:8000/`);
    addLog({ ok: false, req: payload, res: `Invalid URL: "${url}" — check the Endpoint field.`, time: timestamp, typeLabel: 'ERR' });
    document.getElementById('si-status').textContent = 'BAD URL';
    state.active = false;
    return;
  }

  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/xml' },
      body: payload
    });
    const rawText = await res.text();
    const { type, msg, isEnd } = parseResponse(rawText);

    // Update screen
    setScreenText(msg);

    // Breadcrumb
    if (state.input) {
      state.breadcrumbs.push(state.input || '/');
      updateBreadcrumbs();
    }

    // State transition
    state.step++;
    state.sessionType = isEnd ? 1 : 2;
    state.ended = isEnd;
    state.input = '';
    updateInputDisplay();
    updateStatusUI();

    // Update carrier name
    document.getElementById('screen-carrier').textContent = msisdn;

    // Log
    addLog({
      ok: true,
      req: payload,
      res: rawText,
      time: timestamp,
      typeLabel: isEnd ? 'END' : 'CON'
    });

    if (isEnd) {
      setTimeout(() => {
        document.getElementById('si-status').textContent = 'ENDED';
        document.getElementById('status-dot').className = 'status-dot ended';
      }, 300);
    }

  } catch(err) {
    let hint = '';
    if (err.message.includes('fetch') || err.message.includes('Failed to fetch') || err.message.includes('NetworkError')) {
      hint = '\\n\\nPossible causes:\\n• Server not running\\n• Wrong port number\\n• CORS not enabled on server\\n• Try: cardisk runserver app.py';
    }
    setScreenText(`Connection failed:\\n${err.message}${hint}`);
    addLog({ ok: false, req: payload, res: err.message + hint, time: timestamp, typeLabel: 'ERR' });
    state.active = false;
    document.getElementById('si-status').textContent = 'ERROR';
  }
}

// ── Screen text setter ─────────────────────────────────
function setScreenText(txt) {
  const el = document.getElementById('screen-text');
  el.innerHTML = txt + '<span class="cursor"></span>';
}

// ── Breadcrumbs ────────────────────────────────────────
function updateBreadcrumbs() {
  const el = document.getElementById('route-tracker');
  if (!state.breadcrumbs.length) {
    el.innerHTML = '<span style="font-size:11px;color:var(--muted);font-family:var(--mono)">route breadcrumb appears here...</span>';
    return;
  }
  el.innerHTML = state.breadcrumbs.map((c, i) =>
    `<span class="route-crumb">${escapeHtml(c)}</span>${i < state.breadcrumbs.length-1 ? '<span class="route-arrow">›</span>' : ''}`
  ).join('');
}

// ── Status update ──────────────────────────────────────
function updateStatusUI() {
  const dot = document.getElementById('status-dot');
  document.getElementById('si-status').textContent = state.active ? (state.ended ? 'ENDED' : 'ACTIVE') : 'IDLE';
  document.getElementById('si-session').textContent = state.sessionId ? state.sessionId.substring(0,12)+'…' : '—';
  document.getElementById('si-type').textContent = state.sessionType === 1 ? 'NEW (1)' : 'CONT (2)';
  document.getElementById('si-step').textContent = state.step;
  dot.className = 'status-dot' + (state.active && !state.ended ? ' active' : state.ended ? ' ended' : '');
}

// ── Logging ────────────────────────────────────────────
function addLog({ ok, req, res, time, typeLabel }) {
  const emptyState = document.getElementById('empty-state');
  if (emptyState) emptyState.remove();

  const list = document.getElementById('logs-list');
  const entry = document.createElement('div');
  entry.className = 'log-entry';

  entry.innerHTML = `
    <div class="log-header">
      <span class="log-badge ${ok ? 'req' : 'err'}">${typeLabel}</span>
      <span class="log-type-badge">Step ${state.step}</span>
      <span class="log-time">${time}</span>
    </div>
    <div class="log-body">
      <div>
        <div class="log-section">Request →</div>
        <div class="log-xml">${syntaxHighlight(req)}</div>
      </div>
      <div>
        <div class="log-section">${ok ? 'Response ←' : 'Error ←'}</div>
        <div class="log-xml">${syntaxHighlight(res)}</div>
      </div>
    </div>
  `;
  list.prepend(entry);
}

function syntaxHighlight(xml) {
  return escapeHtml(xml)
    .replace(/(&lt;\\/?[\\w:]+)(&gt;|[\\s>])/g, '<span class="xml-tag">$1$2</span>')
    .replace(/(&lt;\\/[\\w:]+&gt;)/g, '<span class="xml-end">$1</span>')
    .replace(/&gt;([^&<]+)&lt;/g, '&gt;<span class="xml-val">$1</span>&lt;');
}

function escapeHtml(str) {
  return String(str)
    .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
    .replace(/"/g,'&quot;');
}

function clearLogs() {
  document.getElementById('logs-list').innerHTML = `
    <div class="empty-logs" id="empty-state">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
        <path d="M8 9h8M8 13h5M3 7c0-1.1.9-2 2-2h14a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/>
      </svg>
      <p>no logs yet — dial to start</p>
    </div>`;
}

// ── Reset ──────────────────────────────────────────────
function resetAll() {
  state.sessionType = 1;
  state.input = '';
  state.sessionId = '';
  state.step = 0;
  state.active = false;
  state.ended = false;
  state.breadcrumbs = [];
  document.getElementById('cfg-session').value = '';
  document.getElementById('screen-text').innerHTML = 'Dial to connect...<span class="cursor"></span>';
  document.getElementById('screen-carrier').textContent = 'NO SIGNAL';
  updateInputDisplay();
  updateBreadcrumbs();
  updateStatusUI();
  clearLogs();
}

// ── Init ───────────────────────────────────────────────
buildKeypad();
updateInputDisplay();
updateStatusUI();
</script>
</body>
</html>

"""