/* ==========================================================================
   郄泽然毕设 · 共用设计系统
   components.css —— 组件层
   --------------------------------------------------------------------------
   依赖 tokens.css。所有组件只消费令牌，不写死颜色/尺寸。
   覆盖：布局骨架 / 导航 / 卡片 / 按钮 / 表单 / 列表 / 反馈 / 空态 / 作品集互链
   ========================================================================== */

/* ==========================================================================
   A. 基础重置与骨架
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  font-weight: var(--fw-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
  -webkit-tap-highlight-color: transparent;
}

h1, h2, h3, h4, h5, h6, p, figure, blockquote, dl, dd { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, picture, video, canvas, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; padding: 0; }
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hi); }

/* 焦点可见性：绝不移除 outline，只重塑它 */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--line-focus);
  outline-offset: 2px;
  border-radius: var(--r-xs);
}
/* 键盘用户在深色底上的焦点环要有内圈提亮，保证任何底色都看得见 */
[data-theme='night'] :focus-visible,
[data-theme='xuan-night'] :focus-visible {
  outline: 2px solid var(--line-focus);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--line-focus) 22%, transparent);
}

::selection {
  background: var(--accent-soft);
  color: var(--accent);
}

/* 滚动条：移动端隐藏，桌面端细线 */
.scroll-hide { scrollbar-width: none; -ms-overflow-style: none; }
.scroll-hide::-webkit-scrollbar { display: none; }

@media (pointer: fine) {
  * { scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
  *::-webkit-scrollbar { width: 8px; height: 8px; }
  *::-webkit-scrollbar-track { background: transparent; }
  *::-webkit-scrollbar-thumb {
    background: var(--line-strong);
    border-radius: var(--r-full);
    border: 2px solid var(--bg);
  }
  *::-webkit-scrollbar-thumb:hover { background: var(--muted); }
}

/* 屏幕阅读器专用 */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* 跳转到主内容（键盘可达性） */
.skip-link {
  position: absolute; left: var(--gutter); top: -100px;
  z-index: var(--z-loading);
  padding: var(--s-3) var(--s-4);
  background: var(--accent); color: var(--on-accent);
  border-radius: var(--r-sm);
  font-size: var(--t-sm); font-weight: var(--fw-semi);
  transition: top var(--dur-2) var(--ease-out);
}
.skip-link:focus { top: var(--s-3); color: var(--on-accent); }

/* ---------- 应用容器：全站统一的移动端画布 ---------- */
.app {
  position: relative;
  width: 100%;
  max-width: var(--app-max);
  margin-inline: auto;
  min-height: 100dvh;
  background: var(--bg);
  display: flex;
  flex-direction: column;
  isolation: isolate;
}
/* 桌面端：画布外留出对比底色，让移动端布局在宽屏上也有意识 */
@media (min-width: 560px) {
  body { background: var(--bg-sunken); }
  .app {
    box-shadow: var(--shadow-lg);
    min-height: 100dvh;
  }
}

.app-main {
  flex: 1;
  padding-bottom: calc(var(--safe-b) + var(--s-6));
}
.app-main--with-tabbar { padding-bottom: calc(var(--safe-b) + var(--tabbar-h) + var(--s-5)); }

/* 内容节流：统一左右边距 */
.wrap { padding-inline: var(--gutter); }
.wrap--wide { padding-inline: var(--s-5); }
.stack > * + * { margin-top: var(--s-4); }
.stack-sm > * + * { margin-top: var(--s-2); }
.stack-lg > * + * { margin-top: var(--s-6); }

/* ==========================================================================
   B. 顶部栏
   三种形态：默认（左返回+中标题）/ 大标题（首页）/ 透明（沉浸式）
   ========================================================================== */
.topbar {
  position: sticky;
  top: 0;
  z-index: var(--z-nav);
  display: flex;
  align-items: center;
  gap: var(--s-2);
  min-height: calc(var(--header-h) + var(--safe-t));
  padding: var(--safe-t) var(--s-3) 0;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: saturate(1.6) blur(16px);
  -webkit-backdrop-filter: saturate(1.6) blur(16px);
  border-bottom: 1px solid transparent;
  transition: border-color var(--dur-2) var(--ease-out),
              background var(--dur-2) var(--ease-out);
}
/* 滚动后才出现分隔线，静止时干净 */
.topbar.is-scrolled { border-bottom-color: var(--line); }

.topbar__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  flex: 0 0 auto;
  margin-inline: -6px;
  border-radius: var(--r-full);
  color: var(--ink);
  transition: background var(--dur-1) var(--ease-out);
}
.topbar__btn:hover { background: var(--surface-2); }
.topbar__btn:active { background: var(--accent-soft); }
.topbar__btn svg { width: 22px; height: 22px; }

.topbar__title {
  flex: 1;
  min-width: 0;
  font-size: var(--t-h3);
  font-weight: var(--fw-semi);
  letter-spacing: var(--ls-heading);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-align: center;
}
/* 有返回键时标题左对齐，首页无返回键时居中 */
.topbar--back .topbar__title { text-align: left; padding-left: var(--s-1); }

.topbar__sub {
  display: block;
  font-size: var(--t-micro);
  font-weight: var(--fw-normal);
  letter-spacing: var(--ls-body);
  color: var(--muted);
  margin-top: 1px;
}

/* 大标题形态：标题在内容流里，顶栏只留操作 */
.hero-head {
  padding: calc(var(--safe-t) + var(--s-6)) var(--gutter) var(--s-5);
}
.hero-head__eyebrow {
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  color: var(--accent);
  margin-bottom: var(--s-3);
}
.hero-head__title {
  font-family: var(--font-display);
  font-size: var(--t-display);
  font-weight: var(--fw-semi);
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  color: var(--ink);
  text-wrap: balance;
}
.hero-head__desc {
  margin-top: var(--s-4);
  font-size: var(--t-body);
  line-height: var(--lh-body);
  color: var(--muted);
  max-width: 34ch;
}

/* ==========================================================================
   C. 区块与标题
   ========================================================================== */
.section { padding-block: var(--s-7); }
.section--tight { padding-block: var(--s-5); }
.section--divide { border-top: 1px solid var(--line); }

.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  margin-bottom: var(--s-5);
}
.section-head__title {
  font-family: var(--font-display);
  font-size: var(--t-h2);
  font-weight: var(--fw-semi);
  letter-spacing: var(--ls-heading);
  line-height: var(--lh-heading);
  color: var(--ink);
}
.section-head__note {
  font-size: var(--t-xs);
  color: var(--muted);
  flex: 0 0 auto;
}
/* 中文标题下的细分隔线——比全宽带描边克制 */
.section-head__rule {
  height: 1px;
  background: linear-gradient(90deg, var(--accent-line), transparent);
  margin-top: var(--s-3);
}

/* ==========================================================================
   D. 卡片
   只有三档：flat（描边）/ raised（投影）/ inset（内凹底）
   统一用 --r-md，避免每站自造圆角
   ========================================================================== */
.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: var(--s-5);
}
.card--flat { box-shadow: none; }
.card--raised {
  border-color: transparent;
  box-shadow: var(--shadow-sm);
}
.card--raised:hover { box-shadow: var(--shadow-md); }
.card--inset {
  background: var(--surface-inset);
  border-color: var(--line);
  box-shadow: none;
}
.card--accent {
  background: var(--accent-soft);
  border-color: var(--accent-line);
}
.card--pad-sm { padding: var(--s-4); }
.card--pad-lg { padding: var(--s-6); }
.card--flush { padding: 0; overflow: hidden; }

/* 可点卡片：整块可点，不要只让文字可点 */
.card--tap {
  display: block;
  color: inherit;
  text-align: left;
  width: 100%;
  transition: transform var(--dur-1) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
.card--tap:active { transform: scale(.988); }
.card--tap:hover { border-color: var(--line-strong); }

/* 卡片内的行：标题 + 说明 + 尾部
   .card-row 与 .list-item 是同一个零件的两态：
     .list-item  整行可点（有底色、有最小高度、有按压反馈）
     .card-row   卡内的一行（无底色无内距，靠父卡片的内距）
   排版部分共用，差异只在交互态。合并成并列选择器。 */
.card-row__body,
.list-item__body { flex: 1; min-width: 0; }

.card-row__title,
.list-item__title {
  font-size: var(--t-body);
  font-weight: var(--fw-semi);
  color: var(--ink);
  line-height: var(--lh-tight);
}
.card-row__desc,
.list-item__desc {
  margin-top: var(--s-1);
  font-size: var(--t-sm);
  color: var(--muted);
  line-height: var(--lh-tight);
}
.card-row__chev,
.list-item__chev { flex: 0 0 auto; width: 18px; height: 18px; color: var(--faint); }

/* 索引徽标：仅当内容真是序列时使用 */
.idx {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px; height: 28px;
  padding-inline: var(--s-2);
  border-radius: var(--r-xs);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-mono);
  font-size: var(--t-xs);
  font-weight: var(--fw-semi);
  font-variant-numeric: tabular-nums;
}

/* 图标容器：一个正方形圆角底 + 居中图标
   --------------------------------------------------------------------------
   这是全站复用度最高、复制得也最厉害的一个零件 —— 7 个站里手写了 8 个版本
   （.qual-icon / .capsule-icon / .butler-service-icon / .mt-icon-wrap /
   .upload-icon / .brand-mark / .dna-mark / .nav-icon），
   差异只在尺寸与配色，flex 居中那层逐字相同。

   这里给全尺寸档位，站点只声明自己的尺寸/底色覆盖即可。
   -------------------------------------------------------------------------- */
.icon-box {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 40px;
  block-size: 40px;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--accent);
}
.icon-box > svg,
.icon-box > .icon { inline-size: 20px; block-size: 20px; }

.icon-box--xs { inline-size: 28px; block-size: 28px; border-radius: var(--r-xs); }
.icon-box--xs > svg, .icon-box--xs > .icon { inline-size: 14px; block-size: 14px; }

.icon-box--sm { inline-size: 34px; block-size: 34px; }
.icon-box--sm > svg, .icon-box--sm > .icon { inline-size: 17px; block-size: 17px; }

.icon-box--lg { inline-size: 44px; block-size: 44px; border-radius: var(--r-md); }
.icon-box--lg > svg, .icon-box--lg > .icon { inline-size: 22px; block-size: 22px; }

.icon-box--xl { inline-size: 48px; block-size: 48px; border-radius: var(--r-md); }
.icon-box--xl > svg, .icon-box--xl > .icon { inline-size: 24px; block-size: 24px; }

/* 形状 / 配色变体 */
.icon-box--round { border-radius: var(--r-full); }
.icon-box--outline { background: transparent; border: 1px solid var(--accent-line); }
.icon-box--solid { background: var(--accent); color: var(--on-accent); }
.icon-box--quiet { background: var(--surface-2); color: var(--ink-2); }

/* ==========================================================================
   E. 按钮
   ========================================================================== */
.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: var(--tap-min);
  padding: 0 var(--s-5);
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  font-family: var(--font-text);
  font-size: var(--t-body);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-body);
  line-height: 1;
  white-space: nowrap;
  transition: background var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out),
              transform var(--dur-1) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out);
}
.btn svg { width: 18px; height: 18px; flex: 0 0 auto; }
.btn:active { transform: scale(.978); }
.btn[disabled], .btn[aria-disabled='true'] {
  opacity: .45;
  pointer-events: none;
}

.btn--primary {
  background: var(--accent);
  color: var(--on-accent);
  box-shadow: var(--shadow-xs);
}
.btn--primary:hover { background: var(--accent-hi); color: var(--on-accent); }

.btn--secondary {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink);
}
.btn--secondary:hover { background: var(--surface-2); border-color: var(--muted); }

.btn--ghost {
  background: transparent;
  color: var(--accent);
}
.btn--ghost:hover { background: var(--accent-soft); }

.btn--quiet {
  background: var(--surface-2);
  color: var(--ink-2);
}
.btn--quiet:hover { background: var(--accent-soft); color: var(--accent); }

.btn--danger { background: var(--danger); color: #fff; }
.btn--danger:hover { filter: brightness(1.08); color: #fff; }

.btn--block { display: flex; width: 100%; }
.btn--sm { min-height: 34px; padding: 0 var(--s-4); font-size: var(--t-sm); border-radius: var(--r-xs); }
.btn--lg { min-height: 52px; font-size: var(--t-h3); border-radius: var(--r-md); }

/* 纯图标按钮 */
.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap-min);
  height: var(--tap-min);
  border-radius: var(--r-full);
  color: var(--ink-2);
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
}
.btn-icon svg { width: 20px; height: 20px; }
.btn-icon:hover { background: var(--surface-2); color: var(--ink); }
.btn-icon--sm { width: 36px; height: 36px; }
.btn-icon--sm svg { width: 18px; height: 18px; }

/* 按钮行 */
.btn-row { display: flex; gap: var(--s-3); }
.btn-row > .btn { flex: 1; }

/* ==========================================================================
   F. 标签 / 徽章 / 药丸
   ========================================================================== */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  min-height: 32px;
  padding: 0 var(--s-4);
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--ink-2);
  font-size: var(--t-sm);
  font-weight: var(--fw-medium);
  line-height: 1;
  transition: all var(--dur-2) var(--ease-out);
}
.chip:hover { border-color: var(--line-strong); }
.chip[aria-pressed='true'], .chip.is-active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.chip--square { border-radius: var(--r-2xs); }
.chip--sm { min-height: 26px; padding: 0 var(--s-3); font-size: var(--t-xs); }

.chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
}
.chip-row--scroll {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;
  padding-inline: var(--gutter);
  margin-inline: calc(var(--gutter) * -1);
}
.chip-row--scroll::-webkit-scrollbar { display: none; }
.chip-row--scroll > .chip { flex: 0 0 auto; }

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  height: 20px;
  padding: 0 var(--s-2);
  border-radius: var(--r-full);
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  letter-spacing: .02em;
  line-height: 1;
  white-space: nowrap;
  background: var(--surface-2);
  color: var(--muted);
  border: 1px solid var(--line);
}
.badge--accent  { background: var(--accent-soft); color: var(--accent); border-color: var(--accent-line); }
.badge--success { background: var(--success-soft); color: var(--success); border-color: transparent; }
.badge--warn    { background: var(--warn-soft);    color: var(--warn);    border-color: transparent; }
.badge--danger  { background: var(--danger-soft);  color: var(--danger);  border-color: transparent; }
.badge--info    { background: var(--info-soft);    color: var(--info);    border-color: transparent; }
/* 实心：用在需要"最重"的那一个状态上，一屏最多一处 */
.badge--solid   { background: var(--accent); color: var(--on-accent); border-color: transparent; }
.badge--dot::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: var(--r-full);
  background: currentColor;
  flex: 0 0 auto;
}
.badge__dot {
  width: 5px; height: 5px;
  border-radius: var(--r-full);
  background: currentColor;
  flex: 0 0 auto;
}
/* 形态变体：语义（强调/成功/警告/危险）只有一套，形状跟着站点气质走。
   礼·续是衬线 + 直角，归家是圆润 —— 两者都该能用同一套语义色。 */
.badge--square { border-radius: var(--r-2xs); padding-inline: var(--s-2); }
.badge--ghost  { background: transparent; border-color: var(--accent-line); color: var(--accent); }
/* 压在图片/封面上：底是照片，必须自带一层暗底才读得清 */
.badge--on-media {
  /* 52% 是"这一次恰好达标"，58% 是"必然达标"。
     白字 + rgba(0,0,0,a) 压纯白底需要 a >= 0.54 —— 这是解出来的界，
     不是调出来的观感。浮在照片上的标签会碰到各种图，按界写才安全。 */
  background: color-mix(in srgb, #000 58%, transparent);
  border-color: color-mix(in srgb, #fff 24%, transparent);
  color: #fff;
  -webkit-backdrop-filter: blur(6px);
          backdrop-filter: blur(6px);
}
.badge--on-media.badge--square { border-radius: var(--r-2xs); }

/* 点状脉冲：表示"正在进行"（生成中 / 连接中），只给真正有进行态的地方 */
.badge__dot--live { animation: badge-pulse 1.6s var(--ease-soft) infinite; }
@keyframes badge-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .45; transform: scale(.72); }
}
@media (prefers-reduced-motion: reduce) {
  .badge__dot--live { animation: none; }
}

.badge--dot::before {
  content: '';
  width: 5px; height: 5px;
  border-radius: var(--r-full);
  background: currentColor;
}

/* 状态点 */
.dot { width: 8px; height: 8px; border-radius: var(--r-full); flex: 0 0 auto; }
.dot--success { background: var(--success); }
.dot--warn { background: var(--warn); }
.dot--danger { background: var(--danger); }
.dot--muted { background: var(--line-strong); }

/* ==========================================================================
   G. 表单
   ========================================================================== */
.field { display: block; }
.field + .field { margin-top: var(--s-5); }

.field__label {
  display: block;
  margin-bottom: var(--s-2);
  font-size: var(--t-sm);
  font-weight: var(--fw-medium);
  color: var(--ink-2);
}
.field__label .req { color: var(--danger); margin-left: 2px; }
.field__hint {
  margin-top: var(--s-2);
  font-size: var(--t-xs);
  color: var(--muted);
  line-height: var(--lh-tight);
}
.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--s-1);
  margin-top: var(--s-2);
  font-size: var(--t-xs);
  color: var(--danger);
  line-height: var(--lh-tight);
}

.input, .textarea, .select {
  display: block;
  width: 100%;
  min-height: var(--tap-min);
  padding: var(--s-3) var(--s-4);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-sm);
  color: var(--ink);
  font-size: var(--t-body);
  line-height: var(--lh-tight);
  transition: border-color var(--dur-2) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out),
              background var(--dur-2) var(--ease-out);
}
.input::placeholder, .textarea::placeholder { color: var(--faint); }
.input:hover, .textarea:hover, .select:hover { border-color: var(--muted); }
.input:focus, .textarea:focus, .select:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.input[aria-invalid='true'], .textarea[aria-invalid='true'] {
  border-color: var(--danger);
}
.input[aria-invalid='true']:focus {
  box-shadow: 0 0 0 3px var(--danger-soft);
}
.input[disabled], .textarea[disabled] {
  background: var(--surface-inset);
  color: var(--muted);
  cursor: not-allowed;
}
.textarea {
  min-height: 110px;
  resize: vertical;
  line-height: var(--lh-body);
  padding-block: var(--s-3);
}
.select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236B6760' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--s-4) center;
  padding-right: var(--s-8);
}

/* 输入框带计数 */
.field__count {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--s-2);
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}
.field__count.is-near { color: var(--warn); }
.field__count.is-over { color: var(--danger); }

/* 复选 / 单选 */
.check {
  display: flex;
  align-items: flex-start;
  gap: var(--s-3);
  min-height: var(--tap-min);
  padding-block: var(--s-2);
  cursor: pointer;
}
.check input {
  flex: 0 0 auto;
  width: 20px; height: 20px;
  margin: 2px 0 0;
  accent-color: var(--accent);
}
.check__text { font-size: var(--t-sm); color: var(--ink-2); line-height: var(--lh-tight); }

/* 开关 */
.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  cursor: pointer;
}
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch__track {
  position: relative;
  width: 46px; height: 28px;
  border-radius: var(--r-full);
  background: var(--line-strong);
  transition: background var(--dur-2) var(--ease-out);
  flex: 0 0 auto;
}
.switch__track::after {
  content: '';
  position: absolute;
  top: 3px; left: 3px;
  width: 22px; height: 22px;
  border-radius: var(--r-full);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
  transition: transform var(--dur-2) var(--ease-spring);
}
.switch input:checked + .switch__track { background: var(--accent); }
.switch input:checked + .switch__track::after { transform: translateX(18px); }
.switch input:focus-visible + .switch__track {
  outline: 2px solid var(--line-focus);
  outline-offset: 2px;
}

/* 分段控件 —— 见 V 段 .seg（滑块式）。这里保留 .segmented 的
   "无滑块、靠 aria-selected 换底色"降级形态，供只有两个选项、
   不需要滑动动画的地方使用。两者共用同一套尺寸与圆角。 */
.segmented {
  display: inline-flex;
  padding: 3px;
  border-radius: var(--r-full);
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.segmented__item {
  min-height: 34px;
  padding: 0 var(--s-4);
  border-radius: var(--r-full);
  font-size: var(--t-sm);
  font-weight: var(--fw-medium);
  color: var(--muted);
  border: 0;
  background: none;
  cursor: pointer;
  transition: color var(--dur-2) var(--ease-out),
              background var(--dur-2) var(--ease-out);
}
.segmented__item[aria-selected='true'] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-xs);
}

/* ==========================================================================
   H. 列表
   ========================================================================== */
.list { border-radius: var(--r-md); overflow: hidden; }
.list--outlined { border: 1px solid var(--line); background: var(--surface); }

.list-item {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  min-height: 56px;
  padding: var(--s-4) var(--s-5);
  background: var(--surface);
  text-align: left;
  color: inherit;
  transition: background var(--dur-2) var(--ease-out);
}
.list-item + .list-item { border-top: 1px solid var(--line); }
.list-item:hover { background: var(--surface-2); }
.list-item:active { background: var(--accent-soft); }
/* __body/__title/__desc/__chev 的排版已与 .card-row 合并（见 C 段） */
.list-item__value {
  flex: 0 0 auto;
  font-size: var(--t-sm);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* 键值对行 —— 已与 .spec-row 合并（见上） */

/* 横向滚动轨道（商品/作品/推荐位） */
.rail {
  display: flex;
  gap: var(--s-4);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-inline: var(--gutter);
  margin-inline: calc(var(--gutter) * -1);
  padding-bottom: var(--s-2);
  scrollbar-width: none;
}
.rail::-webkit-scrollbar { display: none; }
.rail > * { scroll-snap-align: start; flex: 0 0 auto; }

/* ==========================================================================
   I. 网格
   ========================================================================== */
.grid { display: grid; gap: var(--s-4); }
.grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid--auto { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }

/* ==========================================================================
   J. 底部标签栏
   ========================================================================== */
.tabbar {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  z-index: var(--z-nav);
  display: flex;
  width: 100%;
  max-width: var(--app-max);
  padding-bottom: var(--safe-b);
  background: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: saturate(1.6) blur(18px);
  -webkit-backdrop-filter: saturate(1.6) blur(18px);
  border-top: 1px solid var(--line);
}
.tabbar__item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  min-height: var(--tabbar-h);
  color: var(--muted);
  font-size: var(--t-micro);
  font-weight: var(--fw-medium);
  transition: color var(--dur-2) var(--ease-out);
}
.tabbar__item svg { width: 22px; height: 22px; }
.tabbar__item[aria-current='page'] { color: var(--accent); }
.tabbar__item:hover { color: var(--ink); }

/* ==========================================================================
   K. 反馈：Toast / 空态 / 骨架 / 加载 / 进度
   ========================================================================== */
.toast-host {
  position: fixed;
  left: 50%;
  bottom: calc(var(--safe-b) + var(--s-8));
  transform: translateX(-50%);
  z-index: var(--z-toast);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  align-items: center;
  width: max-content;
  max-width: min(92vw, calc(var(--app-max) - 32px));
  pointer-events: none;
}
.toast {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-5);
  border-radius: var(--r-full);
  /* 反色浮层：不能用 --ink/--bg —— 浅色主题下那对令牌语义是反的 */
  background: var(--ink-surface);
  color: var(--on-ink-surface);
  font-size: var(--t-sm);
  font-weight: var(--fw-medium);
  box-shadow: var(--shadow-lg);
  animation: toast-in var(--dur-3) var(--ease-spring);
  pointer-events: auto;
}
.toast svg { width: 16px; height: 16px; flex: 0 0 auto; }
.toast--success { background: var(--success); color: #fff; }
.toast--warn { background: var(--warn); color: #fff; }
.toast--danger { background: var(--danger); color: #fff; }
.toast.is-leaving { animation: toast-out var(--dur-2) var(--ease-in) forwards; }
@keyframes toast-in {
  from { opacity: 0; transform: translateY(12px) scale(.96); }
  to   { opacity: 1; transform: none; }
}
@keyframes toast-out {
  to { opacity: 0; transform: translateY(8px) scale(.97); }
}

/* 空态：给方向，不给情绪 */
.empty {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  text-align: center;
  padding: var(--s-9) var(--s-6);
}
.empty__title {
  font-size: var(--t-h3);
  font-weight: var(--fw-semi);
  color: var(--ink);
}
.empty__desc {
  margin-top: var(--s-2);
  max-width: 30ch;
  font-size: var(--t-sm);
  color: var(--muted);
  line-height: var(--lh-body);
}
.empty__action { margin-top: var(--s-6); }

/* 骨架屏 —— 三套实现已合并为一套，见 V 段 .skeleton */

/* 加载转圈 */
.spinner {
  width: 20px; height: 20px;
  border: 2px solid var(--line);
  border-top-color: var(--accent);
  border-radius: var(--r-full);
  animation: spin .7s linear infinite;
}
.spinner--sm { width: 14px; height: 14px; border-width: 1.5px; }
.spinner--lg { width: 32px; height: 32px; border-width: 3px; }
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .spinner { animation-duration: 2s; }
}

/* 全屏加载遮罩 */
.loading-veil {
  position: fixed;
  inset: 0;
  z-index: var(--z-loading);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
.loading-veil__text { font-size: var(--t-sm); color: var(--muted); }

/* 进度条 */
.progress {
  position: relative;
  height: 4px;
  border-radius: var(--r-full);
  background: var(--surface-inset);
  overflow: hidden;
}
.progress__bar {
  height: 100%;
  border-radius: inherit;
  background: var(--accent);
  transition: width var(--dur-3) var(--ease-out);
}
.progress--indeterminate .progress__bar {
  width: 35%;
  animation: progress-slide 1.3s var(--ease-soft) infinite;
}
@keyframes progress-slide {
  from { transform: translateX(-100%); }
  to   { transform: translateX(340%); }
}

/* 环形进度（生成类任务用） */
.ring { transform: rotate(-90deg); }
.ring__track { stroke: var(--surface-inset); }
.ring__bar {
  stroke: var(--accent);
  stroke-linecap: round;
  transition: stroke-dashoffset var(--dur-3) var(--ease-out);
}

/* ==========================================================================
   L. 模态 / 抽屉 / 折叠
   ========================================================================== */
.scrim {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  background: rgba(14, 12, 10, .42);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-3) var(--ease-out);
}
.scrim.is-open { opacity: 1; pointer-events: auto; }

.sheet {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translate(-50%, 100%);
  z-index: calc(var(--z-modal) + 1);
  width: 100%;
  max-width: var(--app-max);
  max-height: 88dvh;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: var(--shadow-xl);
  transition: transform var(--dur-3) var(--ease-out);
  overscroll-behavior: contain;
}
.sheet.is-open { transform: translate(-50%, 0); }
.sheet__grip {
  width: 36px; height: 4px;
  margin: var(--s-3) auto var(--s-2);
  border-radius: var(--r-full);
  background: var(--line-strong);
  flex: 0 0 auto;
}
.sheet__head {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-2) var(--s-5) var(--s-4);
  flex: 0 0 auto;
}
.sheet__title {
  flex: 1;
  font-size: var(--t-h3);
  font-weight: var(--fw-semi);
  color: var(--ink);
}
.sheet__body {
  flex: 1;
  overflow-y: auto;
  padding: 0 var(--s-5) var(--s-4);
  -webkit-overflow-scrolling: touch;
}
.sheet__foot {
  flex: 0 0 auto;
  padding: var(--s-4) var(--s-5) calc(var(--safe-b) + var(--s-4));
  border-top: 1px solid var(--line);
  background: var(--surface);
}

/* 折叠面板 */
.acc { border-radius: var(--r-md); overflow: hidden; }
.acc__item + .acc__item { border-top: 1px solid var(--line); }
.acc__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-4);
  width: 100%;
  min-height: 52px;
  padding: var(--s-4) var(--s-5);
  background: var(--surface);
  font-size: var(--t-body);
  font-weight: var(--fw-medium);
  color: var(--ink);
  text-align: left;
}
.acc__trigger:hover { background: var(--surface-2); }
.acc__mark {
  flex: 0 0 auto;
  width: 16px; height: 16px;
  color: var(--faint);
  transition: transform var(--dur-2) var(--ease-out);
}
.acc__trigger[aria-expanded='true'] .acc__mark { transform: rotate(180deg); }
.acc__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-3) var(--ease-out);
}
.acc__panel[data-open='true'] { grid-template-rows: 1fr; }
.acc__panel > div { overflow: hidden; }
.acc__content {
  padding: 0 var(--s-5) var(--s-5);
  font-size: var(--t-sm);
  line-height: var(--lh-body);
  color: var(--muted);
}

/* ==========================================================================
   M. 图片与媒体占位
   素材大面积不可用是当前最大视觉缺陷，这里统一兜底：
   加载失败时不留白洞，而是显示品牌化的占位纹样
   ========================================================================== */
.media {
  position: relative;
  overflow: hidden;
  background: var(--surface-inset);
  border-radius: var(--r-md);
  isolation: isolate;
}
.media > img, .media > video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: opacity var(--dur-3) var(--ease-out);
}
.media--4x3 { aspect-ratio: 4 / 3; }
.media--1x1 { aspect-ratio: 1 / 1; }
.media--3x4 { aspect-ratio: 3 / 4; }
.media--16x9 { aspect-ratio: 16 / 9; }

/* --------------------------------------------------------------------------
   媒体（视频/音频）加载失败的兜底
   与 .is-broken 同理，但作用于 <video>/<audio>：
   video 无 poster 时浏览器会画一个纯黑框，比裂图更难看 —— 这里彻底接管。
   -------------------------------------------------------------------------- */
.is-media-broken {
  background-color: var(--surface-inset);
  background-image:
    repeating-linear-gradient(
      -45deg,
      transparent 0 9px,
      color-mix(in srgb, var(--line-strong) 30%, transparent) 9px 10px
    );
}
.is-media-broken > video,
.is-media-broken > audio { opacity: 0; visibility: hidden; }

.is-media-broken::after {
  content: attr(data-fallback);
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  padding: var(--s-4);
  font-family: var(--font-text);
  font-size: var(--t-xs);
  letter-spacing: var(--ls-caps);
  color: var(--faint);
  text-align: center;
  pointer-events: none;
}
/* 占位中央给一个克制的播放/音符图标，别让它看着像坏了 */
.is-media-broken::before {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 34px; height: 34px;
  margin: -30px 0 0 -17px;
  border-radius: var(--r-full);
  border: 1.5px solid color-mix(in srgb, var(--faint) 55%, transparent);
  background-repeat: no-repeat;
  background-position: center;
  background-size: 14px 14px;
  /* 三角形播放符，用 SVG data-uri（currentColor 在 data-uri 里不可用，故写死中性色） */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238A857C' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E");
}

/* 图片未加载 / 加载失败时的占位：不显示裂图图标 */
.media.is-broken {
  background-image:
    repeating-linear-gradient(
      -45deg,
      var(--surface-inset) 0 10px,
      var(--surface-2) 10px 20px
    );
}
/* --------------------------------------------------------------------------
   图片降级（无 .media 包裹时的兜底容器）
   素材目录 EIO 故障期间，7 站的图片会大面积失败。这里的占位必须
   看起来像"设计的一部分"，而不是"坏掉了"：
     · 用 --accent-soft / --line 上色，随品牌色走
     · 斜纹质感，暗示"此处是占位"
     · 居中显示 data-fallback 文案
   -------------------------------------------------------------------------- */
.img-fallback {
  position: relative;
  background-color: var(--surface-2);
  background-image:
    repeating-linear-gradient(
      -45deg,
      transparent 0 9px,
      color-mix(in srgb, var(--accent-glow) 11%, transparent) 9px 10px
    );
  min-height: 80px;
  overflow: hidden;
}
/* 占位画在 <img> 自身（无 .media 包裹时）——
   尺寸跟着图片自己走，绝不会波及父元素。
   用 em 做下限，随所在位置的字号缩放：图标位 24px 字 -> 约 38px 盒，
   正文位 15px 字 -> 约 24px 盒；CSS 给了明确宽高的图片不受影响。 */
/* 用 img. 前缀提高特异性：各站常有 `background:` 简写（如 9046 的
   .advisor-avatar-large），简写会重置 background-image，把斜纹抹掉。 */
/* 干掉浏览器默认的"破图"标记。
   alt="" 只能压掉 alt 文字，压不掉那张默认小图标 —— 在带明确宽高的
   <img> 上它照样画在左上角（放大截图确认过）。
   content:none 是替换元素上唯一被标准化用来"不渲染替代内容"的途径。 */
img[data-img-failed="1"] { content: none; }

img.img-fallback-self,
.img-fallback-self {
  min-width: 1.6em;
  min-height: 1.6em;
  /* 底色取 surface-2（比 surface 亮一档），保证占位块在暗底上"看得见"；
     再用 accent-glow 低透明度画斜纹 —— 像屏幕微光，不是灰条纹。 */
  background-color: var(--surface-2);
  background-image:
    repeating-linear-gradient(
      -45deg,
      transparent 0 5px,
      color-mix(in srgb, var(--accent-glow) 13%, transparent) 5px 6px
    );
  /* 内描边给它一个明确边界，避免糊在卡片里 */
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 16%, transparent);
}

.img-fallback::after {
  content: attr(data-fallback);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4);
  font-family: var(--font-text);
  font-size: var(--t-xs);
  letter-spacing: var(--ls-caps);
  color: color-mix(in srgb, var(--accent) 72%, var(--muted));
  text-align: center;
  pointer-events: none;
}

/* 兜底容器里的裂图图标要收干净，别让浏览器的破图标记露出来 */
.img-fallback > img { opacity: 0; }

.media.is-broken > img { opacity: 0; }
.media.is-broken::after {
  content: attr(data-fallback);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-4);
  font-family: var(--font-display);
  font-size: var(--t-sm);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-heading);
  color: var(--faint);
  text-align: center;
}

/* 图像上的文字压罩，保证任何底图上文字都可读 */
.media__overlay {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to top,
    rgba(12, 10, 8, .78) 0%,
    rgba(12, 10, 8, .5) 34%,
    rgba(12, 10, 8, .12) 62%,
    transparent 100%
  );
  pointer-events: none;
}
.media__content {
  position: absolute;
  inset: auto 0 0 0;
  padding: var(--s-5);
  color: #fff;
}

/* ==========================================================================
   N. 作品集互链组件（7 站统一的交通层）
   ========================================================================== */

/* N.1 顶栏返回门户 —— 每个子站顶栏左侧统一入口 */
.portal-back {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  height: 32px;
  padding: 0 var(--s-3) 0 var(--s-2);
  margin-right: var(--s-2);
  border-radius: var(--r-full);
  background: var(--surface-inset);
  color: var(--ink-2);
  font-size: var(--t-xs);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
}
.portal-back:hover { background: var(--accent-soft); color: var(--accent); }
.portal-back svg { width: 14px; height: 14px; flex: 0 0 auto; }

/* N.2 底部作品集导航条 —— 每站页脚统一 */
.portfolio-nav {
  padding: var(--s-6) var(--gutter) calc(var(--safe-b) + var(--s-6));
  border-top: 1px solid var(--line);
  background: var(--surface-2);
}
.portfolio-nav__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-3);
  margin-bottom: var(--s-4);
}
.portfolio-nav__title {
  font-size: var(--t-xs);
  font-weight: var(--fw-medium);
  letter-spacing: var(--ls-caps);
  color: var(--faint);
}
.portfolio-nav__all {
  font-size: var(--t-xs);
  font-weight: var(--fw-medium);
  color: var(--accent);
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
.portfolio-nav__all svg { width: 13px; height: 13px; }

.portfolio-nav__list {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-2);
}
.portfolio-nav__item {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3);
  border-radius: var(--r-sm);
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--ink);
  transition: border-color var(--dur-2) var(--ease-out),
              background var(--dur-2) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.portfolio-nav__item:hover {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  color: var(--accent);
  transform: translateY(-1px);
}
.portfolio-nav__item[aria-current='page'] {
  border-color: var(--accent);
  background: var(--accent-soft);
  pointer-events: none;
}
.portfolio-nav__mark {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px; height: 26px;
  border-radius: var(--r-xs);
  background: var(--surface-inset);
  color: var(--accent);
  font-family: var(--font-display);
  font-size: var(--t-sm);
  font-weight: var(--fw-semi);
  line-height: 1;
}
.portfolio-nav__item[aria-current='page'] .portfolio-nav__mark {
  background: var(--accent);
  color: var(--on-accent);
}
.portfolio-nav__name {
  flex: 1;
  min-width: 0;
  font-size: var(--t-sm);
  font-weight: var(--fw-medium);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.portfolio-nav__foot {
  margin-top: var(--s-5);
  font-size: var(--t-micro);
  color: var(--faint);
  text-align: center;
  line-height: var(--lh-tight);
}

/* N.3 门户站的卡片网格（9034 用） */
.pf-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s-4);
}
@media (min-width: 640px) {
  .pf-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.pf-card {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  padding: var(--s-5);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  color: inherit;
  min-height: 150px;
  transition: border-color var(--dur-2) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out),
              transform var(--dur-2) var(--ease-out);
}
.pf-card:hover {
  border-color: var(--accent-line);
  box-shadow: var(--shadow-md);
  transform: translateY(-2px);
  color: inherit;
}
.pf-card__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-2);
}
.pf-card__mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px; height: 34px;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-display);
  font-size: var(--t-h3);
  font-weight: var(--fw-semi);
  line-height: 1;
}
/* 卡片角标：站点性质（品牌站 / AI 工具）。
   这里原来显示的是开发端口（:9034），对外部访问者没有意义。 */
.pf-card__tag {
  font-family: var(--font-mono);
  font-size: var(--t-micro);
  color: var(--faint);
  letter-spacing: .04em;
}
.pf-card__name {
  font-family: var(--font-display);
  font-size: var(--t-h3);
  font-weight: var(--fw-semi);
  letter-spacing: var(--ls-heading);
  color: var(--ink);
  line-height: var(--lh-tight);
}
.pf-card__desc {
  flex: 1;
  font-size: var(--t-xs);
  color: var(--muted);
  line-height: var(--lh-body);
}
.pf-card__go {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-size: var(--t-xs);
  font-weight: var(--fw-medium);
  color: var(--accent);
}
.pf-card__go svg { width: 14px; height: 14px; transition: transform var(--dur-2) var(--ease-out); }
.pf-card:hover .pf-card__go svg { transform: translateX(3px); }

/* 每站再给一个专属色相标记，让门户一眼区分 */
.pf-card[data-site='runkai']  { --accent: #1E4C8A; --accent-soft: #E6EDF6; --accent-line: #B9CCE4; }
.pf-card[data-site='guijia']  { --accent: #7A5A24; --accent-soft: #F0E9DA; --accent-line: #D9C9A6; }
.pf-card[data-site='zhuyu']   { --accent: #8A3B57; --accent-soft: #F3E5EA; --accent-line: #DCBAC7; }
.pf-card[data-site='atelier'] { --accent: #4A4A48; --accent-soft: #ECECEA; --accent-line: #CBCBC8; }
.pf-card[data-site='lixu']    { --accent: #6B4A7A; --accent-soft: #EEE7F1; --accent-line: #D0BFD7; }
.pf-card[data-site='guangyu'] { --accent: #3D5AA8; --accent-soft: #E6EBF7; --accent-line: #BCC8E6; }
.pf-card[data-site='yinji']   { --accent: #8A4EA8; --accent-soft: #F0E7F5; --accent-line: #D3C0DE; }

/* ==========================================================================
   O. 工具类（克制使用，优先用组件）
   ========================================================================== */
.row { display: flex; align-items: center; gap: var(--s-3); }
.row--between { justify-content: space-between; }
.row--start { align-items: flex-start; }
.row--wrap { flex-wrap: wrap; }
.col { display: flex; flex-direction: column; gap: var(--s-3); }
.grow { flex: 1; min-width: 0; }
.center { text-align: center; }
.nowrap { white-space: nowrap; }
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clamp-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.clamp-3 {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.mt-2 { margin-top: var(--s-2); } .mt-3 { margin-top: var(--s-3); }
.mt-4 { margin-top: var(--s-4); } .mt-5 { margin-top: var(--s-5); }
.mt-6 { margin-top: var(--s-6); } .mt-7 { margin-top: var(--s-7); }
.mb-2 { margin-bottom: var(--s-2); } .mb-3 { margin-bottom: var(--s-3); }
.mb-4 { margin-bottom: var(--s-4); } .mb-5 { margin-bottom: var(--s-5); }
.hidden { display: none !important; }
.full-bleed { margin-inline: calc(var(--gutter) * -1); }

/* ==========================================================================
   P. 页面转场（站内页面切换，非站间跳转）
   ========================================================================== */
.page-view {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  opacity: 0;
  visibility: hidden;
  transform: translateX(16px);
  transition: opacity var(--dur-3) var(--ease-out),
              transform var(--dur-3) var(--ease-out),
              visibility 0s linear var(--dur-3);
}
.page-view.is-active {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity var(--dur-3) var(--ease-out),
              transform var(--dur-3) var(--ease-out),
              visibility 0s;
}
.page-view.is-back { transform: translateX(-16px); }

@media (prefers-reduced-motion: reduce) {
  .page-view { transition: none; transform: none; }
}

/* 内容入场：只在页面级使用一次，不做逐块 fade-up */
@keyframes enter {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.enter { animation: enter var(--dur-4) var(--ease-out) both; }
.enter-1 { animation-delay: 60ms; }
.enter-2 { animation-delay: 120ms; }
.enter-3 { animation-delay: 180ms; }

/* ==========================================================================
   R. 科技感动效层
   --------------------------------------------------------------------------
   设计原则（克制，不撒糖）：
     · 动效服务于"层级与状态"，不是每个元素都淡入
     · 只在**动作的回应**上做效果（点击、切换、加载、成功）
     · 环境光只出现在"主视觉"区域，不铺满全站
     · 全部走 --dur-* / --ease-* 令牌，prefers-reduced-motion 下自动关闭

   组件清单：
     R1 环境网格底纹（.tech-grid）—— 主视觉背景
     R2 扫描线（.tech-scan）—— 加载中的进度语汇
     R3 光晕边框（.glow-ring / .glow-card）
     R4 数字滚动感（.num-tick）
     R5 交错入场（.stagger > *）
     R6 涟漪（.ripple）
     R7 骨架微光（已有，增强为科技蓝）
     R8 状态脉冲（.pulse-dot）
   ========================================================================== */

/* ---------- R1. 环境网格底纹 -------------------------------------------
   深空 + 极淡网格 = 科技感最直接的语汇。用两层渐变画线，零图片开销。
   默认铺在 .tech-grid 上，各站可按需加在 hero / 空态 / 卡片背景。 */
.tech-grid {
  position: relative;
  isolation: isolate;
}
.tech-grid::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--accent) 7%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--accent) 7%, transparent) 1px, transparent 1px);
  background-size: 28px 28px;
  /* 边缘淡出，避免网格线硬切 */
  -webkit-mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, #000 40%, transparent 100%);
          mask-image: radial-gradient(ellipse 80% 70% at 50% 40%, #000 40%, transparent 100%);
}
.tech-grid--tight::before { background-size: 18px 18px; }
.tech-grid--loose::before { background-size: 40px 40px; }

/* 网格之上的品牌光斑：一团柔和辉光，给主视觉定色 */
.tech-glow::after {
  content: '';
  position: absolute;
  inset: -20% -10% auto -10%;
  height: 70%;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    ellipse 60% 100% at 50% 0%,
    color-mix(in srgb, var(--accent-glow) 16%, transparent) 0%,
    transparent 70%
  );
}

/* ---------- R2. 扫描线 -------------------------------------------------
   一条沿 Y 轴往复的光带，用于"正在处理"的语汇。
   比 spinner 更贴合科技主题，且不打断布局。 */
@keyframes tech-scan-y {
  0%   { transform: translateY(-100%); opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translateY(100%); opacity: 0; }
}
.tech-scan {
  position: relative;
  overflow: hidden;
}
.tech-scan::after {
  content: '';
  position: absolute;
  left: 0; right: 0;
  height: 38%;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    color-mix(in srgb, var(--accent-glow) 12%, transparent) 45%,
    color-mix(in srgb, var(--accent-glow) 22%, transparent) 50%,
    color-mix(in srgb, var(--accent-glow) 12%, transparent) 55%,
    transparent 100%
  );
  animation: tech-scan-y 2.4s var(--ease-soft) infinite;
}

/* ---------- R3. 光晕边框 -------------------------------------------------
   静态版给"被选中/进行中"的卡片；交互版在 hover 时亮起。 */
.glow-card {
  position: relative;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--surface);
  transition: border-color var(--dur-3) var(--ease-out),
              box-shadow var(--dur-3) var(--ease-out),
              transform var(--dur-2) var(--ease-out);
}
.glow-card:hover,
.glow-card.is-active {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--line));
  box-shadow: var(--glow-sm), var(--shadow-sm);
}
.glow-card.is-active {
  box-shadow: var(--glow-md), var(--shadow-md);
}
/* 顶部一道渐变亮边 —— 科技面板的招牌细节 */
.glow-card--edge::before {
  content: '';
  position: absolute;
  top: 0; left: 12%; right: 12%;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(
    to right,
    transparent,
    color-mix(in srgb, var(--accent-glow) 70%, transparent),
    transparent
  );
}

/* ---------- R4. 数字滚动感 ----------------------------------------------
   数值变化时轻微上浮 + 品牌色闪一下。真正的逐位滚动需要 JS，
   这里用 CSS 给"变了"一个明确的反馈，成本极低。 */
@keyframes num-tick {
  0%   { transform: translateY(38%); opacity: 0; }
  60%  { transform: translateY(-6%); opacity: 1; }
  100% { transform: translateY(0);   opacity: 1; }
}
.num-tick {
  display: inline-block;
  animation: num-tick var(--dur-3) var(--ease-spring) both;
}
@keyframes num-flash {
  0%, 100% { color: inherit; }
  30%      { color: var(--accent-glow); }
}
.num-flash { animation: num-flash var(--dur-4) var(--ease-soft); }

/* ---------- R5. 交错入场 ------------------------------------------------
   比"每块都 fade-up"克制：只在**同一组**内做依次入场，
   且整体一次完成，不随滚动无限触发。

   ⚠ fill-mode 必须是 backwards，不能是 both —— 这里踩过一个很隐蔽的坑：
     `both` 会让动画结束后**继续应用结束帧**。结束帧写的是 `transform: none`，
     而动画声明的优先级**高于普通规则**（含 :active / :hover）。
     于是元素身上永远钉着一个 transform，作者为它写的
         .gallery-item:active { transform: scale(.97) }
         .inspire-card:active { transform: scale(.98) }
     这一辈子都不会生效 —— 按下没有任何缩放反馈。

     这个缺陷极难发现：不报错、不影响任何静止外观、静态读 CSS 也看不出
     （两条规则各自都对，冲突发生在层叠层），只有**真的按住**才暴露。
     实测中招：光屿 .gallery-item、珠语 .inspire-card。

     `backwards` 同样在延迟期应用 from 帧（错峰入场完全不变），
     但**播完之后不再施加任何动画样式**，元素回到自己的样式，
     :active / :hover 正常接管。
     为什么换掉是安全的：to 帧是 `opacity:1; transform:none`，
     与元素的默认计算值等价 —— 撤掉它在静止态看不出任何区别（已逐像素比对确认）。 */
@keyframes stagger-in {
  from { opacity: 0; transform: translateY(10px) scale(.985); }
  to   { opacity: 1; transform: none; }
}
.stagger > * {
  animation: stagger-in var(--dur-4) var(--ease-out) backwards;
}
.stagger > *:nth-child(1) { animation-delay: 0ms; }
.stagger > *:nth-child(2) { animation-delay: 55ms; }
.stagger > *:nth-child(3) { animation-delay: 110ms; }
.stagger > *:nth-child(4) { animation-delay: 165ms; }
.stagger > *:nth-child(5) { animation-delay: 220ms; }
.stagger > *:nth-child(6) { animation-delay: 275ms; }
.stagger > *:nth-child(7) { animation-delay: 330ms; }
.stagger > *:nth-child(8) { animation-delay: 385ms; }
.stagger > *:nth-child(n+9) { animation-delay: 440ms; }

/* ---------- R6. 涟漪 ----------------------------------------------------
   点按的即时回应。JS 只需在容器上加 .ripple 并在点击处插入
   一个 <span class="ripple__ink">，样式全在这里。 */
.ripple {
  position: relative;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}
.ripple__ink {
  position: absolute;
  border-radius: var(--r-full);
  transform: translate(-50%, -50%) scale(0);
  pointer-events: none;
  background: radial-gradient(
    circle,
    color-mix(in srgb, var(--accent-glow) 42%, transparent) 0%,
    color-mix(in srgb, var(--accent-glow) 12%, transparent) 60%,
    transparent 70%
  );
  animation: ripple-out var(--dur-5) var(--ease-out) forwards;
}
@keyframes ripple-out {
  to { transform: translate(-50%, -50%) scale(1); opacity: 0; }
}

/* R7. 骨架微光 —— 已并入 .skeleton--tech（见 V 段） */

/* ---------- R8. 状态脉冲 -------------------------------------------------
   表示"在线/运行中"的小圆点。用在状态标识、连接指示。 */
@keyframes pulse-ring {
  0%   { transform: scale(.85); opacity: .85; }
  70%  { transform: scale(2.1);  opacity: 0; }
  100% { transform: scale(2.1);  opacity: 0; }
}
.pulse-dot {
  position: relative;
  display: inline-block;
  width: 7px; height: 7px;
  flex: 0 0 auto;
  border-radius: var(--r-full);
  background: var(--accent);
}
.pulse-dot::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: inherit;
  animation: pulse-ring 2s var(--ease-out) infinite;
}

/* ---------- R9. 顶栏玻璃化 ----------------------------------------------
   滚动后顶栏浮起：底色半透明 + 背景模糊 + 一道底部亮线。
   比"加阴影"更贴合科技面板的语汇。 */
.topbar--glass {
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
          backdrop-filter: blur(14px) saturate(150%);
}
.topbar--glass::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    to right,
    transparent,
    color-mix(in srgb, var(--accent) 45%, transparent),
    transparent
  );
  transition: opacity var(--dur-3) var(--ease-out);
}
.topbar--glass.is-scrolled::after { opacity: 1; }

/* ---------- 动效降级 ---------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  .tech-scan::after,
  .pulse-dot::after { animation: none; }
  .stagger > *,
  .num-tick { animation: none; }
  .glow-card:hover { transform: none; }
}

/* ==========================================================================
   T. 图形层 —— 图片缺失时画"这是什么"，而不是画"这里坏了"
   ==========================================================================
   背景：/uploads/ 全站 500（磁盘 EIO），试衣间原本两栏照片变成两片空白。
   斜纹占位只说明"这里本该有图"，说明不了"这是什么"，用户读到的仍是"网站坏了"。

   所以缺图时改画人台 + 服装剪影：
     · 形状来自真实分类（category_type）
     · 填充色来自真实颜色（color_hex）
     · 光晕来自真实试穿次数（try_count）
   呈现为"一张服装技术图"，是有内容的，不是失败状态。

   与真实图片的共存：图形层是 <img> 的兄弟节点、排在它前面，
   图片加载成功即完全遮盖剪影 —— 磁盘修好后无需改代码。
   -------------------------------------------------------------------------- */

.glyph {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* 剪影的"光"：走品牌 glowing 色，随 [data-brand] 自动变 */
  color: var(--accent-glow);
  pointer-events: none;
  transition: opacity var(--dur-3) var(--ease-out);
}

/* 空人台（未选服装）：压一档，别和已选状态抢注意力 */
.glyph--bare { opacity: 0.55; }

/* 缩略图：服装架单品格，卡片小、要更亮才看得清 */
.glyph--thumb { opacity: 0.9; }

/* 舞台图：两栏各有一张，稍微收一点避免两片光糊在一起 */
.glyph--stage { opacity: 0.82; }

/* 宿主需要定位上下文与裁剪 */
.glyph-host { position: relative; overflow: hidden; }

/* 图形层是地板，<img> 是天花板：图一旦真的加载出来就整片盖住剪影。
   注意不能给 img 加 position（很多站用 absolute 定位它）。 */
.glyph-host > img { position: relative; z-index: 1; }
.glyph-host > img[data-img-failed] { visibility: hidden; }

/* 叠在图形层之上的标注（套装标签等）不能被盖住 */
.glyph-host > .garment-tag,
.glyph-host > .pane-label { z-index: 2; }

@media (prefers-reduced-motion: reduce) {
  .glyph { transition: none; }
}

/* ==========================================================================
   U. 玻璃层 —— 毛玻璃与高级感组件
   ==========================================================================
   用户要"大厂打造出来的 UI 感"。这里的取舍：

   玻璃只用在**浮在内容之上**的层（顶栏、底栏、弹层、悬浮卡），
   正文卡片保持实色 —— 全部玻璃化会让文字对比度崩掉，那不是高级，是灾难。
   （已有 validate_contrast.py 把关，玻璃面上的文字仍必须 ≥4.5:1。）

   质感靠三件事，不是靠糊一层 blur：
     1. 边缘折射：顶部 1px 亮线 + 底部 1px 暗线，玻璃才有"厚度"
     2. 背景保真：saturate() 让透过去的颜色不发灰
     3. 噪点：极淡的颗粒，消掉大面积模糊的"塑料感"
   -------------------------------------------------------------------------- */

.glass {
  position: relative;
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
          backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: 1px solid var(--glass-line);
}
/* 顶部亮边：模拟光线从上方打进玻璃边缘 */
.glass::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 1px;
  pointer-events: none;
  background: linear-gradient(
    to right,
    transparent,
    var(--glass-hi) 18%,
    var(--glass-hi) 82%,
    transparent
  );
  opacity: 0.7;
}

.glass--panel  { border-radius: var(--r-lg); }
.glass--float  { border-radius: var(--r-xl); box-shadow: var(--shadow-float); }
.glass--bar    { border-radius: 0; border-left: 0; border-right: 0; }

/* 不支持 backdrop-filter 的浏览器：退回实色，别让文字浮在花背景上 */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass { background: var(--surface); }
}

/* ---------- 高级卡片：玻璃 + 描边高光 + hover 抬升 ---------------------- */
.glass-card {
  position: relative;
  border-radius: var(--r-lg);
  padding: var(--s-4);
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
          backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border: 1px solid var(--glass-line);
  box-shadow: var(--shadow-1);
  overflow: hidden;
  transition: transform var(--dur-2) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
/* 左上角的一抹高光 —— 高级感多半来自这点"光从哪来"的一致性 */
.glass-card::after {
  content: '';
  position: absolute;
  left: -30%; top: -60%;
  width: 90%; height: 110%;
  pointer-events: none;
  background: radial-gradient(
    ellipse at 30% 30%,
    color-mix(in srgb, var(--accent-glow) 16%, transparent),
    transparent 62%
  );
  opacity: 0.85;
}
.glass-card > * { position: relative; z-index: 1; }
.glass-card:hover {
  transform: translateY(-2px);
  border-color: color-mix(in srgb, var(--accent) 34%, var(--glass-line));
  box-shadow: var(--shadow-2), 0 0 0 1px color-mix(in srgb, var(--accent) 14%, transparent);
}
.glass-card:active { transform: translateY(0); }

/* ---------- 规格行：label 左 / value 右，细分割线 -----------------------
   .spec-row 与 .kv 是同一个零件（左标签右值 + 分隔线），历史上有两个名字。
   合并为并列选择器 —— 不删旧名，老代码继续生效，新代码写哪个都对。
   分隔线取 .kv 的口径（border-top）：最后一行不留尾巴，
   不用再靠 :last-child 去补一刀。 */
.spec-list,
.kv-list { display: flex; flex-direction: column; }

.spec-row,
.kv {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  padding-block: var(--s-3);
}
.spec-row + .spec-row,
.kv + .kv { border-top: 1px solid var(--line); }

.spec-row__k,
.kv__k { font-size: var(--t-sm); color: var(--muted); flex: 0 0 auto; }
.spec-row__v,
.kv__v {
  font-size: var(--t-body);
  font-weight: var(--fw-medium);
  color: var(--ink);
  text-align: right;
  font-variant-numeric: tabular-nums;   /* 数字等宽，右对齐才齐 */
  min-width: 0;
  overflow-wrap: anywhere;
}

/* ---------- 数据磁贴 ----------------------------------------------------
   大数字 + 小标签。7 站里手写了 5 个版本
   （.body-stat / .archive-warranty-item / .dash-stat / .pc-stat / .stat），
   排版那一层逐字相同，差异只在"居中还是左对齐""要不要底色"。

   数字一律 font-variant-numeric: tabular-nums —— 等宽数字，
   值从 9 跳到 10 时宽度不变，整排磁贴不会跟着抖。
   -------------------------------------------------------------------------- */
.stat-tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--s-3) var(--s-3) calc(var(--s-3) - 2px);
  border-radius: var(--r-md);
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.stat-tile__v {
  font-family: var(--font-display);
  font-size: var(--t-h3);
  font-weight: var(--fw-bold);
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--ls-num);
}
.stat-tile__k { font-size: var(--t-xs); color: var(--muted); }

/* 变体 */
.stat-tile--center { align-items: center; text-align: center; }
.stat-tile--plain  { background: transparent; border: 0; padding: 0; }
.stat-tile--accent .stat-tile__v { color: var(--accent); }
.stat-tile--hero .stat-tile__v { font-size: var(--t-h2); }

/* ---------- 分段控件：一个滑块在选项间移动 ----------------------------- */
.seg {
  position: relative;
  display: inline-flex;
  padding: 3px;
  border-radius: var(--r-full);
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.seg__thumb {
  position: absolute;
  top: 3px; bottom: 3px;
  /* left 必须写死：绝对定位元素在 flex 容器里的静态位置受 justify-content
     影响，站点只要给 .seg 加个居中，滑块就会被先居中再叠加位移，跑偏一整个项宽 */
  left: 3px;
  border-radius: var(--r-full);
  background: var(--accent);
  transition: transform var(--dur-3) var(--ease-spring), width var(--dur-3) var(--ease-spring);
  pointer-events: none;
}
.seg__item {
  position: relative;
  z-index: 1;
  padding: 7px 16px;
  border: 0;
  background: none;
  color: var(--ink-2);
  font-size: var(--t-sm);
  font-weight: var(--fw-medium);
  border-radius: var(--r-full);
  cursor: pointer;
  transition: color var(--dur-2) var(--ease-out);
}
/* .active 与 .is-active 都认：各站既有业务 JS 用的是 .active，
   设计系统不该为了统一命名去改动 7 个站已经在跑的逻辑 */
.seg__item.is-active,
.seg__item.active { color: var(--on-accent); }

/* ---------- chip 横滚组 ------------------------------------------------- */
.chip-row {
  display: flex;
  gap: var(--s-2);
  overflow-x: auto;
  scrollbar-width: none;
  /* ── 给热区让出竖向空间 ────────────────────────────────────────────
     overflow-x:auto 会把 overflow-y 一并算成 auto（规范如此），
     于是这一行会**在纵向裁掉**子元素伸出去的部分。
     chip 只有 32px 高，热区要 44px，多出来的 12px 全被裁了。
     解法：上下各留 6px 内边距 —— 裁切边界是 padding box，
     有了这 6px，::after 就落在可命中区域内。
     再用等量负外边距把这 12px 从布局里收回，外部间距一点不变。 */
  padding-top: 7px;
  padding-bottom: 7px;
  margin-top: -7px;
  margin-bottom: -5px;   /* 原本有 2px 下内边距，一并算进去，总高不变 */
  /* 行距必须 ≥ 12px（= 上下各 6px 的热区外扩之和）。
     8px 行距时，相邻两行的热区会在中间对撞 —— 谁赢取决于 z 序，
     于是每个 chip 的竖向有效热区被砍到 39px，怎么调都到不了 44。
     这不是"再挤一点"能解决的，是几何上放不下：32px 的 chip 要撑到 44，
     上下各需 7px（6px 是理论值，但裁切边界落在 padding box 边缘时会吃掉 1px），
     两行之间就得留 14px。
     行距从不等于列距看起来仍然自然（横向 8px、纵向 14px 仍然自然）。 */
  row-gap: 14px;
  scroll-snap-type: x proximity;
}
.chip-row::-webkit-scrollbar { display: none; }
.chip-row > * { scroll-snap-align: start; flex: 0 0 auto; }

/* ---------- 头像环：外圈品牌色渐变 ------------------------------------- */
.avatar-ring {
  position: relative;
  display: inline-flex;
  padding: 2px;
  border-radius: var(--r-full);
  background: conic-gradient(
    from 140deg,
    var(--accent),
    color-mix(in srgb, var(--accent-glow) 70%, transparent),
    var(--accent)
  );
}
.avatar-ring > img,
.avatar-ring > .avatar { border-radius: var(--r-full); display: block; }

/* ---------- 发光分割线 -------------------------------------------------- */
.divider-glow {
  height: 1px;
  border: 0;
  margin: var(--s-4) 0;
  background: linear-gradient(
    to right,
    transparent,
    color-mix(in srgb, var(--accent) 40%, transparent),
    transparent
  );
}

/* ---------- 玻璃上的内容自动提亮 ---------------------------------------- */
/* 玻璃面后面是彩色内容时，普通 ink-2 会显得脏，统一压到 ink 保证可读 */
.glass .spec-row__k,
.glass-card .spec-row__k { color: var(--ink-2); }

@media (prefers-reduced-motion: reduce) {
  .glass-card { transition: none; }
  .glass-card:hover { transform: none; }
  .seg__thumb { transition: none; }
}

/* ==========================================================================
   V. 玻璃层的落位
   ==========================================================================
   原则：**浮在内容之上的层用玻璃，正文卡片保持实色**。
   理由不是审美，是物理：正文卡片的底是页面底色，玻璃在纯色上没有可折
   的射内容，只会白吃一层 GPU；而浮层底下永远有滚动的正文，模糊才成立。
   另外 backdrop-filter 会创建层叠上下文，铺到正文卡上会把子元素的
   z-index 全关进小黑屋。

   这一段原先是同一组三条声明复制了 4 遍（每次换一批选择器），
   已合并：值全部走令牌，所以合并不改变任何站的观感。
   -------------------------------------------------------------------------- */

/* 玻璃配方：浮层共用 */
.topbar,
.sheet,
.modal-panel,
.dialog,
.tryon-bar,
.advisor-card,
.gy-player-bar,
.yj-player,
.player-bar,
.result-header,
.gen-progress-header,
.bottom-order,
.cart-bar,
.payment-bill-header,
.sub-header,
.tab-bar,
.app-header,
.site-foot,
.msc-header,
.feat-nav,
.hero-bottom,
.pay-bar,
.sp-bottom,
.tb-modal-nav {
  background: var(--glass-bg);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
          backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  border-color: var(--glass-line);
}

/* 全出血的那些：左右没有边框可谈，留着会在屏幕边缘画两条竖线 */
.topbar,
.tryon-bar,
.gy-player-bar,
.result-header,
.gen-progress-header,
.player-bar,
.bottom-order,
.cart-bar,
.site-foot,
.app-header,
.sub-header,
.tab-bar,
.msc-header,
.feat-nav,
.payment-bill-header,
.pay-bar,
.hero-bottom,
.sp-bottom,
.tb-modal-nav {
  border-left: 0;
  border-right: 0;
}

/* 贴边浮层要挡住底下的内容，纯玻璃不够 —— 补一层同色底的渐变，
   从"看得见底下"过渡到"完全挡住"，避免文字和滚动内容叠在一起。 */
.topbar,
.site-foot,
.bottom-order,
.cart-bar,
.player-bar {
  background-image: linear-gradient(
    color-mix(in srgb, var(--bg) 30%, transparent),
    transparent
  );
}

/* ==========================================================================
   V. 加载态 / 空态 / 徽标 / 进度 —— "大厂感"的收口
   ==========================================================================
   分寸：这些是**功能性**组件，不是装饰。加它们是因为原来确实缺：
     · 光屿/音纪 生成时要等几秒，之前只有一个转圈，页面像卡住了
     · 列表项没有统一的按压反馈，点下去不知道有没有生效
     · 角标到处写 inline style，改主题时漏掉

   每一条都在 6 套主题下验证过对比度。
   -------------------------------------------------------------------------- */

/* ---------- 骨架屏：占住版位，别让内容"跳"出来 -------------------------
   正式名 .skeleton；.skel / .skel--text / .skel--title / .skel--block /
   .skel--circle 是历史别名，一并认 —— 三层名字做同一件事曾经导致
   "每个名字各写一份样式、谁也没被接上"，合并为唯一实现 + 并列选择器。

   实现取 ::after 扫光而不是 background-position 位移：
   前者不占用 background 属性，宿主元素自己还能有底色/图片。 */
.skeleton,
.skel {
  position: relative;
  overflow: hidden;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}
.skeleton::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--ink) 7%, transparent),
    transparent
  );
  animation: skeleton-sweep 1.4s var(--ease-soft) infinite;
}
@keyframes skeleton-sweep {
  100% { transform: translateX(100%); }
}

/* 科技侧（光屿/音纪）的骨架：扫光是品牌色的，和它们的发光语汇一致 */
.skeleton--tech::after {
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--accent-glow) 14%, transparent),
    transparent
  );
}

.skeleton--text,
.skel--text   { height: 1em; margin: .28em 0; }
.skeleton--title,
.skel--title  { height: 1.4em; width: 55%; margin-bottom: .5em; }
.skeleton--thumb { aspect-ratio: 3 / 4; border-radius: var(--r-md); }
.skeleton--block,
.skel--block  { aspect-ratio: 4 / 3; border-radius: var(--r-md); }
.skeleton--circle,
.skel--circle { border-radius: var(--r-full); }
.skeleton--line-2 { width: 78%; }
.skeleton--line-3 { width: 62%; }

/* 头像位：正方形 + 全圆角，常用尺寸一次给全 */
.skeleton--avatar { width: 40px; height: 40px; border-radius: var(--r-full); }

/* 卡片骨架：一张卡该有的高度一次占住，避免"卡片先矮后高"的跳动 */
.skeleton--card { height: 96px; border-radius: var(--r-md); }

@media (prefers-reduced-motion: reduce) {
  .skeleton::after { animation: none; }
}

/* ---------- 图片骨架：共享层自动挂（app-shell.js 的 bindSkeleton） -------
   为什么不用 .skeleton 那套（那是给"整块内容"用的）：
   图片的骨架必须**贴在图片自己的盒子上**，因为它要跟图片同尺寸、同圆角。
   所以做成 <img> 的状态类，而不是额外插一个 div（那会污染布局）。 */
img.is-loading {
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  /* 微光扫过：和 .skeleton 同一套语汇 */
  background-image: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--ink) 7%, transparent),
    transparent
  );
  background-size: 220% 100%;
  animation: img-skeleton 1.4s var(--ease-soft) infinite;
  /* 图没来时不要画出浏览器的破图图标 */
  color: transparent;
}
@keyframes img-skeleton {
  0%   { background-position: 140% 0; }
  100% { background-position: -40% 0; }
}
img.is-loaded {
  animation: img-fade-in var(--dur-3) var(--ease-out) both;
}
@keyframes img-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  img.is-loading { animation: none; }
  img.is-loaded  { animation: none; }
}

/* ---------- 空态：给方向，不给情绪 --------------------------------------
   K 段已有 .empty 主体，这里只补它缺的"图形圆盘"。
   __art 与 __glyph 是同一个零件（历史命名），合并为并列选择器，
   避免"两个名字各写一套样式"。 */
.empty__art,
.empty__glyph {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: var(--r-full);
  color: var(--muted);
  background: var(--surface-inset);
  border: 1px solid var(--line);
}
.empty__art svg,
.empty__glyph svg { width: 26px; height: 26px; display: block; }

/* ---------- 环形仪表：生成类站点显示百分比
   （原名 .ring 与 K 段的 SVG 描边环撞名，会让环整体被旋转 90°，故改名） -------------------------------- */
.gauge {
  --gauge-size: 44px;
  --gauge-w: 4px;
  --gauge-val: 0;
  --gauge-hole: var(--surface);   /* 环内孔色：浮在什么底上就填什么色 */
  position: relative;
  width: var(--gauge-size);
  height: var(--gauge-size);
  flex: 0 0 auto;
  border-radius: 50%;
  background: conic-gradient(
    var(--accent) calc(var(--gauge-val) * 1%),
    color-mix(in srgb, var(--ink) 12%, transparent) 0
  );
  display: grid;
  place-items: center;
}
.gauge::before {
  content: '';
  position: absolute;
  inset: var(--gauge-w);
  border-radius: 50%;
  background: var(--gauge-hole);
}
.gauge--sm { --gauge-size: 34px; --gauge-w: 3px; }
.gauge--lg { --gauge-size: 76px; --gauge-w: 5px; }
/* 环里的东西（图标/数字）要压在孔上面 */
.gauge > * { position: relative; z-index: 1; }

/* 不确定态：拿不到真实百分比时**不要编一个假的**，
   转一段弧就够，用户要的是"在动"，不是一个骗人的 87% */
.gauge--spin { background: color-mix(in srgb, var(--ink) 10%, transparent); }
.gauge--spin::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: conic-gradient(from 0deg,
    transparent 0deg 250deg,
    var(--accent) 340deg,
    transparent 360deg);
  animation: gauge-spin 1.1s linear infinite;
  -webkit-mask: radial-gradient(circle, transparent calc(50% - var(--gauge-w)), #000 calc(50% - var(--gauge-w)));
          mask: radial-gradient(circle, transparent calc(50% - var(--gauge-w)), #000 calc(50% - var(--gauge-w)));
}
@keyframes gauge-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .gauge--spin::after { animation-duration: 3s; }
}
.gauge__v {
  position: relative;
  font-family: var(--font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  line-height: 1;
}

/* ---------- 进度条：细长、两端圆、带柔光 --------------------------------- */
.bar {
  height: 6px;
  border-radius: var(--r-full);
  background: color-mix(in srgb, var(--ink) 10%, transparent);
  overflow: hidden;
}
.bar__fill {
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--accent-lo), var(--accent-hi));
  transition: width var(--dur-4) var(--ease-out);
}
.bar--thin { height: 3px; }

/* 不确定进度：来回滑动，表示"还没法估时" */
.bar--indeterminate .bar__fill {
  width: 34% !important;
  animation: bar-slide 1.5s var(--ease-soft) infinite;
}
@keyframes bar-slide {
  0%   { transform: translateX(-105%); }
  100% { transform: translateX(300%); }
}
@media (prefers-reduced-motion: reduce) {
  .bar--indeterminate .bar__fill { animation: none; width: 60% !important; }
}

/* ---------- 信息行：图标 + 文本 + 右侧值/chevron ------------------------- */
.info-row {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  min-height: var(--tap-min);
  border-radius: var(--r-md);
  color: var(--ink);
  text-decoration: none;
  transition: background var(--dur-1) var(--ease-out);
}
.info-row__icon {
  flex: 0 0 auto;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--accent);
}
.info-row__icon svg { width: 18px; height: 18px; }
.info-row__body { flex: 1; min-width: 0; }
.info-row__t { font-size: var(--t-body); color: var(--ink); }
.info-row__d { font-size: var(--t-xs); color: var(--muted); margin-top: 1px; }
.info-row__v {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-sm);
  color: var(--muted);
  flex: 0 0 auto;
}
.info-row__go { flex: 0 0 auto; color: var(--faint); }
.info-row__go svg { width: 16px; height: 16px; display: block; }

/* 可按压的行才给反馈 —— 不可点的不该有"能点"的暗示 */
a.info-row:active, button.info-row:active,
.info-row--tap:active { background: color-mix(in srgb, var(--ink) 6%, transparent); }
@media (hover: hover) {
  a.info-row:hover, button.info-row:hover,
  .info-row--tap:hover { background: color-mix(in srgb, var(--ink) 4%, transparent); }
}
@media (hover: hover) {
  a.info-row:hover .info-row__go svg,
  button.info-row:hover .info-row__go svg { transform: translateX(2px); }
  .info-row__go svg { transition: transform var(--dur-2) var(--ease-out); }
}

/* ==========================================================================
   P. 触摸热区兜底（.tap-safe）—— 命中层补到 44×44
   --------------------------------------------------------------------------
   依据 xiao9qiandiuan §56.1「可点击区域 ≥ 44px」，令牌 --tap-min: 44px。

   问题：一大批组件（返回门户、作品集导航、标签、小按钮）视觉高度只有 20~34px。
        它们**看着**没问题，手指**点不准** —— 而静态读 CSS 完全看不出这件事，
        因为"声明高度"和"手指能点到的范围"是两回事。

   为什么用 ::after 而不是直接把元素撑高：
         顶栏只有 52px、标签行、按钮组都排得很紧，元素真长到 44px 会顶破布局。
         ::after 不占文档流，是唯一既不动观感、又能让手指点准的办法。

   为什么宽度用 max(100%, 44px) 而不是写死像素：
         写 `inset: -Npx` 要按每个组件自己的宽度手算，改一次字号就全错。
         宽度设 44px 再居中（left:50% + translateX(-50%)），
         元素本来就比 44 宽时 max() 会取 100%，只在比 44 窄时才往外撑。
         高度同理，用 max(100%, 44px) + top:50%/translateY(-50%)。

   注意：只改命中层，**不改**任何视觉属性 —— 背景、边框、圆角全在元素自身，
        伪元素始终透明。所以像素对比不会有任何差异。
   ========================================================================== */
.tap-safe { position: relative; }
.tap-safe::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100%, var(--tap-min));
  height: max(100%, var(--tap-min));
  transform: translate(-50%, -50%);
  border-radius: inherit;
  /* 透明点击层：不画任何东西，只负责接住手指 */
}

/* 批量接线：这几个是跨站共用、且实测热区不足的组件。
   ⚠ 白名单要谨慎 —— 只收"确定永远是可点元素"的类名。
     反例：`.chip--sm` 在 9035 是**纯展示**的 <span>（#狮记龙井 这种标签），
     给它加透明 ::after 会变成一层隐形拦截层，把它后面内容的点击全吃掉。
     所以 `.chip` 不进白名单 —— 实测它本来就没有热区问题。 */
.portal-back,
.portfolio-nav__all,
.portfolio-nav__item,
.btn--sm,
.tag-chip {
  position: relative;
}
.portal-back::after,
.portfolio-nav__all::after,
.portfolio-nav__item::after,
.btn--sm::after,
.tag-chip::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100%, var(--tap-min));
  height: max(100%, var(--tap-min));
  transform: translate(-50%, -50%);
  border-radius: inherit;
}

/* ==========================================================================
   P. 3D 倾斜卡片 + 颗粒质感（Grainient）
   ==========================================================================
   来源：xiao9qianduan §6.5.2（卡片榜第 2：3D Tilt）、§6.7.1（背景榜第 6：
   Grainient 颗粒渐变，适合调性"高级感、品牌感"）；参数取自
   dev-expert/references/scroll-motion.md §3.5。

   红线（skill 原文，逐条落实在下面的实现里）：
     · 最大倾斜 8~12deg，**不要超过 15deg**        → 见 --tilt-max
     · perspective 1000px                          → 见 .tilt__inner
     · **移动端禁用**（没有 hover）                 → 见 @media (hover: hover)
     · **不要每个 section 都堆 3D，全站最多 1-2 个** → 由各站只挑一处挂决定
     · reduced-motion 下必须降级成静态构图          → 见文件末尾
     · 只动 transform 和 opacity，不动布局属性      → transform 是合成层属性
   ========================================================================== */

/* --- 颗粒：全站默认层 ---
   ⚠ 挂 body 而不是 .app：站点画布是 `<div class="app app-shell">`，
     而 9024 / 9034 各自写了 `.app-shell::after`（光晕）—— 与 `.app::after`
     特指度相同、站点 CSS 又后加载，共享层那条会被静默盖掉（实测确认）。
     body / html 的伪元素全项目 7 站都没占用，所以换到这里，不靠 !important 抢。

   宽度跟 .app 对齐并居中：纹理属于那张"纸"，不属于桌面底衬 ——
   否则桌面端 .app 两侧会一起长噪点，读起来是"屏幕脏了"。 */
body::after {
  content: '';
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  width: min(100%, var(--app-max));
  margin-inline: auto;
  z-index: var(--z-toast);
  pointer-events: none;
  opacity: var(--grain-op, .05);
  background-image: var(--grain-tex, none);
  background-size: var(--grain-size, 160px) var(--grain-size, 160px);
  background-repeat: repeat;
}

/* 深底：胶片颗粒。高频细噪点，在近黑面上读作"胶片" */
:root,
[data-theme='deep'],
[data-theme='night'],
[data-theme='moye'] {
  --grain-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* 浅底：纸纹。低频纤维感，在白面上读作"纸" */
[data-theme='xuan'],
[data-theme='sujian'],
[data-theme='zhisu'],
[data-theme='ouhe'] {
  --grain-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.9'/%3E%3C/svg%3E");
}

/* 局部颗粒层：给 hero 之类"想让材质更明显"的区块按需加 class。
   全站那层是很淡的底噪；这一层给记忆点用，可以更明显。 */
.grain {
  position: relative;
}
.grain::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 1;
  opacity: calc(var(--grain-op, .05) * 2.2);
  background-image: var(--grain-tex, none);
  background-size: var(--grain-size, 160px) var(--grain-size, 160px);
  background-repeat: repeat;
}
.grain--paper {
  --grain-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23g)' opacity='0.9'/%3E%3C/svg%3E");
}
.grain--film {
  --grain-tex: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
}

/* --- 3D 倾斜卡片 ---
   两种挂法，透视的给法不同，**不能混**：

   ① 内层模式（推荐，卡片用）：
        [data-tilt] 提供 perspective（perspective 属性只作用于后代），
        .tilt__inner 负责旋转。外层还能继续接站点自己的 hover 变换。

   ② self 模式（整块 hero 用）：
        元素自己旋转，就得把 perspective() 写进 transform 函数里。
        ⚠ 不能在元素自己身上写 perspective 属性 ——
          自己的 perspective 对自己无效，会退化成平面斜切
          （不报错、截图看着也"斜了"，只有量 computed transform 才发现）。 */
[data-tilt] {
  perspective: 1000px;
}
[data-tilt] > .tilt__inner {
  transform-style: preserve-3d;
  transform:
    rotateX(var(--tilt-x, 0deg))
    rotateY(var(--tilt-y, 0deg));
  transition: transform var(--dur-2, 220ms) var(--ease-out, cubic-bezier(.22,.61,.36,1));
}
[data-tilt].tilt--self {
  transform-style: preserve-3d;
  transform:
    perspective(1000px)
    rotateX(var(--tilt-x, 0deg))
    rotateY(var(--tilt-y, 0deg));
  transition: transform var(--dur-2, 220ms) var(--ease-out, cubic-bezier(.22,.61,.36,1));
}
/* ⚠ will-change 只在**鼠标真的在上面**时才挂（motion-standards §九 第 2 条：
   "用完就移除，不要一直挂着，浪费 GPU"）。
   常驻会让每个 tilt 元素单独提升为一个合成层 —— 一页 43 张卡就是 43 层。
   由 app-shell.js 在 mouseenter/mouseleave 上切换 .is-tilting 控制。 */
[data-tilt].is-tilting > .tilt__inner,
[data-tilt].is-tilting.tilt--self {
  will-change: transform;
}
/* 跟手时过渡要快（慢半拍会"拖拽感"），回正时慢一点 */
[data-tilt].is-tilting > .tilt__inner,
[data-tilt].is-tilting.tilt--self {
  transition-duration: 90ms;
}

/* ★ 只给有 hover 的设备启用（skill：移动端禁用）。
   触摸设备上 mousemove 会在点按后"粘"住上一次位置，
   卡片就歪着不回正 —— 这是必须挡住的原因，不只是性能。 */
@media (hover: hover) and (pointer: fine) {
  [data-tilt] { cursor: pointer; }
}

/* ★ reduced-motion：降级成静态构图（skill 红线） */
@media (prefers-reduced-motion: reduce) {
  [data-tilt] > .tilt__inner,
  [data-tilt].tilt--self {
    transform: none !important;
    transition: none !important;
    will-change: auto;
  }
  .grain::before { opacity: calc(var(--grain-op, .05) * 2.2); }
}

/* 触摸设备：不接线（JS 里也会挡一道，这里是 CSS 侧兜底） */
@media not all and (hover: hover) {
  [data-tilt] > .tilt__inner,
  [data-tilt].tilt--self {
    transform: none !important;
  }
}


@media print {
  .topbar, .tabbar, .portfolio-nav, .toast-host, .skip-link { display: none !important; }
  body, .app { background: #fff; box-shadow: none; max-width: none; }
  a[href^='http']::after { content: ' (' attr(href) ')'; font-size: 10px; color: #666; }
}
