<template>
  <div class="gxnlpt-page portal-page portal-page-shell portal-landing-figma-page">
    <div class="portal-figma-scale-viewport">
      <div class="portal-figma-scale-stage" ref="figmaStage">
    <div
      class="gxnlpt-shell page-content-wrap"
      :class="{ 'gxnlpt-shell--stacked': stackedNavLayout }"
    >
      <div class="gxnlpt-layout" :class="{ 'gxnlpt-layout--stacked': stackedNavLayout }">
        <!-- Figma Frame_left 280px；窄屏为服务中心式子导航面板 -->
        <aside class="gxnlpt-sidebar" :class="{ 'gxnlpt-sidebar--panel': stackedNavLayout }">
          <div class="gxnlpt-sidebar-sticky">
            <div class="gxnlpt-side-nav-wrap">
              <nav class="gxnlpt-side-nav" role="tablist" aria-label="共性能力分类">
                <button
                  v-for="(item, index) in categoryList"
                  :key="item.id"
                  type="button"
                  role="tab"
                  class="gxnlpt-side-item"
                  :class="{ 'is-active': activeTabIndex === index && contentView === 'list' }"
                  :aria-selected="activeTabIndex === index"
                  @click="onSideNavClick(item.id, index)"
                >
                  <img
                    class="gxnlpt-side-icon"
                    :src="getSideIconUrl(item.icon, activeTabIndex === index && contentView === 'list')"
                    alt=""
                  />
                  <span class="gxnlpt-side-label">{{ item.title }}</span>
                </button>
              </nav>
            </div>
            <div class="gxnlpt-side-actions">
              <button
                type="button"
                class="gxnlpt-side-action"
                :class="{ 'is-active': contentView === 'submit' }"
                @click="openSubmitView"
              >
                <span>收录</span>
              </button>
              <button
                type="button"
                class="gxnlpt-side-action"
                :class="{ 'is-active': contentView === 'favorites' }"
                @click="openFavoritesView"
              >
                <span class="gxnlpt-side-action-icon gxnlpt-side-action-icon--star" aria-hidden="true"></span>
                <span>我的收藏</span>
              </button>
            </div>
          </div>
          <!-- Figma 图二：白底向下铺满，空白在收录/收藏下方 -->
          <div class="gxnlpt-sidebar-tail" aria-hidden="true"></div>
        </aside>

        <!-- Figma Frame 228 flex 896 -->
        <main id="anchor-container" class="gxnlpt-content">
          <!-- 收录表单（Figma 共性能力_收录） -->
          <section v-if="contentView === 'submit'" class="gxnlpt-submit" aria-label="收录">
            <header class="gxnlpt-submit-head">
              <div class="gxnlpt-submit-title-row">
                <span class="gxnlpt-submit-title-icon" aria-hidden="true"></span>
                <h2 class="gxnlpt-submit-title">收录</h2>
              </div>
              <p class="gxnlpt-submit-notice">
                欢迎各位网友、同仁和专家提交优质网站，尽量1个工作日内完成审核。
              </p>
            </header>
            <form class="gxnlpt-submit-form" @submit.prevent="handleSubmitForm">
              <div class="gxnlpt-submit-body">
                <label class="gxnlpt-field">
                  <span class="gxnlpt-field-label"><span class="gxnlpt-field-required">*</span>名称</span>
                  <input
                    v-model.trim="submitForm.bt1"
                    type="text"
                    class="gxnlpt-input"
                    :class="{ 'is-error': submitErrors.bt1 }"
                    placeholder="请输入名称"
                    @blur="touchSubmitField('bt1')"
                  />
                  <span v-if="submitErrors.bt1" class="gxnlpt-field-error">{{ submitErrors.bt1 }}</span>
                </label>
                <label class="gxnlpt-field">
                  <span class="gxnlpt-field-label"><span class="gxnlpt-field-required">*</span>链接</span>
                  <input
                    v-model.trim="submitForm.lj"
                    type="text"
                    class="gxnlpt-input"
                    :class="{ 'is-error': submitErrors.lj }"
                    placeholder="请输入链接（http:// 或 https://）"
                    @blur="touchSubmitField('lj')"
                  />
                  <span v-if="submitErrors.lj" class="gxnlpt-field-error">{{ submitErrors.lj }}</span>
                </label>
                <label class="gxnlpt-field">
                  <span class="gxnlpt-field-label"><span class="gxnlpt-field-required">*</span>简介</span>
                  <textarea
                    v-model.trim="submitForm.jj"
                    class="gxnlpt-textarea"
                    :class="{ 'is-error': submitErrors.jj }"
                    maxlength="40"
                    rows="3"
                    placeholder="请输入简介"
                    @blur="touchSubmitField('jj')"
                  ></textarea>
                  <span class="gxnlpt-field-counter">{{ submitForm.jj.length }}/40</span>
                  <span v-if="submitErrors.jj" class="gxnlpt-field-error">{{ submitErrors.jj }}</span>
                </label>
                <div class="gxnlpt-submit-row-2">
                  <label class="gxnlpt-field">
                    <span class="gxnlpt-field-label"><span class="gxnlpt-field-required">*</span>分类</span>
                    <select
                      v-model="submitForm.fl"
                      class="gxnlpt-select"
                      :class="{ 'is-error': submitErrors.fl }"
                      @blur="touchSubmitField('fl')"
                    >
                      <option value="">请选择分类</option>
                      <option
                        v-for="cat in categoryList"
                        :key="cat.id"
                        :value="cat.title"
                      >{{ cat.title }}</option>
                    </select>
                    <span v-if="submitErrors.fl" class="gxnlpt-field-error">{{ submitErrors.fl }}</span>
                  </label>
                  <label class="gxnlpt-field">
                    <span class="gxnlpt-field-label">标签</span>
                    <input
                      v-model.trim="submitForm.bq"
                      type="text"
                      class="gxnlpt-input"
                      placeholder="多个标签用逗号分隔"
                    />
                  </label>
                </div>
              </div>
              <button type="submit" class="gxnlpt-submit-btn">提交审核</button>
            </form>
          </section>

          <!-- 我的收藏 -->
          <section v-else-if="contentView === 'favorites'" class="gxnlpt-favorites" aria-label="我的收藏">
            <header class="gxnlpt-block-head gxnlpt-favorites-head">
              <span class="gxnlpt-favorites-star" aria-hidden="true"></span>
              <h2 class="gxnlpt-block-title">我的收藏</h2>
            </header>
            <div v-if="favoritesLoading" class="gxnlpt-state">加载中...</div>
            <div v-else-if="!favoriteCards.length" class="gxnlpt-state">暂无收藏</div>
            <div v-else class="gxnlpt-card-grid">
              <article
                v-for="card in favoriteCards"
                :key="card.gxUuid || card._demoId"
                class="gxnlpt-card"
                :class="{ 'gxnlpt-card--expired': card.yxzt === 'N' }"
                @click="card.yxzt === 'N' ? null : handleCardClick(card)"
              >
                <div class="gxnlpt-card-body">
                  <div class="gxnlpt-card-info">
                    <div class="gxnlpt-card-name-row">
                      <h3 class="gxnlpt-card-name" :title="card.bt1">{{ card.bt1 }}</h3>
                      <span v-if="card.yxzt === 'N'" class="gxnlpt-card-expired-tag">已失效</span>
                      <button
                        v-else
                        type="button"
                        class="gxnlpt-card-star"
                        aria-label="取消收藏"
                        @click.stop="handleCollect(card)"
                      >
                        <img :src="starFilled" alt="" />
                      </button>
                    </div>
                    <p class="gxnlpt-card-org" :title="card.qymc">{{ card.qymc }}</p>
                  </div>
                  <GxnlptCardTags :tags="card.tagList" />
                </div>
              </article>
            </div>
          </section>

          <template v-else>
          <section
            v-for="{ item, index } in listSections"
            :key="item.id"
            :id="item.id"
            class="gxnlpt-block"
            :data-section-index="index"
          >
            <header class="gxnlpt-block-head">
              <img class="gxnlpt-block-icon" :src="getIconUrl(item.icon)" alt="" />
              <h2 class="gxnlpt-block-title">{{ item.title }}</h2>
            </header>

            <div v-if="pageLoading && index === 0" class="gxnlpt-state">加载中...</div>
            <div v-else-if="!pageLoading && !item.displayList.length" class="gxnlpt-state">暂无相关服务</div>
            <template v-else-if="item.displayList.length">
              <div class="gxnlpt-card-grid">
                <article
                  v-for="card in item.displayList"
                  :key="card.gxUuid || card._demoId"
                  class="gxnlpt-card"
                  @click="handleCardClick(card)"
                >
                  <div class="gxnlpt-card-body">
                    <div class="gxnlpt-card-info">
                      <div class="gxnlpt-card-name-row">
                        <h3 class="gxnlpt-card-name" :title="card.bt1">{{ card.bt1 }}</h3>
                        <button
                          type="button"
                          class="gxnlpt-card-star"
                          aria-label="收藏"
                          @click.stop="handleCollect(card)"
                        >
                          <img :src="card.scbz === 'Y' ? starFilled : starOutline" alt="" />
                        </button>
                      </div>
                      <p class="gxnlpt-card-org" :title="card.qymc">{{ card.qymc }}</p>
                    </div>
                    <GxnlptCardTags :tags="card.tagList" />
                  </div>
                </article>
              </div>
              <div
                v-if="item.cardList.length > item.previewSize"
                class="gxnlpt-more-wrap"
              >
                <button type="button" class="gxnlpt-more" @click="toggleViewMore(item)">
                  {{ item.expanded ? '收起' : '查看更多>>' }}
                </button>
              </div>
            </template>
          </section>

          <!-- 优化需求：碳链接专区（21 条精选外站链接，按 3 大类组织） -->
          <section class="gxnlpt-block gxnlpt-carbon-links" aria-label="碳链接">
            <header class="gxnlpt-block-head">
              <img class="gxnlpt-block-icon" :src="getIconUrl('tanjl.svg')" alt="" />
              <h2 class="gxnlpt-block-title">碳链接</h2>
              <span class="gxnlpt-block-subtitle">产品碳足迹 · 企业碳管理平台 · CBAM</span>
            </header>

            <div
              v-for="group in carbonLinkGroups"
              :key="group.type"
              class="gxnlpt-carbon-group"
            >
              <h3 class="gxnlpt-carbon-group-title">
                <span class="gxnlpt-carbon-group-bar"></span>
                <span>{{ group.type }}</span>
                <span class="gxnlpt-carbon-group-count">{{ group.list.length }}</span>
              </h3>
              <ul class="gxnlpt-carbon-list">
                <li
                  v-for="(item, idx) in group.list"
                  :key="`${group.type}-${idx}`"
                  class="gxnlpt-carbon-item"
                >
                  <a
                    class="gxnlpt-carbon-link"
                    :href="item.url"
                    target="_blank"
                    rel="noopener noreferrer"
                  >
                    <span class="gxnlpt-carbon-name">{{ item.name }}</span>
                    <span v-if="item.desc" class="gxnlpt-carbon-desc">{{ item.desc }}</span>
                  </a>
                  <span class="gxnlpt-carbon-external" aria-hidden="true">↗</span>
                </li>
              </ul>
            </div>
          </section>
          </template>
        </main>
      </div>
    </div>
      </div>
    </div>
  </div>
</template>

<script>
import GxnlptCardTags from '@/pages/index/views/gxnlpt/components/GxnlptCardTags.vue';
import api from '@/pages/index/api/gxnl/index.js';
// 兜底数据：184 条共性能力链接 + 21 条碳链接（共约 200+ 条）
// 来源：可信碳网站优化需求文档；按页面 5 大类自动归类
import gxnlLinksByCategory from '@/pages/index/data/gxnl-links.js';
// 21 条碳链接独立数据（产品碳足迹/企业碳管理平台/CBAM）
import carbonLinksData from '@/pages/index/data/carbon-links.js';
import {
  isPortalLoggedIn,
  isUnauthorizedError,
  showLoginGuide,
} from '@/pages/index/utils/auth-guard';
import { BP } from '@/pages/index/utils/breakpoint.js';
import portalFigmaScaleMixin from '@/pages/index/utils/portal-figma-scale-mixin';
import comingSoonMixin from '@/pages/index/utils/coming-soon-mixin';

const starOutline = require('@/pages/index/assets/fwsc/wsc.svg');
const starFilled = require('@/pages/index/assets/fwsc/ysc.svg');

const PREVIEW_SIZE = 6;

/** 与 breakpoints.less @mq-desktop-sm-max 一致 */
const STACKED_NAV_MEDIA = `(max-width: ${BP.desktopSm - 1}px)`;

function readStackedNavLayout() {
  if (typeof window === 'undefined') return false;
  return window.matchMedia(STACKED_NAV_MEDIA).matches;
}

/** 临时开启：优先展示分类假数据，接口就绪后改为 false */
const FORCE_DEMO_PREVIEW = false;

/**
 * 演示：标签溢出省略 UI 测试（需与 FORCE_DEMO_PREVIEW 同时为 true）
 * 关闭后恢复每卡 2～3 个短标签的正常展示
 */
/** 关闭后卡片仅展示接口/演示数据中的 2～3 个短标签，与图二设计稿一致 */
const DEMO_TAG_OVERFLOW_PREVIEW = false;

/** 按分类注入的碳领域长标签（仅溢出 UI 测试用） */
const DEMO_TAG_OVERFLOW_BY_CATEGORY = {
  'content-1': [
    '温室气体盘查',
    '组织碳排放',
    '产品碳足迹',
    '范围三排放',
    'ISO14064-1',
    'GHG Protocol',
    '排放因子',
    '碳数据核算',
    '年度碳盘查',
    '供应链碳足迹',
    '碳中和路径',
  ],
  'content-2': [
    '第三方碳核查',
    'CCER项目审定',
    '碳中和认证',
    '自愿减排核证',
    '绿色项目认定',
    '产品碳足迹认证',
    '零碳工厂评价',
    '林业碳汇核证',
    '碳标识认证',
  ],
  'content-3': [
    '全国碳配额',
    'CCER自愿减排',
    '碳市场交易',
    '配额回购',
    '履约清缴',
    '试点碳市场',
    '碳资产托管',
    '大宗协议转让',
    '碳普惠交易',
    '跨境碳信用',
  ],
};

function mergeDemoOverflowTags(bqjh, categoryId) {
  let base = [];
  if (bqjh) {
    try {
      const parsed = JSON.parse(bqjh);
      base = Array.isArray(parsed) ? parsed : [];
    } catch (e) {
      base = String(bqjh)
        .split(/[,，]/)
        .map((s) => s.trim())
        .filter(Boolean);
    }
  }
  const extra = DEMO_TAG_OVERFLOW_BY_CATEGORY[categoryId] || [];
  return JSON.stringify([...new Set([...base, ...extra])]);
}

/** content-1 前 3 张、content-3 前 2 张注入长标签，其余保持短标签便于对比 */
function shouldInjectOverflowDemoTags(categoryId, index) {
  if (!DEMO_TAG_OVERFLOW_PREVIEW || !FORCE_DEMO_PREVIEW) return false;
  if (categoryId === 'content-1' && index < 3) return true;
  if (categoryId === 'content-3' && index < 2) return true;
  return false;
}

/**
 * 历史说明：原先这里有 ORIGINAL_DEMO_BY_CATEGORY（5×8=40 条“概念性占位”数据），
 * 它们没有 lj 字段，依赖数据库/接口不可用时显示，但用户点击会触发“敬请期待”，
 * 体感为“点击无反应”，现已全部清理。
 * 当前兜底数据源改为 gxnlLinksByCategory（gxnl-links.js），
 * 含 184 条共性能力链接 + 21 条碳链接，全部有真实外链。
 */

// 页面 5 大类 id → 数据分类标题 映射
const CATEGORY_ID_TO_TITLE = {
  'content-1': '碳核算平台',
  'content-2': '碳认证机构',
  'content-3': '碳交易平台',
  'content-4': '碳金融服务',
  'content-5': '碳技术咨询',
};

/**
 * 真实接口数据失败时，页面会回退到本地的“兜底数据”。
 * 数据源（已清理）：ORIGINAL_DEMO_BY_CATEGORY —— 早期 40 条“概念性占位”数据，
 *                    没有 lj 字段，点击会走“敬请期待”分支造成“点击无反应”体感，已全部清理。
 * 数据源：gxnlLinksByCategory —— 来自《可信碳网站优化需求文档》整理的 184 条真实外链 + 21 条碳链接。
 */
const DEMO_BY_CATEGORY = (() => {
  const merged = {};
  Object.entries(CATEGORY_ID_TO_TITLE).forEach(([id, title]) => {
    const extra = (gxnlLinksByCategory && gxnlLinksByCategory[title]) || [];
    merged[id] = extra.map((item) => ({
      bt1: item.bt1,
      qymc: item.qymc,
      bqjh: item.bqjh,
      lj: item.lj,
      scbz: item.scbz || 'N',
    }));
  });
  return merged;
})();

/** 各分类标签池（来自演示数据，供接口数据补标签） */
const CATEGORY_TAG_POOL = Object.keys(DEMO_BY_CATEGORY).reduce((pools, id) => {
  const set = new Set();
  (DEMO_BY_CATEGORY[id] || []).forEach((item) => {
    try {
      JSON.parse(item.bqjh).forEach((tag) => set.add(tag));
    } catch (e) {
      /* ignore */
    }
  });
  pools[id] = [...set];
  return pools;
}, {});

/** 按标题/简介语义匹配标签（与演示卡片风格一致） */
const CATEGORY_TAG_RULES = {
  'content-1': [
    { test: /盘查|温室气体|组织碳|MRV/, tags: ['碳盘查', '组织碳', 'ISO14064'] },
    { test: /足迹|LCA|PAS|14067/, tags: ['产品碳足迹', 'LCA', 'PAS2050'] },
    { test: /CBAM|边境|欧盟|关税/, tags: ['CBAM', '欧盟碳关税', '出口合规'] },
    { test: /园区|能碳|协同/, tags: ['园区碳管理', '能碳协同'] },
    { test: /供应链|范围三|范围 3|填报/, tags: ['范围三', '供应链', '数据填报'] },
    { test: /资产|年度/, tags: ['碳资产', '年度盘查', '全国'] },
    { test: /监测|核算平台|在线/, tags: ['在线监测', '核算', '平台'] },
    { test: /重点行业|指南/, tags: ['重点行业', '核算指南'] },
  ],
  'content-2': [
    { test: /核查|第三方/, tags: ['碳核查', '第三方核查', '重点排放单位'] },
    { test: /CCER|造林|碳汇/, tags: ['CCER', '项目审定', '造林碳汇'] },
    { test: /绿色.*认定|绿色项目/, tags: ['绿色认定', '全国'] },
    { test: /碳中和|抵消/, tags: ['碳中和', '认证', '抵消'] },
    { test: /自愿减排|验证/, tags: ['自愿减排', '验证', 'MRV'] },
    { test: /零碳工厂|工厂评价/, tags: ['零碳工厂', '评价认证'] },
    { test: /足迹.*认证|产品碳足迹/, tags: ['产品碳足迹', '认证'] },
    { test: /供应链.*认证|绿色认证/, tags: ['供应链', '绿色认证'] },
  ],
  'content-3': [
    { test: /配额|全国碳/, tags: ['碳配额', '全国碳市场', '交易'] },
    { test: /CCER|自愿减排|挂牌/, tags: ['CCER', '挂牌', '自愿减排'] },
    { test: /回购/, tags: ['回购', '流动性', '碳金融'] },
    { test: /试点|转让/, tags: ['试点市场', '配额转让'] },
    { test: /托管|履约/, tags: ['托管', '碳资产', '履约'] },
    { test: /VCS|国际|跨境/, tags: ['VCS', '国际碳信用', '跨境'] },
    { test: /大宗|协议/, tags: ['大宗交易', '配额'] },
    { test: /碳普惠|普惠/, tags: ['碳普惠', '交易'] },
  ],
  'content-4': [
    { test: /信贷|绿色信贷/, tags: ['绿色信贷', '融资', '转型金融'] },
    { test: /质押|贷款/, tags: ['质押', '碳配额', '贷款'] },
    { test: /保险|收益权/, tags: ['碳保险', '收益权', '风险'] },
    { test: /基金|ESG|投资/, tags: ['碳基金', 'ESG', '投资'] },
    { test: /债券|发行/, tags: ['绿色债券', '发行', '顾问'] },
    { test: /回购融资/, tags: ['回购融资', '配额', '全国'] },
    { test: /转型金融/, tags: ['转型金融', '贷款'] },
    { test: /减排.*工具|支持工具/, tags: ['碳减排', '支持工具'] },
  ],
  'content-5': [
    { test: /达峰|路径/, tags: ['碳达峰', '路径规划', '咨询'] },
    { test: /减排.*技术|技术评估/, tags: ['减排技术', '评估', '电力'] },
    { test: /管理体系|ISO|培训/, tags: ['管理体系', 'ISO14067', '培训'] },
    { test: /信息披露|CDP|ESG报告/, tags: ['信息披露', 'CDP', 'ESG报告'] },
    { test: /林业碳汇|项目开发/, tags: ['林业碳汇', '项目开发', 'CCER'] },
    { test: /零碳园区|综合能源/, tags: ['零碳园区', '规划', '综合能源'] },
    { test: /政策|解读/, tags: ['政策解读', '碳市场'] },
    { test: /ESG|议题/, tags: ['ESG', '碳议题', '咨询'] },
  ],
};

function hashText(str) {
  let h = 0;
  const s = String(str || '');
  for (let i = 0; i < s.length; i += 1) {
    h = (h * 31 + s.charCodeAt(i)) | 0;
  }
  return Math.abs(h);
}

function scoreTagAgainstText(tag, text) {
  if (text.includes(tag)) return tag.length + 20;
  let score = 0;
  for (let len = Math.min(tag.length, 4); len >= 2; len -= 1) {
    for (let i = 0; i <= tag.length - len; i += 1) {
      const sub = tag.slice(i, i + len);
      if (text.includes(sub)) score += len;
    }
  }
  return score;
}

/** 接口数据缺标签时，根据标题/简介推断 2～3 个展示标签 */
function inferTagsFromContent(card, category) {
  const text = `${card.bt1 || ''}${card.fwnr || ''}${card.qymc || ''}`;
  const rules = CATEGORY_TAG_RULES[category.id] || [];
  for (let i = 0; i < rules.length; i += 1) {
    if (rules[i].test.test(text)) {
      return rules[i].tags.slice(0, 3);
    }
  }

  const pool = CATEGORY_TAG_POOL[category.id] || [];
  const scored = pool
    .map((tag) => ({ tag, score: scoreTagAgainstText(tag, text) }))
    .filter((item) => item.score > 0)
    .sort((a, b) => b.score - a.score);
  if (scored.length >= 2) {
    return scored.slice(0, 3).map((item) => item.tag);
  }

  const kwTags = (category.keywords || []).filter((kw) => text.includes(kw)).slice(0, 2);
  if (kwTags.length) {
    const extra = pool.find((tag) => !kwTags.includes(tag));
    return [...kwTags, extra].filter(Boolean).slice(0, 3);
  }

  const seed = hashText(card.bt1 || text);
  const picked = [];
  for (let i = 0; i < pool.length && picked.length < 3; i += 1) {
    const tag = pool[(seed + i) % pool.length];
    if (tag && !picked.includes(tag)) picked.push(tag);
  }
  return picked;
}

const SIDE_ICON_GRAY = {
  'thspt.svg': require('../../assets/gxnlpt/icons/01-carbon-accounting.svg'),
  'trzjg.svg': require('../../assets/gxnlpt/icons/02-carbon-certification.svg'),
  'tjypt.svg': require('../../assets/gxnlpt/icons/03-carbon-trading.svg'),
  'tjrfw.svg': require('../../assets/gxnlpt/icons/04-carbon-finance.svg'),
  'tjszx.svg': require('../../assets/gxnlpt/icons/05-carbon-consulting.svg'),
};

const SIDE_ICON_WHITE = {
  'thspt.svg': require('../../assets/gxnlpt/side-white/01-carbon-accounting.png'),
  'trzjg.svg': require('../../assets/gxnlpt/side-white/02-carbon-certification.png'),
  'tjypt.svg': require('../../assets/gxnlpt/side-white/03-carbon-trading.png'),
  'tjrfw.svg': require('../../assets/gxnlpt/side-white/04-carbon-finance.png'),
  'tjszx.svg': require('../../assets/gxnlpt/side-white/05-carbon-consulting.png'),
};

const CATEGORY_META = [
  { id: 'content-1', title: '碳核算平台', flDm: '01', icon: 'thspt.svg', keywords: ['核算', '足迹', 'LCA', 'CBAM', '碳管理', '碳盘查', '标准化'] },
  { id: 'content-2', title: '碳认证机构', flDm: '02', icon: 'trzjg.svg', keywords: ['认证', '核查', '审定', '验证'] },
  { id: 'content-3', title: '碳交易平台', flDm: '03', icon: 'tjypt.svg', keywords: ['交易', '交易所', '配额', 'CCER'] },
  { id: 'content-4', title: '碳金融服务', flDm: '04', icon: 'tjrfw.svg', keywords: ['金融', '融资', '信贷', '保险', '基金', '资产'] },
  { id: 'content-5', title: '碳技术咨询', flDm: '05', icon: 'tjszx.svg', keywords: ['咨询', '技术', '研究', '规划', '方案'] },
];

function buildCategoryList() {
  return CATEGORY_META.map((meta) => ({
    ...meta,
    cardList: [],
    displayList: [],
    total: 0,
    previewSize: PREVIEW_SIZE,
    expanded: false,
  }));
}

const FL_DM_MAP = {
  '碳核算平台': '01',
  '碳认证机构': '02',
  '碳交易平台': '03',
  '碳金融服务': '04',
  '碳技术咨询': '05',
};

function flTitleToDm(title) {
  return FL_DM_MAP[title] || '';
}

const SUBMIT_FORM_EMPTY = () => ({
  bt1: '',
  lj: '',
  jj: '',
  fl: '',
  bq: '',
});

export default {
  name: 'GxnlptIndex',
  mixins: [portalFigmaScaleMixin, comingSoonMixin],
  components: { GxnlptCardTags },
  data() {
    return {
      starOutline,
      starFilled,
      contentView: 'list',
      activeTabIndex: 0,
      suppressSectionObserver: false,
      scrollRootEl: null,
      pageLoading: false,
      useDemoData: false,
      categoryList: buildCategoryList(),
      submitForm: SUBMIT_FORM_EMPTY(),
      submitTouched: {},
      submitErrors: {},
      submitAttempted: false,
      stackedNavLayout: readStackedNavLayout(),
      _stackedNavMq: null,
      favoriteList: [],
      favoritesLoading: false,
    };
  },
  computed: {
    isStackedNavMode() {
      return this.stackedNavLayout;
    },
    listSections() {
      if (this.isStackedNavMode) {
        const item = this.categoryList[this.activeTabIndex];
        if (!item) return [];
        return [{ item, index: this.activeTabIndex }];
      }
      return this.categoryList.map((item, index) => ({ item, index }));
    },
    /* 碳链接：按 3 大类（产品碳足迹 / 企业碳管理平台 / CBAM）分组 */
    carbonLinkGroups() {
      const order = ['产品碳足迹', '企业碳管理平台', 'CBAM'];
      const groups = order.map((type) => ({
        type,
        list: (carbonLinksData || []).filter((x) => x.type === type),
      }));
      return groups.filter((g) => g.list.length > 0);
    },
    favoriteCards() {
      if (this.favoriteList && this.favoriteList.length) {
        return this.favoriteList;
      }
      const list = [];
      this.categoryList.forEach((cat) => {
        cat.cardList.forEach((card) => {
          if (card.scbz === 'Y') {
            list.push(card);
          }
        });
      });
      return list;
    },
  },
  created() {
    this.stackedNavLayout = readStackedNavLayout();
  },
  mounted() {
    this.bindStackedNavMedia();
    this.bootstrap();
    this.$nextTick(() => this.initSidebarSticky());
  },
  activated() {
    this.syncStackedNavLayout();
    if (this.contentView === 'list' && !this.stackedNavLayout) {
      this.$nextTick(() => {
        this.initScrollSpy();
        this.initSidebarSticky();
      });
    }
  },
  beforeDestroy() {
    this.unbindStackedNavMedia();
    this.clearScrollUnlock();
    this.destroyScrollSpy();
    this.destroySidebarSticky();
  },
  methods: {
    /** 收录 / 我的收藏 / 收藏操作前校验登录（弹窗引导，不直接跳登录页） */
    ensureLogin(actionText) {
      if (isPortalLoggedIn()) {
        return true;
      }
      showLoginGuide({ actionText });
      return false;
    },
    async bootstrap() {
      try {
        await this.loadAllSections();
      } catch (e) {
        console.error('共性能力页加载失败', e);
        this.applyDemoFallback();
      }
      this.$nextTick(() => {
        const anchor = this.$route.query.anchor || this.$route.params.anchor;
        if (anchor) {
          const index = this.categoryList.findIndex((item) => item.id === anchor);
          const tabIndex = index >= 0 ? index : 0;
          if (this.isStackedNavMode) {
            this.activeTabIndex = tabIndex;
          } else {
            this.initScrollSpy();
            this.initSidebarSticky();
            this.scrollToSection(anchor, tabIndex);
          }
          return;
        }
        this.initScrollSpy();
        this.initSidebarSticky();
      });
    },
    getIconUrl(iconName) {
      return SIDE_ICON_GRAY[iconName] || SIDE_ICON_GRAY['thspt.svg'];
    },
    getSideIconUrl(iconName, isActive) {
      if (isActive && SIDE_ICON_WHITE[iconName]) {
        return SIDE_ICON_WHITE[iconName];
      }
      return this.getIconUrl(iconName);
    },
    bindStackedNavMedia() {
      if (typeof window === 'undefined' || !window.matchMedia) return;
      this._stackedNavMq = window.matchMedia(STACKED_NAV_MEDIA);
      this._onStackedNavMqChange = () => this.syncStackedNavLayout();
      if (typeof this._stackedNavMq.addEventListener === 'function') {
        this._stackedNavMq.addEventListener('change', this._onStackedNavMqChange);
      } else if (typeof this._stackedNavMq.addListener === 'function') {
        this._stackedNavMq.addListener(this._onStackedNavMqChange);
      }
      this.syncStackedNavLayout();
    },
    unbindStackedNavMedia() {
      if (!this._stackedNavMq || !this._onStackedNavMqChange) return;
      if (typeof this._stackedNavMq.removeEventListener === 'function') {
        this._stackedNavMq.removeEventListener('change', this._onStackedNavMqChange);
      } else if (typeof this._stackedNavMq.removeListener === 'function') {
        this._stackedNavMq.removeListener(this._onStackedNavMqChange);
      }
      this._stackedNavMq = null;
      this._onStackedNavMqChange = null;
    },
    syncStackedNavLayout() {
      const next = readStackedNavLayout();
      const prev = this.stackedNavLayout;
      this.stackedNavLayout = next;
      if (prev === next) return;
      this.$nextTick(() => {
        if (this.contentView !== 'list') return;
        if (next) {
          this.destroyScrollSpy();
        } else {
          this.initScrollSpy();
        }
      });
    },
    scrollListContentToTop() {
      const scrollRoot = this.getScrollRoot();
      const contentEl = this.$el && this.$el.querySelector('.gxnlpt-content');
      if (scrollRoot && contentEl) {
        const rootRect = scrollRoot.getBoundingClientRect();
        const contentRect = contentEl.getBoundingClientRect();
        const targetTop = scrollRoot.scrollTop + (contentRect.top - rootRect.top);
        scrollRoot.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
        return;
      }
      if (scrollRoot) {
        scrollRoot.scrollTo({ top: 0, behavior: 'smooth' });
        return;
      }
      window.scrollTo({ top: 0, behavior: 'smooth' });
    },
    /**
     * 解析后端标签：支持 JSON 数组、对象数组、逗号分隔字符串
     * 例：'["全国","碳核查"]' | '[{"name":"全国"}]' | '全国,碳核查'
     */
    splitTagText(value) {
      if (value == null || value === '') return [];
      if (Array.isArray(value)) {
        return value.flatMap((item) => this.splitTagText(item));
      }
      if (typeof value === 'object') {
        const label =
          value.label ??
          value.name ??
          value.title ??
          value.text ??
          value.bqmc ??
          value.bqMc ??
          value.value;
        const text = label != null ? String(label).trim() : '';
        return text ? [text] : [];
      }
      const str = String(value).trim();
      if (!str) return [];
      if (str.startsWith('[') || str.startsWith('{')) {
        try {
          return this.splitTagText(JSON.parse(str));
        } catch (e) {
          /* 非 JSON，按逗号分隔 */
        }
      }
      return str
        .split(/[,，]/)
        .map((s) => s.trim())
        .filter(Boolean);
    },
    buildTagList(item) {
      const tags = [];
      const append = (list) => {
        list.forEach((t) => {
          if (t && !tags.includes(t)) tags.push(t);
        });
      };
      // 优先：后端标签合集 / 服务类型（后期可为 JSON）
      append(this.splitTagText(item.bqjh));
      append(this.splitTagText(item.tagList));
      append(this.splitTagText(item.bqList));
      append(this.splitTagText(item.fwlxjh));
      append(this.splitTagText(item.fwlxbq));
      if (Array.isArray(item.fwlxbqList) && item.fwlxbqList.length) {
        append(item.fwlxbqList);
      }
      const maxTags = DEMO_TAG_OVERFLOW_PREVIEW && FORCE_DEMO_PREVIEW ? 20 : 12;
      return tags.slice(0, maxTags);
    },
    attachTags(card) {
      const tagList = this.buildTagList(card);
      card.fwlxbqList = tagList;
      card.tagList = tagList;
      return card;
    },
    /** 解析 fwlxjh / bqjh（含 JSON）并生成 tagList */
    normalizeRecord(item) {
      const record = { ...item };
      record.gxUuid = record.wzUuid;
      record.fwnr = record.jj;
      const typeTags = this.splitTagText(record.fwlxjh);
      const labelTags = this.splitTagText(record.bqjh);
      record.fwlxbqList = [...new Set([...typeTags, ...labelTags])];
      return this.attachTags(record);
    },
    ensureCardTags(card, category) {
      if (this.useDemoData) {
        if (card.tagList && card.tagList.length) return;
        card.tagList = [];
        card.fwlxbqList = [];
        return;
      }
      if (card.tagList && card.tagList.length) return;
      const inferred = inferTagsFromContent(card, category);
      if (!inferred.length) {
        card.tagList = [];
        card.fwlxbqList = [];
        return;
      }
      card.tagList = inferred;
      card.fwlxbqList = inferred;
    },
    matchCategory(record, category) {
      // 优先按共性能力分类代码精确匹配（gxnlFlDm: 01~05）
      if (record.gxnlFlDm && category.flDm && record.gxnlFlDm === category.flDm) {
        return true;
      }
      // 兜底：按关键词匹配
      const text = `${record.bt1 || ''}${record.jj || ''}${record.fwlxjh || ''}${record.qymc || ''}`;
      return category.keywords.some((kw) => text.includes(kw));
    },
    assignCategoryCards(allRecords) {
      this.categoryList.forEach((cat) => {
        const list = allRecords
          .filter((r) => this.matchCategory(r, cat))
          .map((r) => {
            const card = this.normalizeRecord(r);
            this.ensureCardTags(card, cat);
            return card;
          });
        cat.cardList = list;
        cat.total = list.length;
        cat.expanded = false;
        this.refreshDisplayList(cat);
      });
    },
    applyDemoFallback() {
      this.useDemoData = true;
      this.categoryList.forEach((cat) => {
        const demos = DEMO_BY_CATEGORY[cat.id] || [];
        cat.cardList = demos.map((c, i) => {
          const demo = shouldInjectOverflowDemoTags(cat.id, i)
            ? { ...c, bqjh: mergeDemoOverflowTags(c.bqjh, cat.id) }
            : c;
          return this.attachTags({
            ...demo,
            _demoId: `${cat.id}-${i}`,
            gxUuid: '',
          });
        });
        cat.total = cat.cardList.length;
        cat.expanded = false;
        this.refreshDisplayList(cat);
      });
    },
    refreshDisplayList(category) {
      category.displayList = category.expanded
        ? category.cardList.slice()
        : category.cardList.slice(0, category.previewSize);
    },
    toggleViewMore(category) {
      category.expanded = !category.expanded;
      this.refreshDisplayList(category);
    },
    async loadAllSections() {
      this.pageLoading = true;
      this.useDemoData = false;
      if (FORCE_DEMO_PREVIEW) {
        this.applyDemoFallback();
        this.pageLoading = false;
        return;
      }
      try {
        const res = await api.wzxxList({
          pageNo: 1,
          pageSize: 200,
        });
        const data = res && res.data;
        const records = ((data && data.records) || []).map((r) => this.normalizeRecord(r));
        if (!records.length) {
          this.applyDemoFallback();
          return;
        }
        this.assignCategoryCards(records);
        const hasCards = this.categoryList.some((c) => c.displayList.length);
        if (!hasCards) {
          this.applyDemoFallback();
        }
      } catch (e) {
        console.error('共性能力列表加载失败', e);
        this.applyDemoFallback();
      } finally {
        this.pageLoading = false;
      }
    },
    getScrollRoot() {
      // 门户落地页共用滚动容器为 .content-wrap（main.vue 唯一滚动区），
      // 必须优先锁定它，避免被内部 .gxnlpt-side-nav-wrap 等 overflow-y:auto 抢走
      const portalRoot = document.querySelector('.content-wrap');
      if (portalRoot) return portalRoot;
      let el = this.$el.parentElement;
      while (el) {
        const style = window.getComputedStyle(el);
        if (/(auto|scroll)/.test(style.overflowY) && el.scrollHeight > el.clientHeight) {
          return el;
        }
        el = el.parentElement;
      }
      return null;
    },
    clearScrollUnlock() {
      if (this._scrollUnlockTimer) {
        clearTimeout(this._scrollUnlockTimer);
        this._scrollUnlockTimer = null;
      }
      const scrollRoot = this.getScrollRoot();
      if (scrollRoot && this._scrollEndHandler) {
        scrollRoot.removeEventListener('scrollend', this._scrollEndHandler);
        this._scrollEndHandler = null;
      }
    },
    unlockSectionObserver(tabIndex) {
      this.suppressSectionObserver = false;
      if (typeof tabIndex === 'number' && !Number.isNaN(tabIndex)) {
        this.activeTabIndex = tabIndex;
      }
    },
    scheduleScrollUnlock(tabIndex) {
      this.clearScrollUnlock();
      this.suppressSectionObserver = true;
      this.activeTabIndex = tabIndex;

      const scrollRoot = this.getScrollRoot();
      let unlocked = false;
      const done = () => {
        if (unlocked) return;
        unlocked = true;
        this.clearScrollUnlock();
        this.unlockSectionObserver(tabIndex);
      };

      if (scrollRoot) {
        this._scrollEndHandler = done;
        scrollRoot.addEventListener('scrollend', done, { once: true });
      }
      this._scrollUnlockTimer = setTimeout(done, 900);
    },
    onSideNavClick(sectionId, tabIndex) {
      if (this.contentView !== 'list') {
        this.contentView = 'list';
        this.$nextTick(() => {
          this.handleCategoryNav(sectionId, tabIndex);
        });
        return;
      }
      this.handleCategoryNav(sectionId, tabIndex);
    },
    handleCategoryNav(sectionId, tabIndex) {
      if (typeof tabIndex === 'number' && !Number.isNaN(tabIndex)) {
        this.activeTabIndex = tabIndex;
      }
      if (this.isStackedNavMode) {
        this.$nextTick(() => this.scrollListContentToTop());
        return;
      }
      this.initScrollSpy();
      this.initSidebarSticky();
      this.scrollToSection(sectionId, tabIndex);
    },
    openSubmitView() {
      if (!this.ensureLogin('提交收录')) {
        return;
      }
      this.destroyScrollSpy();
      this.destroySidebarSticky();
      this.contentView = 'submit';
      this.resetSubmitForm();
    },
    openFavoritesView() {
      if (!this.ensureLogin('查看我的收藏')) {
        return;
      }
      this.destroyScrollSpy();
      this.destroySidebarSticky();
      this.contentView = 'favorites';
      this.loadFavorites();
    },
    async loadFavorites() {
      this.favoritesLoading = true;
      try {
        const res = await api.myFavoriteList({ pageNo: 1, pageSize: 200 });
        const data = res && res.data;
        const records = ((data && data.records) || []).map((r) => this.normalizeRecord(r));
        this.favoriteList = records.map((card) => {
          card.scbz = 'Y';
          return card;
        });
      } catch (e) {
        if (isUnauthorizedError(e)) {
          showLoginGuide({ actionText: '查看我的收藏' });
          return;
        }
        this.$message.warning('我的收藏加载失败');
      } finally {
        this.favoritesLoading = false;
      }
    },
    resetSubmitForm() {
      this.submitForm = SUBMIT_FORM_EMPTY();
      this.submitTouched = {};
      this.submitErrors = {};
      this.submitAttempted = false;
    },
    touchSubmitField(field) {
      this.$set(this.submitTouched, field, true);
      this.validateSubmitField(field);
    },
    validateSubmitField(field) {
      const errors = { ...this.submitErrors };
      const form = this.submitForm;
      const required = {
        bt1: '请输入名称',
        lj: '请输入链接',
        jj: '请输入简介',
      };
      if (field === 'fl' && !form.fl) {
        errors.fl = '请选择分类';
      } else if (field === 'fl') {
        delete errors.fl;
      } else if (required[field] && !form[field]) {
        errors[field] = required[field];
      } else if (required[field]) {
        delete errors[field];
      }
      if (field === 'lj' && form.lj && !/^https?:\/\//i.test(form.lj)) {
        errors.lj = '请输入有效链接（以 http:// 或 https:// 开头）';
      }
      this.submitErrors = errors;
      return !errors[field];
    },
    validateSubmitForm() {
      const fields = ['bt1', 'lj', 'jj', 'fl'];
      let valid = true;
      fields.forEach((field) => {
        this.$set(this.submitTouched, field, true);
        if (!this.validateSubmitField(field)) {
          valid = false;
        }
      });
      return valid;
    },
    async handleSubmitForm() {
      if (!this.ensureLogin('提交收录')) {
        return;
      }
      this.submitAttempted = true;
      if (!this.validateSubmitForm()) {
        return;
      }
      if (this.useDemoData) {
        this.$message.success('提交成功，请等待审核');
        this.resetSubmitForm();
        this.contentView = 'list';
        return;
      }
      try {
        await api.submitSlxx({
          bt: this.submitForm.bt1,
          wzLj: this.submitForm.lj,
          jj: this.submitForm.jj,
          gxnlFlDm: flTitleToDm(this.submitForm.fl),
          bqjh: this.submitForm.bq,
        });
        this.$message.success('提交成功，请等待审核');
        this.resetSubmitForm();
        this.contentView = 'list';
        await this.loadAllSections();
      } catch (e) {
        if (isUnauthorizedError(e)) {
          showLoginGuide({ actionText: '提交收录' });
          return;
        }
        this.$message.warning('提交失败，请稍后重试');
      }
    },
    /**
     * 平滑滚动到指定分类区块。
     * 通过 getBoundingClientRect 获取目标元素在视口内的视觉位置，
     * 反算至滚动容器布局坐标后 scrollTo，不受 scale 变换影响。
     */
    scrollToSection(sectionId, tabIndex) {
      const el = document.getElementById(sectionId);
      const scrollRoot = this.getScrollRoot();
      if (!el || !scrollRoot) return;

      const scale = this._readScale();
      if (scale <= 0) return;

      const rootVisualTop = scrollRoot.getBoundingClientRect().top;
      const elVisualTop = el.getBoundingClientRect().top;
      // 滚动容器内视觉距离 → 布局距离
      const visualDist = elVisualTop - rootVisualTop;
      const layoutDist = visualDist / scale;
      const layoutTarget = scrollRoot.scrollTop + layoutDist;

      // nav (64dp) + gap (24dp)
      const OFFSET_DP = 88; // 设计稿像素
      const target = Math.max(0, layoutTarget - OFFSET_DP);

      scrollRoot.scrollTo({ top: target, behavior: 'smooth' });
      this.scheduleScrollUnlock(tabIndex);
    },
    /* ========== IntersectionObserver 驱动的 ScrollSpy ========== */
    /**
     * 用 IntersectionObserver 监听每个分类区块与滚动容器顶部的交叉比例，
     * 选择最接近顶部且露出面积最大的区块为当前激活项。
     * 相比手写 scroll 事件更精准、更省性能，且不受 scale 变换影响。
     */
    initScrollSpy() {
      this.destroyScrollSpy();
      if (this.isStackedNavMode || this.contentView !== 'list') return;

      const scrollRoot = this.getScrollRoot();
      if (!scrollRoot) return;
      this.scrollRootEl = scrollRoot;

      // 区块交叉记录：{ index, ratio }
      this._spyEntries = [];
      this._spyRootRect = null;

      const obsOptions = {
        root: scrollRoot,
        // rootMargin 在顶部留出 nav(64dp)+gap(24dp)=88dp 的缓冲区，
        // 让区块顶部触及该线时才算"进入视口"。
        rootMargin: `-88px 0px 0px 0px`,
        threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
      };

      this._spyObserver = new IntersectionObserver((entries) => {
        const rootRect = scrollRoot.getBoundingClientRect();
        this._spyRootRect = rootRect;

        entries.forEach((entry) => {
          const index = Number(entry.target.dataset.sectionIndex);
          if (Number.isNaN(index)) return;
          const existing = this._spyEntries.find((e) => e.index === index);
          if (existing) {
            existing.ratio = entry.intersectionRatio;
          } else {
            this._spyEntries.push({ index, ratio: entry.intersectionRatio });
          }
        });

        this._applySpyResult();
      }, obsOptions);

      // 监听所有 gxnlpt-block 区块
      const blocks = this.$el.querySelectorAll('.gxnlpt-block[data-section-index]');
      blocks.forEach((block) => this._spyObserver.observe(block));

      // 立刻触发一次，cover 初始状态
      this.$nextTick(() => {
        this._spyEntries = [];
        const rootRect = scrollRoot.getBoundingClientRect();
        blocks.forEach((block, index) => {
          const sectionEl = document.getElementById(this.categoryList[index]?.id);
          if (!sectionEl) return;
          // 手动检测：section 顶部是否已在缓冲区线以下
          const sectionTop = sectionEl.getBoundingClientRect().top;
          const line = rootRect.top + 88;
          if (sectionTop <= line) {
            this._spyEntries.push({ index, ratio: sectionTop <= rootRect.top ? 1 : 0.5 });
          }
        });
        this._applySpyResult();
      });
    },
    _applySpyResult() {
      if (this.suppressSectionObserver) return;
      if (!this._spyEntries.length) return;

      // 优先取 ratio > 0 且最接近顶部的区块
      const visible = this._spyEntries.filter((e) => e.ratio > 0);
      if (visible.length) {
        // ratio 最高的（最靠近顶部）
        const best = visible.reduce((a, b) => (a.ratio >= b.ratio ? a : b));
        if (this.activeTabIndex !== best.index) {
          this.activeTabIndex = best.index;
        }
        return;
      }

      // 没有交叉的：取最后一个"section 顶部已在缓冲区线之上"的
      const rootRect = this._spyRootRect || this.getScrollRoot()?.getBoundingClientRect();
      if (!rootRect) return;
      const line = rootRect.top + 88;
      let lastAbove = 0;
      this.categoryList.forEach((item, index) => {
        const el = document.getElementById(item.id);
        if (!el) return;
        if (el.getBoundingClientRect().top <= line) {
          lastAbove = index;
        }
      });
      if (this.activeTabIndex !== lastAbove) {
        this.activeTabIndex = lastAbove;
      }
    },
    destroyScrollSpy() {
      if (this._spyObserver) {
        this._spyObserver.disconnect();
        this._spyObserver = null;
      }
      this._spyEntries = [];
      this._spyRootRect = null;
      this.scrollRootEl = null;
    },
    /* ========== 侧边导航 sticky 模拟（替代 CSS position:sticky） ========== */
    /**
     * 原因：.portal-figma-scale-stage 有 transform:scale()，
     * 会创建新的 containing block，导致 CSS position:sticky 完全失效。
     *
     * 解决方案：
     * - 每帧滚动时，实时测量侧边栏"自然视觉位置"（无 transform 时的 getBoundingClientRect）
     * - 与导航栏上方 88dp (64dp 导航栏 + 24dp 间距) 比较
     * - 若自然位置已被滚出视口上方，用 translateY 推回来
     * - translateY 值不超过 sidebar 容器高度，防止侧边栏脱出
     *
     * 坐标系：
     * - getBoundingClientRect → 视觉像素（scale 后）
     * - translateY → 布局像素（scale 容器内部空间）
     */
    _readScale() {
      if (typeof document === 'undefined') return 1;
      const val = parseFloat(
        getComputedStyle(document.documentElement).getPropertyValue('--home-figma-scale'),
      );
      return val > 0 ? val : (window.innerWidth >= 768 ? window.innerWidth / 1440 : 1);
    },
    /**
     * 测量 sticky 元素当前的自然视觉 top（清除 transform 后测量，再恢复）。
     * 返回 { naturalVisualTop, stickyHeight, sidebarHeight } 三个视觉像素值。
     * 若元素不存在或 scale 无效则返回 null。
     */
    _measureStickyNatural() {
      const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
      const sidebarEl = stickyEl?.parentElement;
      const scrollRoot = this.getScrollRoot();
      const scale = this._readScale();
      if (!stickyEl || !sidebarEl || !scrollRoot || scale <= 0) return null;

      // 保存当前 transform，清除后测量自然位置
      const saved = stickyEl.style.transform;
      stickyEl.style.transform = '';
      const rect = stickyEl.getBoundingClientRect();
      const rootRect = scrollRoot.getBoundingClientRect();
      const sidebarRect = sidebarEl.getBoundingClientRect();

      // 恢复 transform
      stickyEl.style.transform = saved;

      return {
        // 自然视觉 top（相对于滚动容器 visual top）
        naturalVisualTop: rect.top - rootRect.top,
        // sticky 元素视觉高度
        stickyHeight: rect.height,
        // 父容器视觉高度
        sidebarHeight: sidebarRect.height,
        // 父容器视觉 top（相对于滚动容器 visual top）
        sidebarVisualTop: sidebarRect.top - rootRect.top,
      };
    },
    initSidebarSticky() {
      this.destroySidebarSticky();
      if (this.isStackedNavMode || this.contentView !== 'list') return;

      const scrollRoot = this.getScrollRoot();
      if (!scrollRoot) return;
      this._sidebarStickyScrollEl = scrollRoot;

      const handler = () => {
        if (this._sidebarStickyRaf) return;
        this._sidebarStickyRaf = requestAnimationFrame(() => {
          this._sidebarStickyRaf = null;
          this.updateSidebarSticky();
        });
      };

      scrollRoot.addEventListener('scroll', handler, { passive: true });
      this._onSidebarStickyScroll = handler;

      // resize 时 scale 可能变化，触发重新测量
      this._onSidebarResize = () => {
        if (this._sidebarResizeTimer) clearTimeout(this._sidebarResizeTimer);
        this._sidebarResizeTimer = setTimeout(() => {
          this.updateSidebarSticky();
        }, 60);
      };
      window.addEventListener('resize', this._onSidebarResize);

      // figmaStage 尺寸变化（异步数据填充导致），也触发重新测量
      const stage = this.$refs.figmaStage;
      if (stage && typeof ResizeObserver !== 'undefined') {
        this._sidebarStageObserver = new ResizeObserver(() => {
          if (this._sidebarStageTimer) clearTimeout(this._sidebarStageTimer);
          this._sidebarStageTimer = setTimeout(() => {
            this.updateSidebarSticky();
          }, 80);
        });
        this._sidebarStageObserver.observe(stage);
      }

      this.$nextTick(() => this.updateSidebarSticky());
    },
    destroySidebarSticky() {
      if (this._sidebarStickyScrollEl && this._onSidebarStickyScroll) {
        this._sidebarStickyScrollEl.removeEventListener('scroll', this._onSidebarStickyScroll);
      }
      this._sidebarStickyScrollEl = null;
      this._onSidebarStickyScroll = null;
      if (this._sidebarStickyRaf) {
        cancelAnimationFrame(this._sidebarStickyRaf);
        this._sidebarStickyRaf = null;
      }
      if (this._onSidebarResize) {
        window.removeEventListener('resize', this._onSidebarResize);
        this._onSidebarResize = null;
      }
      if (this._sidebarResizeTimer) {
        clearTimeout(this._sidebarResizeTimer);
        this._sidebarResizeTimer = null;
      }
      if (this._sidebarStageObserver) {
        this._sidebarStageObserver.disconnect();
        this._sidebarStageObserver = null;
      }
      if (this._sidebarStageTimer) {
        clearTimeout(this._sidebarStageTimer);
        this._sidebarStageTimer = null;
      }

      const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
      if (stickyEl) stickyEl.style.transform = '';
    },
    updateSidebarSticky() {
      const stickyEl = this.$el?.querySelector('.gxnlpt-sidebar-sticky');
      if (!stickyEl) return;

      const info = this._measureStickyNatural();
      if (!info) return;

      const scale = this._readScale();
      // CSS position:sticky 的 top:24px → 相对于滚动容器顶部 24dp
      // naturalVisualTop 也是相对于滚动容器顶部，所以用 24 直接对比
      const DESIRED_DP = 24;
      const desiredVisualTop = DESIRED_DP * scale;

      const naturalVisualTop = info.naturalVisualTop;

      // 自然位置已在目标线之下 → 不需要补偿
      if (naturalVisualTop >= desiredVisualTop) {
        stickyEl.style.transform = '';
        return;
      }

      // 需要推回去的视觉像素距离
      const visualOffset = desiredVisualTop - naturalVisualTop;
      // 转为布局像素（translateY 在 scale 容器内以布局像素为单位）
      const layoutOffset = visualOffset / scale;

      // 不能推出父容器底部
      // sticky 元素在 sidebar 内部有 padding 偏移，需要核算 sidebar 顶部到 sticky 顶部
      // 的真实可用距离，而不能简单地 sidebarHeight - stickyHeight
      const sidebarBottom = info.sidebarVisualTop + info.sidebarHeight;
      const stickyBottom = info.naturalVisualTop + info.stickyHeight;
      // 允许的视觉余量：sidebar 底部 - sticky 自然底部
      const availableVisual = Math.max(0, sidebarBottom - stickyBottom);
      // 由于 naturalVisualTop 已经包含了 padding 偏移，所以 availableVisual
      // 就是 sticky 底部到 sidebar 底部的真实视觉距离
      const maxLayoutOffset = availableVisual / scale;
      const clamped = Math.min(layoutOffset, maxLayoutOffset);

      stickyEl.style.transform = clamped > 0
        ? `translateY(${clamped}px)`
        : '';
    },
    handleCardClick(card) {
      // 优先使用后端返回的 wzLj，否则用兜底数据的 lj
      const target = card.wzLj || card.lj;
      if (target) {
        window.open(target, '_blank', 'noopener,noreferrer');
        return;
      }
      // 仅当后端记录或兜底数据都缺失 lj 时，才走“敬请期待”兜底
      this.showComingSoon(card.bt1 ? `${card.bt1}（详情筹备中）` : '详情筹备中');
    },
    async handleCollect(card) {
      const isAdd = card.scbz !== 'Y';
      if (isAdd && !this.ensureLogin('收藏')) {
        return;
      }
      if (this.useDemoData || !card.gxUuid) {
        card.scbz = card.scbz === 'Y' ? 'N' : 'Y';
        return;
      }
      try {
        const type = card.scbz === 'Y' ? 'remove' : 'add';
        await api.toggleGxsc({ wzUuid: card.gxUuid, type });
        const next = card.scbz === 'Y' ? 'N' : 'Y';
        card.scbz = next;
        this.syncFavoriteListAfterToggle(card, next);
      } catch (e) {
        if (isUnauthorizedError(e)) {
          showLoginGuide({ actionText: '收藏' });
          return;
        }
        this.$message.warning('收藏操作失败');
      }
    },
    /** 收藏切换后同步本地 favoriteList，避免再次进入「我的收藏」时与服务端不一致 */
    syncFavoriteListAfterToggle(card, nextScbz) {
      const list = this.favoriteList || [];
      const idx = list.findIndex((c) => c.gxUuid && c.gxUuid === card.gxUuid);
      if (nextScbz === 'Y') {
        if (idx < 0) {
          list.unshift({ ...card, scbz: 'Y' });
        } else {
          list.splice(idx, 1, { ...card, scbz: 'Y' });
        }
      } else if (idx >= 0) {
        list.splice(idx, 1);
      }
      this.favoriteList = list;
    },
  },
};
</script>

<style lang="less" scoped>
@import '../../styles/breakpoints.less';
@import '../../styles/home-figma-variables.less';

/* Figma 150605:2210 共性能力 */
.gxnlpt-page {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  padding-top: 0;
  overflow: visible;
  font-family: @home-font-family;
  background: @gxnlpt-page-bg;
  box-sizing: border-box;
  .portal-figma-scale-page();
}

/* gxnlpt 内容远大于一屏（5 个分类 + 大量卡片）：
   - 整页走 Figma 缩放时，stage 内的 .gxnlpt-shell (max-width: 1200px) 会被整页 scale 缩放
     到视口宽度。stage 自身 layout 高度 = 内容自然高度（transform 不影响 layout 高度），
     这会远大于 .content-wrap 的固定高度，理论上 .content-wrap 会出现滚动条。
   - 关键陷阱：home-figma-variables.less 中
        html.portal-figma-scale-active.portal-landing-figma-scale-active
          .portal-landing-figma-page { height: var(--home-figma-visual-height) !important; ... }
     会把 page 高度 clamp 到 --home-figma-visual-height。
     而 --home-figma-visual-height 由 syncHomeFigmaStageLayout 在 mounted nextTick 写入：
        visualHeight = stage.offsetHeight * scale
     在 gxnlpt 上 stage 此时还是初始高度（demo 数据/卡片未装填），算出来的值比真实
     stage 高度小几百到上千像素，于是 page 永远被 clamp 裁切，
     --home-figma-visual-height 也不会被重算（除了 resize）。
     表现：F12 关闭视口时 height=616 → 裁切剩 1 屏；F12 打开后视口高度变化 →
     resize 重算 visualHeight → 变成一个不同的值 → 整页能塞下。
   - 解法：让 .gxnlpt-page 不依赖 --home-figma-visual-height 决定高度。直接重写为
     height:auto / max-height:none / overflow:visible。 */
html.portal-figma-scale-active.portal-landing-figma-scale-active .gxnlpt-page,
html.portal-figma-scale-active .gxnlpt-page {
  /* 不再走 landing page 的 height clamp，否则 page 高度被卡死，外层不会滚动 */
  height: auto !important;
  max-height: none !important;
  overflow: visible !important;

  .portal-figma-scale-viewport {
    /* viewport 自身高度由 stage 自然撑开，不再被 clamp 到 --home-figma-visual-height。
       这样 .content-wrap 内的 .gxnlpt-page 能被 5 个分类的内容自然撑高。 */
    height: auto !important;
    /* 保持 overflow:hidden 也无害：stage 是 transform:scale 缩放显示，
       layout 高度 = 缩放前高度，viewport 高度 = 同一个 layout 高度，stage 不会被裁切。 */
  }
}

.gxnlpt-shell.page-content-wrap {
  display: flex;
  flex: 1 0 auto;
  flex-direction: column;
  padding-top: @gxnlpt-shell-padding-top;
  padding-bottom: @gxnlpt-shell-padding-bottom;
}

.gxnlpt-layout {
  display: grid;
  flex: 1 1 auto;
  grid-template-columns: @gxnlpt-sidebar-width minmax(0, 1fr);
  gap: @gxnlpt-layout-gap;
  align-items: stretch;
  min-height: @gxnlpt-content-min-height;
}

@gxnlpt-side-actions-block-h: 114px;
@gxnlpt-side-actions-gap-top: 20px;

/* Figma Frame_left 602px：白卡片，高度拉伸至与右侧内容列同高，
   为 sticky 侧边导航提供足够的垂直滚动空间，确保导航始终可见。
   空白区域由 gxnlpt-sidebar-tail 填充，符合设计稿"白底向下铺满"的意图。 */
.gxnlpt-sidebar {
  display: flex;
  flex-direction: column;
  align-self: stretch;
  width: @gxnlpt-sidebar-width;
  min-height: 0;
  padding: @gxnlpt-sidebar-padding;
  background: #fff;
  border-radius: @gxnlpt-sidebar-radius;
  box-sizing: border-box;
}

/* 分类 + 收录/收藏：滚动时钉在视口 */
.gxnlpt-sidebar-sticky {
  position: sticky;
  top: @gxnlpt-sidebar-sticky-top;
  z-index: 20;
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  max-height: calc(
    100vh - var(--page-offset-top, @home-nav-height) - @gxnlpt-shell-padding-top -
      @gxnlpt-shell-padding-bottom - @gxnlpt-sidebar-sticky-top
  );
}

/* 图三：分类过多时在块内滚动 */
.gxnlpt-side-nav-wrap {
  flex: 0 1 auto;
  min-height: 0;
  max-height: calc(
    100vh - var(--page-offset-top, @home-nav-height) - @gxnlpt-shell-padding-top -
      @gxnlpt-shell-padding-bottom - @gxnlpt-sidebar-sticky-top -
      @gxnlpt-side-actions-block-h - @gxnlpt-side-actions-gap-top -
      @gxnlpt-sidebar-padding * 2
  );
  overflow-x: hidden;
  overflow-y: auto;
}

.gxnlpt-side-nav {
  display: flex;
  flex-direction: column;
  gap: @gxnlpt-side-nav-gap;
  align-items: stretch;
  width: 100%;
}

/* 图二：紧挨最后一条分类，仅上边框 + 20px 间距 */
.gxnlpt-side-actions {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  gap: @gxnlpt-side-nav-gap;
  margin-top: 0;
  padding-top: @gxnlpt-side-actions-gap-top;
  border-top: 1px solid @gxnlpt-side-action-border;
}

.gxnlpt-sidebar-tail {
  flex: 1 1 0;
  min-height: 0;
}

.gxnlpt-side-action {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  width: 100%;
  min-height: 42px;
  margin: 0;
  padding: 10px 20px;
  font-family: @home-font-family;
  font-size: @gxnlpt-side-action-size;
  font-weight: 400;
  line-height: normal;
  color: @gxnlpt-side-action-color;
  cursor: pointer;
  appearance: none;
  border: none;
  border-radius: @gxnlpt-side-item-radius-active;
  background: @gxnlpt-side-action-bg;
  box-sizing: border-box;
  outline: none;
  transition: opacity 0.2s ease, box-shadow 0.2s ease;

  &:hover {
    opacity: 0.88;
  }

  &.is-active {
    box-shadow: inset 0 0 0 1px @gxnlpt-side-action-color;
  }

  &:focus-visible {
    box-shadow: 0 0 0 2px fade(@gxnlpt-side-action-color, 35%);
  }
}

.gxnlpt-side-action-icon {
  flex-shrink: 0;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;

  &--cloud {
    width: 20px;
    height: 20px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M14.5 12.5H6.2C4.4 12.5 3 11.1 3 9.3 3 7.6 4.2 6.2 5.9 6c.3-2 2.2-3.5 4.4-3.5 1.5 0 2.8.7 3.6 1.8.4-.1.8-.2 1.2-.2 2 0 3.6 1.6 3.6 3.6 0 2-1.6 3.6-3.6 3.6h-.6z' stroke='%2300b96b' stroke-width='1.2'/%3E%3C/svg%3E");
  }

  &--star {
    width: 14px;
    height: 14px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M7 1.2l1.45 3.1 3.35.5-2.42 2.36.57 3.34L7 9.2l-2.95 1.3.57-3.34-2.42-2.36 3.35-.5L7 1.2z' fill='%23f5a623'/%3E%3C/svg%3E");
  }
}

.gxnlpt-side-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: @gxnlpt-side-item-height;
  margin: 0;
  padding: 10px 20px;
  font-family: @home-font-family;
  text-align: left;
  color: inherit;
  cursor: pointer;
  appearance: none;
  border: none;
  border-radius: @gxnlpt-side-item-radius;
  background: transparent;
  box-sizing: border-box;
  outline: none;
  transition: background 0.2s ease, color 0.2s ease, border-radius 0.2s ease;

  &:focus-visible {
    box-shadow: 0 0 0 2px fade(@home-color-primary-green, 35%);
  }

  &:not(.is-active):hover {
    background: @gxnlpt-page-bg;
  }

  &.is-active {
    border-radius: @gxnlpt-side-item-radius-active;
    background: linear-gradient(180deg, @home-color-primary-green 0%, @home-color-primary-green-dark 100%);

    .gxnlpt-side-label {
      font-weight: 600;
      color: #fff;
    }
  }
}

.gxnlpt-side-icon {
  display: block;
  width: 24px;
  height: 24px;
  object-fit: contain;
  justify-self: center;
  flex-shrink: 0;
}

.gxnlpt-side-label {
  min-width: 0;
  overflow: hidden;
  font-size: @gxnlpt-side-label-size;
  font-weight: 400;
  line-height: normal;
  color: #000;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gxnlpt-content {
  display: flex;
  flex-direction: column;
  gap: @gxnlpt-section-gap;
  min-width: 0;
  min-height: @gxnlpt-content-min-height;
  padding: @gxnlpt-content-padding;
  overflow-x: hidden;
  overflow-y: visible;
  background: #fff;
  border-radius: @gxnlpt-sidebar-radius;
  box-sizing: border-box;
}

.gxnlpt-block {
  display: flex;
  flex-direction: column;
  gap: @gxnlpt-block-gap;
  align-items: stretch;
  width: 100%;
  margin: 0;
  scroll-margin-top: 24px;
}

.gxnlpt-block-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  padding-bottom: 20px;
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}

.gxnlpt-block-icon {
  width: 16px;
  height: 16px;
  flex-shrink: 0;
}

.gxnlpt-block-title {
  margin: 0;
  font-size: 16px;
  font-weight: 600;
  line-height: 22px;
  color: @home-color-primary-dark;
}

.gxnlpt-block-subtitle {
  margin-left: auto;
  font-size: 12px;
  font-weight: 400;
  line-height: 20px;
  color: rgba(0, 59, 26, 0.6);
  white-space: nowrap;
}

/* ========== 碳链接专区 ========== */
.gxnlpt-carbon-links {
  margin-top: 36px;
}

.gxnlpt-carbon-group {
  margin-top: 18px;
}

.gxnlpt-carbon-group-title {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  font-size: 14px;
  font-weight: 600;
  line-height: 22px;
  color: @home-color-primary-dark;
}

.gxnlpt-carbon-group-bar {
  display: inline-block;
  width: 4px;
  height: 16px;
  background: linear-gradient(180deg, @home-color-primary-green 0%, @home-color-primary-green-dark 100%);
  border-radius: 2px;
}

.gxnlpt-carbon-group-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 18px;
  padding: 0 6px;
  font-size: 12px;
  font-weight: 500;
  line-height: 18px;
  color: @home-color-primary-green-dark;
  background: rgba(0, 185, 107, 0.12);
  border-radius: 9px;
}

.gxnlpt-carbon-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 10px 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.gxnlpt-carbon-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 56px;
  padding: 10px 14px;
  background: #ffffff;
  border: 1px solid rgba(0, 185, 107, 0.2);
  border-radius: 6px;
  transition: all 0.25s ease;
  box-sizing: border-box;
}

.gxnlpt-carbon-item:hover {
  border-color: @home-color-primary-green;
  background: rgba(0, 185, 107, 0.04);
  box-shadow: 0 4px 12px rgba(0, 154, 41, 0.12);
}

.gxnlpt-carbon-link {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  text-decoration: none;
  color: @home-color-primary-dark;
}

.gxnlpt-carbon-name {
  font-size: 14px;
  font-weight: 500;
  line-height: 22px;
  color: @home-color-primary-dark;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gxnlpt-carbon-desc {
  font-size: 12px;
  font-weight: 400;
  line-height: 18px;
  color: rgba(0, 59, 26, 0.6);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gxnlpt-carbon-link:hover .gxnlpt-carbon-name {
  color: @home-color-primary-green-dark;
}

.gxnlpt-carbon-external {
  flex-shrink: 0;
  font-size: 14px;
  color: @home-color-primary-green;
  opacity: 0.5;
  transition: opacity 0.2s ease, transform 0.2s ease;
}

.gxnlpt-carbon-item:hover .gxnlpt-carbon-external {
  opacity: 1;
  transform: translate(2px, -2px);
}

@media (max-width: 640px) {
  .gxnlpt-block-subtitle {
    display: none;
  }
  .gxnlpt-carbon-list {
    grid-template-columns: 1fr;
  }
}

.gxnlpt-state {
  padding: 32px 0;
  font-size: 14px;
  color: #999;
  text-align: center;
}

.gxnlpt-card-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: @gxnlpt-card-grid-gap;
  align-items: stretch;
}

.gxnlpt-card {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  box-sizing: border-box;
  min-height: @gxnlpt-card-min-height;
  padding: @gxnlpt-card-padding-y @gxnlpt-card-padding-x;
  background: #fff;
  border: 1px solid fade(#000, 10%);
  border-radius: @gxnlpt-card-radius;
  cursor: pointer;
  transition: border-color 0.2s ease, box-shadow 0.2s ease, border-radius 0.2s ease;

  &:hover {
    border-color: @home-color-primary-green;
    border-radius: @gxnlpt-card-radius-hover;
    box-shadow: @home-shadow-card-green;
  }
}

.gxnlpt-card-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  width: 100%;
  min-width: 0;
  overflow: hidden;
}

.gxnlpt-card-body .gxnlpt-card-tags {
  flex-shrink: 0;
  margin-top: 2px;
}

.gxnlpt-card-info {
  display: flex;
  flex-shrink: 0;
  flex-direction: column;
  gap: 4px;
  width: 100%;
  min-width: 0;
}

.gxnlpt-card-name-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  min-height: 22px;
}

.gxnlpt-card-name {
  flex: 1;
  min-width: 0;
  margin: 0;
  overflow: hidden;
  font-size: 16px;
  font-weight: 500;
  line-height: 22px;
  color: #333;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gxnlpt-card-org {
  margin: 0;
  min-height: 20px;
  overflow: hidden;
  font-size: 14px;
  font-weight: 400;
  line-height: 20px;
  color: #666;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.gxnlpt-card--expired {
  opacity: 0.55;
  cursor: default;
  pointer-events: none;
}

.gxnlpt-card--expired .gxnlpt-card-name {
  color: #bbb;
}

.gxnlpt-card--expired .gxnlpt-card-org {
  color: #ccc;
}

.gxnlpt-card--expired:hover {
  border-color: fade(#000, 10%);
  box-shadow: none;
}

.gxnlpt-card-expired-tag {
  flex-shrink: 0;
  padding: 1px 6px;
  font-size: 11px;
  line-height: 18px;
  color: #999;
  background: #f5f5f5;
  border-radius: 3px;
  pointer-events: auto;
}

.gxnlpt-card-star {
  flex-shrink: 0;
  width: 14px;
  height: 14px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;

  img {
    display: block;
    width: 14px;
    height: 14px;
  }
}

.gxnlpt-more-wrap {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 30px;
  box-sizing: border-box;
}

.gxnlpt-more {
  display: inline-block;
  margin: 0;
  padding: 0;
  font-family: @home-font-family;
  font-size: 14px;
  font-weight: 400;
  line-height: 20px;
  color: @gxnlpt-more-color;
  letter-spacing: 0;
  white-space: nowrap;
  cursor: pointer;
  appearance: none;
  background: transparent;
  border: none;
  outline: none;

  &:hover {
    opacity: 0.75;
  }

  &:focus-visible {
    box-shadow: 0 0 0 2px fade(@home-color-primary-dark, 25%);
    border-radius: 2px;
  }
}

/* 收录表单 */
.gxnlpt-submit {
  display: flex;
  flex-direction: column;
  gap: 24px;
  width: 100%;
  min-height: 0;
  overflow-y: auto;
}

.gxnlpt-submit-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px 24px;
  padding-bottom: 20px;
  border-bottom: 1px solid fade(#000, 10%);
}

.gxnlpt-submit-title-row {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 22px;
}

.gxnlpt-submit-title-icon {
  display: block;
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath d='M14.5 12.5H6.2C4.4 12.5 3 11.1 3 9.3 3 7.6 4.2 6.2 5.9 6c.3-2 2.2-3.5 4.4-3.5 1.5 0 2.8.7 3.6 1.8.4-.1.8-.2 1.2-.2 2 0 3.6 1.6 3.6 3.6 0 2-1.6 3.6-3.6 3.6h-.6z' stroke='%2300b96b' stroke-width='1.2'/%3E%3C/svg%3E")
    center / 20px 20px no-repeat;
}

.gxnlpt-submit-title {
  margin: 0;
  padding: 0;
  font-size: 16px;
  font-weight: 600;
  line-height: 22px;
  color: @home-color-primary-dark;
}

.gxnlpt-field-label {
  display: inline-flex;
  align-items: center;
  font-size: 14px;
  line-height: 20px;
  color: #333;
}

.gxnlpt-field-required {
  margin-right: 4px;
  font-size: 14px;
  line-height: 20px;
  color: @gxnlpt-form-error;
}

.gxnlpt-submit-notice {
  flex: 1 1 280px;
  max-width: 420px;
  margin: 0;
  font-size: 12px;
  line-height: 18px;
  color: @home-color-text-muted;
  text-align: right;
}

.gxnlpt-submit-form {
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.gxnlpt-submit-body {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.gxnlpt-submit-row-2 {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0 24px;
}

.gxnlpt-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 16px;

  &:last-child {
    margin-bottom: 0;
  }
}

.gxnlpt-input,
.gxnlpt-textarea,
.gxnlpt-select {
  width: 100%;
  padding: 10px 12px;
  font-family: @home-font-family;
  font-size: 14px;
  line-height: 20px;
  color: #333;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  background: #fff;
  box-sizing: border-box;
  outline: none;
  transition: border-color 0.2s ease;

  &:focus {
    border-color: @home-color-primary-green;
  }

  &.is-error {
    border-color: @gxnlpt-form-error;
  }
}

.gxnlpt-textarea {
  min-height: 88px;
  resize: vertical;
}

.gxnlpt-field-counter {
  align-self: flex-end;
  font-size: 12px;
  line-height: 18px;
  color: @home-color-text-muted;
}

.gxnlpt-field-error {
  font-size: 12px;
  line-height: 18px;
  color: @gxnlpt-form-error;
}

.gxnlpt-submit-btn {
  width: 100%;
  min-height: 48px;
  font-family: @home-font-family;
  font-size: 16px;
  font-weight: 500;
  color: #fff;
  cursor: pointer;
  border: none;
  border-radius: 8px;
  background: linear-gradient(180deg, @home-color-primary-green 0%, @home-color-primary-green-dark 100%);
}

.gxnlpt-favorites {
  display: flex;
  flex-direction: column;
  gap: @gxnlpt-block-gap;
  width: 100%;
  min-height: 0;
  overflow-y: auto;
}

.gxnlpt-favorites-head {
  margin: 0;
}

.gxnlpt-favorites-star {
  width: 16px;
  height: 16px;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M7 1.2l1.45 3.1 3.35.5-2.42 2.36.57 3.34L7 9.2l-2.95 1.3.57-3.34-2.42-2.36 3.35-.5L7 1.2z' fill='%23f5a623'/%3E%3C/svg%3E")
    center / contain no-repeat;
}

/* 窄屏：顶部分类子导航 + 下方单块内容（对齐图二 / 服务中心子项） */
.gxnlpt-shell--stacked {
  padding-top: 0;
  padding-left: 0;
  padding-right: 0;
  padding-bottom: 24px;
}

.gxnlpt-layout--stacked {
  grid-template-columns: 1fr;
  gap: 0;
}

.gxnlpt-sidebar--panel {
  width: 100%;
  min-height: 0;
  padding: 12px 16px 16px;
  background: @gxnlpt-page-bg;
  border-radius: 0;
  box-shadow: none;
}

.gxnlpt-sidebar--panel .gxnlpt-side-item:not(.is-active):hover {
  background: fade(#fff, 72%);
}

.gxnlpt-layout--stacked .gxnlpt-sidebar-sticky {
  position: sticky;
  top: 0;
  z-index: 30;
  max-height: none;
  background: @gxnlpt-page-bg;
}

/* JS 未就绪时仍保证 H5 布局与导航可点（与 STACKED_NAV_MEDIA 一致） */
@media screen and (max-width: @mq-desktop-sm-max) {
  .gxnlpt-layout {
    grid-template-columns: 1fr;
    gap: 0;
  }

  .gxnlpt-sidebar {
    width: 100%;
    min-height: 0;
    background: @gxnlpt-page-bg;
    border-radius: 0;
    box-shadow: none;
  }

  .gxnlpt-sidebar-sticky {
    position: sticky;
    top: 0;
    z-index: 30;
    background: @gxnlpt-page-bg;
  }

  .gxnlpt-shell.page-content-wrap {
    padding-top: 0;
    padding-left: 0;
    padding-right: 0;
  }

  .gxnlpt-side-nav {
    flex-direction: column;
    flex-wrap: nowrap;
  }

  .gxnlpt-side-item {
    flex: none;
    width: 100%;
    min-width: 0;
  }

  .gxnlpt-sidebar-tail {
    display: none;
  }
}

.gxnlpt-layout--stacked .gxnlpt-side-nav-wrap {
  max-height: none;
  overflow: visible;
}

.gxnlpt-layout--stacked .gxnlpt-sidebar-tail {
  display: none;
}

.gxnlpt-layout--stacked .gxnlpt-side-nav {
  flex-direction: column;
  flex-wrap: nowrap;
  gap: @gxnlpt-side-nav-gap;
}

.gxnlpt-layout--stacked .gxnlpt-side-item {
  flex: none;
  width: 100%;
  min-width: 0;
}

.gxnlpt-layout--stacked .gxnlpt-side-actions {
  flex-direction: row;
  gap: 12px;
}

.gxnlpt-layout--stacked .gxnlpt-side-action {
  flex: 1 1 0;
  min-width: 0;
}

.gxnlpt-shell--stacked .gxnlpt-content {
  min-height: 0;
  margin-left: 12px;
  margin-right: 12px;
  margin-top: 12px;
}

@media screen and (max-width: 1279px) {
  .gxnlpt-card-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .gxnlpt-submit-row-2 {
    grid-template-columns: 1fr;
  }

  .gxnlpt-submit-notice {
    max-width: none;
    text-align: left;
  }
}

@media screen and (max-width: 767px) {
  .gxnlpt-shell--stacked .gxnlpt-content {
    margin-left: 12px;
    margin-right: 12px;
    padding: 16px;
  }

  .gxnlpt-card-grid {
    grid-template-columns: 1fr;
  }
}
</style>
