<template>
  <page-meta
    :page-style="filterPanelOpen ? 'overflow: hidden;' : ''"
  />
  <view class="page-shell" :class="{ 'page--filter-open': filterPanelOpen }">
    <view
      id="home-sticky-header"
      :style="{ paddingTop: statusBarHeight + 'px' }"
      class="home-header page-home-sticky-header"
    >
      <view id="home-search-row" class="search-row page-home-search-row">
        <view class="filter-left">
          <FilterBar
            v-model="filters"
            @change="onFilterChange"
            :panel-open="filterPanelOpen"
            :mask-top-measure="measureSearchRowBottomForMask"
            :panel-dismiss-stamp="filterPanelDismissStamp"
            @update:panel-open="filterPanelOpen = $event"
          />
        </view>
        <view class="search-box">
          <text class="search-ico">⌕</text>
          <input
            v-model="searchQuery"
            class="search-input"
            type="text"
            placeholder="搜索作品..."
            confirm-type="search"
          />
        </view>
      </view>
    </view>

    <scroll-view
      ref="homeScrollRef"
      scroll-y
      class="home-scroll"
      :refresher-enabled="!filterPanelOpen"
      refresher-default-style="black"
      refresher-background="#F9FAFB"
      :refresher-triggered="pullRefreshing"
      @refresherrefresh="onScrollRefresherRefresh"
    >
      <!-- 接接口时首屏骨架，避免空白闪烁 -->
      <template v-if="showHomeSkeleton">
        <view class="banner sk-banner sk-breathe" />
        <view class="content">
          <view class="seg sk-seg">
            <view class="sk-seg-cell sk-breathe sk-breathe--d1" />
            <view class="sk-seg-cell sk-breathe sk-breathe--d2" />
          </view>
          <view class="home-feed-block">
            <view class="masonry">
              <view
                v-for="(h, idx) in skeletonCardHeights"
                :key="'sk-' + idx"
                class="masonry-item"
              >
                <view class="sk-card sk-breathe" :class="'sk-breathe--d' + (idx % 3)">
                  <view class="sk-card-img" :style="{ height: h }" />
                  <view class="sk-card-line sk-card-line--lg" />
                  <view class="sk-card-line sk-card-line--sm" />
                </view>
              </view>
            </view>
            <view class="end-tip sk-end-tip">加载中…</view>
          </view>
        </view>
      </template>

      <template v-else>
        <swiper
          v-if="bannerImages.length > 0"
          class="banner"
          circular
          autoplay
          :interval="4000"
          indicator-dots
          indicator-color="rgba(255,255,255,0.5)"
          indicator-active-color="#ffffff"
        >
          <swiper-item v-for="b in bannerImages" :key="b.id">
            <view class="banner-slide">
              <image class="banner-img" :src="b.url" mode="aspectFill" />
              <view class="banner-cap">
                <text class="banner-title">{{ b.title }}</text>
                <text class="banner-sub">{{ b.subtitle }}</text>
              </view>
            </view>
          </swiper-item>
        </swiper>
        <view v-else class="banner banner-empty" />

        <view class="content">
          <view class="seg">
            <view
              :class="homeTab === 'ji' ? 'seg-on' : 'seg-off'"
              @tap.stop="homeTab = 'ji'"
            >
              星空集
            </view>
            <view
              :class="homeTab === 'bang' ? 'seg-on' : 'seg-off'"
              @tap.stop="homeTab = 'bang'"
            >
              星空榜
            </view>
          </view>

          <!-- 小程序 scroll-view 内 v-show 对 column-count 瀑布流常失效，两榜会叠在一起；须用 v-if 二选一渲染 -->
          <view v-if="homeTab === 'ji'" key="home-tab-ji" class="home-feed-block">
            <!-- 全端统一 column-count，与 H5 排版一致 -->
            <view
              id="home-masonry-ji"
              class="masonry"
              :style="jiMasonryClipStyle"
            >
              <view class="masonry-col">
                <view
                  v-for="p in jiLeftPhotos"
                  :key="`l-${p.id}`"
                  class="masonry-item"
                >
                  <PhotoCard
                    :id="p.id"
                    :image-url="p.imageUrl"
                    :title="p.title"
                    :likes="p.likes"
                    :liked="!!p.liked"
                    :author="p.author"
                    :tags="cardTagsForPhoto(p)"
                  />
                </view>
              </view>
              <view class="masonry-col">
                <view
                  v-for="p in jiRightPhotos"
                  :key="`r-${p.id}`"
                  class="masonry-item"
                >
                  <PhotoCard
                    :id="p.id"
                    :image-url="p.imageUrl"
                    :title="p.title"
                    :likes="p.likes"
                    :liked="!!p.liked"
                    :author="p.author"
                    :tags="cardTagsForPhoto(p)"
                  />
                </view>
              </view>
            </view>
            <view class="end-tip end-tip--ji">已加载全部作品</view>
          </view>

          <view v-if="homeTab === 'bang'" key="home-tab-bang" class="home-feed-block">
            <view class="rank-panel">
              <view class="rank-list">
                <view
                  v-for="(p, idx) in photosRankList"
                  :key="p.id"
                  class="rank-row"
                  @click="openPhotoDetail(p.id)"
                >
                  <view
                    class="rank-num"
                    :class="{
                      'rank-num--gold': idx === 0,
                      'rank-num--silver': idx === 1,
                      'rank-num--bronze': idx === 2,
                    }"
                  >
                    <text>{{ idx + 1 }}</text>
                  </view>
                  <image
                    class="rank-thumb"
                    :src="p.imageUrl"
                    mode="aspectFill"
                  />
                  <view class="rank-main">
                    <text class="rank-title">{{ p.title }}</text>
                    <text class="rank-meta">{{ p.likes }} 赞</text>
                  </view>
                  <text class="rank-author">@{{ p.author }}</text>
                </view>
              </view>
            </view>
            <view class="end-tip">榜单共 {{ photosRankList.length }} 部作品</view>
          </view>
        </view>
      </template>
    </scroll-view>

    <ImageTabBar current="home" />
  </view>
</template>

<script setup lang="ts">
import {
  computed,
  getCurrentInstance,
  nextTick,
  onBeforeUnmount,
  onMounted,
  ref,
  watch,
} from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import ImageTabBar from "@/components/ImageTabBar.vue";
import { useHideNativeTabBar } from "@/composables/useHideNativeTabBar";
import FilterBar from "@/components/FilterBar.vue";
import PhotoCard from "@/components/PhotoCard.vue";
import {
  bannerImages as defaultBanners,
  photosSortedByLikes,
  photosUnsorted,
  type GalleryPhoto,
} from "@/data/gallery";
import { sortPhotosByLikesOrder } from "@/utils/filterApply";
import { labelsFromCategoryKeys } from "@/utils/categoryLabels";
import { tagsForPhotoTitle } from "@/utils/photoCardTags";
import type { FilterState } from "@/types/filter";
import { apiEnabled } from "@/config/api";
import { get, getMpToken, isMpSessionExpiredError } from "@/utils/api/http";
import { resolveMediaUrl } from "@/utils/mediaUrl";

const defaultFilterState = (): FilterState => ({
  dateRange: "all",
  categories: [],
  likesSort: "all",
});

type BannerItem = (typeof defaultBanners)[number];

const statusBarHeight = ref(0);
const searchQuery = ref("");
/** 星空集 ji | 星空榜 bang */
const homeTab = ref<"ji" | "bang">("ji");
const filters = ref<FilterState>(defaultFilterState());
/** 接接口时不先展示本地默认轮播，避免「像缓存旧图」；离线模式仍用 defaultBanners */
const bannerImages = ref<BannerItem[]>(apiEnabled() ? [] : [...defaultBanners]);
const apiGallery = ref<GalleryPhoto[]>([]);
const apiRank = ref<GalleryPhoto[]>([]);
/** 下拉刷新等场景递增，用于通知 FilterBar 收起面板 */
const filterPanelDismissStamp = ref(0);
/** 筛选面板展开时锁定页面滚动（配合 page-meta + H5 body） */
const filterPanelOpen = ref(false);
/** 须与 scroll-view :refresher-triggered 配合；false 时 H5 才会收起转圈 */
const pullRefreshing = ref(false);

/** 有 API 时首次拉取完成前展示骨架；离线模式一开始即为 true */
const homeListReady = ref(!apiEnabled());

const showHomeSkeleton = computed(() => apiEnabled() && !homeListReady.value);

/** 优先用接口 categories；离线演示仍按标题映射 */
function cardTagsForPhoto(p: GalleryPhoto): string[] {
  const fromApi = labelsFromCategoryKeys(p.categories);
  if (fromApi.length) {
    return fromApi;
  }
  return tagsForPhotoTitle(p.title);
}

/** 骨架双列卡片高度错落，接近瀑布流观感 */
const skeletonCardHeights = ["280rpx", "340rpx", "300rpx", "360rpx", "290rpx", "320rpx"];

useHideNativeTabBar();

watch(filterPanelOpen, (lock) => {
  if (typeof document === "undefined") return;
  const v = lock ? "hidden" : "";
  document.documentElement.style.overflow = v;
  document.body.style.overflow = v;
});

onBeforeUnmount(() => {
  clearJiMasonryClipTimers();
  if (typeof document === "undefined") return;
  document.documentElement.style.overflow = "";
  document.body.style.overflow = "";
});

function buildWorksQuery(): Record<string, string> {
  const q: Record<string, string> = {};
  const kw = searchQuery.value.trim();
  if (kw) {
    q.keyword = kw;
  }
  const f = filters.value;
  if (f.dateRange !== "all") {
    q.date_range = f.dateRange;
  }
  if (f.categories.length) {
    q.categories = f.categories.join(",");
  }
  if (f.likesSort !== "all") {
    q.likes_sort = f.likesSort;
  }
  return q;
}

function filterApiGalleryLocally(list: GalleryPhoto[]): GalleryPhoto[] {
  let out = [...list];
  const kw = searchQuery.value.trim().toLowerCase();
  if (kw) {
    out = out.filter((p) => {
      const t = String(p.title || "").toLowerCase();
      const a = String(p.author || "").toLowerCase();
      const d = String(p.description || "").toLowerCase();
      return t.includes(kw) || a.includes(kw) || d.includes(kw);
    });
  }
  const dateMode = filters.value.dateRange || "all";
  if (dateMode !== "all") {
    const now = new Date();
    const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const weekStart = new Date(dayStart);
    const weekOffset = (dayStart.getDay() + 6) % 7; // 周一为一周起点
    weekStart.setDate(dayStart.getDate() - weekOffset);
    const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
    const yearStart = new Date(now.getFullYear(), 0, 1);
    out = out.filter((p) => {
      const raw = String(p.date || "").trim();
      if (!raw) {
        return false;
      }
      const t = new Date(raw.replace(/-/g, "/"));
      if (Number.isNaN(t.getTime())) {
        return false;
      }
      switch (dateMode) {
        case "today":
          return t >= dayStart;
        case "week":
          return t >= weekStart;
        case "month":
          return t >= monthStart;
        case "year":
          return t >= yearStart;
        default:
          return true;
      }
    });
  }
  const cats = filters.value.categories ?? [];
  if (cats.length > 0) {
    out = out.filter((p) => {
      const pc = Array.isArray(p.categories) ? p.categories : [];
      return pc.some((c) => cats.includes(c));
    });
  }
  out = sortPhotosByLikesOrder(out, filters.value);
  return out;
}

/** 本次运行内仅第一次接口成功写入首页时打散远程图 HTTP 缓存，避免首屏旧图 */
let homeRemoteImagesBustedOnce = false;

function remoteImageUrlForHome(rawUrl: string, stamp: number): string {
  const u = resolveMediaUrl(rawUrl);
  if (!u || u.startsWith("/static/")) {
    return u;
  }
  if (homeRemoteImagesBustedOnce) {
    return u;
  }
  const sep = u.includes("?") ? "&" : "?";
  return `${u}${sep}_t=${stamp}`;
}

/** 避免 onMounted + onShow 各调一次 syncLists，并发 6 个请求加重弱网超时 */
let syncListsInFlight: Promise<void> | null = null;
/** 请求进行中又有新筛选/搜索变更时，结束后补拉一次最新条件 */
let syncListsPending = false;

/** H5 部分环境下 uni.request 的 timeout 不触发 fail，Promise 会一直挂住；用 race 强制结束骨架屏 */
const HOME_FETCH_DEADLINE_MS = 55000;

function raceHomeFetch<T>(p: Promise<T>): Promise<T> {
  return new Promise((resolve, reject) => {
    const tid = setTimeout(() => {
      reject(new Error("首页接口加载超时"));
    }, HOME_FETCH_DEADLINE_MS);
    p.then(
      (v) => {
        clearTimeout(tid);
        resolve(v);
      },
      (e) => {
        clearTimeout(tid);
        reject(e);
      }
    );
  });
}

async function syncLists() {
  if (!apiEnabled()) {
    bannerImages.value = [...defaultBanners];
    apiGallery.value = [];
    apiRank.value = [];
    homeListReady.value = true;
    return;
  }
  if (syncListsInFlight) {
    syncListsPending = true;
    return syncListsInFlight;
  }
  syncListsPending = false;
  syncListsInFlight = (async () => {
    try {
      const bundle = Promise.all([
        get<{
          list: Array<{
            id: number;
            url: string;
            title: string;
            subtitle: string;
          }>;
        }>("/api/v1/banners"),
        get<{ list: GalleryPhoto[] }>("/api/v1/works", buildWorksQuery()),
        get<{ list: GalleryPhoto[] }>("/api/v1/works/rank"),
      ]);
      const [b, w, r] = await raceHomeFetch(bundle);
      const stamp = Date.now();
      const bList = Array.isArray(b?.data?.list) ? b.data.list : [];
      const wList = Array.isArray(w?.data?.list) ? w.data.list : [];
      const rList = Array.isArray(r?.data?.list) ? r.data.list : [];
      bannerImages.value = bList.map((x) => ({
        id: x.id,
        url: remoteImageUrlForHome(x.url, stamp),
        title: x.title,
        subtitle: x.subtitle,
      }));
      apiGallery.value = wList.map((p) => ({
        ...p,
        imageUrl: remoteImageUrlForHome(p.imageUrl, stamp),
      }));
      apiRank.value = rList.map((p) => ({
        ...p,
        imageUrl: remoteImageUrlForHome(p.imageUrl, stamp),
      }));
      homeRemoteImagesBustedOnce = true;
    } catch (e) {
      if (isMpSessionExpiredError(e)) {
        return;
      }
      console.error(e);
      const msg =
        e instanceof Error && typeof e.message === "string"
          ? e.message.toLowerCase()
          : "";
      // 审核场景下避免出现底层英文报错提示（如 url not in domain list）
      if (msg.includes("url not in domain list")) {
        return;
      }
      uni.showToast({ title: "网络异常，请稍后下拉刷新", icon: "none" });
    } finally {
      homeListReady.value = true;
      syncListsInFlight = null;
      if (syncListsPending) {
        syncListsPending = false;
        // 用最新 filters/search 再拉一次，确保筛选最终态生效
        void syncLists();
      }
    }
  })();
  return syncListsInFlight;
}

watch([searchQuery, filters], () => {
  void syncLists();
}, { deep: true });

function onFilterChange(v: FilterState) {
  filters.value = {
    dateRange: v.dateRange,
    categories: [...(v.categories || [])],
    likesSort: v.likesSort || "all",
  };
  void syncLists();
}

onLoad(() => {
  /* 尽早发起首屏请求，缩短骨架停留；与 onMounted 解耦，无 token 时公开接口仍可拉数 */
  if (apiEnabled()) {
    void syncLists();
  }
});

onShow(() => {
  /* 未登录也可浏览公开作品；无 token 时仍结束骨架，避免一直卡在加载中 */
  if (apiEnabled() && !getMpToken()) {
    homeListReady.value = true;
  }
  /* 每次进入首页（含 tab 切回）强制收起筛选，避免小程序端 v-show/层叠导致关不掉 */
  filterPanelOpen.value = false;
  // 避免极少数场景 onMounted 未触发拉取或请求一直无回调导致骨架「加载中」卡死
  if (apiEnabled() && !homeListReady.value) {
    void syncLists();
  }
  scheduleJiMasonryClipMeasure();
});

function applyHomePullRefresh() {
  filters.value = defaultFilterState();
  searchQuery.value = "";
  filterPanelDismissStamp.value += 1;
  filterPanelOpen.value = false;
}

async function onScrollRefresherRefresh() {
  pullRefreshing.value = true;
  applyHomePullRefresh();
  try {
    await syncLists();
  } finally {
    setTimeout(() => {
      pullRefreshing.value = false;
    }, 0);
    nextTick(() => scheduleJiMasonryClipMeasure());
  }
}

/** 搜索行底边（视口 px），供筛选遮罩 top，避免盖住搜索框一行 */
function measureSearchRowBottomForMask(cb: (bottomPx: number) => void) {
  nextTick(() => {
    const plat = process.env.UNI_PLATFORM;
    if (plat === "h5" || plat === "web") {
      if (typeof document !== "undefined") {
        const el =
          document.getElementById("home-search-row") ||
          document.querySelector(".page-home-search-row");
        if (el) {
          cb(Math.ceil(el.getBoundingClientRect().bottom));
          return;
        }
      }
      cb(0);
      return;
    }
    const proxy = getCurrentInstance()?.proxy;
    const q1 = uni.createSelectorQuery();
    if (proxy) {
      q1.in(proxy as any);
    }
    q1.select("#home-search-row")
      .boundingClientRect((rect) => {
        if (rect && typeof rect.bottom === "number") {
          cb(Math.ceil(rect.bottom));
          return;
        }
        const q2 = uni.createSelectorQuery();
        if (proxy) {
          q2.in(proxy as any);
        }
        q2.select(".page-home-search-row")
          .boundingClientRect((r2) => {
            cb(
              r2 && typeof r2.bottom === "number" ? Math.ceil(r2.bottom) : 0
            );
          })
          .exec();
      })
      .exec();
  });
}

const photosByLikesDesc = computed(() => {
  if (apiEnabled()) {
    return filterApiGalleryLocally(apiGallery.value);
  }
  return sortPhotosByLikesOrder(photosUnsorted(), filters.value);
});

const jiLeftPhotos = computed(() =>
  photosByLikesDesc.value.filter((_, idx) => idx % 2 === 0)
);
const jiRightPhotos = computed(() =>
  photosByLikesDesc.value.filter((_, idx) => idx % 2 === 1)
);

/** Android App：WebView 对 column-count 会多算容器高度，裁掉底部空白，排版仍与 H5 一致 */
let jiMasonryClipAndroidCached: boolean | null = null;
function isAndroidAppBuild(): boolean {
  if (jiMasonryClipAndroidCached !== null) {
    return jiMasonryClipAndroidCached;
  }
  try {
    const s = uni.getSystemInfoSync() as { uniPlatform?: string };
    jiMasonryClipAndroidCached =
      (s.uniPlatform || "").toLowerCase() === "app-android";
  } catch {
    jiMasonryClipAndroidCached = false;
  }
  return jiMasonryClipAndroidCached;
}

const homeScrollRef = ref<unknown>(null);
const jiMasonryClipHeightPx = ref<number | null>(null);
let jiMasonryClipTimerIds: ReturnType<typeof setTimeout>[] = [];

function clearJiMasonryClipTimers() {
  for (const t of jiMasonryClipTimerIds) {
    clearTimeout(t);
  }
  jiMasonryClipTimerIds = [];
}

function runJiMasonryClipMeasure() {
  if (!isAndroidAppBuild()) {
    return;
  }
  if (homeTab.value !== "ji" || showHomeSkeleton.value) {
    jiMasonryClipHeightPx.value = null;
    return;
  }
  if (!photosByLikesDesc.value.length) {
    jiMasonryClipHeightPx.value = null;
    return;
  }
  const proxy = getCurrentInstance()?.proxy;
  const scrollVm = homeScrollRef.value as { $?: unknown } | null;
  const q = uni.createSelectorQuery();
  const ctx = scrollVm ?? proxy;
  if (ctx) {
    q.in(ctx as any);
  }
  q.select("#home-masonry-ji")
    .boundingClientRect()
    .selectAll("#home-masonry-ji .masonry-item")
    .boundingClientRect();
  q.exec((res) => {
    if (!isAndroidAppBuild()) {
      return;
    }
    const masonryRect = res?.[0] as
      | { top?: number; bottom?: number }
      | null
      | undefined;
    const items = res?.[1] as
      | Array<{ bottom?: number }>
      | null
      | undefined;
    if (!masonryRect || typeof masonryRect.top !== "number") {
      return;
    }
    if (!Array.isArray(items) || !items.length) {
      return;
    }
    const top = masonryRect.top;
    let maxBottom = top;
    for (const r of items) {
      if (r && typeof r.bottom === "number" && r.bottom > maxBottom) {
        maxBottom = r.bottom;
      }
    }
    const raw = Math.ceil(maxBottom - top);
    const tailGap = Math.ceil(uni.upx2px(18));
    const h = raw + tailGap;
    if (h >= 12) {
      jiMasonryClipHeightPx.value = h;
    }
  });
}

function scheduleJiMasonryClipMeasure() {
  if (!isAndroidAppBuild()) {
    return;
  }
  clearJiMasonryClipTimers();
  const delays = [0, 160, 400, 900, 2000];
  jiMasonryClipTimerIds = delays.map((ms) =>
    setTimeout(() => {
      runJiMasonryClipMeasure();
    }, ms)
  );
}

const jiMasonryClipStyle = computed((): Record<string, string> => {
  if (!isAndroidAppBuild()) {
    return {};
  }
  const h = jiMasonryClipHeightPx.value;
  if (h == null || h < 12) {
    return {};
  }
  return {
    height: `${h}px`,
    overflow: "hidden",
  };
});

/** 星空榜：固定按获赞从高到低，不受筛选里「作品点赞量」影响 */
const photosRankList = computed(() => {
  if (apiEnabled()) {
    return apiRank.value;
  }
  return photosSortedByLikes();
});

watch(
  [homeTab, showHomeSkeleton, photosByLikesDesc],
  () => {
    if (!isAndroidAppBuild()) {
      return;
    }
    if (homeTab.value !== "ji") {
      jiMasonryClipHeightPx.value = null;
      return;
    }
    nextTick(() => scheduleJiMasonryClipMeasure());
  },
  { deep: true }
);

onMounted(() => {
  const info = uni.getSystemInfoSync();
  statusBarHeight.value = info.statusBarHeight || 20;
  scheduleJiMasonryClipMeasure();
});

function openPhotoDetail(id: number) {
  uni.navigateTo({
    url: `/pages/photo-detail/photo-detail?id=${id}`,
  });
}
</script>

<style scoped>
.page-shell {
  display: flex;
  box-sizing: border-box;
  flex-direction: column;
  /* 锁定视口高度，避免整页与 scroll-view 双滚动、或 flex 子项高度算成 0 */
  height: 100vh;
  max-height: 100vh;
  min-height: 0;
  overflow: hidden;
  padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
  background: #f9fafb;
}

@supports (height: 100dvh) {
  .page-shell {
    height: 100dvh;
    max-height: 100dvh;
  }
}

/* H5：page-meta 不生效时兜底禁止背后滚动（与常态一致，保留类便于以后扩展） */
.page-shell.page--filter-open {
  overflow: hidden;
}

.home-header {
  position: relative;
  z-index: 40;
  flex-shrink: 0;
  padding-bottom: 12rpx;
  background: #fff;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}

/* flex 子项 + height:0 让 scroll-y 在各端获得确定高度，从而启用下拉刷新 */
.home-scroll {
  flex: 1;
  width: 100%;
  min-height: 0;
  height: 0;
  /* H5 内层惯性滚动；避免外层误抢滚动 */
  -webkit-overflow-scrolling: touch;
}

.search-row {
  position: relative;
  z-index: 11;
  display: flex;
  align-items: center;
  padding: 16rpx 32rpx 0;
  gap: 16rpx;
}

.filter-left {
  flex-shrink: 0;
  order: 1;
}

.search-box {
  position: relative;
  display: flex;
  flex: 1;
  align-items: center;
  height: 72rpx;
  padding-left: 76rpx;
  border-radius: 999rpx;
  background: #f3f4f6;
  order: 2;
}

.search-ico {
  position: absolute;
  top: 50%;
  left: 14rpx;
  font-size: 48rpx;
  line-height: 1;
  color: #9ca3af;
  transform: translateY(-50%);
}

.search-input {
  flex: 1;
  height: 72rpx;
  padding-right: 24rpx;
  font-size: 28rpx;
}

.banner {
  height: 512rpx;
}

.banner-empty {
  flex-shrink: 0;
  height: 512rpx;
  background: #eef1f5;
}

/* ---------- 骨架屏 ---------- */
.sk-banner {
  flex-shrink: 0;
  height: 512rpx;
  border-radius: 0;
  background: #e2e8f0;
}

.sk-seg {
  display: grid;
  margin-bottom: 16rpx;
  padding: 8rpx;
  border-radius: 24rpx;
  background: #fff;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  grid-template-columns: 1fr 1fr;
  gap: 8rpx;
}

.sk-seg-cell {
  height: 88rpx;
  border-radius: 20rpx;
  background: #e8ecf0;
}

.sk-card {
  overflow: hidden;
  padding-bottom: 20rpx;
  border-radius: 20rpx;
  background: #fff;
  box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.06);
}

.sk-card-img {
  width: 100%;
  border-radius: 0;
  background: #e8ecf0;
}

.sk-card-line {
  margin-top: 20rpx;
  margin-right: 20rpx;
  margin-left: 20rpx;
  height: 24rpx;
  border-radius: 8rpx;
  background: #e8ecf0;
}

.sk-card-line--lg {
  width: 72%;
}

.sk-card-line--sm {
  width: 46%;
  margin-top: 16rpx;
}

.sk-end-tip {
  color: #cbd5e1;
}

@keyframes home-sk-breathe {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.38;
  }
}

.sk-breathe {
  animation: home-sk-breathe 0.85s ease-in-out infinite;
}

.sk-breathe--d0 {
  animation-delay: 0s;
}

.sk-breathe--d1 {
  animation-delay: 0.08s;
}

.sk-breathe--d2 {
  animation-delay: 0.16s;
}

.sk-seg-cell.sk-breathe--d1 {
  animation-delay: 0.06s;
}

.sk-seg-cell.sk-breathe--d2 {
  animation-delay: 0.12s;
}

.banner-slide {
  position: relative;
  width: 100%;
  height: 100%;
}

.banner-img {
  width: 100%;
  height: 100%;
}

.banner-cap {
  position: absolute;
  right: 0;
  bottom: 64rpx;
  left: 0;
  padding: 0 48rpx;
  text-align: center;
}

.banner-title {
  display: block;
  margin-bottom: 16rpx;
  font-size: 44rpx;
  color: #fff;
  text-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.35);
}

.banner-sub {
  font-size: 26rpx;
  color: rgba(255, 255, 255, 0.92);
  text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
}

.content {
  max-width: 750rpx;
  margin: 0 auto;
  padding: 16rpx 20rpx 28rpx;
}


/* App 端 column-count 瀑布流后，兄弟节点易被排版错位；用纵向 flex 把列表与 end-tip 绑在一起 */
.home-feed-block {
  display: flex;
  width: 100%;
  flex-direction: column;
  align-items: stretch;
}

.seg {
  position: relative;
  z-index: 2;
  display: grid;
  flex-shrink: 0;
  margin-bottom: 16rpx;
  padding: 8rpx;
  border-radius: 24rpx;
  background: #fff;
  box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
  grid-template-columns: 1fr 1fr;
}

.seg-on {
  padding: 20rpx;
  font-size: 28rpx;
  color: #fff;
  text-align: center;
  background: #4a90e2;
  border-radius: 20rpx;
  box-shadow: 0 2rpx 8rpx rgba(74, 144, 226, 0.25);
}

.seg-off {
  padding: 20rpx;
  font-size: 28rpx;
  color: #6b7280;
  text-align: center;
  border-radius: 20rpx;
}

.seg-on:active,
.seg-off:active {
  opacity: 0.92;
}

.rank-panel {
  overflow: hidden;
  border-radius: 24rpx;
  background: #fff;
  box-shadow: 0 2rpx 12rpx rgba(15, 23, 42, 0.06);
}

.rank-list {
  padding: 16rpx 0;
}

.rank-row {
  display: flex;
  align-items: center;
  padding: 20rpx 24rpx;
  gap: 20rpx;
  border-bottom: 1rpx solid #f1f5f9;
}

.rank-row:active {
  background: #f8fafc;
}

.rank-row:last-child {
  border-bottom: none;
}

.rank-num {
  display: flex;
  width: 48rpx;
  height: 48rpx;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: #f1f5f9;
  font-size: 24rpx;
  font-weight: 600;
  color: #64748b;
}

.rank-num--gold {
  background: linear-gradient(145deg, #fcd34d, #f59e0b);
  color: #78350f;
  box-shadow: 0 4rpx 12rpx rgba(245, 158, 11, 0.35);
}

.rank-num--silver {
  background: linear-gradient(145deg, #e2e8f0, #94a3b8);
  color: #334155;
  box-shadow: 0 4rpx 12rpx rgba(148, 163, 184, 0.35);
}

.rank-num--bronze {
  background: linear-gradient(145deg, #fdba74, #ea580c);
  color: #7c2d12;
  box-shadow: 0 4rpx 12rpx rgba(234, 88, 12, 0.3);
}

.rank-thumb {
  width: 112rpx;
  height: 112rpx;
  flex-shrink: 0;
  border-radius: 16rpx;
  background: #e5e7eb;
}

.rank-main {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 8rpx;
  min-width: 0;
}

.rank-title {
  overflow: hidden;
  font-size: 28rpx;
  font-weight: 600;
  color: #0f172a;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rank-meta {
  font-size: 22rpx;
  color: #64748b;
}

.rank-author {
  flex-shrink: 0;
  max-width: 140rpx;
  overflow: hidden;
  font-size: 22rpx;
  color: #4a90e2;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.masonry {
  display: flex;
  width: 100%;
  align-items: flex-start;
  gap: 18rpx;
}

.masonry-col {
  display: flex;
  flex: 1;
  flex-direction: column;
}

.masonry-item {
  margin-bottom: 18rpx;
}

.end-tip {
  flex-shrink: 0;
  box-sizing: border-box;
  width: 100%;
  padding: 12rpx 0 0;
  font-size: 26rpx;
  color: #9ca3af;
  text-align: center;
}

.end-tip--ji {
  padding-top: 16rpx;
}
</style>
