<template>
  <div class="root">
    <header class="topbar">
      <h1>🎵 Music</h1>
      <input
        class="search"
        type="text"
        v-model="search"
        placeholder="搜索曲目 / 歌手"
      />
      <span class="count">{{ filtered.length }} / {{ pieces.length }} 首</span>
      <button class="btn-inspire" title="今天练什么？" @click="openInspire">💡</button>
      <router-link to="/upload" class="btn-add" title="新增曲目">＋</router-link>
    </header>

    <nav class="filterbar">
      <span class="fb-label">歌单</span>
      <button
        class="chip"
        :class="{ active: !activePlaylistId }"
        @click="setPlaylist(null)"
      >全部</button>
      <button
        v-for="pl in playlists"
        :key="pl.id"
        class="chip"
        :class="{ active: activePlaylistId === pl.id }"
        @click="setPlaylist(pl.id)"
      >{{ pl.name }}<span class="chip-n">{{ pl.count }}</span></button>
      <button class="chip new-chip" @click="promptNewPlaylist">＋ 新歌单</button>

      <span class="fb-sep">·</span>
      <span class="fb-label">标签</span>
      <button
        class="chip"
        :class="{ active: !activeTagName }"
        @click="setTag(null)"
      >全部</button>
      <button
        v-for="t in tagsList"
        :key="t.id"
        class="chip"
        :class="{ active: activeTagName === t.name }"
        @click="setTag(t.name)"
      >{{ t.name }}<span class="chip-n">{{ t.count }}</span></button>
    </nav>

    <div class="main">
      <aside class="sidebar" :class="{ 'has-selected': !!selected }">
        <div class="sort-bar">
          <button :class="{ active: sortMode === 'name' }" @click="setSort('name')">名称</button>
          <button :class="{ active: sortMode === 'hot' }" @click="setSort('hot')">最多播放</button>
          <button :class="{ active: sortMode === 'least' }" @click="setSort('least')">最少播放</button>
          <button :class="{ active: sortMode === 'recent' }" @click="setSort('recent')">最近</button>
          <button :class="{ active: sortMode === 'random' }" @click="setSort('random')">随机</button>
        </div>

        <div class="playlist">
          <p v-if="loading" class="hint">加载中…</p>
          <p v-else-if="loadError" class="hint err">{{ loadError }}</p>
          <p v-else-if="filtered.length === 0" class="hint">
            空空如也<span v-if="activePlaylistId || activeTagName">（当前筛选下）</span>。
          </p>
          <div
            v-for="p in filtered"
            :key="p.id"
            class="row"
            :class="{ active: selectedId === p.id }"
            @click="selectPiece(p.id)"
          >
            <div class="row-main">
              <div class="row-title">{{ p.title }}</div>
              <div class="row-meta">
                <span v-if="p.artist">{{ p.artist }}</span>
                <span v-if="p.category" class="cat">{{ p.category }}</span>
                <span v-for="tg in p.tags" :key="tg" class="tg">#{{ tg }}</span>
              </div>
            </div>
            <div class="badges">
              <span v-if="p.has_lyrics" class="badge" title="有歌词">词</span>
              <span v-for="k in iconKinds(p.kinds)" :key="k" class="badge" :title="k">{{ kindLabel(k) }}</span>
              <span v-if="p.play_count > 0" class="play-count">{{ p.play_count }}</span>
            </div>
          </div>
        </div>
      </aside>

      <section class="player-area">
        <div v-if="!selected" class="empty">
          <p>从左边挑一首吧 🎶</p>
        </div>

        <template v-else>
          <header class="now-playing">
            <h2>{{ selected.title }}</h2>
            <div class="np-sub">
              <span v-if="selected.artist">{{ selected.artist }}</span>
              <span v-if="selected.category">· {{ selected.category }}</span>
              <span v-if="selected.play_count">· 播放 {{ selected.play_count }} 次</span>
              <router-link :to="{ name: 'edit', params: { id: selected.id } }" class="edit-link">编辑</router-link>
            </div>
            <div v-if="selected.tags && selected.tags.length" class="np-tags">
              <span v-for="tg in selected.tags" :key="tg" class="tg">#{{ tg }}</span>
            </div>
          </header>

          <nav v-if="tabs.length" class="tabs">
            <button
              v-for="t in tabs"
              :key="t.key"
              :class="{ active: activeTab === t.key }"
              @click="setTab(t.key)"
            >{{ t.label }}<span v-if="t.count > 1" class="tab-n">{{ t.count }}</span></button>
          </nav>

          <main class="content">
            <!-- 歌词 -->
            <div v-show="activeTab === 'lyrics'" class="lyrics-box" ref="lyricsBoxEl">
              <div v-if="lyricsLines.length === 0" class="lyrics-none">
                <span v-if="selected.lyrics">这首歌的歌词不是 LRC 格式</span>
                <span v-else>暂无歌词，用心感受 🎶</span>
                <pre v-if="selected.lyrics" class="lyrics-raw">{{ selected.lyrics }}</pre>
              </div>
              <div
                v-for="(line, i) in lyricsLines"
                :key="i"
                class="lyrics-line"
                :class="{ active: i === activeLyricIdx }"
                :data-i="i"
                @click="seek(line.time)"
              >{{ line.text }}</div>
            </div>

            <!-- 和弦谱 -->
            <div v-show="activeTab === 'chord'" class="sheet-box">
              <img
                v-for="att in chordTabAttachments('chord')"
                :key="att.id"
                :src="attachmentUrl(att.id)"
                :alt="att.filename"
                class="sheet-img"
                @click="fullscreenSrc = attachmentUrl(att.id)"
              />
              <div
                v-if="chordTabAttachments('chord').length === 0"
                class="auto-fetch"
              >
                <p v-if="chordStateOf('chord') === 'idle'" class="hint-line">
                  从 yopu.co 抓 <b>和弦谱（歌词 + 数字级数 1/4/5/6m）</b>。
                </p>
                <p v-else-if="['pending','processing'].includes(chordStateOf('chord'))" class="hint-line">
                  正在抓取，约 30-60s…
                </p>
                <p v-else-if="chordStateOf('chord') === 'failed'" class="hint-line err">
                  抓取失败：{{ chordErrors.chord }}
                </p>
                <button
                  class="btn-fetch"
                  :disabled="['pending','processing'].includes(chordStateOf('chord'))"
                  @click="startChordFetch('chord')"
                >
                  <span v-if="['pending','processing'].includes(chordStateOf('chord'))" class="spin">⏳</span>
                  <span v-else>🎸 自动抓取和弦谱</span>
                </button>
              </div>
            </div>

            <!-- 简谱（手动上传的图） / 五线谱 -->
            <div v-show="['numbered', 'staff'].includes(activeTab)" class="sheet-box">
              <img
                v-for="att in roleAttachments(activeTab)"
                :key="att.id"
                :src="attachmentUrl(att.id)"
                :alt="att.filename"
                class="sheet-img"
                @click="fullscreenSrc = attachmentUrl(att.id)"
              />
            </div>

            <!-- PDF -->
            <div v-show="activeTab === 'pdf'" class="pdf-box">
              <iframe
                v-for="att in pdfAttachments"
                :key="att.id"
                :src="attachmentUrl(att.id)"
                :title="att.filename"
                class="pdf-frame"
              />
            </div>

            <!-- 视频 -->
            <div v-show="activeTab === 'video'" class="video-box">
              <video
                v-for="att in videoAttachments"
                :key="att.id"
                :src="attachmentUrl(att.id)"
                controls
                class="video-el"
              />
            </div>

            <!-- 笔记（独立 tab） -->
            <div v-show="activeTab === 'notes'" class="notes-box">
              <div class="notes-head">
                <span>练琴心得 / 难点 / 备注</span>
                <span v-if="notesSavedFlash" class="saved">已保存</span>
              </div>
              <textarea
                v-model="notesDraft"
                @input="onNotesInput"
                class="notes-area"
                placeholder="自动保存。任何想法都丢这里…"
              />
            </div>
          </main>

          <footer class="controls">
            <div class="ctrl-row">
              <label class="repeat" :class="{ on: repeatOne }" @click="repeatOne = !repeatOne">
                <span>循环</span>
                <span class="track"><span class="thumb"></span></span>
              </label>
              <button @click="prev" class="btn-icon" title="上一首">⏮</button>
              <button @click="togglePlay" class="btn-icon big" :title="playing ? '暂停' : '播放'">
                {{ playing ? '⏸' : '▶' }}
              </button>
              <button @click="next" class="btn-icon" title="下一首">⏭</button>
              <span class="time">{{ fmtTime(currentTime) }}</span>
              <div class="bar" @click="seekBar">
                <div
                  v-if="loopA != null"
                  class="loop-mark"
                  :style="{ left: progressForTime(loopA) + '%' }"
                  title="A"
                >A</div>
                <div
                  v-if="loopB != null"
                  class="loop-mark"
                  :style="{ left: progressForTime(loopB) + '%' }"
                  title="B"
                >B</div>
                <div
                  v-if="loopA != null && loopB != null"
                  class="loop-range"
                  :style="loopRangeStyle"
                ></div>
                <div class="fill" :style="{ width: progressPct + '%' }"></div>
              </div>
              <span class="time">{{ fmtTime(duration) }}</span>
              <button class="ab-btn" :class="{ on: loopA != null }" title="A 点" @click="setA">A</button>
              <button class="ab-btn" :class="{ on: loopB != null }" title="B 点" @click="setB">B</button>
              <button
                class="ab-btn"
                :class="{ on: loopOn }"
                :disabled="loopA == null || loopB == null"
                title="A↔B 循环"
                @click="toggleLoop"
              >🔁</button>
              <button v-if="loopA != null || loopB != null" class="ab-btn clear" title="清掉 A/B" @click="clearLoop">✕</button>
              <button class="rate-btn" :title="`变速 ${rateLabel}`" @click="cycleRate">{{ rateLabel }}</button>
              <button
                class="btn-icon vol-icon"
                :title="muted ? '取消静音' : '静音'"
                @click="toggleMute"
              >{{ volIcon }}</button>
              <input
                class="vol-slider"
                type="range"
                min="0"
                max="100"
                :value="muted ? 0 : Math.round(volume * 100)"
                @input="onVolumeInput"
                title="音量"
              />
            </div>
          </footer>

          <audio
            ref="audioEl"
            @timeupdate="onTimeUpdate"
            @loadedmetadata="onLoaded"
            @ended="onEnded"
            @play="playing = true"
            @pause="playing = false"
          />
        </template>
      </section>

      <!-- 灵感 modal -->
      <div v-if="inspireOpen" class="ins-overlay" @click.self="closeInspire">
        <div class="ins-modal">
          <header class="ins-head">
            <span>💡 今天练什么</span>
            <button class="ins-close" @click="closeInspire">✕</button>
          </header>
          <div class="ins-hint-row">
            <input
              v-model="inspireHint"
              class="ins-hint"
              :disabled="inspireRunning"
              placeholder="可选：心情/目标（如 想轻松点 / 陪儿子 / 学新东西）"
              @keydown.enter.prevent="runInspire"
            />
            <button
              class="ins-go"
              :disabled="inspireRunning"
              @click="runInspire"
            >{{ inspireRunning ? '⏳ 生成中…' : '换一批' }}</button>
          </div>
          <div class="ins-body" v-html="inspireHtml"></div>
          <p v-if="inspireError" class="ins-err">{{ inspireError }}</p>
        </div>
      </div>

      <!-- 全屏乐谱 overlay：再点一下关闭，或按 ESC -->
      <div
        v-if="fullscreenSrc"
        class="fs-overlay"
        @click="fullscreenSrc = null"
      >
        <img :src="fullscreenSrc" class="fs-img" />
      </div>

      <!-- 右侧 LLM chat 边栏 -->
      <aside v-if="selected" class="chat">
        <header class="chat-head">
          <span>Chat · {{ selected.title.slice(0, 16) }}</span>
          <button class="chat-clear" @click="onChatClear" title="清空对话">清空</button>
        </header>
        <div ref="chatBodyEl" class="chat-body">
          <p v-if="chatLoading" class="chat-empty">载入对话…</p>
          <p v-else-if="chatMessages.length === 0 && !chatStreaming" class="chat-empty">
            随便聊点啥，比如：<br>
            「这首歌的副歌为啥用 6m 起？」<br>
            「我吉他扫弦节奏总不稳怎么办？」
          </p>
          <div
            v-for="(m, i) in chatMessages"
            :key="i"
            class="msg"
            :class="m.role"
          >
            <div class="msg-bubble">{{ m.content }}</div>
          </div>
          <div v-if="chatStreaming || chatStreamText" class="msg assistant">
            <div class="msg-bubble">{{ chatStreamText || '…' }}</div>
          </div>
          <p v-if="chatError" class="chat-err">{{ chatError }}</p>
        </div>
        <div class="chat-input">
          <textarea
            v-model="chatDraft"
            @keydown.enter.exact.prevent="onChatSend"
            @keydown.enter.shift.exact="$event => null"
            :disabled="chatStreaming"
            placeholder="Enter 发送 · Shift+Enter 换行"
            rows="2"
          />
          <button
            class="chat-send"
            :disabled="chatStreaming || !chatDraft.trim()"
            @click="onChatSend"
          >{{ chatStreaming ? '⏳' : '↑' }}</button>
        </div>
      </aside>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
  listPieces,
  listPlaylists,
  listTags,
  createPlaylist,
  getPiece,
  patchPiece,
  recordPlay,
  attachmentUrl as attUrl,
  chordFetch,
  chordStatus,
  listChat,
  clearChat,
  streamChat,
  streamInspire,
} from '../lib/api.js'
import { parseLrc } from '../lib/lrc.js'

const route = useRoute()
const router = useRouter()

const pieces = ref([])
const loading = ref(true)
const loadError = ref('')
const selected = ref(null)
const selectedId = ref(null)

const playlists = ref([])
const tagsList = ref([])
const activePlaylistId = ref(null)
const activeTagName = ref(null)

const search = ref('')
const sortMode = ref(localStorage.getItem('music.sort') || 'name')
const repeatOne = ref(false)
const volume = ref(parseFloat(localStorage.getItem('music.vol') || '1'))
const muted = ref(localStorage.getItem('music.muted') === '1')

// 变速播放（保留 0.5/0.75/1/1.25/1.5 五档；浏览器 native preserve pitch 在 macOS Safari/Chrome 默认开）
const rateOptions = [0.5, 0.75, 1, 1.25, 1.5]
const playbackRate = ref(parseFloat(localStorage.getItem('music.rate')) || 1)
const rateLabel = computed(() => {
  const r = playbackRate.value
  return r === 1 ? '1×' : (r % 1 === 0 ? r + '×' : r.toFixed(2).replace(/0$/, '') + '×')
})
function cycleRate() {
  const idx = rateOptions.indexOf(playbackRate.value)
  const next = rateOptions[(idx + 1) % rateOptions.length]
  playbackRate.value = next
  localStorage.setItem('music.rate', String(next))
  if (audioEl.value) {
    audioEl.value.playbackRate = next
    audioEl.value.preservesPitch = true
  }
}

// AB Loop
const loopA = ref(null)
const loopB = ref(null)
const loopOn = ref(false)
function setA() {
  if (!audioEl.value) return
  loopA.value = audioEl.value.currentTime
  if (loopB.value != null && loopB.value <= loopA.value) loopB.value = null
}
function setB() {
  if (!audioEl.value) return
  const t = audioEl.value.currentTime
  if (loopA.value != null && t > loopA.value) {
    loopB.value = t
    loopOn.value = true
  } else {
    // A 没设或 t<=A：忽略
  }
}
function toggleLoop() {
  if (loopA.value != null && loopB.value != null) loopOn.value = !loopOn.value
}
function clearLoop() {
  loopA.value = null
  loopB.value = null
  loopOn.value = false
}
function progressForTime(t) {
  if (!duration.value) return 0
  return Math.max(0, Math.min(100, (t / duration.value) * 100))
}
const loopRangeStyle = computed(() => {
  if (loopA.value == null || loopB.value == null || !duration.value) return {}
  const left = progressForTime(loopA.value)
  const right = progressForTime(loopB.value)
  return { left: left + '%', width: (right - left) + '%' }
})

const volIcon = computed(() => {
  if (muted.value || volume.value === 0) return '🔇'
  if (volume.value < 0.34) return '🔈'
  if (volume.value < 0.67) return '🔉'
  return '🔊'
})

function applyVolume() {
  if (audioEl.value) {
    audioEl.value.volume = volume.value
    audioEl.value.muted = muted.value
  }
}

function onVolumeInput(e) {
  const v = parseInt(e.target.value, 10) / 100
  volume.value = isNaN(v) ? 0 : Math.max(0, Math.min(1, v))
  if (volume.value > 0 && muted.value) muted.value = false
  localStorage.setItem('music.vol', String(volume.value))
  localStorage.setItem('music.muted', muted.value ? '1' : '0')
  applyVolume()
}

function toggleMute() {
  muted.value = !muted.value
  localStorage.setItem('music.muted', muted.value ? '1' : '0')
  applyVolume()
}

const audioEl = ref(null)
const lyricsBoxEl = ref(null)
const playing = ref(false)
const currentTime = ref(0)
const duration = ref(0)
const activeTab = ref('lyrics')
const notesDraft = ref('')
const notesSavedFlash = ref(false)
let notesTimer = null
let randomSeed = Math.random()
let lastReportedId = null

// fullscreen 乐谱
const fullscreenSrc = ref(null)

// 灵感 modal
const inspireOpen = ref(false)
const inspireHint = ref('')
const inspireText = ref('')
const inspireRunning = ref(false)
const inspireError = ref('')
let inspireAbort = null
const inspireHtml = computed(() => mdLite(inspireText.value))

// 极简 markdown：**粗体** + 列表 + 换行 → html
function mdLite(s) {
  if (!s) return '<p class="ins-empty">点「换一批」让 LLM 给你推几首</p>'
  // 转义 html，保留我们后面要插的 tag
  let h = s
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
  // **bold**
  h = h.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>')
  // 行首 - / * 列表
  h = h.replace(/(^|\n)\s*[-*]\s+/g, '$1• ')
  // 段落间双换行 → <p>
  return h.split(/\n{2,}/).map(p => `<p>${p.replace(/\n/g, '<br>')}</p>`).join('')
}

function openInspire() {
  inspireOpen.value = true
  if (!inspireText.value) runInspire()
}
function closeInspire() {
  if (inspireAbort) { try { inspireAbort.abort() } catch {} ; inspireAbort = null }
  inspireRunning.value = false
  inspireOpen.value = false
}

async function runInspire() {
  if (inspireRunning.value) return
  inspireText.value = ''
  inspireError.value = ''
  inspireRunning.value = true
  const ctrl = new AbortController()
  inspireAbort = ctrl
  try {
    const r = await streamInspire(inspireHint.value.trim(), (delta) => {
      inspireText.value += delta
    }, ctrl.signal)
    if (!r.ok) inspireError.value = r.error || '出错'
  } catch (e) {
    if (e.name !== 'AbortError') inspireError.value = e.message || String(e)
  } finally {
    inspireRunning.value = false
    inspireAbort = null
  }
}

// chord —— 单 mode（只抓 yopu 默认数字级数版）
const chordStates = ref({ chord: 'idle' })
const chordErrors = ref({ chord: '' })
const chordPollTimers = { chord: null }
const chordPollStarted = { chord: 0 }
function chordStateOf(tab) {
  return chordStates.value[modeForTab(tab)] || 'idle'
}

// chat
const chatBodyEl = ref(null)
const chatMessages = ref([])
const chatLoading = ref(false)
const chatDraft = ref('')
const chatStreaming = ref(false)
const chatStreamText = ref('')
const chatError = ref('')
let chatAbort = null

const lyricsLines = computed(() => parseLrc(selected.value?.lyrics || ''))

const activeLyricIdx = computed(() => {
  const lines = lyricsLines.value
  if (!lines.length) return -1
  let idx = -1
  for (let i = lines.length - 1; i >= 0; i--) {
    if (currentTime.value >= lines[i].time) { idx = i; break }
  }
  return idx
})

const audioAttachments = computed(() =>
  (selected.value?.attachments || []).filter(a => a.kind === 'audio'))
const videoAttachments = computed(() =>
  (selected.value?.attachments || []).filter(a => a.kind === 'video'))
const pdfAttachments = computed(() =>
  (selected.value?.attachments || []).filter(a => a.kind === 'pdf'))

function roleAttachments(role) {
  return (selected.value?.attachments || []).filter(
    a => a.kind === 'image' && a.role === role,
  )
}

// 和弦谱 tab = role='chord'（数字级数版，yopu 自动抓的就这种）
function chordTabAttachments(tab) {
  return (selected.value?.attachments || []).filter(
    a => a.kind === 'image' && a.role === 'chord',
  )
}

function modeForTab(_tab) {
  return 'chord'  // 唯一抓取模式
}

const tabs = computed(() => {
  if (!selected.value) return []
  const list = []
  if (selected.value.lyrics) list.push({ key: 'lyrics', label: '歌词', count: 0 })
  list.push({ key: 'chord', label: '和弦谱', count: chordTabAttachments('chord').length })
  const num = roleAttachments('numbered').length
  if (num) list.push({ key: 'numbered', label: '简谱(自传)', count: num })
  const staff = roleAttachments('staff').length
  if (staff) list.push({ key: 'staff', label: '五线谱', count: staff })
  if (pdfAttachments.value.length) list.push({ key: 'pdf', label: '乐谱 PDF', count: pdfAttachments.value.length })
  if (videoAttachments.value.length) list.push({ key: 'video', label: '视频', count: videoAttachments.value.length })
  list.push({ key: 'notes', label: '笔记', count: 0 })
  return list
})

const filtered = computed(() => {
  const q = search.value.trim().toLowerCase()
  let arr = pieces.value
  if (q) {
    arr = arr.filter(p => {
      const hay = `${p.title} ${p.artist || ''} ${p.category || ''} ${(p.tags || []).join(' ')}`.toLowerCase()
      return hay.includes(q)
    })
  }
  arr = [...arr]
  switch (sortMode.value) {
    case 'hot':
      arr.sort((a, b) => b.play_count - a.play_count || a.title.localeCompare(b.title, 'zh'))
      break
    case 'least':
      arr.sort((a, b) => a.play_count - b.play_count || a.title.localeCompare(b.title, 'zh'))
      break
    case 'recent':
      arr.sort((a, b) => {
        const ta = a.last_played_at || ''
        const tb = b.last_played_at || ''
        return tb.localeCompare(ta) || a.title.localeCompare(b.title, 'zh')
      })
      break
    case 'random': {
      const seeded = arr.map(p => ({ p, k: hash(p.id, randomSeed) }))
      seeded.sort((a, b) => a.k - b.k)
      arr = seeded.map(x => x.p)
      break
    }
    default:
      arr.sort((a, b) => a.title.localeCompare(b.title, 'zh'))
  }
  return arr
})

function hash(id, seed) {
  let x = (id ^ Math.floor(seed * 1e9)) >>> 0
  x = (x ^ (x << 13)) >>> 0
  x = (x ^ (x >>> 17)) >>> 0
  x = (x ^ (x << 5)) >>> 0
  return x
}

function setSort(mode) {
  if (mode === 'random' && sortMode.value === 'random') {
    randomSeed = Math.random()
  }
  sortMode.value = mode
  localStorage.setItem('music.sort', mode)
}

function iconKinds(kinds) {
  const order = ['audio', 'video', 'pdf', 'image']
  return order.filter(k => kinds.includes(k))
}

function kindLabel(k) {
  return ({ audio: '音', video: '视', pdf: 'PDF', image: '谱' })[k] || k
}

async function loadPieces() {
  loading.value = true
  loadError.value = ''
  try {
    pieces.value = await listPieces({
      tag: activeTagName.value,
      playlist: activePlaylistId.value,
    })
  } catch (e) {
    loadError.value = e.message || String(e)
  } finally {
    loading.value = false
  }
}

async function loadPlaylists() {
  try { playlists.value = await listPlaylists() } catch {}
}
async function loadTags() {
  try { tagsList.value = await listTags() } catch {}
}

async function setPlaylist(id) {
  activePlaylistId.value = id
  if (id) activeTagName.value = null
  await loadPieces()
}
async function setTag(name) {
  activeTagName.value = name
  if (name) activePlaylistId.value = null
  await loadPieces()
}

async function promptNewPlaylist() {
  const name = prompt('新歌单名（如：我喜欢的 / 儿子在练）')
  if (!name || !name.trim()) return
  try {
    const r = await createPlaylist(name.trim())
    await loadPlaylists()
    setPlaylist(r.id)
  } catch (e) {
    alert(e.message || String(e))
  }
}

async function loadPiece(id) {
  selected.value = null
  notesDraft.value = ''
  // 切歌清 AB Loop（rate 保留全局）
  clearLoop()
  stopChordPoll('chord')
  chordStates.value = { chord: 'idle' }
  chordErrors.value = { chord: '' }
  abortChat()
  chatMessages.value = []
  chatStreamText.value = ''
  chatError.value = ''
  if (!id) return
  try {
    const p = await getPiece(id)
    selected.value = p
    notesDraft.value = p.notes || ''
    selectedId.value = p.id
    const t = tabs.value
    if (!t.find(x => x.key === activeTab.value)) {
      activeTab.value = t[0]?.key || 'lyrics'
    }
    await nextTick()
    const first = audioAttachments.value[0]
    if (first && audioEl.value) {
      audioEl.value.src = attUrl(first.id)
      audioEl.value.play().catch(() => {})
    } else if (audioEl.value) {
      audioEl.value.removeAttribute('src')
      audioEl.value.load()
    }
    lastReportedId = null
    // 加载 chat 历史
    loadChat(id)
  } catch (e) {
    loadError.value = e.message || String(e)
  }
}

async function loadChat(id) {
  chatLoading.value = true
  try {
    const arr = await listChat(id)
    if (selectedId.value === id) chatMessages.value = arr || []
  } catch (e) {
    chatError.value = e.message || String(e)
  } finally {
    chatLoading.value = false
  }
}

function selectPiece(id) {
  router.push({ name: 'piece', params: { id } })
}

function attachmentUrl(id) { return attUrl(id) }

function togglePlay() {
  if (!audioEl.value || !audioEl.value.src) return
  if (audioEl.value.paused) audioEl.value.play()
  else audioEl.value.pause()
}

function next() {
  if (!filtered.value.length) return
  const idx = filtered.value.findIndex(p => p.id === selectedId.value)
  const nextIdx = (idx + 1) % filtered.value.length
  selectPiece(filtered.value[nextIdx].id)
}

function prev() {
  if (!filtered.value.length) return
  const idx = filtered.value.findIndex(p => p.id === selectedId.value)
  const prevIdx = (idx - 1 + filtered.value.length) % filtered.value.length
  selectPiece(filtered.value[prevIdx].id)
}

function seek(t) {
  if (!audioEl.value) return
  audioEl.value.currentTime = t
  if (audioEl.value.paused) audioEl.value.play().catch(() => {})
}

function seekBar(e) {
  if (!audioEl.value || !duration.value) return
  const rect = e.currentTarget.getBoundingClientRect()
  const ratio = (e.clientX - rect.left) / rect.width
  audioEl.value.currentTime = Math.max(0, Math.min(1, ratio)) * duration.value
}

function onTimeUpdate(e) {
  currentTime.value = e.target.currentTime
  // AB Loop：到 B 点跳回 A
  if (loopOn.value && loopA.value != null && loopB.value != null
      && currentTime.value >= loopB.value) {
    e.target.currentTime = loopA.value
  }
  if (selectedId.value && lastReportedId !== selectedId.value && currentTime.value >= 10) {
    lastReportedId = selectedId.value
    recordPlay(selectedId.value).then(d => {
      if (selected.value) selected.value.play_count = d.play_count
      const inList = pieces.value.find(p => p.id === selectedId.value)
      if (inList) {
        inList.play_count = d.play_count
        inList.last_played_at = new Date().toISOString().replace('T', ' ').slice(0, 19)
      }
    }).catch(() => {})
  }
  if (activeTab.value === 'lyrics' && lyricsBoxEl.value) {
    const idx = activeLyricIdx.value
    if (idx >= 0) {
      const el = lyricsBoxEl.value.querySelector(`.lyrics-line[data-i="${idx}"]`)
      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
    }
  }
  if (selectedId.value) {
    localStorage.setItem('music.last', JSON.stringify({
      id: selectedId.value,
      time: currentTime.value,
    }))
  }
}

function onLoaded(e) {
  duration.value = e.target.duration || 0
  applyVolume()
  e.target.playbackRate = playbackRate.value
  e.target.preservesPitch = true
}

function onEnded() {
  if (repeatOne.value && audioEl.value) {
    audioEl.value.currentTime = 0
    audioEl.value.play().catch(() => {})
  } else {
    next()
  }
}

const progressPct = computed(() => {
  if (!duration.value) return 0
  return Math.max(0, Math.min(100, (currentTime.value / duration.value) * 100))
})

function fmtTime(s) {
  if (!s || isNaN(s)) return '0:00'
  const m = Math.floor(s / 60)
  const sec = Math.floor(s % 60)
  return m + ':' + (sec < 10 ? '0' : '') + sec
}

function setTab(k) {
  activeTab.value = k
}

// notes auto-save
function onNotesInput() {
  if (!selectedId.value) return
  if (notesTimer) clearTimeout(notesTimer)
  notesTimer = setTimeout(async () => {
    try {
      await patchPiece(selectedId.value, { notes: notesDraft.value || null })
      notesSavedFlash.value = true
      setTimeout(() => (notesSavedFlash.value = false), 1500)
    } catch {}
  }, 600)
}

// chord
async function startChordFetch(mode) {
  if (!selectedId.value) return
  chordStates.value = { ...chordStates.value, [mode]: 'pending' }
  chordErrors.value = { ...chordErrors.value, [mode]: '' }
  try {
    const r = await chordFetch(selectedId.value, mode)
    if (r.status === 'completed') {
      await reloadPiece()
      chordStates.value = { ...chordStates.value, [mode]: 'completed' }
      return
    }
    chordStates.value = { ...chordStates.value, [mode]: r.status || 'pending' }
    chordPollStarted[mode] = Date.now()
    if (chordPollTimers[mode]) clearInterval(chordPollTimers[mode])
    chordPollTimers[mode] = setInterval(() => pollChord(mode), 3000)
  } catch (e) {
    chordStates.value = { ...chordStates.value, [mode]: 'failed' }
    chordErrors.value = { ...chordErrors.value, [mode]: e.message || String(e) }
  }
}

async function pollChord(mode) {
  if (!selectedId.value) { stopChordPoll(mode); return }
  if (Date.now() - chordPollStarted[mode] > 90_000) {
    stopChordPoll(mode)
    chordStates.value = { ...chordStates.value, [mode]: 'failed' }
    chordErrors.value = { ...chordErrors.value, [mode]: '抓取超时' }
    return
  }
  try {
    const r = await chordStatus(selectedId.value, mode)
    chordStates.value = { ...chordStates.value, [mode]: r.status || 'pending' }
    chordErrors.value = { ...chordErrors.value, [mode]: r.error || '' }
    if (r.status === 'completed') {
      stopChordPoll(mode)
      await reloadPiece()
    } else if (r.status === 'failed') {
      stopChordPoll(mode)
    }
  } catch (e) {
    chordErrors.value = { ...chordErrors.value, [mode]: e.message || String(e) }
  }
}

function stopChordPoll(mode) {
  if (chordPollTimers[mode]) {
    clearInterval(chordPollTimers[mode])
    chordPollTimers[mode] = null
  }
}

async function reloadPiece() {
  if (!selectedId.value) return
  try {
    const fresh = await getPiece(selectedId.value)
    selected.value = fresh
  } catch {}
}

// chat
async function onChatSend() {
  const msg = chatDraft.value.trim()
  if (!msg || chatStreaming.value || !selectedId.value) return
  // 乐观追加 user msg（assistant 流完整结束后会从 db 读最终 +1）
  chatMessages.value = [
    ...chatMessages.value,
    { role: 'user', content: msg, id: Date.now() },
  ]
  chatDraft.value = ''
  chatStreamText.value = ''
  chatError.value = ''
  chatStreaming.value = true
  await nextTick(); scrollChatBottom()
  const ctrl = new AbortController()
  chatAbort = ctrl
  const pidAtStart = selectedId.value
  try {
    const result = await streamChat(pidAtStart, msg, (delta) => {
      if (selectedId.value !== pidAtStart) return
      chatStreamText.value += delta
      scrollChatBottom()
    }, ctrl.signal)
    if (selectedId.value !== pidAtStart) return
    if (result.ok) {
      chatMessages.value = [
        ...chatMessages.value,
        { role: 'assistant', content: chatStreamText.value, id: Date.now() + 1 },
      ]
    } else {
      chatError.value = result.error || '出错'
    }
  } catch (e) {
    if (e.name !== 'AbortError') chatError.value = e.message || String(e)
  } finally {
    chatStreamText.value = ''
    chatStreaming.value = false
    chatAbort = null
    await nextTick(); scrollChatBottom()
  }
}

async function onChatClear() {
  if (!selectedId.value || !confirm('清空这首歌的对话历史？')) return
  abortChat()
  try {
    await clearChat(selectedId.value)
    chatMessages.value = []
    chatStreamText.value = ''
    chatError.value = ''
  } catch (e) {
    alert(e.message || String(e))
  }
}

function abortChat() {
  if (chatAbort) {
    try { chatAbort.abort() } catch {}
    chatAbort = null
  }
  chatStreaming.value = false
  chatStreamText.value = ''
}

function scrollChatBottom() {
  const el = chatBodyEl.value
  if (el) el.scrollTop = el.scrollHeight
}

// keyboard
function onKeyDown(e) {
  // ESC 在任何输入态下都能关全屏
  if (e.key === 'Escape' && fullscreenSrc.value) {
    e.preventDefault()
    fullscreenSrc.value = null
    return
  }
  const tag = (e.target.tagName || '').toLowerCase()
  if (tag === 'input' || tag === 'textarea') return
  if (e.code === 'Space') { e.preventDefault(); togglePlay() }
  else if (e.code === 'ArrowRight') {
    if (audioEl.value) audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, duration.value)
  }
  else if (e.code === 'ArrowLeft') {
    if (audioEl.value) audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0)
  }
  else if (e.key === 'Tab') {
    e.preventDefault()
    const idx = tabs.value.findIndex(t => t.key === activeTab.value)
    const nx = tabs.value[(idx + 1) % tabs.value.length]
    if (nx) activeTab.value = nx.key
  }
}

// route → selected
watch(() => route.params.id, async (idStr) => {
  const id = idStr ? Number(idStr) : null
  if (id !== selectedId.value) {
    selectedId.value = id
    if (id) await loadPiece(id)
    else selected.value = null
  }
}, { immediate: false })

onMounted(async () => {
  document.addEventListener('keydown', onKeyDown)
  await Promise.all([loadPieces(), loadPlaylists(), loadTags()])
  const id = route.params.id ? Number(route.params.id) : null
  if (id) {
    selectedId.value = id
    await loadPiece(id)
  } else {
    try {
      const last = JSON.parse(localStorage.getItem('music.last') || 'null')
      if (last && pieces.value.find(p => p.id === last.id)) {
        router.replace({ name: 'piece', params: { id: last.id } })
      }
    } catch {}
  }
})

onBeforeUnmount(() => {
  document.removeEventListener('keydown', onKeyDown)
  if (notesTimer) clearTimeout(notesTimer)
  stopChordPoll('chord')
  abortChat()
})
</script>

<style scoped>
.root {
  height: 100%;
  display: flex;
  flex-direction: column;
  background: var(--bg);
}

.topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 20px;
  background: var(--bg-card);
  border-bottom: 1px solid var(--border-soft);
  flex-shrink: 0;
}
.topbar h1 { font-size: 18px; font-weight: 600; white-space: nowrap; }
.topbar .search {
  flex: 1;
  max-width: 380px;
  padding: 8px 14px;
  border-radius: 20px;
  border: 1px solid var(--border);
  background: var(--bg);
  font-size: 14px;
}
.topbar .search:focus { border-color: var(--accent-strong); }
.topbar .count { color: var(--text-mute); font-size: 12px; white-space: nowrap; }
.btn-inspire {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: rgba(192, 132, 252, 0.15);
  color: var(--accent);
  font-size: 16px;
  display: inline-flex; align-items: center; justify-content: center;
}
.btn-inspire:hover { background: rgba(192, 132, 252, 0.3); }

.ins-overlay {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 60px 16px 16px;
  backdrop-filter: blur(2px);
}
.ins-modal {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 12px;
  width: 100%;
  max-width: 640px;
  max-height: calc(100vh - 76px);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 12px 40px rgba(0,0,0,0.5);
}
.ins-head {
  padding: 14px 18px;
  font-size: 16px;
  font-weight: 600;
  border-bottom: 1px solid var(--border-soft);
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--accent);
}
.ins-close {
  font-size: 16px;
  color: var(--text-mute);
  width: 28px; height: 28px;
  border-radius: 50%;
}
.ins-close:hover { background: rgba(255,255,255,0.06); color: var(--text); }

.ins-hint-row {
  display: flex;
  gap: 8px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border-soft);
}
.ins-hint {
  flex: 1;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 12px;
  font-size: 13px;
  color: var(--text);
}
.ins-hint:focus { border-color: var(--accent-strong); outline: none; }
.ins-go {
  background: var(--accent-strong);
  color: #fff;
  padding: 8px 16px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
}
.ins-go:hover:not(:disabled) { background: var(--accent); }

.ins-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px 18px 24px;
  font-size: 14px;
  line-height: 1.7;
  color: var(--text);
}
.ins-body :deep(p) { margin: 0 0 8px; }
.ins-body :deep(b) { color: var(--accent); }
.ins-body :deep(.ins-empty) { color: var(--text-mute); text-align: center; padding: 40px 0; }
.ins-err {
  margin: 0 18px 16px;
  color: var(--accent-red);
  background: rgba(239,68,68,0.08);
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 13px;
}

.topbar .btn-add {
  width: 36px; height: 36px;
  border-radius: 50%;
  background: var(--accent-strong);
  color: #fff;
  font-size: 22px; font-weight: 600;
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none;
}
.topbar .btn-add:hover { background: var(--accent); text-decoration: none; }

.filterbar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 20px;
  background: var(--bg-elev);
  border-bottom: 1px solid var(--border-soft);
  flex-wrap: wrap;
  flex-shrink: 0;
  font-size: 12px;
}
.fb-label {
  color: var(--text-mute);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-size: 10px;
  margin-right: 2px;
}
.fb-sep { color: var(--text-mute); margin: 0 4px; }
.chip {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text-dim);
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 12px;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  transition: all 0.15s;
}
.chip:hover { color: var(--text); }
.chip.active {
  background: var(--bg-active);
  border-color: var(--accent-strong);
  color: var(--accent);
}
.chip-n {
  font-size: 10px;
  color: var(--text-mute);
}
.new-chip {
  border-style: dashed;
  color: var(--text-mute);
}

.main {
  flex: 1;
  display: flex;
  overflow: hidden;
  min-height: 0;
}

.sidebar {
  width: 320px;
  min-width: 260px;
  border-right: 1px solid var(--border-soft);
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
}

.sort-bar {
  display: flex;
  padding: 6px 8px;
  gap: 0;
  border-bottom: 1px solid var(--border-soft);
  flex-wrap: wrap;
}
.sort-bar button {
  font-size: 11px;
  padding: 5px 10px;
  color: var(--text-mute);
  border: 1px solid var(--border);
  background: var(--bg-elev);
  border-right-width: 0;
}
.sort-bar button:first-child { border-radius: 4px 0 0 4px; }
.sort-bar button:last-child { border-radius: 0 4px 4px 0; border-right-width: 1px; }
.sort-bar button:hover { color: var(--text); }
.sort-bar button.active {
  background: var(--bg-active);
  border-color: var(--accent-strong);
  color: var(--accent);
}

.playlist { flex: 1; overflow-y: auto; }
.hint { padding: 40px 20px; text-align: center; color: var(--text-mute); font-size: 14px; }
.hint.err { color: var(--accent-red); }

.row {
  display: flex;
  padding: 10px 14px;
  border-bottom: 1px solid var(--border-soft);
  cursor: pointer;
  align-items: center;
  gap: 10px;
}
.row:hover { background: var(--bg-card); }
.row.active { background: var(--bg-active); }
.row.active .row-title { color: var(--accent); }
.row-main { flex: 1; min-width: 0; }
.row-title {
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row-meta {
  font-size: 11px;
  color: var(--text-mute);
  margin-top: 2px;
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}
.row-meta .cat {
  color: var(--accent-cyan);
  background: rgba(6, 182, 212, 0.1);
  padding: 0 6px;
  border-radius: 8px;
}
.tg {
  color: var(--accent);
  background: rgba(192, 132, 252, 0.1);
  padding: 0 6px;
  border-radius: 8px;
  font-size: 11px;
}
.np-tags { display: flex; gap: 6px; justify-content: center; margin-top: 4px; flex-wrap: wrap; }

.badges {
  display: flex;
  gap: 4px;
  align-items: center;
  flex-shrink: 0;
}
.badge {
  font-size: 10px;
  color: var(--accent-strong);
  background: rgba(124, 92, 191, 0.12);
  padding: 1px 6px;
  border-radius: 8px;
}
.play-count {
  font-size: 10px;
  color: var(--text-mute);
}

.player-area {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-width: 0;
}

.empty {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text-mute);
  font-size: 16px;
}

.now-playing {
  padding: 16px 24px 6px;
  text-align: center;
  flex-shrink: 0;
}
.now-playing h2 {
  font-size: 22px;
  color: var(--accent);
  margin-bottom: 4px;
}
.np-sub {
  color: var(--text-dim);
  font-size: 13px;
  display: flex;
  gap: 6px;
  justify-content: center;
  flex-wrap: wrap;
}
.edit-link {
  margin-left: 6px;
  color: var(--text-mute);
  font-size: 12px;
}
.edit-link:hover { color: var(--accent); }

.tabs {
  display: flex;
  gap: 0;
  padding: 0 24px;
  border-bottom: 1px solid var(--border-soft);
  flex-shrink: 0;
  overflow-x: auto;
}
.tabs button {
  background: none;
  color: var(--text-mute);
  border-bottom: 2px solid transparent;
  padding: 10px 16px;
  font-size: 14px;
  white-space: nowrap;
  transition: all 0.2s;
}
.tabs button:hover { color: var(--text-dim); }
.tabs button.active {
  color: var(--accent);
  border-bottom-color: var(--accent);
}
.tabs .tab-n {
  font-size: 10px;
  color: var(--text-mute);
  margin-left: 4px;
}

.content {
  flex: 1;
  overflow-y: auto;
  padding: 12px 24px 80px;
  min-height: 0;
}

.lyrics-box .lyrics-line {
  padding: 8px 0;
  font-size: 16px;
  text-align: center;
  color: var(--text-mute);
  line-height: 1.6;
  cursor: pointer;
  transition: color 0.3s, font-size 0.3s;
}
.lyrics-box .lyrics-line.active {
  color: var(--text);
  font-size: 19px;
  font-weight: 500;
}
.lyrics-none {
  text-align: center;
  color: var(--text-mute);
  margin-top: 60px;
  font-size: 14px;
}
.lyrics-raw {
  margin-top: 20px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  text-align: left;
  white-space: pre-wrap;
  word-break: break-all;
  font-size: 12px;
  background: var(--bg-elev);
  padding: 12px;
  border-radius: 6px;
}

.sheet-box {
  display: flex;
  flex-direction: column;
  gap: 16px;
  align-items: center;
}
.sheet-img {
  max-width: 100%;
  border-radius: 8px;
  background: #fff;
  cursor: zoom-in;
  transition: transform 0.1s;
}
.sheet-img:hover { transform: scale(1.005); }

.fs-overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.96);
  overflow: auto;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  padding: 16px;
  cursor: zoom-out;
}
.fs-img {
  max-width: 100%;
  height: auto;
  display: block;
  background: #fff;
  border-radius: 4px;
}

.auto-fetch {
  margin-top: 40px;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}
.auto-fetch .hint-line {
  color: var(--text-mute);
  font-size: 14px;
  line-height: 1.6;
}
.auto-fetch .hint-line b { color: var(--accent); }
.auto-fetch .hint-line.err { color: var(--accent-red); }
.btn-fetch {
  background: var(--accent-strong);
  color: #fff;
  padding: 12px 22px;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 600;
}
.btn-fetch:hover:not(:disabled) { background: var(--accent); }
.btn-fetch .spin { display: inline-block; animation: spin-anim 1.5s linear infinite; }
@keyframes spin-anim { to { transform: rotate(360deg); } }

.pdf-box { display: flex; flex-direction: column; gap: 16px; }
.pdf-frame {
  width: 100%;
  height: 90vh;
  border: none;
  background: #fff;
  border-radius: 6px;
}

.video-box { display: flex; flex-direction: column; gap: 16px; align-items: center; }
.video-el {
  max-width: 100%;
  width: 100%;
  max-height: 60vh;
  background: #000;
  border-radius: 8px;
}

.notes-box {
  display: flex;
  flex-direction: column;
  height: 100%;
  gap: 8px;
}
.notes-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 12px;
  color: var(--text-mute);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.notes-head .saved { color: var(--accent-green); }
.notes-area {
  flex: 1;
  min-height: 240px;
  background: var(--bg-elev);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 14px 16px;
  font-size: 14px;
  line-height: 1.6;
  resize: vertical;
}
.notes-area:focus { border-color: var(--accent-strong); }

.controls {
  flex-shrink: 0;
  background: var(--bg-card);
  padding: 12px 20px;
  border-top: 1px solid var(--border);
}
.ctrl-row {
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: 900px;
  margin: 0 auto;
}
.btn-icon {
  font-size: 22px;
  color: var(--text-dim);
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.btn-icon:hover { background: rgba(255,255,255,0.06); color: var(--text); }
.btn-icon.big {
  font-size: 26px;
  background: var(--accent-strong);
  color: #fff;
}
.btn-icon.big:hover { background: var(--accent); color: #fff; }
.repeat {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
  color: var(--text-mute);
  font-size: 11px;
}
.repeat .track {
  width: 30px; height: 16px;
  border-radius: 8px;
  background: var(--border);
  position: relative;
  transition: background 0.2s;
}
.repeat .thumb {
  position: absolute;
  top: 2px; left: 2px;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--text-mute);
  transition: transform 0.2s, background 0.2s;
}
.repeat.on .track { background: var(--accent-strong); }
.repeat.on .thumb { transform: translateX(14px); background: #fff; }

.bar {
  flex: 1;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  cursor: pointer;
  position: relative;
}
.fill {
  height: 100%;
  background: var(--accent-strong);
  border-radius: 2px;
  pointer-events: none;
}
.loop-range {
  position: absolute;
  top: 0;
  height: 100%;
  background: rgba(74, 222, 128, 0.35);
  border-radius: 2px;
  pointer-events: none;
}
.loop-mark {
  position: absolute;
  top: -8px;
  transform: translateX(-50%);
  font-size: 9px;
  color: var(--accent-green);
  background: rgba(74, 222, 128, 0.2);
  padding: 1px 4px;
  border-radius: 3px;
  pointer-events: none;
  font-weight: 700;
}

.ab-btn {
  font-size: 11px;
  padding: 4px 8px;
  border-radius: 4px;
  background: var(--bg-elev);
  color: var(--text-mute);
  border: 1px solid var(--border);
  min-width: 28px;
  height: 28px;
}
.ab-btn:hover:not(:disabled) { color: var(--text); }
.ab-btn.on {
  background: rgba(74, 222, 128, 0.18);
  color: var(--accent-green);
  border-color: var(--accent-green);
}
.ab-btn.clear { color: var(--accent-red); border-color: rgba(239,68,68,0.4); }
.rate-btn {
  font-size: 12px;
  padding: 4px 10px;
  border-radius: 14px;
  background: var(--bg-elev);
  color: var(--text-dim);
  border: 1px solid var(--border);
  min-width: 44px;
  height: 28px;
  font-weight: 600;
}
.rate-btn:hover { color: var(--accent); }
.time {
  font-size: 12px;
  color: var(--text-dim);
  min-width: 40px;
  text-align: center;
}

.vol-icon { font-size: 16px; width: 32px; height: 32px; }
.vol-slider {
  -webkit-appearance: none;
  appearance: none;
  width: 90px;
  height: 4px;
  background: var(--border);
  border-radius: 2px;
  cursor: pointer;
}
.vol-slider:focus { outline: none; }
.vol-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  background: var(--accent-strong);
  border-radius: 50%;
  cursor: pointer;
}
.vol-slider::-moz-range-thumb {
  width: 12px;
  height: 12px;
  background: var(--accent-strong);
  border: none;
  border-radius: 50%;
  cursor: pointer;
}
@media (max-width: 768px) {
  .vol-slider { width: 60px; }
}

/* Chat sidebar */
.chat {
  width: 320px;
  border-left: 1px solid var(--border-soft);
  background: var(--bg-elev);
  display: none;
  flex-direction: column;
  flex-shrink: 0;
}
@media (min-width: 1280px) {
  .chat { display: flex; }
}
.chat-head {
  padding: 10px 14px;
  font-size: 12px;
  color: var(--text-mute);
  border-bottom: 1px solid var(--border-soft);
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.chat-clear {
  background: rgba(239, 68, 68, 0.1);
  color: var(--accent-red);
  font-size: 11px;
  padding: 3px 8px;
  border-radius: 4px;
}
.chat-clear:hover { background: rgba(239, 68, 68, 0.2); }

.chat-body {
  flex: 1;
  overflow-y: auto;
  padding: 12px 12px 8px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.chat-empty {
  color: var(--text-mute);
  font-size: 13px;
  text-align: center;
  padding: 30px 8px;
  line-height: 1.7;
}
.chat-err {
  color: var(--accent-red);
  font-size: 12px;
  background: rgba(239, 68, 68, 0.08);
  padding: 8px 10px;
  border-radius: 6px;
}
.msg {
  display: flex;
}
.msg.user { justify-content: flex-end; }
.msg.assistant { justify-content: flex-start; }
.msg-bubble {
  max-width: 84%;
  padding: 8px 12px;
  border-radius: 12px;
  font-size: 13px;
  line-height: 1.55;
  white-space: pre-wrap;
  word-break: break-word;
}
.msg.user .msg-bubble {
  background: var(--accent-strong);
  color: #fff;
  border-bottom-right-radius: 3px;
}
.msg.assistant .msg-bubble {
  background: var(--bg-card);
  color: var(--text);
  border-bottom-left-radius: 3px;
}

.chat-input {
  border-top: 1px solid var(--border-soft);
  padding: 10px;
  display: flex;
  gap: 8px;
  align-items: end;
}
.chat-input textarea {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 13px;
  line-height: 1.5;
  resize: none;
  color: var(--text);
}
.chat-input textarea:focus { border-color: var(--accent-strong); outline: none; }
.chat-send {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--accent-strong);
  color: #fff;
  font-size: 16px;
  font-weight: 600;
  flex-shrink: 0;
}
.chat-send:hover:not(:disabled) { background: var(--accent); }

@media (max-width: 768px) {
  .main { flex-direction: column; }
  .sidebar { width: 100%; height: 38vh; min-height: 200px; border-right: none; border-bottom: 1px solid var(--border-soft); }
  .sidebar.has-selected { height: 30vh; }
}
</style>
