/* ─────────────────────────────────────────────────────────────
   设计语汇:藏青布面精装 + 烫金 + 一条贯穿全书的红线(月老红线)
   红线同时是「书签 / 进度条 / 章节连接」,是本书的记忆点。
   ───────────────────────────────────────────────────────────── */
:root{
  --night:#96455A;        /* 书封布面:干燥玫瑰。刻意压深,才撑得住浅色书页的对比 */
  --night-deep:#5C2334;   /* 书脊阴影 */
  --gold:#D89A7C;         /* 玫瑰金烫印(不是黄金,是带粉的铜) */
  --gold-lit:#F6DACB;     /* 烫印高光 */
  --paper:#FCF4F1;        /* 内页纸:微粉的米白 */
  --paper-2:#F3E4E0;      /* 纸的暗面 */
  --ink:#4B2F3A;          /* 内文墨色:深梅褐,暖的,不用纯黑也不用蓝黑 */
  --ink-soft:#9A7883;
  --thread:#C43F5C;       /* 红线改胭脂,收进同一个粉色家族 */
  --pw:360px;             /* 单页宽,由 JS 依窗口计算 */
  --ph:512px;
  color-scheme:dark;      /* 跟 index.html 的 meta 同一件事,两处都写才保险 */
  --u:1;                  /* 无单位缩放系数,让字级跟着页面尺寸走 */
  --flip:.95s;
  --ease:cubic-bezier(.62,.03,.31,1);
  /* 书沟(两页中间的接缝)。纸摊开时会往装订处弯下去,所以:
     靠缝最暗 -> 离缝约两成处是拱脊、最亮 -> 再往外缓缓收平。
     原本的写法把最亮点放在接缝上、还铺满半页宽,方向是反的。 */
  --gutter-dark:.66;    /* 装订处最深的浓度 */
  --gutter-span:9%;    /* 阴影延伸多宽 */
  --crest-light:.62;    /* 拱脊高光强度 */
  --crest-at:16%;       /* 拱脊离书沟多远 */
  --serif:"Noto Serif SC","Songti SC","SimSun",serif;
  --kai:"Kaiti SC","KaiTi","STKaiti","DFKai-SB","Noto Serif SC",serif;
  --latin:"Fraunces",Georgia,serif;
}
@media (prefers-reduced-motion:reduce){ :root{ --flip:.001s } }

*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
  background:var(--bg-base,#150A12);
  color:var(--paper);
  font-family:var(--serif);
  overflow:hidden;
  -webkit-font-smoothing:antialiased;
  display:flex;flex-direction:column;
}

/* ── 背景三层:渐层 / 粒子 / 暗角 ─────────────────────────
   书是深色布面,背景若同色系就浮不起来。所以背景走「另一个暖色调
   的夜」,再用一道暖光晕打在书后面,模拟台灯下翻书。            */
.bg{
  position:fixed;inset:0;z-index:-3;
  background-image:var(--bg-layers);
  transition:background-image .9s ease, opacity .9s ease;
}
.fx{position:fixed;inset:0;z-index:-2;pointer-events:none}
.vignette{
  position:fixed;inset:0;z-index:-1;pointer-events:none;
  background:radial-gradient(104% 76% at 50% 46%, transparent 38%, var(--bg-vig,rgba(0,0,0,.55)) 100%);
  transition:background .9s ease;
}

/* ── 顶部工具列 ───────────────────────────────────────────── */
.bar{
  flex:0 0 auto;display:flex;align-items:center;gap:14px;
  /* 三值简写是「上 / 左右 / 下」—— safe-area-inset-top 要加在**上**。
     原本写在第三个位置,等于把刘海的高度补到了顶栏底下 —— 刘海照样压住
     按钮。加到主画面全屏跑的时候这个特别明显。 */
  padding:calc(14px + env(safe-area-inset-top,0px)) 18px 14px;
  font-size:13px;letter-spacing:.08em;color:var(--ui-dim);
}
.bar .mark{font-family:var(--latin);font-style:italic;color:var(--ui-hot);letter-spacing:.02em}
.bar .spacer{flex:1}
.btn{
  appearance:none;background:transparent;color:var(--ui);cursor:pointer;
  border:1px solid var(--ui-line);border-radius:999px;
  padding:6px 14px;font:inherit;font-size:12.5px;letter-spacing:.1em;
  white-space:nowrap;transition:.22s;
}
@media (max-width:600px){
  .bar{gap:7px;padding:calc(10px + env(safe-area-inset-top,0px)) 12px 10px}
  .bar .mark{display:none}          /* 窄屏幕优先给按钮,标记字省略 */
  .btn{padding:6px 10px;font-size:11.5px;letter-spacing:.06em}
}
.btn:hover{border-color:var(--ui-hot);color:var(--ui-hot);background:var(--ui-wash)}
.btn:focus-visible{outline:2px solid var(--ui-hot);outline-offset:3px}
.btn[aria-pressed="true"]{color:var(--ui-hot);border-color:var(--ui-hot)}

/* ── 书的舞台 ─────────────────────────────────────────────── */
.stage{flex:1;position:relative;display:grid;place-items:center;min-height:0}
.book{
  position:relative;
  /* 横向手势留给拖曳翻页,纵向与缩放交还给浏览器 */
  touch-action:pan-y pinch-zoom;
  /* 拖曳翻页时不要选到字。这本书是用来读的,不是用来选取的,
     而拖曳是主要手势 —— 两者在同一块区域上必然打架。       */
  -webkit-user-select:none;user-select:none;
  cursor:grab;                  /* 按钮拿掉了,游标要补上可拖曳的暗示 */
  width:calc(var(--pw)*2);height:var(--ph);
  perspective:2200px;
  transform:translateX(var(--shift,0px));
  transition:transform var(--flip) var(--ease);
}
.book.single{width:var(--pw)}
/* 书底的实体感:书脊阴影 + 落地阴影 */
.book::after{
  content:"";position:absolute;left:6%;right:6%;bottom:calc(-26px * var(--u));height:calc(34px * var(--u));
  background:radial-gradient(50% 100% at 50% 0%, rgba(0,0,0,.62), transparent 74%);
  filter:blur(2px);pointer-events:none;
}
/* 两个边界上只有一半有纸,阴影得跟着收到那一半去 —— 不收的话阴影中心
   (radial-gradient 最深处)会落在书脊上,而书脊此时正是那一页的外缘,
   看起来就是一片阴影飘在空背景上。
   6% 是相对两页宽算的,收成半边之后要改用 var(--pw)*.06 才等宽。
   :not(.single) 是必须的 —— 单页版面的 .book 本来就只有一页宽,
   套上去会把阴影再对折一次。                                        */
.book:not(.single).closed::after{                /* 只剩右半边有纸(封面) */
  left:calc(50% + var(--pw) * .06);
  right:calc(var(--pw) * .06);
}
.book:not(.single).ended::after{                 /* 只剩左半边有纸(尾页) */
  left:calc(var(--pw) * .06);
  right:calc(50% + var(--pw) * .06);
}

.leaf{
  position:absolute;top:0;left:50%;width:var(--pw);height:var(--ph);
  transform-origin:left center;transform-style:preserve-3d;
  transition:transform var(--flip) var(--ease);
}
.book.single .leaf{left:0}
.leaf.flipped{transform:rotateY(-180deg)}
.leaf.tucked{visibility:hidden}     /* 单页模式:翻完的纸收起来,不挡画面 */
.leaf.noanim{transition:none}

.face{
  position:absolute;inset:0;
  backface-visibility:hidden;-webkit-backface-visibility:hidden; /* iOS 15 以下会残影 */
  overflow:hidden;
  background:var(--paper);color:var(--ink);
  display:flex;flex-direction:column;
}
.face.back{transform:rotateY(180deg)}
/* 纸纹 + 靠近书沟的渐层阴影 */
.face::before{
  content:"";position:absolute;inset:0;pointer-events:none;z-index:3;
  background:
    /* ① 装订处的深影:窄而深 */
    linear-gradient(90deg,
      rgba(70,40,52,var(--gutter-dark))                    0%,
      rgba(70,40,52,calc(var(--gutter-dark) * .56))        calc(var(--gutter-span) * .15),
      rgba(70,40,52,calc(var(--gutter-dark) * .24))        calc(var(--gutter-span) * .35),
      rgba(70,40,52,calc(var(--gutter-dark) * .07))        calc(var(--gutter-span) * .65),
      rgba(70,40,52,0)                                     var(--gutter-span)),
    /* ② 拱脊的高光 —— 这道才是「纸是圆弧」的关键 */
    linear-gradient(90deg,
      rgba(255,252,250,0)                                  calc(var(--crest-at) * .20),
      rgba(255,252,250,calc(var(--crest-light) * .68))     calc(var(--crest-at) * .64),
      rgba(255,252,250,var(--crest-light))                 var(--crest-at),
      rgba(255,252,250,calc(var(--crest-light) * .50))     calc(var(--crest-at) * 1.70),
      rgba(255,252,250,0)                                  calc(var(--crest-at) * 2.60)),
    /* ③ 外缘轻微收暗,整片才读得出是圆柱面而不是平板 */
    linear-gradient(270deg,
      rgba(72,44,56,.10) 0%, rgba(72,44,56,0) 22%);
}
.face.back::before{
  background:
    /* ① 装订处的深影:窄而深 */
    linear-gradient(270deg,
      rgba(70,40,52,var(--gutter-dark))                    0%,
      rgba(70,40,52,calc(var(--gutter-dark) * .56))        calc(var(--gutter-span) * .15),
      rgba(70,40,52,calc(var(--gutter-dark) * .24))        calc(var(--gutter-span) * .35),
      rgba(70,40,52,calc(var(--gutter-dark) * .07))        calc(var(--gutter-span) * .65),
      rgba(70,40,52,0)                                     var(--gutter-span)),
    /* ② 拱脊的高光 —— 这道才是「纸是圆弧」的关键 */
    linear-gradient(270deg,
      rgba(255,252,250,0)                                  calc(var(--crest-at) * .20),
      rgba(255,252,250,calc(var(--crest-light) * .68))     calc(var(--crest-at) * .64),
      rgba(255,252,250,var(--crest-light))                 var(--crest-at),
      rgba(255,252,250,calc(var(--crest-light) * .50))     calc(var(--crest-at) * 1.70),
      rgba(255,252,250,0)                                  calc(var(--crest-at) * 2.60)),
    /* ③ 外缘轻微收暗,整片才读得出是圆柱面而不是平板 */
    linear-gradient(90deg,
      rgba(72,44,56,.10) 0%, rgba(72,44,56,0) 22%);
}
/* 翻动中的纸提升成独立的合成层。没有这行的话,box-shadow 会让浏览器
   在每一帧重新绘制这张纸,3D 旋转就会顿。只在 .turning 期间加,
   不常驻 —— 九张纸都常驻会白白吃掉显存。                      */
.leaf.turning,
.leaf.dragging{will-change:transform}
.leaf.turning .face{box-shadow:0 0 40px rgba(0,0,0,.35)}

/* ── 封面:藏青布面 + 烫金 ────────────────────────────────── */
.cover{
  background:
    repeating-linear-gradient(90deg, rgba(255,255,255,.030) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(0deg,  rgba(0,0,0,.055) 0 1px, transparent 1px 3px),
    linear-gradient(152deg,#B85F76 0%, var(--night) 46%, #5A2131 100%);
  color:var(--gold-lit);
  align-items:center;justify-content:center;text-align:center;
  padding:calc(40px * var(--u));cursor:pointer;
}
.cover::before{display:none}
.cover .frame{
  position:absolute;inset:calc(18px * var(--u));
  border:1px solid rgba(230,168,140,.52);
}
.cover .frame::after{
  content:"";position:absolute;inset:calc(6px * var(--u));
  border:1px solid rgba(230,168,140,.24);
}
.cover .eyebrow{
  font-family:var(--latin);font-style:italic;font-size:calc(12px * var(--u));
  letter-spacing:.34em;color:rgba(246,218,203,.70);margin-bottom:calc(26px * var(--u));
}
.cover h1{
  font-family:var(--kai);font-weight:400;
  font-size:calc(var(--title,44px) * var(--u));letter-spacing:.2em;text-indent:.2em;
  line-height:1.25;white-space:nowrap;
  background:linear-gradient(180deg,#FBE6DA,#DFA184 52%,#A9664D);
  -webkit-background-clip:text;background-clip:text;color:transparent;
}
.cover .emblem{margin:calc(22px * var(--u)) 0}
.cover .names{
  font-family:var(--kai);font-size:calc(15px * var(--u));letter-spacing:.3em;
  color:rgba(246,218,203,.88);margin-top:calc(4px * var(--u));
}
.cover .open-hint{
  position:absolute;bottom:calc(40px * var(--u));left:0;right:0;
  font-size:calc(11.5px * var(--u));letter-spacing:.32em;color:rgba(246,218,203,.66);
  animation:breathe 3.4s ease-in-out infinite;
}
@keyframes breathe{0%,100%{opacity:.4}50%{opacity:1}}
@media (prefers-reduced-motion:reduce){.cover .open-hint{animation:none;opacity:.7}}

/* ── 内页通用 ─────────────────────────────────────────────── */
.pg{padding:calc(46px * var(--u)) calc(40px * var(--u));position:relative;z-index:2;
    flex:1;display:flex;flex-direction:column;min-height:0}
.face.back .pg{padding-left:calc(34px * var(--u));padding-right:calc(46px * var(--u))}
.folio{
  position:absolute;bottom:calc(20px * var(--u));left:0;right:0;text-align:center;
  font-family:var(--latin);font-size:calc(11px * var(--u));color:var(--ink-soft);
  letter-spacing:.18em;z-index:4;
  /* 只是页码。小视窗时这条带会压到目录最后一章的按钮,
     不关命中的话那一下会被空白层接走(940x500 实测整颗按钮失效)。 */
  pointer-events:none;
}

.eyebrow-ink{
  font-family:var(--latin);font-style:italic;letter-spacing:.2em;
  font-size:calc(12px * var(--u));color:var(--thread);
}
.rule{height:1px;background:linear-gradient(90deg,var(--thread),rgba(196,63,92,0));
      margin:calc(14px * var(--u)) 0 calc(20px * var(--u));width:56%}
h2.title{
  font-family:var(--kai);font-weight:400;font-size:calc(28px * var(--u));
  letter-spacing:.1em;line-height:1.5;color:var(--ink);
}
.date{
  font-family:var(--latin);font-size:calc(13px * var(--u));color:var(--ink-soft);
  letter-spacing:.14em;margin-top:calc(8px * var(--u));
}
/* --fit 由 app.js 的 fitText() 设:正文太长就整体缩一点,缩到装得下为止。
   字级与段距一起缩,行高不动 —— 只缩字级的话行距会显得过宽。 */
.body p{
  font-size:calc(15px * var(--u) * var(--fit, 1));line-height:2.05;letter-spacing:.03em;
  color:#3A4362;margin-top:calc(15px * var(--u) * var(--fit, 1));text-align:justify;
}
.note{
  margin-top:auto;padding-top:calc(18px * var(--u) * var(--fit, 1));
  font-family:var(--kai);font-size:calc(13.5px * var(--u) * var(--fit, 1));line-height:1.9;
  color:var(--thread);border-top:1px dashed rgba(196,63,92,.3);
}

/* 拍立得 */
.polaroid{
  align-self:center;margin:auto;
  background:#fff;padding:calc(13px * var(--u)) calc(13px * var(--u)) calc(46px * var(--u));
  box-shadow:0 calc(10px * var(--u)) calc(26px * var(--u)) rgba(30,25,15,.22);
  transform:rotate(var(--tilt,-1.6deg));
  max-width:100%;
}
.polaroid .shot{
  width:calc(210px * var(--u));height:calc(210px * var(--u));
  background:linear-gradient(150deg,#EBD9D6,#D8BFC0 58%,#BFA0A6);
  display:grid;place-items:center;overflow:hidden;
}
/* ⚠ 绝对定位不是排版偏好,是修一个会让 object-position 静默失效的 bug。
   .shot 是 display:grid + place-items:center,格线列是 auto 再被拉伸 ——
   在那种列里 height:100% 会被当成 auto,于是 img 回到自己的原始比例把
   格子撑破(实测:162x262 的框里 img 是 162x288)。
   真正的后果不是多裁一点,而是 img 的框一旦等于原始比例,
   **object-fit 与 object-position 就全成了空转** —— focus 写了也没用,
   实际看到的永远是格线置中裁切。后浪橘子海那张底下的字就是这样被切掉的,
   而且量 getComputedStyle 还会回报 "50% 72%",看起来一切正常。
   .shot 本来就有 position:relative,绝对定位能拿到确定的高度。       */
.polaroid .shot img, .polaroid .shot video{position:absolute;inset:0}
.polaroid .shot img{
  width:100%;height:100%;object-fit:cover;display:block;
  /* 预设置中裁切。只有裁切会切到重点的那几张才在 app.js 写 focus,
     由 paintShots 塞进 --focus(不能用 style 属性,CSP 会挡)。 */
  object-position:var(--focus, 50% 50%);
}
/* 直式框。手机拍的是 9:16,塞进 210 的方框会连头带脚一起切掉 ——
   这个比例(200/324 = 0.62)对上 0.55~0.62 的原图,裁掉的不到一成。
   :not(.is-video) 是必须的:影片框的高度另有一条规则在后面,
   两条选择器权重一样,后来的那条会赢,写成无条件的反而互相打架。 */
.polaroid.tall:not(.is-video) .shot{
  width:calc(200px * var(--u));height:calc(324px * var(--u));
}
.polaroid .shot span{
  font-size:calc(11.5px * var(--u));letter-spacing:.16em;color:rgba(255,255,255,.85);
  text-align:center;line-height:1.9;padding:0 12px;
}
.polaroid figcaption{
  font-family:var(--kai);font-size:calc(13px * var(--u));color:#4A4438;
  text-align:center;margin-top:calc(15px * var(--u));letter-spacing:.06em;
}
/* 纸胶带 */
.polaroid::before{
  content:"";position:absolute;top:calc(-11px * var(--u));left:50%;
  width:calc(74px * var(--u));height:calc(22px * var(--u));
  transform:translateX(-50%) rotate(-3deg);
  background:rgba(196,63,92,.14);border-left:1px dashed rgba(196,63,92,.28);
  border-right:1px dashed rgba(196,63,92,.3);
}
.polaroid{position:relative}
.polaroid .shot{position:relative}

/* ── 相册页:一页两张,斜错开来交叠 ──────────────────────────
   为什么不是一页一张:直式照片(0.617)摆在直式页面(0.704)上,拍立得的
   白框再加 .pg 的 46/40 内距,照片实测只占纸面 35.2%,左右各空 61px。
   而单张再放大也救不回来 —— 维持 0.617 的比例时,页高会先卡死宽度,
   算过极限约 44%。使用者看到的就是这个:「一页只有一张图感觉有点空泛」。

   两张斜错开来盖到 93% x 88% 的纸面,而且交叠本来就是相簿的样子。

   座标一律用 px * var(--k),**不能用 --u**。--k 是线性的(pw/360),
   所以 360 与 511 这两个数字在任何屏幕上都精确对应整张纸;
   --u 是 sqrt(pw/360),用它当座标时页面实际只有 sqrt(360*pw) 个单位宽 ——
   横放手机 pw=172 时是 249(卡片右溢 71px),大屏幕 pw=700 时是 501
   (版面只占三分之二,下半页整片空)。两种破法都实测过。

   底下留 47px 不摆东西 —— .folio 是 .pg 的兄弟且 z-index:4,会盖在
   照片上面。不留这条,页码就印在人脸旁边。                          */
.pg.album, .face.back .pg.album{padding:0}   /* 权重要压过 .face.back .pg */
.pg.album .polaroid{
  position:absolute;margin:0;max-width:none;   /* 原本的 margin:auto 会把绝对定位的它推到置中 */
  align-self:auto;
  /* 卡片的每一段几何都要用 --k(线性),不能留任何一段是 --u(平方根)——
     混用会让卡片的比例随屏幕大小漂移,白框与照片就对不上了。
     .polaroid 原本这几个值都是 --u,这里整组盖掉。 */
  padding:calc(13px * var(--k)) calc(13px * var(--k)) calc(46px * var(--k));
  box-shadow:0 calc(10px * var(--k)) calc(26px * var(--k)) rgba(30,25,15,.22);
}
.pg.album .polaroid::before{                   /* 纸胶带 */
  top:calc(-11px * var(--k));
  width:calc(74px * var(--k));height:calc(22px * var(--k));
}
.pg.album .polaroid figcaption{
  font-size:calc(13px * var(--k));
  margin-top:calc(15px * var(--k));
  /* 必须明写行高。不写会继承 normal,而 normal 是字型自己说了算 ——
     开发机的楷体约 1.14,iOS 的 Kaiti 与 Android 的 Noto Serif SC 可以到 1.5。
     卡片总高 = 白边 + 照片 + 这一行,行高一漂,下面那张卡就会压到页码上。 */
  line-height:1.35;
  /* 说明文字现在是空的(使用者要自己慢慢补)。空的 figcaption 高度会塌成 0,
     卡片跟着缩 18px、位置整个上移 —— 版面是照有字的高度算好的,不能让它变。
     留一行的高度占着位:补字进来时版面纹丝不动,空着的时候那一条白边
     看起来就是相片下面本来用来写字的地方。 */
  min-height:1.35em;
}
/* 尺寸写在这里而不是套 .tall:.polaroid.tall:not(.is-video) .shot 是
   (0,4,0),相册这条 (0,3,0) 压不过它。索性不套 tall,直接给。 */
.pg.album .polaroid .shot{
  width:calc(162px * var(--k));height:calc(262px * var(--k));
}
/* ⚠ 影片卡必须明写,不能靠上面那条。后面「章节影片」那一段有一条
   `.polaroid.is-video .shot{height:calc(280px * var(--u))}` —— 它跟上面
   那条同为三个 class(权重打平),而且写在后面,所以影片卡的**高**会被它
   抢走变成 280*u,宽却还是 162*k:两个尺度混在同一张卡上,白框跟画面
   就对不上了(u 是平方根、k 是线性,只有 pw=360 时两者才相等)。
   这条是四个 class,压得过它。 */
.pg.album .polaroid.is-video .shot{
  width:calc(162px * var(--k));height:calc(262px * var(--k));
}
/* 播放键也要跟着卡片走线性尺度,否则大屏幕上键会相对缩小、小屏幕上撑爆 */
.pg.album .polaroid .shot .play{
  width:calc(46px * var(--k));height:calc(46px * var(--k));
}
.pg.album .polaroid .shot .play::after{
  border-left:calc(13px * var(--k)) solid rgba(255,255,255,.92);
  border-top:calc(8px * var(--k)) solid transparent;
  border-bottom:calc(8px * var(--k)) solid transparent;
  transform:translateX(calc(3px * var(--k)));
}
/* 这些数字是量出来的,不是估的。卡片本体 188x352(照片 162x262 + 白框),
   转 ±3 度之后外接框会各往外长 9px —— 所以 left:12 实际会贴到纸缘 3px,
   纸胶带(在卡片上缘往上 11px)还会被裁掉。往内收到 16 / 156 才留得住。
   slot-b 的下缘压在 464,页码在 477 起 —— .folio 是 .pg 的兄弟且 z-index:4,
   会盖在照片上,不留这条页码就印在人身上。

   **照片框不能再放大**(试过 172x278):卡片会变成 198 宽,两张摆在 360 的
   纸上、两边各留 9px 之后必然重叠 54px 以上,而 slot-a 的说明文字是置中的,
   右缘刚好落在那 54px 里 —— 「镜头前的妳」会被压掉最后一个字。
   垂直错开也解不了:卡片 352 高,两张不重叠要 704,纸只有 511。 */
.pg.album .polaroid.slot-a{left:calc(16px * var(--k));  top:calc(20px * var(--k));  --tilt:-3deg;  z-index:1}
.pg.album .polaroid.slot-b{left:calc(156px * var(--k)); top:calc(112px * var(--k)); --tilt:2.6deg; z-index:2}
/* 页码也要跟着走线性尺度。它预设是 bottom:20px*u、字级 11px*u ——
   卡片换成 --k 之后两者比例不再同步:横放手机(u 0.697 / k 0.486)时
   页码相对往上跑,实测跟下面那张卡叠了 3px。整组换成 k 就锁死了。 */
.pg.album ~ .folio{
  bottom:calc(20px * var(--k));
  font-size:calc(11px * var(--k));
}

/* 两页相册在跨页模式下是并排的,左右镜像才不会像复制贴上 */
.pg.album.mirror .polaroid.slot-a{left:calc(156px * var(--k)); --tilt:3deg}
.pg.album.mirror .polaroid.slot-b{left:calc(16px * var(--k));  --tilt:-2.6deg}

/* ── 侧出血单页 ──────────────────────────────────────────────
   照片贴齐上、下与外缘,书沟侧留一条 40px 的窄纸栏。

   尺寸怎么来的:框 320x511 的比例是 0.626,比五张原图(0.547~0.623)都宽,
   所以 cover 只裁高、不裁宽;再把 object-position 压到底,裁掉的那 12.7%
   全部从顶端拿 —— 直播画面顶上的横幅与状态列整条消失,人从头到脚完整。
   纸栏拿 96 与 64 也试过:96 会把整条「N'S LIVE」横幅留在画面顶上,
   64 还留着一枚徽章,40 才干净。

   纸栏永远靠书沟(装订侧),照片往外缘出血 —— 字才不会掉进书沟的阴影里。
   **不能指望 .face.back 的 rotateY(180deg) 自己把它镜像过去**:那张纸本身
   也转了 -180 度,两次相抵,`left:0` 视觉上就还是左边(实测过:跨页时纸栏
   落在左页的外缘,不是书沟)。所以背面要明写一条翻过来。
   单页模式(手机)每一页都是正面,装订在左,预设这条就是对的。       */
.pg.bleed, .face.back .pg.bleed{padding:0}   /* 权重要压过 .face.back .pg */
.pg.bleed figure{position:absolute;inset:0;margin:0}
.pg.bleed .shot{
  position:absolute;top:0;bottom:0;left:calc(40px * var(--k));right:0;
  overflow:hidden;background:#0B0A12;
}
.pg.bleed .shot img{
  position:absolute;inset:0;width:100%;height:100%;
  object-fit:cover;object-position:var(--focus, 50% 50%);display:block;
}
.pg.bleed .shot:has(img){cursor:zoom-in}
/* 跨页时的左页:书沟在右,整组左右对调 */
.face.back .pg.bleed .shot{left:0;right:calc(40px * var(--k))}
.pg.bleed .strip{
  position:absolute;top:0;bottom:0;left:0;width:calc(40px * var(--k));
  display:flex;flex-direction:column;align-items:center;
  /* 底下留 44 是给页码的 —— 页码被收进这条纸栏了(见下面 .folio),
     留 28 的话图说的最后一个字会跟页码叠在一起。 */
  padding:calc(44px * var(--u)) 0 calc(44px * var(--u));
}
/* 直排。text-orientation:upright 让每个字正着叠,不是整行转 90 度 ——
   中文竖排本来就该是正的,数字也一样,不然日期会躺着。 */
.pg.bleed .strip .ch,
.pg.bleed .strip .dt,
.pg.bleed .strip figcaption{
  writing-mode:vertical-rl;text-orientation:upright;
}
.pg.bleed .strip .ch{
  font-family:var(--kai);font-size:calc(11px * var(--u));
  letter-spacing:.3em;color:var(--thread);
}
.pg.bleed .strip .tick{
  width:1px;height:calc(26px * var(--u));margin:calc(12px * var(--u)) 0;
  background:linear-gradient(180deg, var(--thread), rgba(196,63,92,0));
}
.pg.bleed .strip .dt{
  font-family:var(--latin);font-size:calc(10px * var(--u));
  letter-spacing:.2em;color:var(--ink-soft);
}
.pg.bleed .strip figcaption{
  margin-top:auto;                       /* 推到纸栏底部 */
  font-family:var(--kai);font-size:calc(11px * var(--u));
  letter-spacing:.2em;color:var(--ink-soft);
}
/* 页码收进纸栏。.folio 预设是 left:0;right:0 横跨整页居中,那会把它摆到
   照片上 —— 这张照片底部是浅色花瓣,白字加阴影实测仍然读不出来。
   收进纸栏之后它落在裸纸上,用原本的墨色就够,而且这正是纸本书页码
   该在的地方(书口侧的页边)。宽度跟纸栏对齐,text-align:center 自然居中。*/
.pg.bleed ~ .folio{
  left:0;right:auto;width:calc(40px * var(--k));
}
.face.back .pg.bleed .strip{left:auto;right:0}
.face.back .pg.bleed ~ .folio{left:auto;right:0}

/* ── 满版插页 ────────────────────────────────────────────────
   整本书唯一一张不做成拍立得的图。前面每一页照片都有白框跟纸胶带,
   这一页突然满出纸缘 —— 节奏被打断的那一下就是它要的份量。

   .pg 的内距得整个拿掉,图才铺得到纸的边;figure 走绝对定位撑满,
   否则 .pg 的直排 flex 会把它压回内容尺寸。
   纸纹与书沟的阴影盖在上面(.face::before 是 z-index:3),所以满版图
   照样吃得到书页的弧面 —— 这是刻意留的,不要把它压下去。          */
/* .face.back .pg 那条(三个 class)权重压过 .pg.plate,左右内距会被它塞回来。
   现在里头全是绝对定位所以看不出差别,但留着迟早出事 —— 明写压过去。 */
.pg.plate, .face.back .pg.plate{padding:0}
.pg.plate figure{position:absolute;inset:0;margin:0}
.pg.plate .shot{position:absolute;inset:0;overflow:hidden;background:#0B0A12}
.pg.plate .shot img{
  width:100%;height:100%;object-fit:cover;display:block;
  object-position:var(--focus, 50% 50%);
}
.pg.plate .shot:has(img){cursor:zoom-in}
/* 下缘压一层渐层。这张图的底部是草地跟花瓣,亮得很 —— 浅色字直接
   压上去读不出来,光靠 text-shadow 撑不住。 */
.pg.plate figure::after{
  content:"";position:absolute;left:0;right:0;bottom:0;height:40%;
  background:linear-gradient(to top,
    rgba(8,6,16,.84), rgba(8,6,16,.44) 46%, rgba(8,6,16,0));
  pointer-events:none;
}
/* 浅色照片要更重的渐层。量过说明文字那一条(距底 48~72px)的背景亮度:
   直播间那张(淡紫棚拍、底部是白羽毛与浅粉花瓣)套预设渐层只有 2.50:1,
   加到这一档是 4.50:1 —— 对齐青龙那页的 5.88:1(那页使用者没有意见,
   它底部本来就是暗的草地,不必加重,加了反而压掉草)。
   变暗的范围大约从整页 57% 往下,那里是裙摆、羽毛与地板,不是重点。 */
.pg.plate.scrim-strong figure::after{
  height:52%;
  background:linear-gradient(to top,
    rgba(8,6,16,.96), rgba(8,6,16,.64) 46%, rgba(8,6,16,0));
}
.pg.plate figcaption{
  position:absolute;left:0;right:0;bottom:calc(48px * var(--u));
  padding:0 calc(30px * var(--u));z-index:2;
  font-family:var(--kai);font-size:calc(15px * var(--u));
  line-height:1.9;letter-spacing:.1em;text-align:center;color:#F6DACB;
  /* 三层:贴身的硬阴影当描边,中层与外层是晕开的底。字幕就是这样做的 ——
     只靠渐层压背景的话,要压到很暗字才读得出来,那会把照片一起毁掉。 */
  text-shadow:
    0 calc(1px * var(--u)) calc(2px  * var(--u)) rgba(0,0,0,.92),
    0 0                    calc(10px * var(--u)) rgba(0,0,0,.78),
    0 0                    calc(24px * var(--u)) rgba(0,0,0,.55);
}
/* 页码印在满版图上要换浅色。.folio 是 .pg 的兄弟且排在它后面,
   所以用后继兄弟选择器就够,不必等 :has 的支援。 */
.pg.plate ~ .folio{color:rgba(246,218,203,.72)}

/* 扉页 */
.frontis{justify-content:center;text-align:center;gap:calc(6px * var(--u))}
.frontis .ded{font-family:var(--kai);font-size:calc(16px * var(--u));letter-spacing:.16em;color:var(--ink)}
.frontis .big{
  font-family:var(--latin);font-weight:300;font-size:calc(62px * var(--u));
  line-height:1;color:var(--thread);margin:calc(10px * var(--u)) 0 calc(2px * var(--u));
  font-variant-numeric:tabular-nums;
}
.frontis .cap{font-size:calc(12.5px * var(--u));letter-spacing:.24em;color:var(--ink-soft)}
/* 起算日 —— 静的锚,压在会跳动的大数字上面 */
.frontis .from{
  font-family:var(--kai);font-size:calc(12.5px * var(--u));
  letter-spacing:.16em;color:var(--ink-soft);margin-bottom:calc(2px * var(--u));
}
/* 年月日换算:同一件事的第二种说法,所以比 .cap 再退一阶 */
.frontis .cap-ymd{
  font-size:calc(11.5px * var(--u));letter-spacing:.2em;
  opacity:.72;margin-top:calc(6px * var(--u));
}
/* 那两句 —— 全页唯一在讲「我们是什么关系」的地方 */
.frontis .creed{
  margin-top:calc(26px * var(--u));font-family:var(--kai);
  font-size:calc(14px * var(--u));line-height:2.1;color:var(--ink);letter-spacing:.1em;
}
.frontis .creed + .say{margin-top:calc(18px * var(--u))}
.frontis .say{
  margin-top:calc(30px * var(--u));font-family:var(--kai);
  font-size:calc(14.5px * var(--u));line-height:2;color:#3A4362;letter-spacing:.08em;
}

/* 目录 —— 红线把每一章串起来 */
.toc ol{
  list-style:none;margin-top:calc(6px * var(--u));position:relative;
  /* 圆点与红线共用这两个变数,两者的中心都是 --dot-x + --dot/2,
     所以**任何 --u 都必然同心**。
     先前是各写各的(圆 left:2px*u 宽 7px*u,线 left:5px*u 宽 1px),
     偏差 = 0.5(u-1) —— 只有 --u 刚好等于 1 时才对齐。我第一次量的时候
     视窗是 390px、--u 正好 1.000,所以量出 0.00px,纯属巧合;屏幕一大
     --u > 1 偏移就出现,而 1px 的线落在小数位置还会被反锯齿糊成两个像素。*/
  --dot:   calc(7px * var(--u));
  --dot-x: calc(2px * var(--u));
}
/* 连接段:画在圆与圆之间,**不穿过圆点**。每一段的位置与长度由
   app.js 的 fitTocLine() 量出来设(--seg-top / --seg-h)——
   列高、字级、--u 怎么变都不用再调。
   left 与圆点共用同一个式子,所以必然同心(见下方 --dot / --dot-x)。 */
.toc li{margin:calc(11px * var(--u)) 0;position:relative}
.toc li::after{
  content:"";position:absolute;
  left:calc(var(--dot-x) + var(--dot) / 2 - var(--line-w, 1px) / 2);
  top:var(--seg-top, 0px);height:var(--seg-h, 0px);
  width:var(--line-w, 1px);background:var(--thread);
  pointer-events:none;
}
.toc button{
  appearance:none;background:none;border:0;cursor:pointer;font:inherit;text-align:left;
  padding:calc(3px * var(--u)) 0 calc(3px * var(--u)) calc(24px * var(--u));
  position:relative;color:var(--ink);width:100%;
  font-family:var(--kai);font-size:calc(14.5px * var(--u));letter-spacing:.08em;
  transition:.2s;
}
.toc button::before{
  /* box-sizing 必须明写:reset 的 * 选择器**不匹配伪元素**,::before 预设是
     content-box,width 不含 border —— 圆实际画出来比 --dot 宽 2px,视觉中心
     比「left + --dot/2」偏右 1px 整。这就是「线不在圆心」的真正原因:
     线的位置按 --dot/2 算,圆却按 content+border 画。我用探针量了四次都量成
     0,因为探针没有 border;放大测试也没抓到,因为 border 不跟着放大。 */
  box-sizing:border-box;
  content:"";position:absolute;left:var(--dot-x);top:50%;transform:translateY(-50%);
  width:var(--dot);height:var(--dot);border-radius:50%;
  /* 不填色 —— 填了纸色的话线整段被圆盖住,看起来像线没穿过圆心,
     只是从圆的下缘才开始。留空心才看得见线从上缘穿到下缘。 */
  background:transparent;border:1px solid var(--thread);
}
.toc button:hover{color:var(--thread)}
.toc button:hover::before{background:var(--thread)}
.toc button:focus-visible{outline:2px solid var(--thread);outline-offset:2px}
.toc .yr{font-family:var(--latin);font-size:calc(11.5px * var(--u));color:var(--ink-soft);margin-left:.6em}
/* 还没写的章节:同一条红线串下来,但圆点是空心虚线的,字也退一阶 ——
   看得出来「还有」,又看得出来「还没到」。 */
.toc .pending span, .sheet .pending span{
  display:block;padding:calc(3px * var(--u)) 0 calc(3px * var(--u)) calc(24px * var(--u));
  font-family:var(--kai);font-size:calc(13.5px * var(--u));letter-spacing:.16em;
  color:var(--ink-soft);opacity:.75;position:relative;
}
.toc .pending span::before, .sheet .pending span::before{
  box-sizing:border-box;               /* 理由同上面章节圆点 */
  content:"";position:absolute;left:var(--dot-x);top:50%;transform:translateY(-50%);
  width:var(--dot);height:var(--dot);border-radius:50%;
  background:transparent;border:1px dashed var(--thread);opacity:.7;
}

/* 后记 / 尾页 */
.center{justify-content:center;text-align:center}
.center .lead{font-family:var(--kai);font-size:calc(20px * var(--u));letter-spacing:.14em;line-height:1.9;color:var(--ink)}
.center .sub{margin-top:calc(16px * var(--u));font-size:calc(14px * var(--u));line-height:2;color:#3A4362}
.blank{background:linear-gradient(120deg,var(--paper),var(--paper-2))}

/* ── 红线书签(进度) ─────────────────────────────────────── */
.ribbon{
  position:absolute;top:0;right:calc(var(--pw) * 0.12);width:calc(9px * var(--u));
  height:calc(52px + (var(--p,0) * 150px));
  background:linear-gradient(180deg,#93283F,var(--thread));
  z-index:60;pointer-events:none;
  transition:height var(--flip) var(--ease);
  box-shadow:0 2px 10px rgba(0,0,0,.3);
}
.ribbon::after{
  content:"";position:absolute;bottom:calc(-8px * var(--u));left:0;right:0;height:calc(9px * var(--u));
  background:var(--thread);
  clip-path:polygon(0 0,100% 0,100% 100%,50% 55%,0 100%);
}
/* 书签挂在右半边(right: --pw*.12)。closed 时书是合上的,ended 时右半边
   根本没有纸 —— 两个边界都不该让它飘在那里。 */
.book.closed .ribbon,
.book.ended  .ribbon{opacity:0}

/* ── 底部导览 ─────────────────────────────────────────────── */

/* ── 目录浮层(手机用) ───────────────────────────────────── */
.sheet{
  position:fixed;inset:0;z-index:200;display:none;place-items:center;
  background:rgba(8,14,26,.92);padding:24px;      /* 原本 .82 + blur(6px),同 .gate 的理由 */
}
.sheet.on{display:grid}
.sheet .card{
  background:var(--paper);color:var(--ink);max-width:420px;width:100%;
  padding:30px 26px;box-shadow:0 30px 70px rgba(0,0,0,.5);
  max-height:80vh;overflow:auto;
}
.sheet .card h3{font-family:var(--kai);font-weight:400;font-size:20px;letter-spacing:.18em;margin-bottom:6px}

/* ── 音乐:按键上的呼吸点 + 曲名浮字 ─────────────────────── */
.btn .dot{
  display:inline-block;width:6px;height:6px;border-radius:50%;
  background:var(--thread);margin-right:8px;vertical-align:1px;opacity:.25;
  transition:opacity .4s;
}
.btn[aria-pressed="true"] .dot{opacity:1;animation:pulse 2.6s ease-in-out infinite}
@keyframes pulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(.6);opacity:.45}}
@media (prefers-reduced-motion:reduce){.btn[aria-pressed="true"] .dot{animation:none}}

/* left:0;right:0;margin-inline:auto 而不是 left:50% + translate(-50%) ——
   后者排版时可用宽度只剩「50% 到右边缘」那一半(375px 的手机上就是 187.5px),
   于是曲名在明明塞得下的情况下被迫折行,而且中文可以在任意两字之间断开,
   断点还很难看(「这世界那 / 么多人」)。translate 是排版完才做的位移,
   救不了这件事,max-width 也因此从来没起过作用。                        */
.toast{
  position:fixed;left:0;right:0;margin-inline:auto;width:fit-content;
  bottom:calc(84px + env(safe-area-inset-bottom,0px));
  transform:translateY(10px);z-index:150;pointer-events:none;
  /* 真的必须折行时,只在空白 / 标点处断(keep-all),中文词不会被拦腰切开;
     万一单一段落长到一行放不下,overflow-wrap 再放行让它断,不会溢出画面。*/
  word-break:keep-all;overflow-wrap:anywhere;
  padding:9px 20px;border:1px solid var(--ui-line);border-radius:999px;
  background:var(--toast-bg);backdrop-filter:blur(8px);
  font-family:var(--kai);font-size:13.5px;letter-spacing:.18em;color:var(--ui-hot);
  opacity:0;transition:opacity .45s ease,transform .45s var(--ease);
}
.toast.on{opacity:1;transform:translateY(0)}

/* 开场报曲名:拉到画面正中央、字放大。只用在这一次,不给主题提示用。 */
.toast.now{
  bottom:auto;top:50%;
  /* 纯淡入淡出:位移与缩放都不动,曲名就定在正中央慢慢浮现、慢慢化开。
     顺带一个好处 —— 过渡途中位置也是精准置中,不会飘。 */
  transform:translateY(-50%);
  /* 淡入淡出各 .55s:总在画面上的时间 = .55 + 停留 + .55。
     原本进出各 1s、停留 4.2s,加起来 6 秒,报个曲名太久了。 */
  transition:opacity .55s ease;
  padding:15px 34px;font-size:18px;letter-spacing:.22em;
  max-width:calc(100vw - 40px);
  box-shadow:0 18px 50px rgba(0,0,0,.45);
}
.toast.now.on{transform:translateY(-50%)}
@media (max-width:520px){
  .toast.now{padding:13px 22px;font-size:15px;letter-spacing:.16em}
}


/* ── 进场的门:同时也是拿到播放权的那一次点击 ──────────────── */
/* ⚠ 这几层**刻意不用 backdrop-filter**,不是忘了写。

   三星浏览器上实测会画坏:进场那一层原本是 .gate(position:fixed; inset:0;
   backdrop-filter:blur(3px))里面再套一个 .panel(blur(14px))—— 两层
   backdrop-filter 叠在一起,而它们取样的底下正好是一棵 3D 合成子树
   (.book 有 perspective:2200px,.leaf 有 transform-style:preserve-3d,
   .face 有 backface-visibility:hidden)。这个组合会让 backdrop 缓冲区
   取样到错的尺寸与位置,画面变成:书只露出一小块、其余整片黑,
   面板被切成一个偏移的方块。使用者回报「感觉像坏掉」就是这个。

   移掉滤镜之后画面差别很小 —— 底下那层本来就压了 58%~88% 的暗色,
   「往后退」的效果是暗色给的,不是模糊给的。这里把暗色再加一点补回来。

   小元件(.toast 那颗药丸)不在此列:它不叠在 3D 子树上,而且那点玻璃感
   是它的样子。真要再出问题,它是下一个该拆的。                      */
.gate{
  position:fixed;inset:0;z-index:300;display:grid;place-items:center;padding:24px;
  background:rgba(8,5,10,.74);           /* 原本 .58 + blur(3px) */
  transition:opacity .75s ease, visibility .75s;
}
/* visibility 走离散插值:visible → hidden 这 .75s 之内值仍是 visible,
   满版的它照样接走每一次点击。关闭态一定要连 pointer-events 一起关。 */
.gate.off{opacity:0;visibility:hidden;pointer-events:none}
.gate .panel{
  width:100%;max-width:330px;text-align:center;
  padding:34px 28px 28px;
  /* 不透明。原本是 var(--toast-bg)(84%) + blur(14px);模糊拿掉之后
     留着半透明会让底下的封面纹路透进字里,索性做实。 */
  background:#140C1F;border:1px solid var(--ui-line);
  box-shadow:0 26px 60px rgba(0,0,0,.45);
}
.gate .ask{font-size:12.5px;letter-spacing:.2em;color:var(--ui-dim)}
.gate .song{
  font-family:var(--kai);font-size:24px;letter-spacing:.16em;color:var(--ui-hot);
  margin:14px 0 4px;
}
.gate .hr{height:1px;width:44px;margin:20px auto 22px;background:var(--ui-line)}
.gate .btn{display:block;width:100%;padding:12px 0;font-size:13.5px;letter-spacing:.16em}
.gate .btn + .btn{margin-top:10px}
.gate .btn.primary{
  border-color:var(--ui-hot);color:var(--ui-hot);background:var(--ui-wash);
}
.gate .fine{margin-top:18px;font-size:11px;letter-spacing:.12em;color:var(--ui-dim);opacity:.75}

.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* ── 正文打字机 ─────────────────────────────────────────────
   每个字一个 .tw,一开始就排好位置,动的只有 opacity —— 版面全程不动。

   **失效时必须看得见**:这里没有任何一条规则把 .tw 设成 opacity:0。
   隐藏是靠动画的 fill-mode:both 在延迟期间套用 from 状态做到的,
   所以只要 .typing 没有被加上去(JS 没跑、动画被停用、旧浏览器),
   文字就是完整可见的 —— 不会出现空白页。                        */
@keyframes tw-in{ from{opacity:0} to{opacity:1} }
.pg.typing .tw,
.face.typing .tw{
  animation:tw-in .18s ease both;
  /* --tw-lead 是整页的起打延迟(等翻页落定),--i * --tw-step 是每个字的错开 */
  animation-delay:calc(var(--tw-lead, 0ms) + var(--i, 0) * var(--tw-step, 42ms));
}
/* 不想看动画的人直接给全文 */
@media (prefers-reduced-motion:reduce){
  .pg.typing .tw, .face.typing .tw{ animation:none }
}

/* ── 以下取代原本写在 JS 样板里的 inline style ──────────────────
   CSP 的 style-src 不开 'unsafe-inline' 之后,HTML 属性上的 style="…"
   会被浏览器直接挡掉,所以这 8 处全部改成 class。
   注意:JS 用 el.style.setProperty() 设的样式走 CSSOM,不受 CSP 管制 ——
   主题切换、书本尺寸、翻页 z-index 那些动态计算完全不用改。      */
.frontis .big.big-2{font-size:calc(38px * var(--u));margin-top:calc(22px * var(--u))}
.pg.pg-mid{justify-content:center}
.polaroid.tilt-r{--tilt:1.8deg}
.polaroid.tilt-l{--tilt:-1.8deg}
.rule.rule-c{width:38%;margin-left:auto;margin-right:auto}
.center .sub.sub-tight{margin-top:calc(10px * var(--u))}
.since{font-family:var(--latin);letter-spacing:.16em}
/* 起算日底下那道竖线,取代原本跟在日期后面的破折号。
   往下渐隐、不收口 —— 日期是起点,后面还没有终点。
   写法与 .rule 同源,只把 90deg 转成 180deg,视觉语汇不另开一套。 */
.since-on{
  display:block;width:1px;height:calc(34px * var(--u));
  margin:calc(9px * var(--u)) auto 0;
  background:linear-gradient(180deg,var(--thread),rgba(196,63,92,0));
}
.again-wrap{margin-top:calc(26px * var(--u))}
.btn.btn-thread{color:var(--thread);border-color:rgba(196,63,92,.4)}


/* ── 照片封面(只有 STORY.cover.src 填了才会出现)─────────────
   照片铺满封面,上面压一层遮罩。遮罩刻意做成上下重、中间轻 ——
   要护住的是顶端的引题书名和底部的署名,中段留给照片本身。      */
.cover.has-photo{background:none}
.cover-photo{
  position:absolute;inset:0;z-index:0;width:100%;height:100%;
  object-fit:var(--cover-fit,cover);object-position:var(--cover-focus,50% 50%);
}
.cover-scrim{
  position:absolute;inset:0;z-index:1;
  background:linear-gradient(180deg,
    rgba(18,6,11,.92) 0%, rgba(18,6,11,.42) 34%,
    rgba(18,6,11,.48) 62%, rgba(18,6,11,.95) 100%);
  opacity:var(--cover-dim,.45);
}
/* 只给 z-index,不动既有定位 —— .frame 和 .open-hint 本来就是 absolute,
   动了 position 会让它们塌回文档流。                              */
.cover.has-photo .frame,
.cover.has-photo .open-hint{z-index:2}
.cover.has-photo .eyebrow,
.cover.has-photo h1,
.cover.has-photo .emblem,
.cover.has-photo .names{position:relative;z-index:2}
/* 烫金字压在照片上容易糊,补一道暗影把边缘咬出来 */
.cover.has-photo h1{filter:drop-shadow(0 calc(2px * var(--u)) calc(6px * var(--u)) rgba(0,0,0,.72))}
.cover.has-photo .eyebrow,
.cover.has-photo .names,
.cover.has-photo .open-hint{text-shadow:0 1px calc(4px * var(--u)) rgba(0,0,0,.85)}

/* ── 进场第二页:耳机提醒 ───────────────────────────────── */
.gate .hp-icon{color:var(--ui-hot);margin-bottom:calc(10px);opacity:.9}
.gate .fine.hp-sub{margin-top:8px;opacity:.95}
.gate .panel.swap{animation:panel-swap .5s var(--ease) both}
@keyframes panel-swap{
  from{opacity:0;transform:translateY(8px)}
  to  {opacity:1;transform:none}
}
@media (prefers-reduced-motion:reduce){
  .gate .panel.swap{animation:none}
}

/* ── 整张图就是封面(STORY.cover.full)──────────────────────
   图上已经有书名与署名,引擎那层文本整个不出场。
   这张封面比书页略宽(1:1.29 对 1:1.42),用 contain 完整保留那圈
   烫金外框,上下留边补图片自己的边缘色,接缝看不出来。        */
.cover.cover-full{background:var(--cover-pad,#06030F);padding:0}

/* ── 拖曳翻页 ──────────────────────────────────────────────
   拖曳期间关掉 transition,纸才跟得住手指;放开后把 transform
   交还给 class,transition 会从当前角度自然接续过去。       */
.leaf.dragging{transition:none;will-change:transform}
.book.dragging{cursor:grabbing}

/* 拿掉底部按钮之后,进书第一页给一次提示,翻过一次就不再出现 */
.hint{
  flex:0 0 auto;text-align:center;
  padding:16px 18px calc(20px + env(safe-area-inset-bottom,0px));
  font-size:12.5px;letter-spacing:.18em;color:var(--ui-dim);
  opacity:0;transition:opacity .8s ease;pointer-events:none;
}
.hint.on{opacity:.75}

/* ── 翻页的明暗 ────────────────────────────────────────────
   真书之所以像真书是光影不是几何:纸转向侧面时受光面积变小而变暗,
   转过去之后另一面才慢慢亮起来。

   刻意不用 @property 转 --turn:自订属性若 inherits 才传得到子层,
   而那会让翻动中那张纸的每一个后代元素每帧重算样式。这里改成让遮罩
   自己 transition opacity,一张纸只有两个元素在动;拖曳时由 JS 直接
   写 opacity,精准跟手。
   靠书沟那侧压得更重,读起来像纸被压出的折痕。               */
.turn-shade{
  position:absolute;inset:0;z-index:5;pointer-events:none;
  background:linear-gradient(90deg,
    rgba(24,10,18,.72) 0%, rgba(24,10,18,.18) 34%,
    rgba(24,10,18,.12) 68%, rgba(24,10,18,.44) 100%);
  transition:opacity var(--flip) var(--ease);
}
.face.back .turn-shade{
  background:linear-gradient(270deg,
    rgba(24,10,18,.72) 0%, rgba(24,10,18,.18) 34%,
    rgba(24,10,18,.12) 68%, rgba(24,10,18,.44) 100%);
}
.face.front .turn-shade{opacity:0}
.face.back  .turn-shade{opacity:.85}
.leaf.flipped .face.front .turn-shade{opacity:.85}
.leaf.flipped .face.back  .turn-shade{opacity:0}
.leaf.dragging .turn-shade{transition:none}   /* 拖曳时交给 JS 逐帧写值 */

/* ── 页缘卷曲 ──────────────────────────────────────────────
   真正的翻页不是一整片刚硬的板在转 —— 靠外侧那一段会卷起来,
   卷脊受光最亮,卷进去的凹处最暗,纸背在边缘露出一条。

   刻意用 scaleX 而不是改 width:改 width 每帧重排,scaleX 走合成器。
   翻到一半时最宽,两端收成 0 —— 跟真纸一样,刚离手和快贴上时是平的。*/
.curl{
  position:absolute;top:0;bottom:0;right:0;width:13%;
  z-index:6;pointer-events:none;
  transform-origin:right center;transform:scaleX(0);
  background:linear-gradient(90deg,
    rgba(22,9,16,.38)   0%,
    rgba(255,241,234,.26) 30%,
    rgba(255,249,244,.70) 56%,
    rgba(196,166,156,.52) 80%,
    rgba(88,58,64,.60)  100%);
  border-radius:0 calc(9px * var(--u)) calc(9px * var(--u)) 0;
  transition:transform var(--flip) var(--ease);
}
.face.back .curl{
  right:auto;left:0;transform-origin:left center;
  background:linear-gradient(270deg,
    rgba(22,9,16,.38)   0%,
    rgba(255,241,234,.26) 30%,
    rgba(255,249,244,.70) 56%,
    rgba(196,166,156,.52) 80%,
    rgba(88,58,64,.60)  100%);
  border-radius:calc(9px * var(--u)) 0 0 calc(9px * var(--u));
}
/* 点击 / 键盘翻页走这条:两端为 0、中间最宽 */
@keyframes curl-peak{
  0%{transform:scaleX(0.000)}
  12.5%{transform:scaleX(0.069)}
  25%{transform:scaleX(0.284)}
  37.5%{transform:scaleX(0.728)}
  50%{transform:scaleX(0.965)}
  62.5%{transform:scaleX(0.510)}
  75%{transform:scaleX(0.185)}
  87.5%{transform:scaleX(0.039)}
  100%{transform:scaleX(0.000)}
}
.leaf.turning .curl{animation:curl-peak var(--flip) linear}
/* 拖曳时交给 JS 逐帧写值,精准跟手 */
.leaf.dragging .curl{transition:none;animation:none}
@media (prefers-reduced-motion:reduce){ .curl{display:none} }

/* ── 纸面弧度 ──────────────────────────────────────────────
   真书翻页时纸不是刚硬的平板,它会拱起来。但把内容真的弄弯,得把纸
   切成数十条薄片、每条各复制一份页面内容 —— DOM 暴增,跟顿卡冲突。

   关键在于:纸拱起来时你看到的其实是它的侧影。所以弯的是「轮廓」和
   「受光」,不是内容。clip-path 把外缘收成一道弧,上下两角收得最多、
   中间不收,余弦取样所以过渡是平滑的;翻到一半时弧度最深,两端收平。

   下面两个多边形的点数必须相同,clip-path 才能在两者之间过渡。

   刻意不给 .face 一个常驻的 clip-path —— 那会让 18 个面永远挂着裁切
   路径,平常没在翻也要付合成成本。@keyframes 的 0% 已经提供起始值。 */
@keyframes page-bow{
  0%{clip-path:polygon(0 0, 100.0% 0%, 100.0% 20%, 100.0% 50%, 100.0% 80%, 100.0% 100%, 0 100%)}
  12.5%{clip-path:polygon(0 0, 99.5% 0%, 99.8% 20%, 100.0% 50%, 99.8% 80%, 99.5% 100%, 0 100%)}
  25%{clip-path:polygon(0 0, 98.0% 0%, 99.2% 20%, 100.0% 50%, 99.2% 80%, 98.0% 100%, 0 100%)}
  37.5%{clip-path:polygon(0 0, 94.9% 0%, 97.9% 20%, 100.0% 50%, 97.9% 80%, 94.9% 100%, 0 100%)}
  50%{clip-path:polygon(0 0, 93.2% 0%, 97.2% 20%, 100.0% 50%, 97.2% 80%, 93.2% 100%, 0 100%)}
  62.5%{clip-path:polygon(0 0, 96.4% 0%, 98.5% 20%, 100.0% 50%, 98.5% 80%, 96.4% 100%, 0 100%)}
  75%{clip-path:polygon(0 0, 98.7% 0%, 99.5% 20%, 100.0% 50%, 99.5% 80%, 98.7% 100%, 0 100%)}
  87.5%{clip-path:polygon(0 0, 99.7% 0%, 99.9% 20%, 100.0% 50%, 99.9% 80%, 99.7% 100%, 0 100%)}
  100%{clip-path:polygon(0 0, 100.0% 0%, 100.0% 20%, 100.0% 50%, 100.0% 80%, 100.0% 100%, 0 100%)}
}
/* linear —— 取样点里已经把 --ease 算进去了,再套一次会 ease 两遍 */
.leaf.turning .face{animation:page-bow var(--flip) linear}
.leaf.dragging .face{animation:none}   /* 拖曳时交给 JS 逐帧写值 */

/* 曲面受光:纸拱起来时,拱脊迎光最亮,两侧背光。这道高光跟着弧度一起
   进出,是「看起来是弯的」而不只是「边缘被切掉了」的关键。      */
.face-sheen{
  position:absolute;inset:0;z-index:4;pointer-events:none;opacity:0;
  background:linear-gradient(90deg,
    rgba(255,252,248,0) 0%, rgba(255,252,248,.30) 38%,
    rgba(255,250,244,.46) 56%, rgba(255,252,248,.12) 74%,
    rgba(255,252,248,0) 100%);
  transition:opacity var(--flip) var(--ease);
}
.face.back .face-sheen{transform:scaleX(-1)}
.leaf.turning .face-sheen{animation:sheen-peak var(--flip) linear}
@keyframes sheen-peak{
  0%{opacity:0.000}
  12.5%{opacity:0.069}
  25%{opacity:0.284}
  37.5%{opacity:0.728}
  50%{opacity:0.965}
  62.5%{opacity:0.510}
  75%{opacity:0.185}
  87.5%{opacity:0.039}
  100%{opacity:0.000}
}
.leaf.dragging .face-sheen{transition:none;animation:none}

/* ── 放手之后的收尾 ────────────────────────────────────────
   拖到 55% 放手时,只剩 45% 要走。若让 .turning 的 keyframes 接手,
   弧度会先跳回 0、再胀到最满、再收掉 —— 明明纸已经弯到一半了。
   所以放手后进 settling:关掉 keyframes,改用 transition 把三层从
   「当前值」收到 0。这几条必须排在 .turning 之后,同特异度靠顺序决胜。*/
.leaf.settling .face      {animation:none;transition:clip-path var(--flip) var(--ease)}
.leaf.settling .curl      {animation:none;transition:transform var(--flip) var(--ease)}
.leaf.settling .face-sheen{animation:none;transition:opacity   var(--flip) var(--ease)}
@media (prefers-reduced-motion:reduce){
  .face{clip-path:none}
  .leaf.turning .face{animation:none}
  .face-sheen{display:none}
}

/* ── 章节影片 ──────────────────────────────────────────────
   直式片子(9:16)塞进方框会被切掉大半,所以影片用比较高的框。   */
.polaroid.is-video .shot{height:calc(280px * var(--u));cursor:pointer}
.polaroid .shot video{width:100%;height:100%;object-fit:cover;display:block}
.polaroid .shot .play{
  position:absolute;inset:0;margin:auto;
  width:calc(54px * var(--u));height:calc(54px * var(--u));
  padding:0;border-radius:50%;cursor:pointer;
  border:1px solid rgba(255,255,255,.72);
  /* 这颗键直接压在影片上,而影片在 3D 子树里 —— 是整份样式里最危险的
     一个 backdrop-filter 位置。那 2px 模糊本来也几乎看不出来。 */
  background:rgba(24,10,18,.55);                 /* 原本 .42 + blur(2px) */
  transition:opacity .3s ease, background .25s ease, transform .25s ease;
}
.polaroid .shot .play::after{        /* 三角形播放键 */
  content:"";position:absolute;inset:0;margin:auto;width:0;height:0;
  border-left:calc(15px * var(--u)) solid rgba(255,255,255,.92);
  border-top:calc(9px * var(--u)) solid transparent;
  border-bottom:calc(9px * var(--u)) solid transparent;
  transform:translateX(calc(3px * var(--u)));
}
/* 没有 :hover —— 这颗键 pointer-events:none(见下方),永远不会被指标命中,
   hover 规则是死码。:focus-visible 留着,键盘是它唯一的启动路径。 */
.polaroid .shot .play:focus-visible{outline:2px solid var(--thread);outline-offset:3px}
.polaroid.playing .shot .play{opacity:0;pointer-events:none}

/* ── 照片灯箱 ──────────────────────────────────────────────
   翻页锁成拖曳之后,点击就空出来当纯粹的内容互动:影片播放、照片放大。
   拍立得那个框只有两百多 px,照片里的细节看不清。               */
.lens{
  position:fixed;inset:0;z-index:260;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:18px;padding:5vmin;
  background:rgba(10,4,9,.95);                    /* 原本 .88 + blur(6px),同 .gate 的理由 */
  opacity:0;visibility:hidden;cursor:zoom-out;
  pointer-events:none;   /* 同 .gate:关闭态不关命中,淡出那 .45s 会吃掉点击 */
  transition:opacity .45s ease, visibility .45s;
}
.lens.on{opacity:1;visibility:visible;pointer-events:auto}
/* 灯箱开着 -> 整本书从渲染树上拿掉。理由写在 app.js 的 openLens 上面:
   iOS 上「点开照片再两指放大」会把网页行程撑爆当掉,因为立体翻页让每一面
   都各自占一张合成图层,放大会要求全部用更高解析度重画。
   用 display:none 不用 visibility:hidden —— 后者仍会保留图层,
   而 backface-visibility:hidden 与 preserve-3d 正是强制生成图层的元凶。 */
body.lens-on #book{display:none}
.lens-img{
  max-width:100%;max-height:78vh;object-fit:contain;
  box-shadow:0 30px 80px rgba(0,0,0,.6);background:var(--paper);
}
.lens-cap{
  font-family:var(--kai);font-size:15px;letter-spacing:.14em;
  color:var(--ui);text-align:center;max-width:min(90vw,640px);line-height:1.9;
}
.lens-hint{font-size:11px;letter-spacing:.2em;color:var(--ui-dim);opacity:.7}
/* 播放键只是提示,整块 shot 都可以点 —— 见 app.js 的说明 */
.polaroid .shot .play{pointer-events:none}
/* 有照片的框才给放大游标,占位框不给 */
.polaroid:not(.is-video) .shot:has(img){cursor:zoom-in}

/* ── 背面朝外的那一面不参与命中测试 ──────────────────────────
   backface-visibility:hidden 只让元素看不见,不会让它不可点。
   翻过去的纸,它那面朝外的正面仍然叠在上层,照样接走所有点击。

   这就是「点影片播不了」的真正原因:第 3 页的影片在纸的背面,
   而同一张纸的正面是目录页 —— 目录页看不见,却把每一次点击都吃掉。
   前四次修的都是判定逻辑,但事件根本没抵达影片,改再多判定也没用。 */
.leaf.flipped .face.front{pointer-events:none}
.leaf:not(.flipped) .face.back{pointer-events:none}

/* 图片与影片同样不给原生拖放 —— 在 Edge / Chrome 里,从图片上开始拖
   会启动浏览器自己的「拖曳图片」,把翻页手势整个吃掉。            */
.book img, .book video{-webkit-user-drag:none;user-drag:none}
