<script setup lang="ts">
import type { LedgerEntry } from "@cents/domain";
import { ArrowLeft, BarChart3, CalendarDays, ChartNoAxesCombined, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, CircleUserRound, TrendingDown, TrendingUp, WalletCards, X } from "@lucide/vue";
import { computed, onMounted, ref, watch } from "vue";
import { useRoute, useRouter, type LocationQueryRaw } from "vue-router";
import QuickEntryHost from "../components/QuickEntryHost.vue";
import { entryTypes, findCategoryPath } from "../data/categories";
import { ledgerTheme } from "../data/ledgers";
import { isReimbursement, netBaseAmount } from "../data/reimbursements";
import { useEntryStore } from "../stores/entries";
import { useLedgerStore } from "../stores/ledgers";

type StatsTab = "flow" | "category" | "member";
type RangeMode = "month" | "year" | "custom" | "all";
type DailyView = "calendar" | "line" | "bar";
type FlowGranularity = "day" | "month" | "year";

const router = useRouter();
const route = useRoute();
const entryStore = useEntryStore();
const ledgerStore = useLedgerStore();
const now = new Date();

function queryString(value: unknown) {
  return typeof value === "string" ? value : "";
}

function isStatsTab(value: string): value is StatsTab {
  return value === "flow" || value === "category" || value === "member";
}

function isRangeMode(value: string): value is RangeMode {
  return value === "month" || value === "year" || value === "custom" || value === "all";
}

function queryMonth(value: string) {
  return /^\d{4}-(0[1-9]|1[0-2])$/.test(value) ? value : `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
}

function queryYear(value: string) {
  return /^\d{4}$/.test(value) ? Number(value) : now.getFullYear();
}

function queryDate(value: string, fallback: string) {
  return /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : fallback;
}

const tabQuery = queryString(route.query.tab);
const rangeQuery = queryString(route.query.range);
const dailyViewQuery = queryString(route.query.view);
const granularityQuery = queryString(route.query.granularity);
const activeTab = ref<StatsTab>(isStatsTab(tabQuery) ? tabQuery : "flow");
const rangeMode = ref<RangeMode>(isRangeMode(rangeQuery) ? rangeQuery : "month");
const dailyView = ref<DailyView>(dailyViewQuery === "line" || dailyViewQuery === "bar" ? dailyViewQuery : "calendar");
const flowGranularity = ref<FlowGranularity>(granularityQuery === "month" || granularityQuery === "year" ? granularityQuery : "day");
const rangePickerOpen = ref(false);
const rangePickerView = ref<"months" | "years">("months");
const rangePickerYear = ref(now.getFullYear());
const selectedMonth = ref(queryMonth(queryString(route.query.month)));
const selectedYear = ref(queryYear(queryString(route.query.year)));
const customStart = ref(queryDate(queryString(route.query.start), `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`));
const customEnd = ref(queryDate(queryString(route.query.end), toDateInput(now)));
const currentTheme = computed(() => ledgerTheme(ledgerStore.currentLedger?.theme));

function syncStatsStateFromRoute() {
  const tab = queryString(route.query.tab);
  const range = queryString(route.query.range);
  activeTab.value = isStatsTab(tab) ? tab : "flow";
  rangeMode.value = isRangeMode(range) ? range : "month";
  const view = queryString(route.query.view);
  dailyView.value = view === "line" || view === "bar" ? view : "calendar";
  const granularity = queryString(route.query.granularity);
  flowGranularity.value = granularity === "month" || granularity === "year" ? granularity : "day";
  selectedMonth.value = queryMonth(queryString(route.query.month));
  selectedYear.value = queryYear(queryString(route.query.year));
  customStart.value = queryDate(queryString(route.query.start), `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`);
  customEnd.value = queryDate(queryString(route.query.end), toDateInput(now));
}

watch(() => route.query, syncStatsStateFromRoute, { deep: true });

watch(
  [activeTab, rangeMode, dailyView, flowGranularity, selectedMonth, selectedYear, customStart, customEnd],
  () => {
    if (route.name !== "stats") return;

    const query = {
      ...route.query,
      tab: activeTab.value,
      range: rangeMode.value,
      view: dailyView.value,
      granularity: flowGranularity.value,
      month: selectedMonth.value,
      year: String(selectedYear.value),
      start: customStart.value,
      end: customEnd.value,
    };
    const current = [
      queryString(route.query.tab),
      queryString(route.query.range),
      queryString(route.query.view),
      queryString(route.query.granularity),
      queryString(route.query.month),
      queryString(route.query.year),
      queryString(route.query.start),
      queryString(route.query.end),
    ];
    const next = [query.tab, query.range, query.view, query.granularity, query.month, query.year, query.start, query.end];
    if (current.every((value, index) => value === next[index])) return;
    void router.replace({ query });
  },
);

const yearOptions = computed(() => {
  const years = new Set<number>();
  for (let year = now.getFullYear() + 1; year >= now.getFullYear() - 5; year -= 1) years.add(year);
  for (const entry of entryStore.entries) years.add(new Date(entry.occurredAt).getFullYear());
  return [...years].sort((left, right) => right - left);
});

const allRangeBounds = computed(() => {
  const dates = entryStore.entries
    .filter((entry) => entry.ledgerIds.includes(ledgerStore.currentLedgerId))
    .map((entry) => new Date(entry.occurredAt).getTime())
    .filter((value) => Number.isFinite(value));
  if (!dates.length) return null;
  const first = new Date(Math.min(...dates));
  const last = new Date(Math.max(...dates));
  const start = new Date(first.getFullYear(), first.getMonth(), first.getDate());
  const end = new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1);
  return { start, end };
});

const rangeBounds = computed(() => {
  if (rangeMode.value === "all") return allRangeBounds.value;
  if (rangeMode.value === "month") {
    const match = /^(\d{4})-(\d{2})$/.exec(selectedMonth.value);
    if (!match) return null;
    const start = new Date(Number(match[1]), Number(match[2]) - 1, 1);
    return { start, end: new Date(start.getFullYear(), start.getMonth() + 1, 1) };
  }
  if (rangeMode.value === "year") {
    return { start: new Date(selectedYear.value, 0, 1), end: new Date(selectedYear.value + 1, 0, 1) };
  }
  const start = fromDateInput(customStart.value);
  const selectedEnd = fromDateInput(customEnd.value);
  if (!start || !selectedEnd || start > selectedEnd) return null;
  const end = new Date(selectedEnd);
  end.setDate(end.getDate() + 1);
  return { start, end };
});

const rangeLabel = computed(() => {
  if (rangeMode.value === "month") {
    const match = /^(\d{4})-(\d{2})$/.exec(selectedMonth.value);
    return match ? `${match[1]}年${Number(match[2])}月` : "所选月份";
  }
  if (rangeMode.value === "year") return `${selectedYear.value}年`;
  if (rangeMode.value === "all" && rangeBounds.value) {
    return `${formatFullDateLabel(toDateInput(rangeBounds.value.start))} 至 ${formatFullDateLabel(toDateInput(new Date(rangeBounds.value.end.getTime() - 86400000)))}`;
  }
  if (!rangeBounds.value) return "自定义日期";
  return `${formatDateLabel(customStart.value)} 至 ${formatDateLabel(customEnd.value)}`;
});

const entries = computed(() => {
  const bounds = rangeBounds.value;
  if (!bounds) return [];
  return entryStore.entries.filter((entry) => {
    const occurredAt = new Date(entry.occurredAt);
    return entry.ledgerIds.includes(ledgerStore.currentLedgerId)
      && occurredAt >= bounds.start
      && occurredAt < bounds.end;
  });
});

const totals = computed(() => summarize(entries.value));
const statsEntries = computed(() => entries.value.filter((entry) => !isReimbursement(entry)));
const convertedEntries = computed(() => statsEntries.value.filter((entry) => entry.baseAmount !== null));
const pendingConversionCount = computed(() => statsEntries.value.length - convertedEntries.value.length);
const calendarCursorMonth = ref(selectedMonth.value);
const calendarCursorYear = ref(selectedYear.value);

watch([rangeMode, selectedMonth, selectedYear, customStart, customEnd], () => {
  const bounds = rangeBounds.value;
  if (!bounds) return;
  calendarCursorMonth.value = `${bounds.start.getFullYear()}-${String(bounds.start.getMonth() + 1).padStart(2, "0")}`;
  calendarCursorYear.value = bounds.start.getFullYear();
}, { immediate: true });

const dailyStats = computed(() => {
  const groups = new Map<string, LedgerEntry[]>();
  for (const entry of convertedEntries.value) {
    const date = new Date(entry.occurredAt);
    const key = toDateInput(date);
    groups.set(key, [...(groups.get(key) ?? []), entry]);
  }
  return Array.from(groups, ([date, items]) => ({ date, label: dailyLabel(date), ...summarize(items) }))
    .sort((left, right) => right.date.localeCompare(left.date));
});

const maxDailyAmount = computed(() =>
  Math.max(1, ...dailyStats.value.flatMap((day) => [day.income, day.expense])),
);

const flowStats = computed(() => {
  if (flowGranularity.value === "day") return dailyStats.value;
  const groups = new Map<string, LedgerEntry[]>();
  for (const entry of convertedEntries.value) {
    const date = new Date(entry.occurredAt);
    const key = flowGranularity.value === "month"
      ? `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`
      : String(date.getFullYear());
    groups.set(key, [...(groups.get(key) ?? []), entry]);
  }
  return [...groups.entries()]
    .map(([date, items]) => ({ date, label: flowGranularity.value === "month" ? date.replace("-", "/") : `${date}年`, ...summarize(items) }))
    .sort((left, right) => right.date.localeCompare(left.date));
});

const calendarMonthStart = computed(() => {
  const bounds = rangeBounds.value;
  if (!bounds) return null;
  const match = /^(\d{4})-(\d{2})$/.exec(calendarCursorMonth.value);
  const candidate = match ? new Date(Number(match[1]), Number(match[2]) - 1, 1) : bounds.start;
  const first = new Date(bounds.start.getFullYear(), bounds.start.getMonth(), 1);
  const last = new Date(bounds.end.getFullYear(), bounds.end.getMonth(), 1);
  return candidate < first ? first : candidate >= last ? new Date(last.getFullYear(), last.getMonth() - 1, 1) : candidate;
});

const calendarMonthLabel = computed(() => {
  const start = calendarMonthStart.value;
  return start ? `${start.getFullYear()}年${start.getMonth() + 1}月` : "暂无日期";
});

const calendarDays = computed(() => {
  const start = calendarMonthStart.value;
  if (!start) return [];
  const summary = new Map(dailyStats.value.map((day) => [day.date, day]));
  const daysInMonth = new Date(start.getFullYear(), start.getMonth() + 1, 0).getDate();
  const cells: Array<{ date: string; day: number; income: number; expense: number } | null> = Array.from({ length: start.getDay() }, () => null);
  for (let day = 1; day <= daysInMonth; day += 1) {
    const date = toDateInput(new Date(start.getFullYear(), start.getMonth(), day));
    const item = summary.get(date);
    cells.push({ date, day, income: item?.income ?? 0, expense: item?.expense ?? 0 });
  }
  return cells;
});

const calendarMonthCells = computed(() => {
  if (!rangeBounds.value) return [];
  const summary = new Map(flowStats.value.map((item) => [item.date, item]));
  const cells: Array<{ date: string; label: string; income: number; expense: number }> = [];
  for (let month = 0; month < 12; month += 1) {
    const cursor = new Date(calendarCursorYear.value, month, 1);
    const date = `${cursor.getFullYear()}-${String(cursor.getMonth() + 1).padStart(2, "0")}`;
    const item = summary.get(date);
    cells.push({ date, label: `${cursor.getFullYear()}年${cursor.getMonth() + 1}月`, income: item?.income ?? 0, expense: item?.expense ?? 0 });
  }
  return cells;
});

const calendarYearCells = computed(() => {
  const bounds = rangeBounds.value;
  if (!bounds) return [];
  const summary = new Map(flowStats.value.map((item) => [item.date, item]));
  const cells: Array<{ date: string; label: string; income: number; expense: number }> = [];
  for (let year = bounds.start.getFullYear(); year <= new Date(bounds.end.getTime() - 1).getFullYear(); year += 1) {
    const date = String(year);
    const item = summary.get(date);
    cells.push({ date, label: `${year}年`, income: item?.income ?? 0, expense: item?.expense ?? 0 });
  }
  return cells;
});

const lineStats = computed(() => [...flowStats.value].reverse());
const lineMaxAmount = computed(() => Math.max(1, ...lineStats.value.flatMap((day) => [day.income, day.expense])));

function linePoints(field: "income" | "expense") {
  const values = lineStats.value;
  if (!values.length) return "";
  return values.map((day, index) => {
    const x = values.length === 1 ? 168 : 32 + (index / (values.length - 1)) * 272;
    const y = 154 - (day[field] / lineMaxAmount.value) * 126;
    return `${x},${y}`;
  }).join(" ");
}

function chartPoints(field: "income" | "expense") {
  return lineStats.value.map((day, index) => {
    const x = lineStats.value.length === 1 ? 168 : 32 + (index / (lineStats.value.length - 1)) * 272;
    const y = 154 - (day[field] / lineMaxAmount.value) * 126;
    return { x, y, key: `${field}-${day.date}` };
  });
}

function chartBarWidth() {
  return Math.max(2, Math.min(18, 240 / Math.max(1, lineStats.value.length)));
}

function chartBarX(index: number) {
  const width = chartBarWidth();
  const step = lineStats.value.length === 1 ? 0 : 272 / (lineStats.value.length - 1);
  const center = lineStats.value.length === 1 ? 168 : 32 + index * step;
  return center - width / 2;
}

function chartBarY(value: number) {
  return 91 - (value / lineMaxAmount.value) * 63;
}

function chartBarHeight(value: number) {
  return (value / lineMaxAmount.value) * 63;
}

const chartHoverIndex = ref<number | null>(null);
const hoveredChartPoint = computed(() => {
  const index = chartHoverIndex.value;
  const item = index === null ? null : lineStats.value[index];
  if (!item || index === null) return null;
  const x = lineStats.value.length === 1 ? 168 : 32 + (index / (lineStats.value.length - 1)) * 272;
  return { ...item, x };
});

const chartTooltipStyle = computed(() => {
  if (!hoveredChartPoint.value) return undefined;
  const percentage = (hoveredChartPoint.value.x / 320) * 100;
  return { left: `${Math.max(24, Math.min(76, percentage))}%` };
});

function updateChartHover(event: PointerEvent) {
  const target = event.currentTarget as SVGElement | null;
  if (!target || !lineStats.value.length) return;
  const rect = target.getBoundingClientRect();
  const viewBoxX = ((event.clientX - rect.left) / rect.width) * 320;
  const normalizedX = Math.max(32, Math.min(304, viewBoxX));
  const index = lineStats.value.length === 1
    ? 0
    : Math.round(((normalizedX - 32) / 272) * (lineStats.value.length - 1));
  chartHoverIndex.value = Math.max(0, Math.min(lineStats.value.length - 1, index));
}

function clearChartHover(event: PointerEvent) {
  if (event.pointerType !== "touch") chartHoverIndex.value = null;
}

type CategorySummaryNode = {
  key: string;
  label: string;
  color: string;
  tint: string;
  icon: typeof entryTypes[number]["icon"];
  amount: number;
  count: number;
  children: CategorySummaryNode[];
};

const expandedCategoryKeys = ref(new Set<string>());

const categoryTree = computed(() => {
  const roots = new Map<string, CategorySummaryNode>();
  for (const entry of convertedEntries.value) {
    const amount = entry.type === "expense" ? netBaseAmount(entry, entryStore.entries) ?? 0 : entry.baseAmount!;
    const type = entryTypes.find((item) => item.id === entry.type);
    if (!type) continue;
    const root = roots.get(entry.type) ?? {
      key: `${entry.type}:*`,
      label: type.label,
      color: type.color,
      tint: type.tint,
      icon: type.icon,
      amount: 0,
      count: 0,
      children: [],
    };
    root.amount += amount;
    root.count += 1;

    const path = findCategoryPath(entry.type, entry.categoryId);
    const groupCategory = path[0] ?? {
      id: "uncategorized",
      label: "未分类",
      color: type.color,
      tint: type.tint,
      icon: WalletCards,
    };
    let group = root.children.find((item) => item.key === `${entry.type}:${groupCategory.id}`);
    if (!group) {
      group = {
        key: `${entry.type}:${groupCategory.id}`,
        label: groupCategory.label,
        color: groupCategory.color,
        tint: groupCategory.tint,
        icon: groupCategory.icon,
        amount: 0,
        count: 0,
        children: [],
      };
      root.children.push(group);
    }
    group.amount += amount;
    group.count += 1;

    const leafCategory = path[1];
    if (leafCategory) {
      let leaf = group.children.find((item) => item.key === `${entry.type}:${groupCategory.id}:${leafCategory.id}`);
      if (!leaf) {
        leaf = {
          key: `${entry.type}:${groupCategory.id}:${leafCategory.id}`,
          label: leafCategory.label,
          color: leafCategory.color,
          tint: leafCategory.tint,
          icon: leafCategory.icon,
          amount: 0,
          count: 0,
          children: [],
        };
        group.children.push(leaf);
      }
      leaf.amount += amount;
      leaf.count += 1;
    }
    roots.set(entry.type, root);
  }
  return [...roots.values()];
});

const categoryRows = computed(() => {
  const rows: Array<CategorySummaryNode & { level: number; expanded: boolean }> = [];
  const expanded = expandedCategoryKeys.value;
  const append = (node: CategorySummaryNode, level: number) => {
    const isExpanded = expanded.has(node.key);
    rows.push({ ...node, level, expanded: isExpanded });
    if (isExpanded) for (const child of node.children) append(child, level + 1);
  };
  for (const root of categoryTree.value) append(root, 0);
  return rows;
});

const maxCategoryAmount = computed(() => Math.max(1, ...categoryTree.value.map((item) => item.amount)));

function toggleCategoryNode(node: CategorySummaryNode) {
  if (!node.children.length) return;
  const next = new Set(expandedCategoryKeys.value);
  if (next.has(node.key)) next.delete(node.key);
  else next.add(node.key);
  expandedCategoryKeys.value = next;
}

function openCategoryEntries(node: CategorySummaryNode) {
  const { tab: _tab, category: _category, ...restQuery } = route.query;
  const query: LocationQueryRaw = { ...restQuery, category: node.key };
  query.ledgers = ledgerStore.currentLedgerId;
  query.cat = node.key;
  void router.push({ name: "query", query });
}

const memberStats = computed(() => {
  const groups = new Map<string, LedgerEntry[]>();
  for (const entry of convertedEntries.value) groups.set(entry.createdBy, [...(groups.get(entry.createdBy) ?? []), entry]);
  return Array.from(groups, ([userId, items]) => ({
    userId,
    name: ledgerStore.memberName(userId),
    count: items.length,
    ...summarize(items),
  })).sort((left, right) => right.count - left.count);
});

onMounted(async () => {
  await Promise.all([entryStore.loadEntries(), ledgerStore.loadLedgers()]);
});

function summarize(items: LedgerEntry[]) {
  return items.reduce(
    (result, entry) => {
      if (entry.baseAmount === null) return result;
      if (isReimbursement(entry)) return result;
      if (entry.type === "income") result.income += entry.baseAmount;
      else result.expense += netBaseAmount(entry, entryStore.entries) ?? 0;
      return result;
    },
    { income: 0, expense: 0 },
  );
}

function money(value: number) {
  return (value / 100).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

function compactDailyAmount(value: number) {
  const amount = value / 100;
  if (amount >= 10000) return `${(amount / 10000).toFixed(1)}万`;
  if (amount >= 1000) return `${(amount / 1000).toFixed(1)}k`;
  return amount.toFixed(0);
}

function toDateInput(date: Date) {
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
}

function fromDateInput(value: string) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return null;
  const date = new Date(`${value}T00:00:00`);
  return Number.isNaN(date.getTime()) ? null : date;
}

function formatDateLabel(value: string) {
  const date = fromDateInput(value);
  return date ? `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}` : value;
}

function formatFullDateLabel(value: string) {
  const date = fromDateInput(value);
  return date ? `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日` : value;
}

function dailyLabel(value: string) {
  const date = fromDateInput(value);
  if (!date) return value;
  const crossesYear = rangeBounds.value && rangeBounds.value.start.getFullYear() !== new Date(rangeBounds.value.end.getTime() - 1).getFullYear();
  return crossesYear ? `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}` : `${date.getMonth() + 1}/${date.getDate()}`;
}

function openRangePicker() {
  const match = /^(\d{4})-(\d{2})$/.exec(selectedMonth.value);
  rangePickerYear.value = match ? Number(match[1]) : selectedYear.value;
  rangePickerView.value = rangeMode.value === "year" ? "years" : "months";
  rangePickerOpen.value = true;
}

function closeRangePicker() {
  rangePickerOpen.value = false;
}

function chooseRangeMode(mode: RangeMode) {
  rangeMode.value = mode;
  rangePickerView.value = mode === "year" ? "years" : "months";
}

function selectStatsMonth(month: number) {
  selectedMonth.value = `${rangePickerYear.value}-${String(month + 1).padStart(2, "0")}`;
  rangeMode.value = "month";
  closeRangePicker();
}

function isSelectedStatsMonth(month: number) {
  return selectedMonth.value === `${rangePickerYear.value}-${String(month).padStart(2, "0")}`;
}

function selectStatsYear(year: number) {
  selectedYear.value = year;
  rangeMode.value = "year";
  closeRangePicker();
}

function shiftRangePickerYear(offset: -1 | 1) {
  const years = yearOptions.value;
  const index = years.indexOf(rangePickerYear.value);
  const target = years[index + (offset < 0 ? 1 : -1)];
  if (target !== undefined) rangePickerYear.value = target;
}

function shiftCalendarMonth(offset: -1 | 1) {
  const match = /^(\d{4})-(\d{2})$/.exec(calendarCursorMonth.value);
  if (!match) return;
  const next = new Date(Number(match[1]), Number(match[2]) - 1 + offset, 1);
  const bounds = rangeBounds.value;
  if (!bounds) return;
  const first = new Date(bounds.start.getFullYear(), bounds.start.getMonth(), 1);
  const last = new Date(bounds.end.getFullYear(), bounds.end.getMonth(), 1);
  if (next < first || next >= last) return;
  calendarCursorMonth.value = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, "0")}`;
}

function shiftCalendarYear(offset: -1 | 1) {
  const bounds = rangeBounds.value;
  if (!bounds) return;
  const firstYear = bounds.start.getFullYear();
  const lastYear = new Date(bounds.end.getTime() - 1).getFullYear();
  const nextYear = calendarCursorYear.value + offset;
  if (nextYear < firstYear || nextYear > lastYear) return;
  calendarCursorYear.value = nextYear;
}
</script>

<template>
  <main class="app-shell stats-shell">
    <header class="stats-header" :style="{ '--ledger-gradient': currentTheme.gradient }">
      <button class="stats-back-button" type="button" aria-label="返回" title="返回" @click="router.back()"><ArrowLeft :size="20" /></button>
      <strong>统计</strong>
      <button type="button" @click="router.push({ path: '/ledgers', query: { returnTo: route.fullPath } })">
        {{ ledgerStore.currentLedger?.name ?? "选择账本" }}
        <ChevronDown :size="15" />
      </button>
    </header>

    <Transition name="month-picker">
      <div v-if="rangePickerOpen" class="month-picker-layer">
        <button class="month-picker-scrim" type="button" aria-label="关闭时间范围选择" @click="closeRangePicker"></button>
        <section class="month-picker-modal stats-range-modal" role="dialog" aria-modal="true" aria-label="选择统计时间范围">
          <header>
            <button v-if="rangePickerView === 'years'" type="button" aria-label="返回月份选择" title="返回月份选择" @click="rangePickerView = 'months'"><ChevronLeft :size="20" /></button>
            <span v-else></span>
            <strong>统计时间范围</strong>
            <button type="button" aria-label="关闭" title="关闭" @click="closeRangePicker"><X :size="20" /></button>
          </header>
          <div class="stats-range-modes" role="group" aria-label="时间范围类型">
            <button type="button" :class="{ active: rangeMode === 'month' }" @click="chooseRangeMode('month')">月</button>
            <button type="button" :class="{ active: rangeMode === 'year' }" @click="chooseRangeMode('year')">年</button>
            <button type="button" :class="{ active: rangeMode === 'custom' }" @click="chooseRangeMode('custom')">自定义</button>
            <button type="button" :class="{ active: rangeMode === 'all' }" @click="chooseRangeMode('all')">全部</button>
          </div>

          <template v-if="rangeMode === 'month' && rangePickerView === 'months'">
            <div class="stats-picker-year-nav">
              <button type="button" aria-label="上一年" title="上一年" @click="shiftRangePickerYear(-1)"><ChevronLeft :size="18" /></button>
              <button type="button" class="selected" @click="rangePickerView = 'years'">{{ rangePickerYear }}</button>
              <button type="button" aria-label="下一年" title="下一年" @click="shiftRangePickerYear(1)"><ChevronDown :size="16" /></button>
            </div>
            <div class="stats-month-grid">
              <button v-for="month in 12" :key="month" type="button" :class="{ selected: isSelectedStatsMonth(month) }" @click="selectStatsMonth(month - 1)">{{ month }}月</button>
            </div>
          </template>

          <div v-else-if="rangeMode === 'year'" class="stats-year-list">
            <button v-for="year in yearOptions" :key="year" type="button" :class="{ selected: selectedYear === year }" @click="selectStatsYear(year)">{{ year }}年</button>
          </div>

          <div v-else-if="rangeMode === 'all'" class="stats-all-range"><span>{{ rangeLabel }}</span></div>

          <div v-else class="stats-custom-range stats-custom-modal">
            <label><span>开始日期</span><input v-model="customStart" type="date" aria-label="开始日期" :max="customEnd || undefined" /></label>
            <label><span>结束日期</span><input v-model="customEnd" type="date" aria-label="结束日期" :min="customStart || undefined" /></label>
            <p v-if="!rangeBounds" role="alert">开始日期不能晚于结束日期</p>
            <button type="button" class="stats-range-confirm" :disabled="!rangeBounds" @click="closeRangePicker">完成</button>
          </div>
        </section>
      </div>
    </Transition>

    <div class="stats-scroll">
      <section class="stats-range" aria-label="统计时间范围">
        <button class="stats-range-trigger" type="button" @click="openRangePicker">
          <span>{{ rangeLabel }}</span>
          <ChevronDown :size="16" />
        </button>
      </section>

      <section class="stats-summary" :aria-label="`${rangeLabel}统计`">
        <div class="balance">
          <span>结余</span>
          <strong>¥ {{ money(totals.income - totals.expense) }}</strong>
        </div>
        <div class="income"><span><TrendingUp :size="17" />收入</span><strong>{{ money(totals.income) }}</strong></div>
        <div class="expense"><span><TrendingDown :size="17" />支出</span><strong>{{ money(totals.expense) }}</strong></div>
      </section>
      <div class="stats-conversion-slot" aria-live="polite">
        <p v-if="pendingConversionCount" class="stats-conversion-notice" role="status">
          {{ pendingConversionCount }} 条外币账目等待换算，暂未计入人民币统计
        </p>
      </div>

      <div class="stats-tabs" role="tablist" aria-label="统计维度">
        <button type="button" :class="{ active: activeTab === 'flow' }" @click="activeTab = 'flow'">收支</button>
        <button type="button" :class="{ active: activeTab === 'category' }" @click="activeTab = 'category'">分类</button>
        <button type="button" :class="{ active: activeTab === 'member' }" @click="activeTab = 'member'">记录者</button>
      </div>

      <section v-if="activeTab === 'flow'" class="stats-section" aria-label="每日收支">
        <header><strong>收支趋势</strong><div class="daily-controls"><div class="daily-granularity-switch" role="group" aria-label="统计粒度"><button type="button" :class="{ active: flowGranularity === 'day' }" @click="flowGranularity = 'day'">日</button><button type="button" :class="{ active: flowGranularity === 'month' }" @click="flowGranularity = 'month'">月</button><button type="button" :class="{ active: flowGranularity === 'year' }" @click="flowGranularity = 'year'">年</button></div><div class="daily-view-switch" role="group" aria-label="收支视图"><button type="button" :class="{ active: dailyView === 'calendar' }" aria-label="日历视图" title="日历视图" @click="dailyView = 'calendar'"><CalendarDays :size="15" /></button><button type="button" :class="{ active: dailyView === 'line' }" aria-label="折线图视图" title="折线图视图" @click="dailyView = 'line'"><ChartNoAxesCombined :size="15" /></button><button type="button" :class="{ active: dailyView === 'bar' }" aria-label="柱状图视图" title="柱状图视图" @click="dailyView = 'bar'"><BarChart3 :size="15" /></button></div></div></header>
        <div v-if="dailyView === 'calendar'" class="daily-calendar">
          <template v-if="flowGranularity === 'day'">
            <div class="calendar-month-nav"><button type="button" aria-label="上个月" title="上个月" :disabled="calendarMonthStart?.getTime() === new Date(rangeBounds?.start.getFullYear() ?? 0, rangeBounds?.start.getMonth() ?? 0, 1).getTime()" @click="shiftCalendarMonth(-1)"><ChevronLeft :size="17" /></button><strong>{{ calendarMonthLabel }}</strong><button type="button" aria-label="下个月" title="下个月" :disabled="calendarMonthStart?.getMonth() === new Date(rangeBounds?.end.getFullYear() ?? 0, rangeBounds?.end.getMonth() ?? 0, 1).getMonth() && calendarMonthStart?.getFullYear() === new Date(rangeBounds?.end.getTime() ?? 0).getFullYear()" @click="shiftCalendarMonth(1)"><ChevronRight :size="17" /></button></div>
            <div class="calendar-weekdays"><span v-for="weekday in ['日','一','二','三','四','五','六']" :key="weekday">{{ weekday }}</span></div>
            <div class="calendar-grid">
              <span v-for="(cell, index) in calendarDays" :key="cell?.date ?? `blank-${index}`" class="calendar-cell" :class="{ blank: !cell }">
                <template v-if="cell"><b>{{ cell.day }}</b><i class="income" :style="{ width: `${Math.min(100, (cell.income / maxDailyAmount) * 100)}%` }"></i><i class="expense" :style="{ width: `${Math.min(100, (cell.expense / maxDailyAmount) * 100)}%` }"></i><small v-if="cell.income || cell.expense">{{ compactDailyAmount(cell.income || cell.expense) }}</small></template>
              </span>
            </div>
          </template>
          <template v-else-if="flowGranularity === 'month'">
            <div class="calendar-month-nav"><button type="button" aria-label="上一年" title="上一年" :disabled="calendarCursorYear <= (rangeBounds?.start.getFullYear() ?? calendarCursorYear)" @click="shiftCalendarYear(-1)"><ChevronLeft :size="17" /></button><strong>{{ calendarCursorYear }}年</strong><button type="button" aria-label="下一年" title="下一年" :disabled="calendarCursorYear >= (rangeBounds ? new Date(rangeBounds.end.getTime() - 1).getFullYear() : calendarCursorYear)" @click="shiftCalendarYear(1)"><ChevronRight :size="17" /></button></div>
            <div class="period-calendar-grid">
              <span v-for="cell in calendarMonthCells" :key="cell.date" class="period-calendar-cell" :class="{ muted: !cell.income && !cell.expense }">
                <b>{{ cell.label.replace(`${calendarCursorYear}年`, '') }}</b><i class="income" :style="{ width: `${Math.min(100, (cell.income / lineMaxAmount) * 100)}%` }"></i><i class="expense" :style="{ width: `${Math.min(100, (cell.expense / lineMaxAmount) * 100)}%` }"></i><small v-if="cell.income || cell.expense">{{ compactDailyAmount(cell.income || cell.expense) }}</small>
              </span>
            </div>
          </template>
          <div v-else class="period-calendar-grid">
            <span v-for="cell in calendarYearCells" :key="cell.date" class="period-calendar-cell" :class="{ muted: !cell.income && !cell.expense }">
              <b>{{ cell.label }}</b><i class="income" :style="{ width: `${Math.min(100, (cell.income / lineMaxAmount) * 100)}%` }"></i><i class="expense" :style="{ width: `${Math.min(100, (cell.expense / lineMaxAmount) * 100)}%` }"></i><small v-if="cell.income || cell.expense">{{ compactDailyAmount(cell.income || cell.expense) }}</small>
            </span>
          </div>
          <div class="daily-view-legend"><span class="income">收入</span><span class="expense">支出</span></div>
        </div>
        <div v-else-if="dailyView === 'line'" class="daily-line-chart">
          <div v-if="lineStats.length" class="line-chart-wrap">
            <div v-if="hoveredChartPoint" class="chart-tooltip" :style="chartTooltipStyle">
              <strong>{{ hoveredChartPoint.label }}</strong>
              <span><i class="income">收入</i><b>¥ {{ money(hoveredChartPoint.income) }}</b></span>
              <span><i class="expense">支出</i><b>¥ {{ money(hoveredChartPoint.expense) }}</b></span>
              <small>结余 ¥ {{ money(hoveredChartPoint.income - hoveredChartPoint.expense) }}</small>
            </div>
            <svg viewBox="0 0 320 190" role="img" aria-label="收支折线图" @pointermove="updateChartHover" @pointerdown="updateChartHover" @pointerleave="clearChartHover" @pointercancel="clearChartHover">
              <line x1="32" y1="28" x2="32" y2="154" class="chart-axis-line" /><line x1="32" y1="154" x2="304" y2="154" class="chart-axis-line" />
              <line v-for="y in [28,70,112,154]" :key="y" x1="32" :y1="y" x2="304" :y2="y" class="chart-grid-line" />
              <text v-for="(tick, index) in [1, .66, .33, 0]" :key="tick" x="0" :y="[28,70,112,154][index] + 3" class="chart-axis-label">{{ compactDailyAmount(lineMaxAmount * tick) }}</text>
              <line v-if="hoveredChartPoint" :x1="hoveredChartPoint.x" y1="28" :x2="hoveredChartPoint.x" y2="154" class="chart-crosshair" />
              <polyline :points="linePoints('income')" class="chart-line income-line" /><polyline :points="linePoints('expense')" class="chart-line expense-line" />
              <circle v-for="(point, index) in chartPoints('income')" :key="point.key" :cx="point.x" :cy="point.y" :r="chartHoverIndex === index ? 5 : 2.5" class="chart-dot income-dot" :class="{ active: chartHoverIndex === index }" />
              <circle v-for="(point, index) in chartPoints('expense')" :key="point.key" :cx="point.x" :cy="point.y" :r="chartHoverIndex === index ? 5 : 2.5" class="chart-dot expense-dot" :class="{ active: chartHoverIndex === index }" />
              <rect x="32" y="28" width="272" height="126" class="chart-hit-area" aria-label="查看详细数据" />
            </svg>
            <div class="line-chart-labels"><span>{{ lineStats[0]?.label }}</span><span>{{ lineStats.at(-1)?.label }}</span></div>
          </div><div v-else class="daily-view-empty">暂无可绘制的流水</div>
          <div class="daily-view-legend"><span class="income">收入</span><span class="expense">支出</span></div>
        </div>
        <div v-else class="daily-line-chart">
          <div v-if="lineStats.length" class="line-chart-wrap">
            <div v-if="hoveredChartPoint" class="chart-tooltip" :style="chartTooltipStyle">
              <strong>{{ hoveredChartPoint.label }}</strong>
              <span><i class="income">收入</i><b>¥ {{ money(hoveredChartPoint.income) }}</b></span>
              <span><i class="expense">支出</i><b>¥ {{ money(hoveredChartPoint.expense) }}</b></span>
              <small>结余 ¥ {{ money(hoveredChartPoint.income - hoveredChartPoint.expense) }}</small>
            </div>
            <svg viewBox="0 0 320 190" role="img" aria-label="收支柱状图" @pointermove="updateChartHover" @pointerdown="updateChartHover" @pointerleave="clearChartHover" @pointercancel="clearChartHover">
              <line x1="32" y1="28" x2="32" y2="154" class="chart-axis-line" /><line x1="32" y1="91" x2="304" y2="91" class="chart-zero-line" />
              <line v-for="y in [28,60,91,122,154]" :key="y" x1="32" :y1="y" x2="304" :y2="y" class="chart-grid-line" />
              <text v-for="(tick, index) in [1, .5, 0, -.5, -1]" :key="tick" x="0" :y="[28,60,91,122,154][index] + 3" class="chart-axis-label">{{ tick < 0 ? '-' : '' }}{{ compactDailyAmount(lineMaxAmount * Math.abs(tick)) }}</text>
              <line v-if="hoveredChartPoint" :x1="hoveredChartPoint.x" y1="28" :x2="hoveredChartPoint.x" y2="154" class="chart-crosshair" />
              <rect v-for="(item, index) in lineStats" :key="`income-${item.date}`" :x="chartBarX(index)" :y="chartBarY(item.income)" :width="chartBarWidth()" :height="chartBarHeight(item.income)" rx="1.5" class="bar income-bar" :class="{ active: chartHoverIndex === index }" />
              <rect v-for="(item, index) in lineStats" :key="`expense-${item.date}`" :x="chartBarX(index)" y="91" :width="chartBarWidth()" :height="chartBarHeight(item.expense)" rx="1.5" class="bar expense-bar" :class="{ active: chartHoverIndex === index }" />
              <rect x="32" y="28" width="272" height="126" class="chart-hit-area" aria-label="查看详细数据" />
            </svg>
            <div class="line-chart-labels"><span>{{ lineStats[0]?.label }}</span><span>{{ lineStats.at(-1)?.label }}</span></div>
          </div><div v-else class="daily-view-empty">暂无可绘制的流水</div>
          <div class="daily-view-legend"><span class="income">收入</span><span class="expense">支出</span></div>
        </div>
      </section>

      <section v-else-if="activeTab === 'category'" class="stats-section" aria-label="分类统计">
        <header><strong>分类金额</strong><span>收入 / 支出</span></header>
        <div v-for="item in categoryRows" :key="item.key" class="category-stat-row" :class="[`level-${item.level}`, { expandable: item.children.length, expanded: item.expanded }]" :style="{ '--category-indent': `${item.level * 18}px` }" role="button" tabindex="0" @click="toggleCategoryNode(item)" @keydown.enter="toggleCategoryNode(item)">
          <span class="category-stat-expand-indicator" aria-hidden="true"><ChevronUp v-if="item.children.length && item.expanded" :size="15" /><ChevronDown v-else-if="item.children.length" :size="15" /></span>
          <span
            class="category-stat-icon"
            :style="{ color: item.color, background: item.tint }"
          >
            <component :is="item.icon" :size="19" />
          </span>
          <div>
            <strong>{{ item.label }}</strong>
            <i :style="{ width: `${(item.amount / maxCategoryAmount) * 100}%`, background: item.color }"></i>
          </div>
          <span><strong>{{ money(item.amount) }}</strong><small>{{ item.count }} 笔</small></span>
          <button class="category-stat-filter" type="button" aria-label="查看该分类流水" title="查看该分类流水" @click.stop="openCategoryEntries(item)"><ChevronRight :size="16" /></button>
        </div>
      </section>

      <section v-else class="stats-section" aria-label="记录者统计">
        <header><strong>记录者</strong><span>{{ memberStats.length }} 人</span></header>
        <div v-for="member in memberStats" :key="member.userId" class="member-stat-row">
          <span><CircleUserRound :size="22" /></span>
          <div><strong>{{ member.name }}</strong><small>{{ member.count }} 笔记录</small></div>
          <div><small class="income">收入 {{ money(member.income) }}</small><small class="expense">支出 {{ money(member.expense) }}</small></div>
        </div>
      </section>

      <div v-if="rangeBounds && !entries.length" class="stats-empty">{{ rangeLabel }}还没有流水</div>
    </div>

    <QuickEntryHost />
  </main>
</template>

<style scoped>
.stats-shell { background: #f5f8f7; }
.stats-header { height:74px; display:grid; grid-template-columns:40px 1fr auto; align-items:center; gap:8px; padding:14px 18px 8px; background:var(--ledger-gradient, #087f72); color:#fff; }
.stats-back-button { width:38px; height:38px; display:grid; place-items:center; border:0; border-radius:8px; background:rgba(255,255,255,.14); color:#fff; }
.stats-header > strong { font-size: 20px; }
.stats-header button { display: flex; align-items: center; gap: 5px; border: 0; border-radius: 7px; padding: 7px 9px; background: rgba(255,255,255,.14); color: #fff; font-size: 13px; }
.stats-scroll { position:relative; height: calc(100% - 74px); overflow-y: auto; padding: 0 16px 104px; }
.stats-range { display:grid; gap:10px; padding:12px 0; }
.stats-range-trigger { width:100%; min-height:44px; display:flex; align-items:center; justify-content:space-between; border:1px solid #d2dfdc; border-radius:8px; padding:0 12px; background:#fff; color:#344640; font-size:14px; text-align:left; }
.stats-range-modal .stats-range-modes { margin:12px 0; }
.stats-picker-year-nav { display:grid; grid-template-columns:38px 1fr 38px; align-items:center; gap:4px; min-height:52px; }
.stats-picker-year-nav button { min-height:38px; display:grid; place-items:center; border:0; border-radius:8px; background:transparent; color:#536762; font-size:19px; font-variant-numeric:tabular-nums; }
.stats-picker-year-nav button.selected { background:#eaf7f3; color:#087f72; font-weight:720; }
.stats-month-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; }
.stats-month-grid button { min-height:52px; border:1px solid #e1ebe8; border-radius:8px; background:#fff; color:#344640; font-size:14px; }
.stats-month-grid button.selected { border-color:#087f72; background:#eaf7f3; color:#087f72; box-shadow:inset 0 0 0 1px #087f72; font-weight:720; }
.stats-year-list { display:grid; gap:2px; padding-top:8px; }
.stats-year-list button { min-height:54px; border:0; border-bottom:1px solid #e5edeb; background:#fff; color:#344640; font-size:15px; text-align:left; }
.stats-year-list button.selected { color:#087f72; font-weight:720; }
.stats-custom-modal { margin-top:12px; }
.stats-all-range { margin-top:12px; border:1px solid #d2dfdc; border-radius:8px; padding:11px 10px; background:#f8fbfa; color:#536762; font-size:12px; text-align:center; }
.stats-range-confirm { grid-column:1 / -1; min-height:44px; border:0; border-radius:8px; background:#087f72; color:#fff; font-weight:720; }
.stats-range-confirm:disabled { opacity:.5; }
.stats-range-modes { display:grid; grid-template-columns:repeat(4,1fr); gap:4px; border-radius:8px; padding:4px; background:#e7eeec; }
.stats-range-modes button { height:34px; border:0; border-radius:6px; background:transparent; color:#71807c; font-size:12px; font-weight:680; }
.stats-range-modes button.active { background:#fff; color:#087f72; box-shadow:0 2px 7px rgba(28,52,47,.1); }
.stats-range > input,.stats-range > select,.stats-custom-range input { width:100%; min-width:0; height:40px; border:1px solid #d2dfdc; border-radius:7px; padding:0 10px; background:#fff; color:#344640; font-size:13px; }
.stats-custom-range { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:8px; }
.stats-custom-range label { min-width:0; display:grid; gap:4px; }
.stats-custom-range label > span { color:#7b8884; font-size:10px; }
.stats-range > p { margin:0; color:#c74d39; font-size:11px; }
.stats-summary { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); border-top:1px solid #dce7e4; border-bottom:1px solid #dce7e4; background:#fff; }
.stats-summary > div { min-width:0; display:grid; gap:5px; padding:12px; }
.stats-summary .balance { grid-column:1 / -1; border-bottom:1px solid #e1ebe8; padding-top:15px; padding-bottom:15px; }
.stats-summary > div:last-child { border-left:1px solid #e1ebe8; }
.stats-summary span { display:flex; align-items:center; gap:5px; color:#788581; font-size:11px; }
.stats-summary strong { min-width:0; font-size:15px; line-height:1.25; overflow-wrap:anywhere; font-variant-numeric:tabular-nums; }
.stats-summary .balance strong { font-size:24px; }
.stats-summary .income { color: #0d8b67; }
.stats-summary .expense { color: #d84c36; }
.stats-conversion-slot { position:absolute; top:0; right:16px; left:16px; z-index:3; height:0; overflow:visible; pointer-events:none; }
.stats-conversion-notice { margin:0; border-left:3px solid #d69a32; padding:7px 9px; background:#fff8e9; color:#755719; font-size:11px; box-shadow:0 2px 8px rgba(58,43,17,.08); pointer-events:auto; }
.stats-tabs { display: grid; grid-template-columns: repeat(3,1fr); gap: 4px; margin: 14px 0 10px; border-radius: 8px; padding: 4px; background: #e7eeec; }
.stats-tabs button { height: 38px; border: 0; border-radius: 6px; background: transparent; color: #71807c; font-size: 13px; font-weight: 680; }
.stats-tabs button.active { background: #fff; color: #087f72; box-shadow: 0 2px 7px rgba(28,52,47,.12); }
.stats-section { border-top: 1px solid #dce7e4; border-bottom: 1px solid #dce7e4; background: #fff; }
.stats-section > header { height: 48px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #e4ece9; padding: 0 12px; }
.stats-section > header strong { font-size: 14px; }
.stats-section > header span { color: #7b8884; font-size: 11px; }
.daily-controls { display:flex; align-items:center; gap:6px; }
.daily-granularity-switch { display:flex; gap:2px; border-radius:6px; padding:3px; background:#e7eeec; }
.daily-granularity-switch button { min-width:25px; height:26px; border:0; border-radius:4px; background:transparent; color:#71807c; font-size:10px; }
.daily-granularity-switch button.active { background:#fff; color:#087f72; box-shadow:0 1px 4px rgba(28,52,47,.12); }
.daily-view-switch { display:flex; gap:3px; border-radius:6px; padding:3px; background:#e7eeec; }
.daily-view-switch button { width:28px; height:26px; display:grid; place-items:center; border:0; border-radius:4px; background:transparent; color:#71807c; }
.daily-view-switch button.active { background:#fff; color:#087f72; box-shadow:0 1px 4px rgba(28,52,47,.12); }
.daily-calendar { padding:12px; }
.calendar-month-nav { display:grid; grid-template-columns:30px 1fr 30px; align-items:center; margin-bottom:9px; }
.calendar-month-nav button { width:30px; height:28px; display:grid; place-items:center; border:0; border-radius:6px; background:#eef4f2; color:#58716a; }
.calendar-month-nav strong { color:#536762; font-size:12px; font-weight:720; text-align:center; }
.calendar-month-label { margin-bottom:9px; color:#536762; font-size:12px; font-weight:720; text-align:center; }
.calendar-weekdays,.calendar-grid { display:grid; grid-template-columns:repeat(7,minmax(0,1fr)); gap:4px; }
.calendar-weekdays { margin-bottom:4px; color:#8a9994; font-size:10px; text-align:center; }
.calendar-cell { min-width:0; min-height:58px; display:grid; align-content:start; justify-items:center; gap:3px; border:1px solid #e6eeec; border-radius:6px; padding:4px 2px; background:#fff; }
.calendar-cell.blank { border-color:transparent; background:transparent; }
.calendar-cell b { color:#536762; font-size:11px; font-weight:680; }
.calendar-cell i { width:0; max-width:80%; height:3px; display:block; border-radius:2px; }
.calendar-cell i.income { background:#20a57d; }
.calendar-cell i.expense { background:#ef6a4d; }
.calendar-cell small { color:#7b8884; font-size:8px; line-height:1; }
.period-calendar-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:7px; }
.period-calendar-cell { min-width:0; min-height:66px; display:grid; align-content:center; justify-items:center; gap:5px; border:1px solid #e6eeec; border-radius:7px; padding:7px 5px; background:#fff; }
.period-calendar-cell b { color:#536762; font-size:11px; font-weight:680; }
.period-calendar-cell.muted { background:#f8faf9; border-color:#edf2f0; }
.period-calendar-cell.muted b { color:#b0bbb7; }
.period-calendar-cell i { width:0; max-width:80%; height:4px; display:block; border-radius:3px; }
.period-calendar-cell i.income { background:#20a57d; }
.period-calendar-cell i.expense { background:#ef6a4d; }
.period-calendar-cell small { color:#7b8884; font-size:9px; line-height:1; }
.daily-view-legend { display:flex; justify-content:center; gap:15px; margin-top:10px; font-size:10px; }
.daily-view-legend span::before { display:inline-block; width:7px; height:7px; margin-right:4px; border-radius:50%; background:currentColor; content:""; }
.daily-line-chart { padding:12px; }
.line-chart-wrap { position:relative; min-width:0; }
.line-chart-wrap svg { width:100%; height:auto; overflow:visible; touch-action:none; }
.chart-axis-line { stroke:#aebdb8; stroke-width:1.2; }
.chart-zero-line { stroke:#879a93; stroke-width:1.4; }
.chart-grid-line { stroke:#e6eeec; stroke-width:1; }
.chart-axis-label { fill:#8a9994; font-size:9px; text-anchor:start; }
.chart-crosshair { stroke:#667c75; stroke-width:1; stroke-dasharray:3 3; }
.chart-line { fill:none; stroke-width:3; stroke-linecap:round; stroke-linejoin:round; }
.income-line { stroke:#20a57d; }
.expense-line { stroke:#ef6a4d; }
.chart-dot { stroke:#fff; stroke-width:2; }
.income-dot { fill:#20a57d; }
.expense-dot { fill:#ef6a4d; }
.chart-dot:not(.active) { opacity:.5; }
.bar { opacity:.78; }
.bar.active { opacity:1; filter:brightness(.92); }
.income-bar { fill:#20a57d; }
.expense-bar { fill:#ef6a4d; }
.chart-hit-area { fill:transparent; cursor:crosshair; }
.chart-tooltip { position:absolute; top:7px; z-index:2; display:grid; min-width:132px; gap:4px; transform:translateX(-50%); border:1px solid #435b54; border-radius:5px; padding:8px 9px; background:#263a35; color:#eaf4f1; box-shadow:0 5px 14px rgba(30,50,44,.2); pointer-events:none; }
.chart-tooltip strong { color:#fff; font-size:11px; font-weight:720; white-space:nowrap; }
.chart-tooltip span { display:flex; align-items:center; justify-content:space-between; gap:10px; font-size:10px; }
.chart-tooltip span i { font-style:normal; }
.chart-tooltip span b { color:#fff; font-size:10px; font-variant-numeric:tabular-nums; }
.chart-tooltip .income { color:#6ee0b5; }
.chart-tooltip .expense { color:#ff9a83; }
.chart-tooltip small { border-top:1px solid rgba(231,245,241,.15); padding-top:4px; color:#b9ccc6; font-size:9px; }
.line-chart-labels { display:flex; justify-content:space-between; color:#8a9994; font-size:9px; }
.daily-view-empty { min-height:180px; display:grid; place-items:center; color:#7b8884; font-size:12px; }
.daily-stat-row { min-height: 58px; display: grid; grid-template-columns: 34px minmax(0,1fr) 82px; align-items: center; gap: 9px; border-bottom: 1px solid #edf2f0; padding: 8px 12px; }
.daily-stat-row:last-child { border-bottom: 0; }
.daily-stat-row > span { color: #667570; font-size: 11px; }
.daily-bars { display: grid; gap: 5px; }
.daily-bars i { height: 5px; min-width: 2px; border-radius: 3px; }
.daily-bars i.income { background: #20a57d; }
.daily-bars i.expense { background: #ef6a4d; }
.daily-stat-row > div:last-child { display: grid; text-align: right; }
.daily-stat-row small { font-size: 10px; font-variant-numeric: tabular-nums; }
.income { color: #0d8b67; }
.expense { color: #d84c36; }
.category-stat-row { width:100%; min-height:64px; display:grid; grid-template-columns:18px 38px minmax(0,1fr) auto 28px; align-items:center; gap:8px; border-bottom:1px solid #edf2f0; padding:8px 12px 8px calc(12px + var(--category-indent, 0px)); background:#fff; color:#26342f; text-align:left; cursor:pointer; }
.category-stat-row:last-child { border-bottom: 0; }
.category-stat-row:not(.expandable) { cursor:default; }
.category-stat-row.expandable:active { background:#f4f9f7; }
.category-stat-row.level-0 { min-height:68px; gap:10px; padding-top:10px; padding-bottom:10px; }
.category-stat-row.level-1 { min-height:56px; grid-template-columns:18px 34px minmax(0,1fr) auto 28px; gap:7px; padding-top:7px; padding-bottom:7px; background:#fbfdfc; }
.category-stat-row.level-2 { min-height:46px; grid-template-columns:18px 30px minmax(0,1fr) auto 28px; gap:5px; padding-top:5px; padding-bottom:5px; background:#fff; }
.category-stat-row.level-0 .category-stat-icon { width:38px; height:38px; }
.category-stat-row.level-0 .category-stat-icon :deep(svg) { width:22px; height:22px; }
.category-stat-row.level-0 > div strong { font-size:15px; }
.category-stat-row.level-0 > span:not(.category-stat-icon):not(.category-stat-chevron) strong { font-size:14px; }
.category-stat-row.level-1 .category-stat-icon { width:32px; height:32px; }
.category-stat-row.level-1 .category-stat-icon :deep(svg) { width:19px; height:19px; }
.category-stat-row.level-1 > div strong { font-size:13px; }
.category-stat-row.level-1 > span:not(.category-stat-icon):not(.category-stat-chevron) strong { font-size:12px; }
.category-stat-row.level-2 .category-stat-icon { width:28px; height:28px; }
.category-stat-row.level-2 .category-stat-icon :deep(svg) { width:16px; height:16px; }
.category-stat-row.level-2 > div strong { font-size:11px; }
.category-stat-row.level-2 > span:not(.category-stat-icon):not(.category-stat-chevron) strong { font-size:11px; }
.category-stat-icon { width: 36px; height: 36px; display: grid; place-items: center; border-radius: 8px; }
.category-stat-row > div { min-width: 0; display: grid; gap: 7px; }
.category-stat-row > div strong { font-size: 13px; }
.category-stat-row > div i { height: 4px; min-width: 2px; border-radius: 2px; }
.category-stat-row > span:not(.category-stat-icon):not(.category-stat-chevron) { display: grid; text-align: right; }
.category-stat-row > span:not(.category-stat-icon):not(.category-stat-chevron) strong { font-size: 12px; }
.category-stat-filter { width:28px; height:32px; display:grid; place-items:center; border:0; border-radius:7px; background:transparent; color:#8a9994; }
.category-stat-filter:active { background:#eaf7f3; color:#087f72; }
.category-stat-expand-indicator { width:18px; height:24px; display:grid; place-items:center; color:#087f72; }
.category-stat-row:not(.expandable) .category-stat-expand-indicator { color:transparent; }
.category-stat-row small { color: #7d8986; font-size: 10px; }
.member-stat-row { min-height: 72px; display: grid; grid-template-columns: 42px minmax(0,1fr) auto; align-items: center; gap: 10px; padding: 10px 12px; }
.member-stat-row > span { width: 40px; height: 40px; display: grid; place-items: center; border-radius: 50%; background: #edf4ff; color: #3478e5; }
.member-stat-row > div { display: grid; }
.member-stat-row > div:last-child { text-align: right; }
.member-stat-row strong { font-size: 14px; }
.member-stat-row small { color: #7b8884; font-size: 10px; }
.member-stat-row small.income { color: #0d8b67; }
.member-stat-row small.expense { color: #d84c36; }
.stats-empty { min-height: 180px; display: grid; place-items: center; color: #7b8884; font-size: 13px; }
@media (max-width:360px) { .stats-scroll { padding-right:12px; padding-left:12px; } .stats-custom-range { grid-template-columns:1fr; } .stats-custom-range input { padding:0 8px; font-size:12px; } .stats-summary > div { padding-right:10px; padding-left:10px; } }
</style>
