#pragma once

const char WEB_UI_HTML[] PROGMEM = R"rawhtml(
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sip &amp; Puff Config</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Exo+2:wght@300;600;800&display=swap');

  :root {
    --bg:      #0a0d14;
    --panel:   #111622;
    --border:  #1e2a3a;
    --puff:    #00ffe7;
    --sip:     #ff4d6d;
    --text:    #c8d8e8;
    --dim:     #A4B1BD;
    --green:   #39ff82;
    --gold:    #ffd166;
    --latch:   #c084fc;
    --analog:  #38bdf8;
  }

  * { box-sizing: border-box; margin: 0; padding: 0; }

  body {
    background: var(--bg); color: var(--text);
    font-family: 'Exo 2', sans-serif; min-height: 100vh;
    display: flex; flex-direction: column; align-items: center;
    padding: 0 1rem 4rem;
    background-image: radial-gradient(ellipse 80% 40% at 50% -10%, rgba(0,255,231,0.06) 0%, transparent 70%);
  }

  header {
    text-align: center; margin-bottom: 1.5rem;
    width: 100%; max-width: 1200px;
    padding-top: 0;
  }
  .header-logo-wrap {
    background: #000; width: 100%;
    display: flex; justify-content: center; align-items: center;
    padding: .5rem 0 0;
    -webkit-mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
    mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
  }
  .header-logo {
    width: 160px; height: 160px;
  }
  .header-text { padding: .2rem 0 0; }
  header h1 {
    font-size: 1.9rem; font-weight: 800; letter-spacing: .15em;
    text-transform: uppercase; color: var(--puff);
    text-shadow: 0 0 18px rgba(0,255,231,.4);
    margin-top: -.25rem;
  }
  header p { font-family:'Share Tech Mono',monospace; color:var(--dim); font-size:.78rem; margin-top:.3rem; }

  /* ── Console selector ──────────────────────────────────────────────────── */
  .console-panel {
    width:100%; max-width:1200px; background:var(--panel);
    border:1px solid var(--border); border-radius:14px;
    padding:1.2rem 1.4rem; margin-bottom:1rem;
  }
  .console-panel::before {
    content:'BUTTON LABEL STYLE'; display:block; font-family:'Share Tech Mono',monospace;
    font-size:.65rem; letter-spacing:.15em; color:var(--puff); margin-bottom:.9rem;
  }
  .console-btns { display:flex; flex-wrap:wrap; gap:.5rem; }
  .console-btn {
    display:flex; align-items:center; gap:.45rem;
    padding:.45rem .9rem; border-radius:8px;
    border:1px solid var(--border); background:transparent;
    font-family:'Share Tech Mono',monospace; font-size:.75rem; letter-spacing:.06em;
    color:var(--dim); cursor:pointer; transition:all .15s;
  }
  .console-btn:hover { border-color:var(--text); color:var(--text); }
  .console-btn.active {
    border-color:var(--puff); color:var(--puff);
    background:rgba(0,255,231,.07); box-shadow:0 0 10px rgba(0,255,231,.12);
  }
  .con-icon {
    display:inline-flex; align-items:center; justify-content:center;
    width:20px; height:20px; border-radius:4px; font-size:.75rem; font-weight:700;
    line-height:1; flex-shrink:0;
  }
  .con-icon.generic { background:#2a3a4a; color:#8ab; }
  .con-icon.ps      { background:#003791; color:#fff; }
  .con-icon.xbox    { background:#107c10; color:#fff; }
  .con-icon.switch  { background:#e4000f; color:#fff; }
  .con-icon.steam   { background:#1b2838; color:#c7d5e0; }
  .console-hint {
    font-family:'Share Tech Mono',monospace; font-size:.63rem; color:var(--dim);
    margin-top:.65rem; letter-spacing:.04em;
  }

  /* ── Profile panel ─────────────────────────────────────────────────────── */
  .profile-panel {
    width:100%; max-width:1200px; background:var(--panel);
    border:1px solid var(--border); border-radius:14px;
    padding:1.2rem 1.4rem; margin-bottom:1.4rem;
  }
  .profile-panel::before {
    content:'PROFILES'; display:block; font-family:'Share Tech Mono',monospace;
    font-size:.65rem; letter-spacing:.15em; color:var(--gold); margin-bottom:.9rem;
  }
  .profile-slots { display:flex; flex-wrap:wrap; gap:.5rem; margin-bottom:.9rem; }
  .profile-slot {
    display:flex; align-items:center; border:1px solid var(--border);
    border-radius:8px; overflow:hidden; min-width:140px; transition:border-color .2s;
  }
  .profile-slot.occupied       { border-color:rgba(255,209,102,.3); }
  .profile-slot.active-profile { border-color:var(--gold); box-shadow:0 0 10px rgba(255,209,102,.25); }
  .slot-load {
    flex:1; padding:.45rem .7rem; background:transparent; border:none; color:var(--text);
    font-family:'Share Tech Mono',monospace; font-size:.78rem;
    cursor:pointer; text-align:left; transition:background .15s;
  }
  .slot-load:hover { background:rgba(255,209,102,.07); }
  .slot-load .slot-name  { color:var(--gold); }
  .slot-load .slot-empty { color:var(--dim); font-style:italic; }
  .slot-del {
    padding:.45rem .55rem; background:transparent; border:none;
    border-left:1px solid var(--border); color:var(--dim); cursor:pointer;
    font-size:.85rem; transition:color .15s,background .15s;
  }
  .slot-del:hover { color:var(--sip); background:rgba(255,77,109,.08); }
  .slot-del.hidden { display:none; }
  .profile-actions { display:flex; gap:.6rem; align-items:center; flex-wrap:wrap; }
  .btn-save-profile {
    padding:.45rem 1rem; background:var(--gold); color:#000; border:none;
    border-radius:6px; font-family:'Exo 2',sans-serif; font-weight:700;
    font-size:.8rem; letter-spacing:.06em; text-transform:uppercase;
    cursor:pointer; white-space:nowrap; transition:box-shadow .2s;
  }
  .btn-save-profile:hover { box-shadow:0 0 14px rgba(255,209,102,.4); }
  .profile-hint { font-family:'Share Tech Mono',monospace; font-size:.68rem; color:var(--dim); margin-top:.6rem; }
  #active-profile-name { color:var(--gold); font-weight:600; }

  /* ── Drag-and-drop reordering ───────────────────────────────────────────── */
  .drag-handle {
    padding:.45rem .5rem; color:var(--dim); cursor:grab; font-size:.9rem;
    border-right:1px solid var(--border); flex-shrink:0; user-select:none;
    display:flex; align-items:center;
  }
  .drag-handle:active { cursor:grabbing; }
  .profile-slot.dragging   { opacity:.4; border-style:dashed; }
  .profile-slot.drag-over  { border-color:var(--gold); box-shadow:0 0 10px rgba(255,209,102,.35); }
  .profile-empty-hint {
    font-family:'Share Tech Mono',monospace; font-size:.75rem; color:var(--dim);
    font-style:italic; padding:.5rem 0;
  }

  /* ── Sensor grid ────────────────────────────────────────────────────────── */
  .grid {
    display:grid; grid-template-columns:repeat(auto-fit,minmax(290px,1fr));
    gap:1.25rem; width:100%; max-width:1200px;
  }
  .card {
    background:var(--panel); border:1px solid var(--border); border-radius:14px;
    padding:1.4rem; position:relative; overflow:hidden; transition:border-color .2s;
  }
  .card:hover { border-color:var(--puff); }
  .card::before {
    content:''; position:absolute; top:0; left:0; right:0; height:2px;
    background:linear-gradient(90deg,var(--puff),transparent);
  }
  .card-header { display:flex; align-items:center; gap:.75rem; margin-bottom:1.2rem; }
  .sensor-badge {
    width:38px; height:38px; border-radius:50%; border:2px solid var(--puff);
    display:flex; align-items:center; justify-content:center;
    font-family:'Share Tech Mono',monospace; font-size:1rem; color:var(--puff); flex-shrink:0;
  }
  .card-title { font-size:1rem; font-weight:600; letter-spacing:.06em; text-transform:uppercase; }

  /* ── Live bar ───────────────────────────────────────────────────────────── */
  .live-section { margin-bottom:1rem; }
  .bar-track {
    position:relative; height:22px; background:#0a0d14;
    border:1px solid var(--border); border-radius:99px; overflow:hidden;
  }
  .bar-track::after {
    content:''; position:absolute; left:50%; top:0; bottom:0; width:1px; background:var(--border);
  }
  .bar-fill {
    position:absolute; top:2px; bottom:2px; border-radius:99px;
    transition:left .1s ease,width .1s ease;
  }
  .bar-fill.puff { background:linear-gradient(90deg,var(--puff),var(--green)); left:50%; width:0; }
  .bar-fill.sip  { background:linear-gradient(270deg,var(--sip),#ff9a3c); width:0; }
  .bar-labels {
    display:flex; justify-content:space-between;
    font-family:'Share Tech Mono',monospace; font-size:.65rem; color:var(--dim); margin-top:.25rem;
  }

  /* ── Mode rows ──────────────────────────────────────────────────────────── */
  .mode-row {
    border:1px solid var(--border); border-radius:10px;
    padding:.9rem 1rem; margin-top:.75rem; transition:border-color .3s;
  }
  .mode-row.puff-row { border-color:rgba(0,255,231,.2); }
  .mode-row.sip-row  { border-color:rgba(255,77,109,.2); }
  .mode-row.latch-active { border-color:var(--latch)!important; box-shadow:0 0 12px rgba(192,132,252,.2); }
  .mode-row.analog-mode  { border-color:rgba(56,189,248,.35); }

  .mode-label {
    font-family:'Share Tech Mono',monospace; font-size:.7rem; letter-spacing:.12em;
    text-transform:uppercase; margin-bottom:.75rem; display:flex; align-items:center; gap:.4rem;
  }
  .mode-label .dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
  .puff-row .dot { background:var(--puff); box-shadow:0 0 6px var(--puff); }
  .sip-row  .dot { background:var(--sip);  box-shadow:0 0 6px var(--sip); }

  .latch-badge {
    display:none; margin-left:auto; font-family:'Share Tech Mono',monospace;
    font-size:.6rem; letter-spacing:.1em; padding:.15rem .5rem; border-radius:99px;
    border:1px solid var(--latch); color:var(--latch); background:rgba(192,132,252,.1);
    animation:pulse-latch 1.2s ease-in-out infinite;
  }
  .latch-active .latch-badge { display:inline-block; }
  @keyframes pulse-latch { 0%,100%{opacity:1}50%{opacity:.4} }

  .fields-grid { display:grid; grid-template-columns:1fr 1fr; gap:.75rem; }
  .field-full  { grid-column:1/-1; }

  label.field-label {
    display:block; font-family:'Share Tech Mono',monospace; font-size:.65rem;
    color:var(--dim); letter-spacing:.08em; text-transform:uppercase; margin-bottom:.3rem;
  }

  select {
    width:100%; background:var(--bg); border:1px solid var(--border); border-radius:6px;
    color:var(--text); font-family:'Share Tech Mono',monospace; font-size:.85rem;
    padding:.45rem .65rem; appearance:none; outline:none; transition:border-color .2s;
  }
  select:focus { border-color:var(--puff); }

  /* ── Mode pills ─────────────────────────────────────────────────────────── */
  .mode-pills { display:flex; gap:.4rem; }
  .mode-pill {
    flex:1; padding:.38rem .3rem; border-radius:6px; border:1px solid var(--border);
    background:transparent; font-family:'Share Tech Mono',monospace; font-size:.65rem;
    letter-spacing:.05em; text-transform:uppercase; color:var(--dim);
    cursor:pointer; text-align:center; transition:all .15s;
  }
  .mode-pill:hover { border-color:var(--text); color:var(--text); }
  .mode-pill.active-hold   { border-color:var(--puff);   color:var(--puff);   background:rgba(0,255,231,.08); }
  .mode-pill.active-repeat { border-color:var(--gold);   color:var(--gold);   background:rgba(255,209,102,.08); }
  .mode-pill.active-latch  { border-color:var(--latch);  color:var(--latch);  background:rgba(192,132,252,.08); }
  .mode-pill.active-analog { border-color:var(--analog); color:var(--analog); background:rgba(56,189,248,.08); }

  .hidden { display:none!important; }
  .repeat-speed { margin-top:.65rem; }

  /* ── Sliders ────────────────────────────────────────────────────────────── */
  .slider-header { display:flex; justify-content:space-between; align-items:baseline; margin-bottom:.25rem; }
  .slider-val { font-family:'Share Tech Mono',monospace; font-size:.75rem; color:var(--text); }

  input[type=range] {
    -webkit-appearance:none; width:100%; height:6px;
    border-radius:99px; outline:none; cursor:pointer; background:var(--border);
  }
  .puff-row .thresh-slider::-webkit-slider-thumb {
    -webkit-appearance:none; width:18px; height:18px; border-radius:50%;
    background:var(--puff); box-shadow:0 0 8px rgba(0,255,231,.5); cursor:pointer;
  }
  .sip-row .thresh-slider::-webkit-slider-thumb {
    -webkit-appearance:none; width:18px; height:18px; border-radius:50%;
    background:var(--sip); box-shadow:0 0 8px rgba(255,77,109,.5); cursor:pointer;
  }
  input.repeat-slider::-webkit-slider-thumb {
    -webkit-appearance:none; width:18px; height:18px; border-radius:50%;
    background:var(--gold); box-shadow:0 0 8px rgba(255,209,102,.5); cursor:pointer;
  }
  input.analog-slider::-webkit-slider-thumb {
    -webkit-appearance:none; width:18px; height:18px; border-radius:50%;
    background:var(--analog); box-shadow:0 0 8px rgba(56,189,248,.5); cursor:pointer;
  }
  .puff-row .thresh-slider::-moz-range-thumb { background:var(--puff);   width:18px;height:18px;border-radius:50%;border:none; }
  .sip-row  .thresh-slider::-moz-range-thumb { background:var(--sip);    width:18px;height:18px;border-radius:50%;border:none; }
  input.repeat-slider::-moz-range-thumb      { background:var(--gold);   width:18px;height:18px;border-radius:50%;border:none; }
  input.analog-slider::-moz-range-thumb      { background:var(--analog); width:18px;height:18px;border-radius:50%;border:none; }

  /* ── Combo section ──────────────────────────────────────────────────────── */
  .combo-toggle-row {
    display:flex; align-items:center; justify-content:space-between;
    padding:.5rem 0; border-top:1px solid var(--border); margin-top:.5rem;
    cursor:pointer;
  }
  .combo-toggle-label {
    font-family:'Share Tech Mono',monospace; font-size:.65rem;
    color:var(--dim); letter-spacing:.08em; text-transform:uppercase;
    display:flex; align-items:center; gap:.5rem;
  }
  .combo-toggle-label .combo-badge {
    display:none; font-size:.6rem; padding:.1rem .4rem; border-radius:99px;
    border:1px solid var(--gold); color:var(--gold); background:rgba(255,209,102,.08);
    letter-spacing:.04em;
  }
  .combo-badge.visible { display:inline-block; }
  .combo-panel {
    display:none; padding:.65rem .1rem .2rem; border-top:1px solid var(--border);
    margin-top:.35rem;
  }
  .combo-panel.open { display:block; }
  .combo-panel-hint {
    font-family:'Share Tech Mono',monospace; font-size:.6rem; color:var(--dim);
    letter-spacing:.05em; margin-bottom:.5rem;
  }
  .combo-btns-grid {
    display:grid; grid-template-columns:repeat(4,1fr); gap:.3rem;
  }
  .combo-btn-item {
    display:flex; align-items:center; gap:.3rem;
    font-family:'Share Tech Mono',monospace; font-size:.65rem; color:var(--dim);
    cursor:pointer; padding:.25rem .3rem; border-radius:5px;
    border:1px solid transparent; transition:all .12s; user-select:none;
  }
  .combo-btn-item:hover { border-color:var(--border); color:var(--text); }
  .combo-btn-item input[type=checkbox] { accent-color:var(--gold); width:13px; height:13px; flex-shrink:0; cursor:pointer; }
  .combo-btn-item.checked { border-color:rgba(255,209,102,.35); color:var(--gold); background:rgba(255,209,102,.05); }

  /* ── Analog combo hint ──────────────────────────────────────────────────── */
  .analog-combo-hint {
    font-family:'Share Tech Mono',monospace; font-size:.6rem; color:var(--analog);
    letter-spacing:.05em; margin-top:.3rem; line-height:1.5;
    padding:.35rem .5rem; border:1px solid rgba(56,189,248,.2);
    border-radius:6px; background:rgba(56,189,248,.04);
  }

  /* ── Invert toggle ──────────────────────────────────────────────────────── */
  .invert-row {
    display:flex; align-items:center; justify-content:space-between;
    padding:.5rem 0; border-top:1px solid var(--border); margin-top:.5rem;
  }
  .invert-label {
    font-family:'Share Tech Mono',monospace; font-size:.65rem;
    color:var(--dim); letter-spacing:.08em; text-transform:uppercase;
  }
  .toggle-wrap { position:relative; width:40px; height:22px; flex-shrink:0; }
  .toggle-wrap input { opacity:0; width:0; height:0; position:absolute; }
  .toggle-track {
    position:absolute; inset:0; border-radius:99px; background:var(--border);
    cursor:pointer; transition:background .2s;
  }
  .toggle-track::after {
    content:''; position:absolute; left:3px; top:3px;
    width:16px; height:16px; border-radius:50%;
    background:var(--dim); transition:transform .2s, background .2s;
  }
  .toggle-wrap input:checked + .toggle-track { background:rgba(56,189,248,.25); }
  .toggle-wrap input:checked + .toggle-track::after {
    transform:translateX(18px); background:var(--analog);
    box-shadow:0 0 6px rgba(56,189,248,.5);
  }

  /* ── Actions ────────────────────────────────────────────────────────────── */
  .actions { width:100%; max-width:1200px; display:flex; gap:1rem; margin-top:1.5rem; flex-wrap:wrap; }
  button.action-btn {
    flex:1; min-width:140px; padding:.75rem 1.5rem; border:none; border-radius:8px;
    font-family:'Exo 2',sans-serif; font-weight:700; font-size:.9rem; letter-spacing:.08em;
    text-transform:uppercase; cursor:pointer; transition:transform .1s,box-shadow .2s;
  }
  button.action-btn:active { transform:scale(.97); }
  #saveBtn  { background:var(--puff); color:#000; box-shadow:0 0 18px rgba(0,255,231,.3); }
  #saveBtn:hover  { box-shadow:0 0 28px rgba(0,255,231,.55); }
  #calibBtn { background:transparent; border:1px solid var(--border); color:var(--text); }
  #calibBtn:hover { border-color:var(--sip); color:var(--sip); }

  #status {
    font-family:'Share Tech Mono',monospace; font-size:.78rem; color:var(--dim);
    margin-top:.7rem; min-height:1.2em; text-align:center; letter-spacing:.06em; transition:color .3s;
  }
  #status.ok  { color:var(--green); }
  #status.err { color:var(--sip); }

  /* ── Modal ──────────────────────────────────────────────────────────────── */
  .modal-bg {
    display:none; position:fixed; inset:0; background:rgba(0,0,0,.7);
    backdrop-filter:blur(4px); z-index:100; align-items:center; justify-content:center;
  }
  .modal-bg.open { display:flex; }
  .modal {
    background:var(--panel); border:1px solid var(--gold); border-radius:14px;
    padding:1.8rem 2rem; width:min(420px,92vw); box-shadow:0 0 40px rgba(255,209,102,.15);
  }
  .modal h2 {
    font-size:1rem; font-weight:700; letter-spacing:.1em; text-transform:uppercase;
    color:var(--gold); margin-bottom:1rem;
  }
  .modal input[type=text] {
    width:100%; background:var(--bg); border:1px solid var(--border); border-radius:6px;
    color:var(--text); font-family:'Share Tech Mono',monospace; font-size:.9rem;
    padding:.55rem .75rem; outline:none; margin-bottom:1rem; transition:border-color .2s;
  }
  .modal input[type=text]:focus { border-color:var(--gold); }
  .modal-btns { display:flex; gap:.75rem; justify-content:flex-end; }
  .modal-btns button {
    padding:.5rem 1.2rem; border-radius:6px; border:none; font-family:'Exo 2',sans-serif;
    font-weight:700; font-size:.85rem; letter-spacing:.06em; text-transform:uppercase; cursor:pointer;
  }
  #modalConfirm { background:var(--gold); color:#000; }
  #modalCancel  { background:transparent; border:1px solid var(--border); color:var(--text); }

  /* ── Tab bar ────────────────────────────────────────────────────────────── */
  .tab-bar {
    width:100%; max-width:1200px; display:flex; gap:.4rem;
    border-bottom:1px solid var(--border); margin-bottom:1.4rem;
  }
  .tab-btn {
    padding:.55rem 1.2rem; border:none; background:transparent;
    font-family:'Share Tech Mono',monospace; font-size:.78rem; letter-spacing:.1em;
    text-transform:uppercase; color:var(--dim); cursor:pointer;
    border-bottom:2px solid transparent; margin-bottom:-1px; transition:all .15s;
  }
  .tab-btn:hover { color:var(--text); }
  .tab-btn.active { color:var(--puff); border-bottom-color:var(--puff); }
  .tab-pane { display:none; width:100%; }
  .tab-pane.active { display:contents; }

  /* ── WiFi panel ─────────────────────────────────────────────────────────── */
  .wifi-panel {
    width:100%; max-width:1200px; background:var(--panel);
    border:1px solid var(--border); border-radius:14px;
    padding:1.4rem; margin-bottom:1.25rem;
  }
  .wifi-section-title {
    font-family:'Share Tech Mono',monospace; font-size:.65rem; letter-spacing:.15em;
    text-transform:uppercase; margin-bottom:1rem;
  }
  .wifi-status-row {
    display:flex; align-items:center; gap:.75rem; flex-wrap:wrap;
    padding:.75rem 1rem; background:var(--bg); border:1px solid var(--border);
    border-radius:8px; margin-bottom:1rem; font-family:'Share Tech Mono',monospace; font-size:.8rem;
  }
  .wifi-dot {
    width:10px; height:10px; border-radius:50%; background:var(--dim); flex-shrink:0;
    transition:background .3s;
  }
  .wifi-dot.connected   { background:var(--green); box-shadow:0 0 6px var(--green); }
  .wifi-dot.connecting  { background:var(--gold);  box-shadow:0 0 6px var(--gold);
                          animation:pulse-latch 1s ease-in-out infinite; }
  .wifi-dot.failed      { background:var(--sip);   box-shadow:0 0 6px var(--sip); }
  .wifi-status-text { flex:1; color:var(--text); }
  .wifi-status-detail { color:var(--dim); font-size:.7rem; }
  #btnWifiForget {
    padding:.35rem .8rem; background:transparent; border:1px solid var(--sip);
    color:var(--sip); border-radius:6px; font-family:'Share Tech Mono',monospace;
    font-size:.72rem; cursor:pointer; transition:background .15s;
  }
  #btnWifiForget:hover { background:rgba(255,77,109,.1); }

  .wifi-networks { display:flex; flex-direction:column; gap:.4rem; margin-bottom:1rem; }
  .wifi-net-row {
    display:flex; align-items:center; gap:.75rem; padding:.55rem .8rem;
    border:1px solid var(--border); border-radius:8px; cursor:pointer;
    font-family:'Share Tech Mono',monospace; font-size:.8rem; transition:border-color .15s;
  }
  .wifi-net-row:hover { border-color:var(--puff); }
  .wifi-net-row.selected { border-color:var(--puff); background:rgba(0,255,231,.05); }
  .wifi-rssi-bar {
    display:flex; align-items:flex-end; gap:2px; height:16px; flex-shrink:0;
  }
  .wifi-rssi-bar span {
    display:block; width:4px; background:var(--dim); border-radius:2px; transition:background .2s;
  }
  .wifi-rssi-bar.strong span { background:var(--green); }
  .wifi-rssi-bar.medium span { background:var(--gold); }
  .wifi-rssi-bar.weak   span { background:var(--sip); }
  .wifi-net-ssid { flex:1; color:var(--text); }
  .wifi-net-lock { font-size:.75rem; color:var(--dim); }
  .wifi-net-row.connected-net .wifi-net-ssid { color:var(--puff); }

  .wifi-form { display:flex; flex-direction:column; gap:.75rem; }
  .wifi-form input[type=text],
  .wifi-form input[type=password] {
    width:100%; background:var(--bg); border:1px solid var(--border); border-radius:6px;
    color:var(--text); font-family:'Share Tech Mono',monospace; font-size:.85rem;
    padding:.5rem .75rem; outline:none; transition:border-color .2s;
  }
  .wifi-form input:focus { border-color:var(--puff); }
  .wifi-form label.field-label { margin-bottom:.3rem; }
  .wifi-form-row { display:flex; gap:.6rem; align-items:flex-end; flex-wrap:wrap; }
  #btnWifiConnect {
    padding:.5rem 1.2rem; background:var(--puff); color:#000; border:none;
    border-radius:6px; font-family:'Exo 2',sans-serif; font-weight:700;
    font-size:.82rem; letter-spacing:.06em; text-transform:uppercase;
    cursor:pointer; white-space:nowrap; transition:box-shadow .2s;
  }
  #btnWifiConnect:hover { box-shadow:0 0 14px rgba(0,255,231,.4); }
  #btnWifiScan {
    padding:.5rem 1rem; background:transparent; border:1px solid var(--border);
    color:var(--dim); border-radius:6px; font-family:'Share Tech Mono',monospace;
    font-size:.75rem; letter-spacing:.06em; cursor:pointer; transition:all .15s;
  }
  #btnWifiScan:hover { border-color:var(--text); color:var(--text); }
  #wifiMsg {
    font-family:'Share Tech Mono',monospace; font-size:.75rem; color:var(--dim);
    min-height:1.1em; letter-spacing:.04em; transition:color .3s;
  }
  #wifiMsg.ok  { color:var(--green); }
  /* ── Display color tab ──────────────────────────────────────────────────── */
  .display-panel {
    width:100%; max-width:1200px; background:var(--panel);
    border:1px solid var(--border); border-radius:14px;
    padding:1.4rem; margin-bottom:1.25rem;
  }
  .display-section-title {
    font-family:'Share Tech Mono',monospace; font-size:.65rem; letter-spacing:.15em;
    text-transform:uppercase; margin-bottom:1.1rem;
  }
  .color-swatches {
    display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:1.1rem;
    margin-bottom:1.4rem;
  }
  .color-swatch-card {
    background:var(--bg); border:1px solid var(--border); border-radius:10px;
    padding:1rem; display:flex; flex-direction:column; gap:.6rem;
  }
  .color-swatch-card label.field-label { margin-bottom:0; }
  .color-picker-row {
    display:flex; align-items:center; gap:.75rem;
  }
  input[type=color].color-picker {
    width:52px; height:52px; border:2px solid var(--border); border-radius:8px;
    background:none; cursor:pointer; padding:2px; flex-shrink:0;
    transition:border-color .2s;
  }
  input[type=color].color-picker:focus { border-color:var(--puff); outline:none; }
  .color-hex-display {
    font-family:'Share Tech Mono',monospace; font-size:.85rem; color:var(--text);
  }
  .color-preview-dot {
    width:28px; height:28px; border-radius:50%; border:2px solid var(--border); flex-shrink:0;
    transition:background .15s;
  }
  #btnSaveColors {
    padding:.65rem 1.6rem; background:var(--puff); color:#000; border:none;
    border-radius:8px; font-family:'Exo 2',sans-serif; font-weight:700;
    font-size:.9rem; letter-spacing:.08em; text-transform:uppercase;
    cursor:pointer; transition:box-shadow .2s;
  }
  #btnSaveColors:hover { box-shadow:0 0 24px rgba(0,255,231,.45); }
  #colorStatus {
    font-family:'Share Tech Mono',monospace; font-size:.78rem; color:var(--dim);
    margin-top:.75rem; min-height:1.1em; letter-spacing:.06em; transition:color .3s;
  }
  #colorStatus.ok  { color:var(--green); }
  #colorStatus.err { color:var(--sip); }
  .display-hint {
    font-family:'Share Tech Mono',monospace; font-size:.68rem; color:var(--dim);
    letter-spacing:.04em; line-height:1.6; margin-top:.5rem;
  }

  /* ── RGB LED panel ──────────────────────────────────────────────────────────── */
  .led-panel {
    width:100%; max-width:1200px; background:var(--panel);
    border:1px solid var(--border); border-radius:14px;
    padding:1.4rem; margin-bottom:1.25rem;
  }
  .led-section-title {
    font-family:'Share Tech Mono',monospace; font-size:.65rem; letter-spacing:.15em;
    text-transform:uppercase; margin-bottom:1.1rem;
  }
  .led-controls {
    display:flex; flex-wrap:wrap; gap:1.4rem; align-items:flex-start;
  }
  .led-power-col {
    display:flex; flex-direction:column; align-items:center; gap:.5rem; min-width:72px;
  }
  .led-power-btn {
    width:56px; height:56px; border-radius:50%; border:2px solid var(--border);
    background:var(--bg); cursor:pointer; display:flex; align-items:center;
    justify-content:center; font-size:1.4rem; transition:all .2s;
  }
  .led-power-btn.led-on {
    border-color:#fff; box-shadow:0 0 18px currentColor;
  }
  .led-power-label {
    font-family:'Share Tech Mono',monospace; font-size:.65rem; letter-spacing:.08em;
    color:var(--dim); text-transform:uppercase;
  }
  .led-fields { flex:1; min-width:220px; display:flex; flex-direction:column; gap:1rem; }
  .led-field { display:flex; flex-direction:column; gap:.3rem; }
  .led-color-row { display:flex; align-items:center; gap:.75rem; }
  .led-preview {
    width:38px; height:38px; border-radius:50%; border:2px solid var(--border);
    flex-shrink:0; transition:background .15s, box-shadow .2s;
  }
  #btnSaveLed {
    padding:.65rem 1.6rem; background:var(--puff); color:#000; border:none;
    border-radius:8px; font-family:'Exo 2',sans-serif; font-weight:700;
    font-size:.9rem; letter-spacing:.08em; text-transform:uppercase;
    cursor:pointer; transition:box-shadow .2s; margin-top:.25rem;
  }
  #btnSaveLed:hover { box-shadow:0 0 24px rgba(0,255,231,.45); }
  #ledStatus {
    font-family:'Share Tech Mono',monospace; font-size:.78rem; color:var(--dim);
    margin-top:.5rem; min-height:1.1em; letter-spacing:.06em; transition:color .3s;
  }
  #ledStatus.ok  { color:var(--green); }
  #ledStatus.err { color:var(--sip); }
  .led-bri-slider::-webkit-slider-thumb {
    -webkit-appearance:none; width:18px; height:18px; border-radius:50%;
    background:#fff; box-shadow:0 0 8px rgba(255,255,255,.5); cursor:pointer;
  }
  .led-bri-slider::-moz-range-thumb {
    background:#fff; width:18px; height:18px; border-radius:50%; border:none;
  }

  /* ── Thumbstick panel ───────────────────────────────────────────────────── */
  .thumb-panel {
    width:100%; max-width:1200px; background:var(--panel);
    border:1px solid var(--border); border-radius:14px;
    padding:1.4rem; margin-bottom:1.2rem;
  }
  .thumb-panel::before {
    content:'THUMBSTICK'; display:block; font-family:'Share Tech Mono',monospace;
    font-size:.65rem; letter-spacing:.15em; color:var(--analog); margin-bottom:1.1rem;
  }
  .thumb-viz-wrap {
    display:flex; flex-wrap:wrap; gap:2rem; align-items:flex-start;
    margin-bottom:1.4rem;
  }
  .thumb-joystick-svg { flex-shrink:0; }
  .thumb-fields { flex:1; min-width:220px; display:flex; flex-direction:column; gap:.9rem; }
  .thumb-field-row { display:flex; gap:1rem; flex-wrap:wrap; align-items:center; }
  .thumb-field-row label { font-family:'Share Tech Mono',monospace; font-size:.75rem; color:var(--dim); min-width:70px; }
  .thumb-field-row select, .thumb-field-row input[type=number] {
    background:var(--bg); border:1px solid var(--border); border-radius:6px;
    color:var(--text); font-family:'Share Tech Mono',monospace; font-size:.82rem;
    padding:.35rem .55rem; outline:none; transition:border-color .2s;
  }
  .thumb-field-row select:focus, .thumb-field-row input[type=number]:focus { border-color:var(--analog); }
  .thumb-invert-row { display:flex; gap:1.5rem; flex-wrap:wrap; }
  .thumb-toggle-label {
    display:flex; align-items:center; gap:.55rem;
    font-family:'Share Tech Mono',monospace; font-size:.78rem; color:var(--text); cursor:pointer;
  }
  .thumb-toggle-label input[type=checkbox] { accent-color:var(--analog); width:16px; height:16px; cursor:pointer; }
  .btn-save-thumb {
    padding:.5rem 1.4rem; background:var(--analog); color:#000; border:none;
    border-radius:6px; font-family:'Exo 2',sans-serif; font-weight:700;
    font-size:.82rem; letter-spacing:.06em; text-transform:uppercase;
    cursor:pointer; margin-top:.4rem; transition:box-shadow .2s;
  }
  .btn-save-thumb:hover { box-shadow:0 0 14px rgba(56,189,248,.4); }
  #thumbStatus { font-family:'Share Tech Mono',monospace; font-size:.72rem; color:var(--green); margin-top:.5rem; min-height:1.1em; }
  .thumb-hint { font-family:'Share Tech Mono',monospace; font-size:.68rem; color:var(--dim); margin-top:1rem; line-height:1.6; }
  .thumb-live-row { display:flex; gap:1rem; align-items:center; margin-top:.5rem; flex-wrap:wrap; }
  .thumb-live-label { font-family:'Share Tech Mono',monospace; font-size:.7rem; color:var(--dim); width:18px; }
  .thumb-live-bar { flex:1; height:14px; background:var(--bg); border:1px solid var(--border); border-radius:99px; overflow:hidden; position:relative; min-width:100px; }
  .thumb-live-fill { position:absolute; top:2px; bottom:2px; border-radius:99px; background:var(--analog); transition:left .08s,width .08s; }

  /* ── Capacitive touch panel ─────────────────────────────────────────────── */
  .touch-panel {
    width:100%; max-width:1200px; background:var(--panel);
    border:1px solid var(--border); border-radius:14px;
    padding:1.4rem; margin-bottom:1.2rem;
  }
  .touch-panel::before {
    content:'CAP TOUCH · PIN 1'; display:block; font-family:'Share Tech Mono',monospace;
    font-size:.65rem; letter-spacing:.15em; color:var(--latch); margin-bottom:1.1rem;
  }
  .touch-fields { display:flex; flex-direction:column; gap:.9rem; }
  .touch-field-row { display:flex; gap:1rem; flex-wrap:wrap; align-items:center; }
  .touch-field-row label { font-family:'Share Tech Mono',monospace; font-size:.75rem; color:var(--dim); min-width:90px; }
  .touch-field-row select, .touch-field-row input[type=number] {
    background:var(--bg); border:1px solid var(--border); border-radius:6px;
    color:var(--text); font-family:'Share Tech Mono',monospace; font-size:.82rem;
    padding:.35rem .55rem; outline:none; transition:border-color .2s;
  }
  .touch-field-row select:focus, .touch-field-row input[type=number]:focus { border-color:var(--latch); }
  .touch-speed-row { margin-top:.3rem; }
  .touch-live-row {
    display:flex; align-items:center; gap:.75rem; margin-top:.4rem; flex-wrap:wrap;
  }
  .touch-live-label { font-family:'Share Tech Mono',monospace; font-size:.7rem; color:var(--dim); }
  .touch-live-bar {
    flex:1; height:14px; background:var(--bg); border:1px solid var(--border);
    border-radius:99px; overflow:hidden; position:relative; min-width:120px;
  }
  .touch-live-fill {
    position:absolute; top:2px; bottom:2px; left:0; border-radius:99px;
    background:var(--latch); transition:width .1s;
  }
  .touch-live-val { font-family:'Share Tech Mono',monospace; font-size:.7rem; color:var(--latch); width:36px; text-align:right; }
  .touch-active-dot {
    width:12px; height:12px; border-radius:50%; background:var(--border);
    border:1px solid var(--dim); transition:background .1s, box-shadow .1s; flex-shrink:0;
    --dot-on: var(--latch);
  }
  .touch-active-dot.on { background:var(--dot-on); box-shadow:0 0 8px color-mix(in srgb, var(--dot-on) 70%, transparent); }
  .touch-combo-row {
    display:flex; align-items:center; justify-content:space-between;
    padding:.5rem 0; border-top:1px solid var(--border); margin-top:.5rem;
    cursor:pointer;
  }
  .touch-combo-label {
    font-family:'Share Tech Mono',monospace; font-size:.65rem;
    color:var(--dim); letter-spacing:.08em; text-transform:uppercase;
    display:flex; align-items:center; gap:.5rem;
  }
  .btn-save-touch {
    padding:.5rem 1.4rem; background:var(--latch); color:#000; border:none;
    border-radius:6px; font-family:'Exo 2',sans-serif; font-weight:700;
    font-size:.82rem; letter-spacing:.06em; text-transform:uppercase;
    cursor:pointer; margin-top:.4rem; transition:box-shadow .2s;
  }
  .btn-save-touch:hover { box-shadow:0 0 14px rgba(192,132,252,.5); }
  #touchStatus { font-family:'Share Tech Mono',monospace; font-size:.72rem; color:var(--green); margin-top:.5rem; min-height:1.1em; }
  .touch-hint { font-family:'Share Tech Mono',monospace; font-size:.68rem; color:var(--dim); margin-top:1rem; line-height:1.6; }

</style>
</head>
<body>

<header>
  <div class="header-logo-wrap">
    <img class="header-logo" src="/logo.bmp" alt="Sip &amp; Puff Logo">
  </div>
  <div class="header-text">
    <h1>Sip &amp; Puff Config</h1>
    <p>6 sip/puff switches plus a joystick</p>
  </div>
</header>

<!-- ── Tab bar ────────────────────────────────────────────────────────────── -->
<div class="tab-bar">
  <button class="tab-btn active" onclick="switchTab('config')">&#x2699; Config</button>
  <button class="tab-btn"        onclick="switchTab('network')">&#x1F4F6; Network</button>
  <button class="tab-btn"        onclick="switchTab('display')">&#x1F3A8; Display</button>
</div>

<!-- ════════════════════ CONFIG TAB ════════════════════ -->
<div class="tab-pane active" id="tab-config">


<!-- ── profiles panel ──────────────────────────────────────────────────── -->
<div class="profile-panel">
  <div class="profile-slots" id="profileSlots"></div>
  <div class="profile-actions">
    <button class="btn-save-profile" id="btnSaveProfile">&#x2B; Save as New Profile</button>
  </div>
  <div class="profile-hint">Active: <span id="active-profile-name">Unsaved</span> &nbsp;&mdash;&nbsp; Drag &#x22BF; to reorder &nbsp;&middot;&nbsp; First profile loads on boot</div>
</div>
<!-- ── sensor grid ──────────────────────────────────────────────────── -->
<div class="grid" id="grid"></div>
<!-- ── apply panel ──────────────────────────────────────────────────── -->
<div class="actions">
  <button class="action-btn" id="calibBtn">&#x21BA; Recalibrate</button>
  <button class="action-btn" id="saveBtn">&#x2713; Apply Config</button>
</div>
<div id="status">Polling sensors&hellip;</div>

<!-- ── Thumbstick ─────────────────────────────────────────────────────────── -->
<div class="thumb-panel">
  <div class="thumb-viz-wrap">
    <svg class="thumb-joystick-svg" id="thumbSvg" width="160" height="160" viewBox="0 0 160 160" style="display:block;">
      <circle cx="80" cy="80" r="76" fill="#0a0d14" stroke="#1e2a3a" stroke-width="2"/>
      <circle cx="80" cy="80" r="12" fill="none" stroke="#2a3a4a" stroke-width="1" id="thumbDeadRing"/>
      <line x1="80" y1="10" x2="80" y2="150" stroke="#1e2a3a" stroke-width="1"/>
      <line x1="10" y1="80" x2="150" y2="80" stroke="#1e2a3a" stroke-width="1"/>
      <text x="80" y="22"  text-anchor="middle" fill="#4a5a6a" font-family="Share Tech Mono,monospace" font-size="11">U</text>
      <text x="80" y="152" text-anchor="middle" fill="#4a5a6a" font-family="Share Tech Mono,monospace" font-size="11">D</text>
      <text x="14" y="84"  text-anchor="middle" fill="#4a5a6a" font-family="Share Tech Mono,monospace" font-size="11">L</text>
      <text x="146" y="84" text-anchor="middle" fill="#4a5a6a" font-family="Share Tech Mono,monospace" font-size="11">R</text>
      <circle cx="80" cy="80" r="14" fill="rgba(56,189,248,0.15)" stroke="#38bdf8" stroke-width="2" id="thumbDot"/>
      <circle cx="80" cy="80" r="5"  fill="#38bdf8" id="thumbCenter"/>
    </svg>
    <div class="thumb-fields">
      <div class="thumb-field-row">
        <label>X &#x2192; Axis</label>
        <select id="thumbAxisX" onchange="thumbFieldChanged()"></select>
      </div>
      <div class="thumb-field-row">
        <label>Y &#x2192; Axis</label>
        <select id="thumbAxisY" onchange="thumbFieldChanged()"></select>
      </div>
      <div class="thumb-field-row">
        <label>Deadzone</label>
        <input type="number" id="thumbDeadzone" min="0" max="2047" style="width:80px" oninput="thumbDeadzoneChanged()">
        <span style="font-family:Share Tech Mono,monospace;font-size:.7rem;color:var(--dim)">ADC units (0-2047)</span>
      </div>
      <div class="thumb-invert-row">
        <label class="thumb-toggle-label"><input type="checkbox" id="thumbInvertX" onchange="thumbFieldChanged()"> Invert X</label>
        <label class="thumb-toggle-label"><input type="checkbox" id="thumbInvertY" onchange="thumbFieldChanged()"> Invert Y</label>
      </div>
    </div>
  </div>
  <div style="font-family:Share Tech Mono,monospace;font-size:.7rem;color:var(--dim);margin-bottom:.4rem;letter-spacing:.08em">LIVE OUTPUT</div>
  <div class="thumb-live-row">
    <span class="thumb-live-label">X</span>
    <div class="thumb-live-bar"><div class="thumb-live-fill" id="thumbLiveX" style="left:50%;width:0"></div></div>
    <span id="thumbLiveXVal" style="font-family:Share Tech Mono,monospace;font-size:.7rem;color:var(--analog);width:30px;text-align:right">0</span>
  </div>
  <div class="thumb-live-row">
    <span class="thumb-live-label">Y</span>
    <div class="thumb-live-bar"><div class="thumb-live-fill" id="thumbLiveY" style="left:50%;width:0"></div></div>
    <span id="thumbLiveYVal" style="font-family:Share Tech Mono,monospace;font-size:.7rem;color:var(--analog);width:30px;text-align:right">0</span>
  </div>
  <p class="thumb-hint">
    Thumbstick settings are saved per-profile with Apply Config.
  </p>
</div>

<!-- ── Capacitive Touch ───────────────────────────────────────────────────────── -->
<div class="touch-panel">
  <div class="touch-fields">
    <div class="touch-field-row">
      <label>Button</label>
      <select id="touchBtn" onchange="onTouchBtnChange()"></select>
    </div>
    <div class="touch-field-row">
      <label>Mode</label>
      <div class="mode-pills" style="flex:1">
        <button class="mode-pill" id="touchPill-hold"   onclick="setTouchMode(0)">Hold</button>
        <button class="mode-pill" id="touchPill-repeat" onclick="setTouchMode(1)">Repeat</button>
        <button class="mode-pill" id="touchPill-latch"  onclick="setTouchMode(2)">Latch</button>
      </div>
      <input type="hidden" id="touchMode" value="0">
    </div>
    <div class="touch-field-row touch-speed-row" id="touchSpeedRow">
      <label>Repeat Speed</label>
      <div style="flex:1">
        <div class="slider-header">
          <span></span>
          <span class="slider-val" id="touchRepeatVal">5.0 /sec  (200ms)</span>
        </div>
        <input type="range" class="repeat-slider" id="touchRepeat"
               min="50" max="2000" step="10" value="200"
               oninput="document.getElementById('touchRepeatVal').textContent=repeatLabel(Number(this.value))">
      </div>
    </div>
    <div class="touch-field-row">
      <label>Threshold</label>
      <input type="number" id="touchThreshold" min="1000" max="200000" step="1000" value="60000" style="width:90px"
             oninput="updateTouchThreshLine()">
      <span style="font-family:Share Tech Mono,monospace;font-size:.7rem;color:var(--dim)">raw units (touch when reading exceeds this)</span>
    </div>
    <!-- Combo section -->
    <div>
      <div class="touch-combo-row" onclick="toggleTouchCombo()">
        <span class="touch-combo-label">
          &#x2795; Combo Buttons
          <span class="combo-badge" id="touchComboBadge"></span>
        </span>
        <label class="toggle-wrap" onclick="event.stopPropagation();toggleTouchCombo()">
          <input type="checkbox" id="touchComboChk" onclick="event.stopPropagation()">
          <span class="toggle-track" style="pointer-events:none"></span>
        </label>
      </div>
      <div class="combo-panel" id="touchComboPanel">
        <div class="combo-panel-hint">These buttons fire simultaneously with the touch button above.</div>
        <div class="combo-btns-grid" id="touchComboGrid"></div>
      </div>
    </div>
    <!-- Live readout -->
    <div style="font-family:Share Tech Mono,monospace;font-size:.7rem;color:var(--dim);margin-top:.5rem;letter-spacing:.08em">LIVE</div>
    <div class="touch-live-row">
      <span class="touch-live-label">Raw</span>
      <div class="touch-live-bar">
        <div class="touch-live-fill" id="touchLiveFill" style="width:0"></div>
        <div id="touchThreshLine" style="position:absolute;top:0;bottom:0;width:2px;background:rgba(192,132,252,.6);left:50%"></div>
      </div>
      <span class="touch-live-val" id="touchLiveVal">—</span>
      <span class="touch-active-dot" id="touchActiveDot"></span>
    </div>
  </div>
  <p class="touch-hint">
    On ESP32-S3, touch readings <strong>increase</strong> when touched (resting ~30k, active ~80k+).<br>
    Set threshold between your resting value and your touched value. Saved per-profile with Apply Config.
  </p>
</div>

<!-- ── Capacitive Touch 2 ──────────────────────────────────────────────────────── -->
<div class="touch-panel" style="--touch-accent:var(--green)">
  <div style="font-family:'Share Tech Mono',monospace;font-size:.65rem;letter-spacing:.15em;color:var(--green);margin-bottom:1.1rem">CAP TOUCH · PIN 2</div>
  <div class="touch-fields">
    <div class="touch-field-row">
      <label>Button</label>
      <select id="touch2Btn" onchange="onTouch2BtnChange()"></select>
    </div>
    <div class="touch-field-row">
      <label>Mode</label>
      <div class="mode-pills" style="flex:1">
        <button class="mode-pill" id="touch2Pill-hold"   onclick="setTouch2Mode(0)">Hold</button>
        <button class="mode-pill" id="touch2Pill-repeat" onclick="setTouch2Mode(1)">Repeat</button>
        <button class="mode-pill" id="touch2Pill-latch"  onclick="setTouch2Mode(2)">Latch</button>
      </div>
      <input type="hidden" id="touch2Mode" value="0">
    </div>
    <div class="touch-field-row touch-speed-row" id="touch2SpeedRow">
      <label>Repeat Speed</label>
      <div style="flex:1">
        <div class="slider-header">
          <span></span>
          <span class="slider-val" id="touch2RepeatVal">5.0 /sec  (200ms)</span>
        </div>
        <input type="range" class="repeat-slider" id="touch2Repeat"
               min="50" max="2000" step="10" value="200"
               oninput="document.getElementById('touch2RepeatVal').textContent=repeatLabel(Number(this.value))">
      </div>
    </div>
    <div class="touch-field-row">
      <label>Threshold</label>
      <input type="number" id="touch2Threshold" min="1000" max="200000" step="1000" value="60000" style="width:90px"
             oninput="updateTouch2ThreshLine()">
      <span style="font-family:Share Tech Mono,monospace;font-size:.7rem;color:var(--dim)">raw units (touch when reading exceeds this)</span>
    </div>
    <!-- Combo section -->
    <div>
      <div class="touch-combo-row" onclick="toggleTouch2Combo()">
        <span class="touch-combo-label">
          &#x2795; Combo Buttons
          <span class="combo-badge" id="touch2ComboBadge"></span>
        </span>
        <label class="toggle-wrap" onclick="event.stopPropagation();toggleTouch2Combo()">
          <input type="checkbox" id="touch2ComboChk" onclick="event.stopPropagation()">
          <span class="toggle-track" style="pointer-events:none"></span>
        </label>
      </div>
      <div class="combo-panel" id="touch2ComboPanel">
        <div class="combo-panel-hint">These buttons fire simultaneously with the touch button above.</div>
        <div class="combo-btns-grid" id="touch2ComboGrid"></div>
      </div>
    </div>
    <!-- Live readout -->
    <div style="font-family:Share Tech Mono,monospace;font-size:.7rem;color:var(--dim);margin-top:.5rem;letter-spacing:.08em">LIVE</div>
    <div class="touch-live-row">
      <span class="touch-live-label">Raw</span>
      <div class="touch-live-bar">
        <div class="touch-live-fill" id="touch2LiveFill" style="width:0;background:var(--green)"></div>
        <div id="touch2ThreshLine" style="position:absolute;top:0;bottom:0;width:2px;background:rgba(57,255,130,.6);left:50%"></div>
      </div>
      <span class="touch-live-val" id="touch2LiveVal" style="color:var(--green)">—</span>
      <span class="touch-active-dot" id="touch2ActiveDot" style="--dot-on:var(--green)"></span>
    </div>
  </div>
  <p class="touch-hint">
    On ESP32-S3, touch readings <strong>increase</strong> when touched (resting ~30k, active ~80k+).<br>
    Set threshold between your resting value and your touched value. Saved per-profile with Apply Config.
  </p>
</div>
<!-- ── Console selector ──────────────────────────────────────────────────── -->
<div class="console-panel">
  <div class="console-btns">
    <button class="console-btn active" data-console="generic" onclick="setConsole('generic')">
      <span class="con-icon generic">?</span> Generic
    </button>
    <button class="console-btn" data-console="ps" onclick="setConsole('ps')">
      <span class="con-icon ps">PS</span> PlayStation
    </button>
    <button class="console-btn" data-console="xbox" onclick="setConsole('xbox')">
      <span class="con-icon xbox">X</span> Xbox
    </button>
    <button class="console-btn" data-console="switch" onclick="setConsole('switch')">
      <span class="con-icon switch">&#x2B22;</span> Switch
    </button>
    <button class="console-btn" data-console="steam" onclick="setConsole('steam')">
      <span class="con-icon steam">&#x2699;</span> Steam Deck
    </button>
  </div>
  <div class="console-hint">
    Changes button <em>labels only</em> &mdash; the underlying HID button numbers sent to your PC/console are unchanged.
    Button 1 is always HID button 1 regardless of what it is called here.
  </div>
</div>

</div><!-- /tab-pane#tab-config -->

<!-- ════════════════════ NETWORK TAB ════════════════════ -->
<div class="tab-pane" id="tab-network">

  <!-- Status card -->
  <div class="wifi-panel">
    <div class="wifi-section-title" style="color:var(--puff)">&#x1F4F6; Local Network Status</div>
    <div class="wifi-status-row">
      <span class="wifi-dot" id="wifiDot"></span>
      <span class="wifi-status-text" id="wifiStatusText">Checking&hellip;</span>
      <span class="wifi-status-detail" id="wifiStatusDetail"></span>
      <button id="btnWifiForget" style="display:none" onclick="wifiForget()">Forget</button>
    </div>
    <div id="wifiMsg"></div>
  </div>

  <!-- Scan + join card -->
  <div class="wifi-panel">
    <div class="wifi-section-title" style="color:var(--gold)">&#x1F50D; Available Networks
      <button id="btnWifiScan" style="margin-left:.75rem" onclick="wifiScan()">Scan</button>
    </div>
    <div class="wifi-networks" id="wifiNetworks">
      <div style="font-family:'Share Tech Mono',monospace;font-size:.75rem;color:var(--dim)">
        Press Scan to search for networks&hellip;
      </div>
    </div>

    <div class="wifi-form">
      <div>
        <label class="field-label">Network (SSID)</label>
        <input type="text" id="wifiSSID" placeholder="Network name&hellip;" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false">
      </div>
      <div>
        <label class="field-label">Password</label>
        <div class="wifi-form-row">
          <input type="password" id="wifiPass" placeholder="Password&hellip;" style="flex:1" autocomplete="new-password">
          <button id="btnWifiConnect" onclick="wifiConnect()">&#x2713; Join Network</button>
        </div>
      </div>
    </div>
  </div>

</div><!-- /tab-pane#tab-network -->

<!-- ════════════════════ DISPLAY TAB ════════════════════ -->
<div class="tab-pane" id="tab-display">

  <div class="display-panel">
    <div class="display-section-title" style="color:var(--puff)">&#x25CF; Icon Colors</div>

    <div class="color-swatches">

      <div class="color-swatch-card">
        <label class="field-label">Puff Active (exhale)</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorPuff" value="#00ff80"
            oninput="onColorChange('colorPuff','hexPuff','dotPuff')">
          <span class="color-preview-dot" id="dotPuff"></span>
          <span class="color-hex-display" id="hexPuff">#00ff80</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Sip Active (inhale)</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorSip" value="#ff0000"
            oninput="onColorChange('colorSip','hexSip','dotSip')">
          <span class="color-preview-dot" id="dotSip"></span>
          <span class="color-hex-display" id="hexSip">#ff0000</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Inactive / Off</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorOff" value="#2a4a6a"
            oninput="onColorChange('colorOff','hexOff','dotOff')">
          <span class="color-preview-dot" id="dotOff"></span>
          <span class="color-hex-display" id="hexOff">#2a4a6a</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Profile Name Text</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorProfileName" value="#ffffff"
            oninput="onColorChange('colorProfileName','hexProfileName','dotProfileName')">
          <span class="color-preview-dot" id="dotProfileName"></span>
          <span class="color-hex-display" id="hexProfileName">#ffffff</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Profile Label (&ldquo;PROFILE&rdquo;)</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorProfileLabel" value="#444444"
            oninput="onColorChange('colorProfileLabel','hexProfileLabel','dotProfileLabel')">
          <span class="color-preview-dot" id="dotProfileLabel"></span>
          <span class="color-hex-display" id="hexProfileLabel">#444444</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Button Number Labels</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorBtnLabel" value="#ffffff"
            oninput="onColorChange('colorBtnLabel','hexBtnLabel','dotBtnLabel')">
          <span class="color-preview-dot" id="dotBtnLabel"></span>
          <span class="color-hex-display" id="hexBtnLabel">#ffffff</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Screen Background</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorBg" value="#000000"
            oninput="onColorChange('colorBg','hexBg','dotBg')">
          <span class="color-preview-dot" id="dotBg"></span>
          <span class="color-hex-display" id="hexBg">#000000</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Icon Outline (inactive)</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorIconBorder" value="#222222"
            oninput="onColorChange('colorIconBorder','hexIconBorder','dotIconBorder')">
          <span class="color-preview-dot" id="dotIconBorder"></span>
          <span class="color-hex-display" id="hexIconBorder">#222222</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Cap Touch 1 Active (Pin 1)</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorTouch1" value="#cc00ff"
            oninput="onColorChange('colorTouch1','hexTouch1','dotTouch1')">
          <span class="color-preview-dot" id="dotTouch1"></span>
          <span class="color-hex-display" id="hexTouch1">#cc00ff</span>
        </div>
      </div>

      <div class="color-swatch-card">
        <label class="field-label">Cap Touch 2 Active (Pin 2)</label>
        <div class="color-picker-row">
          <input type="color" class="color-picker" id="colorTouch2" value="#00ff80"
            oninput="onColorChange('colorTouch2','hexTouch2','dotTouch2')">
          <span class="color-preview-dot" id="dotTouch2"></span>
          <span class="color-hex-display" id="hexTouch2">#00ff80</span>
        </div>
      </div>

    </div>

    <button id="btnSaveColors" onclick="saveColors()">&#x2713; Save Colors to Device</button>
    <div id="colorStatus"></div>
    <p class="display-hint">
      Colors are saved to flash and applied immediately on screen.<br>
      They persist across reboots independently of sensor profiles.
    </p>
  </div>

  <!-- ── Onboard RGB LED ─────────────────────────────────────────────────── -->
  <div class="led-panel">
    <div class="led-section-title" style="color:var(--latch)">&#x1F4A1; Onboard RGB LED</div>
    <div class="led-controls">

      <div class="led-power-col">
        <button class="led-power-btn" id="ledPowerBtn" onclick="toggleLed()" title="Toggle LED on/off">&#x23FB;</button>
        <span class="led-power-label" id="ledPowerLabel">ON</span>
      </div>

      <div class="led-fields">
        <div class="led-field">
          <label class="field-label">Color</label>
          <div class="led-color-row">
            <input type="color" class="color-picker" id="ledColor" value="#00ffc8"
              oninput="onLedColorChange()">
            <span class="led-preview" id="ledPreview" style="background:#00ffc8;box-shadow:0 0 12px #00ffc8"></span>
            <span class="color-hex-display" id="ledHex">#00ffc8</span>
          </div>
        </div>

        <div class="led-field">
          <div class="slider-header">
            <label class="field-label" style="margin:0">Brightness</label>
            <span class="slider-val" id="ledBriVal">128</span>
          </div>
          <input type="range" class="led-bri-slider" id="ledBri"
            min="0" max="255" step="1" value="128"
            oninput="onLedBriChange()">
        </div>

        <div>
          <button id="btnSaveLed" onclick="saveLed()">&#x2713; Save LED Settings</button>
          <div id="ledStatus"></div>
        </div>
      </div>

    </div>
  </div>

</div><!-- /tab-pane#tab-display -->


<div class="modal-bg" id="modalBg">
  <div class="modal">
    <h2>Save Profile</h2>
    <input type="text" id="modalNameInput" maxlength="23" placeholder="Enter profile name&hellip;">
    <div class="modal-btns">
      <button id="modalCancel">Cancel</button>
      <button id="modalConfirm">Save</button>
    </div>
  </div>
</div>

<script>
const INIT        = __CONFIG_JSON__;
const NUM_SENSORS = 6;
const keys        = ['s0','s1','s2','s3','s4','s5'];
const LABELS      = ['Sensor 1','Sensor 2','Sensor 3','Sensor 4','Sensor 5','Sensor 6'];
const AXIS_LABELS = ['Left Stick X','Left Stick Y','Right Stick X','Right Stick Y','Left Trigger','Right Trigger','Thumbstick X','Thumbstick Y'];
const CURVE_LABELS= ['Linear','Squared (gentle)','Cubed (very gentle)'];
const MAX_SLOTS   = 8;
const REPEAT_MIN  = 50;
const REPEAT_MAX  = 2000;

const MAX_DISP   = 2000000;
const THRESH_MIN = 10000;
const THRESH_MAX = 1500000;
const THRESH_STEP= 10000;
const DZ_MIN     = 100;
const DZ_MAX     = 200000;
const DZ_STEP    = 1000;
const RANGE_MIN  = 10000;
const RANGE_MAX  = 2000000;
const RANGE_STEP = 10000;

let activeProfileName = 'Unsaved';
let pendingSaveSlot   = -1;

// ── Console button label maps ─────────────────────────────────────────────────
const CONSOLE_BTN_LABELS = {
  generic: [
    '— Off —',
    'Button 1','Button 2','Button 3','Button 4',
    'Button 5','Button 6','Button 7','Button 8',
    'Button 9','Button 10','Button 11','Button 12',
    'Button 13','Button 14','Button 15','Button 16'
  ],
  ps: [
    '— Off —',
    'Cross ✕','Circle ○','Square □','Triangle △',
    'L1','R1','L2','R2',
    'Create','Options','L3','R3',
    'Button 13','Button 14','Button 15','Button 16'
  ],
  xbox: [
    '— Off —',
    'A','B','X','Y',
    'LB','RB','LT','RT',
    'View','Menu','LS','RS',
    'Button 13','Button 14','Button 15','Button 16'
  ],
  switch: [
    '— Off —',
    'B','A','Y','X',
    'L','R','ZL','ZR',
    'Minus','Plus','L Stick','R Stick',
    'Button 13','Button 14','Button 15','Button 16'
  ],
  steam: [
    '— Off —',
    'A','B','X','Y',
    'L1','R1','L2','R2',
    'View','Menu','L Stick Click','R Stick Click',
    'Button 13','Button 14','Button 15','Button 16'
  ]
};

let currentConsole = 'generic';

function setConsole(name) {
  currentConsole = name;
  document.querySelectorAll('.console-btn').forEach(b => {
    b.classList.toggle('active', b.dataset.console === name);
  });
  keys.forEach(k => {
    ['p','s'].forEach(pre => {
      const sel = document.getElementById(`${pre}b-${k}`);
      if (!sel) return;
      const curVal = sel.value;
      sel.innerHTML = btnOptions(parseInt(curVal));
      // Also update the analog-mode button selector if it exists
      const selA = document.getElementById(`${pre}ab-${k}`);
      if (selA) {
        const curValA = selA.value;
        selA.innerHTML = btnOptionsAnalog(parseInt(curValA));
      }
    });
  });
  rebuildAllComboGrids();
  // Also update cap-touch button selectors and combo grids
  const touchSel = document.getElementById('touchBtn');
  if (touchSel) touchSel.innerHTML = btnOptions(parseInt(touchSel.value));
  buildTouchComboGrid(readTouchComboMask());
  const touch2Sel = document.getElementById('touch2Btn');
  if (touch2Sel) touch2Sel.innerHTML = btnOptions(parseInt(touch2Sel.value));
  buildTouch2ComboGrid(readTouch2ComboMask());
}

function btnOptions(sel) {
  const labels = CONSOLE_BTN_LABELS[currentConsole];
  let o = `<option value="0"${sel===0?' selected':''}>${labels[0]}</option>`;
  for (let n = 1; n <= 16; n++) {
    o += `<option value="${n}"${sel===n?' selected':''}>${labels[n]}</option>`;
  }
  o += `<option value="17"${sel===17?' selected':''}>⟳ Recalibrate</option>`;
  o += `<option value="18"${sel===18?' selected':''}>⏭ Next Profile</option>`;
  o += `<option value="19"${sel===19?' selected':''}>🎮 D-Pad Up (hat)</option>`;
  o += `<option value="20"${sel===20?' selected':''}>🎮 D-Pad Right (hat)</option>`;
  o += `<option value="21"${sel===21?' selected':''}>🎮 D-Pad Down (hat)</option>`;
  o += `<option value="22"${sel===22?' selected':''}>🎮 D-Pad Left (hat)</option>`;
  return o;
}

// Analog button selector — only HID buttons 1-16 make sense (no special actions)
function btnOptionsAnalog(sel) {
  const labels = CONSOLE_BTN_LABELS[currentConsole];
  let o = `<option value="0"${sel===0?' selected':''}>— None (axis only) —</option>`;
  for (let n = 1; n <= 16; n++) {
    o += `<option value="${n}"${sel===n?' selected':''}>${labels[n]}</option>`;
  }
  return o;
}

function axisOptions(sel) {
  return AXIS_LABELS.map((l,i)=>`<option value="${i}"${sel==i?' selected':''}>${l}</option>`).join('');
}
function curveOptions(sel) {
  return CURVE_LABELS.map((l,i)=>`<option value="${i}"${sel==i?' selected':''}>${l}</option>`).join('');
}
function repeatLabel(ms){ return `${(1000/ms).toFixed(1)} /sec  (${ms}ms)`; }
function fmtN(n){ return Number(n).toLocaleString(); }

// ── Mode pill logic ───────────────────────────────────────────────────────────
function setPillMode(pre, k, mode) {
  ['hold','repeat','latch','analog'].forEach((m,i)=>{
    const pill=document.getElementById(`${pre}pill-${m}-${k}`);
    if(!pill) return;
    pill.classList.remove('active-hold','active-repeat','active-latch','active-analog');
    if(i===mode) pill.classList.add(`active-${m}`);
  });
  document.getElementById(`${pre}m-${k}`).value=mode;
  const isAnalog=mode===3, isRepeat=mode===1;
  document.getElementById(`${pre}digital-${k}`).classList.toggle('hidden', isAnalog);
  document.getElementById(`${pre}analog-${k}`).classList.toggle('hidden',  !isAnalog);
  document.getElementById(`${pre}speed-${k}`).classList.toggle('hidden',   !isRepeat);
  document.getElementById(`${pre}row-${k}`).classList.toggle('analog-mode', isAnalog);
  // Combo section is always shown regardless of mode — no hide/show needed here
}

// ── Special-action button values ──────────────────────────────────────────────
const SPECIAL_ACTIONS = new Set([17, 18, 19, 20, 21, 22]);

function onBtnSelectChange(pre, k, selectEl) {
  const val = parseInt(selectEl.value);
  const pillWrap = document.getElementById(`${pre}row-${k}`).querySelector('.mode-pills');
  if (SPECIAL_ACTIONS.has(val)) {
    setPillMode(pre, k, 0);
    if (pillWrap) { pillWrap.style.opacity = '0.3'; pillWrap.style.pointerEvents = 'none'; }
  } else {
    if (pillWrap) { pillWrap.style.opacity = ''; pillWrap.style.pointerEvents = ''; }
  }
}

// ── Combo panel helpers ───────────────────────────────────────────────────────
function comboCount(mask) {
  let n = 0;
  for (let b = 0; b < 16; b++) if ((mask >> b) & 1) n++;
  return n;
}

// primaryVal: the HID button number (1-16) that is the primary. Pass 0 if none.
function buildComboGrid(pre, k, combo, primaryVal) {
  const grid = document.getElementById(`${pre}combo-grid-${k}`);
  if (!grid) return;
  const labels = CONSOLE_BTN_LABELS[currentConsole];
  grid.innerHTML = '';
  for (let n = 1; n <= 16; n++) {
    const bit = n - 1;
    const checked = !!((combo >> bit) & 1);
    const isPrimary = n === primaryVal;
    const item = document.createElement('label');
    item.className = 'combo-btn-item' + (checked ? ' checked' : '') + (isPrimary ? ' primary-btn' : '');
    item.title = isPrimary ? 'Same as primary button' : '';
    item.innerHTML = `<input type="checkbox" ${checked?'checked':''} ${isPrimary?'disabled':''} data-bit="${bit}" onchange="onComboChange('${pre}','${k}',this)"> ${labels[n].replace(/^Button\s*/i, '')}`;
    grid.appendChild(item);
  }
}

function onComboChange(pre, k, cb) {
  const item = cb.closest('.combo-btn-item');
  if (item) item.classList.toggle('checked', cb.checked);
  const mask = readComboMask(pre, k);
  const badge = document.getElementById(`${pre}combo-badge-${k}`);
  if (badge) {
    const cnt = comboCount(mask);
    badge.textContent = cnt > 0 ? cnt + ' set' : '';
    badge.classList.toggle('visible', cnt > 0);
  }
}

function readComboMask(pre, k) {
  const grid = document.getElementById(`${pre}combo-grid-${k}`);
  if (!grid) return 0;
  let mask = 0;
  grid.querySelectorAll('input[type=checkbox]').forEach(cb => {
    if (cb.checked) mask |= (1 << parseInt(cb.dataset.bit));
  });
  return mask;
}

// Get the current primary button value for this sensor/side.
// In analog mode the primary is read from the analog button selector (ab-).
// In digital mode it's read from the regular selector (b-).
function getPrimaryBtnVal(pre, k) {
  const modeVal = parseInt(document.getElementById(`${pre}m-${k}`).value);
  if (modeVal === 3) {
    const selA = document.getElementById(`${pre}ab-${k}`);
    return selA ? parseInt(selA.value) : 0;
  }
  const sel = document.getElementById(`${pre}b-${k}`);
  return sel ? parseInt(sel.value) : 0;
}

function toggleComboPanel(pre, k) {
  const panel = document.getElementById(`${pre}combo-panel-${k}`);
  const chk   = document.getElementById(`${pre}combo-chk-${k}`);
  const badge = document.getElementById(`${pre}combo-badge-${k}`);
  if (!panel) return;
  const opening = !panel.classList.contains('open');
  panel.classList.toggle('open', opening);
  if (chk) chk.checked = opening;
  if (opening) {
    const currentMask = readComboMask(pre, k);
    buildComboGrid(pre, k, currentMask, getPrimaryBtnVal(pre, k));
    const cnt = comboCount(currentMask);
    if (badge) { badge.textContent = cnt > 0 ? cnt + ' set' : ''; badge.classList.toggle('visible', cnt > 0); }
  }
}

function rebuildAllComboGrids() {
  keys.forEach(k => {
    ['p','s'].forEach(pre => {
      const panel = document.getElementById(`${pre}combo-panel-${k}`);
      if (panel && panel.classList.contains('open')) {
        const mask = readComboMask(pre, k);
        buildComboGrid(pre, k, mask, getPrimaryBtnVal(pre, k));
      }
    });
  });
}

// ── Build one mode block ──────────────────────────────────────────────────────
function modeBlock(k, pre, c) {
  const color    = pre==='p' ? 'puff' : 'sip';
  const title    = pre==='p' ? 'Puff (exhale)' : 'Sip (inhale)';
  const bc       = c[pre];
  const thr      = pre==='p' ? c.pt : c.st;
  const thrId    = pre==='p' ? `pt-${k}` : `st-${k}`;
  const thrLbl   = pre==='p' ? `pt-val-${k}` : `st-val-${k}`;
  const isAnalog = bc.m===3;
  const isRepeat = bc.m===1;
  const combo    = bc.co || 0;
  const comboCnt = comboCount(combo);

  return `
  <div class="mode-row ${color}-row${isAnalog?' analog-mode':''}" id="${pre}row-${k}">
    <div class="mode-label">
      <span class="dot"></span>${title}
      <span class="latch-badge">LATCHED</span>
    </div>
    <div class="fields-grid">

      <div class="field-full">
        <label class="field-label">Mode</label>
        <div class="mode-pills">
          <button class="mode-pill" id="${pre}pill-hold-${k}"   onclick="setPillMode('${pre}','${k}',0)">Hold</button>
          <button class="mode-pill" id="${pre}pill-repeat-${k}" onclick="setPillMode('${pre}','${k}',1)">Repeat</button>
          <button class="mode-pill" id="${pre}pill-latch-${k}"  onclick="setPillMode('${pre}','${k}',2)">Latch</button>
          <button class="mode-pill" id="${pre}pill-analog-${k}" onclick="setPillMode('${pre}','${k}',3)">Analog</button>
        </div>
        <input type="hidden" id="${pre}m-${k}" value="${bc.m}">
      </div>

      <!-- Digital fields (hidden in analog mode) -->
      <div id="${pre}digital-${k}" class="field-full${isAnalog?' hidden':''}">
        <div class="fields-grid">
          <div>
            <label class="field-label">Button</label>
            <select id="${pre}b-${k}" onchange="onBtnSelectChange('${pre}','${k}',this)">${btnOptions(bc.b)}</select>
          </div>
          <div>
            <label class="field-label">Threshold</label>
            <div class="slider-header">
              <span></span>
              <span class="slider-val" id="${thrLbl}">${fmtN(thr)}</span>
            </div>
            <input type="range" class="thresh-slider" id="${thrId}"
                   min="${THRESH_MIN}" max="${THRESH_MAX}" step="${THRESH_STEP}" value="${thr}"
                   oninput="document.getElementById('${thrLbl}').textContent=fmtN(this.value)">
          </div>
          <div class="field-full repeat-speed${isRepeat?'':' hidden'}" id="${pre}speed-${k}">
            <div class="slider-header">
              <label class="field-label" style="margin:0">Repeat Speed</label>
              <span class="slider-val" id="${pre}i-val-${k}">${repeatLabel(bc.i)}</span>
            </div>
            <input type="range" class="repeat-slider" id="${pre}i-${k}"
                   min="${REPEAT_MIN}" max="${REPEAT_MAX}" step="10" value="${bc.i}"
                   oninput="document.getElementById('${pre}i-val-${k}').textContent=repeatLabel(Number(this.value))">
          </div>
        </div>
      </div>

      <!-- Analog fields (hidden in digital modes) -->
      <div id="${pre}analog-${k}" class="field-full${isAnalog?'':' hidden'}">
        <div class="fields-grid">
          <div>
            <label class="field-label">Axis</label>
            <select id="${pre}a-${k}">${axisOptions(bc.a)}</select>
          </div>
          <div>
            <label class="field-label">Response Curve</label>
            <select id="${pre}c-${k}">${curveOptions(bc.c)}</select>
          </div>
          <div class="field-full">
            <label class="field-label">Dead Zone</label>
            <div class="slider-header">
              <span></span>
              <span class="slider-val" id="${pre}d-val-${k}">${fmtN(bc.d)}</span>
            </div>
            <input type="range" class="analog-slider" id="${pre}d-${k}"
                   min="${DZ_MIN}" max="${DZ_MAX}" step="${DZ_STEP}" value="${bc.d}"
                   oninput="document.getElementById('${pre}d-val-${k}').textContent=fmtN(this.value)">
          </div>
          <div class="field-full">
            <label class="field-label">Max Range &mdash; hard clamp, full deflection point</label>
            <div class="slider-header">
              <span></span>
              <span class="slider-val" id="${pre}r-val-${k}">${fmtN(bc.r)}</span>
            </div>
            <input type="range" class="analog-slider" id="${pre}r-${k}"
                   min="${RANGE_MIN}" max="${RANGE_MAX}" step="${RANGE_STEP}" value="${bc.r}"
                   oninput="document.getElementById('${pre}r-val-${k}').textContent=fmtN(this.value)">
          </div>
          <div class="field-full">
            <div class="invert-row">
              <span class="invert-label">Invert Axis Direction</span>
              <label class="toggle-wrap">
                <input type="checkbox" id="${pre}v-${k}" ${bc.v?'checked':''}>
                <span class="toggle-track"></span>
              </label>
            </div>
          </div>
          <!-- Button selector for analog mode: fires alongside the axis -->
          <div class="field-full">
            <label class="field-label">Button (fires when pressure exceeds dead zone)</label>
            <select id="${pre}ab-${k}" onchange="rebuildAllComboGrids()">${btnOptionsAnalog(bc.b)}</select>
            <div class="analog-combo-hint">Optional &mdash; set to &ldquo;None&rdquo; for axis-only output. When set, this button (and any combo buttons below) presses as soon as pressure crosses the dead zone and releases when it drops back.</div>
          </div>
        </div>
      </div>

      <!-- Combo section — always visible regardless of mode -->
      <div class="field-full">
        <div class="combo-toggle-row" onclick="toggleComboPanel('${pre}','${k}')">
          <span class="combo-toggle-label">
            &#x2795; Combo Buttons
            <span class="combo-badge${comboCnt>0?' visible':''}" id="${pre}combo-badge-${k}">${comboCnt>0?comboCnt+' set':''}</span>
          </span>
          <label class="toggle-wrap" onclick="event.stopPropagation();toggleComboPanel('${pre}','${k}')">
            <input type="checkbox" id="${pre}combo-chk-${k}" ${combo?'checked':''} onclick="event.stopPropagation()">
            <span class="toggle-track" style="pointer-events:none"></span>
          </label>
        </div>
        <div class="combo-panel${combo?' open':''}" id="${pre}combo-panel-${k}">
          <div class="combo-panel-hint">These buttons fire simultaneously with the primary button above.</div>
          <div class="combo-btns-grid" id="${pre}combo-grid-${k}"></div>
        </div>
      </div>

    </div>
  </div>`;
}

// ── Build sensor cards ────────────────────────────────────────────────────────
function buildCards(cfg) {
  const grid=document.getElementById('grid');
  grid.innerHTML='';
  keys.forEach((k,i)=>{
    const c=cfg[k];
    grid.innerHTML+=`
    <div class="card">
      <div class="card-header">
        <div class="sensor-badge">${i+1}</div>
        <div class="card-title">${LABELS[i]}</div>
      </div>
      <div class="live-section">
        <label class="field-label">Live Pressure (sip &larr; center &rarr; puff)</label>
        <div class="bar-track">
          <div class="bar-fill puff" id="pbar-${k}"></div>
          <div class="bar-fill sip"  id="sbar-${k}"></div>
        </div>
        <div class="bar-labels"><span>SIP</span><span id="val-${k}">0</span><span>PUFF</span></div>
      </div>
      ${modeBlock(k,'p',c)}
      ${modeBlock(k,'s',c)}
    </div>`;
  });
  keys.forEach(k=>{
    const c=cfg[k];
    setPillMode('p',k,c.p.m);
    setPillMode('s',k,c.s.m);
    ['p','s'].forEach(pre=>{
      const sel=document.getElementById(`${pre}b-${k}`);
      if(sel) onBtnSelectChange(pre,k,sel);
    });
    // Initialize combo grids for any sensor that has combos set
    ['p','s'].forEach(pre=>{
      const bc = pre==='p' ? c.p : c.s;
      const combo = bc.co || 0;
      const badge = document.getElementById(`${pre}combo-badge-${k}`);
      if (badge) {
        const cnt = comboCount(combo);
        badge.textContent = cnt > 0 ? cnt + ' set' : '';
        badge.classList.toggle('visible', cnt > 0);
      }
      if (combo) {
        const panel = document.getElementById(`${pre}combo-panel-${k}`);
        if (panel) { panel.classList.add('open'); buildComboGrid(pre, k, combo, getPrimaryBtnVal(pre, k)); }
        const chk = document.getElementById(`${pre}combo-chk-${k}`);
        if (chk) chk.checked = true;
      }
    });
  });
}
buildCards(INIT);
populateThumb(INIT.thumb || {axisX:0, axisY:1, deadzone:80, invertX:false, invertY:false});
populateTouch(INIT.touch   || {b:0, m:0, i:200, t:60000, co:0});
populateTouch2(INIT.touch2 || {b:0, m:0, i:200, t:60000, co:0});

// ── Thumbstick controls ───────────────────────────────────────────────────────
function thumbAxisOpts(sel) {
  return AXIS_LABELS.slice(0,6).map((l,i)=>
    `<option value="${i}"${sel==i?' selected':''}>${l}</option>`).join('');
}

function populateThumb(t) {
  document.getElementById('thumbAxisX').innerHTML = thumbAxisOpts(t.axisX);
  document.getElementById('thumbAxisY').innerHTML = thumbAxisOpts(t.axisY);
  document.getElementById('thumbDeadzone').value  = t.deadzone;
  document.getElementById('thumbInvertX').checked = t.invertX;
  document.getElementById('thumbInvertY').checked = t.invertY;
  const dzR = 5 + Math.round((t.deadzone / 2047) * 40);
  document.getElementById('thumbDeadRing').setAttribute('r', dzR);
}

function thumbDeadzoneChanged() {
  const dz = parseInt(document.getElementById('thumbDeadzone').value) || 0;
  const dzR = 5 + Math.round((dz / 2047) * 40);
  document.getElementById('thumbDeadRing').setAttribute('r', dzR);
}

function thumbFieldChanged() { /* changes are picked up by gatherPayload on Apply */ }

function updateThumbViz(rx, ry) {
  const cx = 80 + (rx / 127) * 58;
  const cy = 80 + (ry / 127) * 58;
  document.getElementById('thumbDot').setAttribute('cx', cx.toFixed(1));
  document.getElementById('thumbDot').setAttribute('cy', cy.toFixed(1));
  document.getElementById('thumbCenter').setAttribute('cx', cx.toFixed(1));
  document.getElementById('thumbCenter').setAttribute('cy', cy.toFixed(1));
}

function updateThumbBars(rx, ry) {
  function setBar(fillId, valId, v) {
    const half = 50;
    const el = document.getElementById(fillId);
    if (v >= 0) { el.style.left = half+'%'; el.style.width = ((v/127)*half)+'%'; }
    else { const w=((-v)/127)*half; el.style.left=(half-w)+'%'; el.style.width=w+'%'; }
    document.getElementById(valId).textContent = v;
  }
  setBar('thumbLiveX','thumbLiveXVal', rx);
  setBar('thumbLiveY','thumbLiveYVal', ry);
}

// ── Cap-touch controls ────────────────────────────────────────────────────────
function populateTouch(t) {
  document.getElementById('touchBtn').innerHTML = btnOptions(t.b || 0);
  document.getElementById('touchThreshold').value = t.t || 60000;
  document.getElementById('touchRepeat').value    = t.i || 200;
  document.getElementById('touchRepeatVal').textContent = repeatLabel(t.i || 200);
  setTouchMode(t.m || 0);
  onTouchBtnChange();
  buildTouchComboGrid(t.co || 0);
  updateTouchThreshLine();
  const badge = document.getElementById('touchComboBadge');
  const cnt = comboCount(t.co || 0);
  badge.textContent = cnt > 0 ? cnt + ' set' : '';
  badge.classList.toggle('visible', cnt > 0);
  if (t.co) {
    document.getElementById('touchComboPanel').classList.add('open');
    document.getElementById('touchComboChk').checked = true;
  }
}

function setTouchMode(mode) {
  ['hold','repeat','latch'].forEach((m,i) => {
    const pill = document.getElementById(`touchPill-${m}`);
    if (!pill) return;
    pill.classList.remove('active-hold','active-repeat','active-latch');
    if (i === mode) pill.classList.add(`active-${m}`);
  });
  document.getElementById('touchMode').value = mode;
  const isRepeat = mode === 1;
  const row = document.getElementById('touchSpeedRow');
  if (row) row.classList.toggle('hidden', !isRepeat);
}

function onTouchBtnChange() {
  const val = parseInt(document.getElementById('touchBtn').value);
  if (SPECIAL_ACTIONS.has(val)) {
    setTouchMode(0);
    document.querySelectorAll('#touchPill-hold,#touchPill-repeat,#touchPill-latch')
      .forEach(p => { p.style.opacity='0.3'; p.style.pointerEvents='none'; });
  } else {
    document.querySelectorAll('#touchPill-hold,#touchPill-repeat,#touchPill-latch')
      .forEach(p => { p.style.opacity=''; p.style.pointerEvents=''; });
  }
  buildTouchComboGrid(readTouchComboMask());
}

function buildTouchComboGrid(combo) {
  const grid = document.getElementById('touchComboGrid');
  if (!grid) return;
  const primaryVal = parseInt(document.getElementById('touchBtn').value);
  const labels = CONSOLE_BTN_LABELS[currentConsole];
  grid.innerHTML = '';
  for (let n = 1; n <= 16; n++) {
    const bit = n - 1;
    const checked = !!((combo >> bit) & 1);
    const isPrimary = n === primaryVal;
    const item = document.createElement('label');
    item.className = 'combo-btn-item' + (checked ? ' checked' : '');
    item.innerHTML = `<input type="checkbox" ${checked?'checked':''} ${isPrimary?'disabled':''} data-bit="${bit}" onchange="onTouchComboChange(this)"> ${labels[n].replace(/^Button\s*/i,'')}`;
    grid.appendChild(item);
  }
}

function readTouchComboMask() {
  const grid = document.getElementById('touchComboGrid');
  if (!grid) return 0;
  let mask = 0;
  grid.querySelectorAll('input[type=checkbox]').forEach(cb => {
    if (cb.checked) mask |= (1 << parseInt(cb.dataset.bit));
  });
  return mask;
}

function onTouchComboChange(cb) {
  const item = cb.closest('.combo-btn-item');
  if (item) item.classList.toggle('checked', cb.checked);
  const mask = readTouchComboMask();
  const badge = document.getElementById('touchComboBadge');
  if (badge) {
    const cnt = comboCount(mask);
    badge.textContent = cnt > 0 ? cnt + ' set' : '';
    badge.classList.toggle('visible', cnt > 0);
  }
}

function toggleTouchCombo() {
  const panel = document.getElementById('touchComboPanel');
  const chk   = document.getElementById('touchComboChk');
  if (!panel) return;
  const open = panel.classList.toggle('open');
  if (chk) chk.checked = open;
  if (open) buildTouchComboGrid(readTouchComboMask());
}

function updateTouchThreshLine() {
  const thr = parseInt(document.getElementById('touchThreshold').value) || 60000;
  const MAX_TOUCH = 200000;
  const pct = Math.min(100, Math.max(0, (thr / MAX_TOUCH) * 100));
  const line = document.getElementById('touchThreshLine');
  if (line) line.style.left = pct + '%';
}

function updateTouchLive(raw, active) {
  const fill = document.getElementById('touchLiveFill');
  const val  = document.getElementById('touchLiveVal');
  const dot  = document.getElementById('touchActiveDot');
  if (!fill) return;
  const MAX_TOUCH = 200000;
  // Bar fills left-to-right as raw value rises (higher = touched on S3)
  const pct = Math.min(100, Math.max(0, (raw / MAX_TOUCH) * 100));
  fill.style.width = pct + '%';
  if (val) val.textContent = raw;
  if (dot) dot.classList.toggle('on', active);
}

// ── Cap-touch 2 controls (pin 2) ──────────────────────────────────────────────
function populateTouch2(t) {
  document.getElementById('touch2Btn').innerHTML = btnOptions(t.b || 0);
  document.getElementById('touch2Threshold').value = t.t || 60000;
  document.getElementById('touch2Repeat').value    = t.i || 200;
  document.getElementById('touch2RepeatVal').textContent = repeatLabel(t.i || 200);
  setTouch2Mode(t.m || 0);
  onTouch2BtnChange();
  buildTouch2ComboGrid(t.co || 0);
  updateTouch2ThreshLine();
  const badge = document.getElementById('touch2ComboBadge');
  const cnt = comboCount(t.co || 0);
  badge.textContent = cnt > 0 ? cnt + ' set' : '';
  badge.classList.toggle('visible', cnt > 0);
  if (t.co) {
    document.getElementById('touch2ComboPanel').classList.add('open');
    document.getElementById('touch2ComboChk').checked = true;
  }
}

function setTouch2Mode(mode) {
  ['hold','repeat','latch'].forEach((m,i) => {
    const pill = document.getElementById(`touch2Pill-${m}`);
    if (!pill) return;
    pill.classList.remove('active-hold','active-repeat','active-latch');
    if (i === mode) pill.classList.add(`active-${m}`);
  });
  document.getElementById('touch2Mode').value = mode;
  const isRepeat = mode === 1;
  const row = document.getElementById('touch2SpeedRow');
  if (row) row.classList.toggle('hidden', !isRepeat);
}

function onTouch2BtnChange() {
  const val = parseInt(document.getElementById('touch2Btn').value);
  if (SPECIAL_ACTIONS.has(val)) {
    setTouch2Mode(0);
    document.querySelectorAll('#touch2Pill-hold,#touch2Pill-repeat,#touch2Pill-latch')
      .forEach(p => { p.style.opacity='0.3'; p.style.pointerEvents='none'; });
  } else {
    document.querySelectorAll('#touch2Pill-hold,#touch2Pill-repeat,#touch2Pill-latch')
      .forEach(p => { p.style.opacity=''; p.style.pointerEvents=''; });
  }
  buildTouch2ComboGrid(readTouch2ComboMask());
}

function buildTouch2ComboGrid(combo) {
  const grid = document.getElementById('touch2ComboGrid');
  if (!grid) return;
  const primaryVal = parseInt(document.getElementById('touch2Btn').value);
  const labels = CONSOLE_BTN_LABELS[currentConsole];
  grid.innerHTML = '';
  for (let n = 1; n <= 16; n++) {
    const bit = n - 1;
    const checked = !!((combo >> bit) & 1);
    const isPrimary = n === primaryVal;
    const item = document.createElement('label');
    item.className = 'combo-btn-item' + (checked ? ' checked' : '');
    item.innerHTML = `<input type="checkbox" ${checked?'checked':''} ${isPrimary?'disabled':''} data-bit="${bit}" onchange="onTouch2ComboChange(this)"> ${labels[n].replace(/^Button\s*/i,'')}`;
    grid.appendChild(item);
  }
}

function readTouch2ComboMask() {
  const grid = document.getElementById('touch2ComboGrid');
  if (!grid) return 0;
  let mask = 0;
  grid.querySelectorAll('input[type=checkbox]').forEach(cb => {
    if (cb.checked) mask |= (1 << parseInt(cb.dataset.bit));
  });
  return mask;
}

function onTouch2ComboChange(cb) {
  const item = cb.closest('.combo-btn-item');
  if (item) item.classList.toggle('checked', cb.checked);
  const mask = readTouch2ComboMask();
  const badge = document.getElementById('touch2ComboBadge');
  if (badge) {
    const cnt = comboCount(mask);
    badge.textContent = cnt > 0 ? cnt + ' set' : '';
    badge.classList.toggle('visible', cnt > 0);
  }
}

function toggleTouch2Combo() {
  const panel = document.getElementById('touch2ComboPanel');
  const chk   = document.getElementById('touch2ComboChk');
  if (!panel) return;
  const open = panel.classList.toggle('open');
  if (chk) chk.checked = open;
  if (open) buildTouch2ComboGrid(readTouch2ComboMask());
}

function updateTouch2ThreshLine() {
  const thr = parseInt(document.getElementById('touch2Threshold').value) || 60000;
  const MAX_TOUCH = 200000;
  const pct = Math.min(100, Math.max(0, (thr / MAX_TOUCH) * 100));
  const line = document.getElementById('touch2ThreshLine');
  if (line) line.style.left = pct + '%';
}

function updateTouch2Live(raw, active) {
  const fill = document.getElementById('touch2LiveFill');
  const val  = document.getElementById('touch2LiveVal');
  const dot  = document.getElementById('touch2ActiveDot');
  if (!fill) return;
  const MAX_TOUCH = 200000;
  const pct = Math.min(100, Math.max(0, (raw / MAX_TOUCH) * 100));
  fill.style.width = pct + '%';
  if (val) val.textContent = raw;
  if (dot) dot.classList.toggle('on', active);
}

// ── Gather payload ────────────────────────────────────────────────────────────
// For each sensor/side, the "button" field is taken from:
//   - digital mode (0/1/2): the regular button selector  (pb- / sb-)
//   - analog mode (3):      the analog button selector   (pab- / sab-)
// This keeps the hidden selector values independent so switching modes
// doesn't accidentally send a stale button value to the firmware.
function gatherPayload() {
  const p={};
  keys.forEach(k=>{
    const pm = parseInt(document.getElementById(`pm-${k}`).value);
    const sm = parseInt(document.getElementById(`sm-${k}`).value);
    const pBtn = pm === 3
      ? parseInt(document.getElementById(`pab-${k}`).value)
      : parseInt(document.getElementById(`pb-${k}`).value);
    const sBtn = sm === 3
      ? parseInt(document.getElementById(`sab-${k}`).value)
      : parseInt(document.getElementById(`sb-${k}`).value);
    p[k]={
      p:{
        b: pBtn,
        m: pm,
        i: parseInt(document.getElementById(`pi-${k}`).value),
        a: parseInt(document.getElementById(`pa-${k}`).value),
        c: parseInt(document.getElementById(`pc-${k}`).value),
        d: parseInt(document.getElementById(`pd-${k}`).value),
        r: parseInt(document.getElementById(`pr-${k}`).value),
        v: document.getElementById(`pv-${k}`).checked,
        co: readComboMask('p', k),
      },
      s:{
        b: sBtn,
        m: sm,
        i: parseInt(document.getElementById(`si-${k}`).value),
        a: parseInt(document.getElementById(`sa-${k}`).value),
        c: parseInt(document.getElementById(`sc-${k}`).value),
        d: parseInt(document.getElementById(`sd-${k}`).value),
        r: parseInt(document.getElementById(`sr-${k}`).value),
        v: document.getElementById(`sv-${k}`).checked,
        co: readComboMask('s', k),
      },
      pt: parseInt(document.getElementById(`pt-${k}`).value),
      st: parseInt(document.getElementById(`st-${k}`).value),
    };
  });
  p.thumb = {
    axisX:    parseInt(document.getElementById('thumbAxisX').value),
    axisY:    parseInt(document.getElementById('thumbAxisY').value),
    deadzone: parseInt(document.getElementById('thumbDeadzone').value) || 0,
    invertX:  document.getElementById('thumbInvertX').checked,
    invertY:  document.getElementById('thumbInvertY').checked,
  };
  p.touch = {
    b:  parseInt(document.getElementById('touchBtn').value),
    m:  parseInt(document.getElementById('touchMode').value),
    i:  parseInt(document.getElementById('touchRepeat').value),
    t:  parseInt(document.getElementById('touchThreshold').value) || 60000,
    co: readTouchComboMask(),
  };
  p.touch2 = {
    b:  parseInt(document.getElementById('touch2Btn').value),
    m:  parseInt(document.getElementById('touch2Mode').value),
    i:  parseInt(document.getElementById('touch2Repeat').value),
    t:  parseInt(document.getElementById('touch2Threshold').value) || 60000,
    co: readTouch2ComboMask(),
  };
  return p;
}

// ── Profile UI ────────────────────────────────────────────────────────────────
let dragSrcSlot = null;

async function refreshProfiles() {
  const list = await (await fetch('/profiles')).json();
  const container = document.getElementById('profileSlots');
  container.innerHTML = '';

  if (list.length === 0) {
    const hint = document.createElement('div');
    hint.className = 'profile-empty-hint';
    hint.textContent = 'No saved profiles yet — configure and save one below.';
    container.appendChild(hint);
    return;
  }

  list.forEach(p => {
    const isActive = p.name === activeProfileName;
    const div = document.createElement('div');
    div.className = 'profile-slot occupied' + (isActive ? ' active-profile' : '');
    div.draggable = true;
    div.dataset.slot = p.slot;

    const handle = document.createElement('span');
    handle.className = 'drag-handle';
    handle.title = 'Drag to reorder';
    handle.textContent = '⠿';

    const loadBtn = document.createElement('button');
    loadBtn.className = 'slot-load';
    loadBtn.innerHTML = `<span class="slot-name">${p.name}</span>`;
    loadBtn.addEventListener('click', () => loadProfileSlot(p.slot, p.name));

    const delBtn = document.createElement('button');
    delBtn.className = 'slot-del';
    delBtn.textContent = '✕';
    delBtn.addEventListener('click', e => {
      e.stopPropagation();
      if (confirm(`Delete "${p.name}"?`)) deleteProfileSlot(p.slot);
    });

    div.addEventListener('dragstart', e => {
      dragSrcSlot = p.slot;
      e.dataTransfer.effectAllowed = 'move';
      setTimeout(() => div.classList.add('dragging'), 0);
    });
    div.addEventListener('dragend', () => {
      div.classList.remove('dragging');
      container.querySelectorAll('.profile-slot').forEach(el => el.classList.remove('drag-over'));
    });
    div.addEventListener('dragover', e => {
      e.preventDefault();
      e.dataTransfer.dropEffect = 'move';
      container.querySelectorAll('.profile-slot').forEach(el => el.classList.remove('drag-over'));
      if (parseInt(div.dataset.slot) !== dragSrcSlot) div.classList.add('drag-over');
    });
    div.addEventListener('dragleave', () => div.classList.remove('drag-over'));
    div.addEventListener('drop', async e => {
      e.preventDefault();
      div.classList.remove('drag-over');
      const destSlot = parseInt(div.dataset.slot);
      if (dragSrcSlot === null || dragSrcSlot === destSlot) return;
      await reorderProfiles(dragSrcSlot, destSlot);
      dragSrcSlot = null;
    });

    div.appendChild(handle);
    div.appendChild(loadBtn);
    div.appendChild(delBtn);
    container.appendChild(div);
  });
}

async function reorderProfiles(srcSlot, destSlot) {
  const container = document.getElementById('profileSlots');
  const slots = [...container.querySelectorAll('.profile-slot[data-slot]')]
    .map(el => parseInt(el.dataset.slot));

  const srcIdx  = slots.indexOf(srcSlot);
  const destIdx = slots.indexOf(destSlot);
  if (srcIdx < 0 || destIdx < 0) return;

  slots.splice(srcIdx, 1);
  slots.splice(destIdx, 0, srcSlot);

  setStatus('Saving order…', '');
  try {
    await fetch('/profile/reorder', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({order: slots}),
    });
    await refreshProfiles();
    setStatus('✓ Order saved', 'ok');
  } catch(e) {
    setStatus('✗ Reorder failed', 'err');
  }
}

async function loadProfileSlot(slot,name){
  setStatus('Loading…','');
  try{
    const d=await (await fetch(`/profile/load?slot=${slot}`)).json();
    if(d.ok){
      buildCards(d.cfg);
      populateThumb(d.cfg.thumb || {axisX:0, axisY:1, deadzone:80, invertX:false, invertY:false});
      populateTouch(d.cfg.touch   || {b:0, m:0, i:200, t:60000, co:0});
      populateTouch2(d.cfg.touch2 || {b:0, m:0, i:200, t:60000, co:0});
      activeProfileName=name;
      document.getElementById('active-profile-name').textContent=name;
      await refreshProfiles();
      setStatus(`✓ Loaded "${name}"`,'ok');
    } else setStatus('✗ Load failed','err');
  }catch(e){setStatus('✗ Network error','err');}
}

async function deleteProfileSlot(slot){
  await fetch(`/profile?slot=${slot}`,{method:'DELETE'});
  await new Promise(r => setTimeout(r, 150));
  const list=await (await fetch('/profiles')).json();
  if(!list.find(p=>p.name===activeProfileName)){
    activeProfileName='Unsaved';
    document.getElementById('active-profile-name').textContent='Unsaved';
  }
  refreshProfiles();
}

function openSaveModal(slot){
  pendingSaveSlot=slot;
  document.getElementById('modalNameInput').value='';
  document.getElementById('modalBg').classList.add('open');
  document.getElementById('modalNameInput').focus();
}
document.getElementById('modalCancel').addEventListener('click',()=>
  document.getElementById('modalBg').classList.remove('open'));
document.getElementById('modalConfirm').addEventListener('click',async()=>{
  let name=document.getElementById('modalNameInput').value.trim();
  if(!name) name=`Profile ${pendingSaveSlot+1}`;
  document.getElementById('modalBg').classList.remove('open');
  await commitSaveProfile(pendingSaveSlot,name);
});
document.getElementById('modalNameInput').addEventListener('keydown',e=>{
  if(e.key==='Enter') document.getElementById('modalConfirm').click();
  if(e.key==='Escape') document.getElementById('modalCancel').click();
});
document.getElementById('btnSaveProfile').addEventListener('click',async()=>{
  const list=await (await fetch('/profiles')).json();
  const used=new Set(list.map(p=>p.slot));
  let free=-1;
  for(let s=0;s<MAX_SLOTS;s++){if(!used.has(s)){free=s;break;}}
  if(free===-1){setStatus('✗ All 8 slots full','err');return;}
  openSaveModal(free);
});

async function commitSaveProfile(slot, name){
  if (slot < 0) { setStatus('✗ No slot selected', 'err'); return; }
  try {
    const payload = gatherPayload();
    const r = await fetch('/profile/save', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({slot, name, cfg: payload}),
    });
    if (!r.ok) { setStatus('✗ Profile save failed', 'err'); return; }
    await new Promise(res => setTimeout(res, 150));
    activeProfileName = name;
    document.getElementById('active-profile-name').textContent = name;
    await refreshProfiles();
    setStatus(`✓ Saved as "${name}"`, 'ok');
  } catch(e) {
    setStatus('✗ Network error', 'err');
  }
}

document.getElementById('saveBtn').addEventListener('click', async () => {
  try {
    const r = await fetch('/save', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(gatherPayload())});
    const j = await r.json();
    setStatus(j.ok ? '✓ Config applied' : '✗ Save failed', j.ok ? 'ok' : 'err');
  } catch(e) { setStatus('✗ Network error', 'err'); }
});

document.getElementById('calibBtn').addEventListener('click',async()=>{
  setStatus('Recalibrating…','');
  try{await fetch('/calibrate');}catch(e){}
  setStatus('✓ Calibration triggered','ok');
});

function setStatus(msg,cls){
  const el=document.getElementById('status');
  el.className=cls; el.textContent=msg;
  if(cls) setTimeout(()=>{el.className='';el.textContent='Polling sensors\u2026';},3000);
}

// ── Live polling ──────────────────────────────────────────────────────────────
async function poll(){
  try{
    const d=await (await fetch('/readings')).json();
    keys.forEach((k,i)=>{
      const raw=d[k];
      const pt=parseInt(document.getElementById(`pt-${k}`)?.value||100000);
      const st=parseInt(document.getElementById(`st-${k}`)?.value||100000);

      const pPct=raw>0?Math.min(50,(raw/MAX_DISP)*50):0;
      const pBar=document.getElementById(`pbar-${k}`);
      if(pBar){pBar.style.width=pPct+'%';pBar.style.opacity=raw>=pt?'1':'0.5';}

      const sPct=raw<0?Math.min(50,(Math.abs(raw)/MAX_DISP)*50):0;
      const sBar=document.getElementById(`sbar-${k}`);
      if(sBar){
        sBar.style.width=sPct+'%';sBar.style.left=(50-sPct)+'%';
        sBar.style.opacity=(Math.abs(raw)>=st&&raw<0)?'1':'0.5';
      }

      const v=document.getElementById(`val-${k}`);
      if(v) v.textContent=(raw>=0?'+':'')+fmtN(raw);

      if(d.l){
        const pRow=document.getElementById(`prow-${k}`);
        const sRow=document.getElementById(`srow-${k}`);
        if(pRow) pRow.classList.toggle('latch-active',!!d.l[i*2]);
        if(sRow) sRow.classList.toggle('latch-active',!!d.l[i*2+1]);
      }
    });
    const tx = (typeof d.tx === 'number') ? d.tx : 0;
    const ty = (typeof d.ty === 'number') ? d.ty : 0;
    updateThumbViz(tx, ty);
    updateThumbBars(tx, ty);
    if (typeof d.touch === 'number') {
      updateTouchLive(d.touch, !!d.touchActive);
    }
    if (typeof d.touch2 === 'number') {
      updateTouch2Live(d.touch2, !!d.touch2Active);
    }
  }catch(e){}
  setTimeout(poll,350);
}

// ── Tab switching ─────────────────────────────────────────────────────────────
function switchTab(name) {
  document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
  document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
  document.getElementById('tab-' + name).classList.add('active');
  document.querySelectorAll('.tab-btn').forEach(b => {
    if (b.getAttribute('onclick') === "switchTab('" + name + "')")
      b.classList.add('active');
  });
  if (name === 'network') { wifiStatusPoll(); }
  if (name === 'display') { loadColors(); loadLed(); }
}

function onColorChange(pickerId, hexId, dotId) {
  const val = document.getElementById(pickerId).value;
  document.getElementById(hexId).textContent = val;
  document.getElementById(dotId).style.background = val;
}

async function loadColors() {
  try {
    const d = await (await fetch('/colors/get')).json();
    const map = { puff:         ['colorPuff',         'hexPuff',         'dotPuff'],
                  sip:          ['colorSip',          'hexSip',          'dotSip'],
                  off:          ['colorOff',          'hexOff',          'dotOff'],
                  profileName:  ['colorProfileName',  'hexProfileName',  'dotProfileName'],
                  profileLabel: ['colorProfileLabel', 'hexProfileLabel', 'dotProfileLabel'],
                  btnLabel:     ['colorBtnLabel',     'hexBtnLabel',     'dotBtnLabel'],
                  bg:           ['colorBg',           'hexBg',           'dotBg'],
                  iconBorder:   ['colorIconBorder',   'hexIconBorder',   'dotIconBorder'],
                  touch1:       ['colorTouch1',       'hexTouch1',       'dotTouch1'],
                  touch2:       ['colorTouch2',       'hexTouch2',       'dotTouch2'] };
    for (const [key, [pick, hex, dot]] of Object.entries(map)) {
      const v = d[key] || '#000000';
      document.getElementById(pick).value = v;
      document.getElementById(hex).textContent = v;
      document.getElementById(dot).style.background = v;
    }
  } catch(e) {
    document.getElementById('colorStatus').className = 'err';
    document.getElementById('colorStatus').textContent = '✗ Could not load colors from device';
  }
}

async function saveColors() {
  const payload = {
    puff:         document.getElementById('colorPuff').value,
    sip:          document.getElementById('colorSip').value,
    off:          document.getElementById('colorOff').value,
    profileName:  document.getElementById('colorProfileName').value,
    profileLabel: document.getElementById('colorProfileLabel').value,
    btnLabel:     document.getElementById('colorBtnLabel').value,
    bg:           document.getElementById('colorBg').value,
    iconBorder:   document.getElementById('colorIconBorder').value,
    touch1:       document.getElementById('colorTouch1').value,
    touch2:       document.getElementById('colorTouch2').value,
  };
  const st = document.getElementById('colorStatus');
  st.className = ''; st.textContent = 'Saving\u2026';
  try {
    const r = await fetch('/colors/set', {
      method: 'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify(payload),
    });
    const j = await r.json();
    st.className = j.ok ? 'ok' : 'err';
    st.textContent = j.ok ? '✓ Colors saved and applied' : '✗ Save failed';
    if (j.ok) setTimeout(() => { st.className=''; st.textContent=''; }, 3000);
  } catch(e) {
    st.className = 'err'; st.textContent = '✗ Network error';
  }
}

// ── WiFi helpers ──────────────────────────────────────────────────────────────
let wifiStatusTimer = null;

function rssiBar(rssi) {
  const cls = rssi >= -60 ? 'strong' : rssi >= -75 ? 'medium' : 'weak';
  const h = [4, 7, 11, 15];
  const lit = rssi >= -60 ? 4 : rssi >= -75 ? 3 : rssi >= -85 ? 2 : 1;
  return `<span class="wifi-rssi-bar ${cls}">`
    + h.map((ht, i) => `<span style="height:${ht}px;${i < lit ? '' : 'opacity:.25'}"></span>`).join('')
    + '</span>';
}

async function wifiStatusPoll() {
  clearTimeout(wifiStatusTimer);
  try {
    const d = await (await fetch('/wifi/status')).json();
    const dot  = document.getElementById('wifiDot');
    const txt  = document.getElementById('wifiStatusText');
    const det  = document.getElementById('wifiStatusDetail');
    const fBtn = document.getElementById('btnWifiForget');
    dot.className = 'wifi-dot';
    if (d.state === 'connected') {
      dot.classList.add('connected');
      txt.textContent = 'Connected to ' + d.ssid;
      det.textContent = 'IP: ' + d.ip + '  RSSI: ' + d.rssi + ' dBm  MAC: ' + d.mac;
      fBtn.style.display = '';
    } else if (d.state === 'failed') {
      dot.classList.add('failed');
      txt.textContent = 'Connection failed — bad credentials?';
      det.textContent = '';
      fBtn.style.display = '';
    } else {
      txt.textContent = 'Not connected to a local network';
      det.textContent = d.mac ? 'MAC: ' + d.mac : '';
      fBtn.style.display = 'none';
    }
  } catch(e) {}
  wifiStatusTimer = setTimeout(wifiStatusPoll, 4000);
}

let currentConnectedSSID = '';

async function wifiScan() {
  const btn = document.getElementById('btnWifiScan');
  const container = document.getElementById('wifiNetworks');
  btn.textContent = 'Scanning\u2026'; btn.disabled = true;
  container.innerHTML = '<div style="font-family:\'Share Tech Mono\',monospace;font-size:.75rem;color:var(--dim)">Scanning\u2026</div>';
  try {
    const nets = await (await fetch('/wifi/scan')).json();
    try {
      const st = await (await fetch('/wifi/status')).json();
      currentConnectedSSID = st.state === 'connected' ? st.ssid : '';
    } catch(e) { currentConnectedSSID = ''; }

    if (nets.length === 0) {
      container.innerHTML = '<div style="font-family:\'Share Tech Mono\',monospace;font-size:.75rem;color:var(--dim)">No networks found.</div>';
    } else {
      nets.sort((a,b) => b.rssi - a.rssi);
      container.innerHTML = '';
      nets.forEach(n => {
        const row = document.createElement('div');
        const isConn = n.ssid === currentConnectedSSID;
        row.className = 'wifi-net-row' + (isConn ? ' connected-net' : '');
        row.innerHTML = rssiBar(n.rssi)
          + `<span class="wifi-net-ssid">${n.ssid}${isConn ? ' ✓' : ''}</span>`
          + (n.enc ? '<span class="wifi-net-lock">&#x1F512;</span>' : '');
        row.addEventListener('click', () => {
          document.querySelectorAll('.wifi-net-row').forEach(r => r.classList.remove('selected'));
          row.classList.add('selected');
          document.getElementById('wifiSSID').value = n.ssid;
          document.getElementById('wifiPass').focus();
        });
        container.appendChild(row);
      });
    }
  } catch(e) {
    container.innerHTML = '<div style="font-family:\'Share Tech Mono\',monospace;font-size:.75rem;color:var(--sip)">Scan failed.</div>';
  }
  btn.textContent = 'Scan'; btn.disabled = false;
}

async function wifiConnect() {
  const ssid = document.getElementById('wifiSSID').value.trim();
  const pass = document.getElementById('wifiPass').value;
  const msg  = document.getElementById('wifiMsg');
  if (!ssid) { msg.className = 'err'; msg.textContent = '✗ Enter a network name.'; return; }
  msg.className = ''; msg.textContent = 'Connecting\u2026';
  try {
    await fetch('/wifi/connect', {
      method: 'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify({ssid, pass})
    });
    msg.textContent = 'Connecting — checking status in 6 s\u2026';
    setTimeout(async () => {
      await wifiStatusPoll();
      const st = await (await fetch('/wifi/status')).json();
      if (st.state === 'connected') {
        msg.className = 'ok'; msg.textContent = '✓ Connected! IP: ' + st.ip;
      } else {
        msg.className = 'err'; msg.textContent = '✗ Could not connect. Check credentials.';
      }
    }, 6000);
  } catch(e) { msg.className = 'err'; msg.textContent = '✗ Request failed.'; }
}

async function wifiForget() {
  const msg = document.getElementById('wifiMsg');
  try {
    await fetch('/wifi/forget', {method:'POST'});
    msg.className = 'ok'; msg.textContent = '✓ Network forgotten.';
    setTimeout(wifiStatusPoll, 800);
  } catch(e) { msg.className = 'err'; msg.textContent = '✗ Request failed.'; }
}


// ── LED controls ──────────────────────────────────────────────────────────────
let ledOn = true;

function onLedColorChange() {
  const val = document.getElementById('ledColor').value;
  document.getElementById('ledHex').textContent = val;
  const prev = document.getElementById('ledPreview');
  prev.style.background = val;
  prev.style.boxShadow = ledOn ? `0 0 14px ${val}` : 'none';
}

function onLedBriChange() {
  document.getElementById('ledBriVal').textContent =
    document.getElementById('ledBri').value;
}

function applyLedPowerUI() {
  const btn = document.getElementById('ledPowerBtn');
  const lbl = document.getElementById('ledPowerLabel');
  const prev = document.getElementById('ledPreview');
  const color = document.getElementById('ledColor').value;
  btn.classList.toggle('led-on', ledOn);
  btn.style.color = ledOn ? color : 'var(--dim)';
  btn.style.borderColor = ledOn ? color : 'var(--border)';
  btn.style.boxShadow = ledOn ? `0 0 16px ${color}55` : 'none';
  prev.style.boxShadow = ledOn ? `0 0 14px ${color}` : 'none';
  lbl.textContent = ledOn ? 'ON' : 'OFF';
}

function toggleLed() {
  ledOn = !ledOn;
  applyLedPowerUI();
  // Send immediately so the LED responds without needing Save
  const payload = {
    on: ledOn,
    color: document.getElementById('ledColor').value,
    brightness: parseInt(document.getElementById('ledBri').value),
  };
  fetch('/led/set', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) });
}

async function loadLed() {
  try {
    const d = await (await fetch('/led/get')).json();
    ledOn = d.on;
    const color = d.color || '#00ffc8';
    document.getElementById('ledColor').value = color;
    document.getElementById('ledHex').textContent = color;
    document.getElementById('ledPreview').style.background = color;
    document.getElementById('ledBri').value = d.brightness ?? 128;
    document.getElementById('ledBriVal').textContent = d.brightness ?? 128;
    applyLedPowerUI();
  } catch(e) {}
}

async function saveLed() {
  const payload = {
    on: ledOn,
    color: document.getElementById('ledColor').value,
    brightness: parseInt(document.getElementById('ledBri').value),
  };
  const st = document.getElementById('ledStatus');
  st.className = ''; st.textContent = 'Saving\u2026';
  try {
    const r = await fetch('/led/set', {
      method: 'POST',
      headers: {'Content-Type':'application/json'},
      body: JSON.stringify(payload),
    });
    const j = await r.json();
    st.className = j.ok ? 'ok' : 'err';
    st.textContent = j.ok ? '\u2713 LED settings saved' : '\u2717 Save failed';
    if (j.ok) setTimeout(() => { st.className=''; st.textContent=''; }, 3000);
  } catch(e) {
    st.className = 'err'; st.textContent = '\u2717 Network error';
  }
}

// ── Init ──────────────────────────────────────────────────────────────────────
refreshProfiles();
poll();
</script>
</body>
</html>
)rawhtml";
