PageSourceSearch

https://andersaamodt.com/static/post-context.js?v=1376469157

js andersaamodt.com collected 2026-10-08 10:31:48 UTC 93,153 bytes, 2,446 lines download raw bytes

1(function () {
2  var currentRelPath = '';
3  var currentNostrAddress = '';
4  var currentNostrEventId = '';
5  var currentNostrPubkey = '';
6  var currentNostrKind = '';
7  var currentNostrD = '';
8  var currentNostrRelays = [];
9  var refreshInFlight = false;
10  var submitInFlight = false;
11  var postMenuBusy = false;
12  var lastPostMenuTouchAt = 0;
13  var addToListState = {
14    postPath: '',
15    loading: false,
16    submitting: false,
17    lists: [],
18    message: '',
19    tone: ''
20  };
21  var inlineEditState = {
22    open: false,
23    busy: false,
24    draftId: '',
25    sourcePostPath: '',
26    postFilename: '',
27    postFilenameEditing: false,
28    postType: 'longform',
29    title: '',
30    content: '',
31    tags: '',
32    tagList: [],
33    tagsDraftText: '',
34    output: '',
35    outputTone: '',
36    autosaveTimer: null,
37    saveStatus: '',
38    dirtyDuringSave: false
39  };
40
41  function overflowMenuIconSvg() {
42    return '<svg class="overflow-menu-icon-svg" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="5.5" r="1.9" fill="currentColor"/><circle cx="12" cy="12" r="1.9" fill="currentColor"/><circle cx="12" cy="18.5" r="1.9" fill="currentColor"/></svg>';
43  }
44
45  function isPostPage(pathname) {
46    var path = String(pathname || '');
47    if (/^\/posts\/[^/?#]+\/?$/.test(path)) {
48      return true;
49    }
50    if (/^\/pages\/posts\/[^/?#]+(?:\.html?)?\/?$/.test(path)) {
51      return true;
52    }
53    if (path === '/cgi/blog-open-post' || path.indexOf('/cgi/blog-open-post/') === 0) {
54      return true;
55    }
56    return false;
57  }
58
59  function normalizePostMdPath(raw) {
60    var value = String(raw || '').trim();
61    if (!value) {
62      return '';
63    }
64    value = value
65      .replace(/^https?:\/\/[^/]+\//, '')
66      .replace(/^\/+/, '')
67      .replace(/^pages\//, '')
68      .replace(/^posts\//, '');
69    if (!value) {
70      return '';
71    }
72    if (/\.html?$/i.test(value)) {
73      value = value.replace(/\.html?$/i, '');
74    } else if (/\.md$/i.test(value)) {
75      value = value.replace(/\.md$/i, '');
76    }
77    value = value.replace(/^\/*/, '').replace(/\/*$/, '');
78    if (!value || value.indexOf('..') !== -1 || value.indexOf('\\') !== -1) {
79      return '';
80    }
81    return 'posts/' + value + '.md';
82  }
83
84  function normalizePostFilename(raw) {
85    var value = String(raw || '').trim();
86    if (!value) {
87      return '';
88    }
89    value = value
90      .replace(/^https?:\/\/[^/]+\//i, '')
91      .replace(/^\/+/, '')
92      .replace(/^pages\//i, '')
93      .replace(/^posts\//i, '');
94    if (value.indexOf('/') >= 0) {
95      value = value.split('/').pop();
96    }
97    value = value.replace(/\.html?$/i, '').replace(/\.md$/i, '');
98    value = value.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
99    return value;
100  }
101
102  function postSlugFromMdPath(mdPath) {
103    var normalized = normalizePostMdPath(mdPath);
104    if (!normalized) {
105      return '';
106    }
107    return normalized.replace(/^posts\//, '').replace(/\.md$/i, '');
108  }
109
110  function maybeRepairWrongPostShell(mdPath) {
111    var slug = postSlugFromMdPath(mdPath);
112    if (!slug) {
113      return false;
114    }
115    if (window.__wizardryPostRouteRepairing) {
116      return true;
117    }
118    window.__wizardryPostRouteRepairing = true;
119    var endpoint = '/cgi/blog-open-post?path=' + encodeURIComponent(slug) + '&__route_repair=1';
120    fetch(endpoint, { credentials: 'same-origin', cache: 'no-store' })
121      .then(function (res) {
122        if (!res.ok) {
123          throw new Error('repair fetch failed');
124        }
125        return res.text();
126      })
127      .then(function (html) {
128        var text = String(html || '');
129        if (text.indexOf('<html') === -1) {
130          throw new Error('repair payload invalid');
131        }
132        document.open();
133        document.write(text);
134        document.close();
135      })
136      .catch(function () {
137        window.location.replace(endpoint);
138      });
139    return true;
140  }
141
142  function filenameFromPostPath(rawPath) {
143    return normalizePostFilename(normalizePostMdPath(rawPath));
144  }
145
146  function escapeHtml(value) {
147    return String(value)
148      .replace(/&/g, '&amp;')
149      .replace(/</g, '&lt;')
150      .replace(/>/g, '&gt;')
151      .replace(/"/g, '&quot;')
152      .replace(/'/g, '&#39;');
153  }
154
155  function longTimestampTitle(raw) {
156    var value = String(raw || '').trim();
157    if (!value || value.indexOf('T') < 0) {
158      return '';
159    }
160    var date = new Date(value);
161    if (!isFinite(date.getTime())) {
162      return value;
163    }
164    try {
165      return new Intl.DateTimeFormat('en-US', {
166        month: 'long',
167        day: 'numeric',
168        year: 'numeric',
169        hour: 'numeric',
170        minute: '2-digit',
171        second: '2-digit',
172        timeZone: 'UTC',
173        timeZoneName: 'short'
174      }).format(date);
175    } catch (_err) {
176      return value;
177    }
178  }
179
180  function postDateHtml(current, className) {
181    var label = String(current && current.published_date || '').trim();
182    var explicit = String(current && current.published_timestamp || '').trim();
183    var title = explicit || longTimestampTitle(current && current.published_at);
184    return '<span class="' + escapeHtml(className || 'post-date') + '"' + (title ? ' title="' + escapeHtml(title) + '"' : '') + '>' + escapeHtml(label) + '</span>';
185  }
186
187  function postHeaderMetaHtml(current) {
188    return '' +
189      '<div class="post-head-divider" aria-hidden="true"></div>' +
190      '<div class="post-byline post-byline-bottom">' +
191      '<span class="post-author">' + escapeHtml(current.author || 'Blog Author') + '</span>' +
192      '<span class="post-reading-inline">' + escapeHtml(String(current.reading_minutes || 1)) + ' min read</span>' +
193      postDateHtml(current, 'post-date') +
194      '</div>';
195  }
196
197  function normalizeListSlug(raw) {
198    return String(raw || '')
199      .trim()
200      .toLowerCase()
201      .replace(/[^a-z0-9-]+/g, '-')
202      .replace(/-+/g, '-')
203      .replace(/^-|-$/g, '');
204  }
205
206  function getSessionToken() {
207    try {
208      return String(localStorage.getItem('session_token') || '').trim();
209    } catch (_err) {
210      return '';
211    }
212  }
213
214  function getCsrfToken() {
215    try {
216      return String(localStorage.getItem('csrf_token') || '').trim();
217    } catch (_err) {
218      return '';
219    }
220  }
221
222  function postForm(path, payload) {
223    var params = new URLSearchParams();
224    Object.keys(payload || {}).forEach(function (key) {
225      var value = payload[key];
226      if (value === undefined || value === null) {
227        return;
228      }
229      params.set(key, String(value));
230    });
231    return fetch(path, {
232      method: 'POST',
233      headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
234      credentials: 'same-origin',
235      body: params.toString()
236    }).then(function (res) { return res.json(); });
237  }
238
239  function closePostPageMenu() {
240    var panel = document.querySelector('.post-page-menu-panel');
241    var trigger = document.querySelector('.post-page-menu-trigger');
242    if (panel) {
243      panel.hidden = true;
244    }
245    if (trigger) {
246      trigger.setAttribute('aria-expanded', 'false');
247    }
248  }
249
250  function togglePostPageMenu() {
251    var panel = document.querySelector('.post-page-menu-panel');
252    var trigger = document.querySelector('.post-page-menu-trigger');
253    var menu = trigger ? trigger.closest('.post-page-menu') : null;
254    if (!panel || !trigger || (menu instanceof HTMLElement && menu.hidden)) {
255      return;
256    }
257    var nextOpen = !!panel.hidden;
258    panel.hidden = !nextOpen;
259    trigger.setAttribute('aria-expanded', nextOpen ? 'true' : 'false');
260  }
261
262  function activatePostPageMenuFromEvent(event) {
263    var now = Date.now();
264    if (now - lastPostMenuTouchAt < 700) {
265      event.preventDefault();
266      event.stopPropagation();
267      return true;
268    }
269    lastPostMenuTouchAt = now;
270    event.preventDefault();
271    event.stopPropagation();
272    togglePostPageMenu();
273    return true;
274  }
275
276  function nostrToolsNip19() {
277    return window.NostrTools && window.NostrTools.nip19 ? window.NostrTools.nip19 : null;
278  }
279
280  function currentNostrNaddr() {
281    var nip19 = nostrToolsNip19();
282    var kind = parseInt(currentNostrKind, 10);
283    if (nip19 && typeof nip19.naddrEncode === 'function' && currentNostrPubkey && Number.isFinite(kind) && currentNostrD) {
284      return nip19.naddrEncode({
285        identifier: currentNostrD,
286        pubkey: currentNostrPubkey,
287        kind: kind,
288        relays: currentNostrRelays
289      });
290    }
291    return currentNostrAddress || '';
292  }
293
294  function currentNostrNevent() {
295    var nip19 = nostrToolsNip19();
296    var kind = parseInt(currentNostrKind, 10);
297    if (nip19 && typeof nip19.neventEncode === 'function' && currentNostrEventId) {
298      return nip19.neventEncode({
299        id: currentNostrEventId,
300        author: currentNostrPubkey || undefined,
301        kind: Number.isFinite(kind) ? kind : undefined,
302        relays: currentNostrRelays
303      });
304    }
305    return currentNostrEventId || '';
306  }
307
308  function currentNostrOpenValue() {
309    return currentNostrNaddr() || currentNostrNevent();
310  }
311
312  function writeClipboardText(value) {
313    var text = String(value || '').trim();
314    if (!text) {
315      return Promise.reject(new Error('Nothing to copy yet.'));
316    }
317    if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
318      return navigator.clipboard.writeText(text);
319    }
320    return new Promise(function (resolve, reject) {
321      var node = document.createElement('textarea');
322      node.value = text;
323      node.setAttribute('readonly', 'readonly');
324      node.style.position = 'fixed';
325      node.style.left = '-9999px';
326      node.style.top = '0';
327      document.body.appendChild(node);
328      node.select();
329      try {
330        if (document.execCommand('copy')) {
331          resolve();
332        } else {
333          reject(new Error('Copy was not available in this browser.'));
334        }
335      } catch (err) {
336        reject(err);
337      } finally {
338        if (node.parentNode) {
339          node.parentNode.removeChild(node);
340        }
341      }
342    });
343  }
344
345  function setReaderMenuEnabled() {
346    var hasAddress = !!currentNostrAddress || (!!currentNostrPubkey && !!currentNostrKind && !!currentNostrD);
347    var hasEvent = !!currentNostrEventId;
348    document.querySelectorAll('[data-post-page-reader-action="copy_nostr_address"]').forEach(function (node) {
349      node.hidden = !hasAddress;
350    });
351    document.querySelectorAll('[data-post-page-reader-action="copy_nostr_event"]').forEach(function (node) {
352      node.hidden = !hasEvent;
353    });
354    document.querySelectorAll('[data-post-page-reader-action="open_nostr"]').forEach(function (node) {
355      node.hidden = !(hasAddress || hasEvent);
356    });
357    return hasAddress || hasEvent;
358  }
359
360  function postPageReaderMenuItemHtml(action, label) {
361    return '<button type="button" data-post-page-action="' + escapeHtml(action) + '" data-post-page-reader-action="' + escapeHtml(action) + '" role="menuitem" hidden>' + escapeHtml(label) + '</button>';
362  }
363
364  function postPageMenuHtml() {
365    return '' +
366      '<button type="button" class="post-page-menu-trigger" aria-label="Post menu" aria-haspopup="menu" aria-expanded="false">' + overflowMenuIconSvg() + '</button>' +
367      '<div class="post-page-menu-panel" role="menu" hidden>' +
368      postPageReaderMenuItemHtml('copy_nostr_address', 'Copy Nostr address') +
369      postPageReaderMenuItemHtml('copy_nostr_event', 'Copy Nostr event') +
370      postPageReaderMenuItemHtml('open_nostr', 'Open in Nostr client') +
371      '<div class="post-page-menu-separator" data-post-page-admin-separator hidden></div>' +
372      '<button type="button" data-post-page-action="edit_post" data-post-page-admin-action role="menuitem" hidden>Edit post...</button>' +
373      '<button type="button" data-post-page-action="add_to_list" data-post-page-admin-action role="menuitem" hidden>Add to list...</button>' +
374      '<button type="button" class="post-page-menu-delete" data-post-page-action="delete_post" data-post-page-admin-action role="menuitem" hidden>Delete post...</button>' +
375      '</div>';
376  }
377
378  function normalizePostPageMenu(menu) {
379    if (!(menu instanceof HTMLElement)) {
380      return null;
381    }
382    var panel = menu.querySelector('.post-page-menu-panel');
383    if (!(panel instanceof HTMLElement)) {
384      return menu;
385    }
386    if (menu.getAttribute('data-post-page-menu-normalized') === 'true') {
387      return menu;
388    }
389
390    menu.hidden = true;
391    panel.hidden = true;
392    var trigger = menu.querySelector('.post-page-menu-trigger');
393    if (trigger) {
394      trigger.innerHTML = overflowMenuIconSvg();
395      trigger.setAttribute('aria-expanded', 'false');
396    }
397
398    if (!panel.querySelector('[data-post-page-reader-action="copy_nostr_address"]')) {
399      panel.insertAdjacentHTML('afterbegin', postPageReaderMenuItemHtml('copy_nostr_address', 'Copy Nostr address'));
400    }
401    if (!panel.querySelector('[data-post-page-reader-action="copy_nostr_event"]')) {
402      var addressNode = panel.querySelector('[data-post-page-reader-action="copy_nostr_address"]');
403      if (addressNode) {
404        addressNode.insertAdjacentHTML('afterend', postPageReaderMenuItemHtml('copy_nostr_event', 'Copy Nostr event'));
405      } else {
406        panel.insertAdjacentHTML('afterbegin', postPageReaderMenuItemHtml('copy_nostr_event', 'Copy Nostr event'));
407      }
408    }
409    if (!panel.querySelector('[data-post-page-reader-action="open_nostr"]')) {
410      var eventNode = panel.querySelector('[data-post-page-reader-action="copy_nostr_event"]');
411      if (eventNode) {
412        eventNode.insertAdjacentHTML('afterend', postPageReaderMenuItemHtml('open_nostr', 'Open in Nostr client'));
413      } else {
414        panel.insertAdjacentHTML('afterbegin', postPageReaderMenuItemHtml('open_nostr', 'Open in Nostr client'));
415      }
416    }
417
418    panel.querySelectorAll('[data-post-page-action="edit_post"], [data-post-page-action="add_to_list"], [data-post-page-action="delete_post"]').forEach(function (node) {
419      node.setAttribute('data-post-page-admin-action', '');
420      node.hidden = true;
421    });
422
423    if (!panel.querySelector('[data-post-page-admin-separator]')) {
424      var firstAdminAction = panel.querySelector('[data-post-page-admin-action]');
425      if (firstAdminAction) {
426        firstAdminAction.insertAdjacentHTML('beforebegin', '<div class="post-page-menu-separator" data-post-page-admin-separator hidden></div>');
427      }
428    }
429    panel.querySelectorAll('[data-post-page-admin-separator]').forEach(function (node) {
430      node.hidden = true;
431    });
432
433    menu.setAttribute('data-post-page-menu-normalized', 'true');
434    return menu;
435  }
436
437  function ensurePostPageMenu(layout) {
438    if (!layout || !layout.card) {
439      return null;
440    }
441    var head = layout.card.querySelector('.post-head');
442    if (!head) {
443      return null;
444    }
445    var existing = head.querySelector('.post-page-menu');
446    if (existing) {
447      return normalizePostPageMenu(existing);
448    }
449    var wrap = document.createElement('div');
450    wrap.className = 'post-page-menu';
451    wrap.hidden = true;
452    wrap.innerHTML = postPageMenuHtml();
453    head.appendChild(wrap);
454    wrap.setAttribute('data-post-page-menu-normalized', 'true');
455    return wrap;
456  }
457
458  function checkAdminSession() {
459    var token = getSessionToken();
460    if (!token || token === 'undefined' || token === 'null') {
461      return Promise.resolve(false);
462    }
463    return fetch('/cgi/ssh-auth-check-session?session_token=' + encodeURIComponent(token), {
464      credentials: 'same-origin'
465    })
466      .then(function (res) { return res.json(); })
467      .then(function (data) {
468        return !!(data && data.authenticated && data.is_admin);
469      })
470      .catch(function () {
471        return false;
472      });
473  }
474
475  function setPostPageMenuVisibility(menu, isAdmin, hasReaderActions) {
476    menu.querySelectorAll('[data-post-page-admin-action]').forEach(function (node) {
477      node.hidden = !isAdmin;
478    });
479    menu.querySelectorAll('[data-post-page-admin-separator]').forEach(function (node) {
480      node.hidden = !(isAdmin && hasReaderActions);
481    });
482    menu.hidden = !(isAdmin || hasReaderActions);
483    if (menu.hidden) {
484      closePostPageMenu();
485    }
486  }
487
488  function eventHasVerifiedAdminState(event) {
489    return !!(event && event.detail && event.detail.authenticated === true && typeof event.detail.is_admin !== 'undefined');
490  }
491
492  function refreshPostPageMenuVisibility(event) {
493    var menu = normalizePostPageMenu(document.querySelector('.post-page-menu'));
494    if (!menu) {
495      return;
496    }
497    var hasReaderActions = setReaderMenuEnabled();
498    if (eventHasVerifiedAdminState(event)) {
499      setPostPageMenuVisibility(menu, event.detail.is_admin === true, hasReaderActions);
500      return;
501    }
502    checkAdminSession().then(function (isAdmin) {
503      setPostPageMenuVisibility(menu, isAdmin, hasReaderActions);
504    });
505  }
506
507  function addToListDialogNode() {
508    return document.getElementById('post-page-add-to-list-dialog');
509  }
510
511  function closeAddToListDialog() {
512    var dialog = addToListDialogNode();
513    if (dialog && dialog.parentNode) {
514      dialog.parentNode.removeChild(dialog);
515    }
516    addToListState.postPath = '';
517    addToListState.loading = false;
518    addToListState.submitting = false;
519    addToListState.lists = [];
520    addToListState.message = '';
521    addToListState.tone = '';
522  }
523
524  function addToListSelectedSlug(dialog) {
525    var select = dialog ? dialog.querySelector('[data-post-add-list-select]') : null;
526    var newInput = dialog ? dialog.querySelector('[data-post-add-list-new-slug]') : null;
527    var selected = select ? String(select.value || '').trim() : '';
528    if (selected === '__new__') {
529      return normalizeListSlug(newInput ? newInput.value : '');
530    }
531    return normalizeListSlug(selected || 'list');
532  }
533
534  function syncAddToListDialogNewRow(dialog) {
535    var select = dialog ? dialog.querySelector('[data-post-add-list-select]') : null;
536    var row = dialog ? dialog.querySelector('[data-post-add-list-new-row]') : null;
537    var newInput = dialog ? dialog.querySelector('[data-post-add-list-new-slug]') : null;
538    if (!select || !row) {
539      return;
540    }
541    var useNew = String(select.value || '') === '__new__';
542    row.hidden = !useNew;
543    if (useNew && newInput) {
544      newInput.focus();
545      newInput.select();
546    }
547  }
548
549  function renderAddToListDialog() {
550    var dialog = addToListDialogNode();
551    if (!dialog) {
552      dialog = document.createElement('div');
553      dialog.id = 'post-page-add-to-list-dialog';
554      dialog.className = 'post-page-modal-shell';
555      dialog.setAttribute('role', 'presentation');
556      document.body.appendChild(dialog);
557    }
558
559    var lists = Array.isArray(addToListState.lists) ? addToListState.lists : [];
560    var optionsHtml = '';
561    lists.forEach(function (item) {
562      var slug = normalizeListSlug(item && item.slug);
563      if (!slug) {
564        return;
565      }
566      var title = String((item && item.title) || slug).trim();
567      optionsHtml += '<option value="' + escapeHtml(slug) + '">' + escapeHtml(title || slug) + '</option>';
568    });
569    optionsHtml += '<option value="__new__">New list...</option>';
570    var disabledAttr = addToListState.loading || addToListState.submitting ? ' disabled aria-disabled="true"' : '';
571    var statusHtml = addToListState.message
572      ? '<p class="post-page-modal-status is-' + escapeHtml(addToListState.tone || 'info') + '">' + escapeHtml(addToListState.message) + '</p>'
573      : '';
574
575    dialog.innerHTML = '' +
576      '<div class="post-page-modal-backdrop" data-post-add-list-action="cancel"></div>' +
577      '<form class="post-page-modal-panel" data-post-add-list-form="true" role="dialog" aria-modal="true" aria-labelledby="post-page-add-to-list-title">' +
578        '<h3 id="post-page-add-to-list-title">Add Post to List</h3>' +
579        '<label class="post-page-modal-field">' +
580          '<span>List</span>' +
581          '<select data-post-add-list-select="true"' + disabledAttr + '>' + optionsHtml + '</select>' +
582        '</label>' +
583        '<label class="post-page-modal-field" data-post-add-list-new-row="true" hidden>' +
584          '<span>
584New list slug</span>' +
585          '<input type="text" data-post-add-list-new-slug="true" placeholder="new-list"' + disabledAttr + '>' +
586        '</label>' +
587        '<label class="post-page-modal-field">' +
588          '<span>Date <small>(optional)</small></span>' +
589          '<input type="text" data-post-add-list-date="true" placeholder="YYYY or YYYY-MM or YYYY-MM-DD"' + disabledAttr + '>' +
590        '</label>' +
591        '<label class="post-page-modal-field">' +
592          '<span>Text <small>(optional)</small></span>' +
593          '<input type="text" data-post-add-list-markdown="true" placeholder="Markdown line for this entry"' + disabledAttr + '>' +
594        '</label>' +
595        statusHtml +
596        '<div class="post-page-modal-actions">' +
597          '<button type="button" data-post-add-list-action="cancel"' + disabledAttr + '>Cancel</button>' +
598          '<button type="submit" class="post-page-modal-primary"' + disabledAttr + '>' + (addToListState.submitting ? 'Adding...' : 'Add to list') + '</button>' +
599        '</div>' +
600      '</form>';
601
602    var select = dialog.querySelector('[data-post-add-list-select]');
603    if (select && lists.length) {
604      select.value = normalizeListSlug(lists[0] && lists[0].slug) || 'list';
605    } else if (select) {
606      select.value = '__new__';
607    }
608    syncAddToListDialogNewRow(dialog);
609    var firstField = dialog.querySelector(lists.length ? '[data-post-add-list-select]' : '[data-post-add-list-new-slug]');
610    if (firstField && !addToListState.loading && !addToListState.submitting) {
611      firstField.focus();
612    }
613  }
614
615  function openAddToListDialog(postPath, token, csrf) {
616    addToListState.postPath = postPath;
617    addToListState.loading = true;
618    addToListState.submitting = false;
619    addToListState.lists = [];
620    addToListState.message = 'Loading lists...';
621    addToListState.tone = 'info';
622    renderAddToListDialog();
623    postMenuBusy = true;
624    postForm('/cgi/blog-list-pages', {
625      session_token: token,
626      csrf_token: csrf
627    }).then(function (listsData) {
628      if (!listsData || !listsData.success) {
629        throw new Error((listsData && listsData.error) || 'Could not load lists');
630      }
631      addToListState.lists = Array.isArray(listsData.lists) ? listsData.lists : [];
632      addToListState.message = '';
633      addToListState.tone = '';
634    }).catch(function (err) {
635      addToListState.lists = [{ slug: 'list', title: 'List' }];
636      addToListState.message = err && err.message ? err.message : 'Could not load lists. You can still choose a list slug.';
637      addToListState.tone = 'error';
638    }).finally(function () {
639      addToListState.loading = false;
640      postMenuBusy = false;
641      renderAddToListDialog();
642    });
643  }
644
645  function submitAddToListDialog(form) {
646    if (addToListState.submitting || !addToListState.postPath) {
647      return;
648    }
649    var token = getSessionToken();
650    var csrf = getCsrfToken();
651    if (!token || !csrf) {
652      addToListState.message = 'Sign in as admin first.';
653      addToListState.tone = 'error';
654      renderAddToListDialog();
655      return;
656    }
657    var slug = addToListSelectedSlug(form);
658    if (!slug) {
659      addToListState.message = 'List slug is required.';
660      addToListState.tone = 'error';
661      renderAddToListDialog();
662      return;
663    }
664    var dateInput = form.querySelector('[data-post-add-list-date]');
665    var markdownInput = form.querySelector('[data-post-add-list-markdown]');
666    addToListState.submitting = true;
667    addToListState.message = 'Adding post to list draft...';
668    addToListState.tone = 'info';
669    renderAddToListDialog();
670    postForm('/cgi/blog-add-post-to-list', {
671      list_slug: slug,
672      post_path: addToListState.postPath,
673      date: dateInput ? String(dateInput.value || '').trim() : '',
674      markdown: markdownInput ? String(markdownInput.value || '').trim() : '',
675      marker: 'list',
676      session_token: token,
677      csrf_token: csrf
678    }).then(function (data) {
679      if (!data || !data.success) {
680        throw new Error((data && data.error) || 'Could not add post to list');
681      }
682      addToListState.message = (data && data.message) || ('Added to ' + slug + ' draft.');
683      addToListState.tone = 'ok';
684      addToListState.submitting = false;
685      renderAddToListDialog();
686      window.setTimeout(closeAddToListDialog, 700);
687    }).catch(function (err) {
688      addToListState.message = err && err.message ? err.message : 'Could not add post to list.';
689      addToListState.tone = 'error';
690      addToListState.submitting = false;
691      renderAddToListDialog();
692    });
693  }
694
695  function runPostPageAction(action) {
696    var picked = String(action || '').trim();
697    if (!picked || postMenuBusy || !currentRelPath) {
698      return;
699    }
700
701    if (picked === 'copy_nostr_address') {
702      writeClipboardText(currentNostrNaddr())
703        .then(function () {
704          closePostPageMenu();
705        })
706        .catch(function (err) {
707          window.alert((err && err.message) || 'Could not copy Nostr address.');
708        });
709      return;
710    }
711
712    if (picked === 'copy_nostr_event') {
713      writeClipboardText(currentNostrNevent())
714        .then(function () {
715          closePostPageMenu();
716        })
717        .catch(function (err) {
718          window.alert((err && err.message) || 'Could not copy Nostr event.');
719        });
720      return;
721    }
722
723    if (picked === 'open_nostr') {
724      var openValue = currentNostrOpenValue();
725      if (!openValue) {
726        window.alert('This post does not have a Nostr address yet.');
727        return;
728      }
729      closePostPageMenu();
730      window.location.href = openValue.indexOf('nostr:') === 0 ? openValue : 'nostr:' + openValue;
731      return;
732    }
733
734    var token = getSessionToken();
735    var csrf = getCsrfToken();
736    if (!token || !csrf) {
737      window.alert('Sign in as admin first.');
738      return;
739    }
740
741    if (picked === 'edit_post') {
742      postMenuBusy = true;
743      postForm('/cgi/blog-create-draft-from-post', {
744        post_path: currentRelPath,
745        session_token: token,
746        csrf_token: csrf
747      })
748        .then(function (data) {
749          if (!data || !data.success) {
750            throw new Error((data && data.error) || 'Could not create draft from post');
751          }
752          closePostPageMenu();
753          var draftId = String((data && data.draft_id) || '').trim();
754          if (!draftId) {
755            throw new Error('Draft was created but no draft id was returned');
756          }
757          return openInlineEditorForDraft(draftId);
758        })
759        .catch(function (err) {
760          window.alert(err.message || 'Could not create draft from post');
761        })
762        .finally(function () {
763          postMenuBusy = false;
764        });
765      return;
766    }
767
768    if (picked === 'add_to_list') {
769      closePostPageMenu();
770      openAddToListDialog(currentRelPath, token, csrf);
771      return;
772    }
773
774    if (picked === 'delete_post') {
775      if (!window.confirm('Delete this published post from this site? This cannot be undone.')) {
776        return;
777      }
778      postMenuBusy = true;
779      postForm('/cgi/blog-manage-post', {
780        action: 'delete',
781        post_path: currentRelPath,
782        session_token: token,
783        csrf_token: csrf
784      })
785        .then(function (data) {
786          if (!data || !data.success) {
787            throw new Error((data && data.error) || 'Delete failed');
788          }
789          window.location.href = '/archive';
790        })
791        .catch(function (err) {
792          window.alert(err.message || 'Delete failed');
793        })
794        .finally(function () {
795          postMenuBusy = false;
796        });
797    }
798  }
799
800  function ensureMeta(name, value, attrType) {
801    if (!value) {
802      return;
803    }
804    var selector = attrType === 'property'
805      ? 'meta[property="' + name + '"]'
806      : 'meta[name="' + name + '"]';
807    var node = document.querySelector(selector);
808    if (!node) {
809      node = document.createElement('meta');
810      node.setAttribute(attrType === 'property' ? 'property' : 'name', name);
811      document.head.appendChild(node);
812    }
813    node.setAttribute('content', value);
814  }
815
816  function formatType(value) {
817    var raw = String(value || '').trim();
818    if (!raw) {
819      return 'Post';
820    }
821    return raw.replace(/[_-]+/g, ' ').replace(/\b\w/g, function (m) { return m.toUpperCase(); });
822  }
823
824  function postYear(current) {
825    var explicitYear = String((current && current.year) || '').trim();
826    if (/^[0-9]{4}$/.test(explicitYear)) {
827      return explicitYear;
828    }
829    var published = String((current && current.published_at) || '').trim();
830    var match = published.match(/^([0-9]{4})/);
831    return match ? match[1] : '';
832  }
833
834  function renderTagChip(tag, className) {
835    var t = String(tag || '').trim();
836    if (!t) {
837      return '';
838    }
839    return '<a class="' + escapeHtml(className || 'tag') + '" href="/tags#' + encodeURIComponent(t) + '">' + escapeHtml(t) + '</a>';
840  }
841
842  function renderTags(current) {
843    var clean = Array.isArray(current && current.tags) ? current.tags : [];
844    var chips = [];
845    var type = String((current && current.type) || 'post').trim() || 'post';
846    var year = postYear(current);
847    var seen = {};
848
849    chips.push('<a class="tag blog-type-pill" href="/blog?type=' + encodeURIComponent(type) + '">' + escapeHtml(formatType(type)) + '</a>');
850    if (year) {
851      chips.push('<a class="tag blog-year-pill" href="/blog?year=' + encodeURIComponent(year) + '">' + escapeHtml(year) + '</a>');
852      seen[year.toLowerCase()] = true;
853    }
854    seen[type.toLowerCase()] = true;
855    seen[formatType(type).toLowerCase()] = true;
856
857    clean.forEach(function (tag) {
858      var t = String(tag || '').trim();
859      if (!t) {
860        return;
861      }
862      if (seen[t.toLowerCase()]) {
863        return;
864      }
865      seen[t.toLowerCase()] = true;
866      chips.push(renderTagChip(t, 'tag'));
867    });
868
869    if (!chips.length) {
870      return '';
871    }
872    return '<div class="tags post-context-tags">' + chips.join('') + '</div>';
873  }
874
875  function syncStatusConfig(status) {
876    switch (status) {
877      case 'local_newer_than_nostr':
878        return {
879          label: 'Server newer',
880          message: 'Server copy is newer than the latest published Nostr state. Visitors see the server copy.',
881          className: 'status-local-newer-than-nostr'
882        };
883      case 'nostr_newer_than_local':
884        return {
885          label: 'Nostr newer',
886          message: 'Published Nostr state is newer than the server copy. Visitors still see the server copy until the site source changes.',
887          className: 'status-nostr-newer-than-local'
888        };
889      case 'in_sync':
890        return {
891          label: 'Synced',
892          message: 'Server copy and published Nostr state are in sync.',
893          className: 'status-in-sync'
894        };
895      case 'unpublished_local_changes':
896        return {
897          label: 'Server only',
898          message: 'This page exists only on the server so far. 
898Visitors see the server copy.',
899          className: 'status-unpublished-local-changes'
900        };
901      default:
902        return {
903          label: 'Sync unknown',
904          message: 'Cannot determine local-vs-Nostr sync status yet.',
905          className: 'status-unknown'
906        };
907    }
908  }
909
910  function postSyncStatusPillHtml(payload) {
911    var sync = payload && payload.sync_status && typeof payload.sync_status === 'object' ? payload.sync_status : {};
912    var status = String(sync.status || 'unknown').trim();
913    var config = syncStatusConfig(status);
914    var message = String(sync.message || config.message).trim();
915    return '<span class="page-sync-status-pill ' + config.className + '" title="' + escapeHtml(message) + '">' + escapeHtml(config.label) + '</span>';
916  }
917
918  function existingSinglePostLayout() {
919    var root = document.body;
920    if (!root) {
921      return null;
922    }
923    var card = root.querySelector('.post-single-item');
924    if (!card) {
925      return null;
926    }
927    return {
928      anchor: root,
929      card: card,
930      body: card.querySelector('.post-single-body') || card
931    };
932  }
933
934  function renderPostEndTags(current) {
935    var content = renderTags(current);
936    if (!content) {
937      return '';
938    }
939    return '<section class="post-end-tags">' + content + '</section>';
940  }
941
942  function ensureZapHost(layout) {
943    if (!layout || !layout.card) {
944      return null;
945    }
946    var existing = layout.card.querySelector('.zap-inline-host.is-compact');
947    if (existing) {
948      return existing;
949    }
950    var head = layout.card.querySelector('.post-head');
951    if (!head) {
952      return null;
953    }
954    var host = document.createElement('div');
955    host.className = 'post-zap-host';
956    head.appendChild(host);
957    return host;
958  }
959
960  function ensurePostSyncStatusHost(layout) {
961    if (!layout || !layout.card) {
962      return null;
963    }
964    var main = layout.card.querySelector('.post-head-main');
965    if (!main) {
966      return null;
967    }
968    var existing = main.querySelector('.post-sync-status-row');
969    if (existing) {
970      return existing;
971    }
972    var host = document.createElement('div');
973    host.className = 'post-sync-status-row';
974    var byline = main.querySelector('.post-byline');
975    if (byline && byline.parentNode === main) {
976      byline.insertAdjacentElement('afterend', host);
977    } else {
978      main.appendChild(host);
979    }
980    return host;
981  }
982
983  function renderPostSyncStatusPill(payload, layout) {
984    var host = ensurePostSyncStatusHost(layout);
985    if (!host) {
986      return;
987    }
988    host.innerHTML = postSyncStatusPillHtml(payload);
989  }
990
991  function renderPostZapUi(payload, layout) {
992    if (!window.blogZapUi || typeof window.blogZapUi.render !== 'function') {
993      return;
994    }
995    var current = payload && payload.current ? payload.current : null;
996    var nostr = current && current.nostr ? current.nostr : null;
997    if (!nostr) {
998      return;
999    }
1000    var host = ensureZapHost(layout);
1001    if (!host) {
1002      return;
1003    }
1004    window.blogZapUi.render(host, {
1005      zapConfig: payload ? payload.zap_config : null,
1006      display: 'compact',
1007      title: current ? current.title : '',
1008      target: {
1009        label: 'post',
1010        title: current ? current.title : '',
1011        recipientPubkey: nostr ? nostr.pubkey : '',
1012        eventId: nostr ? nostr.id : '',
1013        address: nostr ? nostr.address : '',
1014        kind: nostr ? nostr.kind : ''
1015      }
1016    });
1017  }
1018
1019  function postNeedsWideLayout(body) {
1020    if (!(body instanceof HTMLElement)) {
1021      return false;
1022    }
1023    // Semantic wide-content blocks should opt into a wider card automatically.
1024    if (body.querySelector('table, pre, .table-wrapper, .wide-content, iframe, video')) {
1025      return true;
1026    }
1027    var candidates = body.querySelectorAll('table, pre, img, iframe, video, svg, canvas');
1028    for (var i = 0; i < candidates.length; i += 1) {
1029      var el = candidates[i];
1030      if (!(el instanceof HTMLElement)) {
1031        continue;
1032      }
1033      if ((Number(el.scrollWidth) || 0) - (Number(el.clientWidth) || 0) > 16) {
1034        return true;
1035      }
1036    }
1037    return false;
1038  }
1039
1040  function syncSinglePostLayoutForContent(layout) {
1041    if (!layout || !(layout.card instanceof HTMLElement)) {
1042      return;
1043    }
1044    var body = layout.body instanceof HTMLElement ? layout.body : layout.card.querySelector('.post-single-body');
1045    var wide = postNeedsWideLayout(body);
1046    layout.card.classList.toggle('post-single-has-wide-content', wide);
1047    if (document && document.body) {
1048      document.body.classList.toggle('single-post-has-wide-content', wide);
1049    }
1050  }
1051
1052  function normalizeInlinePostType(raw) {
1053    var value = String(raw || '').trim().toLowerCase();
1054    return value || 'longform';
1055  }
1056
1057  function normalizeInlineTags(raw) {
1058    var seen = {};
1059    var value = Array.isArray(raw) ? raw.join(',') : String(raw || '');
1060    value = value.trim();
1061    if (value === '[]') {
1062      return '';
1063    }
1064    value = value.replace(/^\[+|\]+$/g, '');
1065    return value
1066      .split(',')
1067      .map(function (tag) {
1068        var clean = String(tag || '').trim();
1069        clean = clean.replace(/^["']+|["']+$/g, '');
1070        clean = clean.replace(/^\[+|\]+$/g, '');
1071        return clean;
1072      })
1073      .filter(function (tag) {
1074        var key = tag.toLowerCase();
1075        if (!tag || seen[key]) {
1076          return false;
1077        }
1078        seen[key] = true;
1079        return true;
1080      })
1081      .join(', ');
1082  }
1083
1084  function parseInlineTagList(raw) {
1085    var normalized = normalizeInlineTags(raw);
1086    if (!normalized) {
1087      return [];
1088    }
1089    return normalized.split(',').map(function (tag) {
1090      return String(tag || '').trim();
1091    }).filter(Boolean);
1092  }
1093
1094  function syncInlineTagsFromList() {
1095    inlineEditState.tags = normalizeInlineTags(inlineEditState.tagList || []);
1096    var hiddenTags = document.querySelector('input[data-post-inline-field="tags"]');
1097    if (hiddenTags instanceof HTMLInputElement) {
1098      hiddenTags.value = inlineEditState.tags;
1099    }
1100  }
1101
1102  function inlineTagTokenHtml(tag) {
1103    var label = String(tag || '').trim();
1104    return '<span class="tag-token" data-post-inline-tag-token="' + escapeHtml(label) + '">' +
1105      '<span class="tag-token-label">' + escapeHtml(label) + '</span>' +
1106      '<button type="button" class="tag-token-remove" data-post-inline-tag-remove="' + escapeHtml(label) + '" aria-label="Remove tag ' + escapeHtml(label) + '">×</button>' +
1107    '</span>';
1108  }
1109
1110  function inlineTagsEditorHtml() {
1111    var tokensHtml = (inlineEditState.tagList || []).map(inlineTagTokenHtml).join('');
1112    var emptyClass = ((inlineEditState.tagList || []).length || String(inlineEditState.tagsDraftText || '').trim()) ? '' : ' is-empty';
1113    return '' +
1114      '<div class="tag-token-editor' + emptyClass + '" data-post-inline-field="tags-editor" role="group" aria-label="Post tags">' +
1115        tokensHtml +
1116        '<input type="text" class="tag-token-editor-input" data-post-inline-field="tags-input" value="' + escapeHtml(inlineEditState.tagsDraftText || '') + '" placeholder="tag, tag, tag" autocomplete="off" autocapitalize="none" spellcheck="false">' +
1117      '</div>';
1118  }
1119
1120  function inlineTagsEditorNode() {
1121    return document.querySelector('[data-post-inline-field="tags-editor"]');
1122  }
1123
1124  function inlineTagsEditorInputNode(editor) {
1125    if (!(editor instanceof HTMLElement)) {
1126      return null;
1127    }
1128    return editor.querySelector('[data-post-inline-field="tags-input"]');
1129  }
1130
1131  function inlineTagsEditorReadDraft(editor) {
1132    var node = inlineTagsEditorInputNode(editor);
1133    if (!(node instanceof HTMLInputElement)) {
1134      return '';
1135    }
1136    return String(node.value || '');
1137  }
1138
1139  function inlineTagsEditorSyncDraft(editor) {
1140    inlineEditState.tagsDraftText = inlineTagsEditorReadDraft(editor);
1141    if (editor instanceof HTMLElement) {
1142      editor.classList.toggle('is-empty', !(inlineEditState.tagList || []).length && !String(inlineEditState.tagsDraftText || '').trim());
1143    }
1144  }
1145
1146  function inlineTagsEditorRender(editor) {
1147    if (!(editor instanceof HTMLElement)) {
1148      return;
1149    }
1150    var html = (inlineEditState.tagList || []).map(inlineTagTokenHtml).join('');
1151    html += '<input type="text" class="tag-token-editor-input" data-post-inline-field="tags-input" value="' + escapeHtml(inlineEditState.tagsDraftText || '') + '" placeholder="tag, tag, tag" autocomplete="off" autocapitalize="none" spellcheck="false">';
1152    editor.innerHTML = html;
1153    editor.classList.toggle('is-empty', !(inlineEditState.tagList || []).length && !String(inlineEditState.tagsDraftText || '').trim());
1154  }
1155
1156  function inlineTagsEditorFocusDraft(editor) {
1157    var node = inlineTagsEditorInputNode(editor);
1158    if (!(node instanceof HTMLInputElement)) {
1159      return;
1160    }
1161    if (document.activeElement !== node) {
1162      try {
1163        node.focus({ preventScroll: true });
1164      } catch (_focusErr) {
1165        node.focus();
1166      }
1167    }
1168    try {
1169      node.setSelectionRange(node.value.length, node.value.length);
1170    } catch (_selectionErr) {}
1171  }
1172
1173  function inlineTagsEditorCommit(editor, force) {
1174    if (!(editor instanceof HTMLElement)) {
1175      return false;
1176    }
1177    var raw = inlineTagsEditorReadDraft(editor);
1178    inlineEditState.tagsDraftText = raw;
1179    var value = String(raw || '');
1180    var hasSplit = /[,\n]/.test(value);
1181    if (!hasSplit && !force) {
1182      return false;
1183    }
1184    var pieces = value.split(/[,\n]/g).map(function (piece) {
1185      return String(piece || '').trim();
1186    }).filter(Boolean);
1187    if (!pieces.length) {
1188      inlineEditState.tagsDraftText = hasSplit ? '' : value;
1189      inlineTagsEditorRender(editor);
1190      syncInlineTagsFromList();
1191      return false;
1192    }
1193    var changed = false;
1194    pieces.forEach(function (tag) {
1195      if (!tag) {
1196        return;
1197      }
1198      var lowerTag = tag.toLowerCase();
1199      var exists = (inlineEditState.tagList || []).some(function (item) {
1200        return String(item || '').trim().toLowerCase() === lowerTag;
1201      });
1202      if (exists) {
1203        return;
1204      }
1205      inlineEditState.tagList.push(tag);
1206      changed = true;
1207    });
1208    inlineEditState.tagsDraftText = '';
1209    syncInlineTagsFromList();
1210    inlineTagsEditorRender(editor);
1211    inlineTagsEditorFocusDraft(editor);
1212    return changed || hasSplit;
1213  }
1214
1215  function inlineRemoveTag(tag) {
1216    var value = String(tag || '').trim();
1217    if (!value) {
1218      return false;
1219    }
1220    var next = (inlineEditState.tagList || []).filter(function (item) {
1221      return String(item || '').trim().toLowerCase() !== value.toLowerCase();
1222    });
1223    if (next.length === (inlineEditState.tagList || []).length) {
1224      return false;
1225    }
1226    inlineEditState.tagList = next;
1227    syncInlineTagsFromList();
1228    return true;
1229  }
1230
1231  function inlinePostTypeLabel(postType) {
1232    var value = normalizeInlinePostType(postType);
1233    if (value === 'shortform') return 'shortform';
1234    if (value === 'capture-media') return 'capture';
1235    if (value === 'upload-media') return 'media';
1236    if (value === 'attachment') return 'attachment';
1237    if (value === 'audio-note') return 'audio';
1238    if (value === 'link-share') return 'link';
1239    if (value === 'go-live') return 'go live';
1240    return 'post';
1241  }
1242
1243  function inlinePostKindPillClass(postType) {
1244    var value = normalizeInlinePostType(postType);
1245    if (value === 'shortform') return 'is-shortform';
1246    if (value === 'capture-media') return 'is-capture-media';
1247    if (value === 'upload-media') return 'is-upload-media';
1248    if (value === 'attachment') return 'is-attachment';
1249    if (value === 'audio-note') return 'is-audio-note';
1250    if (value === 'link-share') return 'is-link-share';
1251    if (value === 'go-live') return 'is-go-live';
1252    return 'is-longform';
1253  }
1254
1255  function inlineComposePostTypeIconSvg(type) {
1256    var value = normalizeInlinePostType(type);
1257    if (value === 'shortform') {
1258      return '<svg class="compose-post-type-icon" viewBox="0 0 96.314 100" fill="none" aria-hidden="true">' +
1259        '<path fill="currentColor" stroke="none" d="M84.39,11.825C83.396,8.452,80.595,5,75.097,5c-5.605,0-16.216,3.389-16.216,16.216c0,2.613-1.786,3.205-3.243,3.243c-27.467,0-47.354,32.62-48.187,34.006L5,62.549l4.7,0.785c14.088,2.349,26.242,3.646,36.208,3.879V95h6.486V67.204c1.035-0.03,2.055-0.068,3.041-0.127V95h6.486V66.438c15.209-2.16,22.906-8.613,22.906-19.276V24.459h6.486v-3.243C91.314,17.868,89.414,13.124,84.39,11.825z M78.34,47.162c0,3.367,0,13.606-29.252,13.606c-9.03,0-20.227-1.017-33.326-3.031c6.043-8.383,21.414-26.792,39.876-26.792c3.908,0,9.729-2.591,9.729-9.729c0-9.157,8.12-9.711,9.729-9.73c2.609,0,3.205,1.783,3.243,3.243V47.162z M75.446,17.837c0,1.384-1.128,2.505-2.509,2.505s-2.508-1.121-2.508-2.505c0-1.384,1.127-2.505,2.508-2.505S75.446,16.453,75.446,17.837z"/>' +
1260      '</svg>';
1261    }
1262    if (value === 'longform') {
1263      return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
1264        '<path d="M5 7H19M5 11H19M5 15H17M5 19H19" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
1265      '</svg>';
1266    }
1267    if (value === 'capture-media') {
1268      return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
1269        '<rect x="4" y="7" width="16" height="11" rx="2.2" stroke="currentColor" stroke-width="1.8"/>' +
1270        '<circle cx="12" cy="12.5" r="2.6" stroke="currentColor" stroke-width="1.8"/>' +
1271        '<path d="M9.2 7L10.4 5.2H13.6L14.8 7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
1272      '</svg>';
1273    }
1274    if (value === 'upload-media') {
1275      return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
1276        '<rect x="3.6" y="5.1" width="16.8" height="13.8" rx="2.2" stroke="currentColor" stroke-width="1.8"/>' +
1277        '<circle cx="8.4" cy="9.7" r="1.4" fill="currentColor"/>' +
1278        '<path d="M5.8 16.4L10.2 12.1L13.1 15L15.8 12.5L18.2 16.4" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>' +
1279      '</svg>';
1280    }
1281    if (value === 'attachment') {
1282      return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
1283        '<path d="M21.4 11.1L12.9 19.6C10.6 21.9 6.8 21.9 4.5 19.6C2.2 17.3 2.2 13.5 4.5 11.2L13 2.8C14.6 1.2 17.1 1.2 18.7 2.8C20.2 4.4 20.2 6.9 18.7 8.5L10.2 16.9C9.3 17.8 7.8 17.8 6.9 16.9C6 16 6 14.5 6.9 13.6L14.8 5.7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>' +
1284      '</svg>';
1285    }
1286    if (value === 'audio-note') {
1287      return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
1288        '<rect x="9" y="4.2" width="6" height="10" rx="3" stroke="currentColor" stroke-width="1.8"/>' +
1289        '<path d="M6.6 11.2C6.6 14.4 9.1 16.9 12 16.9C14.9 16.9 17.4 14.4 17.4 11.2M12 16.9V20.2M9.3 20.2H14.7" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
1290      '</svg>';
1291    }
1292    if (value === 'link-share') {
1293      return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
1294        '<rect x="2.7" y="8.5" width="8.8" height="7" rx="3.5" stroke="currentColor" stroke-width="1.8"/>' +
1295        '<rect x="12.5" y="8.5" width="8.8" height="7" rx="3.5" stroke="currentColor" stroke-width="1.8"/>' +
1296        '<path d="M9.8 12H14.2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
1297      '</svg>';
1298    }
1299    if (value === 'go-live') {
1300      return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
1301        '<circle cx="12" cy="12" r="7" stroke="currentColor" stroke-width="1.8"/>' +
1302        '<circle cx="12" cy="12" r="2.5" fill="currentColor"/>' +
1303      '</svg>';
1304    }
1305    return '<svg class="compose-post-type-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true">' +
1306      '<path d="M5 7H19M5 11H19M5 15H17M5 19H19" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>' +
1307    '</svg>';
1308  }
1309
1310  function inlineComposeCloseIconSvg() {
1311    return '<svg class="compose-close-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 6L18 18" stroke="currentColor" stroke-width="2.1" stroke-linecap="round"/><path d="M18 6L6 18" stroke="currentColor" stroke-width="2.1" stroke-linecap="round"/></svg>';
1312  }
1313
1314  function inlineComposeTrashIconSvg() {
1315    return '<svg class="compose-trash-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4.5 7.2H19.5" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M9.4 7.2V5.4C9.4 4.6 10 4 10.8 4H13.2C14 4 14.6 4.6 14.6 5.4V7.2" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M7.6 7.2L8.4 18.1C8.5 19.2 9.4 20 10.5 20H13.5C14.6 20 15.5 19.2 15.6 18.1L16.4 7.2" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M10.4 10.4V16.1" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M13.6 10.4V16.1" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>';
1316  }
1317
1318  function inlineToolbarButtonHtml(action, label, glyph) {
1319    return '<button type="button" class="unobtrusive-icon-button toolbar-button" data-post-inline-toolbar="' + escapeHtml(action) + '" aria-label="' + escapeHtml(label) + '" title="' + escapeHtml(label) + '">' + glyph + '</button>';
1320  }
1321
1322  function inlineToolbarHtml() {
1323    return '' +
1324      '<div class="toolbar blog-compose-toolbar" aria-label="Markdown toolbar">' +
1325        inlineToolbarButtonHtml('bold', 'Bold', '<span class="tb-glyph tb-glyph-bold" aria-hidden="true">B</span>') +
1326        inlineToolbarButtonHtml('italic', 'Italic', '<span class="tb-glyph tb-glyph-italic" aria-hidden="true">I</span>') +
1327        inlineToolbarButtonHtml('h2', 'Heading 2', '<span class="tb-glyph tb-glyph-heading" aria-hidden="true">H2</span>') +
1328        inlineToolbarButtonHtml('h3', 'Heading 3', '<span class="tb-glyph tb-glyph-heading" aria-hidden="true">H3</span>') +
1329        inlineToolbarButtonHtml('code', 'Inline code', '<span class="tb-glyph tb-glyph-code" aria-hidden="true">&lt;/&gt;</span>') +
1330        inlineToolbarButtonHtml('code_block', 'Code block', '<span class="tb-glyph tb-glyph-code" aria-hidden="true">{ }</span>') +
1331        inlineToolbarButtonHtml('link', 'Insert link', '<svg class="tb-icon" viewB
1331ox="0 0 24 24" fill="none" aria-hidden="true"><path d="M10 13.9L8.4 15.5C6.8 17.1 4.2 17.1 2.6 15.5C1 13.9 1 11.3 2.6 9.7L4.2 8.1" stroke="currentColor" stroke-width="2.15" stroke-linecap="round"/><path d="M14 10.1L15.6 8.5C17.2 6.9 19.8 6.9 21.4 8.5C23 10.1 23 12.7 21.4 14.3L19.8 15.9" stroke="currentColor" stroke-width="2.15" stroke-linecap="round"/><path d="M9 12H15" stroke="currentColor" stroke-width="2.15" stroke-linecap="round"/></svg>') +
1332        inlineToolbarButtonHtml('quote', 'Quote', '<span class="tb-glyph tb-glyph-quote" aria-hidden="true">“”</span>') +
1333        inlineToolbarButtonHtml('ul', 'Bullet list', '<svg class="tb-icon" viewBox="0 0 24 24" fill="none" aria-hidden="true"><circle cx="5.1" cy="7.2" r="1.2" fill="currentColor"/><circle cx="5.1" cy="12" r="1.2" fill="currentColor"/><circle cx="5.1" cy="16.8" r="1.2" fill="currentColor"/><path d="M9.2 7.2H19" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M9.2 12H19" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/><path d="M9.2 16.8H19" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/></svg>') +
1334        inlineToolbarButtonHtml('ol', 'Numbered list', '<span class="tb-glyph tb-glyph-list" aria-hidden="true">1.</span>') +
1335      '</div>';
1336  }
1337
1338  function inlineInsertMarkdown(action) {
1339    var textarea = document.querySelector('textarea[data-post-inline-field="content"]');
1340    if (!(textarea instanceof HTMLTextAreaElement)) {
1341      return;
1342    }
1343    var start = textarea.selectionStart;
1344    var end = textarea.selectionEnd;
1345    var value = String(textarea.value || '');
1346    var selected = value.slice(start, end);
1347    var before = value.slice(0, start);
1348    var after = value.slice(end);
1349    var replacement = selected;
1350    var selectStart = start;
1351    var selectEnd = end;
1352
1353    if (action === 'bold') {
1354      replacement = '**' + selected + '**';
1355      selectStart = start + 2;
1356      selectEnd = end + 2;
1357    } else if (action === 'italic') {
1358      replacement = '*' + selected + '*';
1359      selectStart = start + 1;
1360      selectEnd = end + 1;
1361    } else if (action === 'h2') {
1362      replacement = '## ' + (selected || 'Heading');
1363      selectStart = start + 3;
1364      selectEnd = start + replacement.length;
1365    } else if (action === 'h3') {
1366      replacement = '### ' + (selected || 'Heading');
1367      selectStart = start + 4;
1368      selectEnd = start + replacement.length;
1369    } else if (action === 'code') {
1370      replacement = '`' + (selected || 'code') + '`';
1371      selectStart = start + 1;
1372      selectEnd = start + replacement.length - 1;
1373    } else if (action === 'code_block') {
1374      replacement = '```\n' + (selected || '') + '\n```';
1375      selectStart = start + 4;
1376      selectEnd = start + replacement.length - 4;
1377    } else if (action === 'link') {
1378      replacement = '[' + (selected || 'text') + '](https://)';
1379      selectStart = start + replacement.length - 9;
1380      selectEnd = start + replacement.length - 1;
1381    } else if (action === 'quote') {
1382      replacement = '> ' + (selected || 'Quote');
1383      selectStart = start + 2;
1384      selectEnd = start + replacement.length;
1385    } else if (action === 'ul') {
1386      replacement = '- ' + (selected || 'List item');
1387      selectStart = start + 2;
1388      selectEnd = start + replacement.length;
1389    } else if (action === 'ol') {
1390      replacement = '1. ' + (selected || 'List item');
1391      selectStart = start + 3;
1392      selectEnd = start + replacement.length;
1393    }
1394
1395    textarea.value = before + replacement + after;
1396    textarea.selectionStart = selectStart;
1397    textarea.selectionEnd = selectEnd;
1398    textarea.focus();
1399    inlineEditState.content = String(textarea.value || '');
1400    queueInlineAutosave();
1401  }
1402
1403  function renderInlineEditorStatusOnly() {
1404    var host = ensureInlineEditorHost();
1405    if (!host || !inlineEditState.open) {
1406      return;
1407    }
1408    var output = host.querySelector('.blog-compose-status-row .output');
1409    if (output) {
1410      output.className = 'output' + (inlineEditState.outputTone ? (' ' + inlineEditState.outputTone) : '');
1411      output.textContent = inlineEditState.output || '';
1412    }
1413    var autosave = host.querySelector('.compose-editor-autosave');
1414    if (autosave) {
1415      var status = String(inlineEditState.saveStatus || '');
1416      autosave.hidden = !status;
1417      autosave.classList.toggle('is-saving', status === 'saving');
1418      autosave.classList.toggle('is-error', status === 'error');
1419      autosave.textContent = status === 'saving' ? 'Saving...' : (status === 'error' ? 'Save failed' : '✓ Saved');
1420    }
1421  }
1422
1423  function inlineAuthPayload() {
1424    return {
1425      session_token: getSessionToken(),
1426      csrf_token: getCsrfToken()
1427    };
1428  }
1429
1430  function ensureInlineEditorHost() {
1431    var card = document.querySelector('.post-single-item');
1432    if (!card) {
1433      return null;
1434    }
1435    var existing = card.querySelector('.post-inline-editor-host');
1436    if (existing) {
1437      return existing;
1438    }
1439    var host = document.createElement('div');
1440    host.className = 'post-inline-editor-host';
1441    var body = card.querySelector('.post-single-body');
1442    if (body && body.parentNode === card) {
1443      card.insertBefore(host, body);
1444    } else {
1445      card.appendChild(host);
1446    }
1447    return host;
1448  }
1449
1450  function renderInlineEditor() {
1451    var host = ensureInlineEditorHost();
1452    if (!host) {
1453      return;
1454    }
1455    if (!inlineEditState.open) {
1456      host.innerHTML = '';
1457      host.hidden = true;
1458      return;
1459    }
1460    host.hidden = false;
1461    if (!Array.isArray(inlineEditState.tagList)) {
1462      inlineEditState.tagList = parseInlineTagList(inlineEditState.tags || '');
1463    }
1464    if (typeof inlineEditState.tagsDraftText !== 'string') {
1465      inlineEditState.tagsDraftText = '';
1466    }
1467    var outputClass = 'output' + (inlineEditState.outputTone ? (' ' + inlineEditState.outputTone) : '');
1468    var hasEditableFilename = !!String(inlineEditState.sourcePostPath || '').trim();
1469    var showTitleField = normalizeInlinePostType(inlineEditState.postType) !== 'shortform';
1470    var currentFilename = normalizePostFilename(inlineEditState.postFilename || filenameFromPostPath(inlineEditState.sourcePostPath));
1471    if (!currentFilename) {
1472      currentFilename = 'post';
1473    }
1474    var filenameDisplayHidden = inlineEditState.postFilenameEditing ? ' hidden' : '';
1475    var filenameEditHidden = inlineEditState.postFilenameEditing ? '' : ' hidden';
1476    var filenameRow = '';
1477    if (hasEditableFilename) {
1478      filenameRow = '' +
1479        '<div class="field-row compose-post-filename-row">' +
1480          '<label><strong>Slug/Filename</strong></label>' +
1481          '<div class="compose-post-filename-display"' + filenameDisplayHidden + '>' +
1482            '<code class="compose-post-filename-value">posts/' + escapeHtml(currentFilename) + '.md</code>' +
1483            '<button type="button" class="list-inline-edit-link" data-post-inline-action="edit_filename">Edit...</button>' +
1484          '</div>' +
1485          '<div class="compose-post-filename-edit-wrap"' + filenameEditHidden + '>' +
1486            '<span class="compose-post-filename-prefix">posts/</span>' +
1487            '<input type="text" data-post-inline-field="post_filename" inputmode="url" spellcheck="false" value="' + escapeHtml(currentFilename) + '" placeholder="my-post-slug">' +
1488            '<span class="compose-post-filename-suffix">.md</span>' +
1489          '</div>' +
1490        '</div>';
1491    }
1492    var postTypeIcon = inlineComposePostTypeIconSvg(inlineEditState.postType);
1493    var postTypePill = '<span class="nostr-page-kind-badge ' + inlinePostKindPillClass(inlineEditState.postType) + '">' + escapeHtml(inlinePostTypeLabel(inlineEditState.postType)) + '</span><span class="nostr-target-pill">Post type locked</span>';
1494    host.innerHTML = '' +
1495      '<article class="post-item blog-post-item blog-compose-card">' +
1496        '<div class="blog-compose-body">' +
1497          '<div class="field-row blog-compose-head-row is-type-collapsed" data-compose-head-row>' +
1498            '<div class="compose-post-type-control is-collapsed" data-compose-type-control>' +
1499              '<button type="button" class="compose-post-type-current-btn unobtrusive-icon-button" disabled aria-disabled="true" aria-label="Post type is locked for existing posts" title="Post type is locked for existing posts">' + postTypeIcon + '</button>' +
1500            '</div>' +
1501            '<div class="blog-compose-head-actions">' +
1502              '<button type="button" class="unobtrusive-icon-button blog-compose-close" data-post-inline-action="close" aria-label="Close compose" title="Close compose">' + inlineComposeCloseIconSvg() + '<span class="sr-only">Close compose</span></button>' +
1503            '</div>' +
1504          '</div>' +
1505          '<div class="field-row blog-compose-title-row"' + (showTitleField ? '' : ' hidden aria-hidden="true"') + '>' +
1506            '<label><strong>Title</strong></label>' +
1507            '<input type="text" data-compose-field="title" data-post-inline-field="title" placeholder="Post title" value="' + escapeHtml(inlineEditState.title) + '">' +
1508          '</div>' +
1509          filenameRow +
1510          '<div class="field-row">' +
1511            '<label><strong>Body</strong></label>' +
1512            '<div class="editor-shell blog-compose-editor-shell">' +
1513              inlineToolbarHtml() +
1514              '<textarea data-post-inline-field="content" rows="14" placeholder="Post body">' + escapeHtml(inlineEditState.content) + '</textarea>' +
1515              '<div class="autosave-indicator compose-editor-autosave' + (inlineEditState.saveStatus === 'saving' ? ' is-saving' : '') + (inlineEditState.saveStatus === 'error' ? ' is-error' : '') + '"' + (inlineEditState.saveStatus ? '' : ' hidden') + '>
1515' + (inlineEditState.saveStatus === 'saving' ? 'Saving...' : (inlineEditState.saveStatus === 'error' ? 'Save failed' : '✓ Saved')) + '</div>' +
1516            '</div>' +
1517          '</div>' +
1518          '<div class="field-row compose-tags-row">' +
1519            '<label><strong>Tags</strong></label>' +
1520            '<input type="hidden" data-post-inline-field="tags" value="' + escapeHtml(inlineEditState.tags) + '">' +
1521            '<div class="tag-editor">' + inlineTagsEditorHtml() + '</div>' +
1522          '</div>' +
1523        '</div>' +
1524        '<div class="compose-footer blog-compose-footer">' +
1525          '<div class="compose-actions blog-compose-footer-actions">' +
1526            '<button type="button" class="icon-danger unobtrusive-icon-button blog-compose-delete" data-post-inline-action="delete" aria-label="Delete draft" title="Delete draft"' + (inlineEditState.busy ? ' disabled aria-disabled="true"' : '') + '>' + inlineComposeTrashIconSvg() + '</button>' +
1527            '<div class="blog-compose-publish-stack">' +
1528              '<div class="blog-compose-publish-row">' +
1529                postTypePill +
1530                '<button type="button" class="list-admin-primary-btn blog-compose-btn" data-post-inline-action="save" title="Saves unpublished changes only"' + (inlineEditState.busy ? ' disabled aria-disabled="true"' : '') + '>Save Draft</button>' +
1531                '<button type="button" class="list-admin-primary-btn blog-compose-btn" data-post-inline-action="publish"' + (inlineEditState.busy ? ' disabled aria-disabled="true"' : '') + '>Publish Changes</button>' +
1532              '</div>' +
1533            '</div>' +
1534          '</div>' +
1535          '<div class="blog-compose-status-row">' +
1536            '<div class="' + outputClass + '">' + escapeHtml(inlineEditState.output) + '</div>' +
1537          '</div>' +
1538        '</div>' +
1539      '</article>';
1540  }
1541
1542  function setInlineOutput(message, tone) {
1543    inlineEditState.output = String(message || '');
1544    inlineEditState.outputTone = String(tone || '');
1545  }
1546
1547  function inlineEditorPayload(action) {
1548    var postType = normalizeInlinePostType(inlineEditState.postType || 'longform');
1549    return {
1550      action: action,
1551      draft_id: String(inlineEditState.draftId || ''),
1552      source_post_path: String(inlineEditState.sourcePostPath || ''),
1553      post_filename: String(inlineEditState.postFilename || ''),
1554      title: postType === 'shortform' ? '' : String(inlineEditState.title || '').trim(),
1555      tags: normalizeInlineTags(inlineEditState.tags || ''),
1556      summary: '',
1557      content: String(inlineEditState.content || ''),
1558      post_type: postType,
1559      scheduled_at: '',
1560      publish_mode: 'immediate',
1561      publish_destination: 'local_only'
1562    };
1563  }
1564
1565  function saveInlineDraft(action) {
1566    if (!inlineEditState.open) {
1567      return Promise.resolve(false);
1568    }
1569    if (action !== 'autosave' && inlineEditState.autosaveTimer) {
1570      window.clearTimeout(inlineEditState.autosaveTimer);
1571      inlineEditState.autosaveTimer = null;
1572    }
1573    if (inlineEditState.busy) {
1574      inlineEditState.dirtyDuringSave = true;
1575      if (action !== 'autosave') {
1576        setInlineOutput('Finishing the current save. Try again in a moment.', 'warn');
1577        renderInlineEditorStatusOnly();
1578      }
1579      return Promise.resolve(false);
1580    }
1581    var auth = inlineAuthPayload();
1582    if (!auth.session_token || !auth.csrf_token) {
1583      setInlineOutput('Sign in again to edit this post.', 'error');
1584      renderInlineEditor();
1585      return Promise.resolve(false);
1586    }
1587    var payload = inlineEditorPayload(action);
1588    payload.session_token = auth.session_token;
1589    payload.csrf_token = auth.csrf_token;
1590    inlineEditState.busy = true;
1591    inlineEditState.dirtyDuringSave = false;
1592    if (action === 'autosave') {
1593      inlineEditState.saveStatus = 'saving';
1594    }
1595    if (action === 'autosave') {
1596      renderInlineEditorStatusOnly();
1597    } else {
1598      renderInlineEditor();
1599    }
1600    return postForm('/cgi/blog-save-post', payload).then(function (data) {
1601      if (data && data.draft_id) {
1602        inlineEditState.draftId = String(data.draft_id || '').trim();
1603      }
1604      inlineEditState.tags = normalizeInlineTags(inlineEditState.tags || '');
1605      inlineEditState.postFilename = normalizePostFilename(inlineEditState.postFilename || filenameFromPostPath(inlineEditState.sourcePostPath));
1606      if (action === 'autosave') {
1607        inlineEditState.saveStatus = 'saved';
1608        renderInlineEditorStatusOnly();
1609      }
1610      if (action === 'publish_now') {
1611        setInlineOutput('Published. Reloading post…', 'ok');
1612        renderInlineEditor();
1613        window.setTimeout(function () {
1614          window.location.reload();
1615        }, 350);
1616      } else if (action === 'save_draft') {
1617        setInlineOutput('Working draft saved. Live post is unchanged.', 'ok');
1618      }
1619      return true;
1620    }).catch(function (err) {
1621      if (action === 'autosave') {
1622        inlineEditState.saveStatus = 'error';
1623        renderInlineEditorStatusOnly();
1624      } else {
1625        setInlineOutput(err && err.message ? err.message : 'Save failed', 'error');
1626      }
1627      renderInlineEditor();
1628      return false;
1629    }).finally(function () {
1630      inlineEditState.busy = false;
1631      if (inlineEditState.dirtyDuringSave) {
1632        inlineEditState.dirtyDuringSave = false;
1633        queueInlineAutosave();
1634        if (action !== 'autosave') {
1635          renderInlineEditorStatusOnly();
1636        }
1637        return;
1638      }
1639      if (action === 'autosave') {
1640        renderInlineEditorStatusOnly();
1641      } else {
1642        renderInlineEditor();
1643      }
1644    });
1645  }
1646
1647  function queueInlineAutosave() {
1648    if (!inlineEditState.open) {
1649      return;
1650    }
1651    if (inlineEditState.busy) {
1652      inlineEditState.dirtyDuringSave = true;
1653    }
1654    if (inlineEditState.autosaveTimer) {
1655      window.clearTimeout(inlineEditState.autosaveTimer);
1656    }
1657    inlineEditState.saveStatus = 'saving';
1658    renderInlineEditorStatusOnly();
1659    inlineEditState.autosaveTimer = window.setTimeout(function () {
1660      inlineEditState.autosaveTimer = null;
1661      saveInlineDraft('autosave');
1662    }, 1200);
1663  }
1664
1665  function closeInlineEditor() {
1666    if (inlineEditState.autosaveTimer) {
1667      window.clearTimeout(inlineEditState.autosaveTimer);
1668      inlineEditState.autosaveTimer = null;
1669    }
1670    inlineEditState.open = false;
1671    inlineEditState.busy = false;
1672    inlineEditState.draftId = '';
1673    inlineEditState.sourcePostPath = '';
1674    inlineEditState.postFilename = '';
1675    inlineEditState.postFilenameEditing = false;
1676    inlineEditState.postType = 'longform';
1677    inlineEditState.title = '';
1678    inlineEditState.content = '';
1679    inlineEditState.tags = '';
1680    inlineEditState.tagList = [];
1681    inlineEditState.tagsDraftText = '';
1682    inlineEditState.output = '';
1683    inlineEditState.outputTone = '';
1684    inlineEditState.saveStatus = '';
1685    renderInlineEditor();
1686  }
1687
1688  function deleteInlineDraft() {
1689    if (!inlineEditState.draftId || inlineEditState.busy) {
1690      closeInlineEditor();
1691      return;
1692    }
1693    if (!window.confirm('Delete this draft?')) {
1694      return;
1695    }
1696    var auth = inlineAuthPayload();
1697    if (!auth.session_token || !auth.csrf_token) {
1698      setInlineOutput('Sign in again to delete this draft.', 'error');
1699      renderInlineEditor();
1700      return;
1701    }
1702    inlineEditState.busy = true;
1703    renderInlineEditor();
1704    postForm('/cgi/blog-delete-draft', {
1705      draft_id: String(inlineEditState.draftId || ''),
1706      session_token: auth.session_token,
1707      csrf_token: auth.csrf_token
1708    }).then(function () {
1709      closeInlineEditor();
1710    }).catch(function (err) {
1711      inlineEditState.busy = false;
1712      setInlineOutput(err && err.message ? err.message : 'Could not delete draft', 'error');
1713      renderInlineEditor();
1714    });
1715  }
1716
1717  function openInlineEditorForDraft(draftId) {
1718    var auth = inlineAuthPayload();
1719    if (!auth.session_token || !auth.csrf_token) {
1720      return Promise.reject(new Error('Sign in as admin first.'));
1721    }
1722    return postForm('/cgi/blog-get-draft', {
1723      draft_id: String(draftId || ''),
1724      session_token: auth.session_token,
1725      csrf_token: auth.csrf_token
1726    }).then(function (data) {
1727      if (!data || !data.success || !data.draft) {
1728        throw new Error((data && data.error) || 'Could not load draft');
1729      }
1730      var draft = data.draft || {};
1731      inlineEditState.open = true;
1732      inlineEditState.busy = false;
1733      inlineEditState.draftId = String(draft.draft_id || draftId || '').trim();
1734      inlineEditState.sourcePostPath = normalizePostMdPath(draft.source_post_path || '');
1735      inlineEditState.postFilename = normalizePostFilename(draft.post_filename || filenameFromPostPath(inlineEditState.sourcePostPath));
1736      inlineEditState.postFilenameEditing = false;
1737      inlineEditState.postType = normalizeInlinePostType(draft.post_type || 'longform');
1738      inlineEditState.title = String(draft.title || '');
1739      inlineEditState.content = String(draft.content || '');
1740      inlineEditState.tags = normalizeInlineTags(draft.tags || '');
1741      inlineEditState.tagList = parseInlineTagList(inlineEditState.tags);
1742      inlineEditState.tagsDraftText = '';
1743      inlineEditState.output = '';
1744      inlineEditState.outputTone = '';
1745      inlineEditState.saveStatus = '';
1746      renderInlineEditor();
1747      var host = ensureInlineEditorHost();
1748      if (host && typeof host.scrollIntoView === 'function') {
1749        host.scrollIntoView({ behavior: 'smooth', block: 'start', inline: '
1749nearest' });
1750      }
1751      return true;
1752    });
1753  }
1754
1755  function navColumn(label, post, cls) {
1756    if (!post) {
1757      return '<div class="' + cls + '"><span class="post-nav-empty">' + escapeHtml(label) + ': none</span></div>';
1758    }
1759    return '<div class="' + cls + '">' +
1760      '<span class="post-nav-label">' + escapeHtml(label) + '</span>' +
1761      '<a href="' + escapeHtml(post.url || '#') + '">' + escapeHtml(post.title || 'Untitled') + '</a>' +
1762      '</div>';
1763  }
1764
1765  function postNavHtml(newerHtml, olderHtml) {
1766    return '<nav class="post-nav post-nav-enhanced" aria-label="Post navigation">' + newerHtml + olderHtml + '</nav>';
1767  }
1768
1769  function renderPostNav(payload) {
1770    return postNavHtml(
1771      navColumn('Newer', payload.newer, 'post-nav-prev'),
1772      navColumn('Older', payload.older, 'post-nav-next')
1773    );
1774  }
1775
1776  function normalizeMarkdownSeparators(body) {
1777    if (!(body instanceof HTMLElement)) {
1778      return;
1779    }
1780    Array.prototype.forEach.call(body.querySelectorAll('p'), function (node) {
1781      var text = String(node.textContent || '').replace(/\s+/g, '').trim();
1782      if (text === '***') {
1783        node.classList.add('markdown-separator');
1784      }
1785    });
1786  }
1787
1788  function normalizeAllMarkdownSeparators() {
1789    Array.prototype.forEach.call(document.querySelectorAll('.post-single-body, .post-content, .list-entry-markdown, .nostr-page-extra'), normalizeMarkdownSeparators);
1790  }
1791
1792  function renderCommentRow(comment) {
1793    var created = comment.created_at_iso ? escapeHtml(comment.created_at_iso.replace('T', ' ').replace('Z', ' UTC')) : '';
1794    var pubkey = escapeHtml(String(comment.pubkey || '').slice(0, 16));
1795    var body = escapeHtml(comment.content || '').replace(/\n/g, '<br>');
1796    return '<article class="post-comment post-comment-island">' +
1797      '<header><span class="post-comment-author">' + pubkey + '</span>' + (created ? ' <span class="post-comment-time">' + created + '</span>' : '') + '</header>' +
1798      '<p>' + body + '</p>' +
1799      '</article>';
1800  }
1801
1802  function renderComments(comments) {
1803    var list = Array.isArray(comments) ? comments : [];
1804    var container = document.getElementById('post-comments-list');
1805    if (!container) {
1806      return;
1807    }
1808    if (!list.length) {
1809      container.innerHTML = '<p class="placeholder post-comments-empty post-comment-island">No comments mirrored yet.</p>';
1810      return;
1811    }
1812    container.innerHTML = list.map(renderCommentRow).join('');
1813  }
1814
1815  function setCommentCount(count) {
1816    var badge = document.getElementById('post-comments-count');
1817    if (!badge) {
1818      return;
1819    }
1820    var n = Number(count || 0);
1821    if (!Number.isFinite(n) || n < 0) {
1822      n = 0;
1823    }
1824    badge.textContent = String(n);
1825  }
1826
1827  function setCommentStatus(message, kind) {
1828    var status = document.getElementById('post-comments-status');
1829    if (!status) {
1830      return;
1831    }
1832    status.className = 'post-comments-status';
1833    if (kind) {
1834      status.classList.add('is-' + kind);
1835    }
1836    status.textContent = message || '';
1837  }
1838
1839  function setRefreshBusy(isBusy) {
1840    refreshInFlight = !!isBusy;
1841    var button = document.getElementById('post-comments-refresh');
1842    if (!button) {
1843      return;
1844    }
1845    button.disabled = refreshInFlight;
1846    button.textContent = refreshInFlight ? 'Refreshing...' : 'Refresh comments';
1847  }
1848
1849  function setSubmitBusy(isBusy) {
1850    submitInFlight = !!isBusy;
1851    var button = document.getElementById('post-comment-submit');
1852    if (!button) {
1853      return;
1854    }
1855    button.disabled = submitInFlight;
1856    button.textContent = submitInFlight ? 'Posting...' : 'Post comment';
1857  }
1858
1859  function loadComments() {
1860    if (!currentRelPath) {
1861      return;
1862    }
1863    fetch('/cgi/blog-comments?path=' + encodeURIComponent(currentRelPath), { credentials: 'same-origin' })
1864      .then(function (res) { return res.json(); })
1865      .then(function (data) {
1866        if (!data || !data.success) {
1867          return;
1868        }
1869        var list = data.comments || [];
1870        renderComments(list);
1871        setCommentCount(list.length || 0);
1872        setCommentStatus('', '');
1873      })
1874      .catch(function () {
1875        setCommentStatus('Failed to load mirrored comments.', 'warn');
1876      });
1877  }
1878
1879  function refreshComments() {
1880    if (refreshInFlight) {
1881      return;
1882    }
1883    if (!currentRelPath) {
1884      return;
1885    }
1886    setRefreshBusy(true);
1887    setCommentStatus('Refreshing comments from relays...', 'info');
1888    fetch('/cgi/blog-refresh-comments', {
1889      method: 'POST',
1890      headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
1891      body: 'path=' + encodeURIComponent(currentRelPath),
1892      credentials: 'same-origin'
1893    })
1894      .then(function (res) { return res.json(); })
1895      .then(function (data) {
1896        if (!data || !data.success) {
1897          var msg = (data && data.error) ? data.error : 'Comment refresh failed.';
1898          setCommentStatus(msg, 'warn');
1899          return;
1900        }
1901        loadComments();
1902        setCommentStatus('Comments refreshed.', 'ok');
1903      })
1904      .catch(function () {
1905        setCommentStatus('Comment refresh failed.', 'warn');
1906      })
1907      .finally(function () {
1908        setRefreshBusy(false);
1909      });
1910  }
1911
1912  function parseEventJson(raw) {
1913    try {
1914      return JSON.parse(String(raw || ''));
1915    } catch (_) {
1916      return null;
1917    }
1918  }
1919
1920  function signCommentEvent(payload) {
1921    if (!window.nostr) {
1922      return Promise.reject(new Error('No browser Nostr signer detected. Install a NIP-07 extension.'));
1923    }
1924    if (typeof window.nostr.signEvent === 'function') {
1925      return Promise.resolve(window.nostr.signEvent(payload));
1926    }
1927    return Promise.reject(new Error('Browser signer does not expose signEvent.'));
1928  }
1929
1930  function submitComment() {
1931    if (submitInFlight) {
1932      return;
1933    }
1934    var textarea = document.getElementById('post-comment-input');
1935    if (!textarea) {
1936      return;
1937    }
1938    var content = String(textarea.value || '').trim();
1939    if (!content) {
1940      setCommentStatus('Comment text is required.', 'warn');
1941      return;
1942    }
1943    if (!currentNostrAddress || !currentNostrEventId) {
1944      setCommentStatus('Post Nostr metadata is missing for comment submit.', 'w
1944arn');
1945      return;
1946    }
1947    var sessionToken = localStorage.getItem('session_token') || '';
1948    var csrfToken = localStorage.getItem('csrf_token') || '';
1949    if (!sessionToken || !csrfToken) {
1950      setCommentStatus('Sign in first to post comments.', 'warn');
1951      return;
1952    }
1953
1954    var createdAt = Math.floor(Date.now() / 1000);
1955    var relayHint = currentNostrRelays.length ? String(currentNostrRelays[0] || '') : '';
1956    function referencedTag(name, value) {
1957      return relayHint ? [name, value, relayHint] : [name, value];
1958    }
1959    var kindText = String(currentNostrKind || '30023');
1960    var draftEvent = {
1961      kind: 1111,
1962      created_at: createdAt,
1963      tags: [
1964        referencedTag('A', currentNostrAddress),
1965        ['K', kindText],
1966        referencedTag('P', currentNostrPubkey),
1967        referencedTag('a', currentNostrAddress),
1968        referencedTag('e', currentNostrEventId),
1969        ['k', kindText],
1970        referencedTag('p', currentNostrPubkey)
1971      ],
1972      content: content
1973    };
1974
1975    setSubmitBusy(true);
1976    setCommentStatus('Signing comment event...', 'info');
1977    signCommentEvent(draftEvent)
1978      .then(function (signed) {
1979        var signedEvent = signed;
1980        if (typeof signedEvent === 'string') {
1981          signedEvent = parseEventJson(signedEvent);
1982        }
1983        if (!signedEvent || typeof signedEvent !== 'object') {
1984          throw new Error('Signer returned an invalid event payload.');
1985        }
1986        setCommentStatus('Submitting signed comment...', 'info');
1987        return fetch('/cgi/blog-submit-comment', {
1988          method: 'POST',
1989          headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
1990          body: 'session_token=' + encodeURIComponent(sessionToken) +
1991            '&csrf_token=' + encodeURIComponent(csrfToken) +
1992            '&path=' + encodeURIComponent(currentRelPath) +
1993            '&event_json=' + encodeURIComponent(JSON.stringify(signedEvent)),
1994          credentials: 'same-origin'
1995        });
1996      })
1997      .then(function (res) { return res.json(); })
1998      .then(function (data) {
1999        if (!data || !data.success) {
2000          var msg = (data && data.error) ? data.error : 'Comment submit failed.';
2001          throw new Error(msg);
2002        }
2003        textarea.value = '';
2004        setCommentStatus('Comment stored locally. Refreshing comments...', 'ok');
2005        loadComments();
2006      })
2007      .catch(function (err) {
2008        setCommentStatus(err.message || 'Comment submit failed.', 'warn');
2009      })
2010      .finally(function () {
2011        setSubmitBusy(false);
2012      });
2013  }
2014
2015  function ensureCommentShell(layout) {
2016    if (document.querySelector('.post-comments-shell')) {
2017      return;
2018    }
2019    var anchor = layout && layout.card ? layout.card : document.body;
2020    anchor.insertAdjacentHTML(anchor === document.body ? 'beforeend' : 'afterend',
2021      '<section class="post-comments-shell">' +
2022      '<div class="post-comments-head post-comment-island">' +
2023      '<h3>Comments (<span id="post-comments-count">0</span>)</h3>' +
2024      '<button type="button" id="post-comments-refresh">Refresh comments</button>' +
2025      '</div>' +
2026      '<div class="post-comments-compose post-comment-island">' +
2027      '<textarea id="post-comment-input" rows="3" placeholder="Write a Nostr-signed reply..."></textarea>' +
2028      '<button type="button" id="post-comment-submit">Post comment</button>' +
2029      '<p class="post-comments-shortcut">Press Ctrl/Cmd + Enter to post quickly.</p>' +
2030      '<p id="post-comments-status" class="post-comments-status"></p>' +
2031      '</div>' +
2032      '<div id="post-comments-list" class="post-comments-list"><p class="placeholder post-comments-empty post-comment-island">No comments mirrored yet.</p></div>' +
2033      '</section>'
2034    );
2035    var refreshButton = document.getElementById('post-comments-refresh');
2036    if (refreshButton) {
2037      refreshButton.addEventListener('click', refreshComments);
2038    }
2039    var submitButton = document.getElementById('post-comment-submit');
2040    if (submitButton) {
2041      submitButton.addEventListener('click', submitComment);
2042    }
2043    var input = document.getElementById('post-comment-input');
2044    if (input) {
2045      input.addEventListener('keydown', function (event) {
2046        if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') {
2047          event.preventDefault();
2048          submitComment();
2049        }
2050      });
2051    }
2052  }
2053
2054  function applyEnhancements(payload) {
2055    if (!payload || !payload.current) {
2056      return;
2057    }
2058    var canonicalPath = normalizePostMdPath(payload.current.source_path || payload.current.post_path || payload.current.path || payload.current.url || '');
2059    if (canonicalPath) {
2060      currentRelPath = canonicalPath;
2061    }
2062
2063    var layout = existingSinglePostLayout();
2064    if (!layout || !layout.body) {
2065      return;
2066    }
2067
2068    if (!layout.card.querySelector('.post-end-tags')) {
2069      var tagsHtml = renderPostEndTags(payload.current);
2070      if (tagsHtml) {
2071        layout.body.insertAdjacentHTML('beforeend', tagsHtml);
2072      }
2073    }
2074
2075    if (!layout.card.querySelector('.post-nav-enhanced')) {
2076      layout.body.insertAdjacentHTML('beforeend', renderPostNav(payload));
2077    }
2078
2079    syncSinglePostLayoutForContent(layout);
2080    normalizeAllMarkdownSeparators();
2081
2082    ensurePostPageMenu(layout);
2083    refreshPostPageMenuVisibility();
2084    renderPostSyncStatusPill(payload, layout);
2085    renderPostZapUi(payload, layout);
2086
2087    if (payload.current.nostr) {
2088      currentNostrAddress = payload.current.nostr.address || '';
2089      currentNostrEventId = payload.current.nostr.id || '';
2090      currentNostrPubkey = payload.current.nostr.pubkey || '';
2091      currentNostrKind = payload.current.nostr.kind || '';
2092      currentNostrD = payload.current.nostr.d || '';
2093      currentNostrRelays = Array.isArray(payload.current.nostr.relays) ? payload.current.nostr.relays.slice() : [];
2094      refreshPostPageMenuVisibility();
2095      ensureCommentShell(layout);
2096      loadComments();
2097    } else {
2098      currentNostrAddress = '';
2099      currentNostrEventId = '';
2100      currentNostrPubkey = '';
2101      currentNostrKind = '';
2102      currentNostrD = '';
2103      currentNostrRelays = [];
2104      refreshPostPageMenuVisibility();
2105    }
2106
2107    ensureMeta('description', payload.current.summary || '', 'name');
2108    ensureMeta('og:description', payload.current.summary || '', 'property');
2109    ensureMeta('article:published_time', payload.current.published_at || '', 'property');
2110    ensureMeta('twitter:description', payload.current.summary || '', 'name');
2111  }
2112
2113  function loadPostContext() {
2114    if (!isPostPage(window.location.pathname)) {
2115      return;
2116    }
2117    if (window.location.pathname === '/cgi/blog-open-post' || window.location.pathname.indexOf('/cgi/blog-open-post/') === 0) {
2118      var query = new URLSearchParams(window.location.search || '');
2119      var fromPathInfo = String(window.location.pathname || '').replace(/^\/cgi\/blog-open-post\/?/, '');
2120      currentRelPath = normalizePostMdPath(query.get('path') || fromPathInfo || '');
2121    } else {
2122      currentRelPath = normalizePostMdPath(window.location.pathname || '');
2123    }
2124    if (!currentRelPath) {
2125      return;
2126    }
2127    if (maybeRepairWrongPostShell(currentRelPath)) {
2128      return;
2129    }
2130    fetch('/cgi/blog-post-context?path=' + encodeURIComponent(currentRelPath), { credentials: 'same-origin' })
2131      .then(function (res) { return res.json(); })
2132      .then(function (data) {
2133        if (!data || !data.success) {
2134          return;
2135        }
2136        try {
2137          applyEnhancements(data);
2138        } catch (err) {
2139          window.__postContextError = err && err.message ? err.message : String(err || 'post context error');
2140          if (window.console && typeof window.console.warn === 'function') {
2141            window.console.warn('Post context enhancement failed:', err);
2142          }
2143        }
2144      })
2145      .catch(function () {
2146        // Post page should remain readable even if enhancement fetch fails.
2147      });
2148  }
2149
2150  function resolveCurrentPostPathFromLocation() {
2151    if (!isPostPage(window.location.pathname)) {
2152      return '';
2153    }
2154    if (window.location.pathname === '/cgi/blog-open-post' || window.location.pathname.indexOf('/cgi/blog-open-post/') === 0) {
2155      var query = new URLSearchParams(window.location.search || '');
2156      var fromPathInfo = String(window.location.pathname || '').replace(/^\/cgi\/blog-open-post\/?/, '');
2157      return normalizePostMdPath(query.get('path') || fromPathInfo || '');
2158    }
2159    return normalizePostMdPath(window.location.pathname || '');
2160  }
2161
2162  function triggerEarlyPostRouteRepair() {
2163    if (!isPostPage(window.location.pathname)) {
2164      return false;
2165    }
2166    var resolvedPath = resolveCurrentPostPathFromLocation();
2167    if (!resolvedPath) {
2168      return false;
2169    }
2170    if (document.querySelector('.post-single-item')) {
2171      return false;
2172    }
2173    currentRelPath = resolvedPath;
2174    // Keep current shell visible while we fetch and swap in the canonical
2175    // post document so navigation never flashes to a blank frame.
2176    maybeRepairWrongPostShell(currentRelPath);
2177    return true;
2178  }
2179
2180  document.addEventListener('click', function (event) {
2181    if (!isPostPage(window.location.pathname)) {
2182      return;
2183    }
2184    var target = event.target;
2185    if (!(target instanceof Element)) {
2186      return;
2187    }
2188    var trigger = target.closest('.post-page-menu-trigger');
2189    if (trigger) {
2190      activatePostPageMenuFromEvent(event);
2191      return;
2192    }
2193    var actionNode = target.closest('[data-post-page-action]');
2194    if (actionNode instanceof HTMLElement) {
2195      event.preventDefault();
2196      if (actionNode.hidden) {
2197        closePostPageMenu();
2198        return;
2199      }
2200      var action = actionNode.getAttribute('data-post-page-action');
2201      runPostPageAction(action);
2202      return;
2203    }
2204    var addListAction = target.closest('[data-post-add-list-action]');
2205    if (addListAction instanceof HTMLElement) {
2206      event.preventDefault();
2207      if (String(addListAction.getAttribute('data-post-add-list-action') || '') 
2207=== 'cancel') {
2208        closeAddToListDialog();
2209      }
2210      return;
2211    }
2212    var menu = document.querySelector('.post-page-menu');
2213    if (menu && !menu.hidden && !menu.contains(target)) {
2214      closePostPageMenu();
2215    }
2216
2217    var inlineAction = target.closest('[data-post-inline-action]');
2218    if (inlineAction instanceof HTMLElement) {
2219      event.preventDefault();
2220      var actionName = String(inlineAction.getAttribute('data-post-inline-action') || '');
2221      if (actionName === 'close') {
2222        closeInlineEditor();
2223      } else if (actionName === 'save') {
2224        saveInlineDraft('save_draft');
2225      } else if (actionName === 'publish') {
2226        saveInlineDraft('publish_now');
2227      } else if (actionName === 'delete') {
2228        deleteInlineDraft();
2229      } else if (actionName === 'edit_filename') {
2230        inlineEditState.postFilenameEditing = true;
2231        renderInlineEditor();
2232        window.setTimeout(function () {
2233          var input = document.querySelector('[data-post-inline-field="post_filename"]');
2234          if (input && typeof input.focus === 'function') {
2235            input.focus();
2236          }
2237          if (input && typeof input.select === 'function') {
2238            input.select();
2239          }
2240        }, 0);
2241      }
2242      return;
2243    }
2244
2245    var inlineTagRemove = target.closest('[data-post-inline-tag-remove]');
2246    if (inlineTagRemove instanceof HTMLElement) {
2247      event.preventDefault();
2248      var tagValue = String(inlineTagRemove.getAttribute('data-post-inline-tag-remove') || '').trim();
2249      if (inlineRemoveTag(tagValue)) {
2250        var editorNode = inlineTagsEditorNode();
2251        if (editorNode) {
2252          inlineTagsEditorRender(editorNode);
2253          inlineTagsEditorFocusDraft(editorNode);
2254        }
2255        queueInlineAutosave();
2256      }
2257      return;
2258    }
2259
2260    var inlineTagEditor = target.closest('[data-post-inline-field="tags-editor"]');
2261    if (inlineTagEditor instanceof HTMLElement) {
2262      inlineTagsEditorSyncDraft(inlineTagEditor);
2263      inlineTagsEditorFocusDraft(inlineTagEditor);
2264      return;
2265    }
2266
2267    var inlineToolbar = target.closest('[data-post-inline-toolbar]');
2268    if (inlineToolbar instanceof HTMLElement) {
2269      event.preventDefault();
2270      inlineInsertMarkdown(String(inlineToolbar.getAttribute('data-post-inline-toolbar') || ''));
2271      return;
2272    }
2273  });
2274
2275  document.addEventListener('pointerup', function (event) {
2276    if (!isPostPage(window.location.pathname) || event.pointerType === 'mouse') {
2277      return;
2278    }
2279    var target = event.target;
2280    if (!(target instanceof Element)) {
2281      return;
2282    }
2283    if (target.closest('.post-page-menu-trigger')) {
2284      activatePostPageMenuFromEvent(event);
2285    }
2286  }, { passive: false });
2287
2288  if (isPostPage(window.location.pathname)) {
2289    normalizePostPageMenu(document.querySelector('.post-page-menu'));
2290  }
2291
2292  document.addEventListener('input', function (event) {
2293    var target = event.target;
2294    if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLElement)) {
2295      return;
2296    }
2297    if (!inlineEditState.open) {
2298      return;
2299    }
2300    if (target instanceof HTMLInputElement && String(target.getAttribute('data-post-inline-field') || '') 
2300=== 'tags-input') {
2301      var editorNode = target.closest('[data-post-inline-field="tags-editor"]');
2302      inlineTagsEditorSyncDraft(editorNode);
2303      if (/[,\n]/.test(String(target.value || ''))) {
2304        if (inlineTagsEditorCommit(editorNode, false)) {
2305          queueInlineAutosave();
2306        }
2307      }
2308      var hiddenTags = document.querySelector('input[data-post-inline-field="tags"]');
2309      if (hiddenTags instanceof HTMLInputElement) {
2310        hiddenTags.value = inlineEditState.tags;
2311      }
2312      return;
2313    }
2314    if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
2315      return;
2316    }
2317    var field = String(target.getAttribute('data-post-inline-field') || '');
2318    if (!field) {
2319      return;
2320    }
2321    if (field === 'title') {
2322      inlineEditState.title = String(target.value || '');
2323    } else if (field === 'content') {
2324      inlineEditState.content = String(target.value || '');
2325    } else if (field === 'post_filename') {
2326      inlineEditState.postFilename = normalizePostFilename(String(target.value || ''));
2327    }
2328    queueInlineAutosave();
2329  });
2330
2331  document.addEventListener('change', function (event) {
2332    var target = event.target;
2333    if (target instanceof Element && target.closest('[data-post-add-list-select]')) {
2334      syncAddToListDialogNewRow(addToListDialogNode());
2335      return;
2336    }
2337    if (!(target instanceof HTMLInputElement)) {
2338      return;
2339    }
2340    if (!inlineEditState.open) {
2341      return;
2342    }
2343    if (!target.hasAttribute('data-post-inline-field')) {
2344      return;
2345    }
2346    var changedField = String(target.getAttribute('data-post-inline-field') || '');
2347    if (changedField === 'post_filename') {
2348      var nextFilename = normalizePostFilename(target.value || filenameFromPostPath(inlineEditState.sourcePostPath));
2349      if (!nextFilename) {
2350        nextFilename = filenameFromPostPath(inlineEditState.sourcePostPath) || 'post';
2351      }
2352      inlineEditState.postFilename = nextFilename;
2353      inlineEditState.postFilenameEditing = false;
2354      target.value = nextFilename;
2355      renderInlineEditor();
2356      queueInlineAutosave();
2357    }
2358  });
2359
2360  document.addEventListener('submit', function (event) {
2361    if (!isPostPage(window.location.pathname)) {
2362      return;
2363    }
2364    var form = event.target;
2365    if (!(form instanceof Element) || !form.closest('[data-post-add-list-form]')) {
2366      return;
2367    }
2368    event.preventDefault();
2369    submitAddToListDialog(form);
2370  });
2371
2372  document.addEventListener('keydown', function (event) {
2373    var target = event.target;
2374    if (!(target instanceof HTMLElement) || !inlineEditState.open) {
2375      return;
2376    }
2377    if (String(target.getAttribute('data-post-inline-field') || '') !== 'tags-input') {
2378      return;
2379    }
2380    var editorNode = target.closest('[data-post-inline-field="tags-editor"]');
2381    if (event.key === 'Enter') {
2382      event.preventDefault();
2383      if (inlineTagsEditorCommit(editorNode, true)) {
2384        queueInlineAutosave();
2385      }
2386      return;
2387    }
2388    if (event.key === ',' || event.code === 'Comma' || event.key === 'Tab') {
2389      if (event.key === 'Tab' || inlineTagsEditorReadDraft(editorNode).indexOf(',') !== -1 || /\S/.test(inlineTagsEditorReadDraft(editorNode))) {
2390        event.preventDefault();
2391        if (inlineTagsEditorCommit(editorNode, true)) {
2392          queueInlineAutosave();
2393        }
2394      }
2395      return;
2396    }
2397    if (event.key === 'Backspace') {
2398      var draft = inlineTagsEditorReadDraft(editorNode);
2399      if (!String(draft || '').trim() && (inlineEditState.tagList || []).length) {
2400        event.preventDefault();
2401        var last = inlineEditState.tagList[inlineEditState.tagList.length - 1];
2402        if (inlineRemoveTag(last)) {
2403          inlineTagsEditorRender(editorNode);
2404          inlineTagsEditorFocusDraft(editorNode);
2405          queueInlineAutosave();
2406        }
2407      }
2408    }
2409  });
2410
2411  document.addEventListener('focusout', function (event) {
2412    var target = event.target;
2413    if (!(target instanceof HTMLElement) || !inlineEditState.open) {
2414      return;
2415    }
2416    if (String(target.getAttribute('data-post-inline-field') || '') !== 'tags-input') {
2417      return;
2418    }
2419    var editorNode = target.closest('[data-post-inline-field="tags-editor"]');
2420    window.setTimeout(function () {
2421      var active = document.activeElement;
2422      if (editorNode instanceof HTMLElement && active instanceof HTMLElement && editorNode.contains(active)) {
2423        return;
2424      }
2425      if (inlineTagsEditorCommit(editorNode, true)) {
2426        queueInlineAutosave();
2427      }
2428    }, 0);
2429  });
2430
2431  if (!triggerEarlyPostRouteRepair()) {
2432    if (document.readyState === 'loading') {
2433      document.addEventListener('DOMContentLoaded', function () {
2434        normalizeAllMarkdownSeparators();
2435        loadPostContext();
2436      });
2437    } else {
2438      normalizeAllMarkdownSeparators();
2439      loadPostContext();
2440    }
2441    window.__wizardryBindSessionAuthRefresh({
2442      onChange: refreshPostPageMenuVisibility,
2443      storageKeys: ['session_token', 'csrf_token', 'last_auth_is_admin']
2444    });
2445  }
2446})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.