* { box-sizing: border-box; }
:root{
  /* 深紫夜色：牌桌是晚上，底色就别装白天了。
     身份色在暗底上要反过来 —— 底用半透明的深色，字用亮色，否则一片糊 */
  --bg:#160f28; --panel:#20183a; --line:#33294f; --text:#ece8f6; --dim:#9c93bb;
  --raise:#291f47;                      /* 比面板再亮一档，用于气泡、卡片 */
  --wolf-bg:rgba(190,55,55,.26); --wolf-fg:#e8807f;
  --seer-bg:rgba(60,155,240,.22); --seer-fg:#8ccdff;
  --guard-bg:rgba(150,105,60,.22); --guard-fg:#e2b88c;
  --vill-bg:rgba(230,225,245,.10); --vill-fg:#c0b8d6;
  --hunter-bg:rgba(190,110,50,.22); --hunter-fg:#f2b184;
  --witch-bg:rgba(150,105,200,.22); --witch-fg:#d3aaf5;
  /* 整块上色的卡片（发言人列、座位、身份卡）在暗底上要用深色块配亮字，
     直接拿上面那些亮色当背景会刺眼 */
  --wolf-solid:#7e2f2f; --seer-solid:#2b81d8; --guard-solid:#5e4529;   /* 预言家是天蓝 */
  --vill-solid:#433c59; --hunter-solid:#70411f; --witch-solid:#523c76; --judge-solid:#ab8837;   /* 法官不是身份，单拎个金黄出来 */
  /* 吸顶那条「返回 / 停止 / 重开」有多高。钉在它下面的身份卡和座位列都按它算 top。
     刘海、灵动岛、字号都会改它，所以真实值由 renderMeBar() 量出来写进来 ——
     这里只是个兜底初值，别再往上加 env(safe-area-inset-top) 之类的估算 */
  --mebarh:52px;
  --rosterh:96px;                       /* 身份卡那块钉死的高度，见 .rosterbar */
  /* LQIP:begin 24×52，由 scripts/gen_bg.py 写入，别手改 */
  --bg-lqip:url("data:image/webp;base64,UklGRlQCAABXRUJQVlA4IEgCAACQCgCdASoYADQAPrVUn0unJSKhrBqpmOAWiUAVhlZV101TNNre5uoaH0dq1u6f6nH1J4e47RckXjnv05BBsIuxrdrraRXHgrDrISDnNqH4MoW1KpuLB3tdgWS/JAAA/vnQBqj4Zve70TingEh/n9cQAgKiRgkZHjsKaER32rAQzf7ARblWbHHwUO01GJUi2/qikmgq/uvmBwhLbF8LMn2zUSwauU/LN0+DlW+T724JKz75LHWhYcJAOzs9P3HaxVrJ//CBGPwLl5yH15Opp+wKsttIxhvpRaXTnyf2RVDee0i5Y9XWBN8MGEsEMVjfu9Is1k1LN+2fJL4cq9jDKlWV1Ja8TgHkP0rdOhykXoSuiWfUgzqkiZW96TwbOpX0TqyeLlzqQZweRFdXL5BYcI7I7vkz23DW7/3inrNAVnXxrjbnxXB/hqRqnP0xWuq6uZee4B26xTFVypXRtUsh0BWwXWkxoao3uyf5hMLx9fYZ2mLeAAKpyV9QekeOt4MxVdz9O6pbTKS8eXogL0+EccEu+MrPGZnpI4g6LMonpev81jIqOzfGBBCGmHEX1XX0VV/XJSIxf21AJBpkJL1JULjB5i5k++lVCYI7YFEIH/HT9DhtLkin8HWTiGstWJXEfI7wR817YHsYPIcCJVA39QBxOMQ5qr/j0T6mNDIsZFYMmk1lh/5NJez2CFIOOB/ne31pIXmhEHAKe9x31NshcS4wdJXrdRkTAMS9+gf2XLR9oXr0fEW2uvYbdSrAZzToAZ70kKHWaWy0AA==");
  /* LQIP:end */
}
html,body{height:100%;}
/* 手机上只许上下/左右拖动，不许双指捏合、不许双击放大；
   text-size-adjust 是防 iOS 横屏时自己把字放大一档 */
html{-webkit-text-size-adjust:100%;text-size-adjust:100%;}
body{
  touch-action:pan-x pan-y;
  margin:0; background:var(--bg); color:var(--text);
  font:13px/1.65 "PingFang SC","Hiragino Sans GB",-apple-system,"Microsoft YaHei",sans-serif;
  /* 座位号、倒计时这些数字要等宽：不等宽的话 298s→99s 那一刻整行会跳一下 */
  font-variant-numeric:tabular-nums;
}
button{font:inherit;cursor:pointer;}

.tab.active{background:var(--panel);color:var(--text);}
.tab-badge{background:#33294f;border-radius:8px;padding:0 6px;font-size:11px;}
.tab-fill{flex:1;}
.tab-note{display:flex;align-items:center;padding:0 14px;font-size:11px;color:var(--dim);}

.app{display:grid;grid-template-columns:268px 400px 1fr;height:100vh;}
/* ---------- Play 模式：牌桌视图 ---------- */
/* 控制项收成顶上一条，下面整块留给牌桌：左右两列座位夹着中间的事件流 */
body.no-debug .app{grid-template-columns:1fr;grid-template-rows:auto 1fr;}
body.no-debug .center,
body.no-debug .sidebar .sec-head,
body.no-debug #history{display:none;}
/* 开局页：控制项摆成居中一张卡；牌桌页只留返回按钮 */
body.no-debug .sidebar{border-right:none;padding:26px 18px 40px;max-width:520px;margin:0 auto;
                       width:100%;}
body.no-debug .sidebar .field{margin-bottom:6px;}
body.no-debug .sidebar .btn{padding:8px 10px;}
body.no-debug.screen-table .sidebar{display:none;}
body.no-debug:not(.screen-table) .right{display:none;}
body.no-debug .right{padding:0 14px 60px;}
.backbtn{border:1px solid var(--line);background:var(--panel);border-radius:8px;
         padding:4px 10px;font-size:12px;color:var(--dim);margin-right:10px;}
.backbtn:hover{background:var(--raise);}
body:not(.screen-table) .backbtn{display:none;}

/* ---------- 开局页：两种界面共用 ---------- */
/* 进来先是居中的一列选项，点「开一局」才进对局页（Play 是牌桌，Debug 是三栏）。
   以前这套分屏只对 Play 生效，Debug 一进来就是常驻三栏、没有回头路 */
body:not(.screen-table) .app{grid-template-columns:1fr;grid-template-rows:auto 1fr;}
body:not(.screen-table) .center,
body:not(.screen-table) .right{display:none;}
body:not(.screen-table) .sidebar{border-right:none;padding:26px 18px 40px;
                                 max-width:520px;margin:0 auto;width:100%;}
body:not(.screen-table) .sidebar .sec-head,
body:not(.screen-table) #history{display:none;}
body:not(.screen-table) .sidebar .field{margin-bottom:6px;}
body:not(.screen-table) .sidebar .btn{padding:8px 10px;}
/* 开局页没有「上级」可回，这时候让「开一局」独占一行 */
body:not(.screen-table) #btnUp{display:none;}
body:not(.screen-table) .btnrow{grid-template-columns:1fr;}

.table{display:grid;grid-template-columns:112px minmax(0,1fr) 112px;gap:12px;align-items:start;}
/* 留给发言头像外圈光晕的空间；正常位置和吸顶位置必须一起让开。 */
body.no-debug .table{padding-top:10px;}
/* 吸顶：顶栏 + 身份卡 + 光晕间隙（--rosterh 含 .rosterwrap 的上下留白） */
.seatcol{display:flex;flex-direction:column;gap:6px;position:sticky;
         top:calc(var(--mebarh) + var(--rosterh) + 10px);}
.seatcol:empty{display:none;}
body:not(.no-debug) .table{grid-template-columns:1fr;}

/* 号码压在头像偏下方（face 自带的角标），身份写在头像下面一行。
   座位牌不描边、不铺色块 —— 一圈明显的外框只是给每张脸又加了个框，
   六个挨在一起像一排按钮。身份改成染下面那行字（--*-fg 是暗底上的亮色） */
.seat{position:relative;width:fit-content;margin:0 auto;display:flex;flex-direction:column;
      align-items:center;gap:1px;padding:0;border:none;
      border-radius:6px;background:none;color:#fff;}   /* 和头像同圆角：自己那张的红框贴着头像四角 */
/* 号码在头像下面单独一行，不再压在头像上 —— 卡通头像底下垫一层黑渐变太难看 */
.seat-n{font-size:11px;font-weight:700;line-height:16px;height:16px;color:#fff;letter-spacing:.5px;}
.seat-r{max-width:100%;font-size:10px;font-weight:600;line-height:16px;height:16px;padding:0 2px;
        white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--dim);}
.seat.g-狼人 .seat-r{color:var(--wolf-fg);}
.seat.g-预言家 .seat-r{color:var(--seer-fg);}
.seat.g-守卫 .seat-r{color:var(--guard-fg);}
.seat.g-平民 .seat-r{color:var(--vill-fg);}
.seat.g-猎人 .seat-r{color:var(--hunter-fg);}
.seat.g-女巫 .seat-r{color:var(--witch-fg);}
/* 预言家的查验结论：好人绿、狼人红。比身份那行再小一号 ——
   它是「我知道的」，不是「他是什么」，别抢身份的位置 */
/* 和身份同一行（.seat-r 定了行高），所以这里只改字号和底色，不能改 line-height —— 改了这张牌就和别的不一样高 */
.seat-chk{font-size:9.5px;padding:0 3px;border-radius:4px;}
.seat-chk.good{color:#7ee2a8;background:rgba(90,190,130,.18);}
.seat-chk.bad{color:#ff9d92;background:rgba(200,60,60,.24);}
.seat.dead .seat-chk{opacity:.5;}
.seat.me{box-shadow:0 0 0 2px #e0573f;}
.seat.dead{opacity:.42;}
.seat-x{position:absolute;top:-5px;right:-4px;background:#0f0a1e;color:#cfc7e6;
        border:1px solid var(--line);border-radius:6px;padding:0 5px;font-size:9.5px;}
/* 两边的座位列要装得下 62px 的头像（.face.lg）加上底下那行身份/查验标签。
   gap 收到 3px、下面 .right 的左右留白收到 6px，是为了把头像涨掉的宽度
   从边距里抠回来 —— 否则中间的气泡会窄到一行只剩十几个字 */
body.narrow .table{grid-template-columns:70px minmax(0,1fr) 70px;gap:3px;}
/* iOS Safari 的 100vh 含地址栏高度，会把内容顶到可视区外面，用 dvh 修正 */
@supports (height: 100dvh){ .app{height:100dvh;} }
.col{overflow-y:auto;min-width:0;}
.col::-webkit-scrollbar{width:8px;} .col::-webkit-scrollbar-thumb{background:#3e3360;border-radius:4px;}

/* ---------- 左栏 ---------- */
.sidebar{background:var(--panel);border-right:1px solid var(--line);padding:14px 12px 30px;}
.field[hidden]{display:none;}
/* 模式 | 对局 | 界面 | 音色 并排一行。「对局」那条文案最长（9人:3狼3民1预1猎1巫），
   单独给两倍宽，其余三列平分。
   列宽必须写 minmax(0,…)：…fr 的下限是 min-content，而 <select> 的 min-content
   是最长那条 option 的宽度 —— 不写 minmax(0,…) 会被长选项把整页顶出屏幕 */
/* 「对局」那一条最长（9人:3狼3民1预1猎1巫 = 130px），必须完整显示、不许出省略号，
   所以给它 1.8fr；另外三列各自留够最长选项的下限（Debug 42px、Real 29px、单人 28px，
   再加 24px 的内边距和箭头），剩下的宽度全归对局。
   下限不能靠 auto —— auto 量的是 label 里那两个字的宽度，比选项还窄 */
.field4{display:grid;gap:0 6px;
        grid-template-columns:minmax(54px,.6fr) minmax(0,1.8fr)
                              minmax(68px,.7fr) minmax(56px,.6fr);}
.field4 .field{min-width:0;font-size:11px;}
.field4[hidden]{display:none;}
/* 挤成四列之后每列只剩 85px 上下，字号和内边距都收一档 */
.field4 select{font-size:12.5px;padding:7px 17px 7px 7px;background-position:right 6px center;}
.field{display:block;font-size:11.5px;color:var(--dim);margin-bottom:8px;}
/* 自己画下拉箭头（appearance:none）：系统那个箭头在 macOS/iOS 上要占掉快 20px，
   四个框挤一行时这点宽度省不得。底色和箭头分开写成 background-color / background-image ——
   开局页那版半透明只覆盖颜色，箭头还在 */
.field select{display:block;width:100%;min-width:0;margin-top:4px;padding:7px 22px 7px 8px;
              border:1px solid var(--line);border-radius:8px;background-color:var(--panel);
              color:var(--text);font:inherit;text-overflow:ellipsis;
              -webkit-appearance:none;appearance:none;
              background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%239c93bb' stroke-width='1.6' stroke-linecap='round'/></svg>");
              background-repeat:no-repeat;background-position:right 8px center;
              background-size:9px 6px;}
.check{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dim);margin:0 0 10px;cursor:pointer;}
.check input{accent-color:#e0573f;}
.btn{width:100%;border-radius:8px;padding:8px 10px;margin-bottom:8px;border:1px solid var(--line);
     background:var(--panel);color:var(--text);}
.btnrow{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.btn.primary{background:#7b46d9;border-color:#8b58e8;color:#fff;font-weight:600;
             box-shadow:0 4px 16px rgba(123,70,217,.3);}
.btn.primary:disabled{background:#3a2f57;border-color:#3a2f57;color:#fff;cursor:not-allowed;
                      box-shadow:none;}
.btn.ghost{color:#c2a1ff;}
.btn.ghost:disabled{color:#5b5177;cursor:not-allowed;}
.btn.dashed{border-style:dashed;color:var(--dim);}
.sec-head{display:flex;align-items:center;gap:6px;margin:16px 0 8px;font-size:12px;color:var(--dim);}
.sec-right{margin-left:auto;font-size:11px;}
.caret{font-size:9px;}
.prow{display:flex;align-items:center;gap:7px;padding:8px 10px;margin-bottom:6px;border:1px solid var(--line);
      border-radius:8px;background:var(--raise);cursor:pointer;min-width:0;}
.prow:hover{background:var(--raise);}
.prow.active{border-color:#5abe82;box-shadow:inset 3px 0 0 #4c8c62;}
.prow.dead{opacity:.5;}
.prow.judge{cursor:default;color:var(--dim);}          /* 法官不参与筛选，点不动 */
.prow.judge:hover{background:var(--panel);}
.prow .pname{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
             max-width:104px;}
.prow .rtag{border-radius:5px;padding:0 6px;font-size:11px;white-space:nowrap;}
.prow .pmeta{margin-left:auto;text-align:right;font-size:10.5px;color:var(--dim);line-height:1.35;
              white-space:nowrap;flex-shrink:0;}
.r-狼人{background:var(--wolf-bg);color:var(--wolf-fg);}
.r-预言家{background:var(--seer-bg);color:var(--seer-fg);}
.r-守卫{background:var(--guard-bg);color:var(--guard-fg);}
.r-平民{background:var(--vill-bg);color:var(--vill-fg);}
.r-猎人{background:var(--hunter-bg);color:var(--hunter-fg);}
.r-女巫{background:var(--witch-bg);color:var(--witch-fg);}
.judge{background:#e8e4f0;color:#cbb8f5;}
.bubble.judgeline{background:rgba(150,130,225,.20);color:#d9cdfb;border-color:rgba(170,150,240,.34);}

/* 开局这条：左边挂一张「你是几号、什么身份」的卡，右边是这一局的身份清单。
   nowrap：身份卡不许被挤到单独一行去 —— 右边那堆胶囊自己换行就好 */
.roster-box{width:100%;display:flex;flex-wrap:nowrap;gap:10px;align-items:flex-start;}
.roster-main{flex:1 1 0;min-width:0;}
/* 配置摆成一排小胶囊：挤在一句话里读不出「几狼几民」 */
/* 身份全都列出来，装不下就换行 —— 横着划的时候总有几个身份是看不见的，
   而「这局有哪些神职」恰恰是开局最该一眼扫完的东西 */
.roster-cfg{display:flex;flex-wrap:wrap;align-items:center;gap:4px;
            font-size:12px;color:var(--text);}
.roster-cfg>*{flex:0 0 auto;}
.roster-cfg i,.roster-cfg .chip{font-style:normal;background:rgba(255,255,255,.09);
              border:none;border-radius:6px;padding:1px 7px;color:#d6cfea;
              font:inherit;font-size:12px;}
/* 带身份名的那几颗能点，点开看技能 —— 给个边，不然看不出它是按钮 */
.roster-cfg .chip{border:1px solid rgba(255,255,255,.16);padding:0 7px;cursor:pointer;}
.roster-cfg .chip:hover{background:rgba(255,255,255,.16);color:#fff;}
.roster-cfg .chip[aria-expanded="true"]{background:#6a52bd;border-color:transparent;color:#fff;}
/* 获胜条件单独一颗按钮；规则平时不占地方，点问号才展开（见 renderRoster）。 */
.helpdot{flex:0 0 auto;display:inline-flex;align-items:center;gap:4px;
         padding:1px 6px 1px 7px;margin-left:1px;border-radius:6px;
         border:1px solid rgba(255,255,255,.26);background:rgba(255,255,255,.06);
         color:#cdc4e4;font:inherit;font-size:12px;cursor:pointer;}
.helpdot i{font-style:normal;width:15px;height:15px;padding:0;border-radius:50%;
           background:rgba(255,255,255,.17);color:#fff;font-size:10px;font-weight:700;
           display:inline-flex;align-items:center;justify-content:center;}
.helpdot:hover{background:rgba(255,255,255,.13);color:#fff;}
.helpdot[aria-expanded="true"]{background:#6a52bd;border-color:transparent;color:#fff;}
.helpdot[aria-expanded="true"] i{background:rgba(255,255,255,.30);}
.idcard{flex:0 0 auto;display:flex;align-items:center;gap:10px;min-height:76px;padding:7px 12px 7px 7px;
        border-radius:9px;
        background:var(--panel);border:1px solid var(--line);font-size:11.5px;line-height:1.45;}
.idcard-logo{display:flex;flex:0 0 auto;align-items:center;justify-content:center;
             width:62px;height:62px;border-radius:6px;overflow:hidden;   /* 和座位牌的头像（.face.lg）一样大、一样的圆角 */
             background:rgba(255,255,255,.18);color:#fff;font-size:17px;font-weight:700;}
.idcard-logo img{display:block;width:100%;height:100%;object-fit:cover;}
.idcard-t{white-space:nowrap;}
.idcard-s{font-size:10.5px;color:var(--dim);}   /* 存活 / 已出局：身份下面单独一行 */
/* 狼队友独占第二行，和首行的生存状态区分开。 */
.idcard-mate{font-size:11px;color:rgba(255,255,255,.82);}
.idcard-mate b{font-weight:600;}
.idcard-potions{display:flex;align-items:center;gap:6px;margin-top:2px;}
.idcard-potion{display:inline-flex;align-items:center;gap:2px;padding:0 5px 0 2px;
                border:1px solid currentColor;border-radius:6px;font-size:10px;line-height:1.5;}
.idcard-potion svg{display:block;width:15px;height:18px;flex:0 0 auto;}
.idcard-potion.blue{color:#8bd0ff;background:rgba(64,159,235,.18);}
.idcard-potion.green{color:#81e5a8;background:rgba(59,188,112,.18);}
.idcard-potion.used{color:#b1aabb;background:rgba(150,145,160,.14);opacity:.68;}
/* 知道自己身份之后，整张卡就换成身份色的实心块，和下面发言人那列的卡一个色系 */
.idcard.g-狼人{background:var(--wolf-solid);}
.idcard.g-预言家{background:var(--seer-solid);}
.idcard.g-守卫{background:var(--guard-solid);}
.idcard.g-平民{background:var(--vill-solid);}
.idcard.g-猎人{background:var(--hunter-solid);}
.idcard.g-女巫{background:var(--witch-solid);}
/* 底已经是身份色了，里面那颗身份胶囊别再叠一层同色底，直接白字。
   整张卡的字也一律纯白 —— 正文那个带紫调的 --text 压在天蓝底上会发灰 */
.idcard.roled{border-color:transparent;color:#fff;}
.idcard.roled .rtag{background:transparent;color:#fff;padding:0;font-weight:600;}
.idcard.roled .idcard-s{color:rgba(255,255,255,.85);}
.roster{display:flex;flex-wrap:wrap;align-items:stretch;gap:8px;margin-bottom:9px;}
.roster-split{width:1px;background:rgba(255,255,255,.14);margin:2px 3px;flex:0 0 auto;}
.roster-note{font-size:12px;color:var(--dim);}
/* 钉在牌桌最上面那块（1 号头上）：身份卡 + 配置 + 获胜条件。
   吸顶在顶栏底下，高度写死 —— 事件流从它底下往上滚，它自己不动也不变高 */
/* 吸顶和遮挡都归外面这层：它是普通块级元素，宽度正好是右栏的内容宽，
   不会溢出，所以不会让整栏变得能左右拖。上下留白做成它的 padding
   （不是卡片的 margin），这样它把卡片的圆角和缝隙一起盖住。
   高度不写死：身份多少、屏多宽都会影响胶囊换几行，渲染完由 renderRoster()
   量一把真实高度写进 --rosterh（座位列按它算吸顶位置）。这一局里它不会再变 */
.rosterwrap{position:sticky;top:var(--mebarh);z-index:5;
            background:var(--panel);padding:6px 0 2px;}
/* 保险：万一 --mebarh 差了一两个像素（顶栏高度是量出来的，不同机型字号会差），
   往上再垫一段同色挡板，滚上来的消息一律盖住，不许从缝里穿过去。
   只往**上**伸：往上溢出滚不到，不会像往右伸那样让整栏能左右拖 */
.rosterwrap::before{content:"";position:absolute;left:0;right:0;top:-40px;height:40px;
                    background:var(--panel);}
.rosterwrap[hidden]{display:none;}
.rosterbar{background:var(--raise);border:1px solid rgba(255,255,255,.08);border-radius:12px;
           padding:8px 10px;margin:0;}
.rosterbar .roster-box{gap:8px 12px;}
.rosterbar[hidden]{display:none;}
/* 展开的获胜条件不许把这块顶高，所以浮在它底下当个气泡 */
.roster-main>.roster-note{position:absolute;left:0;right:0;top:calc(100% - 5px);
           background:var(--raise);border:1px solid rgba(255,255,255,.13);border-radius:10px;
           padding:8px 11px;box-shadow:0 12px 26px rgba(0,0,0,.5);}
.roster-main>.roster-note[hidden]{display:none;}
/* 一条说明一行，缩进挂住行首那个「-」，折行的时候不会顶到 - 底下 */
.tipl{padding-left:11px;text-indent:-11px;}
.tipl+.tipl{margin-top:4px;}
.tipl b{font-weight:600;color:#cdc4e4;}
.win-paths{margin:4px 0 6px;padding-left:30px;}
.win-paths li+li{margin-top:3px;}
.win-paths b{font-weight:600;color:#cdc4e4;}

/* ---------- 玩家头像：号码压在下半部当角标 ---------- */
.face{position:relative;display:inline-block;flex:0 0 auto;width:58px;height:58px;overflow:hidden;
      border-radius:6px;background:#2c2248;line-height:0;}   /* 头像一律小圆角、接近矩形；外面套框的地方用同样的圆角，四角不留缝 */
.face img,.face-blank{display:block;width:100%;height:100%;object-fit:cover;}
/* 角标压在头像下沿，底下垫一层由深到透的渐变，浅色衣服上也认得出字 */
/* 号码压在最底下：往上占太多会糊住人物的下巴 */
.face-no{position:absolute;left:0;right:0;bottom:0;padding:5px 0 1px;text-align:center;
         font-size:10.5px;font-weight:600;line-height:1.15;color:#fff;letter-spacing:.5px;
         background:linear-gradient(to top,rgba(28,24,20,.78) 40%,rgba(28,24,20,0));}
.face.md{width:38px;height:38px;border-radius:5px;}
.face.sm{width:30px;height:30px;border-radius:4px;}
.face.md .face-no{font-size:10px;padding:4px 0 0;}
.face.sm .face-no{font-size:9.5px;padding:3px 0 0;}
.face.sm .face-no i{display:none;}          /* 30px 上塞不下「号」字，只留数字 */
.face.lg{width:62px;height:62px;border-radius:6px;}     /* 座位牌那档 */
.face.lg .face-no{font-size:11px;padding:4px 0 1px;}
.face .face-no{font-size:10.5px;}           /* 「法官」两个字比号码宽，统一收一点 */
.face-no i{font-style:normal;}              /* <i> 只用来在小尺寸下藏「号」字，不是斜体 */
/* 正在朗读：沿着照片内沿一圈绿色呼吸灯，一亮一暗。
   不做缩放 —— 头像尺寸不变，列表就不会因为谁在说话而抖一下。
   .face 是 overflow:hidden，裁的是里面的内容，管不着元素自己的 box-shadow，
   所以往外那圈光晕挂在 .face 上（::after 在里面，往外打的光会被裁掉） */
.face.speaking::after{content:"";position:absolute;inset:0;border-radius:inherit;
               pointer-events:none;
               box-shadow:inset 0 0 0 2.5px #3ee07a, inset 0 0 12px 2px rgba(62,224,122,.6);
               animation:breathe 1.6s ease-in-out infinite;}
/* 往外的光晕：和里面那圈同一个时长、同时起跑，所以看着是一起呼吸的。
   box-shadow 不参与布局，涨多大都不会把邻座推开 */
.face.speaking{animation:breatheOut 1.6s ease-in-out infinite;}
/* 30px 的小头像上 2.5px 的边太粗，会把脸糊掉 */
.face.sm.speaking::after{box-shadow:inset 0 0 0 2px #3ee07a, inset 0 0 7px 1px rgba(62,224,122,.5);}
/* 呼吸：只动透明度，不动尺寸和阴影，省得触发重排 */
@keyframes breathe{0%,100%{opacity:.32}50%{opacity:1}}
/* 这条只能把阴影本身做关键帧：opacity 会连头像一起变淡 */
@keyframes breatheOut{
  0%,100%{box-shadow:0 0 0 2px rgba(62,224,122,.20), 0 0 12px 3px rgba(62,224,122,.16);}
  50%    {box-shadow:0 0 0 3px rgba(62,224,122,.72), 0 0 30px 12px rgba(62,224,122,.52);}
}
@media (prefers-reduced-motion: reduce){
  .face.speaking::after,.row.speaking .bubble::after{animation:none;opacity:.9;}
  .face.speaking{animation:none;
    box-shadow:0 0 0 3px rgba(62,224,122,.6), 0 0 24px 8px rgba(62,224,122,.4);}
}
/* 发言人那一列：横着一张卡，左脸右身份，配色和中栏那六张一致 */
.who{display:flex;align-items:center;gap:5px;flex:0 0 auto;padding:3px 7px 3px 3px;
     border-radius:10px;background:#4a4363;align-self:flex-start;}
.who .face{width:30px;height:30px;border-radius:4px;}
.who.g-狼人{background:var(--wolf-solid);}
.who.g-预言家{background:var(--seer-solid);}
.who.g-守卫{background:var(--guard-solid);}
.who.g-平民{background:var(--vill-solid);}
.who.g-猎人{background:var(--hunter-solid);}
.who.g-女巫{background:var(--witch-solid);}
.who.judge{background:var(--judge-solid);}
.who.judge .face-no{display:none;}          /* 右边已经写着「法官」了 */
.who-r{color:#fff;font-size:11.5px;font-weight:600;letter-spacing:.2px;white-space:nowrap;}
.history .hrow{display:flex;gap:8px;padding:7px 10px;border:1px solid var(--line);border-radius:8px;
               margin-bottom:6px;background:var(--raise);cursor:pointer;font-size:12px;}
.history .hrow:hover{background:var(--raise);}
.history .hwin{margin-left:auto;font-size:11px;color:var(--dim);}

/* ---------- 中栏 ---------- */
.center{background:var(--raise);border-right:1px solid var(--line);padding:0 12px 40px;}
.col-head{position:sticky;top:0;z-index:2;background:var(--raise);display:flex;align-items:center;
          padding:12px 2px 10px;font-weight:600;border-bottom:1px solid var(--line);}
.col-head-right{margin-left:auto;font-weight:400;font-size:12px;color:var(--dim);}
/* 中栏顶上这两块共用一档头像尺寸，法官那条跟下面六张卡对齐 */
.sysrow .face,.ctxface .face{width:48px;height:48px;border-radius:5px;}
.sysrow .face .face-no,.ctxface .face .face-no{font-size:10.5px;padding:5px 0 0;}
/* 法官卡占一列、系统全局状态占两列，正好压着下面那排卡的网格线 */
.sysrow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;padding:10px 14px 0;}
.sysrow:empty{display:none;}
.ctxface.judge{background:var(--judge-solid);}
.ctxface.judge .face-no{display:none;}      /* 右边已经写着「法官」了，角标就别重复 */
.sysline{grid-column:span 2;display:flex;align-items:center;gap:9px;width:100%;padding:7px 12px;
         border-radius:12px;border:1px solid var(--line);background:var(--raise);font:inherit;
         font-size:12.5px;text-align:left;}
.sysline:hover{background:var(--raise);}
.sysline.active{border-color:#5abe82;background:rgba(90,190,130,.14);}
.sysline-t{font-weight:600;color:var(--text);}
.sysline-n{margin-left:auto;font-size:11px;color:var(--dim);}
.ctxfaces{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;
          padding:10px 14px;border-bottom:1px solid var(--line);}
.ctxfaces:empty{display:none;}
/* 一个人一张小卡：左边脸，右边身份 —— 整张卡按身份上色，身份用白字压在上面 */
/* 左边挤紧一点、文字左对齐：不然「预言家」三个字会被挤成「预…」 */
.ctxface{display:flex;align-items:center;gap:6px;padding:4px 5px 4px 4px;border-radius:12px;
         border:1px solid transparent;background:#4a4363;min-width:0;}
.ctxface.g-狼人{background:var(--wolf-solid);}
.ctxface.g-预言家{background:var(--seer-solid);}
.ctxface.g-守卫{background:var(--guard-solid);}
.ctxface.g-平民{background:var(--vill-solid);}
.ctxface.g-猎人{background:var(--hunter-solid);}
.ctxface.g-女巫{background:var(--witch-solid);}
.ctxface.unknown{background:#4a4363;}
.ctxface:hover{filter:brightness(1.08);}
.ctxface.active{border-color:var(--text);box-shadow:0 0 0 2px rgba(255,255,255,.16);}
.ctxface.dead{opacity:.42;}
.ctxface-r{flex:1;min-width:0;font-size:13.5px;font-weight:600;color:#fff;text-align:left;
           letter-spacing:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}

.sysblk{margin:10px 14px 0;border:1px solid var(--line);border-radius:10px;background:var(--raise);
        padding:8px 12px;font-size:12px;}
.sysblk[hidden]{display:none;}
.sysblk>summary{cursor:pointer;color:var(--dim);}
/* 注入的人设：开局摇的那四条，单独列出来，别埋在几百字的常驻段里 */
.persona{display:flex;flex-direction:column;gap:4px;margin:8px 0 6px;}
.persona span{display:block;font-size:11.5px;color:#d9cfae;background:#f1efe9;border-radius:7px;
              padding:4px 8px;}
.persona i{font-style:normal;color:var(--dim);margin-right:6px;}

.filter-bar[hidden]{display:none;}
.filter-bar{display:flex;gap:8px;align-items:center;font-size:12px;color:var(--dim);padding:8px 2px;}
.filter-bar button{border:none;background:none;color:var(--seer-fg);text-decoration:underline;padding:0;}
.calls{padding-top:10px;}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;margin-bottom:10px;overflow:hidden;}
.card-head{display:flex;align-items:center;padding:9px 12px;border-bottom:1px solid #2c2248;}
.card-head b{font-weight:600;}
.card-head .ctx{margin-left:auto;font-size:11px;color:var(--dim);}
.card-body{padding:8px 12px 12px;}
.card-title{font-size:12.5px;color:var(--text);margin-bottom:8px;line-height:1.7;}
details{border-top:1px solid var(--raise);padding:6px 0;}
details summary{cursor:pointer;font-size:11.5px;color:var(--dim);list-style:none;}
details summary::-webkit-details-marker{display:none;}
details summary::before{content:"▸ ";}
details[open] summary::before{content:"▾ ";}
.blk{margin-top:6px;max-height:190px;overflow:auto;background:#1a1430;border:1px solid #33294f;
     border-radius:6px;padding:8px 10px;font-size:11.5px;white-space:pre-wrap;color:#c7bfe0;
     font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:1.75;}
.out{margin-top:8px;border-top:1px solid var(--raise);padding-top:8px;}
.out .k{font-size:11px;color:var(--dim);}
.out .think{font-size:12px;color:var(--dim);background:#1f1839;border-radius:6px;padding:6px 9px;margin:4px 0;}
.out .act{font-size:12.5px;background:var(--raise);border:1px solid var(--line);border-radius:8px;
          padding:7px 10px;margin-top:4px;}
.pill{display:inline-block;background:#2c2248;border-radius:5px;padding:0 6px;font-size:11px;color:var(--dim);}
.mock{color:#e0b95c;}

/* ---------- 右栏 ---------- */
.right{background:var(--panel);padding:0 18px 60px;position:relative;}
/* 上内边距是给 Debug 那一栏的（事件流直接接在栏头下面）。牌桌视图里 .log 和座位列
   是同一行 grid item，这点内边距会让「第 1 夜」那块比 1 号头像低一截，所以清掉 */
.log{padding-top:14px;padding-bottom:8px;}
body.no-debug .log{padding-top:0;}
.row{display:flex;gap:8px;align-items:flex-start;margin-bottom:9px;}
.badge{flex:0 0 auto;border-radius:6px;padding:2px 8px;font-size:11.5px;white-space:nowrap;margin-top:2px;}
/* 正文后面的注脚（可见范围之类）：小一号、淡一点，跟在句子后面不换行 */
.aside{margin-left:6px;font-size:11px;opacity:.62;white-space:nowrap;}
/* 查验结果 / 刀口：这一夜最要紧的信息，标出来 */
.verdict{font-weight:700;border-radius:5px;padding:0 3px;}
.verdict.good{color:#5abe82;background:rgba(90,190,130,.18);}
.verdict.bad{color:var(--wolf-fg);background:rgba(200,60,60,.20);}
.verdict.kill{color:#ff8d8d;background:rgba(200,60,60,.22);font-weight:700;}
/* 每个气泡都描一道比底色深一档的细边：段落底色一上来，纯靠填充色分不清边界 */
.bubble{position:relative;background:var(--raise);border:1px solid rgba(255,255,255,.10);
        border-radius:10px;padding:7px 12px;max-width:860px;}
/* 正在念的就是这一条：边缘绿色呼吸灯，和头像那圈同一个节奏 */
.row.speaking .bubble{border-color:rgba(62,224,122,.5);}
.row.speaking .bubble::after{content:"";position:absolute;inset:-1px;border-radius:inherit;
        pointer-events:none;border:1.5px solid #3ee07a;
        box-shadow:0 0 24px 4px rgba(62,224,122,.45);
        animation:breathe 1.6s ease-in-out infinite;}
.bubble.revealing{min-height:1.65em;}
.voice-pending{display:inline-flex;align-items:center;justify-content:center;gap:3px;
               width:29px;height:1.3em;vertical-align:middle;}
.voice-pending i{display:block;width:3px;height:5px;border-radius:3px;
                 background:currentColor;opacity:.75;animation:voiceWait 1s ease-in-out infinite;}
.voice-pending i:nth-child(2){animation-delay:.15s;}
.voice-pending i:nth-child(3){animation-delay:.3s;}
@keyframes voiceWait{0%,100%{height:5px;opacity:.45}50%{height:15px;opacity:1}}
.ck{display:none;}
.ck.on{display:inline;animation:ckIn .34s cubic-bezier(.4,0,.2,1) both;}
@keyframes ckIn{from{opacity:0}to{opacity:1}}
@media (prefers-reduced-motion: reduce){.voice-pending i{animation:none;height:8px;opacity:.8;}}
/* 身份揭示时（模拟模式全程、单人/多人模式在结束后）气泡跟着身份上色 */
.bubble.r-狼人{background:var(--wolf-bg);color:var(--wolf-fg);border-color:rgba(200,60,60,.35);}
.bubble.r-预言家{background:var(--seer-bg);color:var(--seer-fg);border-color:rgba(60,155,240,.38);}
.bubble.r-守卫{background:var(--guard-bg);color:var(--guard-fg);border-color:rgba(150,105,60,.35);}
.bubble.r-平民{background:var(--vill-bg);color:var(--vill-fg);border-color:rgba(230,225,245,.16);}
.bubble.r-猎人{background:var(--hunter-bg);color:var(--hunter-fg);border-color:rgba(190,110,50,.35);}
.bubble.r-女巫{background:var(--witch-bg);color:var(--witch-fg);border-color:rgba(150,105,200,.35);}
.bubble.plain{background:var(--raise);color:var(--dim);font-size:12px;}
.bubble.result{background:rgba(90,190,130,.12);color:#a9e3c0;border-color:rgba(90,190,130,.3);}
/* 法官发言：金黄的气泡，正文前面一个「法官：」就够了，不另给头像和身份牌。
   写在 .judgeline 后面才压得住它原来那身紫 */
.bubble.judgesay{background:rgba(171,136,55,.20);color:#f0e2bd;border-color:rgba(171,136,55,.46);}
.judgetag{font-weight:600;color:#ffd875;}
.bubble.wolfnight{background:rgba(200,60,60,.13);border-color:#f2d9d6;}
/* 一天/一夜整段包在一个底色块里：夜里冷灰、白天米黄。
   分隔条自己不再上色（否则底色套底色），只留一根细线压住段首 */
.seg{border-radius:14px;padding:2px 14px 10px;margin:0 0 14px;background:rgba(120,100,190,.13);}
.seg.day{background:rgba(210,170,90,.12);}
.seg .bubble.plain,.seg .bubble.say:not([class*="r-"]),.seg .bubble:not([class*="r-"]):not(.judgeline):not(.result){
  background:rgba(255,255,255,.05);border-color:rgba(255,255,255,.09);}
                                          /* 没有身份色的气泡在底色上要提亮，否则糊成一片 */
.phase{display:flex;align-items:baseline;gap:12px;margin:6px 0 10px;padding:8px 2px;
       border-bottom:1px solid rgba(255,255,255,.08);}
.phase h2{margin:0;font-size:17px;font-weight:600;}
.phase .note{margin-left:auto;font-size:12px;color:var(--dim);}
.info{display:inline-block;background:var(--raise);border-radius:8px;padding:4px 12px;font-size:12px;
      color:var(--dim);margin-bottom:10px;}
.vote{font-size:12px;color:var(--dim);}
/* 票型卡：居中、米色。一人一条太碎，归堆之后一眼看得出谁站谁 */
.votecard{margin:10px auto;width:fit-content;max-width:100%;background:#2e2647;
          border:1px solid rgba(230,215,180,.22);border-radius:12px;padding:9px 13px 11px;}
.votecard-h{font-size:11.5px;color:#b6a77f;margin-bottom:7px;letter-spacing:.5px;}
/* 排成一张四列的表：投票人 / 箭头 / 被投的人 / 票数。
   用 grid 而不是 flex，四列的宽度就由最宽那一行定死，所有行的箭头和被投的人
   都落在同一个 x 上；投票人那一列靠右，从箭头往左长，不然一行三个头像、
   一行一个头像，箭头就歪成阶梯了 */
.votel{display:grid;grid-template-columns:1fr auto auto auto;align-items:center;
       gap:9px;padding:6px 0;font-size:13px;
       border-top:1px solid rgba(255,255,255,.10);}
.votel:first-of-type{border-top:none;}
.votefrom{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:5px;min-width:0;}
.voteto{display:flex;font-weight:600;}
.votearrow{color:#b6a77f;}
.votecnt{font-size:11px;color:#b6a77f;white-space:nowrap;margin-left:5px;}
/* 法官那句「投票结果…」收在卡片底部，不再单独占一行气泡 */
.votecard-foot{margin:9px 0 0;padding-top:9px;border-top:1px solid rgba(255,255,255,.10);}
.votecard-foot .bubble{line-height:1.7;}
.votecard-foot b{font-weight:600;}
.runbar[hidden]{display:none;}
.runbar{position:sticky;bottom:0;display:flex;align-items:center;gap:8px;padding:10px 0 14px;
        background:var(--panel);box-shadow:0 -8px 12px -6px rgba(32,24,58,.95),0 -1px 0 var(--line);
        font-size:12px;color:var(--dim);}
.dots i{display:inline-block;width:4px;height:4px;border-radius:50%;background:#c4bfb5;margin-right:3px;
        animation:blink 1.2s infinite;}
.dots i:nth-child(2){animation-delay:.2s;} .dots i:nth-child(3){animation-delay:.4s;}
@keyframes blink{0%,60%,100%{opacity:.25}30%{opacity:1}}
.runbar.done .dots{display:none;}
.empty{color:var(--dim);font-size:12px;padding:24px 0;}

/* ---------- 事件流顶栏：身份 + 停止/重开 ---------- */
.mebar{position:sticky;top:0;z-index:6;display:flex;align-items:center;gap:8px;
       background:var(--panel);border-bottom:1px solid var(--line);padding:9px 0;font-size:12.5px;}
.mebar[hidden]{display:none;}
.mebar b{font-size:13px;}
.mebar .face{margin-right:2px;}
.mebar .role{border-radius:5px;padding:1px 7px;font-size:12px;}
.mebar-btns{margin-left:auto;display:flex;gap:6px;}
.mebar-btns button{border:1px solid var(--line);background:var(--panel);border-radius:7px;
                   padding:5px 12px;color:var(--text);font-size:12px;}
.mebar-btns button:disabled{color:#5b5177;}
#barStop{color:#ef8f7a;}

/* ---------- 多人房间 ---------- */
.lobby[hidden]{display:none;}
.lobby{margin-bottom:10px;}
.lobby input{width:100%;border:1px solid var(--line);border-radius:8px;padding:8px 10px;
             font:inherit;background:var(--panel);color:var(--text);margin-bottom:6px;}
.lobby .row2{display:flex;gap:6px;margin-bottom:6px;min-width:0;}
.lobby .row2[hidden]{display:none;}
.lobby .row2 input{margin:0;flex:1 1 auto;min-width:0;}   /* 不给 min-width:0 会把侧栏撑宽 */
.lobby .row2 .btn{margin:0;white-space:nowrap;width:auto;flex:0 0 auto;padding:8px 14px;}
.roomcode{background:#fbe9e7;border:1px solid #f0cfca;border-radius:8px;padding:8px 10px;
          margin:8px 0 6px;color:#ef8f7a;font-size:12.5px;}
.roomcode b{font-size:22px;letter-spacing:3px;margin:0 6px;font-variant-numeric:tabular-nums;}
.roomcode span{display:block;font-size:11px;color:#8b5b52;margin-top:2px;word-break:break-all;}
.members{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px;}
.members span{background:var(--raise);border-radius:6px;padding:3px 8px;font-size:11.5px;
              white-space:nowrap;}
.members span.host::after{content:" 房主";color:#ef8f7a;}
.members span.me{background:rgba(90,190,130,.16);color:#5abe82;}
.members .kick{font-style:normal;margin-left:5px;color:#ef8f7a;cursor:pointer;opacity:.55;}
.members .kick:hover{opacity:1;}

/* ---------- 单人模式 ---------- */
.ask{position:sticky;bottom:0;z-index:5;background:#241b40;
     border:1px solid #3d3160;border-bottom:none;border-radius:14px 14px 0 0;
     box-shadow:0 -12px 18px -10px rgba(0,0,0,.45);padding:12px 14px 14px;}
.ask[hidden]{display:none;}
.ask.ask-wolf{border-color:rgba(240,85,69,.9);animation:askWolfBreath 1.8s ease-in-out infinite;}
.ask.ask-seer,.ask.ask-witch-save{border-color:rgba(92,178,255,.9);animation:askSeerBreath 1.8s ease-in-out infinite;}
.ask.ask-witch-poison{border-color:rgba(82,224,133,.9);animation:askPoisonBreath 1.8s ease-in-out infinite;}
@keyframes askWolfBreath{
  0%,100%{box-shadow:0 -12px 18px -10px rgba(0,0,0,.45),0 0 6px rgba(240,85,69,.2);}
  50%{box-shadow:0 -12px 18px -10px rgba(0,0,0,.45),0 0 20px 3px rgba(240,85,69,.65);}
}
@keyframes askSeerBreath{
  0%,100%{box-shadow:0 -12px 18px -10px rgba(0,0,0,.45),0 0 6px rgba(92,178,255,.2);}
  50%{box-shadow:0 -12px 18px -10px rgba(0,0,0,.45),0 0 20px 3px rgba(92,178,255,.65);}
}
@keyframes askPoisonBreath{
  0%,100%{box-shadow:0 -12px 18px -10px rgba(0,0,0,.45),0 0 6px rgba(82,224,133,.2);}
  50%{box-shadow:0 -12px 18px -10px rgba(0,0,0,.45),0 0 20px 3px rgba(82,224,133,.65);}
}
@media (prefers-reduced-motion:reduce){
  .ask.ask-wolf,.ask.ask-seer,.ask.ask-witch-save,.ask.ask-witch-poison{animation:none;box-shadow:0 -12px 18px -10px rgba(0,0,0,.45);}
}
.ask-head{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-size:12.5px;}
.ask-left{margin-left:auto;font-size:11.5px;color:var(--dim);}
.ask-q{font-size:12px;color:var(--dim);margin-bottom:8px;line-height:1.7;white-space:pre-wrap;}
.seats{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;}
.seats button{border:1px solid #fff;background:var(--raise);color:var(--text);
              border-radius:8px;padding:7px 14px;}
.seats button:hover{background:#332757;}
.seats button{position:relative;overflow:hidden;}
.seats button.on{border-color:#fff;background:rgba(224,87,63,.22);color:#ff9d8a;font-weight:600;}
.seats[hidden]{display:none;}
.witch-actions{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;}
.witch-actions button{position:relative;overflow:hidden;border:1px solid #fff;
  border-radius:8px;padding:7px 14px;background:var(--raise);color:var(--text);}
.witch-actions button.on{font-weight:600;}
.witch-actions button.on[data-action="save"]{--potion-glow:rgba(98,185,255,.9);
  border-color:#fff;background:rgba(35,99,177,.35);color:#d8efff;
  box-shadow:0 0 13px rgba(98,185,255,.38);}
.witch-actions button.on[data-action="poison"],.witch-targets button.on{
  --potion-glow:rgba(82,224,133,.9);border-color:#fff;
  background:rgba(28,120,69,.35);color:#d7ffe4;
  box-shadow:0 0 13px rgba(82,224,133,.38);}
.witch-actions button.on[data-action="pass"]{border-color:#fff;background:#39304f;color:#fff;}
/* 选中的药瓶和毒杀目标里浮起几颗小气泡；只动透明度/位移，不影响按钮布局。 */
.witch-actions button.on[data-action="save"]::after,
.witch-actions button.on[data-action="poison"]::after,
.witch-targets button.on::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background:radial-gradient(circle 3px at 18% 77%,var(--potion-glow) 95%,transparent),
             radial-gradient(circle 2px at 43% 58%,var(--potion-glow) 95%,transparent),
             radial-gradient(circle 4px at 76% 80%,var(--potion-glow) 95%,transparent),
             radial-gradient(circle 2px at 89% 42%,var(--potion-glow) 95%,transparent);
  animation:potionBubbles 1.6s ease-in-out infinite;}
@keyframes potionBubbles{
  0%{opacity:.25;transform:translateY(7px);}
  55%{opacity:.9;transform:translateY(-5px);}
  100%{opacity:.15;transform:translateY(-13px);}
}
@media (prefers-reduced-motion:reduce){
  .witch-actions button.on::after,.witch-targets button.on::after{animation:none;opacity:.7;transform:none;}
}
/* 狼人选刀口：选中的那个按钮上斜着划三道红爪印，一挥而过。
   三道是三条各自定位的渐变（repeating 在不同宽度的按钮上条数会变），
   背景不透明、只盖在按钮上，所以号码还看得见 */
.seats.claw button.on{border-color:#fff;background:rgba(150,28,24,.34);color:#ffd7cf;}
.seats.claw button.on::after{
  content:"";position:absolute;inset:-2px;pointer-events:none;mix-blend-mode:screen;
  background:
    linear-gradient(102deg,transparent 25%,rgba(255,74,52,.75) 26.4%,#ffd9cf 27.6%,
                    rgba(255,74,52,.75) 28.8%,transparent 30.2%),
    linear-gradient(102deg,transparent 45%,rgba(255,74,52,.75) 46.4%,#ffd9cf 47.6%,
                    rgba(255,74,52,.75) 48.8%,transparent 50.2%),
    linear-gradient(102deg,transparent 65%,rgba(255,74,52,.75) 66.4%,#ffd9cf 67.6%,
                    rgba(255,74,52,.75) 68.8%,transparent 70.2%);
  animation:claw .34s cubic-bezier(.2,.7,.3,1) both;}
/* 从右上往左下抹过去，抹完留在原地 */
@keyframes claw{
  0%{opacity:0;transform:translate(10px,-10px) scaleX(1.35);}
  40%{opacity:1;}
  100%{opacity:1;transform:none;}
}
@media (prefers-reduced-motion: reduce){ .seats.claw button.on::after{animation:none;} }

/* 预言家选查验目标：选中的按钮整体亮成夜蓝，一道魔法棒的光轨斜扫过去，
   扫完在按钮上留几点星芒。box-shadow 挂在按钮本体上 —— 按钮是 overflow:hidden，
   往外的光晕只能靠阴影，::before/::after 打出去会被裁掉 */
.seats.wand button.on{border-color:#fff;background:rgba(28,92,178,.34);
                      color:#d4e9ff;font-weight:600;
                      box-shadow:0 0 0 2px rgba(92,178,255,.28), 0 0 18px 4px rgba(92,178,255,.35);}
/* 光轨：一条窄亮带，从右往左扫一遍，停下来留一层淡蓝 */
.seats.wand button.on::before{
  content:"";position:absolute;inset:-2px;pointer-events:none;mix-blend-mode:screen;
  background:linear-gradient(64deg,transparent 34%,rgba(150,215,255,.10) 44%,
             rgba(196,234,255,.92) 50%,rgba(150,215,255,.10) 56%,transparent 66%);
  background-size:240% 100%;background-repeat:no-repeat;
  animation:wandSweep .6s cubic-bezier(.3,.7,.25,1) both;}
@keyframes wandSweep{
  0%{background-position:135% 0;opacity:0;}
  22%{opacity:1;}
  100%{background-position:-55% 0;opacity:.30;}
}
/* 星芒：四点大小不一的光点，先炸开一下再定住 */
.seats.wand button.on::after{
  content:"";position:absolute;inset:0;pointer-events:none;mix-blend-mode:screen;
  background:
    radial-gradient(circle 3px at 21% 27%,rgba(226,244,255,.95),transparent 100%),
    radial-gradient(circle 2px at 70% 21%,rgba(174,222,255,.90),transparent 100%),
    radial-gradient(circle 2.5px at 84% 71%,rgba(204,238,255,.92),transparent 100%),
    radial-gradient(circle 1.5px at 33% 77%,rgba(184,228,255,.85),transparent 100%);
  animation:wandSpark .68s ease-out both;}
@keyframes wandSpark{
  0%{opacity:0;transform:scale(.45);}
  42%{opacity:1;transform:scale(1.14);}
  100%{opacity:.92;transform:none;}
}
@media (prefers-reduced-motion: reduce){
  .seats.wand button.on::before,.seats.wand button.on::after{animation:none;}
}
.ask textarea,.ask input[type=text]{width:100%;border:1px solid #3d3160;border-radius:8px;
     padding:8px 10px;font:inherit;background:#1a1430;color:var(--text);resize:vertical;}
.ask textarea::placeholder,.ask input::placeholder{color:#6f6590;}
.ask textarea:focus,.ask input[type=text]:focus{outline:none;border-color:#e0573f;}
.ask textarea{min-height:64px;}
.ask .send{margin-top:8px;width:100%;border:none;border-radius:8px;padding:10px;
     background:#e0573f;color:#fff;font-weight:600;}
.ask .send:disabled{background:#3a2f57;}
.audio-resume{position:fixed;left:50%;bottom:calc(18px + env(safe-area-inset-bottom));
               transform:translateX(-50%);z-index:20;white-space:nowrap;
               border:1px solid #fff;border-radius:10px;padding:10px 16px;
               background:#483571;color:#fff;font:inherit;font-weight:600;
               box-shadow:0 8px 24px rgba(0,0,0,.45);}
.audio-resume[hidden]{display:none;}

/* ---------- 移动端：单栏 + 底部三个页签 ---------- */
.mobnav{display:none;}

/* body.narrow 由 applyLayout() 打：真窄屏，或者 Play 模式（那套界面本来就是照着手机做的，
   桌面上是套在下面那个 iPhone 外框里显示）。以前这里是 @media (max-width:860px)，
   但外框里的宽度媒体查询量的是窗口、不是外框，所以一律改成类名判定 */
body.narrow{font-size:14px;}

/* 三栏改成一次只显示一栏，由 body[data-tab] 决定显示哪一栏 */
body.narrow .app{display:block;height:auto;}
body.narrow .col{display:none;height:calc(100vh - 54px);overflow-y:auto;overflow-x:hidden;
                 border-right:none;
     -webkit-overflow-scrolling:touch;}
body.narrow .ask{padding:12px 14px 16px;}
body.narrow[data-tab="side"] .sidebar{display:block;}
body.narrow[data-tab="ctx"]  .center {display:block;}
body.narrow[data-tab="log"]  .right  {display:block;}

body.narrow .sidebar{padding:12px 12px 20px;}
body.narrow .center{padding:0 10px 20px;}
/* 座位头像放大吃掉的宽度，从栏的左右留白里抠回来 ——
   不补的话气泡一窄，文字就多折一行，省下的竖向空间又还回去了 */
body.narrow .right{padding:0 6px 20px;}
body.narrow .col-head{padding:10px 2px 8px;}

/* 窄屏上气泡要占满宽度，角色标签换行到上一行更好读。
   gap 是发言人小卡和它下面那条气泡之间的距离（row 换行了），
   margin-bottom 是两条消息之间的距离 —— 两个都收紧，一屏能多看两句话 */
body.narrow .row{flex-wrap:wrap;gap:3px;margin-bottom:5px;}
body.narrow .bubble{max-width:100%;padding:6px 10px;}
body.narrow .seg{padding:2px 6px 7px;margin:0 0 8px;}
body.narrow .phase{margin:2px 0 7px;padding:5px 2px;}
body.narrow .votecard{margin:7px auto;padding:8px 11px 9px;}
body.narrow .votecard-foot{margin-top:7px;padding-top:7px;}
body.narrow .badge,body.narrow .only{font-size:11px;}
body.narrow .phase h2{font-size:16px;}
body.narrow .phase .note{display:none;}
body.narrow .blk{color:var(--text);max-height:150px;font-size:11px;}
body.narrow .prow{padding:10px;}
body.narrow .btn{padding:11px 10px;}          /* 手指点得中 */
body.narrow .field select{padding:10px 22px 10px 8px;}
body.narrow .field4 select{padding:9px 17px 9px 7px;}
body.narrow .check{padding:4px 0;}
body.narrow .runbar{bottom:0;padding:8px 0 10px;}

body.narrow .mobnav{
  display:flex;position:fixed;left:0;right:0;bottom:0;height:54px;z-index:20;
  background:var(--panel);border-top:1px solid var(--line);
  padding-bottom:env(safe-area-inset-bottom);
}
body.narrow .mobnav button{
  flex:1;border:none;background:none;color:var(--dim);font-size:13px;
  display:flex;align-items:center;justify-content:center;gap:5px;
}
body.narrow .mobnav button.active{color:#e0573f;font-weight:600;box-shadow:inset 0 -2px 0 #e0573f;}
body.narrow .mobnav .n{background:#2c2248;border-radius:8px;padding:0 6px;font-size:11px;color:var(--dim);}
body.narrow .mobnav button.active .n{background:#fbe4e4;color:var(--wolf-fg);}

/* Play 模式没有页签：中栏（每轮上下文）本来就不出现，剩下开局页和牌桌页两屏，
   靠 screen-table 切就够了 —— 底下那条三个页签只会让人点到空栏。
   所以这里不看 data-tab，直接按当前在哪一屏决定显示哪一栏，并且收回那 54px */
body.no-debug .mobnav{display:none;}
body.narrow.no-debug:not(.screen-table) .sidebar{display:block;}
/* 事件少时让牌桌吃满剩余高度，作答面板贴近屏幕底部；事件多时照常滚动。 */
body.narrow.no-debug.screen-table .right{display:flex;flex-direction:column;}
body.narrow.no-debug.screen-table .table{flex:1 0 auto;}
body.narrow.no-debug .col{height:100vh;}

/* 「在哪一屏」要盖过「选了哪个页签」。
   上面 data-tab 那三条 display:block 和按屏切的 display:none 权重一样重，写在后面的赢了 ——
   于是从牌桌点「返回」回开局页时，data-tab 还停在 "log"，右栏就接在开局页下面一起渲染，
   往下一划就划回牌桌里去了。这几条压在最后面收口 */
body.narrow:not(.screen-table) .center,
body.narrow:not(.screen-table) .right{display:none;}
body.narrow.no-debug.screen-table .sidebar{display:none;}

/* 手机上整页不许滚：body 钉死在视口里，只有 .col 自己能滚。
   不钉的话 body 会比视口高出一点（或者纯粹是橡皮筋回弹），顶上、底下就露出一条底色 ——
   看着像海报没铺满。:not(.framed) 是把桌面上的手机外框排除掉，那边自己有一套定位 */
html{overscroll-behavior:none;}
body.narrow:not(.framed){position:fixed;top:0;right:0;bottom:0;left:0;overflow:hidden;}
body.narrow .col{overscroll-behavior:contain;}

/* 装到主屏之后是全屏的（status bar 直接压在页面上，见 index.html 的
   black-translucent + viewport-fit=cover）：海报自己铺到最顶上不用管，
   但牌桌页顶栏和底下的作答面板得给刘海和 home 条让位 */
body.narrow:not(.framed) .mebar{padding-top:calc(9px + env(safe-area-inset-top));}
body.narrow:not(.framed) .right{padding-bottom:calc(20px + env(safe-area-inset-bottom));}
body.narrow:not(.framed) .ask{padding-bottom:calc(16px + env(safe-area-inset-bottom));}

@supports (height: 100dvh){
  body.narrow .col{height:calc(100dvh - 54px);}
  body.narrow.no-debug .col{height:100dvh;}
}

/* iOS 主屏幕 PWA + viewport-fit=cover：WebKit 的 dvh 会漏算底部安全区，
   海报栏提前结束，露出 body 的深紫色。独立运行时用包含安全区的 vh；
   普通 Safari 仍保留上面的 dvh，避开浏览器工具栏高度变化。 */
@media (display-mode: standalone){
  @supports (-webkit-touch-callout: none){
    body.narrow:not(.framed){height:100vh;}
    body.narrow.no-debug .col{height:100vh;}
    body.narrow:not(.no-debug) .col{height:calc(100vh - 54px);}
  }
}

/* ---------- 桌面上的 iPhone 17 Pro 竖屏外框 ---------- */
/* Play 模式在桌面上不摊成一整屏：整个 .app 收进 402×874（6.3 英寸的逻辑分辨率）的
   机身里，里面就是手机上一模一样的排版。窗口不够高时 applyLayout() 会给 --zoom
   一个小于 1 的值，整台等比缩小 —— 缩的是显示，里面的布局宽度还是 402 */
body.framed{display:flex;align-items:center;justify-content:center;height:100vh;
            overflow:hidden;background:#f3eee4;}
/* 桌面留白里放现成的 App 图标；独立于机身定位，手机布局和预览尺寸都不变。
   默认隐藏，用背景图避免移动端下载这张仅供桌面展示的图。 */
.desktop-brand{display:none;}
body.framed .desktop-brand{
  display:block;position:fixed;top:50%;left:calc((100vw - 440px)/4);
  width:min(220px, calc((100vw - 440px)/2 - 24px));aspect-ratio:1;
  transform:translate(-50%,-50%);border-radius:10px;
  background:#65377e url("/static/img/icons/icon-512-v3.png") center/cover no-repeat;
  box-shadow:0 18px 45px rgba(43,28,58,.18);
}
@media (max-width:600px){body.framed .desktop-brand{display:none;}}
body.framed .app{position:relative;flex:0 0 auto;width:402px;height:874px;
                 border-radius:46px;background:#000;overflow:hidden;
                 transform:scale(var(--zoom,1));transform-origin:center;
                 box-shadow:0 0 0 10px #17151d,0 0 0 12px #45414f,0 24px 60px rgba(0,0,0,.6);}
/* 灵动岛 */
body.framed .app::before{content:"";position:absolute;top:10px;left:50%;z-index:30;
                 transform:translateX(-50%);width:106px;height:28px;border-radius:14px;
                 background:#0c0c0e;pointer-events:none;}
/* 屏幕就这么高，别再跟着窗口走（上面那两条 vh/dvh 要让位，所以放在最后）。
   两条都要写：body.narrow.no-debug .col 那条权重比 body.framed .col 高，
   Play 模式得靠第二条压过去 */
body.framed .col{height:874px;}
body.framed.no-debug .col{height:874px;}
/* 开局页的海报一直铺到最顶上（灵动岛压在图上）；牌桌页顶上那条
   「返回 / 停止 / 重开」要给岛让个位，别钻到它底下去。
   这个位必须让在 .mebar 自己的 padding 上，不能给 .right（它是滚动容器）：
   滚动容器的 padding-top 是在**视口里面**的，内容往上滚会进到那一块并且照样可见，
   于是发言卡就从灵动岛底下穿过去了。挪到吸顶的 .mebar 上，它那层不透明底
   正好把岛那一条盖住 —— 真机上给刘海让位用的也是这招（见上面 env(safe-area-inset-top)） */
body.framed .right{padding-top:0;}
body.framed .mebar{padding-top:42px;}
/* 外框里只会是开局页，底下那条页签用不上 */
body.framed .mobnav{display:none;}
body.framed .mobnav{position:absolute;}

/* ---------- 开局页：海报当背景 ---------- */
/* 背景图 frontend/img/bg-setup.jpg 是 scripts 里用 uhwoo_cover_v4.png 接长出来的
   （见下面那条注释）。三层，从上往下：
   1. 幕布渐变：上半截几乎透明，让海报的标题和那只狼露出来；下半截压到近乎不透明，
      控件才压得住底下的花纹
   2. 海报本身
   3. --bg-lqip：几百字节的模糊小图，内联在 CSS 里（gen_bg.py 写的）。
      海报是背景图，得等 CSS 解析完才开始下，隧道过来要一会儿 ——
      这层不用发请求，解析到就铺上，不至于先黑一片再「啪」地换成海报 */
body:not(.screen-table) .sidebar{
  /* 不认 image-set 的老浏览器吃这条（jpg 兜底），认的被下面那条盖掉 */
  background-image:
    linear-gradient(180deg, rgba(12,8,22,0) 0%, rgba(12,8,22,0) 58%,
                            rgba(12,8,22,.10) 72%, rgba(14,9,26,.34) 82%,
                            rgba(14,9,26,.60) 100%),
    url("/static/img/bg-setup.jpg?v=8"),
    var(--bg-lqip);
  /* webp 只有 jpg 的一半大；换图要顺手改 v=，不然手机上吃的是缓存里的旧图
     （/static 下的图片发 immutable 长缓存，见 worker/scripts/build_assets.mjs 生成的 _headers） */
  background-image:
    linear-gradient(180deg, rgba(12,8,22,0) 0%, rgba(12,8,22,0) 58%,
                            rgba(12,8,22,.10) 72%, rgba(14,9,26,.34) 82%,
                            rgba(14,9,26,.60) 100%),
    image-set(url("/static/img/bg-setup.webp?v=8") type("image/webp"),
              url("/static/img/bg-setup.jpg?v=8") type("image/jpeg")),
    var(--bg-lqip);
  background-size:auto,cover,cover;
  /* 贴底：接长出来的那截糊底在图的顶上，真机比 402×874 宽的时候
     cover 纵向裁掉的正好是它（见 scripts/gen_bg.py） */
  background-position:center,center bottom,center bottom;
  background-repeat:no-repeat;
  background-color:var(--bg);
}
/* 控件整体压到下半屏：第一个元素顶一个 auto 外边距，把后面所有东西推下去。
   用 auto 而不是 justify-content:flex-end —— 内容多到要滚动时（多人模式的房间面板），
   flex-end 会让顶上那一截滚不到。
   这三条选择器都得写：窄屏那套「一次只显示一栏」的规则里，.sidebar 是
   display:block 而且权重比 body:not(.screen-table) 高，不压过去 auto 外边距就没用 */
body:not(.screen-table) .sidebar,
body.narrow.no-debug:not(.screen-table) .sidebar,
body.narrow[data-tab="side"]:not(.screen-table) .sidebar{display:flex;flex-direction:column;}
body:not(.screen-table) .sidebar > *:first-child{margin-top:auto;}
body.narrow:not(.screen-table) .sidebar{padding:12px 14px 26px;}
body.narrow:not(.framed):not(.screen-table) .sidebar{
  padding-bottom:calc(26px + env(safe-area-inset-bottom));}

/* 压在图上的控件一律半透明 + 毛玻璃 */
body:not(.screen-table) .field{color:#d3cbea;text-shadow:0 1px 3px rgba(0,0,0,.65);}
/* 不加 backdrop-filter：控件落在海报最下面那片近乎纯色的夜空上，糊和不糊看不出区别，
   白白让 iOS 每帧多重画一层（滚动、呼吸灯动画的时候尤其肉疼）。
   压住底下花纹靠的是这层半透明深底，不是模糊 */
body:not(.screen-table) .field select,
body:not(.screen-table) .lobby input{
  background-color:rgba(26,17,48,.4);border-color:rgba(255,255,255,.28);color:#fff;
}
body:not(.screen-table) .btn{
  background:rgba(26,17,48,.36);border-color:rgba(255,255,255,.24);
}
body:not(.screen-table) .btn.primary{
  background:rgba(123,70,217,.72);border-color:rgba(190,155,255,.5);
  box-shadow:0 6px 22px rgba(123,70,217,.34);
}
body:not(.screen-table) .btn.primary:disabled{
  background:rgba(58,47,87,.62);border-color:rgba(255,255,255,.14);box-shadow:none;
}
body:not(.screen-table) .members span{background:rgba(26,17,48,.38);}

/* 「开一局」不必占满整行：矮一档、收到七成宽、居中摆 */
body:not(.screen-table) .btnrow{justify-items:center;}
body:not(.screen-table) .sidebar .btn{padding:6px 12px;line-height:1.5;}
body:not(.screen-table) .btnrow .btn{width:28%;min-width:96px;margin-bottom:0;}

/* ---------- 语音输入 ---------- */
.ask-actions{display:flex;gap:8px;align-items:stretch;}
.ask-actions .send{margin-top:8px;flex:1;}
.mic{display:inline-flex;align-items:center;justify-content:center;gap:6px;margin-top:8px;
     border:1px solid var(--line);border-radius:8px;background:var(--panel);color:var(--dim);
     padding:7px 14px;white-space:nowrap;}
.mic:disabled{opacity:.5;cursor:not-allowed;}
.mic.rec{border-color:#e0573f;color:#e0573f;background:rgba(200,60,60,.16);}
.mic.rec .dot{width:8px;height:8px;border-radius:50%;background:#e0573f;animation:blink 1s infinite;}
.mic.busy{color:var(--dim);}
@keyframes blink{50%{opacity:.25;}}
