/* components: cards, timeline, stacks, modal, search, form, toast */
    /* ── 作品网格 ── */
    .portfolio-grid {
      display: grid;
      grid-template-columns: repeat(12, minmax(0, 1fr));
      gap: 1.5rem;
      align-items: stretch;
    }
    .portfolio-card {
      position: relative;
      background: var(--surface);
      border: 1px solid var(--line-strong);
      border-radius: var(--radius-xl);
      overflow: hidden;
      display: flex; flex-direction: column;
      min-width: 0; height: 100%;
      box-shadow: var(--shadow-card);
      contain: layout paint style;
      transition: transform 0.55s var(--ease-spring), box-shadow 0.55s var(--ease-out), border-color 0.4s var(--ease-out);
    }
    /* 顶部内高光：卡面有一层纸的厚度 */
    .portfolio-card::before {
      content: ''; position: absolute; inset: 0; z-index: 3;
      border-radius: inherit; pointer-events: none;
      box-shadow: inset 0 1px 0 color-mix(in srgb, #fff 72%, transparent);
    }
    html[data-theme="dark"] .portfolio-card::before { box-shadow: inset 0 1px 0 rgba(255,255,255,0.06); }

    /* 右上角外链指示：玻璃圆钮 + 斜箭头 */
    .portfolio-card.has-github::after {
      content: '↗';
      position: absolute; top: 0.95rem; right: 0.95rem; z-index: 5;
      width: 34px; height: 34px; border-radius: 50%;
      display: grid; place-items: center;
      font-family: var(--font-body); font-size: 1rem; font-weight: 700; line-height: 1;
      color: var(--ink);
      background: color-mix(in srgb, var(--surface) 88%, transparent);
      backdrop-filter: blur(12px) saturate(1.5);
      -webkit-backdrop-filter: blur(12px) saturate(1.5);
      border: 1px solid var(--line-bold);
      box-shadow: 0 2px 10px rgba(20,40,70,0.14);
      opacity: 0.88;
      pointer-events: none;
      transition: opacity 0.3s var(--ease-out), transform 0.42s var(--ease-spring),
                  background-color 0.32s var(--ease-out), color 0.32s var(--ease-out), border-color 0.32s var(--ease-out);
    }
    .portfolio-card.has-github:hover::after {
      opacity: 1; transform: translate(2px, -2px) scale(1.05);
      background: linear-gradient(130deg, var(--blue-700), var(--blue-500));
      color: #fff; border-color: transparent;
      box-shadow: 0 10px 22px -10px color-mix(in srgb, var(--blue-600) 72%, transparent);
    }
    /* 后台编辑态下让位给编辑/删除按钮 */
    .portfolio-card:has(.btn-delete-card)::after { display: none; }

    .portfolio-card.has-github { cursor: pointer; }
    .portfolio-card:hover {
      transform: translateY(-4px);
      border-color: var(--line-bold);
      box-shadow: var(--shadow-lift);
      will-change: transform;
    }
    .portfolio-card.layout-large { grid-column: span 7; }
    .portfolio-card.layout-narrow { grid-column: span 5; }
    .portfolio-card.layout-half { grid-column: span 6; }
    .portfolio-card.layout-wide { grid-column: 1 / -1; display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(320px, 0.95fr); }

    .portfolio-card .btn-delete-card,
    .portfolio-card .btn-edit-card {
      position: absolute; top: 0.95rem; right: 0.95rem; z-index: 10;
      width: 30px; height: 30px; border-radius: 50%; border: none;
      background: color-mix(in srgb, var(--surface) 88%, transparent);
      backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
      color: var(--danger); font-size: 1rem; cursor: pointer;
      display: flex; align-items: center; justify-content: center;
      opacity: 0; transform: scale(0.9);
      transition: opacity var(--t-mid), transform var(--t-mid), background-color var(--t-mid);
      box-shadow: var(--shadow-sm);
    }
    .portfolio-card .btn-edit-card { right: 3.2rem; color: var(--blue-600); font-size: 0.92rem; }
    .portfolio-card:hover .btn-delete-card,
    .portfolio-card:hover .btn-edit-card { opacity: 1; transform: scale(1); }
    .portfolio-card .btn-delete-card:hover { background: color-mix(in srgb, var(--danger) 14%, var(--surface)); }
    .portfolio-card .btn-edit-card:hover { background: color-mix(in srgb, var(--blue-500) 14%, var(--surface)); }

    .card-image {
      width: 100%; aspect-ratio: 16/10;
      background: linear-gradient(145deg, var(--blue-50) 0%, var(--surface-3) 52%, var(--image-gradient-end) 100%);
      display: flex; align-items: center; justify-content: center;
      position: relative; overflow: hidden;
    }
    .layout-large .card-image { aspect-ratio: 16/11; }
    .layout-narrow .card-image { aspect-ratio: 4/3; }
    .layout-half .card-image { aspect-ratio: 16/10; }
    .layout-wide .card-image { aspect-ratio: auto; min-height: 380px; }
    .card-image.has-photo { background: var(--image-bg); }
    .card-image img {
      width: 100%; height: 100%; object-fit: cover; display: block;
      /* 悬停响应提速：0.75s 是悬停里最慢的一环（要 3/4 秒才到位，感觉「反应慢半拍」），
         收到 0.42s 后仍保留 spring 的柔感，但按下/移入的反馈明显更快。 */
      transition: transform 0.42s var(--ease-spring);
    }
    .portfolio-card:hover .card-image img { transform: scale(1.05); }
    /* 演示视频与封面图同款 cover 铺满。⚠️ 刻意**不**写进上面那条 :hover 放大规则：
       放大静态图只是裁掉外圈，放大视频会把画面内容一块放大裁掉，像「镜头推近」而不是「真屏幕」。
       所以这里的 transform 永远不动。 */
    .card-image video {
      width: 100%; height: 100%; object-fit: cover; display: block;
    }
    .card-image .placeholder-icon { font-size: 3rem; opacity: 0.25; transition: transform 0.5s var(--ease-spring); }
    .portfolio-card:hover .placeholder-icon { transform: scale(1.1) rotate(-3deg); }
    /* 只压顶部：card-tag 在左上、操作钮在右上，都落在顶边一带。
       原来底部那道 24% 的压暗带上面没有任何元素，只会把浅色界面的产品截图
       （后台看板、仪表盘）整体压灰一档，看着像图没加载出来。 */
    .card-image::after {
      content: ''; position: absolute; inset: 0; pointer-events: none;
      background: linear-gradient(180deg, rgba(20,40,70,0.16) 0%, rgba(20,40,70,0.05) 16%, transparent 32%);
      transition: opacity 0.45s var(--ease-out);
    }
    /* 极细内边框高光：给动态画面（视频）一个「嵌在卡片里的屏幕」的收边。
       用 inset box-shadow 而不是 border —— border 会占盒模型、把视频挤出 1px 缝隙。
       z-index 3 压在遮罩 ::after 之上；pointer-events:none 保证不拦卡片点击。 */
    .card-image::before {
      content: ''; position: absolute; inset: 0; z-index: 3; pointer-events: none;
      box-shadow: inset 0 0 0 1px rgba(20,40,70,0.12);
      transition: box-shadow 0.45s var(--ease-out);
    }
    html[data-theme="dark"] .card-image::before { box-shadow: inset 0 0 0 1px rgba(255,255,255,0.13); }

    .card-tag {
      position: absolute; top: 1rem; left: 1rem; z-index: 2;
      display: inline-flex; align-items: center; gap: 0.42rem;
      max-width: calc(100% - 4.6rem);
      padding: 0.36rem 0.78rem 0.36rem 0.68rem;
      font-size: 0.7rem; font-weight: 570; letter-spacing: 0.015em;
      background: color-mix(in srgb, var(--surface) 84%, transparent);
      backdrop-filter: blur(12px) saturate(1.4); -webkit-backdrop-filter: blur(12px) saturate(1.4);
      border: 1px solid color-mix(in srgb, #fff 44%, transparent);
      border-radius: 100px; color: var(--blue-700);
      box-shadow: 0 2px 10px rgba(20,40,70,0.10);
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
      transition: color 0.3s var(--ease-out), background-color 0.3s var(--ease-out);
    }
    .card-tag::before {
      content: ''; width: 5px; height: 5px; border-radius: 50%; flex: 0 0 auto;
      background: linear-gradient(135deg, var(--blue-600), var(--blue-400));
    }
    html[data-theme="dark"] .card-tag { border-color: rgba(255,255,255,0.14); }
    .portfolio-card:hover .card-tag { color: var(--blue-700); }

    .card-body { padding: 1.55rem 1.7rem 1.75rem; flex: 1; display: flex; flex-direction: column; }
    .layout-wide .card-body { padding: 2.4rem 2.5rem; align-self: center; }
    .card-title {
      font-family: var(--font-display); font-size: 1.34rem; font-weight: 600;
      letter-spacing: -0.022em; color: var(--ink); margin-bottom: 0.44rem;
      text-wrap: balance;
    }
    .layout-large .card-title,
    .layout-wide .card-title { font-size: 1.52rem; }
    .card-desc { font-size: 0.9rem; color: var(--ink-muted); font-weight: 400; line-height: 1.66; flex: 1; }
    .card-meta {
      display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
      margin-top: 1.2rem; padding-top: 1rem;
      font-size: 0.75rem; color: var(--ink-light); font-weight: 480;
      border-top: 1px solid var(--line);
      font-variant-numeric: tabular-nums;
    }
    .card-meta span {
      display: inline-flex; align-items: center; gap: 0.3rem;
      padding: 0.19rem 0.55rem; border-radius: 999px;
      background: var(--surface-3);
      white-space: nowrap;
      max-width: 100%; min-width: 0;
      overflow: hidden; text-overflow: ellipsis;
    }
    /* ── 条目状态图标：置顶 / 仅自己（2026-09-23 由文字胶囊改为图标）──
       胶囊时代这两个徽章各自带底色（置顶蓝、仅自己灰），在 meta 行里比正文还抢眼，
       两套配色也读不出是同一类东西。现在统一成无底色小图标、统一取日期同色
       （--ink-light），并让位到日期正下方，只承担「一句注解」的角色。
       上面 .card-meta span 的胶囊规则会连 .item-flags 一起命中，这里显式复位。 */
    .item-flags {
      display: inline-flex; align-items: center; gap: 0.42rem;
      color: var(--ink-light); line-height: 0;
    }
    .item-flag { display: inline-flex; align-items: center; }
    .item-flag svg { width: 14px; height: 14px; display: block; }
    .card-meta .item-flags {
      gap: 0.42rem; padding: 0; background: none; box-shadow: none;
      border-radius: 0; overflow: visible;
    }
    .card-links { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 1.1rem; }
    .card-link {
      min-height: 33px; display: inline-flex; align-items: center; gap: 0.34rem;
      padding: 0 0.85rem; border-radius: 999px;
      border: 1px solid var(--line-strong);
      color: var(--ink-muted); background: transparent; text-decoration: none;
      font-size: 0.78rem; font-weight: 520;
      transition: border-color var(--t-mid), background-color var(--t-mid), color var(--t-mid), transform var(--t-mid), box-shadow var(--t-mid);
    }
    .card-link::after {
      content: '↗'; font-size: 0.78em; opacity: 0.62;
      transition: transform 0.32s var(--ease-spring), opacity 0.32s var(--ease-out);
    }
    .card-link:hover {
      border-color: color-mix(in srgb, var(--blue-500) 34%, transparent);
      background: var(--blue-50);
      color: var(--blue-600);
      transform: translateY(-1.5px);
      box-shadow: var(--shadow-sm);
    }
    .card-link:hover::after { transform: translate(1px, -1px); opacity: 1; }
    .card-link:focus-visible { outline: none; box-shadow: var(--ring); }
    /* 整卡即链接（stretched link）：卡内已经有 GitHub / Demo 两个真 <a>，
       不能再把整张卡套进 <a>（嵌套 a 非法），所以把 GitHub 那一个的命中区拉满整卡，
       替代原先 div[role=link] + tabindex + JS 转发。中键新开、右键复制链接、状态栏
       预览这些浏览器默认行为随之回来。卡内其余可点元素本身带 z-index（链接 2、
       编辑/删除钮 10、右上 ↗ 徽标 5 且 pointer-events:none），都在覆盖层之上。 */
    .card-link--stretch::before {
      content: ''; position: absolute; inset: 0; z-index: 1;
    }
    /* ⚠️ 这条必须排除 --stretch：一旦 GitHub 那个 <a> 自己也 position:relative，
       它的 ::before 就以「按钮」为包含块，inset:0 只盖住按钮自己 —— 覆盖层看着
       写上了、其实一寸没铺开，点卡片正文仍然什么都不发生（实测 elementFromPoint
       打在 h3.card-title 上）。让它保持 static，::before 才会往上找到
       .portfolio-card（position:relative）作为包含块，铺满整卡。 */
    .card-links .card-link:not(.card-link--stretch) { position: relative; z-index: 2; }
    /* ── 旅途：时间轴 ── */
    .travel-list { position: relative; display: flex; flex-direction: column; }
    /* 极细虚线时间轴 */
    .travel-list::before {
      content: ''; position: absolute;
      left: calc(2.55rem + 3px); top: 2.6rem; bottom: 2.8rem;
      border-left: 1px dashed color-mix(in srgb, var(--blue-400) 46%, transparent);
      pointer-events: none;
    }

    .travel-item {
      display: grid; align-items: center;
      grid-template-columns: 104px minmax(0, 1fr) auto;
      gap: 2.1rem;
      padding: 1.9rem 1rem 1.9rem 2.55rem;
      cursor: pointer; position: relative; width: 100%; align-self: stretch;
      border-radius: var(--radius-lg);
      isolation: isolate;
    }
    .travel-item.has-photo { grid-template-columns: 104px 180px minmax(0, 1fr) auto; }

    /* 搜索命中 / 键盘聚焦的描边框：功能性提示，保留（悬停不再触发，见下方 ⑭） */
    .travel-item::before {
      content: '';
      position: absolute; inset: 0;
      border-radius: var(--radius-lg);
      background: linear-gradient(100deg,
        color-mix(in srgb, var(--blue-50) 92%, transparent) 0%,
        color-mix(in srgb, var(--blue-50) 88%, transparent) 100%);
      box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blue-500) 13%, transparent);
      opacity: 0; pointer-events: none;
      transition: opacity 0.22s var(--ease-out);
      z-index: 0;
    }
    .travel-item.search-target::before,
    .travel-item:focus-visible::before { opacity: 1; }
    .travel-item:focus-visible::before { box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--blue-500) 48%, transparent); }
    .travel-item:focus-visible { outline: none; }

    /* 时间轴节点 */
    .travel-date {
      display: inline-flex; align-items: center; gap: 0.62rem;
      font-family: var(--font-mono);
      font-size: 0.78rem; font-weight: 500; color: var(--ink-light);
      letter-spacing: 0.055em; font-variant-numeric: tabular-nums;
      white-space: nowrap;
    }
    .travel-date::before {
      content: ''; width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
      background: var(--surface);
      box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--blue-500) 52%, transparent), 0 0 0 4px var(--bg);
      transition: background-color 0.35s var(--ease-out), box-shadow 0.35s var(--ease-out), transform 0.4s var(--ease-spring);
    }
    .travel-item:hover .travel-date::before {
      background: var(--blue-500);
      box-shadow: inset 0 0 0 2px var(--blue-500), 0 0 0 4px var(--bg), 0 0 0 7px color-mix(in srgb, var(--blue-500) 15%, transparent);
      transform: scale(1.1);
    }

    /* 日期列（2026-09-23 新增）：日期在上，状态图标紧随其下。
       图标用 padding 跳过时间轴圆点的宽度，与日期**文字**左对齐 ——
       视觉上挂在日期下面，而不是挂在圆点下面。 */
    .travel-date-col {
      display: flex; flex-direction: column; align-items: flex-start;
      gap: 0.5rem; min-width: 0;
    }
    .travel-date-col .item-flags { padding-left: calc(7px + 0.62rem); }

    .travel-date-col,
    .travel-date,
    .travel-stack,
    .travel-info,
    .inspo-icon,
    .inspo-media,
    .inspo-info,
    .item-meta-actions { position: relative; z-index: 2; }

    /* ── 旅途照片堆叠（2026-09-23 用户要求；当天第二轮改为「随手一搁」）──
       原来是单张 <img>。现在是一摞相片：前景 1 张 + 纸背 2 张。
       点「照片」= 抽走最上面那张、翻到下一张（循环）；点「正文」= 打开日志详情
       —— 所以照片那半边要 stopPropagation。

       🔴 第二轮的关键改动：纸背的歪斜**方向和幅度都是随机的**（见 stackPoses()）。
       第一版是「同向扇面」—— 每层固定往右上错 7px、转 2.9°，读起来是精心摆好的
       一副牌；用户要的是「随意堆叠」。现在两张纸背各自朝随机方位戳出去，常常一左
       一右分居两侧 —— 「两侧都露边」才读得出是随手撂下的一叠。

       几何全部由 JS 写成行内自定义属性（--tx/--ty/--rot/--sc/--op）。
       切换时只改这些变量的值 → 浏览器对 transform 的**计算值**做补间 ⇒ 不碰布局、
       不触发重排，是纯合成层动画。
       为什么不沿用上一版把几何写成 CSS 里 --d 的 calc()：随机姿态没法用一个标量
       表达。data-d 于是降级为纯「层级标记」，只供 hover 时挑出最上面那张加深投影。

       🔴 窄屏要收小，而且**横向位移 / 纵向位移 / 旋转必须各收各的**
       （--pile-tx / --pile-ty / --pile-r）。手机端堆叠改通栏，容器从 180×135
       变成 329×206，此时三件事的缩放规律完全不同：
         · 位移是绝对像素 —— 容器大了它一点不变，相对就变小了；
         · 旋转的外摆按容器尺寸**等比放大** —— 同样 6°，329 宽的盒子摆幅是 180 的 1.8 倍；
         · 缩放的内缩也按容器尺寸等比放大，而且它是**四边同时往里吃**，
           横向 329×2.6%≈8.6px 的内缩正好把旋转横向补回来的 ~9.7px 抵消干净
           → 实测横向外露只剩 0.4px。
       第一版用单个 --pile 同时收位移与旋转，手机端外露只有 0~9px，等于没堆。
       现在：位移横向多留（手机端左右各还有 ~18px 内边距可用）、纵向少留
       （上下只有 0.85/1.15rem 间距）、旋转只收三成，靠旋转把横向补回来。 */
    .travel-stack {
      position: relative; display: block; width: 100%; aspect-ratio: 4 / 3;
      margin: 0; padding: 0; border: 0; background: none; text-align: left;
      font: inherit; color: inherit; cursor: pointer;
      -webkit-tap-highlight-color: transparent;
      --pile-tx: 1;            /* 横向位移系数 */
      /* 纵向位移系数（2026-09-24）—— 保持满值。纵向只对「第一条旅途记录」
         掐掉朝上那一半：见下面的 --pile-ty-floor 与 .is-no-up-over。 */
      --pile-ty: 1;            /* 纵向位移系数 */
      --pile-r: 1;             /* 旋转系数 */
      /* 🔴 纵向位移的**下限**（2026-09-24，第十一轮）
         ────────────────────────────────────────────────────────────────
         -9999px = 等于不设限（姿态里的 --ty 只会是 ±16px 级别，max() 取不到它）。
         只有第一条旅途记录会被 JS 挂上 .is-no-up-over，那时它变成 0px，
         `max(--ty, 0px)` 就把「朝上错开」这一半整条掐掉。

         为什么用「变量 + max()」而不是直接改 --pile-ty：收 --pile-ty 是**双向**
         缩水，下方纸背的错位会跟着一起变小，「一叠」的层次就没了。
         ⚠️ 只限制第一条是用户 2026-09-24 的明确要求 —— 我先做成了全局夹取，
            用户否掉：「不喜欢这个改动，只限制第一条，剩下内容不做限制」。
            其余 7 条的行内 --tx/--ty 一字未改，随机感完整保留。 */
      --pile-ty-floor: -9999px;
      transition: transform 0.5s var(--ease-spring);
    }
    .travel-stack:focus-visible { outline: none; border-radius: var(--radius-md); box-shadow: var(--ring); }
    /* 🔴 只有第一条旅途记录打开「不许朝上错开」，而且**只在桌面断点**（2026-09-24）
       开关在这里，几何在 .travel-photo 的 max(--ty, --pile-ty-floor) 里。
       默认的 -9999px 让其余 7 条完全不受影响，连姿态数值都不碰。

       为什么限定桌面：用户原话「手机端没事就是电脑端」。而且手机端纸背的纵向露出
       本来就只有 ~3px 是位移贡献的（窄屏 --pile-ty=0.36），剩下 9px 全是旋转把纸角
       翘出包围盒 —— 夹 ty 收益很小却会改变手机端观感，所以窄屏把它还原回 -9999px
       （见 @media max-width:768px 里那一行）。 */
    .travel-stack.is-no-up-over { --pile-ty-floor: 0px; }

    .travel-photo {
      position: absolute; inset: 0; width: 100%; height: 100%;
      object-fit: cover; border-radius: var(--radius-md);
      border: 1px solid var(--line-strong);
      background: var(--blue-50); box-shadow: var(--shadow-sm);
      /* 支点移到正中心：随机姿态里「往左戳」和「往右戳」都有，支点若还压在
         左下角（上一版给同向扇面用的 30%/86%），往右上戳的那张会被缩放的
         内缩吃掉大半，露不出边。 */
      transform-origin: 50% 50%;
      /* 🔴 纵向位移取「--ty 与 --pile-ty-floor 的较大者」（2026-09-24，第十一轮）
         ────────────────────────────────────────────────────────────────
         平时 --pile-ty-floor = -9999px ⇒ max() 恒等于 --ty，**一点没动**。
         只有第一条旅途记录挂了 .is-no-up-over（floor = 0px）⇒ 变成 max(--ty, 0px)，
         把「朝上错开」这一半掐掉。

         为什么第一条要掐：姿态里的 --ty 有正有负（随机决定朝上还是朝下错开），
         朝上的那几张会在前照片的上边**之上**露出。桌面端栈只有 180×135 而位移是
         绝对像素，露出能到 16.8px（照片自身才 135px 高，= 12.4%）；纸背内容若恰好是
         天空/浅墙，就和页面背景连成一片，用户读成「背景网格盖住了照片」/「被切了一刀」。
         手机端之所以没事：窄屏把 --pile-ty 收到 0.36。

         为什么不做全局（做过了，用户否掉）：其余 7 条里 6 条本来就有朝上的层，
         一起掐掉会把「随手一撂」的纵向随机性抹平。用户 2026-09-24 明确：
         「不喜欢这个改动，只限制第一条，剩下内容不做限制」。

         ⚠️ 试过用 overflow:hidden / clip-path 把露出的部分裁掉 —— **全部无效**：
         .travel-photo 带 translate3d() 会被提升为合成层，而 WebKit 的裁剪
         **不跨越合成层边界**（computed 值都正确写入，画面纹丝不动）。
         识别过程见 workspace/band_ident.mjs + desktop_layer_ab.mjs（网格染红证伪）。 */
      transform:
        translate3d(calc(var(--tx, 0px) * var(--pile-tx, 1)), calc(max(var(--ty, 0px), var(--pile-ty-floor, -9999px)) * var(--pile-ty, 1)), 0)
        rotate(calc(var(--rot, 0deg) * var(--pile-r, 1)))
        scale(var(--sc, 1));
      opacity: var(--op, 1);
      transition:
        transform 0.62s var(--ease-deck),
        opacity 0.46s var(--ease-out),
        box-shadow 0.5s var(--ease-out);
    }
    /* 🔴 姿态层不再常驻 will-change（2026-09-24 二轮，iOS「每个页面放大都崩」）
       ────────────────────────────────────────────────────────────────
       原先 [data-d="0"/"1"/"2"] 三层都挂 will-change: transform —— 一页 8 摞就是
       24 个常驻合成层（wk_layer_all_pages.mjs 实测 329×206 / 单层 2.34 MB，合计
       57.8 MB）。iOS pinch-zoom 必须按新倍率把它们**全部重新光栅化**，放大 3×
       就是 520 MB，是全站除图片位图之外最大的一块。
       但这两层其实**不需要**常驻：姿态（--tx/--ty/--rot/--sc/--op）只在点击后
       0.62s 的过渡里变化，其余时间一动不动；WebKit 会在过渡真正开始时按需提升。
       改成只给**真正露在外面的那几张**挂。 */
    /* ⚠️ 实测（2026-09-24 二轮，像素级前后比对）：三层全摘会让照片产生 ~0.5px 的
       亚像素重光栅位移（DM 里 `translate3d(calc(...))` 的分数偏移原来是交给合成器
       按亚像素处理的，去掉 will-change 后改由绘制阶段取整，照片会整体挪半像素）。
       所以**保留最上面那张（data-d="0"）**——那是肉眼唯一看清的一张，必须像素级不动；
       只摘掉 data-d="1"/"2" 两张基本被压住的纸背。8 摞仍是 24 → 16 个层，
       保存 2/3 的收益，同时把可见层的光栅化行为钉死在原样。 */
    .travel-photo[data-d="0"] { will-change: transform; }

    /* 🔴 完全被遮住的层不参与绘制（2026-09-24，iOS「放大页面就崩溃」的根治）
       ────────────────────────────────────────────────────────────────
       一摞 9 张里只有 3 张看得见：depth 0 = 前景张，1 与 2 = 两张纸背。
       depth ≥ 3 的那 6 张，data-d 被 Math.min() 压成 2（见 travelPhotoStack），
       姿态取自同一个 poses[2]，z-index 又更低 ⇒ 它们与最底下那张纸背
       **逐像素重合、且永远被压在它下面**，肉眼绝对看不见。
       但每一张都是一个独立合成层：layer_inventory.mjs（390×844 DPR3）实测
       56 个这样的层（8 条 × 7 张，含那张真底纸），单层 2.34 MB ⇒ 合计 133 MB，
       占全站合成层预算（208 MB）的 64%；iOS pinch-zoom 重新光栅化按缩放倍数²
       膨胀，放大 3× 就是 1200 MB —— WebContent 进程被直接顶崩。
       改成 display:none 后同一清单降到 8 层 / 19 MB。

       ⚠️ 为什么用 display:none，而不是 visibility:hidden / opacity:0 ——
       只有 display:none 能保证浏览器**不创建合成层**。另外两个在 WebKit 里
       仍会为带 will-change 的元素保留离屏 buffer，等于没省。

       副作用为零：这些层被完全遮住，去掉不改变任何一个像素。唯一要配套的是
       露边区域的合成不透明度 —— 原来靠「7 层同图叠加」把 alpha 顶到 ≈1，
       现在只剩 1 层，所以 stackPoses() 里把 poses[2].op 由 0.84 补成 1（见那里注释）。 */
    .travel-photo[data-hidden="1"] { display: none; }

    /* 抽牌：被点掉的前景张朝它**即将落到的那个方位**再冲过头一点，然后回落成纸背。
       起点恒为 identity —— 层位 0 的姿态就是正的（stackPoses 里写死），
       终点直接读行内 --tx/--ty/--rot/--sc/--op ⇒ 收尾（移除 .is-dealing）时
       与静息值**完全一致**，那一刻只发生 z-index 从 60 落回，位置和透明度都不动，无跳变。
       z-index 必须临时顶到最上 —— 否则它一瞬间就沉到纸背底下，
       「飞出去」的过程根本看不见（静态截图看不出来，只有看动画才发现）。 */
    .travel-photo.is-dealing {
      z-index: 60 !important;
      will-change: transform;   /* 唯一该提升的那一张：正在播关键帧 */
      animation: travelDeal 0.7s var(--ease-deck) both;
    }
    /* 飞出阶段**保持不透明**：第一版让 opacity 掉到 0.6，结果两张照片在半路叠成
       一团糊影。真实的一摞相片被抽走时也不会凭空变透明 —— 靠位移+旋转让开就行，
       变淡只留给最后「插回最底下」那一小段（末帧读 --op）。 */
    @keyframes travelDeal {
      0%   { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); opacity: 1; }
      38%  {
        transform:
          translate3d(calc(var(--ox, 0px) * var(--pile-tx, 1)), calc(var(--oy, 0px) * var(--pile-ty, 1)), 0)
          rotate(calc(var(--orot, 0deg) * var(--pile-r, 1)))
          scale(var(--osc, 1));
        opacity: 1;
      }
      100% {
        /* 🔴 末帧必须和 .travel-photo 的静息 transform **逐字一致**（含
           max(--ty, --pile-ty-floor) 这一层）。否则移除 .is-dealing 的那一刻
           会从「未夹取的位置」跳回「已夹取的位置」—— 就是一条肉眼可见的位移。
           38% 那帧的 --ox/--oy 是「飞出去」的量，向上飞是正常动作，**不要**跟着夹。
           ⚠️ 这里读得到 --pile-ty-floor 是因为自定义属性从 .travel-stack 继承下来 ——
              关键帧里的 var() 是按**被动画的那个元素**解析的，所以第一条与非第一条
              各自拿到正确值，一套关键帧够用。 */
        transform:
          translate3d(calc(var(--tx, 0px) * var(--pile-tx, 1)), calc(max(var(--ty, 0px), var(--pile-ty-floor, -9999px)) * var(--pile-ty, 1)), 0)
          rotate(calc(var(--rot, 0deg) * var(--pile-r, 1)))
          scale(var(--sc, 1));
        opacity: var(--op, 1);
      }
    }

    /* ⑭ 旅途条目悬停反馈（2026-09-17 用户要求，2026-09-23 适配堆叠）：
       原先给单张缩略图「抬起 + 加深投影」。改成整叠轻抬起，且只有最上面那张
       加深投影，纸背保持浅影 —— 三层阴影一起加深会糊成一块脏斑。
       阴影用 --shadow-photo（接触影 + 环境影双层），明暗两套主题各带对应色温。 */
    .travel-item:hover .travel-stack,
    .travel-item.search-target .travel-stack { transform: translateY(-4px) scale(1.026) rotate(-0.55deg); }
    .travel-item:hover .travel-photo[data-d="0"],
    .travel-item.search-target .travel-photo[data-d="0"] { box-shadow: var(--shadow-photo); }

    /* 张数角标：多于一张才出现，压在左下角（纸背往右上错，正好避开） */
    .stack-badge {
      position: absolute; left: 0.46rem; bottom: 0.46rem; z-index: 30;
      display: inline-flex; align-items: center; gap: 0.26rem;
      padding: 0.15rem 0.48rem 0.15rem 0.42rem; border-radius: 999px;
      font-family: var(--font-mono); font-size: 0.67rem; font-weight: 500;
      letter-spacing: 0.03em; font-variant-numeric: tabular-nums; line-height: 1.5;
      color: #fff; background: color-mix(in srgb, #0c1420 56%, transparent);
      -webkit-backdrop-filter: blur(9px) saturate(150%); backdrop-filter: blur(9px) saturate(150%);
      opacity: 0.74; pointer-events: none;
      transition: opacity 0.3s var(--ease-out), transform 0.36s var(--ease-spring);
    }
    .stack-badge svg { width: 10px; height: 10px; display: block; opacity: 0.85; }
    .travel-item:hover .stack-badge,
    .travel-stack:focus-visible .stack-badge { opacity: 1; transform: translateY(-1px); }

    .travel-info h3 {
      font-family: var(--font-display); font-size: 1.26rem; font-weight: 600;
      letter-spacing: -0.018em; color: var(--ink); margin-bottom: 0.3rem;
    }
    .travel-info p { font-size: 0.89rem; color: var(--ink-muted); font-weight: 400; line-height: 1.64; max-width: 32em; }

    /* 地点：图钉胶囊 */
    .travel-location {
      display: inline-flex; align-items: center; gap: 0.42rem;
      padding: 0.24rem 0.64rem; border-radius: 999px;
      font-size: 0.78rem; font-weight: 520; color: var(--blue-600);
      background: var(--blue-50);
      box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blue-500) 15%, transparent);
      white-space: nowrap;
    }
    .travel-location::before {
      content: ''; width: 7px; height: 7px; flex: 0 0 auto;
      border-radius: 50% 50% 50% 0;
      transform: rotate(-45deg);
      background: linear-gradient(135deg, var(--blue-600), var(--blue-400));
    }

    .item-meta-actions {
      display: flex; align-items: center; justify-content: flex-end;
      gap: 0.5rem; min-width: 160px;
    }
    .photo-count {
      font-family: var(--font-mono); font-size: 0.72rem; font-weight: 500;
      color: var(--ink-light); white-space: nowrap;
      padding: 0.2rem 0.52rem; border-radius: 999px; background: var(--surface-3);
      font-variant-numeric: tabular-nums;
    }

    /* ── 启发：双列瀑布流大图卡片 ── */
    .inspo-list { columns: 2; column-gap: 1.6rem; display: block; }
    .inspo-item {
      display: block;
      break-inside: avoid; -webkit-column-break-inside: avoid;
      margin: 0 0 1.6rem;
      padding: 0.72rem 0.72rem 0;
      background: var(--surface);
      border: 1px solid var(--line-strong);
      border-radius: var(--radius-xl);
      box-shadow: var(--shadow-card);
      overflow: hidden;
      cursor: pointer; position: relative;
      transition: transform 0.55s var(--ease-spring), box-shadow 0.55s var(--ease-out), border-color 0.4s var(--ease-out);
    }
    .inspo-item:hover {
      transform: translateY(-4px);
      border-color: var(--line-bold);
      box-shadow: var(--shadow-lift);
    }
    /* 搜索命中 / 键盘聚焦描边 */
    .inspo-item::before {
      content: ''; position: absolute; inset: 0; z-index: 5;
      border-radius: inherit; pointer-events: none;
      box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--blue-500) 58%, transparent);
      opacity: 0; transition: opacity 0.22s var(--ease-out);
    }
    .inspo-item.search-target::before,
    .inspo-item:focus-visible::before { opacity: 1; }
    .inspo-item:focus-visible { outline: none; }

    .inspo-media {
      display: block; width: 100%; overflow: hidden;
      border-radius: calc(var(--radius-xl) - 0.72rem);
      border: 1px solid var(--line);
      background: var(--blue-50);
    }
    .inspo-media img {
      display: block; width: 100%; height: auto;
      transition: transform 0.85s var(--ease-spring);
    }
    .inspo-item:hover .inspo-media img { transform: scale(1.04); }

    .inspo-icon {
      display: flex; align-items: center; justify-content: center;
      width: 100%; aspect-ratio: 16/10; font-size: 4.5rem;
      border-radius: calc(var(--radius-xl) - 0.72rem);
      /* 2026-10-09 用户要求：去掉 emoji 外面那个「蓝色圆角方块 + 蓝白渐变背景」，
         emoji 放大，让它直接落在卡片上 —— 原来那层渐变底（blue-100 → blue-50）
         加上 1px 描边，读起来像一块"没有内容的占位色块"，反而把 emoji 压小了。 */
      background: none;
      border: 0;
    }

    .inspo-info { padding: 1.05rem 0.62rem 0.1rem; }
    .inspo-info h4 {
      font-family: var(--font-display); font-size: 1.2rem; font-weight: 600;
      letter-spacing: -0.018em; line-height: 1.32; color: var(--ink); margin-bottom: 0.36rem;
    }
    .inspo-info p { font-size: 0.88rem; color: var(--ink-muted); font-weight: 400; line-height: 1.64; }

    .inspo-item .item-meta-actions {
      position: static;
      justify-content: flex-start; min-width: 0; gap: 0.45rem;
      padding: 0.95rem 0.62rem 1.05rem;
    }
    /* 启发条目的状态胶囊：实心蓝，与地点的浅蓝描边胶囊形成「填充 vs 描边」的区分 */
    .inspo-item .travel-location {
      /* 实心蓝：与地点的浅蓝描边胶囊形成「填充 vs 描边」的区分。
         ㉚ 底色改走 --grad-solid：原来是单色 var(--blue-500)，明暗两侧都是死板一块，
            和主按钮放在同一张卡里看，一个在过渡一个没有 —— 现在同一条令牌。 */
      color: var(--bg); background: var(--grad-solid);
      box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--blue-500) 30%, transparent);
    }
    .inspo-item .travel-location::before {
      width: 6px; height: 6px; border-radius: 50%; transform: none;
      background: color-mix(in srgb, var(--bg) 82%, transparent);
    }
    /* 编辑 / 删除按钮浮到图片右上角（几何与配色继承 .item-actions / .item-icon-btn） */
    .inspo-item .item-actions { top: 1.04rem; right: 1.04rem; }

    @supports (content-visibility: auto) {
      .portfolio-card { content-visibility: auto; contain-intrinsic-size: auto 520px; }
      .travel-item { content-visibility: auto; contain-intrinsic-size: auto 150px; }
    }
    /* ── 详情弹层内容 ── */
    .detail-modal::-webkit-scrollbar { width: 0; height: 0; }
    .detail-modal::-webkit-scrollbar-track { background: transparent; }
    .detail-modal::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--ink-light) 40%, transparent); border-radius: 999px; }
    .detail-kicker {
      font-size: 0.75rem; font-weight: 650; letter-spacing: 0.16em;
      text-transform: uppercase; color: var(--blue-700); margin-bottom: 0.65rem;
    }
    .detail-title {
      font-family: var(--font-display);
      font-size: var(--detail-title-size);
      line-height: 1.16; font-weight: 600; letter-spacing: -0.025em; color: var(--detail-title);
      margin-bottom: 0.8rem;
      /* 标题没有理由居中：它和 kicker / meta / 正文同属一条左基线 */
      text-align: left;
      text-wrap: var(--detail-title-wrap);
    }
    /* 正文块统一走同一左缘：弹性行按自身内容宽度排布，不再被父级 text-align 牵走 */
    .detail-kicker, .detail-title, .detail-desc { text-align: left; }
    .detail-meta {
      display: flex; gap: 0.6rem; flex-wrap: wrap; color: var(--ink-light);
      font-size: 0.85rem; margin-bottom: 1.45rem;
      padding-bottom: 1.1rem; border-bottom: 1px solid var(--line);
      font-variant-numeric: tabular-nums;
      justify-content: flex-start;
    }
    .detail-meta span { text-align: left; }
    .detail-desc { color: var(--ink-muted); line-height: 1.8; font-size: 1rem; white-space: pre-wrap; }
    /* 主图盒：**照片盒整幅居中，照片在盒内居中**（2026-10-01 定稿）。
       ────────────────────────────────────────────────────────────────────
       ⚠️ 这一格我前后改了三稿，教训是：**「左对齐」只管文字，不管照片**。
       用户原话「这两个页面给我左对齐」指的是 kicker/标题/meta/正文这条文字轴；
       我把它当成「连照片一起靠左」，于是绕了两圈：
         · 第一稿 `object-position: left` → 照片贴左、灰底全挤到右侧 502px，更失衡；
         · 第二稿 `width/height: auto` 让盒子贴合照片再 `flex-start` 靠左 →
           照片左缘确实和标题同一条竖线了，但用户看图仍是「比以前奇怪」。
       用户最后一句「照片盒居中别左对齐我没说清楚」把口径钉死：
         · 文字（kicker / 标题 / meta / 正文 / ✕）→ 左对齐（或右上角）
         · **照片盒 → 居中**，与用户改动前看到的完全一致。
       ⇒ 因此这里**退回基线写法**：`width: 100%` 的整幅盒 + `object-fit: contain`
          + `object-position: center center`，竖版照片左右对称留白（灰底 `--bg-subtle`
          本来就是为 contain 留白准备的中性底，见下）。
       只有一处相对基线是改进：高度上限从 `min(52vh, 540px)` 换成
       `--detail-photo-h`（dvh，按**弹窗自身高度**算，见令牌区注释）。 */
    .detail-gallery { margin-top: 1.5rem; }
    .detail-main-photo {
      width: 100%; max-height: var(--detail-photo-h);
      object-fit: var(--detail-photo-fit);
      object-position: var(--detail-photo-align);
      display: block;
      border-radius: var(--radius-md); border: 1px solid var(--line);
      background: var(--bg-subtle);
      box-shadow: var(--shadow-sm);
    }
    /* 主图左对齐后，右半边会空出一块 —— 让剩下的宽度由缩略图条吃下，
       照片与缩略图同处一条左基线，整块版心不掉。 */
    .detail-thumbs { display: flex; gap: 0.55rem; overflow-x: auto; padding: 0.7rem 0 0.1rem; scrollbar-width: thin; }
    .detail-thumb {
      width: 74px; aspect-ratio: 4/3; object-fit: cover; flex: 0 0 auto;
      border-radius: var(--radius-sm); border: 2px solid transparent;
      cursor: pointer; opacity: 0.66; background: var(--blue-50);
      transition: opacity var(--t-mid), border-color var(--t-mid), transform var(--t-mid);
    }
    .detail-thumb:hover { opacity: 0.86; transform: translateY(-1px); }
    .detail-thumb.active { opacity: 1; border-color: var(--blue-500); box-shadow: var(--shadow-sm); }

    /* ── 页脚 ── */
    .footer {
      /* ⑰ 页脚分割线（2026-09-17）：上一轮全删后用户反馈「文本飘起来」，恢复分割。
         线由 ::before 画成「中间实、往两端渐隐」：中间段用 --line-strong(17%)，
         比背景网格线(4.5%)深近 4 倍 → 有区分度；同属蓝灰系 → 不与背景冲突。
         下内边距略收（3.25rem → 2.5rem），减少文本下方悬空感。 */
      padding: 2.9rem 0 2.5rem; text-align: center; margin-top: auto;
      position: relative;
    }
    .footer::before {
      content: ''; position: absolute; top: 0; left: 0; right: 0; height: 1px;
      background: linear-gradient(90deg,
        transparent 0%,
        var(--line-strong) 22%,
        var(--line-strong) 78%,
        transparent 100%);
    }
    .footer p {
      font-family: var(--font-mono);
      font-size: 0.74rem; color: var(--ink-faint); font-weight: 400;
      letter-spacing: 0.045em;
    }
    /* ⚠️ 页脚社交链接的原「药丸按钮」外观已由文件下方 ⑩ 整体替换为「带下划线动效的
       纯文字链接」。2026-09-21 收尾时把这里被 ⑩ 压掉的声明全部删除，本处只保留
       ⑩ 没有重新声明的布局属性（方向 / 对齐 / 字号 / 去下划线）。改外观请看 ⑩。 */
    .footer-links { display: flex; justify-content: center; list-style: none; margin-bottom: 1.4rem; }
    .footer-links a {
      display: inline-flex; align-items: center;
      font-size: 0.86rem; text-decoration: none;
    }

    /* ── 弹窗 ── */
    .modal-overlay {
      position: fixed; inset: 0; z-index: 1000;
      background: var(--modal-overlay-bg);
      align-items: center; justify-content: center;
      /* 🔴 display:none 才是真正的根治（2026-09-24 二轮，iOS「**每个**页面放大都崩」）
         ────────────────────────────────────────────────────────────────
         上一轮只把 backdrop-filter / will-change 挪到 .active 上 —— 那解决的是
         「层里装什么」，**没解决层本身**：只要元素还是 `position: fixed; inset: 0`，
         iOS Safari 就必须为它保留一块全屏图层（全屏 fixed 元素要跟可视视口走）。
         wk_fixed_layers.mjs 实测：modal-overlay / search-overlay 各 11.3 MB，且这是
         **四条路由完全相同**的一份开销（四页固定层预算 588~602 MB @放大3×）——
         这正是"每一个页面都崩"的机制：与页面内容无关。
         改成 display:none 后元素连盒子都不生成，层彻底不存在。
         淡入交给 `display … allow-discrete` + `@starting-style`（Safari 17.5+ / Chrome 117+）；
         老浏览器只是失去淡入，功能不受影响。 */
      display: none;
      opacity: 0; pointer-events: none;
      transition: opacity 0.3s var(--ease-out), display 0.3s allow-discrete;
    }
    .modal-overlay.active {
      display: flex;
      opacity: 1; pointer-events: auto;
      backdrop-filter: blur(8px) saturate(1.2);
      -webkit-backdrop-filter: blur(8px) saturate(1.2);
      will-change: opacity;
    }
    /* 由 display:none 切到 flex 的那一帧，过渡起点由此给出 */
    @starting-style {
      .modal-overlay.active { opacity: 0; }
    }

    .modal {
      background: var(--surface);
      border-radius: var(--radius-xl);
      /* 520 → 600（2026-10-16）：两栏行里的 GitHub 链接等长 URL 在 520 下被截得只剩域名 */
      width: 90%; max-width: 600px; max-height: 85vh;
      /* ⚠️ 必须是 clip，不能只写 hidden（2026-10-25）：
         `overflow: hidden` 依然**是滚动容器**。点「仅自己可见 / 置顶显示」时 checkbox 拿到焦点，
         浏览器的「把聚焦元素滚进视野」会一路往上找可滚动的祖先，body 已经滚到底了，
         于是它去滚 .modal 本身 —— 实测 scrollTop 被推到 94px：整个头部顶出弹窗上沿
         （标题和 ✕ 消失），页脚随之上移，**按钮下面就空出 95px 白块**。
         `clip` 只裁剪、不建立滚动容器，滚不动，也就没有这段位移。 */
      overflow: hidden;
      overflow: clip;
      padding: 0;
      border: 1px solid var(--line);
      box-shadow: var(--shadow-lg), 0 0 0 1px color-mix(in srgb, var(--blue-500) 10%, transparent);
      transform: translateY(22px) scale(0.965);
      transition: transform 0.44s var(--ease-spring);
      display: flex;
      flex-direction: column;
    }
    /* will-change 同样只在打开时才挂（见 .modal-overlay 注释）—— 弹窗本体 339×659
       在 DPR3 下是 7.7 MB 的层，常驻纯属浪费。 */
    .modal-overlay.active .modal { transform: translateY(0) scale(1); will-change: transform, opacity; }

    .modal-header {
      /* ㊺ 全部走令牌，两类弹窗共用一套头部口径（见文件上方那对 token 声明） */
      padding: var(--modal-head-pad-top) var(--modal-head-pad-x) var(--modal-head-pad-bottom);
      display: flex; align-items: center; justify-content: space-between;
      gap: var(--modal-head-gap);
      flex-shrink: 0;
      z-index: 2;
    }
    /* 头部自身的宽度就是内容宽 —— 详情版把 padding-x 归零（内缩交给弹窗的 pad），
       普通版保留自己的 padding-x（它的弹窗 padding 是 0）。 */
    .modal:not(.detail-modal) .modal-header { position: relative; }
    .modal form {
      display: flex;
      flex-direction: column;
      flex: 1;
      min-height: 0;
      overflow: hidden;
      overflow: clip;   /* 同上：form 也是可被 focus 滚动推走的 hidden 容器 */
    }
    .modal-body {
      flex: 1;
      min-height: 0;
      overflow-y: auto;
      overscroll-behavior: contain;
      /* 底部内边距 1.25rem → 0.4rem（2026-10-25）：这块 padding 是滚动内容的末尾，
         滚到底时它会和 `.modal-actions` 的 padding-top 叠成一条 ~38px 的纯白带，
         夹在最后一个字段和页脚描边之间 —— 通篇都是白，看起来就像页脚上方空了一大块。
         页脚自己已经给了 1rem 上内边距，这里留 6px 只是别让卡片贴着描边。 */
      padding: 0.25rem 2.25rem 0.4rem 2.25rem;
      scrollbar-width: thin;
      scrollbar-color: color-mix(in srgb, var(--ink-light) 40%, transparent) transparent;
    }
    /* form 本身是 overflow:hidden、从不滚动，原先并列的 `.modal form::-webkit-scrollbar*` 是死选择器，已删 */
    .modal-body::-webkit-scrollbar { width: 5px; }
    .modal-body::-webkit-scrollbar-track { background: transparent; }
    .modal-body::-webkit-scrollbar-thumb {
      background: color-mix(in srgb, var(--ink-light) 35%, transparent);
      border-radius: 999px;
    }
    .modal-body::-webkit-scrollbar-thumb:hover {
      background: color-mix(in srgb, var(--ink-light) 55%, transparent);
    }
    .modal.detail-modal {
      width: min(92vw, 860px);
      max-width: min(860px, calc(100vw - 2rem));
      max-height: min(86vh, 820px);
      overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none;
      padding: var(--detail-pad-top) var(--detail-pad-x) var(--detail-pad-bottom);
      display: block;
      /* ㊹ 详情弹层版式：容器自己锚定（✕ 已改回普通流，不再依赖它；
         保留是因为本弹窗是滚动容器，留一个定位上下文给内部的装饰层/未来元素） */
      position: relative;
      text-align: left;
    }
    /* ㊺ 详情弹层的头部 = 普通弹窗那套结构（flex 行：文本块 + 定宽 ✕）。
       ⚠️ **刻意不再**让 ✕ 绝对定位贴角（上一稿的做法，实测与普通弹窗差 18.4px）——
          用户对照图的诉求就是「叉号和这个页面统一」：详情弹层与添加/编辑弹窗
          必须是同一个 ✕。两处都用「文本块 + 定宽 ✕ + flex」，位置才自然一致，
          也省掉了「相对内边距盒 / 内容盒」那套基准换算（我在那儿绕过一次弯）。

       头部用 flex-start 而非 center：详情版有两行（kicker + 大标题），整块高约 68px，
       用 center 会把 ✕ 压到「kicker 与标题之间的缝」上（实测偏 −85.7px），飘在半空。
       flex-start 让 ✕ 贴住**第一行 kicker**，与普通弹窗「✕ 贴住标题」是同一种秩序：
       ✕ 对齐标题块的首行；精确的纵向落点由 --detail-close-nudge 校准（见下）。 */
    .modal.detail-modal .modal-header {
      align-items: flex-start;
      padding-left: 0;          /* 横向内缩一律由 .detail-modal 的 pad 负责，免二次内缩 */
      padding-right: 0;
      text-align: left;
    }
    .detail-head-text { min-width: 0; }
    /* ㊺ ✕ 与普通弹窗**逐像素同位**（2026-10-01）。
       ────────────────────────────────────────────────────────────────────────
       两边「上距」都是相对弹窗**边框盒**顶边量的，把式子摊开就明白差在哪：

         普通弹窗  33.8 = 边框 1 + header pad-top 29.6 + (38.4 标题行高 − 32 ✕)/2
         详情弹层  33.8 = 边框 1 + 弹窗 pad-top   36   − margin-top 3.2

       ⇒ 详情版的头部 pad-top 恒为 0（纵向内缩由弹窗自己的 --detail-pad-top 提供），
         直接用会落在 36（比目标深 2.2px），靠 `--detail-close-nudge` 拉回。
         实测 1440×900：两边**都是 33.8**；390×844：两边**都是 23.4**。
         这是纯几何量，不是随手微调 —— 改 --detail-pad-top / ✕ 尺寸 / 断点时要重算。
       （右距无需补偿：两边都是「pad-x + 边框 1px」，天然相等，宽窄屏都实测相等。） */
    .modal.detail-modal .modal-close { margin-top: var(--detail-close-nudge, 0px); }
    .modal-title {
      font-family: var(--font-display); font-size: 1.5rem; font-weight: 600;
      letter-spacing: -0.02em; color: var(--ink);
    }
    .modal-close {
      /* ㊺ 尺寸走令牌（两类弹窗共用），兜底 32px 供 `.modal` 之外的调用点（如登录弹窗） */
      width: var(--modal-close-size, 32px);
      height: var(--modal-close-size, 32px);
      border-radius: 50%; border: none;
      background: var(--control-bg); color: var(--ink-muted);
      font-size: 1.1rem; cursor: pointer; display: flex; align-items: center; justify-content: center;
      transition: background-color 0.2s, color 0.2s, transform 0.2s;
      flex: 0 0 var(--modal-close-size, 32px); min-width: var(--modal-close-size, 32px);
      padding: 0; line-height: 1;
    }
    .modal-close:hover { background: var(--control-hover); color: var(--ink); transform: rotate(90deg); }
    .modal-close:focus-visible { outline: none; box-shadow: var(--ring); }

    /* ── 全局搜索 ── */
    .search-overlay {
      position: fixed; inset: 0; z-index: 1500;
      background: var(--overlay-bg);
      align-items: flex-start; justify-content: center;
      padding: clamp(5.5rem, 10vh, 8rem) 1.5rem 2rem;
      /* display:none 默认 —— 理由同 .modal-overlay（全屏 fixed 层，11.3 MB ⇒ 放大 3× 101.7 MB） */
      display: none;
      opacity: 0; pointer-events: none;
      transition: opacity 0.24s var(--ease-out), display 0.24s allow-discrete;
    }
    .search-overlay.active {
      display: flex;
      opacity: 1; pointer-events: auto;
      backdrop-filter: blur(20px) saturate(1.3);
      -webkit-backdrop-filter: blur(20px) saturate(1.3);
      will-change: opacity;
    }
    @starting-style {
      .search-overlay.active { opacity: 0; }
    }
    .search-panel {
      width: min(720px, 100%); max-height: min(72vh, 680px);
      background: color-mix(in srgb, var(--surface) 92%, transparent);
      border: 1px solid var(--line-strong);
      border-radius: var(--radius-lg);
      box-shadow: var(--shadow-lg), 0 0 0 1px color-mix(in srgb, var(--blue-500) 8%, transparent);
      overflow: hidden;
      transform: translateY(12px) scale(0.975);
      transition: transform 0.3s var(--ease-spring);
      contain: layout style;
    }
    .search-overlay.active .search-panel { transform: translateY(0) scale(1); will-change: transform; }
    .search-box {
      display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 0.85rem;
      padding: 1rem 1.1rem; border-bottom: 1px solid var(--line);
    }
    .search-box svg { width: 18px; height: 18px; color: var(--blue-500); }
    .search-input {
      min-width: 0; min-height: 44px; border: none; background: transparent;
      font-family: var(--font-body); font-size: 1.05rem; color: var(--ink);
    }
    .search-input::placeholder { color: var(--ink-light); }
    /* 隐藏 <input type="search"> 的原生清除按钮，避免与自带的关闭按钮并列出现两个 ✕ */
    .search-input::-webkit-search-cancel-button,
    .search-input::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
    .search-close {
      width: 36px; height: 36px; border-radius: 50%; border: none;
      background: var(--control-bg); color: var(--ink-muted);
      display: inline-flex; align-items: center; justify-content: center;
      cursor: pointer; transition: background-color var(--t-mid), color var(--t-mid), transform var(--t-mid);
    }
    .search-close:hover { background: var(--control-hover); color: var(--ink); transform: rotate(90deg); }
    .search-results { max-height: calc(min(72vh, 680px) - 77px); overflow-y: auto; padding: 0.65rem; }
    .search-result {
      width: 100%; display: grid; grid-template-columns: 92px 1fr; gap: 1rem;
      padding: 0.9rem; border: none; border-radius: var(--radius-md);
      background: transparent; text-align: left; cursor: pointer;
      font-family: var(--font-body);
      transition: background var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast);
    }
    .search-result:hover,
    .search-result:focus-visible {
      background: linear-gradient(100deg,
        color-mix(in srgb, var(--blue-50) 90%, transparent),
        color-mix(in srgb, var(--blue-100) 86%, transparent));
      transform: translateY(-1px);
      box-shadow: var(--shadow-xs);
      outline: none;
    }
    .search-type {
      justify-self: start; align-self: start; padding: 0.29rem 0.64rem; border-radius: 999px;
      background: color-mix(in srgb, var(--blue-500) 13%, transparent);
      color: var(--blue-700);
      font-size: 0.71rem; font-weight: 650; white-space: nowrap;
      letter-spacing: 0.02em;
    }
    .search-result h4 {
      font-family: var(--font-display); font-size: 1.05rem; line-height: 1.28;
      color: var(--ink); font-weight: 600; margin-bottom: 0.28rem;
    }
    .search-result p {
      font-size: 0.84rem; line-height: 1.55; color: var(--ink-muted);
      display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
    }
    .search-empty { padding: 2.5rem 1rem; text-align: center; color: var(--ink-light); font-size: 0.92rem; }

    .search-target { position: relative; border-radius: var(--radius-lg); }
    .search-target::after {
      content: ''; position: absolute; inset: 0;
      border: 3px solid color-mix(in srgb, var(--blue-500) 34%, transparent);
      border-radius: inherit;
      box-shadow: 0 10px 30px -22px color-mix(in srgb, var(--blue-500) 60%, transparent);
      pointer-events: none; z-index: 4;
    }
    .portfolio-card.search-target::after { border-radius: var(--radius-lg); }
    .travel-item.search-target::after,
    .inspo-item.search-target::after {
      content: ''; inset: auto 0 0; height: 1px;
      background: var(--line); border: 0; border-radius: 0; box-shadow: none; z-index: 1;
    }

    /* ── 表单 ── */
    .form-group { margin-bottom: 1.25rem; }
    .form-label {
      display: block; font-size: 0.82rem; font-weight: 520;
      color: var(--ink-muted); margin-bottom: 0.45rem; letter-spacing: 0.012em;
    }
    .form-label .required { color: var(--blue-600); margin-left: 2px; }
    .form-input, .form-textarea, .form-select {
      width: 100%; padding: 0.72rem 0.92rem;
      font-family: var(--font-body); font-size: 0.92rem; color: var(--ink);
      background: var(--bg-subtle); border: 1px solid var(--line-strong);
      border-radius: var(--radius-md);
      transition: border-color var(--t-mid), box-shadow var(--t-mid), background-color var(--t-mid);
    }
    .form-input, .form-select {
      height: 42px;
      line-height: normal;
    }
    .form-input:hover, .form-textarea:hover, .form-select:hover { border-color: color-mix(in srgb, var(--ink) 20%, transparent); }
    .form-input:focus, .form-textarea:focus, .form-select:focus {
      outline: none;
      border-color: var(--blue-400);
      background: var(--surface);
      box-shadow: var(--ring);
    }
    /* 高度随内容自动增长（autoGrowTextarea），上限之后才出现内部滚动 */
    .form-textarea { resize: vertical; min-height: 112px; max-height: 320px; line-height: 1.6; }
    .form-select {
      cursor: pointer; appearance: none;
      background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%238b93a6' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
      background-repeat: no-repeat; background-position: right 0.9rem center; padding-right: 2.2rem;
    }
    .form-hint { font-size: 0.78rem; color: var(--ink-light); margin-top: 0.38rem; line-height: 1.5; }
    .form-error { display: none; font-size: 0.78rem; color: var(--danger); margin-top: 0.38rem; }
    .form-error.show { display: block; }

    .private-toggle {
      display: flex; align-items: center; justify-content: space-between; gap: 1rem;
      padding: 0.85rem 0.95rem; border: 1px solid var(--line-strong);
      border-radius: var(--radius-md); background: var(--bg-subtle); cursor: pointer;
      color: var(--ink-muted); user-select: none;
      transition: border-color var(--t-mid), background-color var(--t-mid);
    }
    .private-toggle:hover { border-color: color-mix(in srgb, var(--blue-500) 26%, transparent); background: var(--surface); }
    .private-toggle input { position: absolute; opacity: 0; pointer-events: none; }
    .private-copy { display: grid; gap: 0.14rem; font-size: 0.86rem; }
    .private-copy strong { color: var(--ink); font-weight: 560; }
    .private-copy span { color: var(--ink-light); font-size: 0.76rem; line-height: 1.45; }
    .private-switch {
      width: 42px; height: 24px; border-radius: 999px; padding: 2px;
      background: var(--line-strong); transition: background var(--t-mid); flex: 0 0 auto;
    }
    .private-switch::before {
      content: ''; display: block; width: 20px; height: 20px; border-radius: 50%;
      background: var(--surface);
      box-shadow: 0 3px 10px -5px rgba(20,23,31,0.6);
      transition: transform 0.24s var(--ease-spring);
    }
    /* 开关打开 = 深蓝 */
    .private-toggle input:checked + .private-switch {
      background: linear-gradient(100deg, var(--blue-600), var(--blue-500));
    }
    .private-toggle input:checked + .private-switch::before { transform: translateX(18px); }
    /* checkbox 本体是透明的，键盘 Tab 到这里原先完全看不出焦点 */
    .private-toggle:has(input:focus-visible) .private-switch { box-shadow: var(--ring); }

    /* 发布设置：两个开关合成一张卡片、中间一条分隔线（原先是两张独立大卡片，抢占首屏） */
    .settings-group {
      border: 1px solid var(--line-strong); border-radius: var(--radius-md);
      background: var(--bg-subtle); overflow: hidden;
    }
    .settings-group .private-toggle { border: 0; border-radius: 0; background: transparent; }
    .settings-group .private-toggle + .private-toggle { border-top: 1px solid var(--line); }
    .settings-group .private-toggle:hover { background: color-mix(in srgb, var(--surface) 70%, transparent); }

    /* 标签里跟在字段名后面的说明（「留空 = 私有仓库」「不填 = 今天」）。
       原先写成输入框下方的 .form-hint，两栏布局里只有左列多一行，
       下一行就会被顶下去、右列下方留个缺口 —— 收进标签行后两栏天然等高。
       2026-10-17：同上，把「图标」的 emoji 快选按钮一并撤掉，
       理由见本文件的 MEMORY 式注释：一排彩色 emoji 与站点克制的排版风格不搭，
       而且只是把「手打一个字符」变成「点一下」，收益远小于视觉噪音。 */
    .form-label .label-note {
      font-weight: 400; font-size: 0.74rem; letter-spacing: 0;
      color: var(--ink-light); margin-left: 0.4rem;
    }

    .photo-uploader {
      display: grid; grid-template-columns: 128px 1fr; gap: 1rem; align-items: center;
      padding: 0.85rem; border: 1px solid var(--line-strong);
      border-radius: var(--radius-md); background: var(--bg-subtle);
      transition: border-color var(--t-mid), background-color var(--t-mid);
    }
    .photo-uploader.is-dragover {
      border-style: dashed; border-color: var(--blue-400);
      background: color-mix(in srgb, var(--blue-50) 80%, transparent);
    }
    .photo-preview {
      width: 100%; aspect-ratio: 4/3; border-radius: var(--radius-sm);
      background: linear-gradient(140deg, var(--blue-100), var(--blue-50));
      border: 1px dashed color-mix(in srgb, var(--blue-500) 34%, transparent);
      display: flex; align-items: center; justify-content: center;
      overflow: hidden; color: var(--blue-500); font-size: 0.78rem; font-weight: 520;
      position: relative;
    }
    .photo-preview { cursor: pointer; text-align: center; padding: 0.5rem; line-height: 1.45; }
    .photo-preview:focus-visible { outline: none; box-shadow: var(--ring); }
    .photo-preview.has-photos { display: block; padding: 0; border-style: solid; }
    .photo-preview img,
    .photo-preview video { width: 100%; height: 100%; object-fit: cover; display: block; }
    .photo-index {
      position: absolute; right: 0.45rem; bottom: 0.45rem;
      padding: 0.18rem 0.46rem; border-radius: 999px;
      background: rgba(20,23,31,0.74); color: #fff;
      font-size: 0.72rem; font-weight: 600;
    }
    .photo-preview-hint {
      position: absolute; left: 0.45rem; bottom: 0.45rem;
      padding: 0.18rem 0.46rem; border-radius: 999px;
      background: color-mix(in srgb, var(--surface) 80%, transparent); color: var(--ink-muted);
      font-size: 0.68rem; font-weight: 520;
    }
    .photo-actions { position: relative; display: flex; flex-direction: column; gap: 0.55rem; min-width: 0; }
    /* 原生 file input 只保留可访问性（键盘可聚焦），视觉上隐藏 ——
       它自带的「未选择任何文件」在编辑已有条目时是错的（明明有封面）。
       真正的按钮是 <label for="photoInput" class="photo-pick">。 */
    .photo-input {
      position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
      overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
    }
    .photo-buttons { display: flex; flex-wrap: wrap; gap: 0.5rem; }

    /* 「选择文件」与「清空」共用一套几何，下面两条各自只给配色：主操作走蓝、次操作走中性灰 */
    .photo-pick,
    .photo-remove {
      display: inline-flex; align-items: center;
      min-height: 38px; padding: 0 0.95rem;
      border: 1px solid transparent; border-radius: 999px;
      font-family: var(--font-body); font-size: 0.86rem; font-weight: 520;
      cursor: pointer;
      transition: background-color var(--t-mid), border-color var(--t-mid), color var(--t-mid), box-shadow var(--t-mid);
    }
    .photo-pick {
      border-color: color-mix(in srgb, var(--blue-500) 30%, transparent);
      background: var(--surface); color: var(--blue-600);
    }
    .photo-pick:hover {
      background: var(--blue-50); border-color: color-mix(in srgb, var(--blue-500) 44%, transparent);
    }
    .photo-input:focus-visible + .photo-buttons .photo-pick { box-shadow: var(--ring); }
    .photo-remove {
      border-color: var(--line-strong);
      background: transparent; color: var(--ink-muted);
    }
    .photo-remove:hover { border-color: color-mix(in srgb, var(--danger) 32%, transparent); color: var(--danger); background: color-mix(in srgb, var(--danger) 6%, transparent); }
    .photo-remove[hidden] { display: none; }

    /* 2026-10-12：原来的「演示视频」独立一格（地址输入 + 选择文件 + 移除视频）
       已并入上面的照片上传器 —— 作品只允许「1 张图 或 1 个视频」二选一，
       所以 .video-uploader / .video-picker / .video-input 三条规则一并删除。 */

    .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }

    .modal-actions {
      padding: 1rem 2.25rem 1.4rem 2.25rem;
      display: flex; gap: 0.85rem; justify-content: flex-end; align-items: center;
      background: var(--surface);
      border-top: 1px solid var(--line);
      /* 阴影从 0.04 提到 0.10（2026-10-25）：0.04 在白底上等于看不见，
         页脚于是既没有底面也没有上沿，只有一条 1px 的线 —— 白对白，容易被当成"空白块" */
      box-shadow: 0 -6px 18px -10px rgba(20,40,70,0.10);
      flex-shrink: 0;
      z-index: 2;
    }
    .btn-cancel {
      padding: 0.72rem 1.5rem; font-family: var(--font-body); font-size: 0.9rem;
      font-weight: 460; border-radius: 100px; border: 1px solid var(--line-strong);
      background: transparent; color: var(--ink-muted); cursor: pointer;
      overflow: hidden; outline: none; -webkit-tap-highlight-color: transparent;
      transition: border-color var(--t-mid), color var(--t-mid), background-color var(--t-mid);
    }
    .btn-cancel:hover { border-color: color-mix(in srgb, var(--ink) 22%, transparent); color: var(--ink); background: var(--bg-subtle); }
    .btn-cancel:focus, .btn-cancel:focus-visible { outline: none; box-shadow: var(--ring); }
    .btn-submit {
      padding: 0.72rem 1.8rem; font-family: var(--font-body); font-size: 0.9rem;
      font-weight: 520; border-radius: 100px;
      /* 透明 1px 边框不是为了画线：`.btn-cancel` 有 1px 描边，box-sizing 是 border-box，
         它靠「内容 + padding + border」自然高 45px；主按钮若写 border:none 就只有 43px，
         两个并排按钮会差 2px（截图里像 取消 比 保存 大一号）。补一条透明边把高度拉齐。 */
      border: 1px solid transparent;
      overflow: hidden; outline: none; -webkit-tap-highlight-color: transparent;
      /* ㉚ 与主按钮同一条令牌（原来单独写 100deg，角度与主按钮不一致，一并拉齐） */
      background: var(--grad-solid);
      color: #fff; cursor: pointer;
      box-shadow: none;
      transition: box-shadow var(--t-mid), transform var(--t-mid), filter var(--t-mid);
    }
    .btn-submit:hover {
      transform: translateY(-1.5px);
      box-shadow: none;
      filter: saturate(1.08);
    }
    .btn-submit:active { transform: translateY(0) scale(0.985); }
    .btn-submit:focus-visible { outline: none; box-shadow: var(--ring); }

    /* ── 空状态 ── */
    .empty-msg { text-align: center; padding: 3rem 0; color: var(--ink-light); font-size: 0.92rem; font-weight: 380; }

    /* ── 滚动揭示 ── */
    /* 🔴 去掉常驻 will-change（2026-09-24 二轮，iOS「放大就崩」）。
       `.reveal` 是**全站批量用**的类：首屏之外的每一条在进入视口前都算「锁定」，
       wk_layer_all_pages.mjs 实测每页 23~24 条，且它们多是整张作品卡（366×522）
       ⇒ 精选作品页 32.8 MB、旅途页 22.7 MB 的常驻合成层；放大 3× 分别是
       295 MB / 204 MB。而入场的淡入+上移只要 `transition` 就够 ——
       WebKit 在过渡期间会自己提升，过渡一结束自动降回。 */
    /* 锁定态只在 JS 存在时成立：.visible 由主脚本的 IntersectionObserver 加，
       没有 JS 就没有观察者，全站每一条 .reveal 会永远停在 opacity:0。
       ⚠️ 必须写成 `:where(html.js)` 而不是 `html.js` —— `:where()` 不吃权重，
       这两条的权重才和原来一字一样是 0,1,0 / 0,2,0。当初加了个 `html` 上去，
       `.reveal.visible` 变成 0,2,1，就把 `.portfolio-card:hover`（0,2,0，见下方
       ⑬ 那段特意标注过权重的注释）压掉了：`--rx/--ry` 照写，transform 却恒等于
       translateY(0) 的单位矩阵，作品卡的鼠标倾角整片失效。 */
    :where(html.js) .reveal {
      opacity: 0; transform: translateY(26px);
      transition: opacity 0.55s var(--ease-out), transform 0.55s var(--ease-out);
    }
    :where(html.js) .reveal.visible { opacity: 1; transform: translateY(0); will-change: auto; }

    /* ── 轻提示 ── */
    .toast {
      position: fixed; bottom: 2rem; left: 50%;
      transform: translateX(-50%) translateY(80px);
      background: color-mix(in srgb, var(--ink) 94%, transparent);
      color: var(--bg);
      padding: 0.78rem 1.55rem; border-radius: 100px;
      font-size: 0.85rem; font-weight: 460;
      z-index: 2000; opacity: 0; pointer-events: none;
      transition: opacity 0.35s var(--ease-spring), transform 0.35s var(--ease-spring);
      box-shadow: var(--shadow-lg);
      contain: layout style;   /* will-change 挪到 .show（2026-09-24 二轮）——见下条 */
      border: 1px solid color-mix(in srgb, var(--blue-400) 24%, transparent);
      backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
    }
    /* will-change 只在真的弹出来时才挂（2026-09-24 二轮）：它是 position:fixed +
       backdrop-filter 的小层（52×27，0.05 MB），量级不大，但属同一病根。 */
    .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); will-change: transform, opacity; }

    /* ── 微交互：主题切换波纹扩散 ── */
    .theme-ripple {
      position: fixed; z-index: 9998; border-radius: 50%;
      pointer-events: none;
      transform: translate(-50%, -50%) scale(0);
      animation: theme-ripple 0.78s var(--ease-out) forwards;
      will-change: transform, opacity;
    }
    @keyframes theme-ripple {
      0%   { transform: translate(-50%, -50%) scale(0); opacity: 1; }
      58%  { opacity: 1; }
      100% { transform: translate(-50%, -50%) scale(1); opacity: 0; }
    }
    @media (prefers-reduced-motion: reduce) { .theme-ripple { display: none; } }

