{"title":"Write Without Limits | Publish Your Fiction on UncutFiction","description":"UncutFiction is the home for writers who refuse to hold back. Share your stories with a passionate community of readers who want exactly what you write.","h1":"Write Without Limits","content":"\n<div id=\"write-app\" class=\"write-page\">\n\n  <!-- Story Type Dropdown -->\n  <div class=\"write-control-row\">\n    <label for=\"story-type\">What are you writing?</label>\n    <select id=\"story-type\">\n      <option value=\"erotica\" selected>Erotica</option>\n      <option value=\"romantic_erotica\">Romantic Erotica</option>\n      <option value=\"taboo_erotica\">Taboo Erotica</option>\n      <option value=\"explicit_erotica\">Very Explicit Erotica</option>\n      <option value=\"dark_romance_erotica\">Dark Romance Erotica</option>\n    </select>\n  </div>\n\n  <!-- Model Selector -->\n  <div class=\"write-control-row\">\n    <label>Writer Model</label>\n    <div class=\"radio-group\" id=\"model-selector\">\n      <label class=\"radio-option selected\" title=\"Fast, clean prose. Great for drafting.\">\n        <input type=\"radio\" name=\"model\" value=\"light\" checked>\n        <span>Light Model</span>\n      </label>\n      <label class=\"radio-option premium-locked\" title=\"Powerful reasoning model. Builds tension and interiority more slowly.\" id=\"pro-model-option\">\n        <input type=\"radio\" name=\"model\" value=\"pro\" disabled>\n        <span>Pro Writer Model</span>\n        <span class=\"premium-badge\" title=\"For premium members only\">Premium</span>\n      </label>\n      <label class=\"radio-option premium-locked\" title=\"Euryale — known for vivid, character-driven adult fiction with strong voice.\">\n        <input type=\"radio\" name=\"model\" value=\"pro2\" disabled>\n        <span>Pro Writer Model #2</span>\n        <span class=\"premium-badge\" title=\"For premium members only\">Premium</span>\n      </label>\n      <label class=\"radio-option premium-locked\" title=\"Hermes 405B — largest model available. Rich prose, deep context handling.\">\n        <input type=\"radio\" name=\"model\" value=\"pro3\" disabled>\n        <span>Pro Writer Model #3</span>\n        <span class=\"premium-badge\" title=\"For premium members only\">Premium</span>\n      </label>\n    </div>\n  </div>\n\n  <!-- Section Length -->\n  <div class=\"write-control-row\">\n    <label>Section Length</label>\n    <div class=\"radio-group\" id=\"length-selector\">\n      <label class=\"radio-option selected\">\n        <input type=\"radio\" name=\"section-length\" value=\"250\" checked>\n        <span>~250 words</span>\n      </label>\n      <label class=\"radio-option premium-locked\">\n        <input type=\"radio\" name=\"section-length\" value=\"350\" disabled>\n        <span>~350 words</span>\n        <span class=\"premium-badge\" title=\"For premium members only\">Premium</span>\n      </label>\n      <label class=\"radio-option premium-locked\">\n        <input type=\"radio\" name=\"section-length\" value=\"500\" disabled>\n        <span>~500 words</span>\n        <span class=\"premium-badge\" title=\"For premium members only\">Premium</span>\n      </label>\n    </div>\n  </div>\n\n  <!-- Continuation banner (hidden by default, shown when writing ch2+) -->\n  <div id=\"continuation-banner\" class=\"continuation-banner\" style=\"display:none;\">\n    <p id=\"continuation-title\"></p>\n    <p id=\"continuation-chapter\"></p>\n  </div>\n\n  <!-- Writing Tips panel -->\n  <div id=\"writing-tips\" class=\"writing-tips\">\n    <button class=\"tips-toggle\" id=\"tips-toggle\" onclick=\"document.getElementById('tips-body').classList.toggle('open'); this.classList.toggle('open');\">\n      <span class=\"tips-icon\">✦</span> Writing tips <span class=\"tips-chevron\">›</span>\n    </button>\n    <div class=\"tips-body\" id=\"tips-body\">\n      <div class=\"tip-item\">\n        <span class=\"tip-label\">Edit freely</span>\n        Each generated section is a draft, not a final product. Change names, fix the pacing, cut what doesn't work — the model picks up from whatever text is in the box. Your edits <em>are</em> the story.\n      </div>\n      <div class=\"tip-item\">\n        <span class=\"tip-label\">Short memory</span>\n        The model only sees the last few sections — not the whole story. If a character, a dynamic, or a detail matters, mention it in your next prompt. Think of it as a director's note, not just a scene description.\n      </div>\n      <div class=\"tip-item\">\n        <span class=\"tip-label\">Each model writes differently</span>\n        Light is fast and clean — good for drafting. The Pro models have stronger voices and go deeper. Try the same prompt on different models and see which fits your story best.\n      </div>\n      <div class=\"tip-item\">\n        <span class=\"tip-label\">Errors &amp; misses</span>\n        If a generation fails or misses the mark, try again with the same prompt before changing anything — the same instructions often produce quite different results. If it keeps failing, rephrase slightly.\n      </div>\n    </div>\n  </div>\n\n  <!-- Generated Sections (above the prompt — chat-style UX) -->\n  <div id=\"sections-container\" class=\"sections-container\"></div>\n\n  <!-- Section limit message -->\n  <div id=\"section-limit-msg\" style=\"display:none;\" class=\"section-limit-msg\">\n    You've reached the max for one chapter.\n  </div>\n\n  <!-- Content filter failed state (Fix 4) -->\n  <div id=\"content-failed-area\" style=\"display:none;\" class=\"content-failed-area\">\n    <p>Content verification failed — the generated text didn't pass our safety filter. This can happen with reasonable prompts too. <strong>Try the same prompt again first</strong> — results vary between runs. If it keeps failing, adjust your instructions slightly.</p>\n    <button id=\"btn-try-again\" class=\"btn btn-primary\">Try Again</button>\n  </div>\n\n  <!-- Save Flow (titles, tags) — injected dynamically -->\n  <div id=\"save-flow-container\"></div>\n\n  <!-- Writing UI — hidden during save flow -->\n  <div id=\"writing-ui\">\n\n    <!-- Prompt Input -->\n    <div class=\"write-control-row\">\n      <label for=\"prompt-input\">Describe what should happen next</label>\n      <textarea id=\"prompt-input\" rows=\"4\" placeholder=\"Describe the scene, characters, and what happens next…\"></textarea>\n    </div>\n\n    <!-- Action Row: regen icon · Generate -->\n    <div class=\"write-action-row\" id=\"action-row\">\n      <div class=\"action-row-left\">\n        <button id=\"btn-regenerate\" class=\"btn-icon-regen\" title=\"Re-generate last section\" style=\"display:none;\">↺</button>\n        <button id=\"btn-generate\" class=\"btn btn-primary\">Generate First Section</button>\n      </div>\n    </div>\n\n  </div><!-- /writing-ui -->\n\n  <!-- Save Area — outside writing-ui so it survives hideWritingUI() -->\n  <div id=\"save-area\" style=\"display:none;\" class=\"save-area-row\">\n    <button id=\"btn-save-chapter\" class=\"btn btn-primary\">Save Chapter</button>\n    <p id=\"save-login-hint\" style=\"display:none;\" class=\"save-login-hint\">\n      <a href=\"/sign-in\">Create a free account</a> to save your story.\n    </p>\n  </div>\n\n  <!-- Status -->\n  <div id=\"write-status\" class=\"write-status\" style=\"display:none;\"></div>\n\n</div>\n\n<!-- Login wall lightbox -->\n<div id=\"login-wall-lightbox\" class=\"lightbox-overlay\" style=\"display:none;\">\n  <div class=\"lightbox-box\">\n    <p>As a non-logged in user, you won't be able to save your story. Create a free account to save stories.</p>\n    <div class=\"lightbox-buttons\">\n      <a href=\"/sign-in\" class=\"btn btn-primary\" id=\"wall-create-account\">Create Free Account</a>\n      <button class=\"btn btn-secondary\" id=\"wall-continue\">Continue Writing</button>\n    </div>\n  </div>\n</div>\n\n<style>\n.write-page { max-width: 720px; margin: 0 auto; }\n.write-control-row { margin-bottom: 20px; }\n.write-control-row label { display: block; margin-bottom: 6px; font-size: 0.95rem; color: #c8bfb0; }\n.write-control-row select,\n.write-control-row textarea {\n  width: 100%; padding: 10px 12px; background: #1a1917; border: 1px solid #3a3630;\n  color: #e8e2d9; border-radius: 6px; font-size: 0.95rem; font-family: inherit;\n}\n.write-control-row textarea { resize: vertical; }\n.radio-group { display: flex; gap: 10px; flex-wrap: wrap; }\n.radio-option {\n  padding: 8px 14px; background: #1a1917; border: 1px solid #3a3630;\n  border-radius: 6px; cursor: pointer; display: flex; align-items: center; gap: 6px;\n  font-size: 0.9rem; color: #c8bfb0; transition: all 0.15s;\n}\n.radio-option.selected { border-color: #e8706a; color: #e8e2d9; }\n.radio-option input[type=\"radio\"] { display: none; }\n.radio-option.premium-locked { opacity: 0.5; cursor: not-allowed; }\n.premium-badge {\n  font-size: 0.7rem; background: #e8706a; color: #0f0e0d;\n  padding: 1px 6px; border-radius: 3px; font-weight: 600;\n}\n.write-button-row { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; }\n.write-action-row {\n  display: flex; align-items: center; justify-content: space-between;\n  gap: 10px; margin-bottom: 20px; flex-wrap: wrap;\n}\n.action-row-left { display: flex; align-items: center; gap: 8px; }\n.action-row-right { display: flex; align-items: center; gap: 8px; }\n.btn-icon-regen {\n  width: 36px; height: 36px; padding: 0; background: #1a1917;\n  border: 1px solid #3a3630; border-radius: 6px; color: #c8bfb0;\n  font-size: 1.1rem; cursor: pointer; line-height: 1;\n  display: flex; align-items: center; justify-content: center;\n  transition: border-color 0.15s, color 0.15s;\n}\n.btn-icon-regen:hover { border-color: #e8706a; color: #e8706a; }\n.btn-primary {\n  padding: 10px 22px; background: #e8706a; color: #fff; border: none;\n  border-radius: 6px; font-family: 'DM Sans', 'Helvetica Neue', sans-serif;\n  font-weight: 600; cursor: pointer; font-size: 0.875rem;\n  display: inline-flex; align-items: center; gap: 6px; transition: all 0.2s ease;\n}\n.btn-primary:hover { background: #f08c87; }\n.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; }\n.btn-secondary {\n  padding: 10px 22px; background: transparent; color: #9a9490; border: 1px solid #3a3633;\n  border-radius: 6px; cursor: pointer; font-family: 'DM Sans', 'Helvetica Neue', sans-serif;\n  font-size: 0.875rem; font-weight: 600; display: inline-flex; align-items: center; gap: 6px; transition: all 0.2s ease;\n}\n.btn-secondary:hover { color: #e8e2d9; border-color: #9a9490; }\n.write-status {\n  padding: 10px 14px; margin-bottom: 16px; border-radius: 6px;\n  background: #1a1917; border: 1px solid #3a3630; font-size: 0.9rem; color: #c8bfb0;\n}\n.write-status.error { border-color: #a33; color: #f88; }\n.write-status.success { border-color: #3a5; color: #8f8; }\n.sections-container { margin-bottom: 20px; }\n.section-block {\n  background: #1a1917; border: 1px solid #3a3630; border-radius: 6px;\n  padding: 16px; margin-bottom: 12px;\n}\n.section-block .section-header {\n  font-size: 0.8rem; color: #8a8070; margin-bottom: 8px;\n}\n.section-block textarea {\n  width: 100%; min-height: 200px; padding: 10px; background: #131210;\n  border: 1px solid #2a2620; color: #e8e2d9; border-radius: 4px;\n  font-family: Georgia, serif; font-size: 1rem; line-height: 1.65; resize: vertical;\n}\n.section-limit-msg {\n  padding: 12px; background: #1e1519; border: 1px solid #e8706a;\n  border-radius: 6px; text-align: center; color: #e8706a; margin-bottom: 16px;\n}\n.save-area { display: flex; justify-content: flex-end; align-items: center; gap: 12px; margin-bottom: 20px; }\n.save-login-hint {\n  margin-top: 8px; font-size: 0.85rem; color: #6a6050;\n}\n.save-login-hint a { color: #8a7a68; text-decoration: underline; }\n.save-login-hint a:hover { color: #c8bfb0; }\n.content-failed-area {\n  padding: 14px 16px; margin-bottom: 16px; background: #1a1210;\n  border: 1px solid #a33; border-radius: 6px;\n}\n.content-failed-area p {\n  color: #f88; font-size: 0.9rem; line-height: 1.5; margin: 0 0 12px 0;\n}\n.continuation-banner {\n  background: #1a1917; border: 1px solid #3a3630; border-radius: 6px;\n  padding: 14px 16px; margin-bottom: 20px;\n}\n.continuation-banner p { margin: 0 0 4px 0; color: #c8bfb0; }\n.continuation-banner p:last-child { font-weight: 600; color: #e8706a; }\n\n/* Writing tips panel */\n.writing-tips { margin-bottom: 20px; }\n.tips-toggle {\n  display: flex; align-items: center; gap: 6px; background: none; border: none;\n  color: #6b6560; font-family: inherit; font-size: 0.82rem; cursor: pointer;\n  padding: 4px 0; letter-spacing: 0.02em; transition: color 0.15s;\n}\n.tips-toggle:hover { color: #9a9490; }\n.tips-toggle .tips-icon { color: #e8706a; font-size: 0.75rem; }\n.tips-toggle .tips-chevron {\n  margin-left: auto; transition: transform 0.2s; display: inline-block; font-style: normal;\n}\n.tips-toggle.open .tips-chevron { transform: rotate(90deg); }\n.tips-body {\n  display: none; margin-top: 10px; background: #1a1917; border: 1px solid #3a3630;\n  border-radius: 6px; padding: 16px; display: none; flex-direction: column; gap: 12px;\n}\n.tips-body.open { display: flex; }\n.tip-item { font-size: 0.85rem; color: #9a9490; line-height: 1.55; }\n.tip-item .tip-label {\n  display: inline-block; font-weight: 600; color: #c8bfb0; margin-right: 6px;\n}\n.tip-item em { font-style: italic; color: #c8bfb0; }\n\n/* Section hint label */\n.section-hint { font-weight: 400; font-style: italic; color: #6b6560; font-size: 0.78rem; }\n\n/* Contextual hint toast */\n.context-hint {\n  display: flex; align-items: flex-start; gap: 10px;\n  background: #1f1e1a; border: 1px solid #e8706a; border-radius: 6px;\n  padding: 12px 14px; margin-bottom: 14px; font-size: 0.85rem; color: #c8bfb0; line-height: 1.5;\n}\n.context-hint-icon { color: #e8706a; font-size: 0.9rem; flex-shrink: 0; margin-top: 1px; }\n.context-hint-body { flex: 1; }\n.context-hint-title { font-weight: 600; color: #e8706a; margin-bottom: 2px; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; }\n.context-hint-dismiss {\n  background: none; border: none; color: #6b6560; cursor: pointer; font-size: 1rem;\n  line-height: 1; padding: 0; flex-shrink: 0; margin-top: 1px; transition: color 0.15s;\n}\n.context-hint-dismiss:hover { color: #9a9490; }\n\n/* Save flow */\n.save-flow { margin-top: 20px; }\n.title-options { display: flex; flex-direction: column; gap: 8px; margin: 12px 0; }\n.title-option {\n  padding: 10px 14px; background: #1a1917; border: 1px solid #3a3630;\n  border-radius: 6px; cursor: pointer; color: #e8e2d9; transition: all 0.15s;\n}\n.title-option:hover { border-color: #e8706a; }\n.title-option.selected { border-color: #e8706a; background: #1e1519; }\n.tag-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }\n.tag-chip {\n  display: inline-flex; align-items: center; gap: 4px;\n  padding: 4px 10px; background: #1e1519; border: 1px solid #e8706a;\n  border-radius: 16px; font-size: 0.85rem; color: #e8706a;\n}\n.tag-chip .remove-tag { cursor: pointer; font-weight: bold; margin-left: 2px; }\n.tag-input-area { margin: 16px 0; position: relative; }\n.tag-autocomplete-wrap { display: flex; gap: 8px; align-items: flex-start; }\n.tag-autocomplete-wrap input {\n  flex: 1; padding: 8px 12px; background: #1a1917; border: 1px solid #3a3630;\n  color: #e8e2d9; border-radius: 6px; font-size: 0.9rem;\n}\n.btn-tag-add {\n  padding: 8px 16px; background: #1a1917; border: 1px solid #3a3630;\n  color: #c8bfb0; border-radius: 6px; font-size: 0.9rem; cursor: pointer;\n  white-space: nowrap; transition: border-color 0.15s, color 0.15s;\n}\n.btn-tag-add:not(:disabled):hover { border-color: #e8706a; color: #e8706a; }\n.btn-tag-add:disabled { opacity: 0.4; cursor: default; }\n.tag-suggestions {\n  position: absolute; top: 100%; left: 0; right: 0; background: #1a1917;\n  border: 1px solid #3a3630; border-top: none; border-radius: 0 0 6px 6px;\n  max-height: 200px; overflow-y: auto; z-index: 100; display: none;\n}\n.tag-suggestion {\n  padding: 8px 12px; cursor: pointer; color: #c8bfb0; font-size: 0.9rem;\n}\n.tag-suggestion:hover { background: #1e1519; color: #e8706a; }\n\n/* Lightbox */\n.lightbox-overlay {\n  position: fixed; inset: 0; background: rgba(0,0,0,0.7);\n  display: flex; align-items: center; justify-content: center; z-index: 100;\n}\n.lightbox-box {\n  background: #1a1917; border: 1px solid #3a3630; border-radius: 8px;\n  padding: 24px; max-width: 420px; width: 90%; text-align: center;\n}\n.lightbox-box p { margin-bottom: 16px; color: #c8bfb0; line-height: 1.5; }\n.lightbox-buttons { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }\n\n@media (max-width: 600px) {\n  .radio-group { flex-direction: column; }\n  .write-button-row { flex-direction: column; }\n  .write-button-row button { width: 100%; }\n}\n</style>\n\n<script>\n(function() {\n  const AUTH = window.__AUTH_STATE__ || { loggedIn: false, username: null, isPremium: false };\n\n  // ── State ──\n  let sections = [];             // Array of section text strings\n  let prompts = [];              // Prompt used for each section (parallel to sections)\n  let sectionCount = 0;\n  let generateCount = 0;         // for non-logged-in wall trigger every 4th\n  let currentStoryId = null;     // set after initial save\n  let currentStorySlug = null;\n  let currentChapterNumber = 1;\n  let storyType = 'erotica';\n  let continuationStoryTitle = null;\n  let lastChapterContext = '';  // tail of previous chapter, loaded on continuation\n\n  // Query params for continuation: ?story_id=xxx&chapter=N\n  const urlParams = new URLSearchParams(window.location.search);\n  const continueStoryId = urlParams.get('story_id');\n  const continueChapter = urlParams.get('chapter');\n\n  // ── DOM refs ──\n  const $storyType = document.getElementById('story-type');\n  const $promptInput = document.getElementById('prompt-input');\n  const $btnGenerate = document.getElementById('btn-generate');\n  const $btnRegenerate = document.getElementById('btn-regenerate');\n  const $sectionsContainer = document.getElementById('sections-container');\n  const $status = document.getElementById('write-status');\n  const $sectionLimitMsg = document.getElementById('section-limit-msg');\n  const $saveArea = document.getElementById('save-area');\n  const $btnSaveChapter = document.getElementById('btn-save-chapter');\n  const $saveFlowContainer = document.getElementById('save-flow-container');\n  const $writingUI = document.getElementById('writing-ui');\n  const $loginWall = document.getElementById('login-wall-lightbox');\n  const $wallContinue = document.getElementById('wall-continue');\n  const $continuationBanner = document.getElementById('continuation-banner');\n  const $continuationTitle = document.getElementById('continuation-title');\n  const $continuationChapter = document.getElementById('continuation-chapter');\n  const $contentFailedArea = document.getElementById('content-failed-area');\n  const $btnTryAgain = document.getElementById('btn-try-again');\n  const $saveLoginHint = document.getElementById('save-login-hint');\n  let contentFilterFailed = false;\n\n  function hideWritingUI() { $writingUI.style.display = 'none'; $saveArea.style.display = 'none'; }\n  function showWritingUI()  { $writingUI.style.display = ''; }\n\n  // Auto-open writing tips on first ever visit\n  if (!sessionStorage.getItem('uf_tips_seen')) {\n    sessionStorage.setItem('uf_tips_seen', '1');\n    const tipsBody = document.getElementById('tips-body');\n    const tipsToggle = document.getElementById('tips-toggle');\n    if (tipsBody) tipsBody.classList.add('open');\n    if (tipsToggle) tipsToggle.classList.add('open');\n  }\n\n  // Contextual hint toast — inserts above sections-container, auto-dismisses\n  function showContextHint(id, title, message) {\n    if (document.getElementById('hint-' + id)) return; // already shown\n    const el = document.createElement('div');\n    el.className = 'context-hint';\n    el.id = 'hint-' + id;\n    el.innerHTML =\n      '<span class=\"context-hint-icon\">✦</span>' +\n      '<div class=\"context-hint-body\">' +\n        '<div class=\"context-hint-title\">' + title + '</div>' +\n        '<div>' + message + '</div>' +\n      '</div>' +\n      '<button class=\"context-hint-dismiss\" title=\"Dismiss\">×</button>';\n    el.querySelector('.context-hint-dismiss').addEventListener('click', () => el.remove());\n    const sc = document.getElementById('sections-container');\n    sc.parentNode.insertBefore(el, sc);\n  }\n\n  // ── Premium unlock ──\n  if (AUTH.isPremium) {\n    document.querySelectorAll('.premium-locked').forEach(el => {\n      el.classList.remove('premium-locked');\n      const radio = el.querySelector('input[type=\"radio\"]');\n      if (radio) radio.disabled = false;\n    });\n  }\n\n  // ── Radio group behavior ──\n  document.querySelectorAll('.radio-group').forEach(group => {\n    group.querySelectorAll('.radio-option').forEach(opt => {\n      opt.addEventListener('click', () => {\n        const radio = opt.querySelector('input[type=\"radio\"]');\n        if (radio.disabled) {\n          if (!AUTH.isPremium) window.location.href = '/upgrade';\n          return;\n        }\n        group.querySelectorAll('.radio-option').forEach(o => o.classList.remove('selected'));\n        opt.classList.add('selected');\n        radio.checked = true;\n      });\n    });\n  });\n\n  // ── Continuation setup ──\n  if (continueStoryId && continueChapter) {\n    currentStoryId = continueStoryId;\n    currentChapterNumber = parseInt(continueChapter, 10);\n    loadContinuationContext();\n  }\n\n  async function loadContinuationContext() {\n    try {\n      const res = await fetch('/api/write/story-context?story_id=' + encodeURIComponent(currentStoryId));\n      const data = await res.json();\n      if (data.error) throw new Error(data.error);\n      continuationStoryTitle = data.story_title;\n      currentStorySlug = data.slug || null;\n      storyType = data.story_type || 'erotica';\n      lastChapterContext = data.last_chapter_context || '';\n      $storyType.value = storyType;\n      $storyType.disabled = true;\n      $continuationBanner.style.display = '';\n      $continuationTitle.textContent = continuationStoryTitle;\n      $continuationChapter.textContent = 'You are now working on Chapter ' + currentChapterNumber;\n    } catch (e) {\n      showStatus('Failed to load story context: ' + e.message, 'error');\n    }\n  }\n\n  // ── Status ──\n  function showStatus(msg, type) {\n    $status.style.display = '';\n    $status.className = 'write-status' + (type ? ' ' + type : '');\n    $status.textContent = msg;\n  }\n  function hideStatus() { $status.style.display = 'none'; }\n\n  // ── Non-logged-in wall ──\n  function maybeShowWall() {\n    if (AUTH.loggedIn) return;\n    generateCount++;\n    if (generateCount % 4 === 0) {\n      $loginWall.style.display = '';\n    }\n  }\n  $wallContinue.addEventListener('click', () => { $loginWall.style.display = 'none'; });\n\n  // ── Try Again (after content filter fail) ──\n  $btnTryAgain.addEventListener('click', () => {\n    contentFilterFailed = false;\n    $contentFailedArea.style.display = 'none';\n    $btnGenerate.style.display = '';\n    updateButtonStates();\n    $promptInput.focus();\n  });\n\n  // ── Generate ──\n  $btnGenerate.addEventListener('click', async () => {\n    const prompt = $promptInput.value.trim();\n    if (!prompt) { showStatus('Enter a prompt to generate content.', 'error'); return; }\n    if (sectionCount >= 12) return;\n\n    hideStatus();\n    $btnGenerate.disabled = true;\n    $btnGenerate.textContent = 'Generating…';\n\n    const selectedModel = document.querySelector('input[name=\"model\"]:checked').value;\n    const wordCount = document.querySelector('input[name=\"section-length\"]:checked').value;\n    storyType = $storyType.value;\n\n    // Build context: last N sections from current chapter, or tail of previous chapter\n    const contextLimit = (selectedModel === 'pro' || selectedModel === 'pro2' || selectedModel === 'pro3') ? 8 : 3;\n    const contextSections = sections.slice(-contextLimit);\n    let contextText = '';\n    if (contextSections.length > 0) {\n      contextText = 'Previous sections:\\n\\n' + contextSections.join('\\n\\n');\n    } else if (lastChapterContext) {\n      contextText = 'End of previous chapter (for continuity):\\n\\n' + lastChapterContext;\n    }\n\n    try {\n      const res = await fetch('/api/write/generate', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          prompt,\n          story_type: storyType,\n          model: selectedModel,\n          word_count: parseInt(wordCount, 10),\n          context: contextText,\n          story_id: currentStoryId,\n          chapter_number: currentChapterNumber,\n        }),\n      });\n\n      const data = await res.json();\n\n      if (data.error) {\n        showStatus(data.error, 'error');\n        return;\n      }\n\n      if (data.content_check_failed) {\n        // Fix 4: Show dedicated content-failed area, hide generate/regenerate\n        contentFilterFailed = true;\n        $btnGenerate.style.display = 'none';\n        $btnRegenerate.style.display = 'none';\n        $contentFailedArea.style.display = '';\n        return;\n      }\n\n      // Success — add section\n      prompts.push(prompt);\n      addSection(data.text);\n      $promptInput.value = '';\n      // Fix 6: Lock story type after first section\n      if (sectionCount === 1) $storyType.disabled = true;\n      contentFilterFailed = false;\n      $contentFailedArea.style.display = 'none';\n      // Contextual hint: memory reminder after section 4 (one-time)\n      if (sectionCount === 4 && !sessionStorage.getItem('uf_memory_hint_shown')) {\n        sessionStorage.setItem('uf_memory_hint_shown', '1');\n        showContextHint('memory', 'Short memory reminder', 'The model only sees the last few sections. Re-mention names, dynamics, or details that matter in your next prompt.');\n      }\n      maybeShowWall();\n\n    } catch (e) {\n      showStatus('Generation failed: ' + e.message, 'error');\n    } finally {\n      $btnGenerate.disabled = false;\n      updateButtonStates();\n    }\n  });\n\n  // ── Regenerate ──\n  $btnRegenerate.addEventListener('click', async () => {\n    if (sections.length === 0) return;\n\n    // Remove last section and restore its prompt\n    sections.pop();\n    const lastPrompt = prompts.pop() || '';\n    sectionCount--;\n    const lastBlock = $sectionsContainer.lastElementChild;\n    if (lastBlock) lastBlock.remove();\n    $promptInput.value = lastPrompt;\n    updateButtonStates();\n\n    // Trigger generate with the restored prompt\n    $btnGenerate.click();\n  });\n\n  // ── Add section to DOM ──\n  function addSection(text) {\n    sectionCount++;\n    sections.push(text);\n\n    const block = document.createElement('div');\n    block.className = 'section-block';\n    const isFirst = sectionCount === 1;\n    block.innerHTML = '<div class=\"section-header\">Section ' + sectionCount + (isFirst ? ' — <span class=\"section-hint\">edit freely before continuing</span>' : '') + '</div>' +\n      '<textarea data-section=\"' + (sectionCount - 1) + '\">' + escHtml(text) + '</textarea>';\n\n    // Track edits\n    const ta = block.querySelector('textarea');\n    ta.addEventListener('input', () => {\n      sections[parseInt(ta.dataset.section, 10)] = ta.value;\n    });\n\n    $sectionsContainer.appendChild(block);\n    block.scrollIntoView({ behavior: 'smooth', block: 'end' });\n  }\n\n  function escHtml(s) {\n    const d = document.createElement('div');\n    d.textContent = s;\n    return d.innerHTML;\n  }\n\n  // ── Button state management ──\n  function updateButtonStates() {\n    if (sectionCount === 0) {\n      $btnGenerate.textContent = 'Generate First Section';\n      $btnGenerate.style.display = '';\n      $btnRegenerate.style.display = 'none';\n      $saveArea.style.display = 'none';\n      $sectionLimitMsg.style.display = 'none';\n      if (!continueStoryId) $storyType.disabled = false;\n    } else if (sectionCount >= 12) {\n      $btnGenerate.style.display = 'none';\n      $sectionLimitMsg.style.display = '';\n      $btnRegenerate.style.display = 'none';\n      $saveArea.style.display = '';\n      $btnSaveChapter.style.display = AUTH.loggedIn ? '' : 'none';\n      $saveLoginHint.style.display = AUTH.loggedIn ? 'none' : '';\n    } else {\n      $btnGenerate.textContent = 'Generate Next Section';\n      $btnGenerate.style.display = '';\n      $sectionLimitMsg.style.display = 'none';\n      $btnRegenerate.style.display = '';\n      $saveArea.style.display = '';\n      $btnSaveChapter.style.display = AUTH.loggedIn ? '' : 'none';\n      $saveLoginHint.style.display = AUTH.loggedIn ? 'none' : '';\n    }\n  }\n\n  // ── Save Chapter (Chapter 1 vs 2+) ──\n  $btnSaveChapter.addEventListener('click', async () => {\n    if (!AUTH.loggedIn) { window.location.href = '/sign-in'; return; }\n    if (sections.length === 0) return;\n\n    $btnSaveChapter.disabled = true;\n    showStatus('Generating title suggestions…', '');\n\n    const chapterText = sections.join('\\n\\n');\n    const firstSection = sections[0];\n    const lastSection = sections[sections.length - 1];\n\n    try {\n      const res = await fetch('/api/write/get-titles', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({\n          first_section: firstSection,\n          last_section: lastSection,\n          is_first_chapter: currentChapterNumber === 1,\n        }),\n      });\n      const data = await res.json();\n      if (data.error) throw new Error(data.error);\n\n      hideStatus();\n\n      if (currentChapterNumber === 1) {\n        showChapter1SaveFlow(data, chapterText);\n      } else {\n        showChapterNSaveFlow(data, chapterText);\n      }\n    } catch (e) {\n      showStatus('Failed to generate titles: ' + e.message, 'error');\n      $btnSaveChapter.disabled = false;\n    }\n  });\n\n  // ── Chapter 1 Save Flow ──\n  function showChapter1SaveFlow(titleData, chapterText) {\n    hideWritingUI();\n    let selectedStoryTitle = null;\n    let selectedChapterTitle = null;\n\n    let html = '<div class=\"save-flow\">';\n    html += '<h3 style=\"color:#e8706a;margin-bottom:12px;\">Choose a Story Title</h3>';\n    html += '<div class=\"title-options\" id=\"story-title-options\">';\n    (titleData.story_titles || []).forEach((t, i) => {\n      html += '<div class=\"title-option\" data-value=\"' + escAttr(t) + '\">' + escHtml(t) + '</div>';\n    });\n    html += '</div>';\n\n    html += '<h3 style=\"color:#e8706a;margin:16px 0 12px;\">Choose a Chapter Title</h3>';\n    html += '<div class=\"title-options\" id=\"chapter-title-options\">';\n    (titleData.chapter_titles || []).forEach((t, i) => {\n      html += '<div class=\"title-option\" data-value=\"' + escAttr(t) + '\">' + escHtml(t) + '</div>';\n    });\n    html += '</div>';\n\n    html += '<button id=\"btn-next-save\" class=\"btn btn-primary\" disabled>Next</button>';\n    html += '</div>';\n\n    $saveFlowContainer.innerHTML = html;\n\n    // Selection logic\n    document.querySelectorAll('#story-title-options .title-option').forEach(el => {\n      el.addEventListener('click', () => {\n        document.querySelectorAll('#story-title-options .title-option').forEach(o => o.classList.remove('selected'));\n        el.classList.add('selected');\n        selectedStoryTitle = el.dataset.value;\n        checkNextEnabled();\n      });\n    });\n\n    document.querySelectorAll('#chapter-title-options .title-option').forEach(el => {\n      el.addEventListener('click', () => {\n        document.querySelectorAll('#chapter-title-options .title-option').forEach(o => o.classList.remove('selected'));\n        el.classList.add('selected');\n        selectedChapterTitle = el.dataset.value;\n        checkNextEnabled();\n      });\n    });\n\n    function checkNextEnabled() {\n      document.getElementById('btn-next-save').disabled = !(selectedStoryTitle && selectedChapterTitle);\n    }\n\n    document.getElementById('btn-next-save').addEventListener('click', async () => {\n      const btn = document.getElementById('btn-next-save');\n      btn.disabled = true;\n      btn.textContent = 'Saving…';\n\n      try {\n        // Step 2: Initial save\n        const saveRes = await fetch('/api/write/save-initial', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({\n            story_title: selectedStoryTitle,\n            chapter_title: selectedChapterTitle,\n            chapter_text: chapterText,\n            story_type: storyType,\n            model: document.querySelector('input[name=\"model\"]:checked').value,\n          }),\n        });\n        const saveData = await saveRes.json();\n        if (saveData.error) throw new Error(saveData.error);\n\n        currentStoryId = saveData.story_id;\n        currentStorySlug = saveData.slug;\n\n        // Step 3: Generate metadata (parallel calls)\n        btn.textContent = 'Generating metadata…';\n        const metaRes = await fetch('/api/write/generate-metadata', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({\n            story_id: currentStoryId,\n            chapter_text: chapterText,\n            story_type: storyType,\n          }),\n        });\n        const metaData = await metaRes.json();\n        if (metaData.error) throw new Error(metaData.error);\n\n        // Step 4: Show tag selection\n        showTagSelection(metaData.prepopulated_tags || []);\n\n      } catch (e) {\n        showStatus('Save failed: ' + e.message, 'error');\n        btn.disabled = false;\n        btn.textContent = 'Next';\n      }\n    });\n  }\n\n  // ── Chapter N Save Flow (streamlined) ──\n  function showChapterNSaveFlow(titleData, chapterText) {\n    hideWritingUI();\n    let selectedChapterTitle = null;\n\n    let html = '<div class=\"save-flow\">';\n    html += '<h3 style=\"color:#e8706a;margin-bottom:12px;\">Choose a Chapter Title</h3>';\n    html += '<div class=\"title-options\" id=\"chapter-title-options\">';\n    (titleData.chapter_titles || []).forEach(t => {\n      html += '<div class=\"title-option\" data-value=\"' + escAttr(t) + '\">' + escHtml(t) + '</div>';\n    });\n    html += '</div>';\n    html += '<button id=\"btn-save-chapter-n\" class=\"btn btn-primary\" disabled>Save Chapter ' + currentChapterNumber + '</button>';\n    html += '</div>';\n\n    $saveFlowContainer.innerHTML = html;\n\n    document.querySelectorAll('#chapter-title-options .title-option').forEach(el => {\n      el.addEventListener('click', () => {\n        document.querySelectorAll('#chapter-title-options .title-option').forEach(o => o.classList.remove('selected'));\n        el.classList.add('selected');\n        selectedChapterTitle = el.dataset.value;\n        document.getElementById('btn-save-chapter-n').disabled = false;\n      });\n    });\n\n    document.getElementById('btn-save-chapter-n').addEventListener('click', async () => {\n      const btn = document.getElementById('btn-save-chapter-n');\n      btn.disabled = true;\n      btn.textContent = 'Saving…';\n\n      try {\n        const res = await fetch('/api/write/save-chapter', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({\n            story_id: currentStoryId,\n            chapter_number: currentChapterNumber,\n            chapter_title: selectedChapterTitle,\n            chapter_text: chapterText,\n          }),\n        });\n        const data = await res.json();\n        if (data.error) throw new Error(data.error);\n\n        showChapterSavedSuccess();\n      } catch (e) {\n        showStatus('Save failed: ' + e.message, 'error');\n        btn.disabled = false;\n        btn.textContent = 'Save Chapter ' + currentChapterNumber;\n      }\n    });\n  }\n\n  // ── Tag Selection (Chapter 1 only) ──\n  function showTagSelection(prepopulatedTags) {\n    let currentTags = [...prepopulatedTags];\n\n    let html = '<div class=\"save-flow\">';\n    html += '<h3 style=\"color:#e8706a;margin-bottom:8px;\">Tag Your Story</h3>';\n    html += '<p style=\"color:#c8bfb0;margin-bottom:12px;\">Add/remove tags to your story. At least 3. More is better.</p>';\n    html += '<div class=\"tag-input-area\">';\n    html += '  <div class=\"tag-chips\" id=\"tag-chips\"></div>';\n    html += '  <div class=\"tag-autocomplete-wrap\">';\n    html += '    <input type=\"text\" id=\"tag-search-input\" placeholder=\"Search tags…\" autocomplete=\"off\">';\n    html += '    <button id=\"btn-add-tag\" class=\"btn-tag-add\" disabled>Add</button>';\n    html += '    <div class=\"tag-suggestions\" id=\"tag-suggestions\"></div>';\n    html += '  </div>';\n    html += '</div>';\n    html += '<button id=\"btn-save-tags\" class=\"btn btn-primary\" disabled>Save Story</button>';\n    html += '</div>';\n\n    $saveFlowContainer.innerHTML = html;\n\n    const $tagChips = document.getElementById('tag-chips');\n    const $tagInput = document.getElementById('tag-search-input');\n    const $tagSuggestions = document.getElementById('tag-suggestions');\n    const $btnSaveTags = document.getElementById('btn-save-tags');\n    const $btnAddTag = document.getElementById('btn-add-tag');\n\n    // Track which tag name is currently confirmed via autocomplete selection\n    let confirmedTagName = null;\n\n    function tryAddTag(tagName) {\n      if (!tagName) return;\n      if (currentTags.includes(tagName)) return;\n      if (currentTags.length >= 10) { alert('Maximum 10 tags.'); return; }\n      currentTags.push(tagName);\n      renderChips();\n      updateSaveTagsBtn();\n      $tagInput.value = '';\n      $tagSuggestions.style.display = 'none';\n      confirmedTagName = null;\n      $btnAddTag.disabled = true;\n    }\n\n    $btnAddTag.addEventListener('click', () => tryAddTag(confirmedTagName));\n\n    $tagInput.addEventListener('keydown', (e) => {\n      if (e.key === 'Enter') { e.preventDefault(); tryAddTag(confirmedTagName); }\n    });\n\n    function renderChips() {\n      $tagChips.innerHTML = currentTags.map(t =>\n        '<span class=\"tag-chip\">' + escHtml(t) +\n        ' <span class=\"remove-tag\" data-tag=\"' + escAttr(t) + '\">&times;</span></span>'\n      ).join('');\n      $tagChips.querySelectorAll('.remove-tag').forEach(el => {\n        el.addEventListener('click', () => {\n          currentTags = currentTags.filter(t => t !== el.dataset.tag);\n          renderChips();\n          updateSaveTagsBtn();\n        });\n      });\n    }\n\n    function updateSaveTagsBtn() {\n      $btnSaveTags.disabled = currentTags.length < 3;\n    }\n\n    renderChips();\n    updateSaveTagsBtn();\n\n    // Autocomplete from approved_tags — single input listener\n    let debounceTimer;\n    $tagInput.addEventListener('input', () => {\n      const q = $tagInput.value.trim();\n\n      // Reset confirmed tag and Add button on every keystroke\n      confirmedTagName = null;\n      $btnAddTag.disabled = true;\n\n      clearTimeout(debounceTimer);\n      if (q.length < 2) { $tagSuggestions.style.display = 'none'; return; }\n\n      debounceTimer = setTimeout(async () => {\n        try {\n          const res = await fetch('/api/write/approved-tags?q=' + encodeURIComponent(q));\n          const tags = await res.json();\n          if (!Array.isArray(tags) || tags.length === 0) { $tagSuggestions.style.display = 'none'; return; }\n          const filtered = tags.filter(t => !currentTags.includes(t.tag_name)).slice(0, 15);\n          if (filtered.length === 0) { $tagSuggestions.style.display = 'none'; return; }\n          $tagSuggestions.innerHTML = filtered\n            .map(t => '<div class=\"tag-suggestion\" data-tag=\"' + escAttr(t.tag_name) + '\">' + escHtml(t.tag_name) + '</div>')\n            .join('');\n          $tagSuggestions.style.display = 'block';\n          $tagSuggestions.querySelectorAll('.tag-suggestion').forEach(el => {\n            el.addEventListener('mousedown', (e) => {\n              e.preventDefault();\n              confirmedTagName = el.dataset.tag;\n              $tagInput.value = el.dataset.tag;\n              $tagSuggestions.style.display = 'none';\n              $btnAddTag.disabled = false;\n              tryAddTag(confirmedTagName);\n            });\n          });\n        } catch { $tagSuggestions.style.display = 'none'; }\n      }, 250);\n    });\n\n    // Hide suggestions when clicking outside the tag input area\n    document.addEventListener('click', (e) => {\n      if (!$tagInput.contains(e.target) && !$tagSuggestions.contains(e.target)) {\n        $tagSuggestions.style.display = 'none';\n      }\n    });\n\n    // Save tags\n    $btnSaveTags.addEventListener('click', async () => {\n      $btnSaveTags.disabled = true;\n      $btnSaveTags.textContent = 'Saving…';\n      try {\n        const res = await fetch('/api/write/save-tags', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ story_id: currentStoryId, tags: currentTags }),\n        });\n        const data = await res.json();\n        if (data.error) throw new Error(data.error);\n\n        showChapterSavedSuccess();\n      } catch (e) {\n        showStatus('Failed to save tags: ' + e.message, 'error');\n        $btnSaveTags.disabled = false;\n        $btnSaveTags.textContent = 'Save Story';\n      }\n    });\n  }\n\n  // ── Success + Continue Writing ──\n  function showChapterSavedSuccess() {\n    const nextChapter = currentChapterNumber + 1;\n    $saveFlowContainer.innerHTML =\n      '<div class=\"save-flow\">' +\n      '<p class=\"write-status success\" style=\"display:block;\">' +\n      'Success! Start Next Chapter Below!' +\n      '</p>' +\n      '<a href=\"/write?story_id=' + encodeURIComponent(currentStoryId) + '&chapter=' + nextChapter +\n      '\" class=\"btn btn-primary\" style=\"margin-top:12px;text-decoration:none;\">Start Chapter ' + nextChapter + '</a>' +\n      ' <a href=\"/stories/' + (currentStorySlug || '') + '\" class=\"btn btn-secondary\" ' +\n      'style=\"margin-top:12px;text-decoration:none;\">View Story</a>' +\n      '</div>';\n  }\n\n  function escAttr(s) {\n    return String(s).replace(/&/g, '&amp;').replace(/\"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n  }\n\n})();\n</script>"}