:root{
  --ink:#e9e3d6;
  --dim:#a29a8c;
  --faint:#6d675e;
  --line:rgba(255,255,255,.15);
  --line2:rgba(255,255,255,.09);
  --panel:rgba(10,12,15,.72);
  --paper:#e9e0cd;
  --bamboo:#8aa47c;
  --red:#b7402f;
  --indigo:#4a6c95;
  --gold:#c9a961;
  /* 三套字，各管一段：
     --serif 正文 / 说明：思源宋，字面比 SimSun 新、比黑体耐看；
     --sans  UI（按钮、标签、数字、参数）：思源黑，小字号不糊；
     --kai   展示字（封面大标题、结果卡、落款印）：楷体，带手工纸和毛笔的笔意，
             只在 20px 以上用，小字不用它。 */
  --serif:"Noto Serif SC","Source Han Serif SC","Songti SC",STSong,SimSun,serif;
  --sans:"Noto Sans SC","Source Han Sans SC","PingFang SC","Microsoft YaHei",sans-serif;
  --kai:KaiTi,STKaiti,"Kaiti SC",楷体,"Noto Serif SC",serif;
}

*{box-sizing:border-box}
html,body{margin:0;height:100%;background:#06070a;color:var(--ink);font-family:var(--serif);
  -webkit-font-smoothing:antialiased;overscroll-behavior:none}
button{font-family:inherit;color:inherit;background:none;border:0;cursor:pointer}
/* 小字号的界面元素（按钮、标签、数字、参数）统一走无衬线，宋体在这个尺寸会糊 */
.chip,.btn,.cap,.node,.pick,.tip,.toast,.gauge,.dial-txt,.pop{font-family:var(--sans)}

/* ---------- 分屏滚动 ---------- */
.deck{height:100dvh;overflow-y:auto;overflow-x:hidden;scroll-snap-type:y mandatory;scrollbar-width:none}
.deck::-webkit-scrollbar{display:none}
.page{position:relative;height:100dvh;display:flex;align-items:center;justify-content:center;
  scroll-snap-align:start;overflow:hidden}
.bglayer{position:absolute;inset:-8%;background-size:cover;background-position:center;
  filter:blur(30px) brightness(.32) saturate(.75);transform:scale(1.05);z-index:0;
  opacity:0;transition:opacity .3s ease}
.page.live .bglayer{opacity:1}

/* ---------- 每屏入场 ----------
   2026-10-04 第五次反馈：「灯光会暗，就看不清了，为什么非要弄变化的灯光」。
   原来这里挂着 scale(1.016) → 1、1.5 秒的推近＋0.85 秒的淡入，每翻一屏画面就自己
   缩一次、亮一次。整条撤掉：画面进来就是定稿的那一帧，只留一段很短的淡入（.32s），
   且不带任何形变。 */
.page .frame{opacity:0;transition:opacity .32s ease}
.page.live .frame{opacity:1}
.page .sheet{opacity:0;transform:translateY(1.2em);
  transition:opacity .7s ease .1s,transform 1s cubic-bezier(.2,.7,.2,1) .1s}
.page.live .sheet{opacity:1;transform:none}

/* ---------- 入场封面 ---------- */
.cover{
  position:fixed;inset:0;z-index:60;overflow:hidden;background:#05060a;
  display:grid;place-items:center;text-align:center;
  animation:cvFadeIn 1s ease both;
}
.cover.out{opacity:0;transform:scale(1.05);pointer-events:none;
  transition:opacity .72s cubic-bezier(.4,0,.2,1),transform .9s cubic-bezier(.4,0,.2,1)}
.cover.gone{display:none}
@keyframes cvFadeIn{from{opacity:0}to{opacity:1}}

.cvfib{position:absolute;inset:0;width:100%;height:100%;opacity:0;
  animation:cvFib 1.6s ease .15s both;pointer-events:none}
@keyframes cvFib{from{opacity:0}to{opacity:.9}}
.cvglow{
  position:absolute;inset:0;pointer-events:none;opacity:0;
  background:
    radial-gradient(60% 42% at 50% -8%, rgba(214,228,255,.20), transparent 70%),
    radial-gradient(120% 90% at 50% 46%, rgba(255,248,232,.05), transparent 72%);
  animation:cvGlow 1.8s ease .1s both;
}
@keyframes cvGlow{from{opacity:0}to{opacity:1}}
.cvsweep{
  position:absolute;top:-40%;left:-60%;width:52%;height:190%;pointer-events:none;
  background:linear-gradient(100deg,transparent,rgba(255,250,235,.13),transparent);
  filter:blur(10px);transform:rotate(14deg);
  animation:cvSweep 1.5s cubic-bezier(.32,.6,.3,1) .45s both;
}
@keyframes cvSweep{from{left:-60%;opacity:0}18%{opacity:1}to{left:118%;opacity:0}}

.cvwrap{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;
  padding:0 6vw}
.cvbrand{display:flex;align-items:center;gap:.7em;opacity:0;
  animation:cvUp .8s cubic-bezier(.2,.7,.2,1) .3s both}
.cvseal{
  width:2.0em;height:2.0em;font-size:clamp(17px,3.4vw,26px);display:grid;place-items:center;
  border:.14em solid var(--red);color:var(--red);border-radius:.12em;font-weight:600;
  font-family:var(--kai);
  transform:scale(1.7) rotate(-6deg);opacity:0;
  animation:cvSeal .5s cubic-bezier(.2,1.5,.4,1) .42s both;
}
.cvmarks{display:flex;flex-direction:column;gap:.28em}
.cvmarks i{display:block;height:1px;background:rgba(233,227,214,.5);opacity:0;
  animation:cvUp .5s ease .5s both}
.cvmarks i:nth-child(1){width:2.6em}
.cvmarks i:nth-child(2){width:2.0em;animation-delay:.56s}
.cvmarks i:nth-child(3){width:1.4em;animation-delay:.62s}
@keyframes cvSeal{from{transform:scale(1.7) rotate(-6deg);opacity:0}to{transform:scale(1) rotate(-3deg);opacity:1}}

.cvtitle{
  margin:.5em 0 0;font-size:clamp(30px,7.6vw,64px);letter-spacing:.16em;font-weight:600;
  font-family:var(--kai);
  color:#f2ecdf;text-shadow:0 6px 40px rgba(0,0,0,.8),0 0 60px rgba(255,240,210,.12);
  opacity:0;animation:cvTitle 1.1s cubic-bezier(.2,.7,.2,1) .6s both;
}
@keyframes cvTitle{
  from{opacity:0;transform:translateY(.4em) scale(.985);filter:blur(6px)}
  to{opacity:1;transform:none;filter:none}
}
.cvsub{margin:.85em 0 0;font-size:clamp(11px,2.4vw,15px);letter-spacing:.42em;color:var(--dim);
  opacity:0;animation:cvUp .8s ease .95s both}
.cvsteps{
  list-style:none;display:flex;flex-wrap:wrap;justify-content:center;gap:.34em;
  margin:1.5em 0 0;padding:0;max-width:min(760px,86vw);
}
.cvsteps li{
  font-size:clamp(11px,2.2vw,14px);letter-spacing:.1em;color:var(--faint);
  padding:.3em .7em;border:1px solid var(--line2);border-radius:999px;
  background:rgba(8,10,13,.5);opacity:0;
  animation:cvUp .6s cubic-bezier(.2,.7,.2,1) both;
}
.cvsteps li:nth-child(1){animation-delay:1.02s}.cvsteps li:nth-child(2){animation-delay:1.07s}
.cvsteps li:nth-child(3){animation-delay:1.12s}.cvsteps li:nth-child(4){animation-delay:1.17s}
.cvsteps li:nth-child(5){animation-delay:1.22s}.cvsteps li:nth-child(6){animation-delay:1.27s}
.cvsteps li:nth-child(7){animation-delay:1.32s}.cvsteps li:nth-child(8){animation-delay:1.37s}
.cvsteps li:nth-child(9){animation-delay:1.42s}
/* 十五个环节（2026-10-04 由九改十五）：多出来的六颗接着往下排 */
.cvsteps li:nth-child(10){animation-delay:1.47s}.cvsteps li:nth-child(11){animation-delay:1.52s}
.cvsteps li:nth-child(12){animation-delay:1.57s}.cvsteps li:nth-child(13){animation-delay:1.62s}
.cvsteps li:nth-child(14){animation-delay:1.67s}.cvsteps li:nth-child(15){animation-delay:1.72s}
@keyframes cvUp{from{opacity:0;transform:translateY(.7em)}to{opacity:1;transform:none}}

.cvgo{
  margin:2em 0 0;padding:.72em 2.4em;font-size:clamp(14px,2.6vw,17px);letter-spacing:.26em;
  border:1px solid rgba(201,169,97,.55);border-radius:.3em;color:#f0e2c0;
  background:rgba(60,48,26,.5);opacity:0;
  animation:cvUp .8s cubic-bezier(.2,.7,.2,1) 1.5s both;
  transition:background .2s,border-color .2s,box-shadow .2s;
}
.cvgo:hover{background:rgba(96,76,38,.78);border-color:rgba(201,169,97,.95);
  box-shadow:0 0 0 1px rgba(201,169,97,.35), 0 8px 26px rgba(0,0,0,.5)}
.cvgo:active{background:rgba(74,58,30,.9)}
.cvhint{margin:1.1em 0 0;font-size:clamp(10px,2vw,13px);letter-spacing:.2em;color:var(--faint);
  opacity:0;animation:cvUp .8s ease 1.7s both}
.cvfoot{
  position:absolute;left:0;right:0;bottom:5%;font-size:clamp(9px,1.7vw,11px);
  letter-spacing:.5em;color:rgba(233,227,214,.22);opacity:0;
  animation:cvUp .9s ease 1.85s both;
}
body.cover-on .deck{overflow:hidden}
.cover.out .cvwrap{transform:scale(.985);transition:transform .9s cubic-bezier(.4,0,.2,1)}

@media (prefers-reduced-motion:reduce){
  .cvfib,.cvglow,.cvsweep,.cvbrand,.cvseal,.cvmarks i,.cvtitle,.cvsub,.cvsteps li,.cvgo,.cvhint,.cvfoot{
    animation-duration:.01ms !important;animation-delay:0s !important;opacity:1 !important;transform:none !important;
  }
  .page .frame,.page .sheet,.page .bglayer{transition-duration:.01ms}
}

/* ---------- 截图 / 静帧模式：?still=1 直接呈现终态 ---------- */
body.still .cover,body.still .cvfib,body.still .cvglow,body.still .cvbrand,
body.still .cvseal,body.still .cvmarks i,body.still .cvtitle,body.still .cvsub,
body.still .cvsteps li,body.still .cvgo,body.still .cvhint,body.still .cvfoot{
  animation:none !important;opacity:1 !important;transform:none !important;
}
body.still .cvsweep{animation:none !important;opacity:0 !important}
body.still .page .frame,body.still .page .bglayer,body.still .page .sheet{
  opacity:1 !important;transform:none !important;transition:none !important;
}
/* 静帧模式下所有过渡直接取终态，否则截图会停在过渡的起始帧 */
body.still .page .zoom,body.still .page .vdim,
body.still .page .node,body.still .page .shot,
body.still .page .lzone,body.still .page .lfull,body.still .page .lwet,
body.still .page .lstep,body.still .page .sfire,body.still .page .bfire,
body.still .page .bliao{
  transition:none !important;
}
body.still .page .steepcol,body.still .page .steepcol img,body.still .page .steepcol video{
  transition:none !important;
}

.frame{
  --u:min(100vw, 66.66dvh);
  position:relative;
  z-index:1;
  /* 自成一层叠上下文：选竹的竹龄色层（.agecol，mix-blend-mode:color）要混的
     是「这一屏的画面」，不能把身后的氛围底 .bglayer 也卷进来 */
  isolation:isolate;
  width:var(--u);
  height:calc(var(--u) * 1.5);
  overflow:hidden;
  background:#05060a;
  font-size:calc(var(--u) * .037);
  line-height:1.5;
  box-shadow:0 0 0 1px rgba(255,255,255,.05), 0 30px 80px rgba(0,0,0,.6);
}

/* 验收模式：?ui=0 —— 只留素材，隐掉一切 HTML 界面。
   用来核对「哪些字是烧在画面里的」「UI 有没有压住画面内容」，
   出图和 play.js 顶部烧录坐标注释、预览\待办_素材重渲染.md 对得上。 */
body.no-ui .bar,body.no-ui .tip,body.no-ui .hot,body.no-ui .legend,
body.no-ui .lead,body.no-ui .obj,
body.no-ui .gauge,body.no-ui .dial,body.no-ui .dial-txt,body.no-ui .lamp,
body.no-ui .fiber,body.no-ui .panel8,body.no-ui .caps,body.no-ui .pager,
body.no-ui .vdim,body.no-ui .flash,body.no-ui .toast,body.no-ui .snd,
body.no-ui .nyP,body.no-ui .rv{display:none!important}
/* 画面亮度恒定：不挂 filter 过渡，也不在任何状态下改 brightness / contrast。
   用户反复说的「灯光会暗，就看不清了」有一半来自这里 ——
   .shot 原本带 brightness(.99) 的 1 秒过渡，一点击就跳到 brightness(1)＋contrast(1.03)，
   一屏之内画面自己亮一下、又暗回去。现在一律 filter:none。 */
.shot{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;z-index:1;
  filter:none}

/* ---------- 画面层 / 选竹亮边层：同一套推近（同一组 CSS 变量） ----------
   --px/--py/--ps 由 play.js 的 pick() 写在 .frame 上：选中哪根，就以那根为原点推近。
   画面（.zoom）、位图亮边（.culms）、竹龄色（.agecol）三层共用这一条，
   同一套 transform-origin，缩放到哪儿线都还贴着秆边。 */
.zoom,.culms,.agecol{position:absolute;inset:0;z-index:1;
  transform-origin:var(--px,50%) var(--py,48%);transform:scale(var(--ps,1));
  transition:transform 1s cubic-bezier(.2,.7,.2,1);will-change:transform}
/* 点在画面上的「快门」推近也撤掉（2026-10-04 第五次反馈）。
   原来 .frame.hit 会把 .zoom/.culms/.agecol 一起推近 2.8%，620ms 后弹回：
   水沤要左右拖、灰腌要按住擦，一指头一下，画面就跟着一推一收 ——
   用户说的「视频渲染也是，就反复放大缩小」就是这个。
   现在画面只在「选中某根竹」时按 --ps 慢推一次，点、拖、擦都不再动画面。 */
.frame.picked .shot{filter:none}

/* ---------- 01 选竹的曝光补正：只有这一屏偏暗 ----------
   2026-10-04 深夜，用户：「后面其他的都有亮度，第一场景没有了」。
   拿十六屏的 **纯素材**（?ui=0，644×1152，画面区 y93~1059）逐屏量：
     p0 选竹  mean 21.0 / p90 64 / 160 以上 0.0%   ← 整片没有一处高光
     p1 水沤  mean 43.0 / p90 147 / 160 以上 8.4%
     p2 捶打  mean 40.6 / p90 107 / 160 以上 3.0%
     p12 压榨 mean 42.4 / p90 135 / 160 以上 7.7%
   21 轮重渲时 01 是照「别要顶光」去的，光压得太平，跟后面几屏不是一个曝光。
   这里先在**网页层**补一档固定曝光：brightness(2.2) + 一点饱和和反差。乘性亮度只抬
   中间调，背景那片黑几乎不动（p50 7→9），秆身和地面提起来；反差 1.05 是为了别把提亮
   后的暗部拉成灰雾（实测只用 brightness 会发灰）。不打光锥、不加动画、任何状态下数值
   都不变（用户红线：不要顶光、不要忽明忽暗）。
   选这一档的依据（644×1152，含 UI 的同一块画面区）：
     基线 21.8 / p90 62 ｜ 本档 39.8 / p90 126 ｜ 对照 02 水沤 43.0/147、03 捶打 40.6/107、13 压榨 42.4/135
     试过 brightness(2.1) → 38.6/117 偏保守；(2.5)+contrast(1.12) → 42.8/143 高光开始顶到 255。
     最终取 (2.2)+saturate(1.1)+contrast(1.05)：39.8/126，落在 03 与 13 中间。
   ⚠ 挂在 .zoom 上而不是 .shot 上：`.frame.picked .shot{filter:none}` 会在选中竹子的
   那一刻把 .shot 的 filter 抹掉 —— 挂 .shot 上就成了「一点竹画面亮回来」，正是要避免的忽明忽暗。
   ?ui=0 验收模式（body.no-ui）强制回生素材，免得以后拿它量亮度被这一档污染。
   这一档只是补光；要跟后面几屏**一样的光**，得用同一条 libtv 流程重渲 01 底片。 */
.page[data-i="0"] .zoom{filter:brightness(2.2) saturate(1.1) contrast(1.05)}
body.no-ui .zoom{filter:none!important}

/* ---------- 背景反应层（react.js）----------
   夹在素材（.zoom，z1）与压暗层（.vdim，z2）之间，所以「其余压暗」照样压在它上面。
   它是 canvas：只画水、汽、火、灰、浆、纸这些该动的东西，pointer-events:none，
   不吃点击、不挡 UI。用户在 ?ui=0 的纯素材验收模式下也不该看见它。 */
.rx{position:absolute;inset:0;width:100%;height:100%;z-index:2;pointer-events:none}
body.no-ui .rx{display:none!important}

/* ---------- 状态叠层：其余压暗 + 选中光柱 ---------- */
.vdim{
  position:absolute;inset:0;z-index:2;pointer-events:none;opacity:0;
  transition:opacity .55s ease;
  background:linear-gradient(90deg,
    rgba(3,4,7,.34) 0,
    rgba(3,4,7,.34) calc(var(--a,50%) - 11%),
    rgba(3,4,7,.30) var(--a,50%),
    rgba(3,4,7,0) calc(var(--a,50%) + 4%),
    rgba(3,4,7,0) calc(var(--b,50%) - 4%),
    rgba(3,4,7,.30) var(--b,50%),
    rgba(3,4,7,.34) calc(var(--b,50%) + 11%),
    rgba(3,4,7,.34) 100%);
}
.vdim.on{opacity:1}
/* ---------- 转场白闪：撤掉 ----------
   2026-10-04 第五次反馈「为什么非要弄变化的灯光」。元素留着（play.js 里多处调用
   flash(p)），但不再画任何光。点击的回执改由背景反应层（react.js 的水纹、震波、
   汽、灰）＋ .ring / .pop 承担 —— 那些只画在器物的位置上，不是整屏一亮。 */
.flash{position:absolute;inset:0;z-index:9;pointer-events:none;opacity:0}

/* ---------- 交互层 ---------- */
.fx{position:absolute;z-index:3;touch-action:manipulation}
.fx.drag{touch-action:none}
.fx.grab{cursor:grab}
.fx.grab:active{cursor:grabbing}

/* 场景内「点得着」的元素单独一层，压在交互层之上，否则点击会被交互层吃掉 */
.hot{position:absolute;inset:0;z-index:5;pointer-events:none}
.hot .node{pointer-events:auto}

/* 操作条（画面中下部，避开原海报已印的说明卡与底部导航） */
.bar{
  position:absolute;left:4%;right:4%;top:45.2%;height:6.2%;
  z-index:4;display:flex;align-items:center;gap:.4em;
  /* 2026-10-06 修（§71）：操作条整条是**只读**的，可点的只有里面的按钮。
     条身自己是 z-index:4，压在各屏的交互热区 .fx（z-index:3）上面 ——
     条身若照默认 eat 事件，热区被它压住的那一块就点不着、拖不动
     （09 舂淘实测：在条身那一带按下，pointerdown 收不到，淘洗永远是 0 遍）。 */
  pointer-events:none;
}
/* .pick 也是可点的（12 抄纸那条「加料」选项就挂在条里），不写这一条点不动 ——
   条身自己是 pointer-events:none（见下面那段注释）。 */
.bar .btn,.bar input,.bar select,.bar .pick{pointer-events:auto}
/* 选竹那一屏：读数与「砍下」挪到右上角的纯黑区（见 play.js 的 R.bamboo）。
   原来的 top:45.2% 正压在两根秆的身上，按钮的可点区吃掉秆的点击（用户：
   「这里的选择会和竹子的选择冲突」）。右上角实测 mean=2.9 / max=7.3，素材没烧东西。
   再往上提到 7%：993×1244 下选竹那根秆被推近 1.06 倍后秸秆顶到过 y≈195，
   11% 的读数条下沿（y≈192）几乎贴上去；7% 留出约 50px 的余量。
   再高就撞到素材右上角烧录的「01 / 09」页码（帧高 3.6% 处）。 */
/* 第二次挪位（2026-10-04）：7% 那一档虽然躲开了秆身，却横着压过右上角那簇竹叶
   （叶实测占 x50%~74%、y5.5%~21%）。整条改成**右侧竖排**，落进 x69.8%~96.8%、
   y17% 以下的纯背景区（叶在 y>17% 基本为 0，右侧一栏除页码外没烧任何东西）：
   读数一行、已看一行、按钮一行，右对齐。 */
.bar.tr{left:auto;right:3.2%;top:17%;height:auto;width:27%;
  flex-direction:column;align-items:flex-end;gap:.5em}
/* 新增的 04～10 七屏素材是「干净底板」，出图时就留白了上方与左下，
   所以这几屏的操作条放在**上方留白带**（.bar.tp），不压画面正中那件器物。
   旧的九条素材左上角烧着正文（x4%~52%、y7%~23%），那几屏照旧用画面中下部。 */
.bar.tp{top:6.4%;height:auto;flex-wrap:wrap;align-items:center;row-gap:.4em}
/* ★ 2026-10-06 用户「导语和 123 统一一下，包括其他的，每个环节的互动部分，
   则根据具体情况调整」之后：导语一律回到 01 的 y5.3% / w44%（05/06 两屏因底片上有
   汽柱与天窗，走 02 那档的 .narrow 27%），04~10 这七屏的操作条不再一条横贯顶部，
   改成逐屏按底片空场落位 —— .bar.tp 本体只剩 07 碱煮用（右半条，见下面 .tpr），
   其余六屏分别走 .bar.tb（04/05/06/10 右栏）、.bar.tl.tla（08 左栏上段）、
   .bar.tl（09 左栏下段，与 03 同款）。七屏的导语位置从此和 01/02/03 一字不差。 */
/* ★ 2026-10-06 用户第二条批注（点在这条操作条上）「靠右对齐」：这条右半条的
   内容原本是 justify-content:flex-start，三颗读数从 x50% 起排，右侧白白空出
   x81%~96% 一条；三颗按钮又因为不折行一路冲到 108.8%。两处的解法是同一条：
   **每一行都靠右端对齐**（flex-wrap 的每一行各自贴右沿 x96%，与 .tip 的右沿
   45.5%→95.5%、.caps 的右沿同一条线）。下面那条 .acts 只是再让它自己折行。 */
.bar.tp.tpr{left:50%;width:auto;justify-content:flex-end}
/* ★ 2026-10-06 用户批注②（点在三颗按钮上）「换成两行」：07 碱煮的按钮行原本和
   三颗读数挤在同一条 46% 宽的右半条里，「填灰池下碱 添柴 挖料出锅」三颗合计
   58.8%（实测 x50%~108.8%），第三颗「挖料出锅」整颗切在帧外。
   .acts 原本 flex:0 0 auto（不折行、不收缩），这里让**按钮行自己折两行**、
   靠右端对齐：实测折成「填灰池下碱 添柴」/「挖料出锅」两行，右沿收回 x96%。
   只 07 吃这条（.bar.tp.tpr 专属），其余十五屏的 .acts 一个像素不动。 */
.bar.tp.tpr>.acts{flex-wrap:wrap;justify-content:flex-end;max-width:100%;min-width:0}
/* 02 水沤：操作条原来落在帧内 y45.2%（画面正中），四颗读数正压在池里的竹麻与水光上
   （用户 2026-10-04：「这几个可以找空的地方放，别遮挡」）—— 先整条搬进顶部暗墙带。
   再一轮（用户：「ui 要针对每个场景设计，疏密有致」「第一个就和场景很适配」）：
   横排一整条虽然不压内容，但四颗同样宽窄、同一水平线，没有疏密，也和画面不发生关系。
   ?ui=0 纯素材量过的空场：y0%~35% 这条带里 x40%~96% 的 mean 只有 2~5、max≤10
   —— 整块顶部暗墙，素材什么都没画（竹竿梢尖 y36.5% 才从 x56.7% 进来，
   石槽远沿 y36%~40% 才出现）。
   所以读数和 01 选竹一样收成**右边一列**（01 那一列被用户点了名：读数贴着秆的「势」排成竖列），
   一颗一行、右沿对齐在 x95.5%（和下面那条 .tip 的右沿同一条线），
   三颗成组（组内 .5em 的紧档 = 密），「起料」按钮再单独空开一档（.9em = 疏）。
   曾试过按竹竿的斜向做逐级阶梯，用户回「倒也不用强行做成阶梯」——已撤，保持齐整。
   .seg 占着左上（x3.4%~33%、y0.7%~5%）、.legend 在左栏（x3.4%~30.8%、y14% 起），
   这一列贴右栏（x69%~95.5%），三块互不叠。只给 02 用，另外十五屏版面一点不动。
   top 5.2% 让第一颗上方留一格（再往上就撞页面右上角那颗音效按钮，它压在 y1%~5%）。 */
.bar.tb{left:auto;right:4.5%;top:5.2%;height:auto;width:auto;
  flex-direction:column;align-items:flex-end;flex-wrap:nowrap;row-gap:.5em}
.bar.tb>.acts{margin-top:.9em}
/* ★ 10 纸药专用：右栏只有 y5%~20% 是暗的（mean≈3 / max≈11），y20% 往下就进了露出的
   木梁（整栏 y5%~30% 的 mean 50.7 / max 139）。.bar.tb 默认在按钮前空 .9em 一档
   （02 那一屏的「疏」），这一屏空不起 —— 两颗读数贴着起、按钮紧跟，整条压在 y5%~19%。
   条身本身用 .bar.tb 的收窄档（chip .8em / btn .86em，同 .bar.tl），
   2 颗读数 + 1 颗按钮的实测高度 11.9%（见 预览\_rects_after.txt）。 */
.bar.tb.r9{row-gap:.28em}
.bar.tb.r9>.acts{margin-top:.34em}
.bar.tb.r9 .chip{padding:.18em .46em;font-size:.8em}
.bar.tb.r9 .btn{padding:.3em .62em;font-size:.86em}
/* 03 捶打的专属操作条：整条贴左栏、竖排。
   2026-10-04 反馈：用户对着横排那条（舂料 N 次 / 连击 xN / 水碓代劳 / 出料）
   点着说「这里移到左边，竖排」——它原来走 .bar 默认的中下部横排，四颗读数
   正压在石臼与碓架之间。做法照 02 的 .bar.tb 镜像到左栏（那一条在右栏，见上）。
   落位是拿 ?ui=0 的纯底片逐 2% 一行量的（预览\_ui17b\p2.png，644×1152）：
     x22~135 / y40%~66% 整块是暗场（mean 2~15、max ≤40，只有 y48~54 有 13~15 的灰影）；
     x135~200 从 y54% 起进石臼左沿（max 238）、x200~300 从 y32% 起有碓架，
     所以竖列只占 x22~135 这一条 —— **按钮必须上下摞**：并排的话右沿伸到 x215，
     正好压在石臼上（用户反复说的「UI 不许压内容」）。
   上下：.lead y5.3%~20.8%、看点卡 y21.2%~45.1%、操作条 y46%~66%，
   与卡片留 0.9%、与石臼（y66% 起）刚好不叠。左沿 x3.4% 和 .seg / .lead / 看点卡
   同在一条线上，整条左栏像 01 那样一气贯下来。只给 03 用，别屏版面不动。 */
.bar.tl{left:3.4%;right:auto;top:46%;height:auto;width:auto;
  flex-direction:column;align-items:flex-start;flex-wrap:nowrap;row-gap:.34em;
  font-size:.94em}
/* 四行摞起来比横排那条高得多，默认的 .chip/.btn 身量塞不进 y46%~66% 那 20% 空带：
   原型实测 chip 42px + 42px + btn 55px + 55px 加档距 = 237px（24.5%），
   末行「出料」垂到 y70.5%，右沿 x150 已经压上石臼左沿、下沿进了地面。
   所以这一条单独收一档：chip 收到 .8em、btn 收到 .86em 并把内边距压到 .3em，
   四行加档距 = 190px（19.7%）——「水碓代劳」右沿 x133，正好落在 x135 那条空场里。 */
.bar.tl .chip{padding:.18em .46em;font-size:.8em}
.bar.tl .btn{padding:.3em .62em;font-size:.86em}
.bar.tl>.acts{flex-direction:column;align-items:flex-start;margin-top:.3em;row-gap:.34em}
/* ★ 08 枢料专用：同一个左栏竖列，但起点上提到 y14.5%。
   这一屏的看点卡在**右上**（LAYOUT 7 = tr,t14），左栏从导语下沿（y12.1%）到
   看点卡下方那一格都是空的 —— 实测 x3.4%~47.4% / y13.5%~31% 的 mean 只有 3.2 / max 10。
   而 03 用的 y46% 那一格在这一屏是石槽（mean 56.2），落不得，所以整条上提。
   条身身量照 03（.bar.tl 的 chip .8em / btn .86em），4 行实测 19.9%。 */
.bar.tl.tla{top:14.5%}
.bar .chips{display:flex;gap:.28em;flex:1 1 auto;min-width:0;overflow:hidden}
.chip{
  display:inline-flex;align-items:baseline;gap:.22em;white-space:nowrap;
  padding:.22em .5em;border:1px solid var(--line2);border-radius:.35em;
  /* 读数是压在素材上的：底色压到 .8 并加一道细阴影，
     画面亮的地方（石臼、木料、纸垛）才读得清，没到「拿色块盖素材」的程度 */
  background:rgba(8,10,13,.8);color:var(--dim);font-size:.82em;
  text-shadow:0 1px 2px rgba(0,0,0,.9);box-shadow:0 1px 5px rgba(0,0,0,.45);
}
.chip b{color:var(--ink);font-size:1.08em;font-weight:600}
/* 读数进「理想区间」的高亮。
   2026-10-04 修：这个类原名叫 .hot，和「点得着的元素层」.hot{position:absolute;inset:0;z-index:5}
   撞了名 —— 只要任一读数到位，那个小标签就被撑成一条横贯画面的金色半透明长条，
   正好盖在器物上（用户反复说的「一变亮就挡住内容」）。改名 .ok，两层再不相干。 */
.chip.ok{border-color:rgba(201,169,97,.5);color:var(--gold)}
/* 读数被这次操作推动时闪一下（02 的翻动，见 play.js 的 bump()）。
   只动这一颗读数的边框与底色、只闪 0.42 秒：不加光晕、不缩放、
   更不碰画面亮度 —— 用户否过「灯圈呼吸、过曝」，也否过「一变亮就挡住内容」；
   要让玩家把「我这一下」和「它动了」连起来，动那颗数字本身最省事也最不打扰。 */
.chip.bump{animation:chipBump .42s ease-out}
@keyframes chipBump{
  0%{border-color:rgba(214,186,120,.95);background:rgba(201,169,97,.28)}
  100%{border-color:rgba(201,169,97,.5);background:rgba(8,10,13,.8)}
}
/* 02 水沤的「水纹 / +1 天」抬高一层（play.js 的 turn() 打的 .ripple.zup / .pop.zup）。
   不加这一条的话这两样落在 z-index:auto 那一档，被 .soakcol（z1，竹色层，天数少时
   不透明度 .85）和反应层 canvas（z2）整片盖住，翻动看起来「没有互动的展现」——
  2026-10-04 用 预览\_ui16f 逐像素量过：把透明度强推到 1 也读不出来。
  定在 z3：在素材和上面两层之上，仍在操作条（z4）之下，不遮 UI。 */
.frame .zup{z-index:3}
/* ★ 2026-10-05：同一档的坑，`.pop` 也踩了一次，而且踩了九屏。
   `.pop` 原本没有 z-index（auto = z0 那一档），而 `.zoom` 是 z-index:1 ——
   于是所有飘字都被压在底片底下，点一下「有反应」是看不见的。
   上面那条 `.zup` 就是 02 撞见同一个坑时打的局部补丁（只抬了 .ripple/.pop），
   剩下 03「嗵！」/ 04「灰」/ 05「火」/ 06「捞」/ 09「吱——」/ 11·12 那几处
   一直没抬，实测确认过：只把这条加上，飘字立刻出现在点击点上。
   定在 z3 与 `.zup` 同层：在底片与 .soakcol/.agecol 之上，仍在操作条（z4）、
   .hot/.node（z5）、底部工序轨（z6）之下，不遮 UI。
   选择器限定 `.frame`：12 抄纸那两处 `burst(plate|sheet,…)` 不在 .frame 里，
   它们各自的层叠上下文跟这屏无关，不动。 */
.frame .pop{z-index:3}
.acts{display:flex;gap:.35em;flex:0 0 auto}
.btn{
  padding:.5em .8em;border:1px solid var(--line);border-radius:.35em;
  background:rgba(8,10,13,.8);font-size:.92em;letter-spacing:.04em;transition:.16s;
  text-shadow:0 1px 2px rgba(0,0,0,.9);box-shadow:0 1px 5px rgba(0,0,0,.4);
}
.btn:hover{border-color:rgba(255,255,255,.3);background:rgba(20,24,30,.8)}
.btn.primary{border-color:rgba(201,169,97,.55);color:#f0e2c0;background:rgba(60,48,26,.55)}
.btn.primary:hover{background:rgba(86,68,34,.7)}
/* 不可点状态：底衬不跟着一起淡，否则压在亮画面（浆水、纸垛、透光纸）上整颗按钮糊掉。
   用「底衬照常 + 字色变灰」表示不可点，画面亮的地方也读得出来。 */
.btn[disabled]{opacity:1;color:rgba(233,227,214,.40);border-color:rgba(255,255,255,.10);
  background:rgba(8,10,13,.66);box-shadow:0 1px 4px rgba(0,0,0,.35);pointer-events:none}
.btn.primary[disabled]{color:rgba(240,226,192,.46);border-color:rgba(201,169,97,.26);
  background:rgba(26,22,15,.66)}
.btn.tiny{padding:.32em .6em;font-size:.82em}

.meter{position:absolute;left:4%;right:4%;top:44.4%;height:2px;background:rgba(255,255,255,.1);z-index:4}
.meter i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--bamboo),var(--gold));transition:width .2s}

/* 老师傅提示条。
   2026-10-04 用户：「这里信息和后面亮部重合了」——原来这里只有裸文字 + 上沿一条细金线，
   压在 01 底部那截亮竹节 / 砍刀上就读不出来；落到 02 水沤、12 抄纸 那种整片白亮底更糊
   （预览\_shot_plain16 逐屏量这一带：02 mean 90.3、>110 的像素 48.3%；12 mean 149.7、
   占 90.8%）。十六屏里没有一条是「全暗」的横带 —— 挪位置解决不了，所以改成和
   .seg / .obj 同一族的墨底牌：上沿仍留那条金线，底衬把画面压住，字才立得住。
   只读，pointer-events:none：这一带在 12 抄纸正好压着拖拽区（zone 到 y90%），
   底衬不许吃点击。 */
.tip{
  position:absolute;left:31.5%;right:4.5%;top:79.4%;
  z-index:4;pointer-events:none;
  padding:.52em .95em .56em 1em;
  font-size:.66em;color:rgba(236,230,218,.9);letter-spacing:.05em;
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  background:linear-gradient(180deg,rgba(6,8,12,.9),rgba(6,8,12,.66));
  border-top:1px solid rgba(201,169,97,.42);
  box-shadow:inset 0 0 0 1px rgba(233,227,214,.06);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  text-shadow:0 1px 3px rgba(0,0,0,.85);
  opacity:0;transition:opacity .55s ease .25s;
}
.page.live .tip{opacity:1}
body.still .tip{opacity:1}
/* 07 碱煮专用：这一屏的柴火在画面上是**偏左**的（常火 x32.7%~39.9%、
   过火 x18.5%~43.2%），而提示条默认从左 31.5% 起 —— 正好横盖在灶门上。
   2026-10-05 实测：常火的亮像素有 25.8% 落在提示条的框里（那一档火最小，
   被盖住的偏偏最多，等于看不见「刚点着」的样子）；旺火 6.5%、过火 2.5%。
   所以这一屏的提示条右移让开灶门（左沿 31.5%→44%），右沿仍与
   02 的读数列、下面那条 06 的提示条对齐在同一条 x95.5% 的线上。
   文本长度由 预览\_tipwidth.js 逐屏量着守（nowrap+ellipsis，长了会被截成「…」）。 */
.tip.r7{left:44%}

/* ---------- 通用反馈 ---------- */
.ring{
  position:absolute;width:.9em;height:.9em;margin:-.45em;border-radius:50%;
  border:1px solid rgba(233,227,214,.7);pointer-events:none;
  animation:ring .62s ease-out forwards;
}
@keyframes ring{from{transform:scale(.4);opacity:.9}to{transform:scale(3.6);opacity:0}}
.pop{
  position:absolute;transform:translate(-50%,-120%);pointer-events:none;
  font-size:.82em;color:var(--ink);text-shadow:0 2px 8px #000;white-space:nowrap;
  animation:pop .8s ease-out forwards;
}
@keyframes pop{from{opacity:0;transform:translate(-50%,-100%)}30%{opacity:1}to{opacity:0;transform:translate(-50%,-210%)}}
.ripple{
  position:absolute;width:1.6em;height:1.6em;margin:-.8em;border-radius:50%;pointer-events:none;
  border:1px solid rgba(140,190,210,.55);animation:ring .9s ease-out forwards;
}
/* ★ §89 晒叶的「沙沙」（10 纸药）：手在竹簟上来回拖时，每 0.045 净行程扬起一小片**碎叶末**。
   为什么要有：档位是**离散**的（DRY 才算一步），两格之间本是一段死寂，
   而旧版连推档那一下也只弹「晒开」两个字、层还不换 —— 用户报「叶子又晒不开了」，
   一半的锅在「拖了像没反应」。这一层不报进度，只报「手正蹭在簟上」这件事。
   ⚠ 形式**刻意不是白点圆环**：§74 淘洗那版就是打了一圈 .ripple 圆环，用户当场否掉
     （「去掉淘洗的圆圈，改为水波动」）。所以这里做成**扁的碎叶末**，
     带一点自转和横向飘移，像从簟条缝里搓下来的叶屑；三颗一循环（--k 给 左/右/直 三种飘向）。
   z-index 必须写 3：同 414 行的 .zup / 420 行的 .pop，不写就落到 auto=z0，会被 .zoom 压住。 */
/* ⚠ §89 颜色必须是**两截**（上亮下暗），单色两头都不成立 —— 竹簟区横跨两种底，
   实拍 + PIL 采样（预览\_tr10_shot\89rustle.png）量过：
     frame(0.10,0.60) 底下是暗桌布 RGB 25,32,34；frame(0.22,0.60) 底下是竹簟 RGB 199,183,159。
   第一版用单色暖黄 rgba(228,216,162)：在簟上等于没有（簟本身就是米黄竹色）；
   第二版换单色深绿：桌布那一段又看不见。所以做成上缘暖白、下缘橄榄 + 一圈极淡黑边，
   落在哪种底上都有一半是反的。 */
.frame .rustle{
  position:absolute;width:.4em;height:.26em;margin:-.13em -.2em;
  border-radius:50%;pointer-events:none;z-index:3;opacity:0;
  background:linear-gradient(165deg,rgba(255,247,222,.92) 0 42%,rgba(92,78,46,.9) 54% 100%);
  box-shadow:0 0 0 .04em rgba(18,14,6,.3);
  animation:rustle .6s ease-out forwards;
}
@keyframes rustle{
  0%{transform:translate(0,0) rotate(0deg) scale(.7);opacity:0}
  25%{opacity:.8}
  100%{transform:translate(var(--dx,0em),-.36em) rotate(40deg) scale(1.25);opacity:0}}

/* ---------- 彩蛋 · 花草纸：★ §108b 已整条退役（2026-10-08 用户定「乙」）----------
   原先是「12 抄纸浆水面上漂着三片花叶（两瓣野花瓣、一片竹叶），点中就捞进帘里」，
   一路带到 15 成纸的透光、16 题款的回执、结果页的纸面（.bloom）和分享卡。
   用户看过实片之后要「把这几个花瓣去掉」，于是连同那第三档料「撒花叶」一起拿掉：
   play.js 里不再建 .eggLayer、不再有 pickEgg()，.bloom / .rvEggs / EGG_SVG /
   drawPetal() / drawLeaf() 也一并删除。这一段只留个记号，免得后面有人从
   旧截图里看到花叶、回头找样式却找不到。 */

/* ---------- 选竹：位图亮边（play/rim-1..3.png）----------
   亮边不是画的折线，是预览/_ring15.py 对着 1000×1500 定格帧【逐行】量出来的位图，
   和 .zoom 里的画面共用同一条推近变换（见上面 .zoom,.culms,.agecol），
   所以点下去线也不会从秆边滑开，也不会有「有的出去、有的没到边」。
   状态：默认很淡（看得出这三根点得着）→ 悬停半亮 → 选中拉满。 */
.culms{overflow:visible}
.culms .rim{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  pointer-events:none;user-select:none;-webkit-user-drag:none;opacity:.15;
  transition:opacity .3s ease}
.culms .rim.hov{opacity:.5}
.frame.picked .culms .rim{opacity:.06}
.culms .rim.on,.frame.picked .culms .rim.on{opacity:1}
/* 竹龄色：①太嫩偏青、③过老偏黄（用户：「不同年份的竹子做成不同样式，老的会发黄」）。
   color 混色只换色相、不动明暗，秆上的节环和明暗都原样保留——
   不是往画面上糊一块色（那正是用户禁止的「影响观感」）。
   2026-10-04：素材色改成低饱和（见 预览/_trace16.py 的 TINTS），不透明度也收下来。
   之前 ③ 用 (247,205,96) + .70，实际混出来是 (160,131,31) 的芥末黄，
   看着像给秆贴了一张黄板；现在同一点只走成 (119,117,71) 那种表层的旧黄。
   这一层必须挂在 .frame 下：.zoom / .hot 各自成层叠上下文，挂在里面就混不进素材。 */
/* 关键：mix-blend-mode 必须挂在 .agecol 上，不能挂在 .tint 上。
   .agecol 带 transform，本身就自成层叠上下文；把混色写在里面的 .tint 上，
   它的「背景」就是 .agecol 里那层空白，没得混 —— 浏览器退化成普通 alpha 合成，
   于是黄色的秆变成一块实心贴片（用户看到的「太明显」就是这个）。
   挂到 .agecol 上，背景才是 .frame 里的画面，color 混色才真正生效：
   只拿画面的明暗、换成竹龄的色相。 */
.agecol{mix-blend-mode:color}
.agecol .tint{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  pointer-events:none;transition:opacity .35s ease;filter:blur(.6px)}
.agecol .tint[data-age="1"],.agecol .tint[data-age="3"]{opacity:0}
.agecol .tint.on[data-age="1"]{opacity:.50}
.agecol .tint.on[data-age="3"]{opacity:.58}

/* ---------- 水沤：竹麻「青 → 黄」两个真素材（2026-10-04 第十四轮，第三十三轮重做）----
   这一层配的是 play\stage\soak-green.png，它是**重新渲染出来的一张青竹图**，
   不是色相混色：画布节点「水沤-青竹-02底」以**当前底片的定格帧**
   （02水沤.mp4 的 0.30s；预览\_soaknew\f030.png）当参考图跑了一张真渲染
   （libtv 画布 4cc6a6b5…，模型 Seedream 5.0 Pro，2K / 2:3，image2image），
   预览\_soakfit2.py 先按**水面以外的石沿与背景**求缩放／平移把它配准到定格帧
   （实测 scale=1.000、残差 2.1，等于原样对齐），
   再按「底片黄 ↔ 渲染绿」的色彩差自动求竹麻遮罩（暖度差 + 绿度差），
   形态学闭/开 + 大核去碎点 + 羽化，做成这张 RGBA 素材。
   为什么改成「按当前底片重渲」：第一版拿的是**旧片**（方池那一版）的定格帧当参考，
   02 底片换成圆池新片之后，那张青竹图的竹捆位置与新底片完全对不上 ——
   用户 2026-10-04 看到的「青色虚影」就是两套竿子叠出来的。

   为什么换掉上一版的 mix-blend-mode:color：
   用户 2026-10-04 的原话是「这张图的绿色素材层，铺的太假了，你可以去 libtv
   再渲染一张绿色竹子的」——上一版只把素材的色相换成竹青，明暗还是原图里那根
   已经沤黄的秆，看起来就是蒙了一层绿。现在这一层本身就是一根青竹的画。

   切换：play.js 按沤制天数改它的不透明度 —— 0 天→1（整池青竹），
   20 天→0（全退，露出素材本来的黄），中间是两段素材互相溶接的「半脱青」。
   合成方式是普通叠加（source-over），因为这是一张有明暗的照片，不是色层。

   这一层是页面加的，不是素材里烧的，所以 ?ui=0 的纯素材验收里跟 .rx 一起隐掉。 */
.soakcol{position:absolute;inset:0;width:100%;height:100%;z-index:1;
  pointer-events:none}
.soakcol img{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  pointer-events:none;user-select:none;-webkit-user-drag:none;
  transition:opacity .6s linear}
body.no-ui .soakcol{display:none!important}

/* ---------- 02 水沤：抹掉底片里那根搅料竹竿的钩头（2026-10-04 第二十八轮）----------
   用户原话：「还有向下的圆环是什么」「也去掉」。
   那不是 UI，是底片 02水沤.mp4 里搅拌竹竿的钩头：竹竿中段被青竹层盖住，
   只剩钩头孤零零浮在黑墙前，就读成一个悬空的圆环。
   play\stage\soak-nohook.png 由 预览\_hookgen.py 生成 ——
   钩头轮廓按亮度抠出来（那块背景几乎纯黑，mean 1.4~2.0，亮的只剩竿子），
   长胖 12px 吃掉暗边与传感器抖动，再按「调和填充」补成黑墙。
   补丁压在青竹层下面（play.js 里先 insert 补丁、后 insert 青竹层），
   所以青竹层盖住的那一段照旧，竿子看起来是从竹捆后面伸出来的。
   同样是页面加的层，?ui=0 的纯素材验收里跟 .soakcol 一起隐掉。 */
.hookfx{position:absolute;inset:0;width:100%;height:100%;z-index:1;pointer-events:none}
.hookfx img{position:absolute;pointer-events:none;user-select:none;-webkit-user-drag:none}
body.no-ui .hookfx{display:none!important}

/* 04 灰腌：石灰的「状态层」。2026-10-05 深夜第五版 —— 见 play.js 的 R.lime 注释。
   自下而上（都挂在 .zoom 里、紧贴着底片视频）：
     六个区的局部真渲 lime-zone-<id>.jpg（各自一个 0–1 的覆盖度：擦到哪块哪块才亮）
     → 收口的满灰 lime-full.jpg
     → 浇过水之后的湿灰 lime-wet.jpg（.6s 溶接）。
   六张全是 1174×1762 的整幅无 alpha 图，object-fit:fill 上是同尺寸对同尺寸，
   没有缩放误差。区图与满灰用 mix-blend-mode:lighten —— 粉比底片亮，撒到的地方
   就亮起来，没撒到的地方在 lighten 下与底片逐像素一致，等于「没动」。
   这正是不要蒙版的解法：画面里出现的是模型真画出来的粉，不是我们抹的一块白。
   lighten 必须和底片**在同一个层叠上下文里**才混得动，所以这几层直接挂在 .zoom 下
   （.zoom 带 transform，本身就是那一层）；另套一个 position+z-index 的容器就
   自成层叠上下文，混色只在容器内部发生，等于白挂。
   湿灰是整幅不透明的真画，走普通叠加、盖住上面两层。
   同样是页面加的层，?ui=0 的纯素材验收里跟 .rx 一起隐掉。 */
.lzone,.lfull,.lwet,.lstep{position:absolute;inset:0;width:100%;height:100%;
  object-fit:fill;z-index:1;pointer-events:none;user-select:none;-webkit-user-drag:none}
.lzone,.lfull{mix-blend-mode:lighten}
/* 擦过去的反馈要跟手：区图给一条很短的过渡，长了就又像「整幅淡入」。
   收口与湿灰是状态变化，仍走慢档。 */
.lzone{transition:opacity .18s linear}
.lfull,.lwet{transition:opacity .6s linear}
/* 兜底：区图还没生成出来时用 1049–1051 那套整幅五档真渲（见 play.js 的 zoneBad），
   是整幅不透明的真画，走普通叠加、不混色；换档给 .22s 短溶。 */
.lstep{transition:opacity .22s linear}
body.no-ui .lzone,body.no-ui .lfull,body.no-ui .lwet,body.no-ui .lstep{display:none!important}

/* 05 灰蒸：灶火的「四档真渲」。2026-10-05 深夜第七版 —— 见 play.js 的 R.steam。
   自下而上（都挂在 .zoom 里、紧贴着底片视频）：
     旺火 fire-mid.jpg / 过火 fire-high.jpg   mix-blend-mode:lighten
       —— 火、以及火光映亮的砖与地面都比底片亮，取逐像素最大值就出来。
     余烬 fire-ember.jpg                      mix-blend-mode:darken
       —— 火与它映在砖石上的暖光在候选图里比底片暗，取最小值正好抹掉；
          候选图不比底片暗的地方原样留底片，所以砖石不偏色。
   ★ 三张图都只铺「灶台以下那条带」（画面 y 60%~100%）：三张与底片同为
   1174×1762、同在 .zoom 里 inset:0 铺满 .frame，行 60% 以下天然逐像素对位，
   所以只用遮罩把上面 60% 隐掉（详见 .sfire 里的注释）。
   为什么不能整幅铺：
     · 这三张是 libtv 按「照搬参考图」重画的同场景，不是像素级对齐
       （实测木甑蓝通道差 17、地面差 90），整幅叠上去整屏会跟着火候换一套明暗
       —— 用户说的「画面的色度变了」就是这个；
     · 余烬那幅上半部没有汽，整幅铺会把底片里正在翻卷的蒸汽一起盖住
       （实测甑口区被暗掉 14.6 个灰阶），汽就冻住了。
   裁成一条带之后，带以上的木甑 / 墙 / 甑口蒸汽逐像素零改动，汽照旧在动。
   上边界落在灶台正面的水平分界上，再给一条 2% 带高的软过渡（mask 只做那一小段
   渐变，不是抠形，也不改素材本身），与 预览\_steamfire.py 的 FIRE_FEATHER 对齐。
   过渡 .4s 缓出：添柴是 +11℃ 一步跳，靠它抹平；四档之间本来就是连续溶接，
   没有任何「换帧」。三张与 04 的石灰层同理，必须直接挂 .zoom
   （.zoom 带 transform，本身就是那一层；node 在 .frame 的 isolation 里面）。 */
/* 07 碱煮的灶火层 .bfire 与 05 灰蒸的 .sfire **同一条几何**（1174×1762 同尺寸图
   铺满 .frame、同一条 y60% 横带遮罩）—— 07 的过火火舌最高只到 y62.8%，
   整根都在带内，所以不需要 05 那条按灶门抬起来的 L 形遮罩。
   两屏的差别只在混色方向与档位（见 play.js 的 R.boil / R.steam）。 */
.sfire,.bfire{position:absolute;inset:0;width:100%;height:100%;
  object-fit:fill;z-index:1;pointer-events:none;user-select:none;-webkit-user-drag:none;
  transition:opacity .4s ease-out;
  /* ★ 只留「灶台以下那条带」= 画面 y 60%~100%。
     三张 fire-*.jpg 与底片同为 1174×1762、同在 .zoom 里 inset:0 铺满 .frame，
     所以行 60% 以下天然逐像素对位 —— 上面 60% 直接遮掉即可，
     不需要（也绝不能）把图放大或位移：.sfire 的框就是 .frame，改 top/height
     等于把那条带拉伸后铺满整屏（2026-10-05 深夜修掉的坑）。
     上边界给一条 2% 带高的短羽化（y60→62%），否则带内带外的亮度差会看成一条横线；
     羽化长度与 预览\_steamfire.py 的 FIRE_BAND=(0,.60,1,1)+FIRE_FEATHER=(0,.05) 对齐，
     所以 _steamfire 出的合成预览与页面看到的是同一条带。 */
  -webkit-mask-image:linear-gradient(to bottom,transparent 60%,rgba(0,0,0,.42) 60.8%,#000 62%);
  mask-image:linear-gradient(to bottom,transparent 60%,rgba(0,0,0,.42) 60.8%,#000 62%)}
.smid,.shigh,.bmid,.bhigh{mix-blend-mode:lighten}
.sember,.blow{mix-blend-mode:darken}
/* ★ 2026-10-05 深夜第八版：旺火 / 过火 换成动片之后，火舌窜到灶门之上
   （旺火最高 y≈0.39、过火 y≈0.34），被上面那条 60% 的横带切出一条平口
   （实拍 预览\_steamfire\firevid\_cut60-door.jpg）。这两层因此不再用横带，
   改用**按片子逐帧量出来的「L 形」遮罩**（生成脚本 预览\_firemask_build.py，
   两张 PNG 就在 play\stage\mask-fire-{mid,high}.png）：
     · y > 60% 那条带：全宽，与老规则完全一样；
     · 灶门那一列（x < 52%，往右 8% 羽化）：上边界按档抬到旺火 34% / 过火 30%，
       把火舌整根露出来。
   为什么只抬这一列、不把整条带抬上去：带以上是木甑与甑口蒸汽，动片里那层
   **冻住的汽**顶上来会和底片里正在翻卷的汽打架。脚本的验收数是
   「灶门里的火被遮住的部分 0%（旺火）/ 1.99%、残留最亮 34 灰阶（过火）」，
   「多露的面积落在甑身与汽区 y<34% 的只有 0%（旺火）/ 4.15%（过火）」。
   余烬那一层仍是老横带（它本来就只在灶门以下），所以 .sember 不动。 */
.smid,.shigh{
  -webkit-mask-image:url("stage/mask-fire-mid.png");
  -webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;-webkit-mask-position:0 0;
  mask-image:url("stage/mask-fire-mid.png");
  mask-size:100% 100%;mask-repeat:no-repeat;mask-position:0 0}
.shigh{
  -webkit-mask-image:url("stage/mask-fire-high.png");
  mask-image:url("stage/mask-fire-high.png")}
body.no-ui .sfire,body.no-ui .bfire,body.no-ui .bliao{display:none!important}

/* ★ 2026-10-05 深夜第九版：07 旺火 / 过火的下沿羽化。
   fire07-{mid,high}.jpg 里的火舌一直铺到画面底（过火实测到 y100%），
   而环节图 .caps 从 y85.9% 起是实心黑 —— 火舌就在那一行被平切，
   实拍 预览\_boilshot\_small\q14_fire.jpg 里是一条很硬的橙白直边，
   又变回用户说的「下面的火被环节图挡住」。
   给这两层加一条下沿羽化：y83% 起渐隐，y86%（.caps 实心线之前）收干净，
   火舌是「淡进暗里」，不是「被切一刀」。
   · 为什么要 y86 而不是更低：低于 85.9% 就落进 .caps 实心段，白羽化；
   · 为什么常火 .blow 不羽化：它在底部那点暖光正是「刚点着的余烬」，
     羽化会把底片里那团大火从下面透出来，常火就不像常火了（实测
     底片 y0.82~0.86 那一带 max 还有 130~232 灰阶）。 */
.bmid,.bhigh{
  -webkit-mask-image:linear-gradient(to bottom,transparent 60%,
    rgba(0,0,0,.42) 60.8%,#000 62%,#000 83%,rgba(0,0,0,0) 86%);
  mask-image:linear-gradient(to bottom,transparent 60%,
    rgba(0,0,0,.42) 60.8%,#000 62%,#000 83%,rgba(0,0,0,0) 86%)}

/* ★ 2026-10-06 §70：07 碱煮**甑口的那堆料**（.bliao）。用户原话是这一屏的料
   「接不上 07 出锅的熟料」（原来甑口是一层黑褐糊成一坨）。做法与 08 那四档同源：
   libtv 重渲甑口料（概念图\libtv\gen-boil07-liao.ps1）→ 预览\_pulp4.py 只在
   **料堆掩膜内**贴回底片定格帧（掩膜 9.75%，bbox x0.243~0.786 y0.149~0.344）。
   所以这个 png 自带 alpha，**不用 mix-blend-mode、也不加任何遮罩** ——
   木勺、碱水柱、稻草圈、竹签、木甑、砖灶、炉膛火逐像素还是底片自己的。
   挂法与 .washcol / .steepcol 一致：.zoom 的直系子节点（见 play.js R.boil 里
   为什么不能另套一层 container），满幅 inset:0 铺满 .frame。
   DOM 序排在灶火三层**之前**（p.zoom.append(liaoImg, midImg, highImg, lowImg)），
   同 z-index 下后画的在上面 —— 料在甑口、火在灶门，本来就不重叠。 */
.bliao{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  z-index:1;pointer-events:none;user-select:none;-webkit-user-drag:none;
  transition:opacity .4s ease-out}

/* ★ 2026-10-06 方案丁（第三轮）：「火到中间的时候，火焰变化，出现没对齐情况」。
   病根就是上一轮（方案丙）加在这里的那条 transform:scale(.80)：
   实测三层矩形（预览\_boilalign.js）——
     .bmid  缩放后 rect 只有 x7.4% y16.0% w80% h80%，matrix(0.8,0,0,0.8,0,0)；
     .blow / .bhigh 都是满幅 x0 y0 w100% h100%、transform:none。
   温度交叠带（52~70℃ 常↔旺、92~105℃ 旺↔过）里两层同时可见，
   于是一大一小两团火错开 —— 用户看到的就是这个「没对齐」。

   修法是**把缩放还给素材**：重新渲染一张「火苗本身就收在门洞里、
   但比底片那团火明显更旺」的旺火档（概念图\libtv\gen-boil07-fire4.ps1，
   候选 碱煮4-火-旺火-3），页面侧恢复成与过火完全同一条几何：
   满幅 inset:0、同一条 60/60.8/62/83/86 遮罩（见上面 .bmid,.bhigh 那条）。
   所以这一条现在**只剩说明**，没有任何覆盖样式 —— 千万别再往 .bmid 上加
   transform / 百分比位移：三层几何一不一样，交叠带就会露馅。
   亮度仍由 play.js R.boil 的 MID_CAP 压一档（旺火不许到过火那种烧白）。
   （历史：丙版的 scale(.84)+反解遮罩停点 55/56/57.5/83.75/87.5 已随本版废弃。） */

/* ---------- 06 洗灰（2026-10-05 深夜重做：四档洗净 + 一瓢水，全部整幅真渲）----------
   用户三条：「看不见水的来源」「洗清应该是洗清料子」「有浇水，料子不断变清」
   「这里游戏的交互一般」→ 定案「① 4 档・浇水＋翻料」。

   旧的两张（wash-muddy/wash-clear）是照着**影棚版**概念图抠的多边形蒙版，
   本屏底片却是**房间版**视频（顶上天窗、砖石墙与木柱、料子泡在石台水洼里），
   两版背景根本不同。预览\_imdiff.py 实拍量出来：翻洗到全清时
   **19.01% 的像素被动过 12 灰阶以上** —— 那就是用户报的「可见蒙版脚印」。

   现在四档洗净 + 一张「浇水」共五张都是 libtv 房间版整幅真渲
   （概念图\libtv\gen-wash-room.ps1，Seedream 5.0 Pro / image2image，
     第一参考 = 本屏底片），再由 预览\_washroom.py 整幅配准 + 主体之外逐通道分位
   映射回底片影调：scale=1.0000、dx=dy=0，主体之外平均绝对差 2.6~7.4 灰阶。
   **不抠图、不遮罩、不用 mix-blend-mode**（lighten 只会抬亮，洗灰要的是变暗，
   实测会把壳区越洗越白，还会在墙/柱/天窗上蹭鬼影，见 play.js R.wash 的说明）。
   四档照 02 水沤 / 04 灰腌 / 08 枢料的老规矩叠：下层恒定 opacity:1、只推上一层，
   合成退化成 (1-q)·下 + q·上，过渡区一滴底片都不漏。
   第五张「浇水」单独漂在最上面（z-index:2，由 play.js 内联给），
   只在「按住画面上方浇水」时浮起来 —— 它就是那股从晒棚顶浇下来的水。
   2026-10-05：这一层由**静图**改成了**活水动片**（06-洗灰-活水-loop.mp4，
   Wan 3.0 frames2video，首帧=尾帧=那张静图 → 预览\_washvid.py 影调对齐 + 剪无缝循环）。
   play.js 里照 05 灰蒸的 upgradeFire() 挂：canplay 才原地把 <img> 换成 <video>、
   失败就留静图。所以下面这条选择器必须**同时**认 img 和 video。 */
.washcol{position:absolute;inset:0;width:100%;height:100%;z-index:1;
  pointer-events:none}
.washcol img,.washcol video{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  pointer-events:none;user-select:none;-webkit-user-drag:none;
  transition:opacity .6s linear}
/* 「按住浇水」的一次性动作提示：水柱落点上画一个虚线圈 + 四个字。
   浇过第一瓢就由 play.js 从 DOM 里撤掉 —— 不常驻、不吃点击、也不在纯素材模式出现
   （用户明确不喜欢画面上到处浮字，所以这里只做「第一次怎么开始」的路标）。 */
.wspout{position:absolute;left:46%;top:26%;transform:translate(-50%,-50%);
  z-index:4;pointer-events:none;display:flex;flex-direction:column;align-items:center;gap:7px}
.wspout i{width:clamp(32px,7.4%,58px);aspect-ratio:1;border-radius:50%;
  border:1px dashed rgba(255,255,255,.52);animation:wspoutPulse 2.4s ease-in-out infinite}
.wspout em{font-style:normal;font-size:clamp(10px,1.5vh,13px);letter-spacing:.14em;
  color:rgba(255,255,255,.8);text-shadow:0 1px 6px rgba(0,0,0,.85)}
@keyframes wspoutPulse{0%,100%{transform:scale(.86);opacity:.55}50%{transform:scale(1.06);opacity:.95}}
body.no-ui .washcol,body.no-ui .wspout{display:none!important}

  /* 08 枢料：料相的「状态层」——§70 起是**四档**（浅姜黄 / 姜黄 / 深姜黄 / 枢透发乌），
     每档都是 libtv 真渲的画，由 预览\_pulp4.py 只在**料堆掩膜内**贴回底片定格帧，
     所以这四个 png 自带一条 alpha（掩膜占 9.39%），掩膜之外底片动片照旧在动。
     挂法与 02/03/04/06/09 一致（普通叠加，不用 mix-blend-mode）——
     千万不要给 .steepcol 加 position:absolute 之外的层级设定，也不要挂进 .zoom：
     .zoom 带 transform，自成层叠上下文，四档 png 就再也对不上底片了。
     叠法看 play.js 的 R.steep：下层（档1）恒定 1，上面三档各在自己那一段里推上来。 */
  .steepcol{position:absolute;inset:0;width:100%;height:100%;z-index:1;
    pointer-events:none}
  .steepcol img,.steepcol video{position:absolute;inset:0;width:100%;height:100%;
    object-fit:fill;pointer-events:none;user-select:none;-webkit-user-drag:none;
    transition:opacity .6s linear}
  /* 四档拨料动片是 mp4，带不了 alpha，靠这条**同一块料堆掩膜**（play\stage\steep-lmask.png，
     也是 预览\_pulp4.py maskimg 出的）抠出料堆那一块。羽化已经在掩膜里，
     这里只要按 100%×100% 铺开、不重复即可 —— 与 .sfire 那条 mask-fire-*.png 同一个套路。 */
  .steepcol video{
    -webkit-mask-image:url("stage/steep-lmask.png");
    -webkit-mask-size:100% 100%;-webkit-mask-repeat:no-repeat;-webkit-mask-position:0 0;
    mask-image:url("stage/steep-lmask.png");
    mask-size:100% 100%;mask-repeat:no-repeat;mask-position:0 0}
  body.no-ui .steepcol{display:none!important}

  /* 09 舂淘：三档料层（粗料散 / 成团 / 淘净，都是 libtv 真渲、掩膜外透明）
     ＋ 版子（thrash-plate.png，把杵擦干净的同一张画）
     ＋ 杵精灵（thrash-pestle.png，同一根杵拎出来的那块像素）。
     叠法：版子 → 料层 → 杵精灵，同一条 z-index:1 的带子里按 DOM 顺序压；
     .vdim（压暗）是 z-index:2，照旧压在它们上面。 */
  .thrashcol{position:absolute;inset:0;width:100%;height:100%;z-index:1;
    pointer-events:none}
  .thrashcol img,.thrashplate,.thrashpole{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    transition:opacity .6s linear}
  /* ★ 2026-10-06 §73：**穿模的真病根**。.thrashcol 是 z-index:1，而 .thrashplate / .thrashpole
     没写 z-index（= auto，落到 z0 那一档），于是不管 DOM 怎么排，料层永远压在杵上面 ——
     杵一进料掩膜的范围就被那把「刀」切掉一截，抬杵时看着像从石缝里抽出来。
     同一个坑 play.css 里踩过两次（414 行的 .zup、420 行的 .pop），这里第三次。
     修法：把版子与杵精灵一起抬到 z1 带，先后仍由 DOM 决定（版子 → 料层 → 空臼 → 杵 ↗ 水盆）。 */
  .thrashplate,.thrashpole{z-index:1}
  .thrashplate,.thrashpole{transition:none}
  /* 09 舂淘：空臼层（料团被捧走之后的石臼 —— 掩膜内是湿石壁 + 臼底一点浆水，
     掩膜外透明）。压在料层之上：料层一被它盖住，臼口就「空」了。 */
  .thrashbowl{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    z-index:1;opacity:0;transition:opacity .6s linear}
  /* 09 舂淘：水盆层（w0 清水空篮 / w1 料刚捧来 / w2 淘净发白，三档真素材，
     掩膜外透明）。叠法同 .thrashcol：w0 恒定 1，w1 / w2 往上推。
     底下这一格原来是烧死在底片里的「篮子已经堆着料」，所以 w0 是必需的 ——
     料还在臼里的时候，盆里必须是空的、水必须是清的。 */
  .thrashwater{position:absolute;inset:0;width:100%;height:100%;z-index:1;
    pointer-events:none}
  .thrashwater img{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    transition:opacity .6s linear}
  /* 一杵的量与方向就写在这一个变量上：正数 = 砸进料里，负数 = 抬起来。
     --tl = 「抬满量」，占画面高。2026-10-06 三次修（§71）：
       2.4%（14px）/ 3.6%（21px）都只是「往下沉一点」，用户在手机上读不出「棍子动」；
        现在是**沿杵自己的轴先抬起、再砸进料里**。2026-10-06 §72 又从 6% 提到 **10%**
        （390×844 上抬 ≈52px、砸下 ≈16px）：6% 时杵尖只挪 30px，用户还是说「看不到棍子动」；
        抬量再大也**不会漏底片** —— 杵让位后露出来的是版子（无杵底片），
        而版子的不透明区**就是杵自己那只脚印**，杵怎么挪都在里面。
     沿轴的方向：实测 x = 0.322·y（杵中心线），画面 2:3，
     所以 X 那一项要用**画面宽**的百分比 —— 0.322×(2/3) ≈ 0.21，再除掉行程系数就是 .48。
     抬起来的让位区已被版子/料层 100% 盖住（_thrash9 里逐行程量过，8% 抬量都 0 漏底片）。*/
  /* 2026-10-06 §79：抬量从 10% 收到 **--tl:3.5%**（≈18px），砸下另开一个
     **--td:14%**（≈25px）—— 用户要的是「圆头只在料面附近探个头」：抬太多，
     合成出来的圆头会整个飘在料面之上，看着反而假；而「看得见棍子动」交给
     幅度更大的**下砸**（下砸时圆头是隐形的，埋进料里，不会露馅）。
     ★ 这两个变量 `.thrashpole` 与 `.thrashhead` **必须同时给**：头部图层是另一个
     元素、不在杵的继承链上，动画里的 var(--tl)/(--td) 要在它自己身上解析。 */
  .thrashpole,.thrashhead{--tl:3.5%;--td:14%}
  .thrashpole.hit{animation:thrashHit .5s cubic-bezier(.3,.9,.4,1)}
  @keyframes thrashHit{
    0%{transform:translate(0,0)}
    18%{transform:translate(calc(var(--tl) * -.43), calc(var(--tl) * -.90))}
    62%{transform:translate(calc(var(--td) * .14), calc(var(--td) * .30))}
    100%{transform:translate(0,0)}}
  /* 09 舂淘 · **圆杵头**（§79）。底片里杵的下端被料盖住了，从来没露过圆头，
     所以一抬起来亮出的是一条平切断面（用户报「抬起来是个假的杵头，不圆」）。
     这一层是照杵身木纹合成出来的圆头（预览\_pest_head.py），**只在 .thrashpole.hit
     这 0.5 秒里出现**：静止时 opacity 0，所以静止帧仍然逐像素等于底片，不会在
     料里冒出一个木疙瘩。它跟杵精灵共用同一段 thrashHit 动画，所以与杵身同步。 */
  .thrashhead{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;z-index:1;opacity:0}
  .thrashpole.hit ~ .thrashhead{animation:thrashHit .5s cubic-bezier(.3,.9,.4,1),
    thrashHead .5s linear}
  @keyframes thrashHead{
    0%{opacity:0} 3%{opacity:1} 26%{opacity:1} 40%{opacity:0} 100%{opacity:0}}
  /* ★ §83：**盆沿进度圈整套删掉**（原来是 §74 用户点名要的 .thrashring：
     track 底圈 + prog 进度弧 + 两层 drop-shadow）。用户 2026-10-07 改口：
     「去掉淘洗的圆圈，改为水波动」—— 淘洗的份量现在由盆里那面水自己报：
     一条真水波片（.thrashwave）＋ 每遍一道水花、一个「+1」、料团晃一下。
     删圈不是把反馈删掉：原来那个圈只是「进度」，看得见的水一直只有 w2 那一档水色。 */
  /* 09 舂淘 · 盆里的水波（§83）。素材是一条黑底的涟漪亮部差值片
     （libtv frames2video 出片 → 预览\_tr9\_wave_diff.py 取亮部差 + 盆口椭圆羽化）。
     为什么非得走差值 + screen：**mp4 没有 alpha**，原片盖上来会把盆外一起挡掉。
     z-index:1 与料层 / 空臼层同一个带子，压在 .thrashwater（水色）之上、.vdim（z2）之下
     —— 所以夜晚压暗时它跟着一起暗，不会像原来那个圈（z3）那样浮在压暗层上面。
     opacity 只在「料团已经捧进盆里」之后才抬起来（.on 由 play.js 的 waveOn 打）。 */
  .thrashwave{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    z-index:1;mix-blend-mode:screen;opacity:0;transition:opacity .6s linear}
  .thrashwave.on{opacity:.72}
  /* 搓满一遍的那一下：水波自己亮一下。只动 opacity —— 缩放一条 screen 混合的片子
     会让波纹跟着缩放，很假（别处已经否过这种假法）。末帧回到 .on 的稳态值，
     所以连搓十遍也不会累积漂移。 */
  .thrashwave.churn{animation:thrashChurn .5s ease-out}
  @keyframes thrashChurn{
    0%{opacity:1} 60%{opacity:.87} 100%{opacity:.72}}
   /* ---------- 11 打浆 · 木搅棍画圈搅动（★ §97）----------
      和上面 .thrashwave / .poundvid 都不完全一样，这里走的是**整幅替换**：
      片子里木槽、木搅棍、浆水、背景全都有（libtv frames2video，首帧 = 尾帧 = 底片），
      所以这层不要 mix-blend-mode、不要遮罩，opacity 直接到 1 盖在底片上。
      z-index 仍然是 1（和 .zoom 里的底片同层，按 DOM 顺序压在上面的才是它）。
      ⚠ 为什么不能像 §96 那样用 screen：screen 只能加亮 —— 用户回「棍子要动」，
        一根深色木棍转过去，加亮型混合永远表达不了。
      opacity 只在「手按在槽上**且片子已解码**」时抬起来（.on 由 play.js 的 syncStir 打，
      两个条件缺一个都不亮，否则黑片会整屏发黑）。 */
  /* ★ §98：这一层从「一条 <video>」变成「一个壳 ＋ 两条片」——
     壳只管 opacity 0/1（条件仍是「当前该显的那条解码好了」，不然整屏发黑，§97 老坑）；
     里面 `beat-floc.mp4`（抱团 → 散开，按进度定位）与 `beat-stir.mp4`（棍子画圈）
     由 play.js 用 display 硬切，切换点在两条片都是底片的那一帧上，不留叠影。 */
  .beatstir{position:absolute;inset:0;z-index:1;
    pointer-events:none;user-select:none;
    opacity:0;transition:opacity .28s linear}
  .beatstir.on{opacity:1}
  .beatstir video{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none}
  /* 淘洗每一遍的反馈（§74）：白水花 + 「+1」。§73 那版只打一个 1px 的淡蓝 .ripple，
    用户在手机上根本看不见（原话「淘洗没有反馈」）。 */
  .frame .splash{position:absolute;width:.5em;height:.5em;margin:-.25em;border-radius:50%;
    pointer-events:none;z-index:3;
    background:radial-gradient(circle,rgba(255,255,255,.95),rgba(214,235,245,.55) 52%,rgba(214,235,245,0) 72%);
    animation:splash .55s ease-out forwards}
  @keyframes splash{from{transform:scale(.35);opacity:1}to{transform:scale(3.4);opacity:0}}
  .frame .pop.wash{color:#e9f5fc;font-size:.9em}
  /* 篮子里的料团跟着搓洗晃一下（只推 w1/w2 料与水色两层，w0 垫在下面兜住掩膜边）。 */
  .thrashwater img.jig{animation:thrashJig .38s ease-out}
  @keyframes thrashJig{
    0%{transform:translate(0,0)}
    32%{transform:translate(-1.6px,.7px)}
    68%{transform:translate(1.4px,-.6px)}
    100%{transform:translate(0,0)}}
  /* 09 舂淘 · **真杵动片**（§80，2026-10-06）。用户看完 §79 那版「合成圆头」仍说
     「还是比较假」，点名「学习一下 3 环节」—— 于是照搬 03 捣打那条已经验证的路：
     libtv 的 Wan 3.0 / frames2video，首帧 = 尾帧 = 这一屏静止时的那一格，
     出的是一整幅真木杵起落；三条各钉在自己的料相底片上（t1 粗料 / t2 成团 / t3 淘净）。
     片子是不透明的整幅画（杵、料、石臼、木桶都在里面），所以它一就位就把
     底下那套「版子＋料层＋杵精灵」整片盖住 —— §73 的穿模顺带消失。
     三条同刻播放，只推 t2 / t3 的透明度（与 .thrashcol 同一套 q2 / q3），
     叠化时不会出现两个杵头。片子没就绪时这一层一直 opacity:0，
     露出来的还是那套老层，等于一条退路。 */
  .thrashvid{position:absolute;inset:0;width:100%;height:100%;z-index:1;
    pointer-events:none;opacity:0;transition:opacity .45s linear}
  .thrashvid.on{opacity:1}
  .thrashvid video{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none}
  /* ★ §81：这里**故意没有 transition**。三条片子里木杵的位置各不相同（t1 与 t2 差
     1.5~3.1% 画宽、那条淘净的偏左 5.4%），只要两条同时半透明就会出现第二根杵
     （用户 2026-10-07 报「拿过去之后，这边就虚影了」）。所以切换必须是**硬切**：
     父层 .thrashvid 那一条 .45s 的淡入（第一次就位时用）保留，逐条片子的 opacity 动画撤掉。 */
  body.no-ui .thrashvid{display:none!important}
  body.no-ui .thrashcol,body.no-ui .thrashwater,body.no-ui .thrashbowl,
  body.no-ui .thrashwave,body.no-ui .beatstir{display:none!important}
  /* 09 舂淘 · 「每一杵当场看得见」（§75，2026-10-06）。
     病根：料相看的是舂料次数，q2=(thrash-4)/6 —— **前 3 下画面一点变化都没有**，
     杵只是沿轴晃 0.5s ＋ 一个「嗵！」，用户读到的是「点了没反应」。
     这里给每一杵补两样**瞬时**的东西：凹坑 .dent ＋ 飞絮 .fluff。
     两样都不累积、不留痕（setTimeout 自己 remove），料相仍由 thrashcol 那三层负责
     —— 所以「点了 100 下画面还是那样」不会复现：每一下都有自己的落点回执，
     只是不会在画面上叠出 100 个坑。
     落点由 play.js 的 HEAD_CX / HEAD_CY 算好（杵头真正压进料面那一点），
     不跟手指走 —— 杵是沿自己那条轴砸的，手指点哪只决定「这一杵算不算」。
     ★ z-index 必须写 3：同 414 行的 .zup / 420 行的 .pop，不写就落到 auto=z0，
       被 .thrashcol / .thrashplate / .thrashpole（都是 z1）整片盖住，等于没加。 */
  /* ★ 渐变必须写 `ellipse closest-side`：默认的 closest-side 是**正圆**，
     半径只取短半轴（这里 3% 高的一半 ≈ 8.8px），在一个 2:1 宽的盒子里
     只会画出中间一小坨 —— 第一版就是这么写的，实拍（预览\_zoom\p1_dent.jpg）
     里几乎读不出来。 */
  .frame .dent{
    position:absolute;z-index:3;pointer-events:none;
    width:11%;height:3.4%;transform:translate(-50%,-50%);
    border-radius:50%;
    background:radial-gradient(ellipse closest-side,
      rgba(9,7,5,.95),rgba(14,11,8,.88) 52%,rgba(26,21,15,.5) 78%,rgba(26,21,15,0));
    box-shadow:inset 0 1px 0 rgba(255,248,232,.42),inset 0 -1px 1px rgba(0,0,0,.35);
    animation:dent .64s ease-out forwards}
  @keyframes dent{
    0%{transform:translate(-50%,-50%) scale(.52);opacity:0}
    16%{transform:translate(-50%,-50%) scale(1.14);opacity:.96}
    100%{transform:translate(-50%,-50%) scale(.80);opacity:0}}
  /* 飞絮：落点朝上扇形散开的 6 撮白麻絮（方向与落点由 play.js 的 dent() 写进
     --dx / --dy，单位 px）。两个关键帧都保留 translate(-50%,-50%) 基准，
     否则 6 撮会一起偏出去半个身位。 */
  .frame .fluff{
    position:absolute;z-index:3;pointer-events:none;
    width:3.4px;height:3.4px;transform:translate(-50%,-50%);
    border-radius:56% 44% 50% 50%/62% 58% 42% 38%;
    background:radial-gradient(circle at 42% 36%,
      #fff,rgba(246,242,232,.88) 66%,rgba(246,242,232,.1));
    box-shadow:0 0 2px rgba(255,252,244,.5);
    animation:fluff .80s ease-out forwards}
  @keyframes fluff{
    0%{transform:translate(-50%,-50%) scale(.6);opacity:0}
    20%{opacity:.95}
    100%{transform:translate(calc(-50% + var(--dx,0px)),
                          calc(-50% + var(--dy,0px))) scale(.45);opacity:0}}
  body.no-ui .dent,body.no-ui .fluff{display:none!important}

  /* ★ §84（2026-10-07）10 纸药 · 乙案（见 play.js 的 R.slime）。
     这一屏的底片换成了「镜头不动」的静帧片，画面里的动全部由**三层真素材**给：
        yao-leaf-1/2.webp  晒叶两态（摊平 / 卷边），差值层、带 alpha，直接盖；
        yao-thick.webp     桶里「稠到挂勺」那一态，差值层、带 alpha，跟稠度淡进来；
        yao-swirl.mp4      桶水旋涡的亮部差值片 —— **mp4 没有 alpha**，只能 screen。
     ★ §94（2026-10-07）：第三步「木桶画圈搅到挂勺」撤掉 → 上面 .yaothick / .yaoswirl
      这两条规则**已经没人用**了（play.js 连元素都不再创建）。**留着不删**：
      这两个文件还在 stage\ 里当孤儿素材存着，将来要做回「搅水」那一步可以直接复用。
     层序与 09 的 .thrashwave 同一条带子：z-index:1（底片在 .zoom 里也是 z1，
     这三层 DOM 靠后 → 压在上面），.vdim 是 z2，所以夜里压暗时它们跟着一起暗。
     ⚠ 两张晒叶**故意不写 transition**：它们是同一个主体的两态（摊平 / 卷边），
       交叉淡入的那半秒会出现两套叶子 —— §82 那个「两根杵」的毛病是同一个。
       切换由 play.js 硬切，落点上给一个「晒开」。 */
  .yaoleaf,.yaothick,.yaomash{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;z-index:1;opacity:0}
  /* 稠水跟着稠度一档档（0.07/档）淡进来，0.4s 把台阶抹平。 */
  .yaothick{transition:opacity .4s linear}
  /* ★ §91 臼口那团料的**两态**（干粗 → 湿细，见 play.js 的 R.slime）。
     ⚠ 这一对**和晒叶那一对不一样，是要 transition 的**：晒叶是「两个主体」
       （交叉淡入会出现两套叶子），而料相是同一块料的两个**状态**，同一个位置出的
       （背景差只有 2.5–2.9 灰阶），淡进去正好把「一杵一下」连成渐变。
       0.35s ≈ 一杵的节拍（POUND 一次上下 ≈ 0.3~0.5s），看着像杵把它捶细的。 */
  .yaomash{transition:opacity .35s linear}
  /* 旋涡只动 opacity、不动几何 —— 缩放一条 screen 混合的片子波纹会跟着缩放（§83 否过）。 */
  .yaoswirl{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    z-index:1;mix-blend-mode:screen;opacity:0;transition:opacity .5s linear}
  /* ★ §85（2026-10-07）舂那一段：纸药-舂.mp4（捣动片**已按遮罩烤到底片上**，
     遮罩外 78.5% 的像素就是底片本人 —— 见 play.js 的 R.slime 与顶部说明）。
     所以这一层**不挂 mask-image**：file:// 下 Chrome 拒载 mask 资源，一挂就整层消失
     （双击「启动游戏.cmd」那条路），这一屏会变成「杵根本没动」。
     ⚠ 没有 transition，**硬切**：交叉淡入会在「杵 ＋ 臼口」那一块同时显出两根杵
       （§82 那个毛病）。
     ⚠ 只动 opacity、不动几何：挂 transform 会让这一层和底片错位（§83 否过缩放）。 */
  .yaomortar{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    z-index:1;opacity:0}
  body.no-ui .yaoleaf,body.no-ui .yaothick,body.no-ui .yaoswirl,
  body.no-ui .yaomortar,body.no-ui .yaomash{display:none!important}

  /* 03 捶打：石臼里的料「整秆竹麻 → 捶散的雪白麻筋」。两档 libtv 真素材整幅溶接，
     木架、石臼、地面都在，两层之和恒为 1，过渡区不露底片。 */
  .poundcol{position:absolute;inset:0;width:100%;height:100%;z-index:1;
    pointer-events:none}
  .poundcol img{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    transition:opacity .6s linear}
  body.no-ui .poundcol{display:none!important}

  /* 03 捶打：碓头/碓杆整幅真素材，**没有蒙版**。
     2026-10-05 用户定调「不要用蒙版，可以多出图，或者渲染视频的形式」。
     之前那套是「原片 03捣打.mp4 再挂一层 + .poundhead 只留碓头那一竖条」：
     蒙版下沿贴着料面走，碓头砸到料面就被切掉，用户截图报「锤子到石臼后面去了」；
     往下压又会把底片的生青→捶散白盖住。两头不讨好，所以整条路线作废。
     现在改用 libtv 的 frames2video（首尾帧）以两版底片各出一整幅「一杵起落」循环片：
     碓头是片子自己画的、进臼口后由近侧臼沿自然遮挡，不需要任何遮挡层。
     两层都是整帧铺满，和 .poundcol 那两张底片同一个位置：
     下层粗料恒 1，上层捶散的 opacity = S.pound 的进度（play.js 里推）。
     片子没就绪时 .poundvid 一直 opacity:0，露出来的还是那两张底片。 */
  .poundvid{position:absolute;inset:0;width:100%;height:100%;z-index:1;
    pointer-events:none;opacity:0}
  .poundvid.on{opacity:1}
  .poundvid video{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none}
  .poundvid video:last-child{transition:opacity .6s linear}
  body.no-ui .poundvid{display:none!important}

  /* 12 压榨：纸垛「湿松含水 → 紧实压干」。同上整幅溶接。 */
  .presscol{position:absolute;inset:0;width:100%;height:100%;z-index:1;
    pointer-events:none}
  .presscol img{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    transition:opacity .6s linear}
  body.no-ui .presscol{display:none!important}

  /* 13 焙纸：贴在焙墙上的湿纸 → 干纸。同上整幅溶接；焙墙始终定住不动。 */
  .firecol{position:absolute;inset:0;width:100%;height:100%;z-index:1;
    pointer-events:none}
  .firecol img{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
    pointer-events:none;user-select:none;-webkit-user-drag:none;
    transition:opacity .6s linear}
  body.no-ui .firecol{display:none!important}
.culmhit{position:absolute;pointer-events:auto;cursor:pointer}

.node{
  position:absolute;width:1.5em;height:1.5em;margin:-.75em;border-radius:50%;
  border:1px solid rgba(233,227,214,.42);background:rgba(6,8,11,.55);
  display:grid;place-items:center;font-size:.66em;z-index:5;
  transition:border-color .2s,background .2s,transform .2s,color .2s;
}
.node i{font-style:normal;font-size:.86em;line-height:1;letter-spacing:0;
  color:rgba(233,227,214,.62);transition:color .2s}
.node:hover{border-color:rgba(233,227,214,.78);background:rgba(10,13,17,.72)}
.node:hover i{color:var(--ink)}
.node.on{border-color:var(--gold);background:rgba(58,46,20,.7);transform:scale(1.06)}
.node.on i{color:#f0e2c0}
/* 「点哪根 → 弹一条说明标签」整套已删（2026-10-04）。那条标签把答案写在脸上
   （「① 太嫩 · 新笋初长，纤维未成」），没有考察；而且窄高视口下会横跨到旁边
   那根秆身上。现在点一下只把「看出来的现象」写到下方 .tip 里，结论玩家自己下
   （见 play.js 的 R.bamboo）。 */

/* ---------- 上一版 UI 的两块文字：左上「品牌行 + 导语」/ 「器物说明卡」 ----------
   这两块原来是**印在素材里的**（PAPERMAKING 抬头 + 中文段 + 英文段、右中器物说明卡），
   用户：「之前一版的 ui 我觉得还可以，现在少了很多内容」——21 轮把 01/02/15 三屏换成
   干净底板后，连同这些字一起没了。现在照 选竹场景设计.md 的分区表用网页 UI 做回来：
     品牌行 x6.5–34 / y8.9–11.2 ｜ 中文段 x6.5–38 / y14.9–20.3 ｜ 英文段 y21.7–25.5
     器物说明卡（旧稿）x60–91 / y54–64
   ⚠ 旧稿那张说明卡的坐标**在新底片上已经作废**：重渲后三根秆整体右移（实测 ③ 号秆
   占 x68.5%~79.4%、y22%~72%），x60–91 / y54–64 正好整块压在秆身上。
   拿 ?ui=0 的纯素材图量过（644×1152，中位亮度网格）：
     左栏 x0~30% / y8%~70% 全黑（≤8）—— 是这一版唯一能放下整条文字栏的暗场；
     右栏 x79%~97% 虽然也暗，但只有 17.6% 宽，装不下这一段，而且上方 y17%~34% 是操作条。
   所以这一屏把两块摞成一条**左栏**：品牌行+导语（y5.3%~20.2%）→ 本页看点卡（让到 y21.2%）→
   器物说明卡（y52%~69%）。三块加起来正好落在左栏那条黑带里，一个像素都不压秆身。
   （2026-10-04 又往下让了 1.7%：顶上那格要给十六屏通用的 .seg「环节 / 工序」读数，
     实测 644×1152 —— .seg y100~142 / .lead y144~294 / 看点卡 y298~588 / 说明卡 y595~752，
     相邻两块最小间隙 2px，都不叠。）
   两块都是 pointer-events:none —— 只读的字，不许吃点击。 */
.lead{
  position:absolute;left:3.4%;top:5.3%;width:44%;z-index:4;pointer-events:none;
  font-size:min(.5em,12.6px);line-height:1.62;
  text-shadow:0 1px 3px rgba(0,0,0,.85);
}
/* 导语的窄一档：02 水沤（右上有专属操作条 .bar.tb，left 31.5% 起、top 7.4%）、
   以及 05 灰蒸、06 洗灰这两屏（底片上那一片有汽柱 / 天窗亮光，见下面 04~10 那条注释）。
   沿用 01 的 44% 宽（到 x47.4%）会横穿到操作条或亮场里。收到 27%
   （到 x30.4%，留 1.1% 空档），文字自然折行，和 01 同一套版式；
   多折一行，所以底下那张「本页看点」卡再让一格（y25.4%，见 play.js 的 LAYOUT）。 */
.lead.narrow{width:27%}
/* ★ 06 洗灰专用：十六屏里只有这一屏的导语位底下是亮的 —— 作坊天窗的白口从 x20.1%
   起、y3%~10.5% 实测 mean 74~82 / max 234（别的屏同一块 mean 4~12）。
   导语按统一的 y5.3% / 27% 停上去，中文末行「一瓢水翻一遍」正压在白口上，白字糊掉。
   位置一个字不动，只给这一屏补一块 .seg / .obj 同一族的墨底牌：底衬挂在 ::before 上、
   inset 全是负值，所以文字自身的折行宽度（27%）与左沿（x3.4%）和别的屏一模一样，
   不会被 padding 挤窄。z-index:-1 让它压在本层文字下面、又不出本层的层叠上下文。
   .lead 本来就是 pointer-events:none 的只读层，底衬也不吃点击。

   ★ 2026-10-06 用户截 06 洗灰左栏：「左侧没对齐」——量下来左沿一共四条线：
     金线三条（环节读数 x3.4% / 看点卡 x3.4% / **这块底衬 x2.33%**）、
     文字三条（导语 3.4% / 环节读数 4.35% / 看点卡 4.76%）。
     底衬那条金线之所以往左冒 1em，就是下面 inset 的左边给了 -1em。
     用户选了「金线对齐」：把整个 .lead 往右推 1em（margin-left:1em），
     inset 的左边仍是 -1em，**两者正好抵消** → 底衬金线落回 x3.4%，
     和「环节」「看点」并成一条；顺带导语文字也从 3.4% 挪到 4.47%，
     与「环节读数」的 4.35% 挨上了。
     两处的 1em 都取自 .lead 自己的 font-size（min(.5em,12.6px)），
     所以任何窗口宽度下都严格抵消。width:27% 是 content-box，没碰 padding，
     折行宽度与 05 灰蒸/02 水沤一模一样，不会多折行。 */
.lead.scrim{margin-left:1em}
 .lead.scrim::before{
  content:'';position:absolute;inset:-.75em -1.1em -.9em -1em;z-index:-1;
  border-left:2px solid rgba(201,169,97,.5);
  background:linear-gradient(180deg,rgba(6,8,12,.86),rgba(6,8,12,.58));
  box-shadow:inset 0 0 0 1px rgba(233,227,214,.07);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
}
.lead .ld-brand{display:flex;align-items:center;gap:.55em;font-family:var(--sans);
  font-size:1em;letter-spacing:.34em;text-transform:uppercase;color:rgba(233,227,214,.70)}
/* 那个小方「印」：细边框 + 一点金，和上一版左上角那枚印章同一个意思 */
.lead .ld-brand i{flex:0 0 auto;width:1.1em;height:1.1em;border-radius:.1em;
  border:1px solid rgba(233,227,214,.42);background:rgba(201,169,97,.16);
  box-shadow:inset 0 0 0 2px rgba(6,8,11,.55)}
.lead .ld-cn{margin:.6em 0 0;font-size:1.16em;letter-spacing:.02em;color:rgba(233,227,214,.86)}
.lead .ld-en{margin:.45em 0 0;font-size:.94em;color:var(--faint);letter-spacing:.01em}

/* 器物说明卡：接在「本页看点」卡下面，同宽同左沿，左栏底段（y52%~69%）。
   卡面底色提到 .88/.72：这一版底片左栏是纯黑，衬一层墨让字和黑底分得开。 */
.obj{
  position:absolute;left:3.4%;top:52%;width:27.4%;z-index:4;pointer-events:none;
  font-size:min(.5em,12.6px);line-height:1.6;
  padding:.8em .9em .75em .95em;
  background:linear-gradient(180deg,rgba(6,8,12,.88),rgba(6,8,12,.72));
  border-left:2px solid rgba(201,169,97,.42);
  box-shadow:inset 0 0 0 1px rgba(233,227,214,.07);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
}
.obj .ob-h{font-size:1.02em;letter-spacing:.14em;color:var(--ink)}
.obj .ob-p{margin:.44em 0 0;color:rgba(233,227,214,.74)}

/* ---------- 顶部左「环节 / 工序」读数（十六屏通用）----------
   2026-10-04 用户：「在顶部左边加一个环节和工序的 ui，是什么环节，有多少工序」。
   一块窄牌：上行「环节 01 / 15 · 选竹」，下行「本环节 6 道工序 · 第 1–6 / 72 道」。
   落位依据（预览\_shot_plain16，644×1152 的 ?ui=0 纯素材逐屏量）：
   帧顶 x0~38% / y0~4.2% 那条带十六屏全暗（mean 0.5~22.8、max ≤72），是唯一
   顶部整条空着的地方；其余各屏的看点卡都在 y14% 以下，不会撞。
   01 屏另有 .lead，原先起在 y3.6%：这里把 .lead 顺势下移到 y4.7%，
   末行落到 y20.2%，离「本页看点」卡（.legend.has-lead，y20.6%）还留 0.4% —— 不叠。
   只读，pointer-events:none；?ui=0 验收模式整块隐藏。 */
.seg{
  position:absolute;left:3.4%;top:.7%;z-index:5;pointer-events:none;
  padding:.34em .74em .3em .8em;
  font-size:min(.46em,11.6px);line-height:1.42;
  background:linear-gradient(180deg,rgba(6,8,12,.88),rgba(6,8,12,.64));
  border-left:2px solid rgba(201,169,97,.5);
  box-shadow:inset 0 0 0 1px rgba(233,227,214,.07);
  -webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);
  text-shadow:0 1px 3px rgba(0,0,0,.85);
}
.seg .sg-h{letter-spacing:.1em;color:rgba(233,227,214,.72);white-space:nowrap}
.seg .sg-h b{color:var(--gold);font-weight:600;letter-spacing:.04em}
.seg .sg-h em{font-style:normal;color:var(--ink);font-size:1.14em;letter-spacing:.08em}
.seg .sg-p{margin-top:.18em;letter-spacing:.04em;color:var(--faint);white-space:nowrap}
.seg .sg-p b{color:var(--gold);font-weight:600}
body.no-ui .seg{display:none}

/* ---------- 左下「本页看点」：替换掉视频里那张九屏通用的造纸工具图例 ---------- */
.legend{
  /* 2026-10-04 第五次反馈：「这里字体都挡着了，后面的也是」——
     固定 height:27.9% 的卡片装不下「标题 + 英文 + 三行看点 + 说明 + 七十二道」，
     .lg-rows 被 flex 压扁后第三行溢出到 .lg-note 上，两行字叠在一起
     （实测 644×1152：lg-rows 只给到 109px，内容要 124px）。十五屏通用。
     改成按内容定高：min-height 保住「必须盖住素材里那张烧录的造纸工具图例」
     （素材实测 y60%~80%，卡面 57.9%→85.8% 正好盖住），max-height 兜住底部导航。 */
  position:absolute;left:3.4%;top:53%;width:27.4%;
  /* min-height 31.4%：素材里那张烧录的「造纸工具」图例一直排到帧高 83.6% 处
     （最后一项「纸页 / sheet」），卡面止在 27.9% 时会让那一行露在卡下沿外面。
     31.4% 正好把整段图例收进卡里，同时离底部导航带还留 14px。 */
  /* 2026-10-04 第七次反馈：「这个下方一段空白」（卡片比内容高，底下空出一条）。
     新底板（_shot\_plates，?ui=0）上已经没有烧录的旧图例要盖了，卡片不再需要
     固定高度撑场面：min-height 改 auto，卡面随内容收高，底部不留空条。
     max-height 仍兜住底部提示条与工序轨。 */
  height:auto;min-height:auto;max-height:32.2%;z-index:4;
  padding:.85em .9em .7em .95em;display:flex;flex-direction:column;overflow:hidden;
  /* 大视口（993×1244 这类）上 .frame 的字号会涨到 30px，.6em 就是 18px，
     卡片一行行堆下来会顶到 max-height；给一个 15.2px 的上限，各视口版面一致。 */
  font-size:min(.6em,15.2px);line-height:1.26;
  /* 底色提到 .975/.955：素材里那张烧录的「造纸工具」图例（帘床/竹帘/丝线/压榨/纸页）
     行距比卡面宽，最后一项压在卡面下沿时还会从 .94 的底色里透出半个字（用户：
     「这里字体都挡着了」）。卡面本来就是盖住它的，加两分不透明度即可，不动版面。 */
  background:linear-gradient(180deg,rgba(6,8,12,.975),rgba(6,8,12,.955));
  box-shadow:inset 0 0 0 1px rgba(233,227,214,.09);
  border-left:2px solid rgba(201,169,97,.48);
  -webkit-backdrop-filter:blur(7px);backdrop-filter:blur(7px);
}

/* 干净底板那七屏（03 灰腌 / 04 灰蒸 / 05 洗灰 / 06 碱煮 / 07 枢料 / 08 舂淘 / 09 纸药）：
   这几屏的素材左下角**没有**烧录的旧版「造纸工具」图例要盖，卡片再停在左下就只是白压器物
   （用户在前几轮反复说的「UI 影响内容了，换个地方」）。
   改停到上方空白区：.tl 贴左、.tr 贴右，纵向落在 y14%~45%。
   这两块地方是拿 ?ui=0 的纯素材截图量过亮度的 —— 左上 max≤66（03/04/05/09），
   右上 max≤29（06/07/08），都是纯暗场；左右的选择见 play.js 的 LG_POS。
   卡片比左下那版收矮一档（30%~31%）并把字号压 2%，保证「标题＋英文＋三行看点＋说明＋
   七十二道」仍装得下（左下那版实测内容高 303px，这里留 300px）。 */
.legend.tl,.legend.tr{top:14%;min-height:auto;max-height:31%;font-size:min(.585em,14.8px)}
.legend.tr{left:auto;right:3.4%}
/* 带导语的那一屏（现在只有 01 选竹）：.lead 占掉左栏顶段 y3.6%~19.2%，
   卡片跟着让 6.6% 才不叠字（实测原来 y14% 起，导语英文末行正压在卡片标题上）。
   只有 .has-lead 的卡片走这一档，其余十四屏仍是 y14%，版面一点不动。 */
.legend.has-lead{top:21.2%}
/* 02 的导语窄（27%），同样字数会多折两行，卡片再往下让一格才不叠字。
   实测（644×1152）导语末行落在 y24.6%，这里取 25.4%，留 0.8% 空档。 */
.legend.has-lead.narrow-lead{top:25.4%}
.legend .lg-title{font-size:1.18em;letter-spacing:.08em;color:var(--ink);white-space:nowrap}
.legend .lg-en{margin-top:.26em;font-size:.66em;letter-spacing:.12em;color:var(--faint);
  text-transform:uppercase;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.legend .lg-rule{height:1px;background:rgba(233,227,214,.15);margin:.6em 0 .2em;flex:0 0 auto}
/* 三行看点按内容高度排，不再参与 flex 分配 —— 压扁才会溢出叠字。 */
.legend .lg-rows{flex:0 0 auto;overflow:visible}
.legend .lg-row{display:flex;flex-direction:column;gap:.06em;padding:.12em 0;
  border-top:1px dashed rgba(233,227,214,.075)}
.legend .lg-row:first-of-type{border-top:0}
.legend .lg-k{font-size:.78em;color:var(--dim);letter-spacing:.18em}
/* 值最多折两行。以前是 nowrap + 省略号，长句（「慈竹农历八九月，新秆未老化」）会被截成
   「慈竹农历八九月，新…」，信息看不全；卡片宽度固定，改成折行更实在。 */
.legend .lg-v{font-size:1em;color:var(--ink)}
.legend .lg-v .vt{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  overflow:hidden;line-height:1.22}
.legend .lg-v em{font-style:normal;color:var(--gold)}
.legend .lg-note{flex:0 0 auto;padding-top:.3em;font-size:.76em;color:var(--faint);
  letter-spacing:.04em;border-top:1px dashed rgba(233,227,214,.075)}
/* 「七十二道工序」逐屏落地：卡片最下面这一条写清这一屏管的是第几道到第几道。
   高 2 行、超了裁掉——卡片本身 overflow:hidden，宁可少一行也不把前面挤走。 */
.legend .lg-craft{flex:0 0 auto;margin-top:.3em;padding:.36em .48em .32em;
  border-left:2px solid rgba(201,169,97,.5);background:rgba(201,169,97,.07)}
/* 2026-10-04 第六次反馈：「这里字体都挡着了」之外，这行自己也排不下 ——
   「第 7–11 / 72 道 · 本环节 5 道」在 644 宽的卡面（内宽约 134px）里会被
   截成「本环节…」。卡高已经卡在 max-height 上，让它折行会顶到导航带，
   所以把字距收干净（.06em→0）字号也收两格（.72em→.64em）。
   实测三档视口：最长的一句（第 9–12 / 72 道 · 本环节 6 道）在 644 宽还剩
   6px 排不下，640 一档是 5px，收掉这两处之后 0px。十五屏通用。 */
.legend .lg-craft-h{font-size:.64em;letter-spacing:0;color:var(--dim);white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
.legend .lg-craft-h b{color:var(--gold);font-weight:600}
.legend .lg-craft-l{margin-top:.16em;font-size:.7em;line-height:1.25;color:var(--faint);
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}

/* ---------- 打浆圆环 ---------- */
.dial{
  position:absolute;width:3.2em;height:3.2em;margin:-1.6em;border-radius:50%;z-index:4;
  background:conic-gradient(var(--bamboo) calc(var(--p) * 1turn), rgba(255,255,255,.12) 0);
  -webkit-mask:radial-gradient(circle, transparent 56%, #000 57%);
          mask:radial-gradient(circle, transparent 56%, #000 57%);
  pointer-events:none;
}
.dial-txt{
  position:absolute;transform:translate(-50%,-50%);z-index:5;pointer-events:none;
  /* 这行字落在浆水上（画面最亮的地方），给一块小底衬，和操作条的读数保持一致 */
  font-size:.76em;color:var(--ink);white-space:nowrap;
  padding:.24em .6em;border-radius:.35em;border:1px solid var(--line2);
  background:rgba(8,10,13,.74);text-shadow:0 1px 2px rgba(0,0,0,.9);
  box-shadow:0 1px 5px rgba(0,0,0,.45);
}

/* ---------- 温度计 ---------- */
.gauge{
  position:absolute;right:3.4%;top:30%;width:.5em;height:15%;z-index:5;
  border:1px solid var(--line2);border-radius:.3em;background:rgba(8,10,13,.6);overflow:hidden;
}
.gauge i{position:absolute;left:0;right:0;bottom:0;background:linear-gradient(0deg,#8a5a2a,#d8892f);transition:.18s}
.gauge .band{position:absolute;left:0;right:0;border-top:1px dashed rgba(201,169,97,.55);border-bottom:1px dashed rgba(201,169,97,.55);background:rgba(201,169,97,.12)}
/* ★ 07 碱煮专用量条：2026-10-06 用户批注③（点在量条上）「移到右边」。
   上一轮它是贴**左边缘**停的（x3.4%，y33%）—— 当时 .legend 在右上 y36%，
   05 灰蒸那条「贴右下」会被卡片压住；而贴左正好和「卡在右上」错开。
   这一轮用户把卡片点名挪回左栏（批注①「移到左边对齐」，LAYOUT 6 = tl,l3.4,t21.2），
   两者就抢同一栏了：卡高实测 28.7%，从 21.2% 一直盖到 49.9%，量条（15% 高）
   横在中间会被卡面吃掉。所以两边对调 ——
   量条贴**右边缘** x3.4%（右沿与 .bar / .tip / .legend 的右沿同一条线），
   y 由 33% 下移到 36%：让开折成两行后的操作条（条底 ~30%），
   仍留在右栏那条背光暗墙上，不碰底部提示条（79.4%）与工序轨（84.8%）。 */
.gauge.r7{left:auto;right:3.4%;top:36%}

/* ---------- 定制面板 ---------- */
.panel8{
  /* 改成**贴底排**（bottom 而不是 top）：年画印完之后这一屏会多一条「彩蛋回执」
     （.note8.nyNote），按 top 起排会把「定稿」压到底部提示条上（.tip 的顶边在
     79.4%）。贴底排之后，回执有没有、有几种，主按钮都停在提示条上方，不会再撞。
     bottom 21% = 提示条顶边（79.4%）上面一条窄缝。 */
  position:absolute;left:31%;right:5%;bottom:21%;z-index:5;
  display:flex;flex-direction:column;gap:.3em;
}
.row8{display:flex;align-items:center;gap:.4em}
.row8>span{flex:0 0 3.4em;font-size:.76em;color:var(--dim);letter-spacing:.1em;
  text-shadow:0 1px 5px rgba(0,0,0,.9)}   /* 标签直接压在画面上，faint 在暗部看不清 */
.chips8{display:flex;gap:.28em;flex:1 1 auto}
.pick{
  flex:1 1 0;max-width:calc((100% - .56em)/3);   /* 封顶成 1/3 宽：两选项那行（落款）才不会抻满整行 */
  padding:.42em .2em;border:1px solid var(--line2);border-radius:.3em;
  background:rgba(8,10,13,.6);font-size:.76em;color:var(--dim);text-align:center;transition:.16s;
}
.pick.on{border-color:rgba(201,169,97,.6);color:#f0e2c0;background:rgba(60,48,26,.5)}

/* ★ 12 抄纸 · 右栏竖栏（§103，用户「这个环节的ui要进行一下设计，都挤在一块了」）----------
   §102 时这一屏中带是**两条叠着**：上一条「加料」四档（.bar.mat，x33%~96% /
   y37.6%~42%），下一条读数与「揭帘」（.bar 默认档，x4%~96% / y45.2%~51.4%）——
   两条只差 3.2% 帧高；而且下面那条的左半截还压着左栏看点卡的下沿：卡实测
   y21.2%~48.13% / x3.4%~30.8%，探针量到**重叠 2.93% 帧高**，读数那三颗字直接
   糊在卡的最后两行上。用户读到的「都挤在一块了」是这两件事加在一起。

   甲案（2026-10-07 用户核定）：「加料」四档从横条改成**右栏的竖栏**、一颗一行、
   右沿对齐 x95.5%（与 .tip / .caps / 02 水沤的 .bar.tb / 10 纸药的 .bar.tb.r9
   同一条金线），中带只剩读数那一条。落位依据（?ui=1 实拍帧，x69%~97% 逐 5% 一行）：
     y5%~10% mean 10.7 / y15%~20% mean 41.2 / y20%~30% mean 77~87（露出的堆竹）
     / y35%~40% mean 60 —— 整条都在作坊背光的暗墙与堆竹上，而 .pick / .chip 那种
     rgba(8,10,13,.8) 的深底本来就是为这种中灰背景设的（见 .chip 的注释）。
   top 取 5.6%：上面让开右下角的音效按钮（.snd 是 fixed，占 y0.9%~4.0%）；
   四档 + 「加料」标签实测占到 y26.8%，离木槽前沿（y42% 才进来）还隔一大截，
   不压水面、也不进 .fx 热区。只给 12 抄纸用，另外十五屏的版面一个像素不动。 */
.bar.mat{left:auto;right:4.5%;top:5.6%;height:auto;width:auto;
  flex-direction:column;align-items:flex-end;flex-wrap:nowrap;row-gap:.24em}
.bar.mat>.row8{flex:0 0 auto;flex-direction:column;align-items:flex-end;gap:.18em}
.bar.mat .row8>span{flex:0 0 auto;font-size:.74em}
.bar.mat .chips8{flex:0 0 auto;flex-direction:column;align-items:stretch;gap:.22em}
.bar.mat .pick{flex:0 0 auto;max-width:none;padding:.26em .55em;font-size:.8em;white-space:nowrap}
/* ★ §108 用户「加料锁在进水前」：入池过一轮 → 这一条整体锁死。
   ⚠ 走 pointer-events:none 而不是 disabled：按钮不接事件，点击落到 .bar.mat 上，
   play.js 那一枪会报一句「料在进水前就定了」，比灰着毫无反应清楚（见 R.dip 里那两处闸）。 */
.bar.mat.locked .pick{opacity:.42;pointer-events:none;filter:grayscale(.55)}
.bar.mat.locked .row8>span{opacity:.6}

/* 同屏的读数条（.bar.r12）原地不动，只**往右**让开左栏看点卡：卡右沿 x30.8%，
   条从左 33% 起，实测三个元素（入池 / 厚薄 / 揭帘）右沿收到 x88%，
   不再有 2.93% 那一段叠字。纵向仍停原来的 y45.2%。 */
.bar.r12{left:33%}

/* ---------- 12 抄纸 · 换料画面层 ----------
   三张有料的料图（麻 / 花 / 粗；不加料不占图）是 libtv image2image 对着**同一格底片**出的
   （概念图\libtv\gen-dip-material.ps1），
   只在浆水面上多了一样东西，构图/机位/木架/竹帘全对得上，所以直接盖一层就行。

   ⚠ 为什么**不是整幅盖**（03 捣打的 .poundvid、09 舂淘的 .thrashvid 都是整幅）：
   那两条盖的是**片**，动的东西还在动；这里盖的是一张**静帧**，整幅盖下去
   底片那条 05抄纸.mp4 的水面就不动了 —— 等于用「加了个料」换掉「这一屏是活的」。
   料图与底片的差异本来就只在浆水面那一带（_matpost.py 就是按 y0.50~0.86 之外做配色拟合的，
   带外 mean|Δ| 只有 2.07~2.75），所以**只在那一带显形**：上下各留一段渐变淡出，
   淡出区里两层几乎逐像素重合，看不出接缝；带外的水面仍在走底片的动画。

   ★ §105：**动作层在场时这一层让位**。用户这一轮报「目前的动画，将加料弄没了」——
   就因为下面那条 .dipswing 是**整幅真片**（z 3），一按住就把这一层盖掉，料看不见了。
   甲案（用户已确认）改成「每种料各出一套动作片」之后，料已经画在片子里，
   静帧再压上去只是抢同一块水面，所以 play.js 的 actShow() 里：只要当前那条片
   `data-ok==='1'`，就把这一层的 `on` 摘掉；这一层收起（actKey 空）时按
   `S.material>0 && matReady` 恢复。**z-index 不动**（仍是 2 < 3），让位是逻辑上的。 */
.dipmat{position:absolute;inset:0;z-index:2;pointer-events:none;user-select:none;
  opacity:0;transition:opacity .35s linear;
  -webkit-mask-image:linear-gradient(180deg,transparent 42%,#000 52%,#000 83%,transparent 91%);
          mask-image:linear-gradient(180deg,transparent 42%,#000 52%,#000 83%,transparent 91%)}
.dipmat.on{opacity:1}
.dipmat img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  pointer-events:none;user-select:none;-webkit-user-drag:none}
body.no-ui .dipmat{display:none!important}

/* ★ 12 抄纸 · 帘上的纸絮层（§108，用户「进水前，要让先判断是否加料，根据选择，
   再进行抄纸，入池次数不同，抄捞的帘上，应该有不同的纸絮，最后变成湿纸」）-------
   一个类挂 12 张静帧（4 档料 × 3 档絮，play.js 的 paintFloc() 按入池次数选档）：
     dip-floc-<k>-1.jpg  薄：刚提起，薄得还透得出竹丝
     dip-floc-<k>-2.jpg  中：竹丝透不太清，纸面平整还带水光
     dip-floc-<k>-3.jpg  湿纸：一张完整的湿纸，边缘压在木框内侧
   ⚠ 显隐规则与 .dipmat 同源（play.js 的 paintFloc）：只有「帘回到静置（mode==='idle'）
   且这一张絮已经解出来」才亮 —— 动作层（.dipswing，z 3）在场时让位，那三条片里
   帘已经在水里、絮也画在片子里了，再压一层静帧只会抢同一块帘面。
   ⚠ 蒙版只露**帘**那一块：出的图是整幅的（机位/槽/浆水/木勺都照搬参考），
   只有帘面在变。用一个软的椭圆罩住帘（实测底片 600×900：
   帘的包围框 x 0.21–0.72 / y 0.39–0.65，见 预览\_tr14\_flocpost.py 里那条 FLOC_*），
   罩外一律不显形 —— 罩外的像素跟底片本来就对得上（_flocpost.py 已经按帘以外的区域
   做过配色最小二乘），过渡带看不出来。 */
.dipfloc{position:absolute;inset:0;z-index:2;pointer-events:none;user-select:none;
  opacity:0;transition:opacity .3s linear;
  -webkit-mask-image:radial-gradient(ellipse 34% 22% at 46.5% 52%,#000 0 44%,transparent 92%);
          mask-image:radial-gradient(ellipse 34% 22% at 46.5% 52%,#000 0 44%,transparent 92%)}
.dipfloc.on{opacity:1}
.dipfloc img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  pointer-events:none;user-select:none;-webkit-user-drag:none}
body.no-ui .dipfloc{display:none!important}

/* ★ 12 抄纸 · 帘的动作层（§105，用户「帘要全部没进去、沉一秒、出来一半、反复」＋
   「目前的动画，将加料弄没了」）----------------------------------------------------------
   一个类挂**每档三条** libtv `frames2video` 出的片（play.js 的 PARTS + srcOf()），
   同一时刻只亮一条，换料时整套换 src：
     dip-entry-<k>.mp4  首帧＝该档静置图、尾帧＝deep-<k>.png：按下水面，帘缓缓沉到整床没入
     dip-loop-<k>.mp4   首帧＝尾帧＝deep-<k>.png：水下静 1.0s → 抬出一半 → 沉回（loop）
     dip-lift-<k>.mp4   ＝ entry 倒放：点「揭帘」，帘从水里提起来、回到静置那一格
   `<k>` 四档跟 MAT 走：plain / ma / flower / baba。
   `_tr13\_dippost.py` 只做本地该做的事：配色对齐到**浆水带之外**（entry 第 0 帧 vs 静置图带外
   mean|Δ| 3.27 → 2.39、loop 第 0 帧 vs entry 末帧 2.43 → 1.42），静态区三块逐帧最大 |Δ| 个位数
   到十几，四个切点全在 1.4~2.4 —— 硬切进来、循环回去、提完帘回静置都不跳景别，
   不必再写遮罩（.dipmat 那种「只在浆水带显形」的遮罩是给**静帧**用的，
   防的是「用一张静图换掉这一屏是活的」；这里是片，动的东西本来就该在动）。

   ⚠ **每档一套片 ＝ 「加料被盖掉」的根修**。§104 只有一套素纸片，换料层的料
   （.dipmat，一张静帧）一被这层整幅真片盖住就没了。甲案（用户确认）改成片子自带料相：
   加麻/粗料各有自己的 entry+loop+lift（§108b 前还有一档花叶），料画在片子里，页面侧再让 .dipmat 让位
   （actShow() 里 `data-ok==='1'` 就摘 on，见上面 .dipmat 那段）。**z-index 不动**（仍是 2 < 3）。
   ⚠ **「抬出一半」是本地截出来的**：libtv 那条 loop 中间几格是整床抬到空中，用户只要一半；
   `_dippost.py` 的 loop_half() 量升段曲线、取到峰值一半那一帧折回（循环体＝升段＋升段倒放，
   两端同为全没入那一格）⇒ 环接缝天然为零，不用再赌一次模型。
   ⚠ `object-fit:fill`：源片 1440×2176 的宽高比与底片差 0.7%，fill 正好把这个缩放
   反算回去；底片本身在 .frame 里走 cover、实际只裁掉 0.06%，两层逐像素对得上。
   ⚠ 片子没就绪（`data-ok` 不是 '1'）时那一层不显，露出来的还是底片那一格 —— 不回黑，
  与 03 捣打的 .poundvid / 09 舂淘的 .thrashvid 同一条老规矩。
  ⚠ **这一层必须是硬切，不给 opacity 过渡**（★ §108c，用户「在抬起来的时候会出现两个板子的
  幻影」＋「放下去的时候也有，会突然闪一下」）。原来这里是 `transition:opacity .28s linear`：
  淡的这 0.28s 里上面是「帘没在水里」的动作片、下面是不跟着淡的底片静置格（帘架在水上），
  **两层各有一块板** ⇒ 双影；两块板叠合那一刻亮度跳一下 ⇒「突然闪一下」。
  两条动作片之间（entry ↔ loop ↔ lift）也是同一件事。改成硬切之后双影消失，且下面四个切点
  本来就逐像素对齐（_tr14\_cutcheck.py：点击 2.49 / settle 2.44 / 抬起 2.52 / 回静置 2.85），
  硬切看不出跳变 —— 与上面 1430 行「四个切点全在 1.4~2.4，硬切进来…都不跳景别」对得上。
  ⚠ 硬切的**配套**：play.js 的 playOnce() 改成「先 seek 回首帧、seeked 之后再显形」。
  entry 那一条上一轮停在自己的末帧（全没入，与静置格 mean|Δ| 10.59），不先回帧就显形会闪一帧。 */
.dipswing{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;
  z-index:3;pointer-events:none;user-select:none;-webkit-user-drag:none;
  opacity:0}
.dipswing.on{opacity:1}
body.no-ui .dipswing{display:none!important}

.note8{font-size:.72em;line-height:1.5;letter-spacing:.04em;color:#e8d9b4;
  text-shadow:0 1px 5px rgba(0,0,0,.9)}
.panel8 .btn{align-self:flex-end;margin-top:.2em}

/* ---------- 成品透光 ---------- */
.lamp{position:absolute;inset:0;z-index:4;pointer-events:none;
  background:radial-gradient(circle calc(var(--r,22%)) at var(--x,50%) var(--y,40%),
    rgba(255,244,214,.30) 0 38%,rgba(255,244,214,.10) 62%,transparent 78%);
  mix-blend-mode:screen;opacity:0;transition:opacity .5s}
.lamp.on{opacity:1}
.fiber{position:absolute;inset:0;z-index:4;pointer-events:none;opacity:0;transition:opacity .5s;
  mix-blend-mode:screen;
  -webkit-mask-image:radial-gradient(circle calc(var(--r,22%)) at var(--x,50%) var(--y,40%),#000 0 55%,transparent 82%);
          mask-image:radial-gradient(circle calc(var(--r,22%)) at var(--x,50%) var(--y,40%),#000 0 55%,transparent 82%);}
.fiber.on{opacity:.85}

/* ---------- 结果页 ---------- */
.result{display:flex;height:100dvh;align-items:center;justify-content:center;background:
  radial-gradient(120% 80% at 50% 0%,#15171c 0%,#06070a 70%)}
.sheet{
  --u2:min(100vw,66.66dvh);
  width:var(--u2);height:calc(var(--u2) * 1.5);position:relative;
  background:
    repeating-linear-gradient(90deg,rgba(120,110,90,.07) 0 1px,transparent 1px 4px),
    linear-gradient(160deg,#efe7d5,#ddd2bb 60%,#cabfa6);
  color:#26231d;padding:7% 8%;display:flex;flex-direction:column;
  font-size:calc(var(--u2) * .034);
  box-shadow:0 30px 80px rgba(0,0,0,.65);
}
.sheet h2{margin:0;font-size:1.9em;letter-spacing:.14em;font-weight:600;font-family:var(--kai)}
.sheet .sub{margin:.3em 0 0;font-size:.8em;letter-spacing:.3em;color:#6c6350}
.hr{height:1px;background:rgba(60,52,38,.22);margin:.75em 0}
.grade{display:flex;align-items:baseline;gap:.6em;margin:.2em 0 .4em}
.grade b{font-size:2.5em;letter-spacing:.06em;font-family:var(--kai)}
.grade em{font-style:normal;font-size:.86em;color:#6c6350;letter-spacing:.1em}
.grade .sc{font-size:1.1em;color:#8a5a2a}
.tagbloom{font-size:.72em;letter-spacing:.14em;color:#8a5a2a;align-self:center;
  border:1px solid rgba(138,90,42,.45);border-radius:.2em;padding:.1em .45em}
/* 结果页纸面上的装饰层（.bloom 花草纸花叶，★ §108b 已退役）只剩纸角那一版年画。
   用 :where() 把权重让掉：写成 `.sheet>*:not(.nyPrint)` 是 (0,2,0)，
   会盖过下面 `.sheet>.seal` 的 (0,2,0) —— 印章直接丢掉 absolute，被挤到左边压住参数行。 */
.sheet>*:where(:not(.nyPrint)){position:relative;z-index:1}
.sheet>.seal{position:absolute;z-index:2}
.bars{display:flex;flex-direction:column;gap:.3em;margin:.2em 0 .1em}
.bline{display:flex;align-items:center;gap:.5em;font-size:.82em}
.bline>span{flex:0 0 4.6em;color:#5d5545}
.bline .track{flex:1 1 auto;height:.42em;background:rgba(60,52,38,.14);border-radius:1em;overflow:hidden}
.bline .track i{display:block;height:100%;background:linear-gradient(90deg,#7d9670,#8a5a2a)}
.bline>em{flex:0 0 2.6em;text-align:right;font-style:normal;color:#5d5545;font-size:.9em}
.words{font-size:.86em;line-height:1.78;color:#3d382e}
.words p{margin:.2em 0}
.words .lore{margin-top:.35em;font-size:.82em;line-height:1.62;color:#6c6350}
/* 参数行要给纸角那方朱印让出一栏：留出 padding-right，长行会整条折下去，
   文字右沿停在印的左边（不留的话「打浆 搅动 0」正好顶进印框里）。 */
.facts{display:flex;flex-wrap:wrap;gap:.3em .8em;font-size:.76em;color:#5d5545;padding-right:2.6em}
.facts b{font-weight:600;color:#332f26}
.seal{
  position:absolute;right:4%;bottom:11%;width:3.2em;height:3.2em;border:.14em solid #b7402f;border-radius:.2em;
  display:grid;place-items:center;color:#b7402f;font-size:1em;letter-spacing:.05em;transform:rotate(-4deg);opacity:.9;
}
.seal i{font-style:normal;font-size:1.1em;font-family:var(--kai)}
/* 彩蛋② 夹江年画：题款屏上印的那一版，落在结果页纸面的右上角。
   和已退役的花草纸一样是装饰层（z-index:0），文字层全部抬在上面一层，不会压字。
   右上角只有 h2/副题所在的左上区之外的留白——题目两行都是左对齐，右上是空的。 */
.nyPrint{position:absolute;right:7.5%;top:5.6%;width:4.4em;height:4.4em;z-index:0;
  opacity:.9;transform:rotate(-3deg);pointer-events:none}
.nyPrint svg{display:block;width:100%;height:100%}
.sheet .acts2{display:flex;flex-wrap:wrap;gap:.5em;margin-top:auto;font-size:.82em}
.sheet .btn{border-color:rgba(40,34,24,.3);background:rgba(255,255,255,.35);color:#2c281f}
.sheet .btn:hover{background:rgba(255,255,255,.6)}

/* ---------- 底部工序轨道 ----------
   素材在 y 86.4%~89.0% 烧了一排灰胶囊（`概念图\视频\成品` 里那一条），
   用户连着两次说它丑。那排只有导航功能、不含画面信息，所以这里在游戏层重画：
    一条细轨道 + 一批节点（2026-10-04 起是**十五颗**，对应十五个环节；
    题款不算环节、不进这一排，见 play.js 的 CAP_LIST），
   当前环节金色并放大，走过的环节暗金，没到的灰。

   带子的上下用同底色渐隐（不是模糊、不是色块——素材这块本身就是近黑的导航底），
   所以换屏时不会在画面里切出一条硬边。每颗的热区仍可点，能直接跳屏；
    槽宽由 play.js 按 CAP_LIST.length 现算（左右各留 4.5%，十五颗即每颗 6.067%）。
   等以后重渲染素材时，可以用 -vf 把这排胶囊从画面里去掉，见 预览\待办_素材重渲染.md。 */
/* 底衬必须**比工序轨道本身高**：素材里烧录的灰胶囊在帧高 90.3%–92.2%
   （993×1244 实测 y 1124–1147），而轨道自己的盒子只有 84.8%–91.4%，
   底衬的实心段到 91.4% 就淡出了——宽屏 / 窄高视口下正好把那排灰胶囊露在下面，
   变成上下两层工序条（用户第三条评论：「下面这里还有上一轮的步骤」）。
   现在盒子放到 84.8%–94.4%：实心段 11%–80%（帧高 85.9%–92.5%）完整包住那排胶囊，
   下沿再渐隐到 94.4%——那一段素材本身就是近黑（实测 (1,1,3)~ (4,8,15)），看不出边。
   `.cap` 的高度百分比跟着盒子一起改（68.8% = 原来的 82px），轨道、节点、工序名的
   位置一律不动。 */
.caps{position:absolute;left:0;right:0;top:84.8%;height:9.6%;z-index:6;pointer-events:none;
  background:linear-gradient(180deg,
    rgba(4,5,8,0) 0%, rgba(4,5,8,1) 11%,
    rgba(4,5,8,1) 100%)}
/* 下沿原本在 80%→100% 渐隐（那时盒子外就是素材的近黑底，看不出边）。现在盒子外
   要接黑块，渐隐就等于把画面又透出来一层（实测 94.4% 上方两行仍露出石块，mean 19.8），
   所以实心段一直铺到盒子底。 */
/* 2026-10-04 反馈：「这个下方一段空白」。工序轨盒子下沿 94.4% 到帧底还剩 5.6%
   （790 档 = 44px，实测那段 mean 14.3 / max 255，石块和水面还在画面里），
   用户给了两个方案：把环节图下移，或用黑色遮挡。环节图不能动——素材里烧录的
   灰胶囊在 90.3%–92.2%，下移会让它落在渐隐段上露出来。所以走遮挡：伪元素从
   盒子底边接着铺到帧底，颜色取 `.caps` 实心段的同值 #040508，接缝两边一致，
   不会多出一条硬边。帧外部分由 .frame 的 overflow:hidden 裁掉。 */
.caps::after{content:'';position:absolute;left:0;right:0;top:99.5%;height:60%;
  background:#040508}
.cap{position:absolute;top:0;height:68.8%;border:0;background:none;padding:0;
  pointer-events:auto;color:rgba(233,227,214,.34);
  font-size:.44em;letter-spacing:.16em;transition:color .22s ease}
.cap::before{content:'';position:absolute;left:50%;right:-50%;top:38%;height:1px;
  background:rgba(233,227,214,.13)}
.cap:last-child::before{display:none}
.cap i{position:absolute;left:50%;top:38%;width:.3em;height:.3em;border-radius:50%;
  transform:translate(-50%,-50%);background:rgba(233,227,214,.34);transition:.24s}
.cap em{position:absolute;left:50%;top:58%;transform:translate(-50%,-50%);
  white-space:nowrap;font-style:normal}
.cap:hover{color:rgba(233,227,214,.78)}
.cap:focus-visible{outline:1px solid rgba(201,169,97,.55);outline-offset:3px;border-radius:.25em}
.cap.done{color:rgba(233,227,214,.5)}
.cap.done i{background:rgba(201,169,97,.62)}
.cap.done::before,.cap.cur::before{background:rgba(201,169,97,.4)}
.cap.cur{color:var(--gold)}
.cap.cur i{width:.62em;height:.62em;background:var(--gold);
  box-shadow:0 0 .55em rgba(201,169,97,.55)}

.pager{position:fixed;right:6px;top:50%;transform:translateY(-50%);z-index:20;
  display:flex;flex-direction:column;gap:7px;align-items:center}
.pager button{width:6px;height:6px;border-radius:50%;background:rgba(255,255,255,.18);transition:.2s}
.pager button.on{background:var(--gold);transform:scale(1.35)}
.pager button:hover{background:rgba(255,255,255,.5)}

/* ---------- 音效开关（右上角） ----------
   宽屏时落在画面外的暗底上，完全不碰素材；窄屏（画面满宽）时在右上角，
   那里视频没有烧录任何东西（烧录块是左下工具图例、右中说明卡、右缘圆点、左上正文）。 */
.snd{
  position:fixed;right:10px;top:10px;z-index:70;width:36px;height:36px;
  display:grid;place-items:center;border:1px solid var(--line2);border-radius:.5em;
  background:rgba(8,10,13,.62);box-shadow:0 1px 6px rgba(0,0,0,.45);transition:.18s;
}
.snd svg{width:20px;height:20px;fill:none;stroke:rgba(233,227,214,.82);stroke-width:1.7;
  stroke-linecap:round;stroke-linejoin:round}
.snd .spk{fill:rgba(233,227,214,.82);stroke:none}
.snd .sl{opacity:0;stroke:#e6b27a}
.snd.off .wv{opacity:0}
.snd.off .sl{opacity:1}
.snd.off .spk{fill:rgba(233,227,214,.55)}
.snd:hover{border-color:rgba(201,169,97,.6);background:rgba(20,24,30,.82)}

/* ---------- toast ---------- */
.toast{
  position:fixed;left:50%;bottom:6%;transform:translate(-50%,1em);z-index:30;
  padding:.55em 1em;border:1px solid var(--line);border-radius:.35em;
  background:rgba(8,10,13,.9);color:var(--ink);font-size:14px;letter-spacing:.06em;
  opacity:0;pointer-events:none;transition:.35s;max-width:80vw;text-align:center;line-height:1.6;
}
.toast.on{opacity:1;transform:translate(-50%,0)}

@media (max-width:520px){
  .chip{font-size:.76em}
  .btn{font-size:.86em}
}
/* 十五颗时槽宽只有九颗的六成：窄屏（390 那一档的字面只有 6px）把工序名收起来，
   只留节点，当前那颗放大——名字在左下「本页看点」卡片和提示条里都有。
   宽屏 993 及以上保留工序名。 */
@media (max-width:620px){
  .cap em{display:none}
  .cap i{width:.44em;height:.44em}
  .cap.cur i{width:.86em;height:.86em}
}

/* ---------- 彩蛋② · 夹江年画：印一张年画（16 题款屏） ----------
   放在题款屏（第 16 屏，成纸之后）的原因：夹江年画本来就是印在夹江纸上的，
   纸做好了才谈得上印一张；放在结尾也正好是「作品成型」的收口。
    它不占七十二道、不计分，只是纸上的一个去处。

   四步：刷色（在线版上再刷三块色版）→ 覆纸 → 刷印 → 揭纸。
   雕版（.nyPlate）按刷了几版显色——线版常在，其余三版刷一版露一版；
   纸（.nySheet）盖上时是白纸，揭起来才带上套印好的画（错位是故意的）。
   这一块是**面板位置**的替代：打开时 .panel8 加 .hide 让位，不叠在五行走廊上。 */
.panel8.hide{display:none}
.nyP{
  position:absolute;left:33%;right:5%;top:40%;z-index:6;
  display:flex;flex-direction:column;gap:.34em;
  padding:.58em .72em .62em;border:1px solid rgba(201,169,97,.34);border-radius:.45em;
  background:linear-gradient(180deg,rgba(15,14,12,.93),rgba(8,9,11,.93));
  box-shadow:0 16px 40px rgba(0,0,0,.62);
}
.nyHd{display:flex;align-items:baseline;gap:.6em}
.nyHd b{font-family:var(--kai);font-size:.94em;font-weight:600;letter-spacing:.16em;color:var(--gold)}
.nyHd em{font-style:normal;font-size:.72em;letter-spacing:.18em;color:var(--dim)}
.nyBd{display:flex;gap:.85em;align-items:flex-start}
/* 版：木纹底 + 内框，四块版共用一块版身 */
.nyStack{position:relative;flex:0 0 auto;width:6.1em;height:6.1em;border-radius:.22em;
  background:
    repeating-linear-gradient(94deg,rgba(0,0,0,.18) 0 1px,transparent 1px 6px),
    linear-gradient(150deg,#7d6446,#5b462f 58%,#443422);
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.55),0 6px 16px rgba(0,0,0,.5)}
.nyStack::after{content:'';position:absolute;inset:.3em;border:1px solid rgba(0,0,0,.32);
  border-radius:.12em;pointer-events:none;z-index:3}
/* 版身用浅木色：墨线是近黑的（NY_INK #2b2620），压在深木面（.nyStack）上根本看不见。
   版身浅、外框深，既读得出线，也仍然像一块嵌在木块里的雕版。 */
.nyPlate{position:absolute;inset:.3em;display:block;padding:.22em;border-radius:.1em;
  background:
    repeating-linear-gradient(96deg,rgba(118,90,52,.16) 0 1px,transparent 1px 7px),
    linear-gradient(150deg,#d3b98e,#bd9f72 62%,#a98a5e);
  cursor:crosshair;touch-action:none}
.nyPlate svg{display:block;width:100%;height:100%}
/* 刷一版露一版；四步走完才是套印成品 */
.nyPlate g.c1,.nyPlate g.c2,.nyPlate g.c3{opacity:0;transition:opacity .28s ease}
.nyPlate.ink2 g.c1{opacity:1}
.nyPlate.ink3 g.c2{opacity:1}
.nyPlate.ink4 g.c3{opacity:1}
/* 覆在版上的纸：盖着时是白纸，揭起来才显画 */
.nySheet{position:absolute;inset:.3em;display:block;padding:.22em;border-radius:.1em;
  background:
    repeating-linear-gradient(90deg,rgba(140,124,96,.1) 0 1px,transparent 1px 5px),
    linear-gradient(150deg,#f5eeda,#e6dabf 68%,#d6c8ab);
  box-shadow:0 8px 20px rgba(0,0,0,.5);
  opacity:0;transform:translateY(-16%) rotate(-1.8deg);pointer-events:none;cursor:crosshair;
  touch-action:none;transition:opacity .3s ease,transform .5s cubic-bezier(.3,.72,.3,1)}
.nySheet.on{opacity:1;transform:none;pointer-events:auto}
/* 揭纸：纸不飞走，只微微掀起一角，把印好的一面亮出来——
   飞走的话玩家只看见「版上有画」，看不到「画在纸上」。 */
.nySheet.lift{transform:translateY(-7%) rotate(-1.2deg);pointer-events:none}
.nySheet svg{display:block;width:100%;height:100%;opacity:0;transition:opacity .3s linear}
.nySheet.lift svg{opacity:1}
/* 套印的轻微错位：四块版对不齐才是木版年画的样子（不是模糊、不是重影） */
.nySheet svg g.c1{transform:translate(.9px,.7px)}
.nySheet svg g.c2{transform:translate(-.8px,.9px)}
.nySheet svg g.c3{transform:translate(.7px,-.8px)}
.nySide{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:.3em}
.nySide>b{font-family:var(--kai);font-size:.88em;font-weight:600;letter-spacing:.14em;color:#f0e2c0}
.nySide>em{font-style:normal;font-size:.72em;line-height:1.6;letter-spacing:.08em;color:var(--dim)}
.nyVers{display:flex;flex-direction:column;gap:.14em;margin-top:.16em}
.nyVers i{position:relative;font-style:normal;font-size:.68em;letter-spacing:.16em;
  padding-left:.95em;color:rgba(233,227,214,.32);transition:color .24s}
.nyVers i::before{content:'';position:absolute;left:0;top:.42em;width:.42em;height:.42em;
  border-radius:50%;background:rgba(233,227,214,.2);transition:.24s}
.nyVers i.v2::before{background:rgba(183,64,47,.5)}
.nyVers i.v3::before{background:rgba(217,164,65,.5)}
.nyVers i.v4::before{background:rgba(63,109,122,.55)}
.nyVers i.on{color:#f0e2c0}
.nyVers i.v1.on::before{background:#2b2620;box-shadow:0 0 0 1px rgba(233,227,214,.55)}
.nyVers i.v2.on::before{background:#b7402f;box-shadow:0 0 0 1px rgba(233,227,214,.3)}
.nyVers i.v3.on::before{background:#d9a441;box-shadow:0 0 0 1px rgba(233,227,214,.3)}
.nyVers i.v4.on::before{background:#3f6d7a;box-shadow:0 0 0 1px rgba(233,227,214,.3)}
.nyTip{margin:0;min-height:2.6em;font-size:.72em;line-height:1.62;letter-spacing:.06em;color:#e8d9b4}
.nyActs{display:flex;gap:.42em;justify-content:flex-end}
/* 彩蛋回执：印完的那一版在题款屏上留一枚缩略 */
.note8.nyNote{display:flex;align-items:center;gap:.55em}
.note8.nyNote .nyMini{flex:0 0 auto;display:block;width:1.8em;height:1.8em;border-radius:.1em;
  background:linear-gradient(150deg,#f5eeda,#e2d5b9);box-shadow:0 1px 5px rgba(0,0,0,.55)}
.note8.nyNote .nyMini svg{display:block;width:100%;height:100%}

/* ---------- 结尾：成品呈现（把这张纸立起来看） ----------
   用户原话：「结尾要有一个渲染的作品出来，现在还是 2d 的，要怎么处理」。
   结果页那张纸上的东西本来就都是玩家这一路做出来的（底纹 / 套色 / 题字 / 落款
    + 纸角的年画），缺的只是「它是一个物件」这件事——所以用 CSS 3D
   把它摆成斜置的一块纸：有厚度、有落地影、背后一盏**不闪**的灯（纯渐变，无动画），
   做一次「平放 → 揭起 → 落定」。动画只走这一次，落定之后完全静止：
   不做呼吸、不做脉冲（用户为「顶光一闪一闪」「灯圈呼吸、过曝」发过不止一次火）。 */
.rv{position:fixed;inset:0;z-index:90;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:2.2vh;
  /* 底衬必须**不透**：之前用 95% 的 rgba，结果页的字会从后面透出来，
     和纸上叠成两层字。这里直接给实色。 */
  background:radial-gradient(130% 92% at 50% 26%,#1a1815,#050507 72%);
  opacity:0;transition:opacity .45s ease}
.rv.in{opacity:1}
.rv.out{opacity:0;transition-duration:.6s}
.rvStage{--rvw:min(78vw,46vh);position:relative;width:var(--rvw);height:calc(var(--rvw)*1.5);
  perspective:1600px}
/* 背后的灯：一层静态径向渐变，不循环、不呼吸 */
.rvGlow{position:absolute;z-index:0;left:50%;top:44%;width:170%;height:130%;transform:translate(-50%,-50%);
  background:radial-gradient(closest-side,rgba(255,241,212,.19),rgba(255,241,212,.06) 56%,transparent 76%);
  pointer-events:none}
.rvPaper{position:absolute;inset:0;z-index:1;font-size:calc(var(--rvw)*.036);
  background:
    repeating-linear-gradient(90deg,rgba(120,110,90,.08) 0 1px,transparent 1px 4px),
    linear-gradient(158deg,#f0e8d6,#ded3bc 58%,#cdc2a8);
  box-shadow:0 34px 78px rgba(0,0,0,.7);
  transform-style:preserve-3d;opacity:0;
  transform:rotateX(68deg) scale(.88) translateZ(0);
  transition:transform 1.15s cubic-bezier(.22,.72,.24,1),opacity .68s ease .08s}
.rv.in .rvPaper{opacity:1;transform:rotateX(7deg) rotateY(-13deg) rotateZ(-1.4deg) scale(1)}
/* 纸的厚度：往后退一层，沿 3D 的 Z 轴压出一块板 */
.rvPaper::before{content:'';position:absolute;inset:0;z-index:-1;transform:translateZ(-14px);
  background:linear-gradient(158deg,#c7bb9e,#b3a688)}
.rvSkin{position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(70% 46% at 32% 22%,rgba(255,252,240,.5),transparent 72%);
  mix-blend-mode:multiply}
.rvTint{position:absolute;inset:0;pointer-events:none;mix-blend-mode:multiply}
.rvPat{position:absolute;inset:0;pointer-events:none}
/* ★ §108b：纸里的花叶（.rvEggs，和结果页 .bloom 同一套坐标与画法）已随花草纸退役。 */
/* 纸角的年画（题款屏印的那一版） */
.rvNy{position:absolute;right:8%;top:6.5%;width:4.6em;height:4.6em;opacity:.92;
  transform:rotate(-3deg);pointer-events:none}
.rvNy svg{display:block;width:100%;height:100%}
.rvVerse{position:absolute;right:11%;top:26%;writing-mode:vertical-rl;text-orientation:upright;
  font-family:var(--kai);font-size:1.75em;letter-spacing:.3em;color:#3a3428;opacity:.88;
  pointer-events:none}
/* 中央水印：手工纸本来就有帘纹和水印。极淡、静态，不参与任何动画；
   玩家全选「留白」时，纸面也不至于是一张白板。 */
.rvMark{position:absolute;left:50%;top:52%;transform:translate(-50%,-52%);
  font-family:var(--kai);font-size:10em;line-height:1;color:rgba(122,102,72,.10);
  pointer-events:none;user-select:none}
/* 竖排题名：不管客人选没选题字，纸上都有这张纸的名字 */
.rvTitle{position:absolute;left:9%;top:10%;writing-mode:vertical-rl;text-orientation:upright;
  font-family:var(--kai);font-size:1.95em;letter-spacing:.34em;color:#332e24;opacity:.82;
  pointer-events:none}
.rvSeal{position:absolute;right:10%;bottom:14%;width:2.5em;height:2.5em;border:.12em solid #b7402f;
  border-radius:.18em;display:grid;place-items:center;color:#b7402f;font-family:var(--kai);
  font-size:1em;transform:rotate(-4deg);opacity:.9;pointer-events:none}
/* 落地影必须在纸**下面**：DOM 顺序上 .rvFloor 在 .rvPaper 之后，
   不给 z-index 的话会在纸面压出一道黑弧（截到过）。 */
.rvFloor{position:absolute;z-index:0;left:50%;bottom:-7%;width:118%;height:15%;transform:translateX(-50%);
  background:radial-gradient(closest-side,rgba(0,0,0,.62),rgba(0,0,0,.22) 58%,transparent 78%);
  pointer-events:none}
.rvFoot{display:flex;flex-direction:column;align-items:center;gap:.7em}
.rvCap{margin:0;font-size:13px;letter-spacing:.18em;color:rgba(233,227,214,.62);text-align:center;
  max-width:86vw;line-height:1.8}
@media (max-height:520px){
  .rvCap{display:none}
}
