<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { init, dispose } from 'klinecharts'
import type { KlineResolution } from '@/stores/futures'
import { useSpotStore, apiSymbolFromRouteParam, formatSpotSymbolForDisplay } from '@/stores/spot'
import { useCoinsStore } from '@/stores/coins'
import { useAuthStore } from '@/stores/auth'
import { useSpotMarketWs as useMarketWs, BAR_MS, BAR_COUNT, type WsTradeItem } from '@/composables/useSpotMarketWs'
import { useToast } from '@/composables/useToast'
import { useSpotUserPush } from '@/composables/useSpotUserPush'
import { useSpotI18n } from '@/composables/useSpotI18n'
import { useI18n } from 'vue-i18n'
import { buildSpotOrderBookDisplay, spotPrecisionLabel } from '@/utils/spotDepthBook'
import { get, post } from '@/utils/request'
import { useSpotTransferCoin } from '@/composables/useSpotTransferCoin'

// ── Stores ────────────────────────────────────────────
const route = useRoute()
const router = useRouter()
const spotStore = useSpotStore()
const coinsStore = useCoinsStore()
const authStore = useAuthStore()
const toast = useToast()
const { t } = useI18n()
const {
  sideLabel,
  orderTypeLabel,
  orderStatusFromOrder,
  recordTypeLabel,
  marketPrice,
} = useSpotI18n()

const {
  symbolList, symbolsLoading, symbolsError, currentOrders, assets,
  historyOrders, historyOrderNoMore,
  historyDeals, historyDealNoMore,
  accountRecords, accountRecordNoMore,
} = storeToRefs(spotStore)
const { isLoggedIn, user } = storeToRefs(authStore)

// ── Route symbol ──────────────────────────────────────
const routeSymbol = computed(() => String(route.params.symbol ?? 'BTC-USDT'))
const apiSymbol   = computed(() => apiSymbolFromRouteParam(routeSymbol.value))
const symInfo     = computed(() => spotStore.symbolMap.get(apiSymbol.value) ?? null)
const symBase     = computed(() => symInfo.value?.base ?? apiSymbol.value.replace(/USDT$/, ''))
const displaySymbol = computed(() => {
  const info = symInfo.value
  if (info) {
    return `${info.base}/${info.quote}`
  }
  return formatSpotSymbolForDisplay(apiSymbol.value)
})

const currentCoinIcon = computed(() => {
  const fromPair = symInfo.value?.icon?.trim()
  if (fromPair) {
    return fromPair
  }
  return spotStore.coinIconMap.get(symBase.value.toUpperCase()) ?? ''
})

// ── UI state ──────────────────────────────────────────
const showPairSelector = ref(false)
const pairSearch       = ref('')
const isMobile         = ref(typeof window !== 'undefined' && window.innerWidth <= 768)
const mobileTab        = ref<'chart' | 'trade'>('chart')

const subNavTab     = ref<'chart' | 'coin-info'>('chart')
const bookTab       = ref<'orderbook' | 'trades'>('orderbook')
const bookLayout    = ref<'full' | 'asks' | 'bids'>('full')
/** 0=最少合并粒度 1=中 2=最细（与 App _depthStep 一致） */
const depthStep     = ref<0 | 1 | 2>(2)
const bookColRef    = ref<HTMLElement | null>(null)
const bookTabsRef   = ref<HTMLElement | null>(null)
const bookCtrlRef   = ref<HTMLElement | null>(null)
const bookHeadRef   = ref<HTMLElement | null>(null)
const bookMidRef    = ref<HTMLElement | null>(null)
const bsRatioRef    = ref<HTMLElement | null>(null)
const dynamicBookRows = ref(13)
const activeTab     = ref<'orders' | 'history-orders' | 'trades' | 'assets'>('orders')
const activeDrawTool = ref('cursor')
const chartWrapWidth = ref(0)
const coinExtInfo   = ref<any>(null)
const coinExtLoading = ref(false)
const tabScrollRef  = ref<HTMLElement | null>(null)

const filteredSymbols = computed(() => {
  const q = pairSearch.value.trim().toLowerCase()
  if (!q) return symbolList.value
  return symbolList.value.filter(s => {
    const base = s.base.toLowerCase()
    const sym = s.symbol.toLowerCase()
    const quote = s.quote.toLowerCase()
    const slash = `${base}/${quote}`
    return base.includes(q) || sym.includes(q) || quote.includes(q) || slash.includes(q)
  })
})

function selectPair(sym: string) {
  const s = symbolList.value.find(x => x.symbol === sym)
  if (!s) return
  pairSearch.value = ''
  showPairSelector.value = false
  router.push({ name: 'spot', params: { symbol: `${s.base}-${s.quote}` } })
}

// ── Ticker ────────────────────────────────────────────
const close   = ref('--')
const open24  = ref('0')
const high24  = ref('--')
const low24   = ref('--')
const vol24   = ref('--')
const priceUp = ref(true)

const change24Pct = computed(() => {
  const c = parseFloat(close.value)
  const o = parseFloat(open24.value)
  if (!o || !c) return '--'
  const pct = (c - o) / o * 100
  return (pct >= 0 ? '+' : '') + pct.toFixed(2) + '%'
})

function fmtVol(v: string) {
  const n = parseFloat(v)
  if (isNaN(n)) return '--'
  if (n >= 1e9) return (n / 1e9).toFixed(2) + 'B'
  if (n >= 1e6) return (n / 1e6).toFixed(2) + 'M'
  if (n >= 1e3) return (n / 1e3).toFixed(2) + 'K'
  return n.toFixed(2)
}

// ── Order book ────────────────────────────────────────
interface BookRow { price: string; amount: string; total: string }
const rawBookAsks = ref<[number, number][]>([])
const rawBookBids = ref<[number, number][]>([])
const recentTrades = ref<WsTradeItem[]>([])
const klineRealtimeClose = ref<number | null>(null)

const bookMode = computed(() => {
  if (bookLayout.value === 'asks') return 1 as const
  if (bookLayout.value === 'bids') return 2 as const
  return 0 as const
})
function recalcBookRowCount() {
  const col = bookColRef.value
  if (!col) return
  const rowEl = col.querySelector('.book-row') as HTMLElement | null
  const rowH = Math.max(1, rowEl?.offsetHeight || 22)
  const tabsH = bookTabsRef.value?.offsetHeight || 40
  const ctrlH = bookCtrlRef.value?.offsetHeight || 34
  const headH = bookHeadRef.value?.offsetHeight || 24
  const ratioH = bsRatioRef.value?.offsetHeight || 28
  const midH = bookLayout.value === 'full' ? (bookMidRef.value?.offsetHeight || 32) : 0
  // 盘口行本身带有 padding，按“尽量填满”策略给出半行缓冲，减少底部空白
  const usable = col.clientHeight - tabsH - ctrlH - headH - ratioH - midH
  if (usable <= 0) return
  const rows = Math.floor((usable + rowH * 0.5) / rowH)
  dynamicBookRows.value = Math.min(40, Math.max(4, rows))
}

// 根据面板高度动态计算展示条数，避免固定条数导致末档被裁掉
const bookRowCount = computed(() => {
  const rows = dynamicBookRows.value
  // 双向盘口也尽量填满：按上取整分配到单侧
  return bookLayout.value === 'full' ? Math.max(1, Math.ceil(rows / 2)) : rows
})
const depthPrecision = computed(() => {
  const s = symInfo.value
  if (!s) return 2
  if (depthStep.value === 0) return s.depth0Pre
  if (depthStep.value === 1) return s.depth1Pre
  return s.depth2Pre
})
const displayBook = computed(() =>
  buildSpotOrderBookDisplay(
    rawBookAsks.value,
    rawBookBids.value,
    depthPrecision.value,
    bookMode.value,
    bookRowCount.value,
  ),
)
const displayBookAsks = computed<BookRow[]>(() => displayBook.value.askRows)
const displayBookBids = computed<BookRow[]>(() => displayBook.value.bidRows)
const bookMidPrice = computed(() => {
  if (klineRealtimeClose.value !== null && Number.isFinite(klineRealtimeClose.value)) {
    return fmtPrice(klineRealtimeClose.value)
  }
  return close.value !== '--' ? fmtPrice(close.value) : '--'
})

const bsRatio = computed(() => {
  const b = displayBookBids.value.reduce((s, r) => s + parseFloat(r.amount), 0)
  const a = displayBookAsks.value.reduce((s, r) => s + parseFloat(r.amount), 0)
  const t = b + a
  if (!t) return { buy: 50, sell: 50 }
  return { buy: Math.round(b / t * 100), sell: Math.round(a / t * 100) }
})

function barWidth(items: BookRow[], total: string) {
  const maxTotal = Math.max(...items.map(i => parseFloat(i.total) || 0))
  if (!maxTotal) return '0%'
  return Math.min((parseFloat(total) / maxTotal) * 100, 100) + '%'
}

// ── Trade panel ───────────────────────────────────────
const sideBuy   = ref(true)
const limitMode = ref(true)
const priceIn   = ref('')
const amountIn  = ref('')
const orderBusy = ref(false)
const orderErr  = ref('')
/** 有效单位：买入→quote(USDT)；卖出→base(币数量) */
const effectiveAmountUnit = computed<'base' | 'quote'>(() =>
  sideBuy.value ? 'quote' : 'base'
)
const pricePrecision = computed(() => symInfo.value?.pricePrecision ?? 2)
const baseAmountPrecision = computed(() => symInfo.value?.volumePrecision ?? 4)
// 对齐 flutter：价格精度取 pricePre；数量/金额输入精度统一取 volumePre（随币对切换）
const quoteAmountPrecision = computed(() => symInfo.value?.volumePrecision ?? 4)

const baseBalance = computed(() => spotStore.balanceForCoin(symBase.value))
const usdtBalance = computed(() => spotStore.balanceForCoin('USDT'))

watch(sideBuy, () => { amountIn.value = ''; orderErr.value = '' })
watch(limitMode, () => { amountIn.value = ''; orderErr.value = '' })
watch([pricePrecision, baseAmountPrecision, quoteAmountPrecision, effectiveAmountUnit], () => {
  if (priceIn.value) priceIn.value = sanitizeDecimalInput(priceIn.value, pricePrecision.value)
  if (amountIn.value) {
    const dp = effectiveAmountUnit.value === 'quote' ? quoteAmountPrecision.value : baseAmountPrecision.value
    amountIn.value = sanitizeDecimalInput(amountIn.value, dp)
  }
})
// 兜底：无论输入来源（手输/粘贴/程序回填），都强制按当前精度约束
watch(priceIn, (v) => {
  const next = sanitizeDecimalInput(v, pricePrecision.value)
  if (next !== v) priceIn.value = next
})
watch(amountIn, (v) => {
  const dp = effectiveAmountUnit.value === 'quote' ? quoteAmountPrecision.value : baseAmountPrecision.value
  const next = sanitizeDecimalInput(v, dp)
  if (next !== v) amountIn.value = next
})

function pctAmount(pct: number) {
  if (sideBuy.value) {
    // 买入：始终以 USDT 金额计算
    amountIn.value = trimDp(usdtBalance.value * pct / 100, quoteAmountPrecision.value)
  } else {
    // 卖出：始终以 base 数量计算
    amountIn.value = trimDp(baseBalance.value * pct / 100, baseAmountPrecision.value)
  }
}

function trimDp(n: number, dp: number) {
  return (Math.floor(n * 10 ** dp) / 10 ** dp).toFixed(dp)
}

function sanitizeDecimalInput(raw: string, decimals: number): string {
  const clean = raw.replace(/[^\d.]/g, '')
  if (!clean) return ''
  const firstDot = clean.indexOf('.')
  if (firstDot < 0) return clean
  const intPart = clean.slice(0, firstDot)
  const fracRaw = clean.slice(firstDot + 1).replace(/\./g, '')
  if (decimals <= 0) return intPart
  return `${intPart}.${fracRaw.slice(0, decimals)}`
}

function onPriceInput() {
  const next = sanitizeDecimalInput(priceIn.value, pricePrecision.value)
  if (next !== priceIn.value) priceIn.value = next
  orderErr.value = ''
}

function onAmountInput() {
  const dp = effectiveAmountUnit.value === 'quote'
    ? quoteAmountPrecision.value
    : baseAmountPrecision.value
  const next = sanitizeDecimalInput(amountIn.value, dp)
  if (next !== amountIn.value) amountIn.value = next
  orderErr.value = ''
}

function fmtBal(n: number, dp = 4) {
  return n.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: dp })
}

// 显示可用余额时使用截断（不四舍五入），避免 0 被显示成 0.000001
function fmtBalTrunc(n: number, dp = 4) {
  if (!Number.isFinite(n)) return '0'
  const factor = 10 ** dp
  const truncated = Math.trunc(n * factor) / factor
  return truncated.toLocaleString('en-US', {
    minimumFractionDigits: 0,
    maximumFractionDigits: dp,
  })
}

async function submitOrder() {
  orderErr.value = ''
  const info = symInfo.value
  if (!info) { toast.error(t('spot.errors.symbolNotReady')); return }
  if (!isLoggedIn.value) {
    router.push({ name: 'login', query: { redirect: route.fullPath } })
    return
  }
  const isBuy = sideBuy.value
  const isLimit = limitMode.value
  const pxText = sanitizeDecimalInput(priceIn.value, pricePrecision.value)
  const amtText = sanitizeDecimalInput(
    amountIn.value,
    effectiveAmountUnit.value === 'quote' ? quoteAmountPrecision.value : baseAmountPrecision.value,
  )
  if (pxText !== priceIn.value) priceIn.value = pxText
  if (amtText !== amountIn.value) amountIn.value = amtText
  const px = parseFloat(pxText)
  const amt = parseFloat(amtText)
  if (!amt || amt <= 0) { orderErr.value = t('spot.errors.invalidAmount'); return }
  if (isLimit && (!px || px <= 0)) { orderErr.value = t('spot.errors.invalidPrice'); return }

  let volume = amt
  const pxPre = pricePrecision.value
  let price = isLimit ? Math.floor(px * 10 ** pxPre) / 10 ** pxPre : 0

  if (isLimit) {
    // 买入：用户输入 USDT → 换算为 base 数量；卖出：用户直接输入 base 数量
    const baseAmt = isBuy ? amt / px : amt
    volume = Math.floor(baseAmt * 10 ** baseAmountPrecision.value) / 10 ** baseAmountPrecision.value
    if (volume < info.minLimitVolume && info.minLimitVolume > 0) {
      orderErr.value = t('spot.errors.minLimitVolume', { min: info.minLimitVolume }); return
    }
  } else if (isBuy) {
    volume = Math.floor(amt * 10 ** quoteAmountPrecision.value) / 10 ** quoteAmountPrecision.value
    if (info.minMarketBuy > 0 && volume < info.minMarketBuy) {
      orderErr.value = t('spot.errors.minMarketBuy', { min: info.minMarketBuy }); return
    }
  } else {
    volume = Math.floor(amt * 10 ** baseAmountPrecision.value) / 10 ** baseAmountPrecision.value
    if (info.minMarketSell > 0 && volume < info.minMarketSell) {
      orderErr.value = t('spot.errors.minMarketSell', { min: info.minMarketSell }); return
    }
  }

  orderBusy.value = true
  try {
    await spotStore.createOrder({
      symbol: apiSymbol.value,
      side: isBuy ? 'BUY' : 'SELL',
      type: isLimit ? 1 : 2,
      price,
      volume,
    })
    toast.success(t('spot.errors.orderSubmitted'))
    amountIn.value = ''
    await spotStore.fetchAssets()
    await spotStore.fetchCurrentOrders(apiSymbol.value)
  } catch (e) {
    orderErr.value = e instanceof Error ? e.message : t('spot.errors.orderFailed')
  } finally {
    orderBusy.value = false
  }
}

async function cancelOrder(id: string) {
  try {
    await spotStore.cancelOrder(id)
    toast.success(t('spot.errors.cancelSuccess'))
    spotStore.fetchCurrentOrders(apiSymbol.value)
  } catch (e) {
    toast.error(e instanceof Error ? e.message : t('spot.errors.cancelFailed'))
  }
}

const cancelAllBusy = ref(false)
async function cancelAllOrders() {
  if (cancelAllBusy.value) return
  cancelAllBusy.value = true
  try {
    await spotStore.cancelAllOrders(apiSymbol.value)
    toast.success(t('spot.errors.cancelAllSuccess'))
  } catch {
    toast.error(t('spot.errors.cancelAllPartialFailed'))
  } finally {
    cancelAllBusy.value = false
  }
}

// ── 现货划转弹窗 ──────────────────────────────────────
// direction 1 = 资金账户→现货账户；2 = 现货账户→资金账户
const showTransfer   = ref(false)
const tfDirection    = ref<1 | 2>(1)
const tfAmount       = ref('')
const tfSubmitting   = ref(false)
const tfError        = ref('')

const {
  symbol: tfSymbol,
  coinOptions: tfCoinOptions,
  assetsLoading: tfLoading,
  loadAssets: loadTfAssets,
  availableFor: tfAvailableFor,
} = useSpotTransferCoin('USDT')

const tfAvail = computed(() => {
  if (tfDirection.value === 1) {
    return tfAvailableFor('SPOT', tfSymbol.value)
  }
  return tfAvailableFor('SPOT_TRADING', tfSymbol.value)
})

const tfFromLabel = computed(() =>
  tfDirection.value === 1 ? t('assets.fundAccount') : t('assets.spotAccount'))
const tfToLabel = computed(() =>
  tfDirection.value === 1 ? t('assets.spotAccount') : t('assets.fundAccount'))

function openTransfer(dir: 1 | 2 = 1) {
  tfDirection.value = dir
  tfAmount.value = ''
  tfError.value = ''
  showTransfer.value = true
  void loadTfAssets()
}

function swapTfDirection() {
  tfDirection.value = tfDirection.value === 1 ? 2 : 1
  tfAmount.value = ''
}

watch(tfSymbol, () => {
  tfAmount.value = ''
})

watch(tfDirection, () => {
  tfAmount.value = ''
})

async function confirmTransfer() {
  const amt = parseFloat(tfAmount.value)
  if (!tfAmount.value || isNaN(amt) || amt <= 0) {
    tfError.value = t('spot.transferModal.invalidAmount'); return
  }
  if (amt > parseFloat(tfAvail.value || '0')) {
    tfError.value = t('spot.transferModal.insufficientBalance'); return
  }
  tfSubmitting.value = true
  tfError.value = ''
  try {
    await post('/spot/v1/account/transfer', { symbol: tfSymbol.value, amount: amt, direction: tfDirection.value })
    showTransfer.value = false
    tfAmount.value = ''
    await spotStore.fetchAssets()
  } catch (e: any) {
    tfError.value = e?.message ?? t('spot.transferModal.transferFailed')
  } finally {
    tfSubmitting.value = false
  }
}

// ── K-line chart ──────────────────────────────────────
const chartEl = ref<HTMLDivElement | null>(null)
let chart: ReturnType<typeof init> | null = null
let ro: ResizeObserver | null = null
let loadMoreCallback: ((bars: any[], more: boolean) => void) | null = null
let klineBarCallback: ((bar: any) => void) | null = null
let currentBars: any[] = []
let bookRo: ResizeObserver | null = null

const klineResolution = ref<KlineResolution>('15')
const showMA  = ref(true)
const showVol = ref(true)
const crosshairData = ref<any>(null)

const tfOptions = computed((): { label: string; value: KlineResolution }[] => [
  { label: t('spot.tf.m1'), value: '1' },
  { label: t('spot.tf.m5'), value: '5' },
  { label: t('spot.tf.m15'), value: '15' },
  { label: t('spot.tf.m30'), value: '30' },
  { label: t('spot.tf.h1'), value: '60' },
  { label: t('spot.tf.h4'), value: '240' },
  { label: t('spot.tf.d1'), value: '1D' },
  { label: t('spot.tf.w1'), value: '1W' },
  { label: t('spot.tf.mo1'), value: '1M' },
  { label: t('spot.tf.mo3'), value: '3M' },
])

const DRAW_TOOLS: Record<string, string> = {
  trend:   'segment',
  hline:   'horizontalStraightLine',
  ray:     'rayLine',
  channel: 'priceChannelLine',
  fib:     'fibonacciLine',
  text:    'simpleAnnotation',
}

const RESOLUTION_PERIOD: Record<string, { type: 'minute' | 'hour' | 'day'; span: number }> = {
  '1':   { type: 'minute', span: 1  },
  '5':   { type: 'minute', span: 5  },
  '15':  { type: 'minute', span: 15 },
  '30':  { type: 'minute', span: 30 },
  '60':  { type: 'hour',   span: 1  },
  '240': { type: 'hour',   span: 4  },
  '1D':  { type: 'day',    span: 1  },
}

const tooltipStyle = computed(() => {
  const x = crosshairData.value?.x ?? 0
  if (x > chartWrapWidth.value / 2) return { left: '4px', right: 'auto' }
  return { left: 'auto', right: '4px' }
})

const crosshairMa = computed(() => {
  const kd = crosshairData.value?.kLineData
  if (!kd || !currentBars.length) return null
  const idx = currentBars.findIndex((b: any) => b.timestamp === kd.timestamp)
  if (idx < 0) return null
  const calcMa = (period: number): string | null => {
    if (idx < period - 1) return null
    const sum = currentBars.slice(idx - period + 1, idx + 1).reduce((s: number, b: any) => s + b.close, 0)
    return (sum / period).toFixed(symInfo.value?.pricePrecision ?? 2)
  }
  return { ma5: calcMa(5), ma10: calcMa(10), ma30: calcMa(30) }
})

function destroyChart() {
  ro?.disconnect(); ro = null
  if (chartEl.value) dispose(chartEl.value)
  chart = null; currentBars = []; klineBarCallback = null; loadMoreCallback = null
}

function initChart() {
  if (!chartEl.value) return
  chart = init(chartEl.value, {
    locale: 'zh-CN',
    timezone: 'Asia/Shanghai',
    styles: {
      grid: {
        horizontal: { color: 'rgba(255,255,255,0.04)', size: 1 },
        vertical:   { color: 'rgba(255,255,255,0.04)', size: 1 },
      },
      candle: {
        type: 'candle_solid',
        bar: {
          upColor: '#02c076', downColor: '#f6465d', noChangeColor: '#888',
          upBorderColor: '#02c076', downBorderColor: '#f6465d', noChangeBorderColor: '#888',
          upWickColor: '#02c076', downWickColor: '#f6465d', noChangeWickColor: '#888',
        },
        priceMark: {
          high: { show: false }, low: { show: false },
          last: {
            show: true,
            upColor: '#02c076', downColor: '#f6465d', noChangeColor: '#888',
            line: { show: true, style: 'dashed', dashedValue: [4, 4], size: 1 },
            text: { show: true, size: 11 },
          },
        },
        tooltip: { showRule: 'none', showType: 'standard' },
      },
      crosshair: {
        show: true,
        horizontal: {
          show: true,
          line: { show: true, style: 'dashed', dashedValue: [4, 2], size: 1, color: 'rgba(255,255,255,0.2)' },
          text: { show: true, size: 11, color: '#d1d1d2', backgroundColor: '#1d1d1d' },
        },
        vertical: {
          show: true,
          line: { show: true, style: 'dashed', dashedValue: [4, 2], size: 1, color: 'rgba(255,255,255,0.2)' },
          text: { show: true, size: 11, color: '#d1d1d2', backgroundColor: '#1d1d1d' },
        },
      },
      xAxis: {
        axisLine: { show: true, color: 'rgba(255,255,255,0.07)', size: 1 },
        tickText: { show: true, color: '#6a7182', size: 11 },
        tickLine: { show: true, color: 'rgba(255,255,255,0.07)', size: 1, length: 3 },
      },
      yAxis: {
        axisLine: { show: true, color: 'rgba(255,255,255,0.07)', size: 1 },
        tickText: { show: true, color: '#6a7182', size: 11 },
        tickLine: { show: true, color: 'rgba(255,255,255,0.07)', size: 1, length: 3 },
      },
      separator: { color: '#2a2a2a', size: 3 },
      indicator: { tooltip: { showRule: 'none' } },
      overlay: {
        point: {
          color: '#f0b90b', borderColor: 'rgba(240,185,11,0.35)',
          activeColor: '#f0b90b', activeBorderColor: 'rgba(240,185,11,0.6)',
          borderSize: 1, radius: 4, activeBorderSize: 2, activeRadius: 5,
        },
        line: { color: '#f0b90b', size: 1, style: 'solid' },
        text: { color: '#f0b90b', size: 12 },
      },
    },
  })
  if (!chart) return

  if (showMA.value) {
    chart.createIndicator({
      name: 'MA', calcParams: [5, 10, 30],
      styles: { lines: [{ color: '#f0b90b', size: 1 }, { color: '#5d9cf5', size: 1 }, { color: '#e84d5b', size: 1 }] },
    }, { isStack: true, pane: { id: 'candle_pane' } } as any)
  }
  if (showVol.value) {
    chart.createIndicator(
      { name: 'VOL', calcParams: [], styles: {
        bars: [{ style: 'fill' }, { style: 'fill' }],
        lines: [{ size: 0, color: 'rgba(0,0,0,0)' }, { size: 0, color: 'rgba(0,0,0,0)' }, { size: 0, color: 'rgba(0,0,0,0)' }],
        lastValueMark: { show: false },
        tooltip: { showRule: 'none' },
      }},
      { isStack: false, pane: { id: 'vol-pane', height: 80, minHeight: 40 } } as any,
    )
  }

  const dp = symInfo.value?.pricePrecision ?? 2
  chart.setSymbol({ name: apiSymbol.value, pricePrecision: dp, volumePrecision: 4 })
  chart.setPeriod(RESOLUTION_PERIOD[klineResolution.value] ?? { type: 'hour', span: 1 })

  chart.setDataLoader({
    getBars: (params: any) => {
      if (params.type === 'init') {
        params.callback(currentBars, { backward: false, forward: true })
        return
      }
      if (params.type === 'backward') { params.callback([], false); return }
      if (!params.timestamp) { params.callback([], false); return }
      const to = params.timestamp as number
      const from = to - (BAR_MS[klineResolution.value] ?? BAR_MS['60']) * BAR_COUNT
      loadMoreCallback = params.callback
      ws.requestKlinesRange(from, to)
    },
    subscribeBar:   (params: any) => { klineBarCallback = params.callback },
    unsubscribeBar: () => { klineBarCallback = null },
  })

  chart.subscribeAction('onCrosshairChange', (data: any) => {
    if (!data?.x || !data?.paneId) { crosshairData.value = null; return }
    const pts = (chart as any).convertFromPixel([{ x: data.x, y: data.y ?? 0 }])
    const ts: number | undefined = Array.isArray(pts) ? pts[0]?.timestamp : (pts as any)?.timestamp
    if (!ts) { crosshairData.value = null; return }
    const dataList: any[] = (chart as any).getDataList?.() ?? []
    const kLineData = dataList.find((b: any) => b.timestamp === ts)
    crosshairData.value = kLineData ? { ...data, kLineData } : null
  })

  ro = new ResizeObserver(() => {
    chart?.resize()
    chartWrapWidth.value = chartEl.value?.offsetWidth ?? 0
  })
  ro.observe(chartEl.value!)
  chartWrapWidth.value = chartEl.value.offsetWidth
  chart.setMaxOffsetRightDistance(20)
}

// ── Drawing tools ─────────────────────────────────────
function activateDrawTool(tool: string) {
  activeDrawTool.value = tool
  if (!chart || tool === 'cursor') return
  const name = DRAW_TOOLS[tool]
  if (name) chart.createOverlay({ name })
}
function clearOverlays() { chart?.removeOverlay() }
function zoomOut() { (chart as any)?.zoomAtCoordinate?.(0.8) }

// ── MA / VOL toggle ───────────────────────────────────
watch(showMA, (show) => {
  if (!chart) return
  if (show) {
    chart.createIndicator({ name: 'MA', calcParams: [5, 10, 30], styles: { lines: [{ color: '#f0b90b', size: 1 }, { color: '#5d9cf5', size: 1 }, { color: '#e84d5b', size: 1 }] } }, { isStack: true, pane: { id: 'candle_pane' } } as any)
  } else {
    chart.removeIndicator({ name: 'MA', paneId: 'candle_pane' })
  }
})
watch(showVol, (show) => {
  if (!chart) return
  if (show) {
    chart.createIndicator(
      { name: 'VOL', calcParams: [], styles: {
        bars: [{ style: 'fill' }, { style: 'fill' }],
        lines: [{ size: 0, color: 'rgba(0,0,0,0)' }, { size: 0, color: 'rgba(0,0,0,0)' }, { size: 0, color: 'rgba(0,0,0,0)' }],
        lastValueMark: { show: false }, tooltip: { showRule: 'none' },
      }},
      { isStack: false, pane: { id: 'vol-pane', height: 80, minHeight: 40 } } as any,
    )
  } else {
    chart.removeIndicator({ name: 'VOL', paneId: 'vol-pane' })
  }
})

// ── Coin ext info ─────────────────────────────────────
async function fetchCoinExtInfo() {
  coinExtLoading.value = true
  try {
    const raw = await get<any>('/contract/coin-ext/detail', { symbol: symBase.value })
    if (raw && typeof raw === 'object') {
      coinExtInfo.value = 'symbol' in raw ? raw : (raw.data ?? raw.result ?? null)
    }
  } catch { /* silent */ } finally { coinExtLoading.value = false }
}

watch(subNavTab, (tab) => {
  if (tab === 'chart') nextTick(() => { chart?.resize() })
  if (tab === 'coin-info' && !coinExtInfo.value) fetchCoinExtInfo()
})

// ── Infinite scroll ───────────────────────────────────
let tabScrollLoading = false
function onTabScroll() {
  const el = tabScrollRef.value
  if (!el || tabScrollLoading) return
  // 内容未超出容器时不触发加载（避免在顶部也能反复触发分页）
  if (el.scrollHeight <= el.clientHeight + 1) return
  const distBottom = el.scrollHeight - el.scrollTop - el.clientHeight
  if (distBottom >= 100) return

  const tab = activeTab.value
  if (tab === 'history-orders' && historyOrderNoMore.value) return
  if (tab === 'trades' && historyDealNoMore.value) return
  if (tab === 'assets' && accountRecordNoMore.value) return

  tabScrollLoading = true
  let promise: Promise<void> | undefined
  if (tab === 'history-orders') promise = spotStore.fetchHistoryOrders(false, apiSymbol.value)
  else if (tab === 'trades') promise = spotStore.fetchHistoryDeals(false, apiSymbol.value)
  else if (tab === 'assets') promise = spotStore.fetchAccountRecords()
  ;(promise ?? Promise.resolve()).finally(() => { tabScrollLoading = false })
}

// ── WebSocket ─────────────────────────────────────────
const ws = useMarketWs({
  onConnected() { ws.requestKlines() },
  onKlineHistory(bars) {
    if (!bars.length || !chart) return
    currentBars = bars
    const period = RESOLUTION_PERIOD[klineResolution.value] ?? { type: 'hour', span: 1 }
    if (JSON.stringify(chart.getPeriod()) !== JSON.stringify(period)) {
      chart.setPeriod(period)
    } else {
      chart.resetData()
    }
  },
  onKline(bar) {
    klineRealtimeClose.value = bar.close
    klineBarCallback?.({
      timestamp: bar.timestamp, open: bar.open, high: bar.high,
      low: bar.low, close: bar.close, volume: bar.volume,
    })
  },
  onKlineHistoryMore(bars) {
    const cb = loadMoreCallback; loadMoreCallback = null
    if (!cb) return
    if (!bars.length) { cb([], false); return }
    const firstTs = currentBars[0]?.timestamp ?? Infinity
    const newBars = bars.filter(b => b.timestamp < firstTs).sort((a, b_) => a.timestamp - b_.timestamp)
    if (newBars.length) currentBars = [...newBars, ...currentBars]
    cb(newBars, newBars.length > 0)
  },
  onTicker(data) {
    const c = parseFloat(data.close)
    priceUp.value = c >= parseFloat(close.value === '--' ? data.open : close.value)
    close.value  = data.close
    open24.value = data.open
    high24.value = data.high
    low24.value  = data.low
    vol24.value  = data.turnover
    if (!priceIn.value && c > 0 && limitMode.value) {
      priceIn.value = c.toFixed(pricePrecision.value)
    }
    if (chart) {
      document.title = `${displaySymbol.value} ${c.toFixed(pricePrecision.value)} | iBit`
    }
  },
  onBook(data) {
    const toPair = (rows: [string, string][]) =>
      rows
        .map(([p, q]) => [parseFloat(p), parseFloat(q)] as [number, number])
        .filter(([p, q]) => p > 0 && q > 0 && Number.isFinite(p) && Number.isFinite(q))
    const sortedAsks = toPair([...data.asks]).sort((a, b) => a[0] - b[0])
    const sortedBids = toPair([...data.bids]).sort((a, b) => b[0] - a[0])
    rawBookAsks.value = sortedAsks.slice(0, 400)
    rawBookBids.value = sortedBids.slice(0, 400)
  },
  onTrade(item) {
    recentTrades.value.unshift(item)
    if (recentTrades.value.length > 40) recentTrades.value.length = 40
  },
})

// ── 用户资产 / 委托 WS（首屏 REST，后续推送） ───────────
const spotUserWs = useSpotUserPush({
  onAsset: msg => spotStore.applyWsAssetPush(msg),
  onOrder: msg => spotStore.applyWsOrderPush(msg, apiSymbol.value),
  onReconnect: () => {
    spotStore.fetchAssets()
    spotStore.fetchCurrentOrders(apiSymbol.value)
  },
})

function syncSpotUserWs() {
  const uid = user.value?.uid
  if (isLoggedIn.value && uid) spotUserWs.connect(String(uid))
  else spotUserWs.disconnect()
}

// ── Lifecycle ─────────────────────────────────────────
async function boot() {
  if (!coinsStore.fetched) coinsStore.fetchCoins()
  spotStore.fetchCoinIcons()
  if (symbolList.value.length === 0) {
    try { await spotStore.loadSymbols() } catch { /* ignore */ }
  }
}

onMounted(async () => {
  if (!route.params.symbol) {
    await router.replace({ name: 'spot', params: { symbol: 'BTC-USDT' } })
    return
  }
  await boot()
  await nextTick()
  initChart()
  recalcBookRowCount()
  if (typeof ResizeObserver !== 'undefined' && bookColRef.value) {
    bookRo = new ResizeObserver(() => recalcBookRowCount())
    bookRo.observe(bookColRef.value)
  }
  ws.connect(routeSymbol.value, klineResolution.value)
  if (isLoggedIn.value) {
    await spotStore.fetchAssets()
    await spotStore.fetchCurrentOrders(apiSymbol.value)
    syncSpotUserWs()
  }
  window.addEventListener('resize', onWindowResize)
})

function onWindowResize() {
  isMobile.value = window.innerWidth <= 768
  recalcBookRowCount()
}

watch(() => route.params.symbol, async (sym) => {
  if (!sym) return
  recentTrades.value = []
  rawBookAsks.value = []
  rawBookBids.value = []
  klineRealtimeClose.value = null
  depthStep.value = 2
  close.value = '--'
  priceIn.value = ''
  crosshairData.value = null
  coinExtInfo.value = null
  destroyChart()
  await boot()
  await nextTick()
  initChart()
  recalcBookRowCount()
  ws.changeSymbol(String(sym))
  if (isLoggedIn.value) {
    spotStore.fetchAssets()
    spotStore.fetchCurrentOrders(apiSymbol.value)
    syncSpotUserWs()
  }
})

watch(klineResolution, (res) => { ws.changeResolution(res) })
watch([bookLayout, bookTab], async () => {
  await nextTick()
  recalcBookRowCount()
})

watch(isLoggedIn, (v) => {
  if (v) {
    spotStore.fetchAssets()
    spotStore.fetchCurrentOrders(apiSymbol.value)
  }
  syncSpotUserWs()
})

watch(() => user.value?.uid, () => {
  syncSpotUserWs()
})

/** 打开币对选择器时确保列表已拉取（首屏 load 失败时可重试） */
watch(showPairSelector, async (open) => {
  if (!open) return
  if (symbolList.value.length > 0 && !symbolsError.value) return
  try {
    await spotStore.loadSymbols()
  } catch {
    /* loadSymbols 已写入 symbolsError */
  }
})

onUnmounted(() => {
  destroyChart()
  ws.disconnect()
  spotUserWs.disconnect()
  bookRo?.disconnect()
  bookRo = null
  window.removeEventListener('resize', onWindowResize)
})

// ── Helpers ───────────────────────────────────────────
function fmtPrice(v: string | number) {
  const n = typeof v === 'string' ? parseFloat(v) : v
  if (isNaN(n) || v === '--') return '--'
  return n.toFixed(symInfo.value?.pricePrecision ?? 2)
}
/** 盘口档位价：按当前选中的合并精度展示 */
function fmtDepthPrice(price: string | number, dp: number) {
  const n = typeof price === 'string' ? parseFloat(price) : price
  if (isNaN(n)) return '--'
  return n.toFixed(dp)
}
function fmtTime(ts: number) {
  const d = new Date(ts)
  return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`
}
function fmtDateTime(ts: number) {
  const d = new Date(ts)
  const Y = d.getFullYear()
  const M = String(d.getMonth()+1).padStart(2,'0')
  const D = String(d.getDate()).padStart(2,'0')
  const h = String(d.getHours()).padStart(2,'0')
  const m = String(d.getMinutes()).padStart(2,'0')
  const s = String(d.getSeconds()).padStart(2,'0')
  return `${Y}/${M}/${D} ${h}:${m}:${s}`
}
function fmtOrderTime(ts: number | null) {
  if (!ts) return '--'
  const d = new Date(ts)
  return `${d.getMonth()+1}/${d.getDate()} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`
}
function coinIcon(base: string) {
  const b = base.toUpperCase()
  const row = symbolList.value.find(x => x.base.toUpperCase() === b)
  const fromRow = row?.icon?.trim()
  if (fromRow) {
    return fromRow
  }
  return spotStore.coinIconMap.get(b) ?? ''
}
function pickPrice(p: string) {
  priceIn.value = sanitizeDecimalInput(p, pricePrecision.value)
  limitMode.value = true
}

</script>

<template>
  <div class="sp-page" @click="showPairSelector = false">

    <!-- ═══ TICKER BAR ═══ -->
    <div class="ticker-bar">
      <div class="pair-trigger" @click.stop="showPairSelector = !showPairSelector">
        <div class="coin-icon">
          <img
            v-if="currentCoinIcon"
            :src="currentCoinIcon"
            :alt="symBase"
            referrerpolicy="no-referrer"
          />
          <span v-else class="coin-icon-fb">{{ symBase[0] }}</span>
        </div>
        <div class="pair-name-col">
          <div class="pair-name-row">
            <span class="pair-name">{{ displaySymbol }}</span>
            <svg class="caret" :class="{ open: showPairSelector }" width="10" height="6" viewBox="0 0 10 6">
              <path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round"/>
            </svg>
          </div>
          <span class="pair-sub">{{ t('spot.tag') }}</span>
        </div>
        <Transition name="pop">
          <div v-if="showPairSelector" class="pair-drop" @click.stop>
            <div class="pair-drop-search">
              <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#6a7182" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
              <input v-model="pairSearch" class="pair-search-input" :placeholder="t('spot.pair.search')" autofocus />
            </div>
            <div class="pair-list">
              <div v-if="symbolsLoading" class="pair-empty">{{ t('spot.pair.loading') }}</div>
              <template v-else>
                <div
                  v-for="s in filteredSymbols" :key="s.symbol"
                  class="pair-item" :class="{ active: s.symbol === apiSymbol }"
                  @click="selectPair(s.symbol)"
                >
                  <div class="pi-icon">
                    <img
                      v-if="coinIcon(s.base)"
                      :src="coinIcon(s.base)"
                      :alt="s.base"
                      referrerpolicy="no-referrer"
                    />
                    <span v-else class="pi-icon-fb">{{ s.base[0] || '?' }}</span>
                  </div>
                  <span class="pi-sym">{{ s.base }}/{{ s.quote }}</span>
                </div>
                <div v-if="symbolsError && !filteredSymbols.length" class="pair-empty">
                  {{ symbolsError }}{{ t('spot.pair.retryLater') }}
                </div>
                <div v-else-if="!filteredSymbols.length" class="pair-empty">{{ t('spot.pair.noMatch') }}</div>
              </template>
            </div>
          </div>
        </Transition>
      </div>

      <div class="ticker-price-wrap">
        <div class="ticker-price" :class="priceUp ? 'up' : 'dn'">{{ close !== '--' ? fmtPrice(close) : '--' }}</div>
        <div class="ticker-usd">≈ ${{ close !== '--' ? fmtPrice(close) : '--' }}</div>
      </div>

      <div class="ticker-stats">
        <div class="ts-item">
          <span class="ts-lbl">{{ t('spot.ticker.change24h') }}</span>
          <span class="ts-val" :class="parseFloat(change24Pct) >= 0 ? 'up' : 'dn'">{{ change24Pct }}</span>
        </div>
        <div class="ts-item">
          <span class="ts-lbl">{{ t('spot.ticker.high24h') }}</span>
          <span class="ts-val">{{ fmtPrice(high24) }}</span>
        </div>
        <div class="ts-item">
          <span class="ts-lbl">{{ t('spot.ticker.low24h') }}</span>
          <span class="ts-val">{{ fmtPrice(low24) }}</span>
        </div>
        <div class="ts-item">
          <span class="ts-lbl">{{ t('spot.ticker.turnover24h') }}</span>
          <span class="ts-val">{{ fmtVol(vol24) }} USDT</span>
        </div>
      </div>
    </div>

    <!-- ═══ MOBILE TAB BAR ═══ -->
    <div v-if="isMobile" class="mobile-trade-tabs">
      <button class="mtt-btn" :class="{ act: mobileTab === 'chart' }" @click="mobileTab = 'chart'">{{ t('spot.mobileTab.chart') }}</button>
      <button class="mtt-btn" :class="{ act: mobileTab === 'trade' }" @click="mobileTab = 'trade'">{{ t('spot.mobileTab.trade') }}</button>
    </div>

    <!-- ═══ PAGE BODY (display:contents → children join sp-page grid) ═══ -->
    <div class="page-body">

      <!-- LEFT + MIDDLE: chart-area -->
      <div class="chart-area" :class="{ 'mobile-hidden': isMobile && mobileTab !== 'chart' }">
        <div class="chart-plus-book">

          <!-- COL 1: Chart -->
          <div class="chart-col">
            <!-- Drawing sidebar -->
            <div class="draw-side">
              <button class="db" :class="{ 'db--act': activeDrawTool === 'cursor' }" :title="t('spot.draw.cursor')" @click="activateDrawTool('cursor')">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M5 3l14 9-7 1-3 7z"/></svg>
              </button>
              <div class="db-sep"/>
              <button class="db" :class="{ 'db--act': activeDrawTool === 'trend' }" :title="t('spot.draw.trend')" @click="activateDrawTool('trend')">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><line x1="4" y1="20" x2="20" y2="4"/><circle cx="4" cy="20" r="1.8" fill="currentColor"/><circle cx="20" cy="4" r="1.8" fill="currentColor"/></svg>
              </button>
              <button class="db" :class="{ 'db--act': activeDrawTool === 'hline' }" :title="t('spot.draw.hline')" @click="activateDrawTool('hline')">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><line x1="3" y1="12" x2="21" y2="12"/><line x1="5" y1="9" x2="5" y2="15"/><line x1="19" y1="9" x2="19" y2="15"/></svg>
              </button>
              <button class="db" :class="{ 'db--act': activeDrawTool === 'ray' }" :title="t('spot.draw.ray')" @click="activateDrawTool('ray')">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><line x1="4" y1="20" x2="20" y2="4"/><circle cx="4" cy="20" r="1.8" fill="currentColor"/></svg>
              </button>
              <button class="db" :class="{ 'db--act': activeDrawTool === 'channel' }" :title="t('spot.draw.channel')" @click="activateDrawTool('channel')">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><line x1="3" y1="17" x2="21" y2="7"/><line x1="3" y1="21" x2="21" y2="11"/></svg>
              </button>
              <button class="db" :class="{ 'db--act': activeDrawTool === 'fib' }" :title="t('spot.draw.fib')" @click="activateDrawTool('fib')">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
                  <line x1="3" y1="19" x2="21" y2="19"/><line x1="3" y1="14" x2="21" y2="14"/>
                  <line x1="3" y1="10" x2="21" y2="10"/><line x1="3" y1="6" x2="21" y2="6"/>
                </svg>
              </button>
              <div class="db-sep"/>
              <button class="db" :class="{ 'db--act': activeDrawTool === 'text' }" :title="t('spot.draw.text')" @click="activateDrawTool('text')">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><text x="4" y="18" font-size="14" font-weight="700" font-family="Microsoft YaHei, sans-serif">T</text></svg>
              </button>
              <div class="db-sep"/>
              <button class="db" :title="t('spot.draw.clearAll')" @click="clearOverlays()">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
              </button>
              <div class="db-spacer"/>
              <button class="db" :title="t('spot.draw.zoomOut')" @click="zoomOut()">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="10" cy="10" r="6"/><line x1="14.5" y1="14.5" x2="20" y2="20"/><line x1="7" y1="10" x2="13" y2="10"/></svg>
              </button>
            </div>

            <!-- Chart main -->
            <div class="chart-main">
              <!-- Sub-nav tabs -->
              <div class="sub-nav">
                <button class="snav-tab" :class="{ act: subNavTab === 'chart' }" @click="subNavTab = 'chart'">{{ t('spot.chartSub.chart') }}</button>
                <button class="snav-tab" :class="{ act: subNavTab === 'coin-info' }" @click="subNavTab = 'coin-info'">{{ t('spot.chartSub.coinInfo') }}</button>
              </div>

              <!-- TF bar -->
              <div v-show="subNavTab === 'chart'" class="tf-bar">
                <button
                  v-for="tf in tfOptions" :key="tf.value"
                  class="tf-btn" :class="{ act: klineResolution === tf.value }"
                  @click="klineResolution = tf.value; ws.changeResolution(tf.value)"
                >{{ tf.label }}</button>
                <div class="tf-spacer"/>
                <template v-if="showMA && currentBars.length">
                  <span class="ma-leg ma5">MA5</span>
                  <span class="ma-leg ma10">MA10</span>
                  <span class="ma-leg ma30">MA30</span>
                </template>
                <button class="tf-ind" :class="{ act: showMA }" @click="showMA = !showMA">MA</button>
                <button class="tf-ind" :class="{ act: showVol }" @click="showVol = !showVol">VOL</button>
              </div>

              <!-- Canvas + panels -->
              <div class="chart-area-rel">
                <div class="chart-canvas-wrap" @mouseleave="crosshairData = null">
                  <!-- OHLCV strip -->
                  <div class="ohlcv-overlay">
                    <span class="ohlcv-sym">{{ displaySymbol }} · {{ tfOptions.find(tf => tf.value === klineResolution)?.label }} · {{ t('spot.ohlcv.spotLabel') }}</span>
                    <template v-if="!crosshairData?.kLineData && close !== '--'">
                      <span class="ohlcv-item">{{ t('spot.ohlcv.open') }}=<em>{{ fmtPrice(open24) }}</em></span>
                      <span class="ohlcv-item">{{ t('spot.ohlcv.high') }}=<em class="up">{{ fmtPrice(high24) }}</em></span>
                      <span class="ohlcv-item">{{ t('spot.ohlcv.low') }}=<em class="dn">{{ fmtPrice(low24) }}</em></span>
                      <span class="ohlcv-item">{{ t('spot.ohlcv.close') }}=<em>{{ fmtPrice(close) }}</em></span>
                    </template>
                  </div>
                  <!-- Hover tooltip -->
                  <div v-if="crosshairData?.kLineData" class="kline-tooltip" :style="tooltipStyle">
                    <div class="kt-time">{{ fmtDateTime(crosshairData.kLineData.timestamp) }}</div>
                    <div class="kt-row"><span class="kt-lbl">{{ t('spot.ohlcv.open') }}</span><span class="kt-val" :class="crosshairData.kLineData.close >= crosshairData.kLineData.open ? 'up' : 'dn'">{{ fmtPrice(crosshairData.kLineData.open) }}</span></div>
                    <div class="kt-row"><span class="kt-lbl">{{ t('spot.ohlcv.high') }}</span><span class="kt-val up">{{ fmtPrice(crosshairData.kLineData.high) }}</span></div>
                    <div class="kt-row"><span class="kt-lbl">{{ t('spot.ohlcv.low') }}</span><span class="kt-val dn">{{ fmtPrice(crosshairData.kLineData.low) }}</span></div>
                    <div class="kt-row"><span class="kt-lbl">{{ t('spot.ohlcv.close') }}</span><span class="kt-val" :class="crosshairData.kLineData.close >= crosshairData.kLineData.open ? 'up' : 'dn'">{{ fmtPrice(crosshairData.kLineData.close) }}</span></div>
                    <div class="kt-row"><span class="kt-lbl">{{ t('spot.ohlcv.volume') }}</span><span class="kt-val">{{ crosshairData.kLineData.volume?.toFixed(4) }}</span></div>
                    <div class="kt-row">
                      <span class="kt-lbl">{{ t('spot.ohlcv.changePct') }}</span>
                      <span class="kt-val" :class="crosshairData.kLineData.close >= crosshairData.kLineData.open ? 'up' : 'dn'">
                        {{ ((crosshairData.kLineData.close - crosshairData.kLineData.open) / crosshairData.kLineData.open * 100).toFixed(2) }}%
                      </span>
                    </div>
                    <template v-if="crosshairMa">
                      <div v-if="crosshairMa.ma5 !== null" class="kt-row"><span class="kt-lbl kt-c-ma5">MA5</span><span class="kt-val kt-c-ma5">{{ crosshairMa.ma5 }}</span></div>
                      <div v-if="crosshairMa.ma10 !== null" class="kt-row"><span class="kt-lbl kt-c-ma10">MA10</span><span class="kt-val kt-c-ma10">{{ crosshairMa.ma10 }}</span></div>
                      <div v-if="crosshairMa.ma30 !== null" class="kt-row"><span class="kt-lbl kt-c-ma30">MA30</span><span class="kt-val kt-c-ma30">{{ crosshairMa.ma30 }}</span></div>
                    </template>
                  </div>
                  <div ref="chartEl" class="kline-canvas"/>
                </div>

                <!-- 币种资料面板 -->
                <div v-show="subNavTab === 'coin-info'" class="coin-info-panel">
                  <div v-if="coinExtLoading" class="ci-loading"><span class="spin-sm"/></div>
                  <template v-else-if="coinExtInfo">
                    <div class="cip-header">
                      <img
                        v-if="coinExtInfo.icon"
                        :src="coinExtInfo.icon"
                        class="cip-icon"
                        alt="icon"
                        referrerpolicy="no-referrer"
                      />
                      <div class="cip-names">
                        <span class="cip-name-cn">{{ coinExtInfo.nameCn || coinExtInfo.name || symBase }}</span>
                        <span class="cip-name-en">{{ coinExtInfo.nameEn || symBase }}</span>
                      </div>
                      <span v-if="coinExtInfo.rank" class="cip-rank">#{{ coinExtInfo.rank }}</span>
                    </div>
                    <div class="cip-grid">
                      <div class="cip-item"><span class="cip-lbl">{{ t('spot.coinInfo.marketCap') }}</span><span class="cip-val">{{ coinExtInfo.marketCap || '--' }}</span></div>
                      <div class="cip-item"><span class="cip-lbl">{{ t('spot.coinInfo.circulating') }}</span><span class="cip-val">{{ coinExtInfo.circulatingSupply || '--' }}</span></div>
                      <div class="cip-item"><span class="cip-lbl">{{ t('spot.coinInfo.issuePrice') }}</span><span class="cip-val">{{ coinExtInfo.issuePrice != null ? '$' + Number(coinExtInfo.issuePrice).toFixed(6) : '--' }}</span></div>
                      <div class="cip-item"><span class="cip-lbl">{{ t('spot.coinInfo.ath') }}</span><span class="cip-val up">{{ coinExtInfo.athPrice != null ? '$' + Number(coinExtInfo.athPrice).toFixed(2) : '--' }}</span></div>
                      <div v-if="coinExtInfo.athDate" class="cip-item"><span class="cip-lbl">{{ t('spot.coinInfo.athDate') }}</span><span class="cip-val">{{ coinExtInfo.athDate }}</span></div>
                      <div v-if="coinExtInfo.whitepaper" class="cip-item cip-item--full"><span class="cip-lbl">{{ t('spot.coinInfo.whitepaper') }}</span><a :href="coinExtInfo.whitepaper" target="_blank" class="cip-link">{{ coinExtInfo.whitepaper }}</a></div>
                    </div>
                  </template>
                  <div v-else class="cip-empty">
                    <span>{{ t('spot.coinInfo.empty') }}</span>
                    <button class="cip-retry" @click="fetchCoinExtInfo()">{{ t('spot.coinInfo.reload') }}</button>
                  </div>
                </div>
              </div><!-- /chart-area-rel -->
            </div><!-- /chart-main -->
          </div><!-- /chart-col -->

          <!-- COL 2: Order book -->
          <div ref="bookColRef" class="book-col">
            <div ref="bookTabsRef" class="book-hd-tabs">
              <button class="book-tab" :class="{ act: bookTab === 'orderbook' }" @click="bookTab = 'orderbook'">{{ t('spot.book.orderbook') }}</button>
              <button class="book-tab" :class="{ act: bookTab === 'trades' }" @click="bookTab = 'trades'">{{ t('spot.book.trades') }}</button>
            </div>
            <div ref="bookCtrlRef" class="book-hd-ctrl">
              <div v-if="bookTab === 'orderbook'" class="book-ctrl-row">
              <div class="book-layouts">
                <button class="bl-btn" :class="{ act: bookLayout === 'full' }" @click="bookLayout = 'full'">
                  <svg width="16" height="14" viewBox="0 0 16 14">
                    <rect x="1" y="1" width="6" height="2" rx="0.5" fill="#f6465d"/>
                    <rect x="1" y="4.5" width="6" height="2" rx="0.5" fill="#f6465d"/>
                    <rect x="9" y="1" width="6" height="2" rx="0.5" fill="currentColor" opacity="0.3"/>
                    <rect x="9" y="4.5" width="6" height="2" rx="0.5" fill="currentColor" opacity="0.3"/>
                    <rect x="1" y="8" width="6" height="2" rx="0.5" fill="#02c076"/>
                    <rect x="1" y="11" width="6" height="2" rx="0.5" fill="#02c076"/>
                    <rect x="9" y="8" width="6" height="2" rx="0.5" fill="currentColor" opacity="0.3"/>
                    <rect x="9" y="11" width="6" height="2" rx="0.5" fill="currentColor" opacity="0.3"/>
                  </svg>
                </button>
                <button class="bl-btn" :class="{ act: bookLayout === 'asks' }" @click="bookLayout = 'asks'">
                  <svg width="16" height="14" viewBox="0 0 16 14">
                    <rect x="1" y="1" width="14" height="2" rx="0.5" fill="#f6465d"/>
                    <rect x="1" y="4.5" width="14" height="2" rx="0.5" fill="#f6465d"/>
                    <rect x="1" y="8" width="14" height="2" rx="0.5" fill="currentColor" opacity="0.2"/>
                    <rect x="1" y="11" width="14" height="2" rx="0.5" fill="currentColor" opacity="0.2"/>
                  </svg>
                </button>
                <button class="bl-btn" :class="{ act: bookLayout === 'bids' }" @click="bookLayout = 'bids'">
                  <svg width="16" height="14" viewBox="0 0 16 14">
                    <rect x="1" y="1" width="14" height="2" rx="0.5" fill="currentColor" opacity="0.2"/>
                    <rect x="1" y="4.5" width="14" height="2" rx="0.5" fill="currentColor" opacity="0.2"/>
                    <rect x="1" y="8" width="14" height="2" rx="0.5" fill="#02c076"/>
                    <rect x="1" y="11" width="14" height="2" rx="0.5" fill="#02c076"/>
                  </svg>
                </button>
              </div>
              <select
                v-if="symInfo"
                v-model.number="depthStep"
                class="depth-select"
                :title="t('spot.book.pricePrecision')"
              >
                <option :value="0">{{ spotPrecisionLabel(symInfo.depth0Pre) }}</option>
                <option :value="1">{{ spotPrecisionLabel(symInfo.depth1Pre) }}</option>
                <option :value="2">{{ spotPrecisionLabel(symInfo.depth2Pre) }}</option>
              </select>
              </div>
            </div>

            <template v-if="bookTab === 'orderbook'">
              <div ref="bookHeadRef" class="book-col-head">
                <span>{{ t('spot.book.priceUsdt') }}</span>
                <span class="tr">{{ t('spot.book.amountBase', { base: symBase }) }}</span>
                <span class="tr">{{ t('spot.book.total') }}</span>
              </div>

              <div v-if="bookLayout !== 'bids'" class="book-asks">
                <div
                  v-for="(row, i) in displayBookAsks"
                  :key="'a'+i" class="book-row ask-row" @click="pickPrice(row.price)"
                >
                  <div class="br-depth br-depth--ask" :style="{ width: barWidth(displayBookAsks, row.total) }"/>
                  <span class="br-p dn">{{ fmtDepthPrice(row.price, depthPrecision) }}</span>
                  <span class="br-a">{{ parseFloat(row.amount).toFixed(symInfo?.volumePrecision ?? 4) }}</span>
                  <span class="br-t">{{ parseFloat(row.total).toFixed(2) }}</span>
                </div>
              </div>

              <div v-if="bookLayout === 'full'" ref="bookMidRef" class="book-mid" :class="priceUp ? 'up' : 'dn'">
                <span class="bm-price bm-price--fit">{{ bookMidPrice }} USDT</span>
                <svg v-if="priceUp" width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M6 1l5 9H1z"/></svg>
                <svg v-else width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M6 11L1 2h10z"/></svg>
              </div>

              <div v-if="bookLayout !== 'asks'" class="book-bids">
                <div
                  v-for="(row, i) in displayBookBids"
                  :key="'b'+i" class="book-row bid-row" @click="pickPrice(row.price)"
                >
                  <div class="br-depth br-depth--bid" :style="{ width: barWidth(displayBookBids, row.total) }"/>
                  <span class="br-p up">{{ fmtDepthPrice(row.price, depthPrecision) }}</span>
                  <span class="br-a">{{ parseFloat(row.amount).toFixed(symInfo?.volumePrecision ?? 4) }}</span>
                  <span class="br-t">{{ parseFloat(row.total).toFixed(2) }}</span>
                </div>
              </div>

              <div ref="bsRatioRef" class="bs-ratio">
                <div class="bs-bar">
                  <div class="bs-b" :style="{ width: bsRatio.buy + '%' }"/>
                  <div class="bs-s" :style="{ width: bsRatio.sell + '%' }"/>
                </div>
                <div class="bs-lbl">
                  <span class="up">{{ t('spot.book.buyRatio', { n: bsRatio.buy }) }}</span>
                  <span class="dn">{{ t('spot.book.sellRatio', { n: bsRatio.sell }) }}</span>
                </div>
              </div>
            </template>

            <template v-else>
              <div class="book-col-head">
                <span>{{ t('spot.book.priceUsdt') }}</span><span class="tr">{{ t('spot.book.amount') }}</span><span class="tr">{{ t('spot.book.time') }}</span>
              </div>
              <div class="trades-list">
                <div v-for="(t, i) in recentTrades.slice(0, 20)" :key="i" class="trade-row">
                  <span :class="t.isSell ? 'dn' : 'up'">{{ parseFloat(t.price).toFixed(depthPrecision) }}</span>
                  <span class="tr">{{ parseFloat(t.qty).toFixed(symInfo?.volumePrecision ?? 4) }}</span>
                  <span class="tr trade-time">{{ fmtTime(t.time) }}</span>
                </div>
                <div v-if="!recentTrades.length" class="book-empty">{{ t('spot.book.waiting') }}</div>
              </div>
            </template>
          </div><!-- /book-col -->
        </div><!-- /chart-plus-book -->
      </div><!-- /chart-area -->

      <!-- RIGHT: Trading panel -->
      <div class="trade-panel" :class="{ 'mobile-show': isMobile && mobileTab === 'trade' }" @click.stop>
        <div class="tp-side-tabs">
          <button class="tpst" :class="{ 'act-buy': sideBuy }" @click="sideBuy = true">{{ t('spot.trade.buy') }}</button>
          <button class="tpst" :class="{ 'act-sell': !sideBuy }" @click="sideBuy = false">{{ t('spot.trade.sell') }}</button>
        </div>

        <div class="ot-row">
          <button class="ot-btn" :class="{ act: limitMode }" @click="limitMode = true">{{ t('spot.trade.limit') }}</button>
          <button
            class="ot-btn" :class="{ act: !limitMode }"
            @click="limitMode = false"
          >{{ t('spot.trade.market') }}</button>
        </div>

        <div class="tp-body">
          <div v-if="limitMode" class="inp-block">
            <div class="inp-wrap">
              <span class="inp-lbl">{{ t('spot.trade.priceUsdt') }}</span>
              <div class="inp-row">
                <input
                  v-model="priceIn"
                  type="text"
                  inputmode="decimal"
                  class="inp"
                  :placeholder="t('spot.trade.limitPlaceholder')"
                  :step="1 / 10 ** pricePrecision"
                  min="0"
                  autocomplete="off"
                  @input="onPriceInput"
                />
                <span class="inp-unit">USDT</span>
              </div>
            </div>
          </div>
          <div v-else class="inp-block">
            <div class="inp-market">{{ t('spot.trade.marketHint') }}</div>
          </div>

          <div class="inp-block">
            <div class="inp-wrap">
              <span class="inp-lbl">{{ effectiveAmountUnit === 'quote' ? t('spot.trade.amountUsdt') : t('spot.trade.amountBase', { base: symBase }) }}</span>
              <div class="inp-row">
                <input
                  v-model="amountIn"
                  type="text"
                  inputmode="decimal"
                  class="inp"
                  placeholder="0"
                  :step="1 / 10 ** (effectiveAmountUnit === 'quote' ? quoteAmountPrecision : baseAmountPrecision)"
                  min="0"
                  autocomplete="off"
                  @input="onAmountInput"
                />
                <span class="inp-unit">{{ effectiveAmountUnit === 'quote' ? 'USDT' : symBase }}</span>
              </div>
            </div>
          </div>

          <div class="pct-btns">
            <button v-for="p in [25,50,75,100]" :key="p" class="pct-btn" @click="pctAmount(p)">{{ p }}%</button>
          </div>

          <div v-if="orderErr" class="order-err">{{ orderErr }}</div>

          <div class="avail-row">
            <template v-if="isLoggedIn">
              <span class="avail-lbl">{{ t('spot.trade.available') }}</span>
              <span v-if="sideBuy" class="avail-val">{{ fmtBalTrunc(usdtBalance) }} USDT</span>
              <span v-else class="avail-val">{{ fmtBalTrunc(baseBalance, symInfo?.volumePrecision ?? 4) }} {{ symBase }}</span>
              <button class="avail-transfer" @click="openTransfer(1)">{{ t('spot.trade.transfer') }}</button>
            </template>
            <template v-else>
              <span class="avail-val muted">{{ t('spot.trade.loginForBalance') }}</span>
            </template>
          </div>

          <div class="submit-block">
            <div v-if="!isLoggedIn" class="auth-btns">
              <RouterLink :to="{ name: 'register' }" class="auth-btn auth-register">{{ t('header.register') }}</RouterLink>
              <RouterLink :to="{ name: 'login', query: { redirect: route.fullPath } }" class="auth-btn auth-login">{{ t('header.login') }}</RouterLink>
            </div>
            <button
              v-else
              class="submit-btn"
              :class="sideBuy ? 'submit-buy' : 'submit-sell'"
              :disabled="orderBusy"
              @click="submitOrder"
            >{{ orderBusy ? t('spot.trade.processing') : (sideBuy ? t('spot.trade.buySymbol', { symbol: symBase }) : t('spot.trade.sellSymbol', { symbol: symBase })) }}</button>
          </div>

          <div v-if="isLoggedIn && assets.length" class="asset-summary">
            <div class="asset-summary-title">{{ t('spot.trade.spotAssets') }}</div>
            <div v-for="a in assets.slice(0, 8)" :key="a.coin" class="asset-row">
              <span class="asset-coin">{{ a.coin }}</span>
              <span class="asset-bal">{{ fmtBal(a.balance, 4) }}</span>
            </div>
          </div>
        </div>
      </div><!-- /trade-panel -->

      <!-- ═══ BOTTOM AREA ═══ -->
      <div class="bottom-area">
        <div class="btab-bar">
          <button class="btab" :class="{ act: activeTab === 'orders' }" @click="activeTab = 'orders'">
            {{ t('spot.tabs.currentOrders') }} <span class="badge">{{ currentOrders.length }}</span>
          </button>
          <button class="btab" :class="{ act: activeTab === 'history-orders' }" @click="activeTab = 'history-orders'; spotStore.fetchHistoryOrders(true, apiSymbol)">{{ t('spot.tabs.historyOrders') }}</button>
          <button class="btab" :class="{ act: activeTab === 'trades' }" @click="activeTab = 'trades'; spotStore.fetchHistoryDeals(true, apiSymbol)">{{ t('spot.tabs.trades') }}</button>
          <button class="btab" :class="{ act: activeTab === 'assets' }" @click="activeTab = 'assets'; spotStore.fetchAccountRecords(true)">{{ t('spot.tabs.funding') }}</button>
          <div class="btab-sp"/>
          <template v-if="activeTab === 'orders' && isLoggedIn && currentOrders.length">
            <button class="close-all" :disabled="cancelAllBusy" @click="cancelAllOrders">{{ t('spot.order.cancelAll') }}</button>
          </template>
        </div>

        <div ref="tabScrollRef" class="tab-scroll-area" @scroll.passive="onTabScroll">

          <!-- 当前委托 -->
          <template v-if="activeTab === 'orders'">
            <div v-if="!currentOrders.length" class="empty-tip">
              <template v-if="!isLoggedIn">
                <i18n-t keypath="spot.empty.loginOrdersPrompt" tag="span">
                  <template #link>
                    <RouterLink to="/login" class="tip-link">{{ t('spot.empty.loginLink') }}</RouterLink>
                  </template>
                </i18n-t>
              </template>
              <template v-else>{{ t('spot.empty.noOrders') }}</template>
            </div>
            <div v-else class="data-tbl">
              <div class="dt-head dt-head--spot-ord">
                <span>{{ t('spot.table.pair') }}</span><span>{{ t('spot.table.side') }}</span><span>{{ t('spot.table.type') }}</span>
                <span>{{ t('spot.table.price') }}</span><span>{{ t('spot.table.amount') }}</span><span>{{ t('spot.table.filled') }}</span>
                <span>{{ t('spot.table.status') }}</span><span>{{ t('spot.table.time') }}</span><span>{{ t('spot.table.action') }}</span>
              </div>
              <div v-for="o in currentOrders" :key="o.id" class="dt-row dt-row--spot-ord">
                <span class="fw6">{{ o.symbol }}</span>
                <span :class="o.side === 'buy' ? 'up' : 'dn'">{{ sideLabel(o.side) }}</span>
                <span>{{ orderTypeLabel(o.type, o.typeCode) }}</span>
                <span>{{ o.price ? fmtPrice(o.price) : marketPrice() }}</span>
                <span>{{ o.amount.toFixed(symInfo?.volumePrecision ?? 4) }}</span>
                <span>{{ o.tradedAmount.toFixed(symInfo?.volumePrecision ?? 4) }}</span>
                <span>{{ orderStatusFromOrder(o) }}</span>
                <span>{{ fmtOrderTime(o.createTime) }}</span>
                <span class="op-col">
                  <button
                    v-if="o.statusCode !== 2 && o.statusCode !== 4 && o.statusCode !== 5 && o.statusCode !== 6"
                    class="act-btn" @click="cancelOrder(o.id)"
                  >{{ t('spot.order.cancel') }}</button>
                </span>
              </div>
            </div>
          </template>

          <!-- 历史委托 -->
          <template v-else-if="activeTab === 'history-orders'">
            <div v-if="!historyOrders.length" class="empty-tip">
              <template v-if="!isLoggedIn">
                <i18n-t keypath="spot.empty.loginViewPrompt" tag="span">
                  <template #link>
                    <RouterLink to="/login" class="tip-link">{{ t('spot.empty.loginLink') }}</RouterLink>
                  </template>
                </i18n-t>
              </template>
              <template v-else>{{ t('spot.empty.noHistoryOrders') }}</template>
            </div>
            <div v-else class="data-tbl">
              <div class="dt-head dt-head--spot-ord">
                <span>{{ t('spot.table.pair') }}</span><span>{{ t('spot.table.side') }}</span><span>{{ t('spot.table.type') }}</span>
                <span>{{ t('spot.table.orderPrice') }}</span><span>{{ t('spot.table.orderAmount') }}</span><span>{{ t('spot.table.filled') }}</span>
                <span>{{ t('spot.table.status') }}</span><span>{{ t('spot.table.time') }}</span><span></span>
              </div>
              <div v-for="o in historyOrders" :key="o.id" class="dt-row dt-row--spot-ord">
                <span class="fw6">{{ o.symbol }}</span>
                <span :class="o.side === 'buy' ? 'up' : 'dn'">{{ sideLabel(o.side) }}</span>
                <span>{{ orderTypeLabel(o.type, o.typeCode) }}</span>
                <span>{{ o.price ? fmtPrice(o.price) : marketPrice() }}</span>
                <span>{{ o.amount.toFixed(symInfo?.volumePrecision ?? 4) }}</span>
                <span>{{ o.tradedAmount.toFixed(symInfo?.volumePrecision ?? 4) }}</span>
                <span>{{ orderStatusFromOrder(o) }}</span>
                <span>{{ fmtOrderTime(o.createTime) }}</span>
                <span></span>
              </div>
              <div v-if="historyOrderNoMore" class="load-more-row"><span class="all-loaded">{{ t('spot.loadedAll') }}</span></div>
            </div>
          </template>

          <!-- 成交明细 -->
          <template v-else-if="activeTab === 'trades'">
            <div v-if="!historyDeals.length" class="empty-tip">
              <template v-if="!isLoggedIn">
                <i18n-t keypath="spot.empty.loginViewPrompt" tag="span">
                  <template #link>
                    <RouterLink to="/login" class="tip-link">{{ t('spot.empty.loginLink') }}</RouterLink>
                  </template>
                </i18n-t>
              </template>
              <template v-else>{{ t('spot.empty.noTrades') }}</template>
            </div>
            <div v-else class="data-tbl">
              <div class="dt-head dt-head--spot-deal">
                <span>{{ t('spot.table.pair') }}</span><span>{{ t('spot.table.side') }}</span><span>{{ t('spot.table.dealPrice') }}</span>
                <span>{{ t('spot.table.dealVolume') }}</span><span>{{ t('spot.table.turnover') }}</span><span>{{ t('spot.table.time') }}</span>
              </div>
              <div v-for="o in historyDeals" :key="o.id" class="dt-row dt-row--spot-deal">
                <span class="fw6">{{ o.symbol }}</span>
                <span :class="o.side === 'buy' ? 'up' : 'dn'">{{ sideLabel(o.side) }}</span>
                <span>{{ o.avgPrice ? fmtPrice(o.avgPrice) : (o.price ? fmtPrice(o.price) : '--') }}</span>
                <span>{{ o.tradedAmount.toFixed(symInfo?.volumePrecision ?? 4) }}</span>
                <span>{{ o.tradedTurnover.toFixed(2) }}</span>
                <span>{{ fmtOrderTime(o.createTime) }}</span>
              </div>
              <div v-if="historyDealNoMore" class="load-more-row"><span class="all-loaded">{{ t('spot.loadedAll') }}</span></div>
            </div>
          </template>

          <!-- 资金流水 -->
          <template v-else-if="activeTab === 'assets'">
            <div v-if="!accountRecords.length" class="empty-tip">
              <template v-if="!isLoggedIn">
                <i18n-t keypath="spot.empty.loginViewPrompt" tag="span">
                  <template #link>
                    <RouterLink to="/login" class="tip-link">{{ t('spot.empty.loginLink') }}</RouterLink>
                  </template>
                </i18n-t>
              </template>
              <template v-else>{{ t('spot.empty.noFunding') }}</template>
            </div>
            <div v-else class="data-tbl">
              <div class="dt-head dt-head--spot-rec">
                <span>{{ t('spot.table.recordType') }}</span><span>{{ t('spot.table.quantity') }}</span><span>{{ t('spot.table.coin') }}</span><span>{{ t('spot.table.time') }}</span>
              </div>
              <div v-for="r in accountRecords" :key="r.id" class="dt-row dt-row--spot-rec">
                <span>{{ recordTypeLabel(r.type) }}</span>
                <span :class="r.flag === 1 ? 'up' : r.flag === 2 ? 'dn' : ''">
                  {{ r.flag === 1 ? '+' : r.flag === 2 ? '-' : '' }}{{ r.amount.toFixed(4) }}
                </span>
                <span>{{ r.symbol }}</span>
                <span>{{ fmtOrderTime(r.createTime) }}</span>
              </div>
              <div v-if="accountRecordNoMore" class="load-more-row"><span class="all-loaded">{{ t('spot.loadedAll') }}</span></div>
            </div>
          </template>

        </div><!-- /tab-scroll-area -->
      </div><!-- /bottom-area -->

    </div><!-- /page-body -->

  </div><!-- /sp-page -->

  <!-- ═══ 划转弹窗 ═══ -->
  <Transition name="modal">
    <div v-if="showTransfer" class="modal-mask" @click.self="showTransfer = false">
      <div class="modal-box" @click.stop>
        <div class="modal-hd">
          <span class="modal-title">{{ t('spot.transferModal.title') }}</span>
          <button class="modal-close" @click="showTransfer = false">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </div>

        <div class="modal-body">
          <!-- 币种 -->
          <div class="tf-field">
            <div class="tf-label">{{ t('spot.transferModal.selectCoin') }}</div>
            <select v-model="tfSymbol" class="tf-select" :disabled="tfLoading">
              <option v-for="c in tfCoinOptions" :key="c" :value="c">{{ c }}</option>
            </select>
          </div>

          <!-- 从 / 到 -->
          <div class="tf-direction">
            <div class="tf-dir-col">
              <div class="tf-dir-lbl">{{ t('common.from') }}</div>
              <div class="tf-select tf-select--static"><span>{{ tfFromLabel }}</span></div>
            </div>
            <button class="tf-swap" @click="swapTfDirection">
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 16V4m0 0L3 8m4-4l4 4M17 8v12m0 0l4-4m-4 4l-4-4"/></svg>
            </button>
            <div class="tf-dir-col">
              <div class="tf-dir-lbl">{{ t('common.to') }}</div>
              <div class="tf-select tf-select--static"><span>{{ tfToLabel }}</span></div>
            </div>
          </div>

          <!-- 划转数量 -->
          <div class="tf-field">
            <div class="tf-label">{{ t('spot.transferModal.amount') }}</div>
            <div class="tf-amount-wrap">
              <input
                v-model="tfAmount"
                type="number"
                class="tf-amount-inp"
                :placeholder="t('spot.transferModal.amountPlaceholder')"
                :disabled="tfSubmitting"
                @input="tfError = ''"
              />
              <div class="tf-amount-right">
                <span class="tf-currency">{{ tfSymbol }}</span>
                <span class="tf-sep">|</span>
                <button class="tf-all" @click="tfAmount = tfAvail">{{ t('common.all') }}</button>
              </div>
            </div>
            <div class="tf-avail">
              {{ t('spot.transferModal.availableCoin', { coin: tfSymbol }) }}
              <span v-if="tfLoading" class="tf-avail-loading">{{ t('common.loading') }}</span>
              <span v-else>{{ tfAvail }}</span>
            </div>
            <div v-if="tfError" class="tf-error">{{ tfError }}</div>
          </div>
        </div>

        <div class="modal-ft">
          <button class="tf-cancel" :disabled="tfSubmitting" @click="showTransfer = false">{{ t('common.cancel') }}</button>
          <button class="tf-confirm" :disabled="tfSubmitting || tfLoading" @click="confirmTransfer">
            {{ tfSubmitting ? t('spot.transferModal.transferring') : t('common.confirm') }}
          </button>
        </div>
      </div>
    </div>
  </Transition>
</template>

<style scoped>
* { box-sizing: border-box; }

.sp-page {
  /* 不要用 100vh 撑满：否则网格内容不足时会出现 #141414 底条夹在委托区与全站 Footer 之间 */
  min-height: 0;
  padding-top: var(--header-height, 60px);
  background: #141414;
  color: #d1d1d2;
  display: grid;
  grid-template-columns: 1fr 283px;
  grid-template-rows: 58px 420px 260px;
  column-gap: 1px;
  font-size: 13px;
  font-family: var(--font-family);
}

/* ─── Ticker bar ─── */
.ticker-bar {
  grid-column: 1; grid-row: 1;
  display: flex; align-items: center;
  height: 58px; padding: 0 12px 0 8px;
  background: #000; border-bottom: 1px solid #212121;
  z-index: 30; min-width: 0;
}
.pair-trigger {
  display: flex; align-items: center; gap: 8px;
  cursor: pointer; padding: 5px 10px 5px 6px;
  border-radius: 6px; position: relative;
  flex-shrink: 0; margin-right: 4px; transition: background .15s;
}
.pair-trigger:hover { background: rgba(255,255,255,0.04); }
.coin-icon {
  width: 32px; height: 32px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0; overflow: hidden; background: #2a2a2a;
}
.coin-icon img { width: 100%; height: 100%; object-fit: cover; }
.coin-icon-fb { font-size: 13px; font-weight: 700; color: #f0b90b; }
.pair-name-col { display: flex; flex-direction: column; gap: 1px; }
.pair-name-row { display: flex; align-items: center; gap: 4px; }
.pair-name { font-size: 18px; font-weight: 700; color: #e8ecf4; letter-spacing: 0.3px; }
.pair-sub { font-size: 10px; color: #5a6278; }
.caret { color: #6a7182; transition: transform .2s; flex-shrink: 0; }
.caret.open { transform: rotate(180deg); }

.pair-drop {
  position: absolute; top: calc(100% + 6px); left: 0;
  width: 300px; background: #1d1d1d;
  border: 1px solid rgba(255,255,255,0.1); border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0,0,0,.9); overflow: hidden; z-index: 200;
}
.pair-drop-search {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 14px; border-bottom: 1px solid #212121;
}
.pair-search-input { flex: 1; background: none; color: #c7cce4; font-size: 13px; }
.pair-search-input::placeholder { color: #5a6278; }
.pair-list { max-height: 320px; overflow-y: auto; }
.pair-list::-webkit-scrollbar { width: 4px; }
.pair-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 2px; }
.pair-item {
  display: grid; grid-template-columns: 22px 1fr;
  align-items: center; gap: 8px; padding: 9px 14px;
  cursor: pointer; transition: background .12s;
}
.pair-item:hover, .pair-item.active { background: rgba(255,255,255,0.04); }
.pi-icon {
  width: 20px; height: 20px; border-radius: 50%; overflow: hidden;
  background: #2a2a2a; display: flex; align-items: center; justify-content: center;
}
.pi-icon img { width: 100%; height: 100%; object-fit: cover; }
.pi-icon-fb { font-size: 10px; font-weight: 700; color: #f0b90b; }
.pi-sym { font-weight: 600; font-size: 12px; }
.pair-empty { padding: 14px; text-align: center; color: #5a6278; font-size: 12px; }

.ticker-price-wrap { flex-shrink: 0; padding: 0 20px; border-right: 1px solid #212121; }
.ticker-price { font-size: 24px; font-weight: 700; line-height: 1.2; font-variant-numeric: tabular-nums; }
.ticker-usd { font-size: 11px; color: #5a6278; margin-top: 2px; }
.ticker-stats { display: flex; align-items: center; flex: 1; overflow: hidden; padding-left: 8px; }
.ts-item { display: flex; flex-direction: column; gap: 3px; flex-shrink: 0; padding: 0 14px; }
.ts-lbl { font-size: 11px; color: #5a6278; white-space: nowrap; }
.ts-val { font-size: 12px; font-weight: 500; white-space: nowrap; color: #c7cce4; }

/* ─── Mobile ─── */
.mobile-trade-tabs {
  display: none; grid-column: 1 / -1; height: 38px;
  background: #000; border-bottom: 1px solid #212121;
  align-items: center; justify-content: center; gap: 4px;
}
.mtt-btn { flex: 1; height: 100%; font-size: 13px; font-weight: 500; color: #6a7182; transition: all .12s; }
.mtt-btn.act { color: #e8ecf4; }

/* ─── Page body: display:contents lets children join the outer grid ─── */
.page-body { display: contents; }

/* ─── Chart area ─── */
.chart-area {
  grid-column: 1; grid-row: 2;
  min-width: 0; min-height: 0;
  display: flex; flex-direction: column; background: #000;
}
.chart-plus-book { flex: 1; min-height: 0; display: flex; }

/* ─── Chart column ─── */
.chart-col { flex: 1; min-width: 0; display: flex; border-right: 1px solid #212121; }

/* Drawing sidebar */
.draw-side {
  width: 44px; flex-shrink: 0;
  display: flex; flex-direction: column; align-items: center;
  padding: 44px 0 8px; gap: 2px; border-right: 1px solid #212121;
}
.db {
  width: 34px; height: 32px;
  display: flex; align-items: center; justify-content: center;
  color: #6a7182; border-radius: 4px; transition: all .12s; flex-shrink: 0;
}
.db:hover, .db--act { color: #e8ecf4; background: rgba(255,255,255,0.08); }
.db-sep { width: 20px; height: 1px; background: rgba(255,255,255,0.07); margin: 3px 0; }
.db-spacer { flex: 1; }

/* Chart main */
.chart-main { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }

/* Sub-nav */
.sub-nav {
  display: flex; align-items: center;
  height: 38px; padding: 0 8px;
  border-bottom: 1px solid #212121; flex-shrink: 0;
}
.snav-tab {
  padding: 0 14px; height: 100%;
  font-size: 13px; color: #6a7182;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: all .15s; white-space: nowrap;
}
.snav-tab:hover { color: #c7cce4; }
.snav-tab.act { color: #e8ecf4; border-bottom-color: #f0b90b; }

/* TF bar */
.tf-bar {
  display: flex; align-items: center; gap: 1px;
  padding: 0 8px; height: 36px;
  border-bottom: 1px solid #212121; flex-shrink: 0;
}
.tf-btn { padding: 3px 7px; font-size: 12px; color: #6a7182; border-radius: 3px; transition: all .12s; white-space: nowrap; }
.tf-btn:hover { color: #c7cce4; background: rgba(255,255,255,0.05); }
.tf-btn.act { color: #f0b90b; }
.tf-spacer { flex: 1; }
.ma-leg { font-size: 11px; font-weight: 600; margin-right: 2px; }
.ma5 { color: #f0b90b; } .ma10 { color: #5d9cf5; } .ma30 { color: #e84d5b; }
.tf-ind {
  padding: 2px 6px; font-size: 11px; font-weight: 600;
  border: 1px solid rgba(255,255,255,0.1); border-radius: 3px;
  color: #6a7182; transition: all .12s; flex-shrink: 0; margin-left: 1px;
}
.tf-ind.act { color: #f0b90b; border-color: rgba(240,185,11,0.4); background: rgba(240,185,11,0.07); }

/* Chart canvas */
.chart-area-rel { flex: 1; position: relative; min-height: 0; overflow: hidden; }
.chart-canvas-wrap { position: absolute; inset: 0; overflow: hidden; z-index: 1; }
.kline-canvas { width: 100%; height: 100%; }

/* OHLCV overlay strip */
.ohlcv-overlay {
  position: absolute; top: 4px; left: 4px; right: 0; z-index: 10;
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  font-size: 11px; color: #6a7182; pointer-events: none; padding: 2px 6px;
}
.ohlcv-sym { color: #9298ab; font-weight: 500; }
.ohlcv-item em { font-style: normal; color: #9298ab; }

/* Hover tooltip */
.kline-tooltip {
  position: absolute; top: 24px; z-index: 20;
  background: rgba(16,17,22,0.88); border: 1px solid rgba(255,255,255,0.07);
  border-radius: 4px; padding: 6px 10px; font-size: 11px; pointer-events: none; min-width: 160px;
}
.kt-time { color: #6a7182; margin-bottom: 3px; font-size: 10.5px; }
.kt-row { display: flex; justify-content: space-between; gap: 16px; line-height: 1.75; }
.kt-lbl { color: #6a7182; }
.kt-val { color: #d1d4dc; }
.kt-c-ma5 { color: #f0b90b; } .kt-c-ma10 { color: #5d9cf5; } .kt-c-ma30 { color: #e84d5b; }

/* Coin info panel */
.coin-info-panel {
  position: absolute; inset: 0; z-index: 2;
  background: #000; overflow-y: auto; padding: 16px;
}
.ci-loading { display: flex; justify-content: center; padding: 40px; }
.cip-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; }
.cip-icon { width: 40px; height: 40px; border-radius: 50%; }
.cip-names { display: flex; flex-direction: column; gap: 2px; flex: 1; }
.cip-name-cn { font-size: 16px; font-weight: 700; color: #e8ecf4; }
.cip-name-en { font-size: 12px; color: #5a6278; }
.cip-rank { font-size: 11px; color: #f0b90b; background: rgba(240,185,11,0.1); border-radius: 4px; padding: 2px 6px; }
.cip-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
.cip-item { display: flex; flex-direction: column; gap: 4px; }
.cip-item--full { grid-column: 1 / -1; }
.cip-lbl { font-size: 11px; color: #5a6278; }
.cip-val { font-size: 13px; color: #c7cce4; font-weight: 500; word-break: break-all; }
.cip-link { color: #4a8fff; font-size: 12px; word-break: break-all; text-decoration: none; }
.cip-link:hover { text-decoration: underline; }
.cip-empty { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 40px; color: #5a6278; }
.cip-retry {
  font-size: 12px; color: #f0b90b; border: 1px solid rgba(240,185,11,0.4);
  border-radius: 4px; padding: 4px 12px; transition: all .12s;
}
.cip-retry:hover { background: rgba(240,185,11,0.08); }

/* ─── Book column ─── */
.book-col {
  width: 214px; flex-shrink: 0; background: #000;
  display: flex; flex-direction: column; overflow: hidden;
}
.book-hd-tabs {
  display: flex; align-items: center;
  padding: 0 12px; height: 40px;
  border-bottom: 1px solid #212121; flex-shrink: 0;
}
.book-hd-ctrl {
  display: flex; align-items: center;
  padding: 0 12px; min-height: 34px;
  border-bottom: 1px solid #212121; flex-shrink: 0; gap: 6px;
}
.book-ctrl-row {
  display: flex; align-items: center; justify-content: space-between;
  width: 100%; gap: 8px; min-width: 0;
}
.depth-select {
  flex: 1; min-width: 0; max-width: 76px; height: 26px;
  font-size: 11px; color: #c7cce4; background: #141414;
  border: 1px solid #2a2a2a; border-radius: 4px; padding: 0 4px;
  cursor: pointer;
}
.depth-select:focus { outline: none; border-color: #f0b90b55; }
.book-tab {
  padding: 0 6px; height: 100%;
  font-size: 13px; font-weight: 500; color: #45454a;
  position: relative; transition: color .12s; white-space: nowrap;
}
.book-tab:hover { color: #d1d1d2; }
.book-tab.act { color: #fff; }
.book-tab.act::after {
  content: ''; position: absolute; bottom: 0; left: 50%;
  transform: translateX(-50%); width: 16px; height: 3px;
  background: #fff; border-radius: 2px 2px 0 0;
}
.book-layouts { display: flex; align-items: center; gap: 4px; }
.bl-btn {
  width: 24px; height: 24px;
  display: flex; align-items: center; justify-content: center;
  color: #5a6278; border-radius: 3px; transition: all .12s;
}
.bl-btn:hover { background: rgba(255,255,255,0.06); color: #c7cce4; }
.bl-btn.act { background: rgba(255,255,255,0.08); color: #c7cce4; }
.book-col-head {
  display: grid; grid-template-columns: 1fr 1fr 1fr;
  padding: 4px 8px; font-size: 10px; color: #5a6278; flex-shrink: 0;
}
.book-asks, .book-bids { overflow: hidden; }
.book-row {
  position: relative; display: grid; grid-template-columns: 1fr 1fr 1fr;
  padding: 2px 8px; font-size: 11px; align-items: center; cursor: pointer;
}
.book-row:hover { background: rgba(255,255,255,0.02); }
.br-depth { position: absolute; top: 0; right: 0; bottom: 0; opacity: 0.12; pointer-events: none; transition: width .3s; }
.br-depth--ask { background: #f6465d; }
.br-depth--bid { background: #02c076; }
.br-p { font-weight: 500; z-index: 1; font-variant-numeric: tabular-nums; }
.br-a { text-align: right; color: #7a8096; z-index: 1; font-size: 10.5px; }
.br-t { text-align: right; color: #5a6278; z-index: 1; font-size: 10.5px; }
.tr { text-align: right; }
.book-mid { display: flex; align-items: center; gap: 6px; padding: 6px 8px; flex-shrink: 0; min-width: 0; }
.book-mid.up { background: rgba(2,192,118,0.06); }
.book-mid.dn { background: rgba(246,70,93,0.06); }
.bm-price { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
.bm-price--fit { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bs-ratio { margin-top: auto; padding: 6px 8px; border-top: 1px solid #212121; flex-shrink: 0; }
.bs-bar { display: flex; height: 3px; border-radius: 2px; overflow: hidden; margin-bottom: 4px; }
.bs-b { background: #02c076; transition: width .5s; }
.bs-s { background: #f6465d; transition: width .5s; }
.bs-lbl { display: flex; justify-content: space-between; font-size: 11px; }
.trades-list { flex: 1; min-height: 0; overflow: hidden; }
.trade-row { display: grid; grid-template-columns: 1fr 1fr 1fr; padding: 3px 10px; font-size: 12px; line-height: 1.6; }
.trade-time { color: #6a7182; }
.book-empty { text-align: center; color: #5a6278; font-size: 11px; padding: 20px; }

/* ─── Trade panel ─── */
.trade-panel {
  grid-column: 2; grid-row: 1 / span 3;
  background: #000; border-left: 1px solid #212121;
  display: flex; flex-direction: column;
  overflow-y: auto; min-height: 0;
}
.trade-panel::-webkit-scrollbar { width: 3px; }
.trade-panel::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }

.tp-side-tabs { display: flex; flex-shrink: 0; border-bottom: 1px solid #212121; }
.tpst { flex: 1; height: 44px; font-size: 14px; font-weight: 600; color: #5a6278; transition: all .12s; }
.tpst:hover { color: #c7cce4; }
.tpst.act-buy { color: #02c076; border-bottom: 2px solid #02c076; }
.tpst.act-sell { color: #f6465d; border-bottom: 2px solid #f6465d; }

.ot-row { display: flex; align-items: center; padding: 6px 10px; gap: 2px; border-bottom: 1px solid #212121; flex-shrink: 0; }
.ot-btn { padding: 5px 9px; font-size: 12px; font-weight: 500; border-radius: 4px; color: #6a7182; transition: all .12s; }
.ot-btn.act { background: rgba(255,255,255,0.08); color: #e8ecf4; }
.ot-btn:disabled { opacity: 0.3; cursor: not-allowed; }

.tp-body { display: flex; flex-direction: column; padding-bottom: 10px; }
.avail-row { display: flex; align-items: center; font-size: 12px; padding: 6px 12px; flex-shrink: 0; gap: 6px; border-top: 1px solid #212121; margin-top: 4px; }
.tp-assets { flex-shrink: 0; }
.avail-lbl { color: #6a7182; flex-shrink: 0; }
.avail-val { color: #c7cce4; font-weight: 500; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.avail-transfer { font-size: 11px; color: #f0b90b; flex-shrink: 0; background: none; padding: 0; cursor: pointer; }
.avail-transfer:hover { opacity: 0.8; }
.muted { color: #5a6278; }

.inp-block { padding: 6px 12px 0; flex-shrink: 0; }
.inp-wrap { background: rgba(255,255,255,0.08); border-radius: 8px; padding: 8px 10px 6px; transition: background .15s; }
.inp-wrap:focus-within { background: rgba(255,255,255,0.12); }
.inp-lbl { display: block; font-size: 12px; color: #8b8b8e; margin-bottom: 2px; }
.inp-row { display: flex; align-items: center; gap: 4px; }
.inp {
  flex: 1; background: none; color: #fff;
  font-size: 17px; font-weight: 700; min-width: 0;
  border: none; outline: none; -webkit-appearance: none; appearance: none;
}
.inp::placeholder { color: #5a6278; font-size: 12px; font-weight: 400; }
.inp::-webkit-inner-spin-button, .inp::-webkit-outer-spin-button { display: none; }
.inp-unit { font-size: 12px; color: #8b8b8e; flex-shrink: 0; }
.inp-market {
  height: 42px; display: flex; align-items: center;
  padding: 0 10px; background: rgba(255,255,255,0.08);
  border-radius: 8px; color: #6a7182; font-size: 12px;
}

.pct-btns { display: flex; gap: 4px; padding: 10px 12px 4px; flex-shrink: 0; }
.pct-btn {
  flex: 1; height: 26px; font-size: 11px; font-weight: 600;
  background: rgba(255,255,255,0.06); color: #9298ab;
  border-radius: 4px; transition: all .12s;
}
.pct-btn:hover { background: rgba(255,255,255,0.12); color: #e8ecf4; }

.order-err { font-size: 12px; color: #f6465d; padding: 0 12px 4px; flex-shrink: 0; }

.submit-block { padding: 6px 12px 8px; flex-shrink: 0; }
.submit-btn { width: 100%; height: 44px; border-radius: 8px; font-size: 15px; font-weight: 700; transition: opacity .15s; }
.submit-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.submit-buy { background: #02c076; color: #fff; }
.submit-buy:hover:not(:disabled) { opacity: 0.9; }
.submit-sell { background: #f6465d; color: #fff; }
.submit-sell:hover:not(:disabled) { opacity: 0.9; }
.auth-btns { display: flex; flex-direction: column; gap: 8px; }
.auth-btn {
  width: 100%; height: 44px; border-radius: 9999px;
  font-size: 14px; font-weight: 600;
  display: flex; align-items: center; justify-content: center;
  text-decoration: none; transition: all .15s;
}
.auth-register { background: #fff; color: #000; }
.auth-register:hover { background: rgba(255,255,255,0.9); }
.auth-login { background: rgba(255,255,255,0.12); color: #fff; }
.auth-login:hover { background: rgba(255,255,255,0.18); }

.asset-summary { margin: 8px 12px; padding: 10px; background: rgba(255,255,255,0.04); border-radius: 8px; flex-shrink: 0; }
.asset-summary-title { font-size: 11px; color: #6a7182; margin-bottom: 8px; }
.asset-row { display: flex; justify-content: space-between; font-size: 12px; padding: 2px 0; }
.asset-coin { color: #8b8b8e; }
.asset-bal { color: #c7cce4; font-variant-numeric: tabular-nums; }

/* ─── Bottom area ─── */
.bottom-area {
  grid-column: 1; grid-row: 3;
  background: #000; border-top: 1px solid #212121;
  display: flex; flex-direction: column;
  min-height: 260px; min-width: 0;
}
.tab-scroll-area {
  flex: 1;
  overflow-y: auto;
  min-height: 0;
  min-width: 0;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
}
.tab-scroll-area::-webkit-scrollbar { width: 3px; }
.tab-scroll-area::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }

.btab-bar {
  display: flex; align-items: center;
  padding: 0 12px; height: 42px;
  border-bottom: 1px solid #212121;
}
.btab-sp { flex: 1; }
.btab {
  padding: 0 12px; height: 100%;
  font-size: 12.5px; color: #6a7182;
  border-bottom: 2px solid transparent; margin-bottom: -1px;
  transition: all .15s; display: flex; align-items: center; gap: 4px; white-space: nowrap;
}
.btab:hover { color: #c7cce4; }
.btab.act { color: #f0b90b; border-bottom-color: #f0b90b; }
.badge {
  font-size: 10px; background: rgba(240,185,11,0.15); color: #f0b90b;
  border-radius: 10px; padding: 0 5px; min-width: 16px; text-align: center;
}
.close-all {
  padding: 5px 12px; font-size: 12px; font-weight: 500;
  background: transparent; color: #f0b90b;
  border: 1px solid rgba(240,185,11,0.45); border-radius: 4px;
  transition: all .12s; white-space: nowrap;
}
.close-all:hover { background: rgba(240,185,11,0.1); }

/* Data tables */
.empty-tip {
  padding: 28px 16px; text-align: center; color: #6a7182; font-size: 13px;
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.tip-link { color: #f0b90b; text-decoration: none; }
.tip-link:hover { text-decoration: underline; }
.data-tbl { overflow-x: auto; position: relative; }

.dt-head, .dt-row {
  display: grid; padding: 6px 10px;
  align-items: center; font-size: 12px;
  min-width: 100%; width: max-content; box-sizing: border-box;
}
.dt-head > span, .dt-row > span {
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-align: center; padding: 0 5px;
}
.dt-head > span { color: #6a7182; }
.dt-head > span:first-child, .dt-row > span:first-child { text-align: left; }
.dt-row > span.op-col { display: flex; overflow: visible; white-space: nowrap; text-align: left; padding: 0; }

.dt-head--spot-ord, .dt-row--spot-ord {
  grid-template-columns: 100px 56px 56px 110px 100px 100px 70px 110px 60px;
}
.dt-head--spot-deal, .dt-row--spot-deal {
  grid-template-columns: 100px 56px 110px 100px 100px 110px;
}
.dt-head--spot-rec, .dt-row--spot-rec {
  grid-template-columns: 90px 120px 100px 120px;
}

.dt-head { border-bottom: 1px solid #212121; }
.dt-row { color: #9298ab; border-bottom: 1px solid #1a1a1a; transition: background .12s; }
.dt-row:hover { background: #111; }
.fw6 { font-weight: 600; color: #e8ecf4 !important; }
.act-btn {
  font-size: 10px; padding: 2px 6px;
  border: 1px solid rgba(240,185,11,0.35); border-radius: 3px;
  color: #f0b90b; transition: all .12s; white-space: nowrap;
}
.act-btn:hover { background: rgba(240,185,11,0.08); }
.load-more-row { display: flex; justify-content: center; padding: 10px 0; }
.all-loaded { color: #5a6278; font-size: 12px; }

/* ─── Colors ─── */
.up { color: #02c076; }
.dn { color: #f6465d; }

/* ─── Spinner ─── */
.spin-sm {
  display: inline-block; width: 16px; height: 16px;
  border: 2px solid rgba(255,255,255,0.3); border-top-color: currentColor;
  border-radius: 50%; animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ─── Transitions ─── */
.pop-enter-active, .pop-leave-active { transition: opacity .15s, transform .15s; }
.pop-enter-from, .pop-leave-to { opacity: 0; transform: translateY(-4px); }

/* ─── Responsive ─── */
@media (max-width: 1100px) {
  .ts-item:nth-child(n+4) { display: none; }
}
@media (max-width: 768px) {
  /* 单列 flex：底部委托区固定高度内滚动，避免整页跟着下拉 */
  .sp-page {
    display: flex;
    flex-direction: column;
    height: calc(100dvh - var(--header-height, 60px));
    min-height: calc(100dvh - var(--header-height, 60px));
    max-height: calc(100dvh - var(--header-height, 60px));
    overflow: hidden;
    grid-template-columns: unset;
    grid-template-rows: unset;
  }

  .mobile-trade-tabs {
    display: flex;
    height: 38px;
    flex-shrink: 0;
    grid-column: unset;
    grid-row: unset;
  }
  .mtt-btn { flex: 1; }

  .ticker-bar {
    grid-column: unset;
    grid-row: unset;
    flex-shrink: 0;
    height: auto;
    min-height: 54px;
    padding: 8px 10px;
  }
  .ticker-stats .ts-item { display: none; }
  .ticker-stats .ts-item:nth-child(1),
  .ticker-stats .ts-item:nth-child(2) { display: flex; }

  .chart-area {
    grid-column: unset;
    grid-row: unset;
    height: 280px;
    flex-shrink: 0;
  }
  .chart-area.mobile-hidden { display: none; }

  .trade-panel {
    grid-column: unset;
    grid-row: unset;
    border-left: none;
    display: none;
    flex-shrink: 0;
    max-height: 42vh;
    overflow-y: auto;
  }
  .trade-panel.mobile-show { display: flex; }

  .bottom-area {
    grid-column: unset;
    grid-row: unset;
    flex: 1;
    min-height: 0;
    overflow: hidden;
  }

  .btab-bar {
    flex-shrink: 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    padding: 0 6px;
  }
  .btab-bar::-webkit-scrollbar { display: none; }
  .btab { padding: 0 8px; font-size: 12px; }

  .data-tbl { -webkit-overflow-scrolling: touch; }

  .book-col { display: none; }
  .mobile-hidden { display: none; }
  .draw-side { display: none; }
}

/* ── 划转弹窗 ── */
.modal-mask {
  position: fixed; inset: 0; z-index: 1000;
  background: rgba(0,0,0,0.6);
  display: flex; align-items: center; justify-content: center;
}
.modal-box {
  width: 440px; background: #1e2026; border-radius: 12px;
  display: flex; flex-direction: column; overflow: hidden;
  box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.modal-hd {
  display: flex; align-items: center; justify-content: space-between;
  padding: 20px 24px 16px; border-bottom: 1px solid #2a2d35;
}
.modal-title { font-size: 16px; font-weight: 600; color: #e8ecf4; }
.modal-close { color: #6a7182; padding: 4px; border-radius: 4px; transition: color .15s; }
.modal-close:hover { color: #c7cce4; }
.modal-body { padding: 20px 24px; display: flex; flex-direction: column; gap: 18px; }
.modal-ft { display: flex; gap: 12px; padding: 8px 24px 24px; }
.tf-field { display: flex; flex-direction: column; }
.tf-label { font-size: 12px; color: #6a7182; margin-bottom: 8px; }
.tf-select {
  display: flex; align-items: center;
  background: #2a2d35; border-radius: 8px; padding: 12px 14px;
  color: #e8ecf4; font-size: 14px;
}
.tf-select--static { cursor: default; }
.tf-direction { display: flex; align-items: flex-end; gap: 10px; }
.tf-dir-col { flex: 1; }
.tf-dir-lbl { font-size: 12px; color: #6a7182; margin-bottom: 8px; }
.tf-swap {
  flex-shrink: 0; width: 36px; height: 44px;
  background: #2a2d35; border-radius: 8px; color: #c7cce4;
  display: flex; align-items: center; justify-content: center;
  transition: background .15s;
}
.tf-swap:hover { background: #363a45; }
.tf-amount-wrap {
  display: flex; align-items: center;
  background: #2a2d35; border-radius: 8px; padding: 0 14px; height: 46px;
}
.tf-amount-inp {
  flex: 1; background: none; color: #e8ecf4; font-size: 14px;
  outline: none; border: none;
}
.tf-amount-inp::placeholder { color: #3a3d45; }
.tf-amount-inp:disabled { opacity: 0.6; }
.tf-amount-inp::-webkit-inner-spin-button,
.tf-amount-inp::-webkit-outer-spin-button { display: none; }
.tf-amount-right { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.tf-currency { font-size: 13px; color: #6a7182; }
.tf-sep { color: #3a3d45; }
.tf-all { font-size: 13px; color: #f0b90b; }
.tf-all:hover { opacity: 0.8; }
.tf-avail { font-size: 12px; color: #6a7182; margin-top: 8px; }
.tf-avail span { color: #c7cce4; }
.tf-avail-loading { color: #6a7182; font-style: italic; }
.tf-error { font-size: 12px; color: #f6465d; margin-top: 6px; }
.tf-cancel {
  flex: 1; height: 44px; border-radius: 8px;
  background: #2a2d35; color: #c7cce4; font-size: 14px; font-weight: 500;
  transition: background .15s;
}
.tf-cancel:hover { background: #363a45; }
.tf-confirm {
  flex: 1; height: 44px; border-radius: 8px;
  background: #f0b90b; color: #000; font-size: 14px; font-weight: 600;
  transition: opacity .15s;
}
.tf-confirm:hover { opacity: 0.9; }
.tf-cancel:disabled, .tf-confirm:disabled { opacity: 0.5; cursor: not-allowed; }
.modal-enter-active, .modal-leave-active { transition: opacity .2s; }
.modal-enter-from, .modal-leave-to { opacity: 0; }
.modal-enter-active .modal-box, .modal-leave-active .modal-box { transition: transform .2s; }
.modal-enter-from .modal-box, .modal-leave-to .modal-box { transform: scale(0.95); }
</style>
