<!-- SS · part 44 -->
ut name="preview_start" type="number" min="0" max="1800" step="1" value="' + esc(s.preview_start) + '"></label>' +
      '</div>' +
      '<label class="es-sell-full">Description<textarea name="description" rows="3" maxlength="2000">' + esc(s.description) + '</textarea></label>' +
      '<label class="es-sell-full">License<textarea name="license" rows="2" maxlength="1000">' + esc(s.license) + '</textarea></label>' +
      '<div class="es-sell-cover"><div class="es-sell-art">' + (s.cover_url ? '<img src="' + esc(s.cover_url) + '" alt="cover">' : '<span>no cover yet</span>') + '</div>' +
      '<div><label class="es-sell-sw"><input type="radio" name="cover_kind" value="template"' + (s.cover_kind !== 'upload' ? ' checked' : '') + '> Template cover (title, red/black/gold)</label>' +
      '<label class="es-sell-sw"><input type="radio" name="cover_kind" value="upload"' + (s.cover_kind === 'upload' ? ' checked' : '') + '> My own image</label>' +
      '<input type="file" name="cover" accept="image/jpeg,image/png,image/webp" id="es-sell-file"></div></div>' +
      (s.preview_url ? '<div class="es-sell-pv">30-second public preview:<audio controls preload="none" src="' + esc(s.preview_url) + '"></audio></div>' : '') +
      '<div class="es-hint">' + (j.orders ? j.orders + ' sale(s), ' + esc(j.revenue) + ' · ' : '') + 'Saving makes the 30-second preview and the cover. Buyers get the full MP3 + WAV only after Stripe confirms payment.</div>' +
      '<div class="es-prompt-row"><button type="button" class="es-btn es-btn-red" id="es-sell-save">Save</button>' +
      '<a class="es-btn" href="' + esc(s.page_url) + '" target="_blank" rel="noopener" id="es-sell-page">Open song page ↗</a>' +
      '<button class="es-btn" value="cancel">Close</button></div></form>';
    dlg.querySelector('#es-sell-file').addEventListener('change', function () { if (this.files.length) dlg.querySelector('input[name=cover_kind][value=upload]').checked = true; });
    dlg.querySelector('#es-sell-save').addEventListener('click', save);
  }
  function open(id) {
    ES().api('GET', '/api/ss/songs/' + id + '/sale').then(function (j) { render(j); if (!dlg.open) dlg.showModal(); })
      .catch(function (e) { ES().toast(e.message); });
  }
  function save() {
    var f = dlg.querySelector('#es-sell-form'), b = this, fd = new FormData();
    fd.append('for_sale', f.for_sale.checked ? '1' : '0');
    ['price', 'visibility', 'artist', 'preview_start', 'description', 'license'].forEach(function (k) { fd.append(k, f[k].value); });
    var kind = f.querySelector('input[name=cover_kind]:checked'); fd.append('cover_kind', kind ? kind.value : 'template');
    var file = f.cover.files[0]; if (file && kind && kind.value === 'upload') fd.append('cover', file);
    if (kind && kind.value === 'upload' && !file && !(cur && cur.cover_url)) { ES().toast('Choose an image, or use the template cover.'); return; }
    ES().busy(b, true);
    ES().api('POST', '/api/ss/songs/' + cur.song_id + '/sale', fd).then(function (j) {
      ES().toast(j.sale.for_sale ? 'Listed for sale at $' + j.sale.price + '.' : 'Selling settings saved.');
      return ES().api('GET', '/api/ss/songs/' + cur.song_id + '/sale').then(render);
    }).catch(function (e) { ES().toast(e.message, 5000); }).then(function () { var x = dlg.querySelector('#es-sell-save'); if (x) ES().busy(x, false); });
  }
  document.addEventListener('click', function (e) {
    var b = e.target.closest && e.target.closest('#es-songlist [data-act="sell"]'); if (!b) return;
    var li = b.closest('.es-song'); if (li) open(li.getAttribute('data-id'));
  });
})();


===== FILE: public/ss/studio-sing.js =====
/* Epistles Studio — SING (admin only): melody per syllable, sung by Kokoro + WORLD on the server. Hands-on note editor. */
(function () {
  'use strict';
  var host = document.getElementById('es-sing'); if (!host) return;
  var root = document.getElementById('es'); if (!root || root.getAttribute('data-admin') !== '1') { host.remove(); return; }
  var ES = null; function $(s) { return host.querySelector(s); }
  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]; }); }
  var NN = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']; function nm(m) { return NN[m % 12] + (Math.floor(m / 12) - 1); }
  var PK = 'es_sing_presets_v1', MK = 'es_melody_v1', GK = 'es_sing_params_v1';
  var DEF = { robot: 0.8, vib: 35, vibRate: 5.5, slide: 60, formant: 0, bright: 0, breath: 0.1, cons: 0.3, engine: 'world', lo: 48, hi: 64 };
  var BUILTIN = { 'Natural': { robot: 0.45, vib: 30, vibRate: 5.2, slide: 90, breath: 0.15, bright: 0, formant: 0, cons: 0.3 }, 'Hard tune (robot)': { robot: 1, vib: 0, vibRate: 5.5, slide: 15, breath: 0.05, bright: 2, formant: 0, cons: 0.25 },
    'Breathy soul': { robot: 0.6, vib: 55, vibRate: 5, slide: 120, breath: 0.45, bright: -2, formant: -0.5, cons: 0.3 }, '