/* ===========================================================
   Photographer portfolio — global stylesheet
   Design tokens live in :root below. This is a strictly
   black / white / grayscale palette by design — do not add
   hue-carrying colors here (see the project README).
   =========================================================== */

:root {
  /* Grayscale palette only — no hue */
  --bg: #ffffff;
  --bg-alt: #f4f4f3;
  --bg-elevated: #ececeb;
  --ink: #111111;
  --ink-muted: #6b6b68;
  --ink-soft: #9b9b97;
  --line: rgba(17, 17, 17, 0.12);
  --line-strong: rgba(17, 17, 17, 0.4);
  --invert-bg: #0c0c0c;
  --invert-ink: #f5f5f4;
  --invert-line: rgba(245, 245, 244, 0.18);

  /* Single sans-serif family, used for everything */
  --font: "Inter", "Helvetica Neue", "PingFang SC", "Microsoft YaHei",
    -apple-system, BlinkMacSystemFont, Arial, sans-serif;

  --container-width: 1280px;
  --nav-height: 80px;
}

* {
  box-sizing: border-box;
}

/* Defensive: some elements we show/hide via the HTML `hidden` attribute
   (e.g. the Elbe map project's mode toggle) also carry a class that sets
   `display: grid/flex`, which — because it comes from an author
   stylesheet — otherwise wins over the browser's built-in `[hidden]`
   rule and silently keeps the element visible. This keeps `hidden`
   authoritative everywhere on the site. */
[hidden] {
  display: none !important;
}

/* WebP：瀑布流卡片、全屏大图、胶片缩略图这些地方新加了 <picture><source>
   包一层来配合 <img>（详见 js/main.js 里 webpUrl() 上方的注释）。
   <picture> 默认是个 inline 元素，会在 DOM 里插进一层自己的盒子——下面
   一大批依赖 flex/grid 对 <img> 本身做尺寸控制的规则（.lightbox-main img
   width/height:100%、.showcase-item 等瀑布流卡片里 <img> 撑满卡片……）
   都是靠 <img> 是"容器的直接可见子元素"这个前提生效的，多出来的
   <picture> 盒子会打乱这些布局。display: contents 让 <picture> 在渲染时
   "消失"（不产生自己的盒子），<source>/<img> 表现得就像直接摆在原来的
   父元素下一样，现有那些 CSS 规则不用为 <picture> 改一行。 */
picture {
  display: contents;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

img {
  max-width: 100%;
  display: block;
}

a {
  color: inherit;
  text-decoration: none;
}

ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

h1, h2, h3, h4 {
  font-family: var(--font);
  font-weight: 500;
  letter-spacing: -0.01em;
  margin: 0 0 0.5em;
}

p {
  margin: 0 0 1em;
  color: var(--ink-muted);
}

.container {
  max-width: var(--container-width);
  margin: 0 auto;
  padding: 0 32px;
}

.eyebrow {
  display: inline-block;
  font-size: 11px;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 16px;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 14px 32px;
  border: 1px solid var(--ink);
  border-radius: 0;
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink);
  transition: all 0.25s ease;
  cursor: pointer;
  background: transparent;
  font-family: var(--font);
}

.btn:hover {
  background: var(--ink);
  color: var(--bg);
}

.btn-solid {
  background: var(--ink);
  color: var(--bg);
}

.btn-solid:hover {
  background: transparent;
  color: var(--ink);
}

/* ---------- Header / nav ---------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  height: var(--nav-height);
  display: flex;
  align-items: center;
  background: rgba(255, 255, 255, 0.92);
  backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--line);
}

.site-header .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
}

.logo {
  /* 原来占位文字 "Your Name." 时期用的就是这个 15px，换成真名后
     试过调到 18px，后来用户反馈导航栏（见下面）配的 15px 太大，
     两边都退回各自原来的大小：名字还是 15px，导航栏改小到 12px。 */
  font-size: 15px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  font-weight: 600;
}

.logo span {
  color: var(--ink-soft);
}

.nav-links {
  display: flex;
  gap: 44px;
}

.nav-links a {
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-muted);
  position: relative;
  padding-bottom: 4px;
  transition: color 0.2s ease;
}

/* 桌面端导航栏（Home / Selected Pictures / ...）字号调大——应用户
   要求先调到跟 logo 一样大（15px），后来反馈太大了，改成 12px。
   只在桌面宽度生效，手机端下拉菜单（.nav-links.open，见下面 680px
   那条媒体查询）保持原来 11px 不变，不受这次调整影响。用 min-width
   卡在跟手机菜单相反的那个断点上，两条媒体查询刚好衔接，不会有中间
   地带两边规则都不生效/都生效的问题。 */
@media (min-width: 681px) {
  .nav-links a {
    font-size: 12px;
  }
}

.nav-links a:hover,
.nav-links a.active {
  color: var(--ink);
}

.nav-links a.active::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -8px;
  height: 1px;
  background: var(--ink);
}

.nav-toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
}

.nav-toggle span {
  width: 22px;
  height: 1px;
  background: var(--ink);
}

/* ---------- Hero (homepage) ----------
   背景图不再写在 index.html 的 style="" 里了，挪到这里用 CSS 控制——
   因为现在有两张不同的拼图（宽屏格子 12x6 用于桌面，竖屏格子 6x12
   用于手机），要靠媒体查询在两张之间切换，写在 HTML 内联样式里没法
   被媒体查询覆盖，所以移到这儿。想换封面图，改下面这两处文件名
   即可，不用碰 index.html。

   英雄区的高度不再是固定的 92vh，改成用 aspect-ratio 跟背景图的
   比例直接绑死（桌面 2:1、手机 1:2）——这样英雄区的形状永远和
   背景图完全一致，图片用 background-size: cover 铺上去正好严丝
   合缝，不会再出现"图片比例跟容器比例对不上，只能二选一：要么
   裁边、要么露出底色"的问题（之前几版分别试过 cover 会裁边、
   100% auto/auto 100% 会在比例差得远的窗口露出一截底色，本质
   原因都是容器的高度是单独定的 92vh、跟图片比例没关系）。副作用
   是窗口越"高瘦"（比如把桌面浏览器拖窄），算出来的英雄区就会
   跟着变矮——这是应用户要求特意这样设计的："把这张背景图和英雄区
   的大小进行绑定，如果窗口高瘦那就英雄区也变小一点，没有上下
   自适应的黑边"。

   竖排（手机）那张图只在真正的触屏设备上才会切换，用的是
   (pointer: coarse) 这个媒体特征（触屏/手指操作是 coarse，鼠标是
   fine），而不是单纯按窗口宽度 768px 来切——单纯按宽度切的话，
   电脑上把浏览器窗口拖窄到 768px 以下也会被误判成"手机"，切换成
   竖排图，用户明确反馈不想要这样："电脑端永远保持横屏模式，不
   切换成竖屏"。改成 (pointer: coarse) 之后，电脑（鼠标操作，
   pointer:fine）不管窗口多窄都只会显示横排的桌面图（就算变矮，
   形状也还是横的 2:1），只有触屏手机/平板才会换成竖排图。 */
.hero {
  position: relative;
  aspect-ratio: 2 / 1;
  display: flex;
  align-items: flex-end;
  background-image: url("../images/misc/hero-mosaic.jpg");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  overflow: hidden;
  background-color: var(--invert-bg);
}

@media (max-width: 768px) and (pointer: coarse) {
  .hero {
    aspect-ratio: 1 / 2;
    background-image: url("../images/misc/hero-mosaic-mobile.jpg");
  }
}

.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.05) 0%,
    rgba(0, 0, 0, 0.25) 55%,
    rgba(0, 0, 0, 0.88) 100%
  );
}

.hero-content {
  position: relative;
  z-index: 2;
  padding-bottom: 80px;
  width: 100%;
  color: var(--invert-ink);
}

.hero h1 {
  font-size: clamp(38px, 6.4vw, 80px);
  line-height: 1.08;
  margin-bottom: 18px;
  font-weight: 400;
  letter-spacing: -0.01em;
  color: var(--invert-ink);
  /* 背景换成拼图（images/misc/hero-mosaic.jpg）之后，画面里到处都是
     局部高对比的小图（比如亮墙面、天空），下面 .hero::after 那层
     底部渐变已经把文字这一片压暗了大半，但拼图本身花纹比单张照片
     碎，只靠那层渐变有些地方还是不够——加一层文字阴影，让白色文字
     不管压在拼图哪个位置上都稳定可读。两层阴影：外面一层大范围、
     低透明度的模糊阴影负责整体"托底"，里面一层小范围、稍深的阴影
     负责把字母边缘勾清楚。 */
  text-shadow: 0 4px 28px rgba(0, 0, 0, 0.55), 0 1px 4px rgba(0, 0, 0, 0.5);
}

.hero .eyebrow {
  color: rgba(245, 245, 244, 0.65);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55);
}

.hero p.lead {
  max-width: 520px;
  font-size: 16px;
  color: rgba(245, 245, 244, 0.8);
  text-shadow: 0 2px 14px rgba(0, 0, 0, 0.55), 0 1px 3px rgba(0, 0, 0, 0.45);
}

.hero-actions {
  display: flex;
  gap: 16px;
  margin-top: 32px;
}

.hero .btn {
  border-color: rgba(245, 245, 244, 0.6);
  color: var(--invert-ink);
}

.hero .btn:hover {
  background: var(--invert-ink);
  color: var(--invert-bg);
}

.hero .btn-solid {
  background: var(--invert-ink);
  color: var(--invert-bg);
  border-color: var(--invert-ink);
}

.hero .btn-solid:hover {
  background: transparent;
  color: var(--invert-ink);
}

/* ---------- Sections ---------- */
section {
  padding: 120px 0;
}

section.tight {
  padding: 76px 0;
}

.section-head {
  max-width: 640px;
  margin-bottom: 56px;
}

.section-head.center {
  margin-left: auto;
  margin-right: auto;
  text-align: center;
}

/* ---------- Generic grid utility (used by About "how it works" steps) ---------- */
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}

/* ---------- Homepage showcase: two-column masonry, one random photo
   per series, each linking to that series' page. Photos keep their
   original aspect ratio (no cropping/zooming to fill a fixed frame) —
   they're distributed alternately into the two columns in js/main.js,
   so a mix of landscape and portrait shots naturally creates an
   uneven, staggered layout like stevemccurry.com/artistic-expression.

   每张图左下角叠了一张系列标题（.showcase-series-title，js/main.js
   里 renderItem 渲染），用 .showcase-frame 把 <img> 和这个标题包在
   一起、单独开一个 position: relative 的定位上下文——不能直接把
   position: relative 加在 .showcase-item（外层 <a>）上，因为
   .showcase-item 底下除了图还有 .showcase-caption（图片下方那行
   照片说明小字），如果定位上下文是整个 <a>，标题会跑到"图+说明文字"
   整体的左下角，而不是图片本身的左下角。.showcase-frame 加了
   overflow: hidden，把下面 hover 放大时图片和标题溢出的部分裁掉，
   放大效果只在这张照片自己的框里发生，不会盖到旁边的图或下面一行。

   鼠标悬停时标题的放大比图片更明显（图片 1.015、标题 1.5），而且
   transform-origin 设成 left bottom——标题这个 div 本身是绝对定位、
   宽度自动收缩贴合文字（shrink-to-fit），所以它自己的左下角就是
   文字视觉上的左下角，放大是以这个点为原点，文字离图片左边、下边
   的距离（这两个值都是靠 left/bottom 定位到 .showcase-frame 上的，
   不受 transform 影响）就永远不变，只会朝右上方变大，应用户要求
   "保证字距离图片左边和下边的距离不变，朝右上方进行放大"。字号
   （22px）跟 Selected Pictures 入口页系列卡片标题（.series-card h3）
   用的是同一个数值，应用户要求两处字号保持一致——具体数值改的话
   两边分开改就行，不是共用同一条规则，只是数值恰好设成一样。 ---------- */
.showcase-wide {
  max-width: 2200px;
  margin: 0 auto;
  padding: 0 clamp(20px, 4vw, 64px);
}

.showcase-grid {
  display: flex;
  gap: clamp(14px, 2vw, 28px);
}

.showcase-col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: 48px;
  min-width: 0;
}

.showcase-item {
  display: block;
}

.showcase-frame {
  position: relative;
  overflow: hidden;
}

.showcase-frame img {
  width: 100%;
  height: auto;
  display: block;
  background: var(--bg-alt);
  transition: transform 0.7s ease;
}

.showcase-item:hover .showcase-frame img {
  transform: scale(1.015);
}

.showcase-series-title {
  position: absolute;
  left: 20px;
  bottom: 16px;
  max-width: calc(100% - 40px);
  color: #fff;
  font-size: 22px;
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1.3;
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), 0 1px 3px rgba(0, 0, 0, 0.55);
  transform-origin: left bottom;
  transition: transform 0.7s ease;
}

.showcase-item:hover .showcase-series-title {
  transform: scale(1.5);
}

.showcase-caption {
  margin-top: 14px;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.showcase-item:hover .showcase-caption {
  color: var(--ink);
}

/* ---------- 易北河 Gallery View 主题详情页：两栏瀑布流，不裁剪图片、
   保持每张图原始比例（参考 stevemccurry.com/conflict 的排布方式）。
   之前试过两种都有问题的做法：(1) 按 i%2 简单交替分配两栏——两栏
   谁高谁矮全靠运气，遇到连续几张竖图挤在同一栏，两栏就会明显不
   同步，看着像顺序乱了；(2) 改成真正的 CSS 网格、按行对齐——同一行
   如果两张图原始比例差很多，矮的那张下面就会空出一大块，间距忽大
   忽小。现在用的是"贪心最短栏"算法（js/elbe.js 里的
   layoutElbeMasonryColumns()）：图片按顺序依次放入，每次都放进当前
   累计高度更小的那一栏，这样两栏高度会自然保持接近、不会一边越堆
   越高，栏内每张图之间的间距是固定的 gap，不会因为图片高矮不同就
   跟着变。这个算法需要预先知道每张图的宽高比（elbe-data.js 里每张
   照片的 ratio 字段），所以是提前算好放哪一栏，不是等图片加载完再
   调整位置，不会有布局跳动。 ---------- */
.elbe-theme-grid {
  display: flex;
  gap: clamp(14px, 2vw, 28px);
}

.elbe-theme-col {
  flex: 1 1 0;
  display: flex;
  flex-direction: column;
  gap: clamp(14px, 2vw, 28px);
  min-width: 0;
}

@media (max-width: 680px) {
  .elbe-theme-grid {
    flex-direction: column;
    gap: 32px;
  }
  .elbe-theme-col {
    gap: 32px;
  }
}

/* ---------- Selected Pictures index: series entry cards ---------- */
.series-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 40px 24px;
}

.series-card {
  display: block;
}

.series-card-frame {
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--bg-alt);
  margin-bottom: 20px;
}

.series-card-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s ease;
}

.series-card:hover .series-card-frame img {
  transform: scale(1.04);
}

.series-card h3 {
  font-size: 22px;
  font-weight: 400;
  margin-bottom: 8px;
}

/* 易北河 Gallery View 的主题选择网格（#elbe-theme-grid）不显示"N
   photographs in this theme"这行小字（elbe.js 里就没渲染这个 <p>了），
   这里把标题下面留给它的间距也去掉，不然标题下面会空出一截没用的
   留白。只用 ID 精确限定在这个网格里，不影响 Selected Pictures 页面
   那边的系列卡片（还在用同一个 .series-card h3 规则）。 */
#elbe-theme-grid .series-card h3 {
  margin-bottom: 0;
}

/* ---------- Series detail page: intro + inline large-image viewer ---------- */
.series-intro {
  max-width: 720px;
  margin-bottom: 56px;
}

.inline-viewer-main {
  width: 100%;
  max-height: 78vh;
  min-height: 320px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg-alt);
  cursor: zoom-in;
  overflow: hidden;
}

.inline-viewer-main img {
  max-width: 100%;
  max-height: 78vh;
  object-fit: contain;
}

.inline-viewer-caption {
  margin: 18px 0 24px;
  font-size: 12px;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  text-align: center;
}

/* Filmstrip used inline (on a white page) needs a dark-on-light active
   state instead of the modal's white-on-black one. */
.inline-viewer .filmstrip {
  justify-content: center;
  flex-wrap: wrap;
  overflow-x: visible;
  padding: 4px 0;
}

.inline-viewer .filmstrip-item.active {
  outline-color: var(--ink);
}

/* ---------- About split ---------- */
.about-split {
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  gap: 72px;
  align-items: center;
}

.about-split img {
  width: 100%;
  aspect-ratio: 4/5;
  object-fit: cover;
}

.stat-row {
  display: flex;
  gap: 48px;
  margin-top: 36px;
}

.stat-row .num {
  font-size: 32px;
  font-weight: 300;
  color: var(--ink);
}

.stat-row .label {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-top: 4px;
}

.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 24px;
}

.tag-list span {
  border: 1px solid var(--line-strong);
  padding: 8px 16px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

/* ---------- Video Project cards (list page) ---------- */
/* 封面框比例原来是 16:10，跟视频本身的 16:9 不一致，封面图会被
   object-fit:cover 稍微裁掉一点上下——改成 16:9，跟 .video-embed
   （详情页播放器，见下面）保持一致，封面图基本就是完整显示，不会跟
   视频画面的实际取景范围对不上。 */
.video-card-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--bg-alt);
  margin-bottom: 22px;
}

.video-card-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 0.6s ease;
}

.video-card:hover .video-card-frame img {
  transform: scale(1.04);
}

/* 原来是居中放的（top/left 50% + translate 回正），改成贴左下角——
   有几张视频封面图本身中间就带字幕/标题文字，播放键居中放会跟这些字
   叠在一起互相挡住。这里只改了这条基础规则，不会影响下面
   .video-embed-placeholder .play-icon 那条（它已经单独把 position 转成
   static、不再依赖 top/left/transform 定位，那是详情页"还没填视频链接"
   时的占位提示图标，跟封面缩略图是两个不同的使用场景，各自独立，改这边
   不会影响那边）。 */
.play-icon {
  position: absolute;
  bottom: 20px;
  left: 20px;
  width: 56px;
  height: 56px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
}

.play-icon::after {
  content: "";
  border-style: solid;
  border-width: 9px 0 9px 14px;
  border-color: transparent transparent transparent #ffffff;
  margin-left: 3px;
}

/* ---------- Video embed (detail page) ---------- */
.video-embed {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  margin: 8px 0 48px;
  background: var(--bg-alt);
}

.video-embed iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.video-embed-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  border: 1px dashed var(--line-strong);
  color: var(--ink-muted);
  text-align: center;
  padding: 32px;
}

.video-embed-placeholder .play-icon {
  position: static;
  transform: none;
  border-color: var(--line-strong);
}

.video-embed-placeholder .play-icon::after {
  border-color: transparent transparent transparent var(--ink-muted);
}

.video-embed-placeholder p {
  max-width: 380px;
  font-size: 13px;
}

/* ---------- Lightbox / large image viewer with filmstrip ---------- */
/* 这里的水平内边距特意不放在 .lightbox 本身上，而是分别放到
   .lightbox-main（大图）和 .lightbox-footer 内部的说明文字上——这样
   .lightbox-footer 这一整行可以铺满到屏幕最左右两端，左右切换箭头才能
   真正贴在"屏幕两端"，而不是被 .lightbox 自己的内边距往里挤。 */
.lightbox {
  position: fixed;
  inset: 0;
  background: rgba(8, 8, 8, 0.97);
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  z-index: 1000;
  padding: 32px 0 24px;
}

.lightbox.open {
  display: flex;
}

.lightbox-main {
  position: relative;
  width: 100%;
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  padding: 0 32px;
  box-sizing: border-box;
  /* 允许浏览器自己处理纵向手势（比如极少数情况下的下拉刷新），但横向
     的手指拖动交给 main.js 里的左右滑动切图逻辑自己处理，不让浏览器把
     横向拖动当成别的手势（比如 Safari 有的边缘滑动返回上一页）抢先
     接管掉。 */
  touch-action: pan-y;
  /* 裁剪"上一张/下一张"露出来的部分这件事，特意不写在这一层（以前是
     写在这儿的，overflow:hidden——下面 .lightbox-viewport 那条规则有
     踩过的坑和详细说明，这里不再重复）。 */
}

/* .lightbox-track 里实际是"上一张/当前/下一张"三张幻灯片横向排成一排
   （见下面 .lightbox-track 规则），平时只露出中间那张，左右两张要露在
   可视范围之外、被裁掉，拖动切换时才会看到它们从两侧滑进来——负责这个
   裁剪的就是这一层。
   之所以专门为了裁剪多包一层、不是直接把 overflow:hidden 写在
   .lightbox-main 自己身上：.lightbox-main 左右各有 32px 的内边距（手机
   端 10px，见媒体查询里的覆盖规则），而 overflow:hidden 的裁剪边界是
   元素自己的 padding box（含内边距那一整块），不是内容区——也就是说
   直接写在 .lightbox-main 上时，裁剪边界比 .lightbox-track/.lightbox-
   slide 实际占据的内容区（宽度是内边距刨除掉之后那一份）还要再往左右
   各宽出 32px，多出来的这一圈"内边距地带"并不会被裁掉。平时这一圈里
   空空荡荡看不出问题，但"上一张/下一张"停靠的位置只是紧挨着当前这张的
   右/左边缘（隔一个 .lightbox-track 的 gap，24px，见下面注释），24px
   比这多出来的 32px 小，算下来正好会有 8px 宽的一条缝隙落在"没被裁掉"
   的这圈内边距地带里——大多数照片这 8px 缝隙对应的是相邻那张照片自己
   的留白/暗部，不容易发现，但如果相邻那张恰好是那种长宽比很宽的照片
   （比如 21:9 全景，物理内容几乎铺满自己那一格的整个宽度、没有留白），
   这 8px 就会变成一条看得出来的、属于相邻照片的边缘，diagonally 穿帮到
   当前这张照片的旁边——这正是之前报过的那个 bug。
   现在把 overflow:hidden 移到这个专门的 .lightbox-viewport 上，让它
   自己不带任何内边距（宽高就是 100%，紧贴 .lightbox-main 的内容区），
   裁剪边界因此正好卡在 .lightbox-track/.lightbox-slide 实际占据的这块
   区域上，跟"上一张/下一张"停靠的位置分毫不差，不会再露出来——不管
   gap 和内边距具体数值是多少，结构上就保证了两者不可能有缝隙，不用
   再靠凑数字对齐。 */
.lightbox-viewport {
  width: 100%;
  height: 100%;
  overflow: hidden;
}

/* .lightbox-track 横向排着三张幻灯片（见 main.js ensureLightboxMarkup
   里的 DOM 结构，以及 updateLightboxImage 怎么把"上一张/当前/下一张"
   三份内容分别塞进这三个格子），每张都跟 .lightbox-main 的可视区域
   一样宽——手指拖动时 main.js 直接设置这个元素的 transform:translateX()
   跟着手指 1:1 移动，松手后要么用弹簧动画把剩下的距离走完（切到下一/
   上一张），要么弹簧回弹到原位（拖得不够多/不够快，取消这次切换）。
   gap 就是效果图里"两张照片中间那一小段距离"——三张幻灯片本身都是
   撑满整个可视宽度的，中间空出来的这段间隙由 flex 的 gap 属性产生，
   不是靠某张图片自己留白，所以拖到一半时看到的是"上一张的右边缘 +
   一小段黑色间隙 + 下一张的左边缘"，视觉上两张照片并不会直接拼接在
   一起，跟 iPhone 相册的效果一致。 */
.lightbox-track {
  display: flex;
  align-items: center;
  width: 100%;
  height: 100%;
  gap: 24px;
  will-change: transform;
}

.lightbox-slide {
  flex: 0 0 100%;
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
}

/* 三张幻灯片视觉上从左到右的排列顺序（上一张/当前/下一张）由这里的
   order 决定，不依赖 DOM 里写的先后顺序——DOM 里 main.js 特意把"当前"
   这张放在最前面（原因见 main.js ensureLightboxMarkup 里这段 DOM
   结构上方的注释），用 order 把视觉顺序纠正回来，两者互不影响。 */
.lightbox-slide[data-slot="prev"] {
  order: -1;
}

.lightbox-slide[data-slot="current"] {
  order: 0;
}

.lightbox-slide[data-slot="next"] {
  order: 1;
}

.lightbox-slide picture {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* 这里特意用 width/height:100%（而不是只用 max-width/max-height），
   是因为全屏大图现在优先加载的是"展示图"（最长边压缩到 2000px，参见
   elbe-data.js 的 display 字段），在足够大/超宽的显示器上，2000px
   可能反而比可用的显示区域还要小——max-width/max-height 只会在图片
   比容器大时把图片缩小，并不会在图片比容器小时把它放大填满，于是就
   会出现"明明是全屏模式，21:9 这种宽幅照片却没有撑满、周围留一圈空白"
   的问题。width/height:100% 让 <img> 标签本身始终撑满 .lightbox-slide
   这个格子，再交给 object-fit:contain 在这个已经撑满的框里，按原图
   比例把图片内容居中缩放显示——这样无论展示图的原始像素是比容器大
   还是比容器小，都能保证图片始终撑满可用空间（在保持原比例、不裁切
   的前提下）。 */
.lightbox-slide img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* .lightbox-footer 是标题文字和左右切换箭头共用的一行——箭头是这一行
   里 position:absolute 的两个按钮，贴在这一行（也就是整个屏幕）的最
   左/最右边，标题文字居中显示、跟箭头保持距离不会贴在一起（见下面
   .lightbox-caption 的左右内边距）。桌面/平板和手机用的是同一套定位
   逻辑，只是 @media (max-width: 680px) 里把间距和按钮尺寸调小一圈——
   不再需要 JS 根据屏幕宽度把按钮在"悬浮于图片上"和"排在标题这一行"
   之间来回搬动，所以这两个按钮在 DOM 里从一开始就固定放在
   .lightbox-footer 里（见 main.js ensureLightboxMarkup），逻辑更简单，
   以后别的系列复用这套全屏看图组件时也不会有额外的适配成本。 */
/* margin 从加浏览进度这行之前的 18px/20px 改窄了（12px/13px）——加了
   浏览进度之后标题和它下面这行进度数字变成了两行，如果 margin 还是
   保持原来的数值，大图和下面胶片缩略图之间的空隙会明显变宽（这两行
   文字本身要占的高度更高了）。用户明确要求这个空隙保持之前只有一行
   标题时的样子不变，所以这里把 margin 相应收窄了一些，让"变窄的
   margin + 两行更紧凑的文字"加起来，跟"之前的 margin + 一行文字"
   总高度基本一致（实测量过：改之前大图到缩略图的间距约 58px，现在
   也约 58px，几乎没有变化）。如果以后又要调整这两行文字本身的字号/
   行距，记得这里的 margin 也要跟着重新核算一遍，不然空隙又会变。 */
.lightbox-footer {
  position: relative;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 12px 0 13px;
  box-sizing: border-box;
}

/* 标题文字下面现在多了一行"浏览进度"（比如 5/20，见 main.js
   updateLightboxImage() 里怎么算的），两行要作为一个整体在
   .lightbox-footer 这一行里居中——.lightbox-footer 本身是
   justify-content:center 的横向 flex，只包了这一个会参与布局的子元素
   （左右切换箭头是 position:absolute，脱离了横向布局），所以这里用
   flex-direction:column 把标题和进度数字纵向堆起来，整体还是按原来的
   方式居中，标题本身相当于比之前"整体上移"了一些（给下面多出来的进度
   文字腾地方），不需要再额外调整标题自己的定位。
   gap 特意压得很窄（2px）——用户要求"行间距可以窄一点"，这两行本来就是
   同一组信息（标题 + 它的浏览进度），紧挨着显示比较合理，也是能在不
   放大整体空隙的前提下塞下两行字的关键之一。 */
.lightbox-caption-group {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.lightbox-caption {
  text-align: center;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 0 64px;
  box-sizing: border-box;
  /* 全站默认的行距（body 上的 line-height: 1.7）对单独一行短标题来说
     太松——以前只有这一行文字时不明显，现在下面又多了一行浏览进度，
     两行都用这么松的行距会让整体占的高度涨很多，所以这里单独收紧到
     1.3（比默认紧凑不少，但对这么小的字号来说仍然够透气、不会显得
     挤在一起）。 */
  line-height: 1.3;
}

/* 2023 Zhuozhou Flood 这种没有标题的系列（照片本来就不需要标题，见
   js/zhuozhou-flood-data.js 头部注释），.lightbox-caption 每次都是空的
   ——完全没有文字内容的 <div> 本身高度会自动收缩成 0（浏览器不会为空
   元素保留一整行的行高），但上面 .lightbox-caption-group 的 gap 不管
   两侧元素是不是空的都会照样生效，等于平白多出一段空隙。这里用 :empty
   直接把空标题整个隐藏掉（display:none 的元素完全不参与 flex 布局，
   gap 也就不会算在它和下面浏览进度之间），这样"标题下方只剩浏览进度
   一行"的系列，就不会因为一个看不见的空标题多占地方——不是专门针对
   Zhuozhou Flood 写死的规则，以后任何系列只要某张照片的标题是空的，
   都会自动套用同样的效果，不用再回来改。 */
.lightbox-caption:empty {
  display: none;
}

/* 字号/字体/颜色/行距都跟标题（.lightbox-caption）保持一致——用户明确
   要求"字号字体可以和图片标题一样"，这里没有另起一套样式，就是把上面
   那条规则原样抄了一份。以后如果标题样式改了，记得这里也同步改一下。 */
.lightbox-progress {
  text-align: center;
  font-size: 12px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-soft);
  padding: 0 64px;
  box-sizing: border-box;
  line-height: 1.3;
}

.lightbox-close,
.lightbox-prev,
.lightbox-next {
  z-index: 2;
  background: none;
  border: 1px solid rgba(245, 245, 244, 0.35);
  color: var(--invert-ink);
  cursor: pointer;
  border-radius: 999px;
  display: flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  transition: all 0.2s ease;
  flex: 0 0 auto;
}

.lightbox-close:hover,
.lightbox-prev:hover,
.lightbox-next:hover {
  border-color: var(--invert-ink);
}

/* 关闭按钮用固定的 top/right 数值贴在屏幕右上角——不随每张照片的长宽比
   重新计算/移动位置（这是特意要求的：按钮位置本身保持完全静止，不会
   出现"跟着图片跳来跳去"的感觉）。

   这个按钮现在是 .lightbox 自己的直接子元素（不再嵌在 .lightbox-main
   里面）——应用户要求，Documentary Photography 的"滚动揭示图说"模式下
   滚到图说那一屏时，关闭按钮也要一直留在右上角，不能跟着 .lightbox-main
   一起被滚出画面（见 main.js ensureLightboxMarkup 里这个按钮 DOM 位置
   上方的注释）。top/right 数值因此是相对 .lightbox 自身（相当于直接
   相对屏幕）算的，不再是相对 .lightbox-main：.lightbox-main 之前的
   top:48px 是相对它自己算的，而 .lightbox-main 本身因为 .lightbox 有
   32px 的上内边距（手机端 14px），不是紧贴在屏幕最顶上——现在把这段
   内边距的差值加回了下面的 top 值里（48+32=80，手机端 30+14=44），
   right 因为 .lightbox 左右内边距是 0、跟 .lightbox-main 的右边缘本来
   就重合，数值不用变。这样调整之后，按钮在屏幕上的实际视觉位置跟改动
   前完全一样，没有任何变化，只是不再会被纵向滚动带走。

   right 值本来是特意跟左右切换箭头的横坐标对齐的（箭头是
   right: 28px，见下面 .lightbox-prev/.lightbox-next）——但实机看下来
   （电脑和手机都是）按钮贴屏幕边缘太近，视觉上有点局促，先往里挪了
   12px（top: 48px / right: 40px），之后又实机反馈右边缘还是有点紧，
   right 又单独再往里挪了 15px 变成 55px（top 没再动，还是 48px）。
   跟箭头横坐标已经不是像素级对齐了，但视觉上仍然接近，贴边的局促感
   基本消除。

   照片用 object-fit:contain 居中缩放显示，实际可见范围会随长宽比和
   容器比例变化，纯静态的 top/right 数值没法对任意窗口尺寸、任意长宽比
   都同时保证"绝不卡边"——这一点在常见桌面窗口宽高比连续变化时是数学
   上无法完全避免的，之前也确实验证过：在浏览最宽幅的全景照片（长宽比
   接近 2.33，即 Ústí nad Labem / Giant Mountains / Torgau Elbe Bridge /
   Vltava And Elbe 这几张）时，某些窗口比例下按钮可能会有一部分卡在
   照片边缘——具体会不会卡边、卡多少，随窗口宽高比连续变化，right 改成
   55px 之后，作者本人常用的窗口尺寸下实测反而比 40px 时更贴合照片边缘
   （不再明显卡边），但换到另一些窗口比例（比如更窄更矮的笔记本分辨率）
   下看同一批照片，还是可能出现卡边。这是用户在看到实际卡边效果后明确
   选择接受的折中——"跟箭头位置大致接近、贴边不局促"优先于"对任意窗口
   尺寸都绝不卡边"，不需要再为了规避这个情况而改小 right 的值。如果
   以后又有人想要两者都满足，只有两条路：要么恢复按图片动态计算位置
   （见 main.js 里 ensureLightboxMarkup 函数上方的说明，那里记录了
   动态算法本身该怎么写、以及当年踩过的坑），要么用
   @media (min-aspect-ratio) 针对不同窗口比例分别设置不同的 right 值。
   手机端见下方 @media (max-width: 680px) 里的数值——手机屏幕比例种类
   有限，之前的 18px 已经验证过对所有常见机型都不会卡边，这次只是同样
   往里挪了 12px（改成 30px）解决贴边的局促感，不受上面说的折中影响。 */
.lightbox-close {
  position: absolute;
  top: 80px;
  right: 55px;
  width: 44px;
  height: 44px;
  font-size: 18px;
  /* 这个按钮现在跟 .lightbox-scroll 是平级的兄弟元素（见上方注释），
     .lightbox-scroll 本身没有设置 z-index（默认 auto），下面这行连同
     .lightbox-close/.lightbox-prev/.lightbox-next 共用的 z-index:2（见
     本文件靠后 .lightbox-close, .lightbox-prev, .lightbox-next 那条
     规则）已经足够保证它显示在 .lightbox-scroll（不管当前滚动停在图片
     还是图说那一屏）之上，这里不需要再重复设置。 */
}

/* 关闭按钮里的"×"以前直接用字符 &times;，实测在不同字体/浏览器下
   这个字符本身的视觉重心并不在它的排版框正中间（常见字体渲染问题），
   即使外层按钮已经用 flex 居中，看起来还是像叉号偏在圆圈一角。这里
   改成用两条 CSS 画的短线交叉代替字符，两条线都以自身中心为基准
   旋转、再用 top/left 50% + translate(-50%,-50%) 精确摆到圆圈正中间，
   不依赖任何字体，保证在所有浏览器里都是真正的居中。 */
.lightbox-close-icon {
  position: relative;
  width: 14px;
  height: 14px;
  display: block;
}

.lightbox-close-icon::before,
.lightbox-close-icon::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100%;
  height: 1.5px;
  background: currentColor;
}

.lightbox-close-icon::before {
  transform: translate(-50%, -50%) rotate(45deg);
}

.lightbox-close-icon::after {
  transform: translate(-50%, -50%) rotate(-45deg);
}

/* 左右切换箭头：绝对定位在 .lightbox-footer 这一行（等于整个屏幕）的
   左右两端，垂直方向用 top:50%+translateY(-50%) 相对这一行自身居中，
   正好跟标题文字对齐在同一条水平线上——而不是像以前那样悬浮在图片
   正中间，会挡住照片内容。尺寸比关闭按钮小一圈（跟这套全屏组件最早
   的版本一致），边距也留得比屏幕物理边缘宽一些，不会显得整排顶死。 */
.lightbox-prev,
.lightbox-next {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 40px;
  height: 40px;
  font-size: 16px;
}

.lightbox-prev {
  left: 28px;
}

.lightbox-next {
  right: 28px;
}

/* Filmstrip: the row of thumbnails from the same series, shown
   below the large image — click one to swap the main photo. */
.filmstrip {
  display: flex;
  gap: 10px;
  max-width: 100%;
  overflow-x: auto;
  padding: 4px 4px 12px;
  scrollbar-width: thin;
}

.filmstrip::-webkit-scrollbar {
  height: 5px;
}

.filmstrip::-webkit-scrollbar-thumb {
  background: rgba(245, 245, 244, 0.25);
}

.filmstrip-item {
  flex: 0 0 auto;
  width: 84px;
  height: 84px;
  cursor: pointer;
  opacity: 0.45;
  transition: opacity 0.2s ease, outline-color 0.2s ease;
  outline: 1px solid transparent;
  outline-offset: -1px;
}

.filmstrip-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.filmstrip-item:hover {
  opacity: 0.75;
}

.filmstrip-item.active {
  opacity: 1;
  outline: 1px solid var(--invert-ink);
}

/* ============ Documentary Photography 专属："滚动揭示图说" 全屏模式
   ============
   应用户要求："在 Documentary Photography 这个系列的全屏模式中把图片
   作为一个可以滚动的界面，无论是使用电脑滚轮还是手机/平板的触屏，往下
   翻就可以看到图说和图片信息"。只有这一个系列会用到（通过 main.js
   openLightbox() 新增的第四个参数 scrollReveal 开启，只有 documentary.js
   调用 renderMasonryGrid() 时传了 { scrollReveal: true }，见该函数上方
   注释），不影响 Church/Elbe/Street Performer/2023 Zhuozhou Flood/
   Landscape Photography 这五个系列——它们全屏看图的行为完全不变。

   实现思路：新增一层 .lightbox-scroll，包住原有的 .lightbox-main（大图，
   内部横向滑动切图那套逻辑完全不动）和新增的 .lightbox-info-panel（图说
   + 拍摄信息，目前是占位文字，见 main.js updateLightboxImage 里怎么填）。
   普通模式下 .lightbox-scroll 只是一个普通的 flex:1 容器，跟以前
   .lightbox-main 单独占据这块空间时表现完全一样（.lightbox-info-panel
   默认 display:none，不占任何空间、不影响布局）。只有 .lightbox 带上
   .scroll-reveal 这个 class 时，.lightbox-scroll 才会变成一个纵向、强制
   吸附（scroll-snap-type: y mandatory）的滚动容器，.lightbox-main 和
   .lightbox-info-panel 各自占满一屏、彼此吸附对齐。

   不需要任何 JS 去分辨"这次是鼠标滚轮还是触屏划动"——原生的
   overflow-y:auto 滚动容器本身就会同时响应两者（电脑上滚轮事件、手机/
   平板上触屏滑动手势，浏览器都会转换成对这个容器的原生滚动）。这也是
   为什么横向切图那套手势代码完全不用改一行：.lightbox-main 上已经有
   touch-action: pan-y（见该规则上方注释），纵向的触屏手势本来就没有被
   JS 拦截、一直是交给浏览器原生处理的——现在浏览器原生处理的对象从
   "什么都不做"变成了"滚动这个新增的祖先容器"，横向那套 touchstart/
   touchmove/touchend 逻辑（判断 axisLocked、跟手拖拽切图……）完全不受
   影响，两者互不冲突。 */
.lightbox-scroll {
  width: 100%;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.lightbox-info-panel {
  display: none;
}

.lightbox.scroll-reveal .lightbox-scroll {
  overflow-y: auto;
  overflow-x: hidden;
  scroll-snap-type: y mandatory;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  /* 视觉上希望是"翻页"的观感，不是一个带滚动条的普通网页长列表，所以
     隐藏原生滚动条（滚动本身完全不受影响，只是不显示滚动条这个视觉
     元素）。 */
  scrollbar-width: none;
}

.lightbox.scroll-reveal .lightbox-scroll::-webkit-scrollbar {
  display: none;
}

.lightbox.scroll-reveal .lightbox-main {
  flex: none;
  width: 100%;
  height: 100%;
  min-height: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.lightbox.scroll-reveal .lightbox-info-panel {
  display: flex;
  flex: none;
  width: 100%;
  height: 100%;
  min-height: 100%;
  box-sizing: border-box;
  align-items: center;
  justify-content: center;
  padding: 64px 32px;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.lightbox-info-inner {
  max-width: 560px;
  width: 100%;
}

.lightbox-info-eyebrow {
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-soft);
  margin-bottom: 14px;
}

.lightbox-info-caption {
  font-size: 22px;
  line-height: 1.4;
  color: var(--invert-ink);
  margin-bottom: 22px;
}

.lightbox-info-story {
  font-size: 14.5px;
  line-height: 1.75;
  color: rgba(245, 245, 244, 0.7);
  padding-top: 22px;
  border-top: 1px solid rgba(245, 245, 244, 0.16);
}

/* 占位文字（还没写真正的图片故事时）特意调淡一档，跟以后填了真正内容
   时的正文区分开，一眼能看出"这是占位"。见 main.js updateLightboxImage
   里 PLACEHOLDER_STORY 那段注释。 */
.lightbox-info-story.is-placeholder {
  color: rgba(245, 245, 244, 0.45);
  font-style: italic;
}

/* 图片这一屏下方一个很轻的"往下滑看图说"提示，纯装饰，不接受点击——只有
   scroll-reveal 模式才显示；带一个很慢的上下浮动动画提醒这里可以往下滚，
   幅度和速度都刻意做得很克制，不抢图片本身的视觉焦点。因为它是
   .lightbox-main 内部 position:absolute 的一个元素，.lightbox-main 整个
   随滚动吸附着离开可视区域时，这个提示也会跟着一起滚走，不用另外写 JS
   去控制它什么时候该消失。 */
.lightbox-scroll-hint {
  display: none;
}

.lightbox.scroll-reveal .lightbox-scroll-hint {
  display: flex;
  position: absolute;
  left: 50%;
  bottom: 14px;
  transform: translateX(-50%);
  flex-direction: column;
  align-items: center;
  gap: 4px;
  color: rgba(245, 245, 244, 0.55);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  pointer-events: none;
  animation: lightbox-scroll-hint-bob 2.2s ease-in-out infinite;
}

.lightbox-scroll-hint svg {
  width: 14px;
  height: 14px;
  display: block;
}

@keyframes lightbox-scroll-hint-bob {
  0%, 100% { transform: translate(-50%, 0); opacity: 0.55; }
  50% { transform: translate(-50%, 5px); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .lightbox-scroll-hint {
    animation: none;
  }
}

/* ---------- Video Project list ---------- */
.video-list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 48px;
}

/* Selected Pictures 入口网格的系列卡片（.series-card）年份标签直接
   复用这条规则——布局改了好几版之后，最后定下来的效果就是跟 Video
   Project 列表卡片完全一样：年份在上（这条 .meta 规则）、标题在下
   （下面 .video-card h3，系列卡片这边靠 .series-card h3 那条规则，
   数值也保持一致），所以两边共用同一份 CSS，不用分别维护。 */
.video-card .meta,
.series-card .meta {
  font-size: 11px;
  letter-spacing: 0.1em;
  color: var(--ink-muted);
  text-transform: uppercase;
  margin-bottom: 10px;
}

.video-card h3 {
  font-size: 24px;
  font-weight: 400;
  margin-bottom: 10px;
}

.video-card .excerpt {
  color: var(--ink-muted);
  font-size: 15px;
}

.video-card .read-more {
  display: inline-block;
  margin-top: 14px;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1px solid var(--ink);
  padding-bottom: 2px;
}

/* ---------- Detail page header/body (used by Video Project detail,
   and by series.html by way of .series-intro instead) ---------- */
.detail-header {
  max-width: 760px;
  margin: 0 auto 40px;
  text-align: center;
}

.detail-body {
  max-width: 720px;
  margin: 0 auto;
  font-size: 17px;
  color: var(--ink-muted);
}

.detail-body p {
  color: var(--ink-muted);
  margin-bottom: 1.4em;
}

.detail-body h2 {
  font-size: 26px;
  font-weight: 400;
  margin-top: 1.6em;
}

.detail-body img {
  margin: 2em 0;
}

.detail-nav-back {
  display: inline-block;
  margin-bottom: 40px;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
}

.detail-nav-back:hover {
  color: var(--ink);
}

/* ---------- Contact ---------- */
.contact-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 72px;
}

.info-block {
  margin-bottom: 32px;
}

.info-block .label {
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--ink-muted);
  text-transform: uppercase;
  margin-bottom: 8px;
}

.info-block .value {
  font-size: 17px;
}

.social-links {
  display: flex;
  gap: 14px;
  margin-top: 8px;
}

.social-links a {
  border: 1px solid var(--line-strong);
  padding: 10px 20px;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: all 0.2s ease;
}

.social-links a:hover {
  border-color: var(--ink);
  background: var(--ink);
  color: var(--bg);
}

form {
  display: flex;
  flex-direction: column;
  gap: 22px;
}

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

label {
  display: block;
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: 8px;
}

input,
select,
textarea {
  width: 100%;
  background: var(--bg-alt);
  border: 1px solid var(--line);
  color: var(--ink);
  padding: 14px 16px;
  border-radius: 0;
  font-family: var(--font);
  font-size: 15px;
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--ink);
}

textarea {
  min-height: 140px;
  resize: vertical;
}

.form-note {
  font-size: 13px;
  color: var(--ink-muted);
}

.form-success {
  display: none;
  padding: 16px 20px;
  border: 1px solid var(--ink);
  color: var(--ink);
  font-size: 14px;
}

.form-success.show {
  display: block;
}

/* ---------- Footer ---------- */
.site-footer {
  background: var(--invert-bg);
  color: var(--invert-ink);
  padding: 56px 0;
}

.site-footer .logo {
  color: var(--invert-ink);
}

.site-footer .logo span {
  color: rgba(245, 245, 244, 0.5);
}

.footer-grid {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 20px;
}

.footer-links {
  display: flex;
  gap: 28px;
}

.footer-links a {
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(245, 245, 244, 0.6);
}

.footer-links a:hover {
  color: var(--invert-ink);
}

.site-footer .social-links a {
  border-color: var(--invert-line);
  color: rgba(245, 245, 244, 0.75);
}

.site-footer .social-links a:hover {
  border-color: var(--invert-ink);
  background: var(--invert-ink);
  color: var(--invert-bg);
}

.footer-copy {
  font-size: 11px;
  letter-spacing: 0.04em;
  color: rgba(245, 245, 244, 0.4);
  margin-top: 24px;
  text-align: center;
}

/* ---------- Interior page hero (Selected Pictures/About/Video Project/Contact) ---------- */
.page-hero {
  padding: 160px 0 60px;
  border-bottom: 1px solid var(--line);
}

.page-hero .eyebrow {
  margin-bottom: 18px;
}

.page-hero h1 {
  font-size: clamp(32px, 4.6vw, 52px);
  font-weight: 400;
}

.page-hero p {
  max-width: 560px;
  font-size: 16px;
}

/* ---------- "The Elbe River" map project (elbe-river.html) ----------
   This one page intentionally breaks the site's black/white/grayscale
   rule for its satellite basemap — the photos themselves are in color
   and this was a deliberate choice, not an oversight (see README). ---------- */
.elbe-hero {
  position: relative;
  height: calc(100vh - var(--nav-height));
  min-height: 480px;
  padding: 0;
}

#elbe-map {
  width: 100%;
  height: 100%;
  background: var(--bg-alt);
}

/* Church 项目（church.html）的地图容器用的是自己的 id（两个页面从不
   同时出现在同一个 DOM 里，但用不同 id 更清楚），样式跟上面 #elbe-map
   逐字一样，就是那 3 行，没必要为了这么小一块规则改成共用的 class。 */
#church-map {
  width: 100%;
  height: 100%;
  background: var(--bg-alt);
}

/* Custom photo-thumbnail map pin: a square thumbnail with a small
   triangle "pointer" fused to the bottom, anchored so the triangle
   tip sits exactly on the photo's coordinates. */
.elbe-pin-icon {
  background: transparent;
  border: none;
}

.elbe-pin {
  display: flex;
  flex-direction: column;
  align-items: center;
  cursor: pointer;
  transform-origin: bottom center;
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}

.elbe-pin-thumb {
  width: 52px;
  height: 52px;
  border-radius: 6px;
  overflow: hidden;
  border: 2px solid #fff;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.45);
  background: #222;
}

.elbe-pin-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.elbe-pin-arrow {
  width: 0;
  height: 0;
  margin-top: -1px;
  border-left: 7px solid transparent;
  border-right: 7px solid transparent;
  border-top: 9px solid #fff;
  filter: drop-shadow(0 2px 3px rgba(0, 0, 0, 0.35));
}

.elbe-pin:hover {
  transform: scale(1.12);
}

.elbe-pin.active {
  transform: scale(1.7);
  z-index: 10000;
}

/* 易北河/教堂系列切换 Map View/Gallery View 的按钮——试过挪进顶部
   导航栏（跟着 Contact 走、后来又改成对齐瀑布流右边缘），用户看了
   效果还是决定改回最早的样子：固定悬浮在右下角。这一版在"改回原样"
   的基础上又加了三点调整：
   1. 手机端右边缘对齐瀑布流照片区（.showcase-wide，见下面 680px
      媒体查询里单独写的一条规则）最右边照片的右边缘；电脑端不做
      这个限制，就是简单的 right:24px，随浏览器宽度自适应，不用
      刻意对齐哪张照片。
   2. 明确写死 opacity:1，保证按钮平时（没有悬停/没有全屏/没有贴近
      页脚）看着都是纯黑不透明的一整块。鼠标悬停时的 opacity:0.85
      淡出效果保留（试过换成纯色背景反馈，效果不如原来的透明度
      变化，改回来了）——这只在桌面端鼠标悬停时短暂出现，跟"默认
      状态必须不透明"不冲突。
   3. 默认离底边的距离从 24px（桌面）/16px（手机）都往上提了不少
      （见下面具体数值），避开地图右下角 MapTiler/OpenStreetMap 的
      版权信息条——原来贴得太近，版权文字那一行本来就是不透明的浅色
      背景条，跟按钮紧挨在一起容易第一眼看着像是连在一起的半透明
      色块，往上挪开一段距离后两者不会再有任何视觉上的粘连。 */
.elbe-mode-toggle {
  position: fixed;
  right: 24px;
  bottom: 64px;
  z-index: 500;
  padding: 16px 32px;
  background: var(--invert-bg);
  color: var(--invert-ink);
  border: 1px solid var(--invert-bg);
  font-size: 12px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-family: var(--font);
  cursor: pointer;
  opacity: 1;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  transition: transform 0.25s ease, opacity 0.25s ease;
}

/* 悬停变淡这个反馈只应该在"真的有鼠标/触控板可以悬停"的设备上生效。
   之前直接写 .elbe-mode-toggle:hover，手机浏览器会在点击之后把
   :hover 样式留在按钮上（不少手机浏览器点击一下就触发一次
   "悬停"，而且要等点了页面别的地方才会消失），按钮又是固定悬浮、
   会被盯着看一会儿的，这个"点一下就变淡、还不容易自己消失"的效果
   很可能就是用户看到"按钮好像半透明"的真正原因。用 (hover: hover)
   and (pointer: fine) 这个组合限定：只有当前设备的主要输入方式
   "既能真正悬停、又是精确指点设备"（也就是鼠标/触控板）时才生效——
   手机、平板，以及能触屏操作的笔记本电脑（哪怕它同时也有鼠标/
   触控板，只要是用手指触屏点这个按钮），点按钮都不会触发这个变淡
   效果；只有真正用鼠标/触控板把指针移到按钮上悬停时才会生效。 */
@media (hover: hover) and (pointer: fine) {
  .elbe-mode-toggle:hover {
    opacity: 0.85;
  }
}

.elbe-mode-toggle.is-hidden,
.elbe-mode-toggle.is-lightbox-open {
  opacity: 0;
  pointer-events: none;
  transform: translateY(10px);
}

.elbe-normal-intro {
  max-width: 720px;
  margin-bottom: 56px;
}

.elbe-theme-title {
  font-size: 28px;
  font-weight: 400;
  margin-bottom: 32px;
}

/* ---------- Responsive ---------- */
@media (max-width: 900px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .about-split,
  .contact-grid {
    grid-template-columns: 1fr;
  }
  .about-split img {
    max-width: 340px;
  }
  .video-list {
    grid-template-columns: 1fr;
  }
  .series-grid {
    grid-template-columns: repeat(2, 1fr);
  }
  .showcase-col {
    gap: 36px;
  }
}

@media (max-width: 680px) {
  :root {
    --nav-height: 64px;
  }
  .nav-links {
    position: fixed;
    top: var(--nav-height);
    left: 0;
    right: 0;
    background: var(--bg);
    flex-direction: column;
    padding: 24px 32px;
    border-bottom: 1px solid var(--line);
    display: none;
    gap: 20px;
  }
  .nav-links.open {
    display: flex;
  }
  .nav-toggle {
    display: flex;
  }
  .grid {
    grid-template-columns: repeat(2, 1fr);
    gap: 14px;
  }
  section {
    padding: 70px 0;
  }
  .form-row {
    grid-template-columns: 1fr;
  }
  .series-grid {
    grid-template-columns: 1fr;
  }
  .showcase-grid {
    flex-direction: column;
    gap: 32px;
  }
  .showcase-col {
    gap: 32px;
  }
  .showcase-wide {
    padding: 0 20px;
  }
  .container {
    padding: 0 20px;
  }
  .hero-actions {
    flex-direction: column;
  }
  /* 手机端页脚这四个链接（Selected Pictures/Video Project/About/
     Contact）——桌面端是横排一行，照搬到手机上因为宽度不够，每个链接
     会在自己内部按单词换行（"Selected"/"Pictures" 拆成两行），看着很
     乱。应用户要求改成固定的两列两行网格：每个标题本身不换行，第一行
     放前两个（Selected Pictures / Video Project），第二行放后两个
     （About / Contact）——正好是 index.html 等页面里 <nav class=
     "footer-links"> 内 4 个 <a> 原本的先后顺序，网格默认就是从左到右、
     从上到下填格子，不用额外调整 HTML 顺序或加 order。两列各自的宽度
     由 grid-template-columns: repeat(2, auto) 按该列最长的那个标题自动
     撑开（不是各占 50%），比固定等分更紧凑、更贴合每列文字实际长度。 */
  .footer-links {
    display: grid;
    grid-template-columns: repeat(2, auto);
    column-gap: 28px;
    row-gap: 14px;
  }

  .footer-links a {
    white-space: nowrap;
  }

  .elbe-hero {
    /* 手机屏幕上希望地图尽量占满整个屏幕（像原生地图 App 那种
       "几乎全屏、只留一条顶部导航栏"的观感），而不是像桌面端那样
       固定留一截。这里先给一个 78vh 的保底值（不支持 dvh 的老浏览器
       会用这个），再用 100dvh 覆盖——手机 Safari 的地址栏会跟着滚动
       展开/收起，用 vh 算出来的高度是按"地址栏展开、可视区域最小"
       那种情况算的，实际地址栏收起后可视区域明显更高，地图却还是
       停在按小尺寸算出来的高度，看起来比其实能用的空间小一截；dvh
       (dynamic viewport height) 会跟着地址栏收起/展开动态调整，能
       真正用满当前实际可见的屏幕高度。 */
    height: 78vh;
    height: calc(100dvh - var(--nav-height));
    min-height: 420px;
  }
  .elbe-mode-toggle {
    padding: 13px 24px;
    font-size: 11.5px;
    bottom: 56px;
    /* 手机端右边缘对齐瀑布流照片区（.showcase-wide，最大宽度
       2200px，见该 class 定义上方的注释）里最右边照片的右边缘。
       手机屏幕宽度不可能超过 2200px，所以 .showcase-wide 在手机上
       跟视口永远一样宽（居中留白那一项恒为 0），要对齐的距离直接
       就是 .showcase-wide 自己的右内边距——这里的 clamp(20px, 4vw,
       64px) 是照抄 .showcase-wide 的 padding 数值，改的话要两边
       一起改，否则会对不齐。 */
    right: clamp(20px, 4vw, 64px);
  }
  .elbe-pin-thumb {
    width: 42px;
    height: 42px;
  }

  /* 全屏看图弹窗在手机上专门收紧过一遍：边距、标题文字、胶片
     缩略图都比桌面端小一圈，腾出来的空间全部让给照片本身，让它
     尽量接近（甚至超过）没进全屏之前瀑布流里图片的显示大小；但
     图片上下的箭头/标题这一行、跟胶片缩略图之间，还是要留出看得出来
     的呼吸空间，不能挤成一条缝——这几条规则是这个全屏看图组件本身的
     样式（main.js 里 ensureLightboxMarkup 那套逻辑是全站共用的，不是
     易北河专属），以后别的系列复用同一套全屏看图时会自动套用，不用
     再单独适配一遍。 */
  .lightbox {
    padding: 14px 0 10px;
  }

  .lightbox-main {
    padding: 0 10px;
  }

  /* 手机屏幕本身窄，桌面端 24px 的间隙比例上显得有点宽，收窄到 16px——
     跟参考的 iPhone 相册截图（一张 1179px 宽的手机截图里，两张照片中间
     大约留了 60px 出头的间隙）比例上更接近。 */
  .lightbox-track {
    gap: 16px;
  }

  /* 关闭按钮位置同样是固定的 top/right（不随图片变化），用常见手机
     屏幕比例（19.5:9 到 20:9 附近，包含 iPhone SE 这种更接近传统
     16:9 的机型）配合 Elbe 全部照片的真实长宽比核算过，能保证按钮要么
     完全在照片外、要么完全在照片内且留有 ≥8px 余量，没有已知的卡边
     情况（跟桌面端不同，手机屏幕比例的种类本身就有限，不存在"窗口可以
     拖成任意宽度"的问题）。原本这个数值是 18px，实机看下来贴屏幕边缘
     太近、有点局促，所以往里挪了 12px 改成 30px（30px 重新核算过，
     仍然没有卡边情况）。 */
  .lightbox-close {
    top: 44px;
    right: 30px;
    width: 34px;
    height: 34px;
  }

  .lightbox-close-icon {
    width: 12px;
    height: 12px;
  }

  /* 跟桌面端同样的道理（见上面 .lightbox-footer 主规则那条注释）：
     margin 从原来的 18px/18px 收窄，抵消两行文字（标题+浏览进度）比
     之前一行标题多占的高度，让大图到缩略图的空隙保持手机端原来的
     样子。 */
  .lightbox-footer {
    margin: 12px 0 12px;
  }

  /* 左右切换箭头用回这套全屏组件最早版本的尺寸/字号（比关闭按钮还要
     小一圈），边距也比屏幕物理边缘多留了几像素，不会显得整排顶到底。 */
  .lightbox-prev,
  .lightbox-next {
    width: 28px;
    height: 28px;
    font-size: 12px;
  }

  .lightbox-prev {
    left: 14px;
  }

  .lightbox-next {
    right: 14px;
  }

  .lightbox-caption,
  .lightbox-progress {
    font-size: 10.5px;
    padding: 0 46px;
  }

  .lightbox-caption-group {
    gap: 2px;
  }

  .filmstrip {
    gap: 6px;
    padding: 4px 4px 4px;
  }

  .filmstrip-item {
    width: 44px;
    height: 44px;
  }

  .lightbox.scroll-reveal .lightbox-info-panel {
    padding: 40px 24px;
  }

  .lightbox-info-caption {
    font-size: 18px;
    margin-bottom: 16px;
  }

  .lightbox-info-story {
    font-size: 13.5px;
    padding-top: 16px;
  }
}
