  @import url('https://fonts.googleapis.com/css2?family=Zen+Maru+Gothic:wght@500;700;900&display=swap');

  :root {
    --wood-deep: #241710;
    --wood: #38281a;
    --field-grass: #3d4a2c;
    --field-grass2: #46552f;
    --parchment: #f1e3c4;
    --parchment-dim: #d9c8a3;
    --ink: #3b2c1a;
    --gold: #e8b33d;
    --gold-deep: #b07f1e;
    --war: #d4583a;
    --tank: #4d8ec2;
    --mage: #a06ee0;
    --merc: #d8a62e;
    /* 契シナジーの識別色。祈り手・神官・預言者の法衣と精霊の絵に合わせた深緑（色相140）。
       同じ緑系の --pet(捕獲魔物) とは明度・彩度で分ける（こちらは暗く落ち着いた緑、
       あちらは明るいミント）。盤面では契の精霊と捕獲魔物が同時に並ぶので、
       この差を詰めないこと。白文字とのコントラストは 3.3:1（旧・桃紫の 3.26:1 と同等） */
    --pact: #37a05a;
    --pet: #4ad67e;   /* 捕獲魔物の識別色（明るいミント。--pact と混同しない明度差を保つ） */
    --hp-red: #e35648;
    --win: #7cbd6b;
    --t1: #a9744a;
    --t2: #c4ccd8;
    --t3: #e8b33d;
    /* 3D 俯瞰ビュー: 俯角と縦プリストレッチ率。
       ※ ここは JS が動くまでの初期値でしかない。実際の値は render.js の
         updateBoardShape() が盤の実寸から毎回計算して :root に上書きする
         （端末ごとにマスの奥行き比が2倍以上ばらついていたのを揃えるため）。
         調整したいのは CELL_DEPTH / TILT_MIN / TILT_MAX の3定数（render.js）。 */
    --tilt: 44deg;
    --stretch: 1.39;   /* = 1/cos(44deg)。地面が箱をちょうど埋める比 */
  }

  * { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
  html, body { height: 100%; }

  body {
    font-family: 'Zen Maru Gothic', 'Hiragino Maru Gothic ProN', 'Yu Gothic', sans-serif;
    background: var(--wood-deep);
    color: var(--parchment);
    display: flex;
    justify-content: center;
    overflow: hidden;
    touch-action: manipulation;
    user-select: none;
    -webkit-user-select: none;
  }

  #app {
    width: 100%;
    max-width: 430px;
    height: 100dvh;
    display: flex;
    flex-direction: column;
    padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
    gap: 6px;
    position: relative;
    background:
      repeating-linear-gradient(90deg, rgba(0,0,0,.08) 0 2px, transparent 2px 46px),
      linear-gradient(180deg, #3f2c1b 0%, var(--wood) 40%, #2c1e12 100%);
  }
  #app::before {
    content: '';
    position: absolute; inset: 0;
    pointer-events: none;
    background: radial-gradient(ellipse 90% 60% at 50% 30%, #ffb84d10 0%, transparent 60%),
                radial-gradient(ellipse at 50% 110%, #00000088 0%, transparent 55%);
    animation: torch 3.4s ease-in-out infinite;
    z-index: 0;
  }
  @keyframes torch { 0%,100% { opacity:.8 } 40% { opacity:1 } 60% { opacity:.6 } }
  #app.shake { animation: appshake .28s; }
  @keyframes appshake {
    0%,100%{transform:translate(0,0)} 20%{transform:translate(-5px,2px)}
    40%{transform:translate(4px,-3px)} 60%{transform:translate(-3px,-2px)} 80%{transform:translate(3px,2px)}
  }
  #app > * { position: relative; z-index: 1; }

  /* ───── top bar ───── */
  #topbar {
    display: flex; align-items: center; justify-content: space-between; gap: 6px;
    padding: 6px 10px;
    background: linear-gradient(180deg, #1d120b, #261810);
    border: 1px solid #00000066;
    border-radius: 12px;
    box-shadow: inset 0 1px 0 #ffffff14, 0 2px 6px #00000066;
    flex: 0 0 auto;
    z-index: 2; /* 3D盤面からはみ出す駒より前面に */
  }
  .round-badge { font-weight: 900; font-size: 14px; }
  .round-badge small { font-weight: 700; font-size: 9px; color: var(--parchment-dim); display: block; line-height: 1; }
  #hearts { font-size: 11px; letter-spacing: -1.5px; transition: transform .2s; }
  #hearts.pop { transform: scale(1.25); }

  #vault {
    display: flex; align-items: center; gap: 6px;
    background: linear-gradient(180deg, #5a3f1e, #3c2a14);
    border: 1px solid var(--gold-deep);
    border-radius: 999px;
    padding: 4px 9px 4px 7px;
    box-shadow: 0 0 0 2px #00000044, inset 0 1px 0 #ffffff22;
    transition: box-shadow .3s;
  }
  #vault.glow { box-shadow: 0 0 14px #e8b33d99; }
  #goldNum { font-weight: 900; font-size: 17px; color: var(--gold); line-height: 1; text-shadow: 0 1px 0 #00000088; transition: transform .15s; }
  #goldNum.pop { transform: scale(1.3); }
  /* 絵文字🪙はOS(特にiPhone)によって銀色寄りに描画されるため、金色を保証するCSS描画コインに置換 */
  #goldNum::before {
    content: '';
    display: inline-block;
    width: 10px; height: 10px;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 28%, #fff2c2 0%, #f6d275 32%, #e8b33d 62%, #b07f1e 100%);
    border: 1px solid #8a5f14;
    box-shadow: inset 0 1px 1px #ffffffaa;
    vertical-align: -1px;
    margin-right: 3px;
  }
  #pips { display: flex; gap: 3px; }
  .pip { width: 7px; height: 7px; border-radius: 50%; background: #00000066; border: 1px solid var(--gold-deep); transition: all .3s; }
  .pip.lit { background: var(--gold); box-shadow: 0 0 7px var(--gold); animation: pippulse 1.8s steps(9) infinite; }
  @keyframes pippulse { 50% { box-shadow: 0 0 12px var(--gold); } }
  #interestLabel { font-size: 9px; font-weight: 700; color: var(--gold); line-height: 1.1; text-align: center; }
  #interestLabel small { display: block; font-size: 8px; color: var(--parchment-dim); font-weight: 500; }
  #muteBtn, #helpBtn, #recBtn { background: none; border: none; font-size: 15px; cursor: pointer; padding: 2px 3px; line-height: 1; filter: drop-shadow(0 1px 1px #000); }

  /* ───── battlefield (3D 俯瞰ボード) ───── */
  #boardWrap {
    flex: 1 1 auto;
    min-height: 0;
    position: relative; /* #rumorLine の基準 */
    display: flex;
    flex-direction: column;
    gap: 3px;
    perspective: 1300px; /* 子の #board に遠近を与える */
    /* 盤を枠より横に広げてある（#board の --ovs）ので、あふれた分を**左右だけ**切る。
       ※ overflow:hidden は使えない。片方の軸だけ visible にはできない仕様なので、
         上下も切られて「盤の上へ飛び出す駒の頭」が欠ける（実測で頭の飛び出しは最大24px）。
         clip-path の inset に負値を書くと、その辺は外側へ広がる＝切らない。
       ※ overflow ではなく clip-path なのは 3D も理由。preserve-3d の祖先に
         overflow を付けると 3D が平面化されうる（decal層で実際に踏んだ）。 */
    clip-path: inset(-260px 0 -48px 0);
  }
  /* ⚠️ **この行が2行になると盤がそのぶん縮む。**（#boardWrap の高さは固定で、
       中の #board が残りを取る）。実測: 1行18px で盤318.5 ／ 2行36pxで盤305.7＝**13px縮む**。
       しかも「⚑ ○コンボ」はコンボが増えるほど長くなるので、**遊ぶほど盤が小さくなっていた**。
     対策は2つセット:
       ① 3つの札に white-space:nowrap ＝ 札の中で折り返さない
       ② 伸び縮みするのは #formCombo だけ（min-width:0 で潰れられるようにする）。
          左右の札は flex:none で削らない
     ⚠️ align-items も要る: 既定の stretch だと1つ2行になった瞬間に**3つとも2行の高さ**になる */
  #boardLabel {
    display: flex; justify-content: space-between; align-items: center;
    font-size: 10px; font-weight: 700; color: var(--parchment-dim);
    padding: 0 4px; gap: 3px;
    /* ⚑コンボ札だけ font-size が 1px 大きいので、出た瞬間に行が 1.5px 伸びて盤が動く。
       札が有っても無くても同じ高さになるよう、いちばん高い状態で固定する */
    min-height: 20px;
    position: relative;
    z-index: 3;            /* 盤・駒より常に前面 */
    pointer-events: none;  /* 下の駒のタップを妨げない */
  }
  #boardLabel span {
    background: #1d120bcc;
    padding: 2px 8px;
    border-radius: 999px;
    white-space: nowrap; flex: none;
  }
  /* 唯一の伸縮枠。狭い端末では末尾から省略記号になる（盤を縮めるよりは良い） */
  #boardLabel #formCombo { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  /* 部隊枠あふれ（恩返しの駆け込み合流）: 枠に収めるまで出撃不可の警告色 */
  #teamCount.over { color: #ffc9c0; background: #6a1e14cc; }
  /* 陣形のコンボ数（試作）。駒を動かすたびに動く数字なので、周りより一段強く出す */
  #formCombo { color: #ffd66e; font-size: 11px; background: #2a1a0acc; }
  #formCombo[hidden] { display: none; }
  /* 盤上の噂書き: 敵陣の上に漂う一行フレーバー（募兵フェイズのみ） */
  #rumorLine {
    position: absolute;
    top: 22px; left: 50%; /* ラベル直下〜盤の margin-top の余白帯。奥行きの敵トークンに被る場合はここを調整 */
    transform: translateX(-50%);
    max-width: 92%;
    padding: 3px 12px;
    border-radius: 999px;
    background: #1d120bd9;
    border: 1px solid #e8b33d2e;
    color: var(--parchment-dim);
    font-size: 10px; font-style: italic; font-weight: 700;
    text-align: center; line-height: 1.45;
    white-space: normal;
    z-index: 3;
    pointer-events: none;
    opacity: 0;
    transition: opacity .6s ease;
  }
  #rumorLine.show { opacity: 1; }
  #board {
    flex: 1;
    position: relative;
    margin-top: 18px; /* 奥の駒の頭がはみ出す分の余白（ラベル被り対策） */
    /* 手前（下端）の左右の角を枠の外へ少しはみ出させる＝広角レンズの効き。
       盤だけを横に広げ、あふれた分は #boardWrap の clip-path が左右だけ切る。
       ※ 上げすぎ注意: 角のマス(c=0/6, r=8)に置いた駒が画面から切れてはいけない。
         盤が広がるとマスの中心も外へ動くので、駒の箱の半分(29px)が画面内に残る値が上限。
         375px幅で実測: 3%=はみ出し0px / 3.5%=1px / 4%=2px / 5%=5px。**3% が上限**。
       ※ clientWidth が増えるので updateBoardShape / renderField は自動で追従する。
         盤が横に広がると俯角も自動で深くなる（実測 42.9°→46.5°）＝広角の効きが二重に乗る */
    --ovs: 3%;
    width: calc(100% + var(--ovs) * 2);
    margin-left: calc(var(--ovs) * -1);
    border-radius: 12px;
    overflow: visible; /* preserve-3d を活かすため（駒が盤外へはみ出す演出も許容） */
    border: 2px solid #1c1208;
    box-shadow: inset 0 0 30px #00000088, 0 3px 8px #00000066;
    transform-origin: 50% 100%;
    transform: rotateX(var(--tilt)) scaleY(var(--stretch));
    transform-style: preserve-3d;
    /* ⚠️ マス目・敵陣の帯・配置ゾーンは **--cols / --rows / --foe-h から引く**こと。
       ここに分数を直に書くと、盤の寸法を変えたときに駒だけ動いてマス目が取り残される
       （実際に 7×9 → 7×8 でそうなった）。値は render.js の syncBoardMetrics が
       data.js の COLS / ROWS / DEPLOY_ROW から流し込む。var() の第2引数は保険 */
    background:
      linear-gradient(180deg, #4a3527cc 0%, transparent 24%),
      repeating-linear-gradient(0deg, transparent 0 calc(100%/var(--rows,8) - 1px), #00000026 calc(100%/var(--rows,8) - 1px) calc(100%/var(--rows,8))),
      repeating-linear-gradient(90deg, transparent 0 calc(100%/var(--cols,7) - 1px), #00000026 calc(100%/var(--cols,7) - 1px) calc(100%/var(--cols,7))),
      repeating-conic-gradient(var(--field-grass) 0% 25%, var(--field-grass2) 0% 50%) 0 0 / calc(200%/var(--cols,7)) calc(200%/var(--rows,8));
  }
  /* enemy territory tint + deploy zone */
  #board::before {
    content: '';
    position: absolute; left: 0; right: 0; top: 0;
    height: var(--foe-h, 50%); /* 敵陣 = r0..DEPLOY_ROW-1 */
    background: linear-gradient(180deg, #6e2c2433, transparent);
    pointer-events: none;
  }
  #zoneHint {
    position: absolute; left: 0; right: 0;
    top: var(--foe-h, 50%); bottom: 0; /* 自陣 = r DEPLOY_ROW..ROWS-1 */
    background: repeating-linear-gradient(45deg, #e8b33d22 0 8px, #e8b33d0d 8px 16px);
    border-top: 2px dashed #e8b33d88;
    border-radius: 0 0 10px 10px;
    opacity: 0;
    transition: opacity .25s;
    pointer-events: none;
  }
  #board.placing #zoneHint { opacity: 1; }

  /* ═════ 盤の層（奥→手前） ═════
     #board の直下は元々すべてフラットな兄弟で、駒が行番号(1〜ROWS)、地面の演出が 1、
     薙ぎ払いが 4 …と z が混ざっていた。結果、薙ぎ払い(4)は自陣(下4行=z6〜9)の駒に
     必ず負けて隠れ、宝箱は z:auto(=0) でどの行でも全部の駒の後ろに回っていた。
     層に切り分けて「何が何の前か」を1箇所で決める。

     ■ transform-style: preserve-3d が必須。
       素の div で包むと、駒のビルボード変形（.tok の rotateX 打ち消し）が
       包んだ div の面へ潰される。実測: 駒の高さ 50.2px → 29.6px、位置も 20.7px 下へ
       ずれて地面に寝てしまった。preserve-3d を付けると変形前と完全一致する。
     ■ pointer-events: none が必須。
       空きマスのタップは #board 自身が受ける必要がある（team.js onBoardTap は
       ev.target===board のときだけ offsetX/Y ＝ ブラウザによる3D逆写像を使い、
       それ以外は clientX/Y の近似にフォールバックしてマスがずれる）。
       駒だけ pointer-events:auto で拾い直す。
     ■ 層自体には filter / opacity<1 / overflow:hidden を付けないこと（3Dが潰れる）。 */
  #board > .ly {
    position: absolute; inset: 0;
    pointer-events: none;
    transform-style: preserve-3d;
  }
  /* 演出だけの2層は盤の縁で切る。範囲攻撃の輪は3×3ぶんの大きさがあるので、
     最下段で撃つと盤の下枠からはみ出す（#board は overflow:visible なので切られない）。
     ※ transform-style: flat を**明示的に**書くこと。preserve-3d のままだと Chrome は
       overflow:hidden のクリップを効かせない（実際に効かず、輪が枠外へ出たままだった）。
     ※ この2層に入るのは .aoecell / .shockfx / .sweepfx / .burncell ＝ 3D変形を
       持たない要素だけなので flat で問題ない。
       **ビルボード（駒・草）をこの層へ入れてはいけない**（潰れて地面に寝る） */
  /* ※ セレクタは #board > を付けること。上の #board > .ly（id+class）の方が詳細度が高く、
       #lyDecalHi 単独では transform-style が preserve-3d のまま上書きできない */
  #board > #lyDecalLo, #board > #lyDecalHi { overflow: hidden; border-radius: 12px; transform-style: flat; }
  #fieldgrid    { z-index: 5; }   /* マス目 */
  #zoneHint     { z-index: 6; }   /* 配置可能ゾーン */
  #lyDecalLo    { z-index: 10; }  /* 地面デカール（着弾域・衝撃波・燃える地面） */
  #lyProps      { z-index: 20; }  /* 地物・奥（岩山・草花） */
  #predictLayer { z-index: 25; }  /* 攻撃予測ライン: 岩山より上・駒より下（顔を隠さない） */
  #lyUnits      { z-index: 30; }  /* 駒・宝箱（この中で行番号が z） */
  #lyGrassHi    { z-index: 40; }  /* 地物・手前（最前列の足元に被さる） */
  #lyDecalHi    { z-index: 50; }  /* 空中デカール（薙ぎ払いの軌跡） */
  #predictHeads { z-index: 56; }  /* 矢じりは駒より上 */
  #lyLight      { z-index: 60; }  /* 光 */
  /* 層が pointer-events:none なので、拾いたいものだけ戻す。
     宝箱は元々クリックしても何も起きないので none のまま＝タップが盤へ抜け、
     空きマス扱いになる（従来は宝箱が target になり、3D逆写像に乗らない
     clientX/Y 経路へ落ちてマスがずれていた） */
  #lyUnits .tok { pointer-events: auto; }

  /* ── 配置中だけ「足元」しかタップ判定にしない ──
     駒の絵は行間の約1.5倍の高さがあるので、頭が**1マス奥のマスに被る**。
     判定が絵全体のままだと、奥へ動かそうとしたタップが必ず自分自身の頭に吸われて
     一歩も下がれない（実測: 自陣の駒の1マス奥のセル中心が、その駒自身に当たっていた）。
     .placing（＝動かせる味方を選択中）のときだけ、判定を足元の .thit に絞る。
     ※ 選択していない間は絵全体が判定のまま＝駒を選ぶのは今までどおり押しやすい。
     ※ .thit は .tok の子なので onBoardTap の closest('.tok') はそのまま通る。
     ※ 高さは**駒の高さに対する%**で持つこと。駒の高さ＝行間×TOK_OVER(1.5) が全端末で
       固定なので、42% は必ず「行間の0.63倍」に落ち、1マス奥の中心（足元から行間ぶん上）に
       決して届かない。px でJSから渡す方式は失敗した — 盤のパースで手前の行ほど拡大され、
       駒座標系のpxが画面pxと一致しないため（実測 34.1px指定が画面では40pxになり1px足りず）。 */
  .tok .thit {
    position: absolute; left: 50%; bottom: -2px;
    /* 55% ＝ 行間の約0.73倍。1マス奥の中心（足元から行間＋約9px 上）まで
       どの端末でも 6px 以上の余裕を残しつつ、指で押せる大きさ（375で44×32px）を確保する。
       横幅は駒の箱いっぱい。列の間隔(53px)より狭いので左右のマスも潰さない */
    width: 100%; height: 55%;
    margin-left: -50%;
    pointer-events: none;      /* 通常時は絵全体で拾うので、この矩形は無効のまま */
  }
  #board.placing #lyUnits .tok { pointer-events: none; }
  #board.placing #lyUnits .tok .thit { pointer-events: auto; }

  /* ───── 地形の小物（草花・img/map/*.webp 32×32）─────
     駒(.tok)と同じビルボード変形で立てる。transform-origin が足元なので、
     どれだけ拡縮しても接地点（style の top）は動かない＝草が浮かない。
     ※ 変形の打ち消しは親（層）が preserve-3d でないと効かない。上の「盤の層」参照 */
  .prop {
    position: absolute;
    width: var(--pw, 24px); height: var(--pw, 24px);
    margin-left: calc(var(--pw, 24px) / -2);
    margin-top: calc(var(--pw, 24px) * -1);   /* 絵の下端を top の線に合わせる＝そこが接地点 */
    image-rendering: pixelated;
    pointer-events: none;
    -webkit-user-drag: none;
    transform-origin: 50% 100%;
    transform: scaleY(calc(1 / var(--stretch))) rotateX(calc(-1 * var(--tilt))) scale(var(--tok-scale, 1));
  }
  /* 風で揺らす。transform だけのアニメ＝合成のみで動くので枚数が増えても負荷は乗らない。
     ※ keyframe には打ち消し変換ごと書くこと。rotate だけ書くと transform が丸ごと
       置き換わり、草が寝てしまう（アニメーション由来の値は宣言より強い） */
  @media (prefers-reduced-motion: no-preference) {
    .prop { animation: propsway 3.9s ease-in-out infinite; }
    #lyGrassHi .prop { animation-duration: 4.7s; }
    .prop:nth-child(2n) { animation-delay: -1.3s; }
    .prop:nth-child(3n) { animation-delay: -2.6s; }
    .prop:nth-child(5n) { animation-delay: -.7s; }
  }
  @keyframes propsway {
    0%, 100% { transform: scaleY(calc(1 / var(--stretch))) rotateX(calc(-1 * var(--tilt))) scale(var(--tok-scale, 1)) rotate(-1.6deg); }
    50%      { transform: scaleY(calc(1 / var(--stretch))) rotateX(calc(-1 * var(--tilt))) scale(var(--tok-scale, 1)) rotate(1.6deg); }
  }

  /* ───── 光の層 ─────
     写真でいうグレーディング。絵は足さず、地形・駒・演出の「上から」まとめて色を掛ける。
       ① 空気遠近: 奥（盤の上端）ほど霞ませる。色は地形ごと（BIOMES[].haze → --haze）
       ② ビネット: 四隅を落として中央へ視線を集める
     ※ mix-blend-mode は使わない。3D空間(preserve-3d)の中で合成モードを使うと
       ブラウザによっては層が平面化される。素のアルファ合成だけで足りる。
     ※ --haze0 は「同じ色のゼロアルファ」。素の transparent へ補間するとグレーに濁る */
  #lyLight {
    border-radius: 12px;
    background:
      radial-gradient(ellipse 84% 76% at 50% 44%, #00000000 50%, #00000014 74%, #00000047 100%),
      linear-gradient(180deg, var(--haze, #cfe0ea3d) 0%, var(--haze0, #cfe0ea00) 40%);
  }
  /* 地形画像モードのマス目オーバーレイ。クラシックは盤の背景自体が格子なので使わない。
     デフォルト(image)は配置・移動時(.placing)だけ、imagegridは常時うっすら表示。線は明色で
     暗い地形(草原/岩地/溶岩)によく乗る。雪原では淡いが、明るい地形はマスが見やすいので許容。
     ※ 行数・列数は #board と同じく --rows / --cols から引く（分数を直に書かないこと） */
  #fieldgrid {
    position: absolute; inset: 0; border-radius: 12px; pointer-events: none;
    opacity: 0; transition: opacity .2s ease;
    background:
      repeating-linear-gradient(0deg, transparent 0 calc(100%/var(--rows,8) - 1px), #ffffff2e calc(100%/var(--rows,8) - 1px) calc(100%/var(--rows,8))),
      repeating-linear-gradient(90deg, transparent 0 calc(100%/var(--cols,7) - 1px), #ffffff2e calc(100%/var(--cols,7) - 1px) calc(100%/var(--cols,7)));
  }
  #board.fieldimg.placing #fieldgrid { opacity: 1; }
  #board.fieldgridalways #fieldgrid { opacity: 1; }

  /* ───── map token（盤の変形を打ち消して直立するビルボード） ───── */
  /* width は「タップ判定の幅」でもある（.tport は 58px までこの箱からはみ出して描かれる）。
     立ち絵を大きくしたぶん、指で掴める幅も広げてある。中身は align-items:center で
     中央に揃うので、幅を変えても絵の位置はずれない */
  .tok {
    position: absolute;
    width: 44px;
    margin-left: -22px;
    margin-top: -16px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 1px;
    transition: left .2s cubic-bezier(.3,.8,.4,1), top .2s cubic-bezier(.3,.8,.4,1), opacity .3s;
    cursor: pointer;
    transform-origin: 50% 100%;
    /* 末尾の scale は端末ごとの駒サイズ調整（js/render.js updateTokenScale が --tok-scale を設定）。
       変数が無ければ scale(1)＝無操作。原点が足元なので接地位置は動かない */
    transform: scaleY(calc(1 / var(--stretch))) rotateX(calc(-1 * var(--tilt))) scale(var(--tok-scale, 1));
  }
  /* 足元の影。二重にして「接地」を作る:
       ① 中心の濃い核 = 接地点。ここが締まっていないと駒が浮いて見える
       ② 外側の淡い半影 = 光の回り込み。単層のぼかしだけだと綿のような塊になる
     色は --shc（rgbの3値）で差し替える。陣営で染めるとき background を上書きすると
     二層構造ごと消えるので、必ず変数側を変えること。
     ※ 盤を寝かせても影の形はほぼ変わらない（実測: 30×8px の楕円が地面に寝ると 30×9.6px。
       盤の rotateX(34°) を scaleY(1.45) がほぼ打ち消すため）。地面の層へ移す価値は無い */
  .tok .tsh {
    --shc: 0, 0, 0;
    position: absolute;
    bottom: -3px; left: 50%;
    width: 28px; height: 9px;
    margin-left: -14px;
    border-radius: 50%;
    background:
      radial-gradient(ellipse 44% 62% at 50% 54%, rgba(var(--shc), .60) 0%, rgba(var(--shc), 0) 100%),
      radial-gradient(ellipse at center, rgba(var(--shc), .34) 0%, rgba(var(--shc), .15) 52%, rgba(var(--shc), 0) 78%);
    z-index: -1;
    pointer-events: none;
  }
  /* 駒が idlebob で浮いた分だけ影を縮めて薄くする（周期・遅延は .tport と揃える）。
     影が固定のままだと、上下しているのに接地しっぱなしに見えてしまう */
  @keyframes shbob { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(.88); opacity: .72; } }
  .tok .tsh { animation: shbob 2.6s ease-in-out infinite; }
  .tok:nth-child(2n) .tsh { animation-delay: -.8s; }
  .tok:nth-child(3n) .tsh { animation-delay: -1.6s; }
  .tok.boss .tsh, .tok.s3 .tsh, .tok.s4 .tsh { width: 34px; margin-left: -17px; height: 11px; }
  .tok.hid { opacity: .45; } /* 隠れ身中は薄く表示 */
  .tok .tport {
    width: 25px; height: 25px;
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 15px;
    border: 2px solid;
    box-shadow: 0 1px 3px #000000aa, inset 0 1px 0 #ffffff66;
    background: radial-gradient(circle at 35% 30%, #fff, #d9c8a3);
    animation: idlebob 2.6s ease-in-out infinite;
  }
  .tok:nth-child(2n) .tport { animation-delay: -.8s; }
  .tok:nth-child(3n) .tport { animation-delay: -1.6s; }
  @keyframes idlebob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-1.5px); } }
  .tok.foe .tport { background: radial-gradient(circle at 35% 30%, #d8c5b2, #9a8270); border-color: #4a3326; }
  .tok.boss .tport {
    width: 31px; height: 31px; font-size: 19px;
    border-color: #c0392b;
    box-shadow: 0 0 10px #e74c3c88, 0 1px 3px #000;
  }
  .tok.boss { margin-top: -19px; }
  .ring-war .tport  { border-color: var(--war); }
  .ring-tank .tport { border-color: var(--tank); }
  .ring-mage .tport { border-color: var(--mage); }
  .ring-merc .tport { border-color: var(--merc); }
  .ring-pact .tport { border-color: var(--pact); }
  /* 契約の精霊: 蝋燭の灯のような淡い輝き（星・陣営色とは干渉しない控えめな光） */
  /* 契約の精霊（召喚）。契の識別色に合わせた緑の光彩（立ち絵表示では .tok.hasimg .tport が
     box-shadow を打ち消すので、円メダル＝クラシック表示のときだけ効く） */
  .tok.smn .tport { box-shadow: 0 0 8px #37a05a88, 0 1px 3px #000000aa, inset 0 1px 0 #ffffff66; }
  .tok.tier3 .tport { box-shadow: 0 0 8px #e8b33d77, 0 1px 3px #000; }
  /* 闘技場: 敵味方は「アイコンの背景色」で識別（自軍=薄い青／相手軍=薄い赤）。
     星レベルは 枠色・グロー・回転リング・★マーク で表現されるので、
     背景＝陣営 ／ 枠まわり＝星 と役割を分けることで色が競合しない。
     （足元の影は .tok が直立ビルボードのため地面に寝ず見えにくいので識別には使わない） */
  #board.arena .tok:not(.foe) .tport { background: radial-gradient(circle at 35% 30%, #cbe6ff, #5f9fdb); }
  #board.arena .tok.foe .tport { background: radial-gradient(circle at 35% 30%, #ffd5c8, #dd7154); }
  /* 捕獲魔物（円メダル＝クラシック表示のとき）: 緑リング+緑肌+🐾バッジで通常味方と識別。
     ※ :not(.foe) が要る。闘技場の幽霊は敵側にも pet が付く（arena.js の side:'E', pet:true）ため、
       付けないと「相手の幽霊が味方色の緑リングを纏う」＝識別が逆に壊れる
     ※ :not(.hasimg) も必須。詳細度が上がった分だけ「円メダルを消す」.tok.hasimg .tport に
       競り勝ってしまい、立ち絵の背後に緑の四角が復活する（実際にそうなった）。
       立ち絵側の識別は下の .tok.hasimg.pet ブロックが受け持つ */
  .tok.pet:not(.foe):not(.hasimg) .tport {
    border-color: var(--pet);
    box-shadow: 0 0 8px #4ad67e88, 0 1px 3px #000;
    background: radial-gradient(circle at 35% 30%, #d9f0e0, #7fbb93);
  }
  .tok .tpet {
    position: absolute; top: -8px; right: -10px;
    font-size: 10px; z-index: 2;
    filter: drop-shadow(0 1px 1px #000);
    pointer-events: none;
  }
  /* star-level appearance */
  .tok.s2 { margin-top: -17px; }
  .tok.s2 .tport {
    width: 27px; height: 27px; font-size: 16px;
    border-width: 2.5px;
    box-shadow: 0 0 8px #dfe6f0bb, inset 0 0 5px #ffffff99, 0 1px 3px #000;
  }
  .tok.s2 .tstars { color: #eef3fa; text-shadow: 0 0 5px #aebfd6, 0 1px 1px #000; }
  .tok.s3 { margin-top: -19px; }
  .tok.s3 .tport {
    width: 30px; height: 30px; font-size: 18px;
    border-width: 2.5px;
    border-color: var(--gold);
    background: radial-gradient(circle at 35% 30%, #fffbe8, #f0d589);
    box-shadow: 0 0 11px var(--gold), inset 0 0 6px #ffffffcc, 0 1px 3px #000;
    animation: idlebob 2.6s ease-in-out infinite, s3pulse 1.6s steps(8) infinite;
  }
  @keyframes s3pulse { 50% { box-shadow: 0 0 18px var(--gold), inset 0 0 6px #ffffffcc, 0 1px 3px #000; } }
  .tok.s3::before {
    content: '';
    position: absolute;
    top: -4px; left: 50%;
    width: 38px; height: 38px;
    margin-left: -19px;
    border-radius: 50%;
    border: 2px dashed var(--gold);
    opacity: .8;
    animation: spinring 5s linear infinite;
    pointer-events: none;
  }
  @keyframes spinring { to { transform: rotate(360deg); } }
  /* ★4 — prismatic */
  .tok.s4 { margin-top: -20px; }
  .tok.s4 .tport {
    width: 32px; height: 32px; font-size: 19px;
    border-width: 3px;
    border-color: #fff;
    background: radial-gradient(circle at 35% 30%, #fff, #ffe9c4);
    animation: idlebob 2.6s ease-in-out infinite, s4pulse 2.4s steps(8) infinite;
  }
  @keyframes s4pulse {
    0%   { box-shadow: 0 0 13px var(--gold), inset 0 0 6px #fff, 0 1px 3px #000; }
    33%  { box-shadow: 0 0 13px #ff7eb5, inset 0 0 6px #fff, 0 1px 3px #000; }
    66%  { box-shadow: 0 0 13px #7ecbff, inset 0 0 6px #fff, 0 1px 3px #000; }
    100% { box-shadow: 0 0 13px var(--gold), inset 0 0 6px #fff, 0 1px 3px #000; }
  }
  .tok.s4::before {
    content: '';
    position: absolute;
    top: -4px; left: 50%;
    width: 40px; height: 40px;
    margin-left: -20px;
    border-radius: 50%;
    border: 2px dashed var(--gold);
    opacity: .85;
    animation: spinring 4s linear infinite;
    pointer-events: none;
  }
  .tok.s4::after {
    content: '';
    position: absolute;
    top: -8px; left: 50%;
    width: 48px; height: 48px;
    margin-left: -24px;
    border-radius: 50%;
    border: 1.5px dotted #ff8fd6;
    opacity: .7;
    animation: spinringrev 6s linear infinite;
    pointer-events: none;
  }
  @keyframes spinringrev { to { transform: rotate(-360deg); } }
  .tstars.s4 { color: #fff; text-shadow: 0 0 6px var(--gold), 0 0 3px #ff8fd6, 0 1px 1px #000; animation: starglow 1s steps(5) infinite; }
  /* dangerous ace */
  .tok.ace { margin-top: -18px; }
  .tok.ace .tport {
    width: 28px; height: 28px; font-size: 17px;
    border-color: #8e2f9e;
    box-shadow: 0 0 10px #c04ad6cc, 0 1px 3px #000;
    animation: idlebob 2.6s ease-in-out infinite, acepulse 1.3s steps(7) infinite;
  }
  @keyframes acepulse { 50% { box-shadow: 0 0 17px #c04ad6, 0 1px 3px #000; } }
  .tthreat {
    position: absolute;
    top: -8px; right: -6px;
    background: linear-gradient(180deg, #c0392b, #8e2420);
    color: #fff;
    font-size: 7.5px; font-weight: 900;
    padding: 1px 4px;
    border-radius: 999px;
    border: 1px solid #5e1512;
    box-shadow: 0 1px 3px #000;
    pointer-events: none;
    z-index: 3;
    animation: hintbounce 1.2s infinite;
  }
  .tok.selected .tport {
    box-shadow: 0 0 0 3px var(--gold), 0 0 12px var(--gold), 0 1px 3px #000;
    animation: selbounce .6s ease-in-out infinite;
  }
  @keyframes selbounce { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
  .tok.spawn { animation: spawnin .35s cubic-bezier(.34,1.56,.64,1); }
  @keyframes spawnin { from { transform: scaleY(calc(1 / var(--stretch))) rotateX(calc(-1 * var(--tilt))) scale(calc(var(--tok-scale, 1) * .2)); opacity: 0; } }

  /* HPバーは頭上。俯角を深くして駒が前後に重なるようにしたので、
     足元に置くと前列の駒に隠れて読めなくなる（隠れるのは足元だけ＝頭上は必ず見える）。
     ※ position:absolute で flow から外すのが肝。.tok の margin-top は
       ★・ボス・立ち絵で別々に詰めてあり、flow に3px足すと全部ずれる。
       外に出せば箱の高さは .tport だけになり、transform-origin(50% 100%)＝足元も
       ちょうど絵の下辺に一致する（足元の影 .tsh の bottom:0 も正しくなる）。
     ※ render.js の TOK_H は「flowに載る高さ」なので 54 → 50 に合わせてある

     ■ 高さを詰めるときの top の基準は「.tport の箱の上端」であって「頭の上端」ではない。
       この2つは一致しない:
       - クラシック(絵文字)表示: .tport は border-box の円が箱いっぱいに描かれるので、
         箱の上端＝頭（円）の上端。top:-3px（高さ3pxぶん）で厳密にゼロ距離まで詰まる。
       - 立ち絵(hasimg)表示: .tport は 46×50（正方形の絵に対して縦长）。
         object-fit:contain は幅46基準で高さ46に収まり、余った 50-46=4px は
         object-position:bottom により**箱の上側に丸ごと空く**（＝透明な余白）。
         さらに絵そのものの中にも少量の余白がある（手持ちのドット絵4種を実測: 頭頂は
         キャンバス上端から 1%〜9%＝約0.5〜4.3px）。合計で箱の上端から頭の上端まで
         実測 約4.5〜8px の見えない距離があり、クラシックと同じ top では全く詰まらない。
       → 表示モードごとに別の top を持たせること。立ち絵側は「箱の上端に揃える(0px)」
         だけで、透明な余白がそのまま安全マージンになり、どの絵柄でも重ならない
         （実測した最小の余白でも約4.5px残る）。 */
  .tok .thp {
    position: absolute;
    left: 50%; top: -3px;             /* クラシック(絵文字)表示: 円の上端＝頭の上端に厳密フィット */
    width: 24px; height: 3px;
    margin-left: -12px;
    border-radius: 2px;
    background: #00000088;
    overflow: hidden;
    pointer-events: none;
  }
  /* 立ち絵表示。箱を正方形にして絵が上端まで届くようになったので、その分だけ持ち上げる。
     ⚠️ **箱の上端(0px)ではもう足りない。** 以前は contain の letterbox で 6px の透明帯が
       あり、それが安全マージンになっていた（上の長い注記はその頃のもの）。
       いま頭上に残るのは**絵そのものの余白だけ**で、全44体を実測すると padTop は
       キャンバス比 0%〜13%、**最小は0px**（城塞兵・妖剣士＝箱の上端が頭のてっぺん）。
     ⚠️ top は **.tport ではなく .tok 基準**（.tport は .tok より 1px 上にはみ出す。実測）。
       バーは高さ3px なので、頭上に 4.5px 残すには top = -(4.5+3+1) ≒ **-9px**。
       これで旧版（letterbox 6px ＋ top:0）と同じ余白に戻る。 */
  .tok.hasimg .thp { top: -9px; }
  .tok.hasimg .tstars { top: -24px; }  /* HPバーを9px上げたぶん★も同じだけ上げる（間隔7pxを保つ） */
  .tok .thp i {
    display: block; height: 100%;
    background: linear-gradient(90deg, #8ed47c, #5da34c);
    border-radius: 2px;
    transition: width .3s;
  }
  .tok .thp i.low { background: linear-gradient(90deg, #e3a24a, #d4583a); }
  .tok .tstars {
    position: absolute; top: -15px; left: 50%; transform: translateX(-50%);   /* 頭上のHPバーの上へ */
    font-size: 8px; color: var(--gold); line-height: 1;
    text-shadow: 0 0 4px #b07f1e, 0 1px 1px #000;
    letter-spacing: -1px;
    pointer-events: none;
  }
  /* 戦闘が始まったら★は消す。星は「編成をどう組んだか」の情報で、戦闘中に要るのは
     生死＝HPだけ。俯角を深くして駒が重なるぶん、頭上の情報は1本に絞る。
     （render.js が S.phase==='battle' で #board に .battling を付ける） */
  #board.battling .tstars { display: none; }
  .tok .tstars.s3 { animation: starglow 1.4s steps(7) infinite; }
  @keyframes starglow { 50% { text-shadow: 0 0 9px var(--gold), 0 1px 1px #000; } }

  /* ───── 宝箱 ───── */
  .chest {
    position: absolute;
    width: 24px;
    margin-left: -12px;
    margin-top: -13px;
    display: flex;
    flex-direction: column;
    align-items: center;
    pointer-events: none;
    transform-origin: 50% 100%;
    transform: scaleY(calc(1 / var(--stretch))) rotateX(calc(-1 * var(--tilt))) scale(var(--tok-scale, 1));
  }
  .chest::before {
    content: '';
    position: absolute;
    bottom: -2px; left: 50%;
    width: 20px; height: 7px;
    margin-left: -10px;
    border-radius: 50%;
    background: radial-gradient(ellipse at center, #e8b33d55, transparent 70%);
  }
  /* 中身は gi('chest') ＝ img/ui/chest.webp（🔤絵文字モードでは🎁へ戻る）。
     .gi のサイズは 1.05em なので、絵の大きさはここの font-size で決まる。
     14px（絵文字だった頃の値）→腰くらいの高さにするため30px。
     display:flex は .gi のベースライン分の余白を殺して接地位置をぶらさないため */
  .chest .cbody {
    font-size: 30px; line-height: 1;
    display: flex; align-items: center; justify-content: center;
    filter: drop-shadow(0 1px 2px #000);
    animation: chestbob 2.2s ease-in-out infinite;
  }
  @keyframes chestbob { 50% { transform: translateY(-2px); } }
  .chest .cg {
    font-size: 7px; font-weight: 900; color: var(--gold);
    text-shadow: 0 0 3px #b07f1e, 0 1px 1px #000;
    line-height: 1; margin-top: 1px;
  }

  /* ───── info strip ───── */
  #infoBar {
    flex: 0 0 auto;
    /* 高さを固定する（min-height だと中身によって 22〜25px で揺れ、その差だけ盤が縮む＝
       駒をタップするたびにマップが拡大縮小して見えた）。中身は nowrap+hidden で切り詰める */
    height: 26px;
    font-size: 10.5px;
    font-weight: 700;
    color: var(--parchment);
    background: #00000044;
    border: 1px solid #ffffff14;
    border-radius: 8px;
    padding: 3px 9px;
    display: flex;
    align-items: center;
    gap: 6px;
    overflow: hidden;
    white-space: nowrap;
  }
  #infoBar .btag { color: var(--gold); }
  #infoBar:empty { opacity: .3; }

  /* ───── synergy ───── */
  #synergy { display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; flex: 0 0 auto; }
  .syn {
    display: flex; align-items: center; gap: 3px;
    font-size: 9.5px; font-weight: 700;
    padding: 2px 7px; border-radius: 999px;
    background: #00000044; border: 1px solid #ffffff18;
    color: #ffffff66;
    transition: all .3s;
  }
  .syn.lv1 { color: var(--parchment); border-color: #ffffff55; }
  .syn.lv2 {
    color: #fff; border-color: var(--gold);
    background: linear-gradient(180deg, #e8b33d44, #b07f1e33);
    box-shadow: 0 0 10px #e8b33d55;
  }
  /* 8体シナジー（神の加護）: 金を超える神聖な輝き */
  .syn.lv3 {
    color: #fff; border-color: #fff;
    background: linear-gradient(180deg, #e8b33d66, #c04ad633);
    box-shadow: 0 0 12px #e8b33d99, 0 0 22px #c04ad655;
  }
  .syn b { font-size: 10.5px; }
  /* 段階しきい値（2/4/6）: 発動中の段階だけ系統色で点灯、他はグレー */
  .syn .stp { font-style: normal; color: #ffffff3d; }
  .syn .stp.sl { color: #ffffff24; }
  .syn .stp.on { text-shadow: 0 0 6px currentColor; }
  .syn[data-t="war"] .stp.on { color: var(--war); }
  .syn[data-t="tank"] .stp.on { color: var(--tank); }
  .syn[data-t="mage"] .stp.on { color: var(--mage); }
  .syn[data-t="merc"] .stp.on { color: var(--merc); }
  .syn[data-t="pact"] .stp.on { color: var(--pact); }
  .syn.justlit { animation: synpop .4s cubic-bezier(.34,1.56,.64,1); }
  @keyframes synpop { 0% { transform: scale(.7); } }
  .tdot { width: 6px; height: 6px; border-radius: 50%; box-shadow: 0 0 2px #00000066; }
  .tdot.war { background: var(--war); } .tdot.tank { background: var(--tank); }
  .tdot.mage { background: var(--mage); } .tdot.merc { background: var(--merc); }
  .tdot.pact { background: var(--pact); }

  /* ───── shop ───── */
  #shopArea {
    flex: 0 0 auto;
    background: linear-gradient(180deg, #1d120b, #281a10);
    border: 1px solid #00000066;
    border-radius: 14px;
    padding: 7px;
    display: flex; flex-direction: column; gap: 6px;
    box-shadow: inset 0 1px 0 #ffffff10, 0 -3px 10px #00000044;
  }
  /* 見出し行。左=店名／中=控えの顔／右=利子の予告（2段）。
     ⚠️ **高さは常に一定に保つこと。** 控えが空でも枠を描き、利子も必ず2行にしてある。
       条件で行数が変わると #shopArea が伸縮して**盤が拡大縮小して見える**（過去に踏んだ） */
  #shopTitle { display: flex; justify-content: space-between; align-items: center; font-size: 10.5px; font-weight: 700; color: var(--parchment-dim); padding: 0 2px; gap: 6px; }
  #shopName { white-space: nowrap; flex: none; }
  #incomeNote { white-space: nowrap; flex: none; text-align: right; line-height: 1.25; font-size: 9.5px; }
  #incomeNote i { font-style: normal; opacity: .7; }
  /* ── 2段目: Tier出現率 ＋ 控えの顔 ──
     ⚠️ **高さは常に一定**。#tierOdds は :empty で消える（宴・チュートリアル・戦闘中）ので、
       行の高さは #benchSlots が決める。控えが空でも枠を出すのはそのため */
  #shopRow2 { display: flex; align-items: center; gap: 6px; padding: 1px 2px 0; }
  /* ── 控えの顔 ──
     見出し2行の**まん中の空き**に重ねる。行の中に入れないので**高さを一切食わない**
     （右端に流し込んだ版は2行目を 15→31px に押し広げ、盤を16px削っていた）。
     ⚠️ 絶対配置なので #tierOdds が消えても動かない。
     ⚠️ **枠の高さは2行ぶん（約39px）を超えないこと。** 超えると下の募兵カードに被る。
     ⚠️ 左は店名・右は利子の予告と当たらない幅に収めること（実測: 360pxでも
       店名は 95px まで、利子は 275px から。控えは 2枠＋すき間で 101px）。 */
  /* ⚠️ **高さを固定すること。** #tierOdds は :empty で消える（戦闘中・宴・チュートリアル）ので、
     固定しないと**戦闘に入った瞬間にブロックが 38→25px に縮んで盤が11px伸びる**。
     募兵→戦闘の切り替わりで盤が animate せずに跳ねるのが、いちばん目につく崩れ方だった。
     値は「控え枠(34px) ＋ 上下の余白」。控えを大きくするならここも一緒に上げること */
  #shopHead { position: relative; min-height: 38px; }
  #benchSlots {
    display: flex; gap: 5px;
    position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
    z-index: 2;
  }
  .bslot {
    width: 48px; height: 34px; flex: none; position: relative;
    border-radius: 4px; border: 1px dashed #ffffff44; background: #00000038;
    display: flex; align-items: flex-end; justify-content: center; overflow: hidden;
  }
  /* 空き枠にも 🪑 を薄く入れる。**枠だけだと「そこに何かある」と気づけない**
     （前の版が「分かりにくい」と言われた原因がこれ） */
  .bslot:not(.has)::after {
    content: '🪑'; font-size: 16px; opacity: .3; line-height: 34px;
  }
  .bslot.has { border-style: solid; border-color: #ffffff33; background: #3a2a18; cursor: pointer; }
  .bslot.has.on { border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold), 0 0 6px #e8b33d66; }
  .bslot:active { transform: translateY(1px); }
  /* 盤の駒を選んでいる間は「送り先」。**部隊→控えの道はここにしか無い**ので、
     光らせないと片道の機能に見える（🪑控えへ ボタンを廃した代わり）。
     ⚠️ box-shadow だけで示すこと。枠の大きさを変えると見出し行の高さが動いて盤が跳ねる */
  .bslot.drop { border-color: var(--gold); border-style: dashed; cursor: pointer;
                box-shadow: 0 0 5px #e8b33d55 inset; }
  .bslot.drop:not(.has)::after { opacity: .6; }
  /* 募兵カードと**同じ「胸像」の切り方**（.card .cport.bust .uimg 参照）。
     ⚠️ 枠が正方形だと cover は何も切らない（絵も 96×96 の正方形）＝全身が縮むだけで、
       object-position を書いても効かない。**横長の枠にして初めて下半身が枠外へ逃げる。**
       34×24（比1.42）で上から 71%＝頭〜腰。札絵と同じ見え方になる */
  .bface { width: 100%; height: 100%; object-fit: cover; object-position: top center;
           image-rendering: pixelated; }
  .bslot b {
    position: absolute; right: 1px; top: 0; font-size: 9px; color: #ffe9a8;
    text-shadow: 0 1px 2px #000, 0 0 3px #000;
  }
  /* 募兵のTier出現率。募兵カードの真上に独立した行として置く（幅の狭い端末でも
     見出し行を圧迫しない）。中身が空＝宴・チュートリアル・戦闘中は行ごと消える */
  /* padding は #shopRow2 が持つ（二重に付けない） */
  #tierOdds { display: flex; gap: 3px; flex: none; }
  #tierOdds:empty { display: none; }
  .todds { font-size: 9px; font-weight: 900; padding: 0 4px; border-radius: 999px; color: #1c150c; white-space: nowrap; }
  .todds.t1 { background: var(--t1); color: #f4e6d2; }
  .todds.t2 { background: var(--t2); }
  .todds.t3 { background: var(--t3); }
  /* 0%（宴で出ないTier）は暗く沈めて「来ない」ことをひと目で示す */
  .todds.zero { background: #ffffff14; color: #ffffff55; }
  /* ───── 募兵カードと実況ログの共有枠 ─────
     「開戦の瞬間に盤面が跳ねる」対策。この枠の高さは **常に募兵カード側だけ** が決め、
     実況ログは絶対配置＝レイアウト上の高さを持たないので、⚔️で入れ替えても
     #shopArea の高さは1pxも動かない（＝#boardWrap に余りが渡らない）。
     カードの高さは固定できない: 肖像枠が幅追従(aspect-ratio)なので画面幅で 121〜131px と動き、
     珍客（ワンコ/ネコ/ナース/ｱｶﾈﾁｬﾝ）と疲れた実力者は説明文が2行に折り返してさらに +11px 伸びる。
     ※ 以前は #shopCards の min-height と #battleLog の height を手で揃えていたが、
       一致するのは「特定の画面幅 × 珍客のいない募兵」だけで、それ以外では必ずずれていた */
  #shopStage { position: relative; }
  /* min-height は実況ログの下限（カードが低い狭幅端末でも実況が窮屈にならない高さ）。
     跳ね対策ではないので #battleLog と揃える必要はもう無い */
  #shopCards { display: flex; gap: 6px; justify-content: center; min-height: 128px; }

  /* ───── 戦況実況ログ（戦闘中は募兵枠を再利用）───── */
  #battleLog {
    display: none;               /* 戦闘中のみ #shopArea.battling で表示 */
    position: absolute; inset: 0; /* 募兵カードの枠にぴったり重ねる。
                                     高さを持たない＝カードの高さがそのまま酒場の高さになる */
    flex-direction: column;
    justify-content: flex-end;    /* 下端に最新、古いものは上へ押し上げられて流れる */
    align-items: stretch;
    overflow: hidden;
    padding: 0 2px;
    gap: 2px;
    -webkit-mask-image: linear-gradient(180deg, transparent 0, #000 36%, #000 100%);
            mask-image: linear-gradient(180deg, transparent 0, #000 36%, #000 100%);
  }
  /* display:none にしてはいけない — 枠の高さを決めているのはカード側なので、消すと高さが失われて盤が跳ねる。
     visibility なら場所を取ったまま消え、タップも通らない */
  #shopArea.battling #shopCards { visibility: hidden; }
  #shopArea.battling #battleLog { display: flex; }

  /* ───── コンボ枠（左のレール） ─────
     **戦闘中ずっと出しっぱなし**にする（S.phase==='battle' のあいだ #shopArea.combo）。
     点呼のときだけ出して消していたが、戦闘が始まった瞬間に「何コンボでいくら盛れているか」が
     見えなくなるのは情報として惜しい。
     右の 7割は実況ログがそのまま使う。点呼で読み上げる陣形名も**実況ログの1行として**積むので、
     コンボの読み上げ → 戦況実況 が同じ列を流れて途切れない（別の箱に分けると切り替わりが出る）。
     募兵カードの枠に重ねる絶対配置なので、点呼中も #shopArea の高さは1pxも動かない＝盤が跳ねない。 */
  #comboPane {
    display: none;
    position: absolute; left: 0; top: 0; bottom: 0;
    width: 30%;
    padding: 1px 3px;
    border-right: 1px solid #ffffff22;
    overflow: hidden;
  }
  #shopArea.combo #comboPane { display: flex; }
  /* 実況ログはレールのぶんだけ右へ寄る（幅を持つのはここだけ＝高さには一切触らない） */
  #shopArea.combo #battleLog { left: 30%; padding-left: 7px; }

  /* ⚠️ **中身はレールの高さ（＝募兵カードの高さ）に収まりきること。** はみ出すと下が切れる
     （overflow:hidden なので崩れはしないが、3位が消える）。
     実測（`v1.66.0`）: 360/375px幅で 枠126 中身126 ／ 430px幅で 枠128 中身128。**余白ゼロ**。
     行や文字を増やすときは必ず幅3種で測り直すこと
     （fx.js の comboPaneSet に「行数は常に2行」という同じ注意書きがある）

     🛑 **`#cpLeft` の高さを測っても、はみ出しは絶対に見つからない。**
       flex で高さが枠に固定されるので、中身が何px溢れても `getBoundingClientRect().height`
       は枠と同じ値を返す（`justify-content:center` なので上下へ均等に溢れて下だけ切れる）。
       確かめるときは **最後の `.cpT` の bottom と `#comboPane` の bottom を比べる**こと。
       これで測らずに「126/128 で収まっている」と誤判定して、3位が6px切れたまま出した。 */
  #cpLeft {
    flex: 1;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 2px;
    min-width: 0; overflow: hidden;
  }
  #cpLeft .cpN { display: flex; align-items: baseline; gap: 3px; line-height: 1; flex: none; }
  #cpLeft .cpN b {
    font-size: 17px; font-weight: 900; color: var(--gold);
    text-shadow: 0 0 8px #e8b33d66, 0 2px 2px #000;
    font-variant-numeric: tabular-nums;   /* 数が増えても幅が動かない＝左右にガタつかない */
  }
  #cpLeft .cpN span { font-size: 8px; font-weight: 700; color: var(--parchment-dim); letter-spacing: .04em; }
  /* バフの内訳。陣形（割合）とシナジー（定額）を2段で出す。
     どちらも「いま盤の駒に乗っている数字」で、ここが唯一まとまって見える場所 */
  #cpLeft .cpBuff {
    font-size: 10.5px; font-weight: 800; color: var(--parchment);
    text-align: center; line-height: 1.08; white-space: nowrap;
    display: flex; flex-direction: column; gap: 1px; flex: none;
  }
  #cpLeft .cpBuff i { font-style: normal; opacity: .5; font-size: 7px; display: block; letter-spacing: .02em; }
  #cpLeft .cpBuff .cpRow { display: block; }
  #cpLeft .cpBuff .cpRow b { display: block; font-size: 10.5px; }
  #cpLeft .cpBuff .cpRow.form b { color: var(--gold); }
  #cpLeft .cpBuff .cpRow.syn  b { color: #b6e3ff; }
  #cpLeft .cpBuff .cpRow.roll b { color: #ffc9e6; }
  /* バフ上位3体。**並べ替えは transform だけ**で動かす（fx.js の FLIP）ので、
     ここでレイアウトアニメを足さないこと（二重に動いて酔う） */
  #cpTop { display: flex; flex-direction: column; gap: 0; width: 100%; flex: none; }
  /* ⚠️ **幅に余りが無い**（レールは募兵枠の3割）。順位・顔・数字を足すと 375px幅で
     数字の右端が切れる。顔を大きくしたぶん、順位の桁とすき間を削って帳尻を合わせてある */
  #cpTop .cpT {
    display: flex; align-items: center; gap: 1px;
    font-size: 9px; font-weight: 800; color: var(--parchment);
    background: #00000030; border-radius: 4px; padding: 0 3px;
    white-space: nowrap; overflow: hidden;
  }
  /* 1位だけ金文字。順位の数字は幅が足りず置けない（fx.js の comboPaneTop の⚠️） */
  #cpTop .cpT:first-of-type b { color: var(--gold); }
  /* 控え枠・募兵カードと**同じ「胸像」の切り方**。正方形の枠では cover が何も切らない
     （.bface の⚠️と同じ罠）ので、ここも横長にすること。26×18（比1.44） */
  #cpTop .cpIcon { width: 24px; height: 16px; object-fit: cover; object-position: top center;
                   border-radius: 3px; background: #00000030;
                   image-rendering: pixelated; flex: none; }
  #cpTop .cpT b { font-weight: 800; letter-spacing: -.03em; }
  #cpTop .cpTopLbl {
    font-style: normal; font-size: 7px; font-weight: 700; opacity: .5;
    letter-spacing: .02em; text-align: center; line-height: 1; padding-bottom: 1px;
  }
  /* 数が増えた瞬間だけ跳ねる。transform なので合成のみ（README「描画コストの原則」） */
  #cpLeft .cpN b.pop { animation: cpPop .34s cubic-bezier(.3,.9,.35,1.3); }
  @keyframes cpPop { 0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); } }

  /* 点呼で読み上げる陣形名は**実況ログの1行**として積む（専用の箱は持たない）。
     そのまま戦況実況へ流れていくので、コンボ → 戦闘 の切り替わりが見えない */

  .blog-entry {
    text-align: center;
    line-height: 1.18;
    font-weight: 700;
    color: var(--parchment-dim);
    font-size: 10px;
    opacity: .28;
    transform: scale(.9);
    transition: font-size .3s ease, opacity .3s ease, transform .3s ease, color .3s ease;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    will-change: transform, opacity;
  }
  .blog-entry b { font-weight: 900; }
  /* 直近ほど大きく鮮やかに。最新行は下端でポップ */
  .blog-entry:nth-last-child(4) { font-size: 10px;   opacity: .32; }
  .blog-entry:nth-last-child(3) { font-size: 11.5px; opacity: .58; }
  .blog-entry:nth-last-child(2) { font-size: 13px;   opacity: .84; color: var(--parchment); }
  .blog-entry:nth-last-child(1) {
    font-size: 16px; opacity: 1; color: var(--parchment);
    transform: scale(1);
    animation: blogPop .34s cubic-bezier(.34,1.56,.64,1);
    text-shadow: 0 1px 3px #000a;
  }
  @keyframes blogPop {
    0%   { transform: translateY(11px) scale(.7); opacity: 0; }
    60%  { transform: translateY(-2px) scale(1.09); opacity: 1; }
    100% { transform: translateY(0) scale(1); opacity: 1; }
  }
  /* イベント種別の差し色（最新行のみ強調） */
  .blog-entry.k-kill:nth-last-child(1)   { color: #ffd66e; text-shadow: 0 0 10px #e8b33d66, 0 1px 3px #000a; }
  .blog-entry.k-boss:nth-last-child(1)   { color: #ffb0a0; text-shadow: 0 0 12px #d4583a88, 0 1px 3px #000a; }
  .blog-entry.k-down:nth-last-child(1)   { color: #ff8f7a; }
  .blog-entry.k-gold:nth-last-child(1)   { color: var(--gold); }
  .blog-entry.k-pet:nth-last-child(1)    { color: var(--pet); }
  .blog-entry.k-hit:nth-last-child(1)    { color: #ffe08a; }
  .blog-entry.k-ambush:nth-last-child(1) { color: #e79cf0; text-shadow: 0 0 10px #c04ad666, 0 1px 3px #000a; }
  /* 点呼の陣形名。実況と同じ列を流れるが、自分の成果か相手のものかは色で分ける */
  .blog-entry.k-form:nth-last-child(1)    { color: #ffd66e; text-shadow: 0 0 10px #e8b33d55, 0 1px 3px #000a; }
  .blog-entry.k-formfoe                   { color: #ff9aa0; }
  .blog-entry.k-formfoe:nth-last-child(1) { color: #ff9aa0; text-shadow: 0 0 10px #d4583a66, 0 1px 3px #000a; }

  /* ───── チュートリアル ───── */
  #tutBox {
    position: absolute;
    left: 12px; right: 12px;
    top: 31%;
    z-index: 40;                 /* 盤・駒より前、タイトル等のoverlayより後ろ */
    background: linear-gradient(180deg, #2a1c10f7, #1c130bf7);
    border: 2px solid var(--gold-deep);
    border-radius: 14px;
    box-shadow: 0 8px 24px #000b, inset 0 1px 0 #ffffff16;
    padding: 12px 14px;
    color: var(--parchment);
    font-size: 13.5px; line-height: 1.55; font-weight: 700;
    opacity: 0; transform: translateY(-8px) scale(.97);
    transition: opacity .25s ease, transform .25s ease;
    pointer-events: none;
  }
  /* ⚠️ **表示中も枠自体はタップを拾わないこと（pointer-events:none）。**
     この枠は盤の上に重なる（実測 375×812 で**盤の48%**を覆う）。auto にしていたせいで、
     盤を触らせるステップ（陣形）で**駒も置き先のマスも押せなかった**
     （elementFromPoint が枠の中身を返す状態）。押させたいのは「次へ」だけなので、
     ボタンにだけ auto を戻す。こうしておけば、将来ステップを足しても同じ詰み方をしない。 */
  #tutBox.show { opacity: 1; transform: translateY(0) scale(1); }
  /* 盤を操作させるステップは枠を下段へ逃がす（募兵欄の上）。
     ⚠️ 触れないだけでなく**見えない**のも困る。どの駒をどこへ動かすのか、
       盤が全部見えていないと案内そのものが読めない */
  #tutBox.low { top: auto; bottom: 6px; }
  #tutBox .tut-msg b { color: var(--gold); font-weight: 900; }
  #tutBox .tut-next {
    margin-top: 11px; width: 100%;
    font-family: inherit; font-weight: 900; font-size: 13.5px;
    color: #241710; background: linear-gradient(180deg, var(--gold), var(--gold-deep));
    border: none; border-radius: 10px; padding: 9px; cursor: pointer;
    box-shadow: 0 3px 0 #00000066;
    pointer-events: auto;        /* 枠は none。押させるのはこのボタンだけ */
  }
  #tutBox .tut-next:active { transform: translateY(2px); box-shadow: 0 1px 0 #00000066; }
  #tutBox .tut-hint { margin-top: 9px; font-size: 11.5px; font-weight: 700; color: var(--gold); text-align: center; }
  /* ハイライト: 対象を金色パルスで縁取る */
  /* steps(6): box-shadow は合成できない＝値が変わるたび矩形を再ペイントする。
     README「描画コストの原則」の式 n × 区間数 ÷ 秒 = 10 より 10×1.2÷2 = 6 */
  .tut-hi { position: relative; z-index: 41; animation: tutPulse 1.2s steps(6) infinite; border-radius: 12px; }
  @keyframes tutPulse {
    0%,100% { box-shadow: 0 0 0 2px var(--gold), 0 0 8px 2px #e8b33d55; }
    50%     { box-shadow: 0 0 0 3px var(--gold), 0 0 18px 6px #e8b33daa; }
  }
  /* チュートリアル中、案内対象でないカードは灰色化してタップ不可にする */
  .card.tut-off { filter: grayscale(1) brightness(.5); opacity: .5; pointer-events: none; box-shadow: 0 3px 0 #00000055; transition: filter .2s, opacity .2s; }

  .card {
    flex: 1; max-width: 96px; min-width: 0;
    border-radius: 11px;
    background: linear-gradient(180deg, var(--parchment), var(--parchment-dim));
    color: var(--ink);
    border: 2px solid var(--t1);
    box-shadow: 0 3px 0 #000000aa;
    padding: 6px 2px 5px;
    display: flex; flex-direction: column; align-items: center; gap: 2px;
    cursor: pointer;
    transition: transform .12s, opacity .25s, box-shadow .12s, filter .2s;
    position: relative;
  }
  /* フリップ演出は募兵が新しく並んだ時（新ラウンド・リロール）だけ。
     購入や選択での再描画では発火させない */
  #shopCards.deal .card,
  #shopCards.deal .card.dogcard,
  #shopCards.deal .card.nursecard,
  #shopCards.deal .card.akanecard,
  #shopCards.deal .card.viscard { animation: cardflip .4s cubic-bezier(.2,.8,.3,1) backwards; }
  #shopCards.deal .card:nth-child(2) { animation-delay: .06s; }
  #shopCards.deal .card:nth-child(3) { animation-delay: .12s; }
  #shopCards.deal .card:nth-child(4) { animation-delay: .18s; }
  #shopCards.deal .card:nth-child(5) { animation-delay: .24s; } /* 看板娘のいる店（5枠目） */
  @keyframes cardflip { from { transform: rotateY(80deg) translateY(8px); opacity: 0; } }
  .card.tier2 { border-color: var(--t2); }
  .card.tier3 { border-color: var(--t3); box-shadow: 0 3px 0 #000000aa, 0 0 10px #e8b33d44; }

  /* ───── 募兵カードの枠絵（img/units/frame/*.webp・96×120）─────
     カードの地ごと差し替える1枚絵。どの絵を使うかは render.js の cardFrame() が決め、
     CSS変数 --frame で受け取る。

     ■ なぜ background ではなく border-image（9スライス）か
       カードの縦横比は枠数と画面幅で変わる（4枚=0.69 / 5枚=0.58）のに、絵は 0.8。
       background-size:100% 100% で敷くと**角のリベットが最大3割潰れて楕円になる**。
       9スライスなら四隅は原寸のまま置かれ、伸びるのは辺の帯だけ（帯は無地なので分からない）。
     ■ border-width は 0 のまま border-image-width に px を入れている。
       こうすると枠絵は padding 側へ描かれるので、レイアウト（＝中身の幅）を
       枠の太さに縛られずに決められる。中身が枠に乗らないよう padding で逃がすこと。
     ■ 下の background は「絵が落ちたときの地」。border-image の fill が乗ると隠れる */
  /* 帯の太さ(--fw)はカード幅に合わせて変える。原画は幅96pxに対して帯11px＝約12%なので、
     固定12pxのままだと看板娘の5枠×狭い端末（カード66px）で帯が18%まで太り、
     中身の幅が48pxまで削られて「なでてあげよう」等が折り返す */
  .card.framed { --fw: 12px; }
  #shopCards.slim .card.framed { --fw: 10px; }              /* 看板娘の5枠 */
  @media (max-width: 400px) {
    .card.framed { --fw: 11px; }
    #shopCards.slim .card.framed { --fw: 8px; }
  }
  .card.framed {
    border: 0;                        /* border-image-source は render.js がインラインで入れる */
    border-image-slice: 15 fill;      /* 96×120 の外周15px＝帯＋角の飾り */
    border-image-width: var(--fw);
    border-image-repeat: stretch;
    /* padding は border-image-width とほぼ同じにすること。大きく下回ると中身が帯の上に乗る
       （下だけ 9px にしていて系統バッジが木枠に食い込んだ）。
       左右は帯より2px内側まで許す（原画の側面の帯は角より細いため）。
       増えた分は gap を詰めて相殺し、カードの高さを 137px 以下に保つ
       — 実況ログは #shopStage で追従するので跳ねはしないが、酒場が伸びたぶん盤面が削られる */
    padding: var(--fw) calc(var(--fw) - 2px);
    gap: 1px;
    box-shadow: 0 3px 0 #00000088;
    /* ■ 地のグラデーションは content-box までに切ること
       枠絵の外周1〜4px（角はもっと）は透明なので、border-box のまま敷くと
       .card の羊皮紙色(#f1e3c4)が絵の外側にはみ出し、**枠の周りに白い余白**として見える。
       content-box に切れば padding(12/9px)の内側だけになり、必ず絵の下に隠れる。
       絵が落ちたときの受け皿としては残るので、文字が背景に溶けることもない */
    background-clip: content-box;
  }
  /* 枠絵が付いたぶん、地色・縁取りの重ね描きは外す（絵と喧嘩する）。
     ※ background の一括指定は background-clip を border-box に戻してしまうので、
       ここでは longhand で書くこと（白い余白が復活する） */
  .card.framed.tier2, .card.framed.tier3 { box-shadow: 0 3px 0 #00000088; }
  .card.framed.dogcard, .card.framed.nursecard, .card.framed.akanecard, .card.framed.viscard {
    background-image: linear-gradient(180deg, var(--parchment), var(--parchment-dim));
    background-clip: content-box;
    box-shadow: 0 3px 0 #00000088;
  }
  /* 枠絵のぶん中身の幅が減るので、はみ出す文字は詰める。
     ※ .card に min-width:0 が要る: flex項目の既定 min-width:auto は「中身より狭くならない」ので、
       名前が長い1枚（くたびれた老人）だけカードが太り、並びが不揃いになる（実際になった）。
     ※ .card 自体に overflow:hidden は付けないこと — コストバッジと「合体!」は枠の外へ出している */
  .card.framed .cname {
    font-size: 8.8px;
    width: 100%; text-align: center;
    overflow: hidden; text-overflow: ellipsis;
  }
  .card.framed .btag { padding: 1px 5px; max-width: 100%; overflow: hidden; }
  .card.framed .cstats { max-width: 100%; overflow: hidden; }
  .card:active { transform: translateY(2px); box-shadow: 0 1px 0 #000000aa; }
  .card.sold { pointer-events: none; filter: grayscale(1) brightness(.5); transform: scale(.95); }
  .card.sold::after {
    content: '購入済';
    position: absolute; inset: 0; z-index: 2;
    display: flex; align-items: center; justify-content: center;
    font-size: 13px; font-weight: 900; letter-spacing: 3px; color: #fffd;
    background: #000000b0; border-radius: 9px;
  }
  .card.cantafford { filter: grayscale(.75); opacity: .55; }
  /* 募兵カードの肖像枠。丸メダル(34px)だとカード幅 76px の半分も使えず、
     全身96pxの立ち絵を入れても顔が数pxで誰か分からなかった。
     → 角を取った四角にしてカード幅いっぱいまで広げる。横幅が主役なので
     絵の描画サイズは「枠の幅」で決まり、同じ縦位置のまま約1.7倍になる。 */
  .card .cport {
    width: 100%; aspect-ratio: 1.46;
    border-radius: 12px;
    display: flex; align-items: center; justify-content: center;
    font-size: 27px;                                       /* 絵文字(クラシック表示)も枠に合わせて拡大 */
    border: 2.5px solid;
    background: radial-gradient(ellipse at 50% 25%, #fff, #d9c8a3);
    overflow: hidden;          /* 胸像トリミング用。はみ出した下半身を枠で切る */
  }
  /* 立ち絵カードは「胸像」で見せる。
     cover ＝ 枠の広い辺（＝横幅）に合わせて拡大し、あふれた下半身を枠外へ逃がす。
     見えるのは上から 66%（頭〜腰）で、兜・フード・盾など識別要素は残る。
     ※ 高さを固定せず aspect-ratio にしてあるのは、見える割合＝高さ÷幅 だから。
       固定高だと広いカード（枠102px）で 48% まで下がり、顎が切れた。
       比で持てば、4枚並び/5枚並び・折り返しの端カード、どの幅でも同じ胸像になる。
     ※ transform で更に寄せてはいけない。この絵は 96×96 のほぼ全面に描かれており
       （実測 bbox 幅 72〜96px）、拡大すると腕・武器・盾の左右が切れる。cover が上限。
     ※ object-position が top なのは頭頂を切らないため（絵の中身は y=0〜9 から始まる）。
     ※ 来訪者(vis_*)も v1.49.0 で全身立ち絵になり、他と同じ胸像で映すようになった
       （仮絵の丸バッジだった頃だけ bust を外していた） */
  .card .cport.bust .uimg {
    width: 100%; height: 100%;
    object-fit: cover;
    object-position: top center;
  }
  .card .cname { font-size: 9.5px; font-weight: 900; line-height: 1.1; white-space: nowrap; }
  .card .cstats { font-size: 11px; font-weight: 900; display: flex; gap: 5px; }
  .card .cstats .atk { color: var(--war); } .card .cstats .hp { color: var(--tank); }
  .card .btag {
    font-size: 8px; font-weight: 700;
    color: #6b4f23;
    background: #00000012;
    padding: 1px 6px;
    border-radius: 999px;
    white-space: nowrap;
  }
  .card .ctraits { display: flex; gap: 3px; }
  .ctrait { font-size: 8px; font-weight: 700; color: #fff; padding: 1px 6px; border-radius: 999px; box-shadow: 0 1px 0 #00000044; }
  .ctrait.war { background: var(--war); } .ctrait.tank { background: var(--tank); }
  .ctrait.mage { background: var(--mage); } .ctrait.merc { background: var(--merc); }
  .ctrait.pact { background: var(--pact); }
  .card .cost {
    position: absolute; top: -7px; left: -5px;
    background: linear-gradient(180deg, var(--gold), var(--gold-deep));
    color: #3b2800; font-size: 11px; font-weight: 900;
    width: 22px; height: 22px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    border: 2px solid #4a3000; box-shadow: 0 1px 3px #000;
  }
  /* 買えば★が上がるカード。到達する★をバッジに出し、枠そのものも光らせる。
     ★が高いほど強く光らせるのは「★4に届く1枚」を見逃さないため
     （以前は★1同士しか判定しておらず、★4に届くカードも普通の「合体!」でしかなかった）。
     box-shadow のアニメは Tier色の枠(.tier3)やロック(.locked)の影に勝つので、
     持ち上げの影 0 3px 0 は各キーフレームに必ず残すこと（残さないとカードが紙のように潰れる） */
  .card .mergeHint {
    position: absolute; top: -8px; right: -4px;
    background: var(--win); color: #fff; font-size: 9px; font-weight: 900;
    padding: 2px 6px; border-radius: 999px; border: 1px solid #2c5424;
    animation: hintbounce 1s infinite;
    white-space: nowrap;
  }
  .card .mergeHint.up3 { background: linear-gradient(180deg, #f3c556, #d09a1e); border-color: #7a5510; color: #402c00; }
  .card .mergeHint.up4 { background: linear-gradient(90deg, #ffd66e, #ff8fd6, #7ecbff); border-color: #fff; color: #2a1c3a; }
  .card.willmerge.up2 { animation: mergeGlow2 1.3s steps(7) infinite; }
  .card.willmerge.up3 { animation: mergeGlow3 1.1s steps(6) infinite; }
  .card.willmerge.up4 { animation: mergeGlow4 1.6s steps(5) infinite; }
  @keyframes mergeGlow2 {
    0%, 100% { box-shadow: 0 3px 0 #000000aa, 0 0 6px #b9cbe0aa; }
    50%      { box-shadow: 0 3px 0 #000000aa, 0 0 16px 3px #dbe7f5; }
  }
  @keyframes mergeGlow3 {
    0%, 100% { box-shadow: 0 3px 0 #000000aa, 0 0 8px #e8b33daa; }
    50%      { box-shadow: 0 3px 0 #000000aa, 0 0 22px 5px #ffd66e; }
  }
  @keyframes mergeGlow4 {
    0%   { box-shadow: 0 3px 0 #000000aa, 0 0 22px 5px #ffd66e; }
    33%  { box-shadow: 0 3px 0 #000000aa, 0 0 22px 5px #ff8fd6; }
    66%  { box-shadow: 0 3px 0 #000000aa, 0 0 22px 5px #7ecbff; }
    100% { box-shadow: 0 3px 0 #000000aa, 0 0 22px 5px #ffd66e; }
  }
  /* 🔒取り置きバッジ: ロック中のみ、コストバッジと対の位置（右上）に出す。普段は何も表示しない */
  .card .lockMark {
    position: absolute; top: -7px; right: -5px;
    width: 22px; height: 22px; border-radius: 50%;
    background: linear-gradient(180deg, #ffe9b0, #e8c56a);
    border: 2px solid #4a3000; box-shadow: 0 1px 3px #000;
    display: flex; align-items: center; justify-content: center;
    font-size: 11px; line-height: 1;
  }
  .card.locked .mergeHint { top: 15px; }   /* ロックバッジと重なる場合は合体!を下へ */
  /* ロック中カード: Tier色の枠は保ったまま金の縁取り＋淡い光で「取り置き」を示す */
  .card.locked {
    outline: 2px solid var(--gold); outline-offset: 1px;
    box-shadow: 0 3px 0 #000000aa, 0 0 10px #d4a94777;
  }
  /* 取り置きで据え置かれた駒は、新しい並びのフリップ演出をしない（実際は変わっていない） */
  #shopCards.deal .card.nodeal { animation: none; }
  #shopCards.deal .card.herocard.nodeal { animation: s4pulseCard 2.4s steps(8) infinite; }
  /* 🔒モードボタン: コンパクト幅。武装中は金色に灯る */
  #lockModeBtn { flex: 0 0 auto; min-width: 42px; padding-left: 8px; padding-right: 8px; }
  #lockModeBtn.armed {
    background: linear-gradient(180deg, #ffe9b0, #e8c56a); color: #3b2800;
    border-color: var(--gold); box-shadow: 0 0 10px #d4a947aa;
  }
  @keyframes hintbounce { 50% { transform: translateY(-3px); } }
  .card.dogcard {
    border-color: #ff8fb3;
    background: linear-gradient(180deg, #fdeef3, #f3d6e0);
    box-shadow: 0 3px 0 #000000aa, 0 0 10px #ff8fb366;
    animation: dogwiggle 1.6s ease-in-out infinite;
  }
  @keyframes dogwiggle { 0%,100% { transform: rotate(0); } 25% { transform: rotate(-2deg); } 75% { transform: rotate(2deg); } }
  .card.dogcard .cport { background: radial-gradient(ellipse at 50% 25%, #fff, #ffe3ec); }
  .card.nursecard {
    border-color: #4fc47e;
    background: linear-gradient(180deg, #eefbf2, #d7f0e0);
    box-shadow: 0 3px 0 #000000aa, 0 0 10px #4fc47e66;
    animation: nursepulse 1.8s steps(9) infinite;
  }
  @keyframes nursepulse { 50% { box-shadow: 0 3px 0 #000000aa, 0 0 16px #4fc47e99; } }
  .card.nursecard .cport { background: radial-gradient(ellipse at 50% 25%, #fff, #ddf5e6); }
  .card.akanecard {
    border-color: #c9a0ff;
    background: linear-gradient(180deg, #f8f0fe, #e9d9f6);
    box-shadow: 0 3px 0 #000000aa, 0 0 10px #c9a0ff66;
    animation: akanefloat 2s ease-in-out infinite;
  }
  @keyframes akanefloat {
    0%,100% { transform: translateY(0) rotate(0); }
    30% { transform: translateY(-2.5px) rotate(-1.5deg); }
    70% { transform: translateY(-1px) rotate(1.5deg); }
  }
  .card.akanecard .cport { background: radial-gradient(ellipse at 50% 25%, #fff, #f0e2fc); }
  /* 疲れた実力者: 影を落とした暗色カード。項垂れるようにゆっくり揺れる */
  .card.viscard {
    border-color: #3b3f4a;
    background: linear-gradient(180deg, #5b5f6c, #3f434f);
    box-shadow: 0 3px 0 #000000cc, inset 0 0 18px #00000055;
    /* steps(17): filter が合成できないので 60fps だとカード1枚を塗り続ける。
       **これは通常プレイ中（ショップに並ぶ間ずっと）回る**ので3本の中で一番効く。10×3.4÷2 = 17 */
    animation: visdroop 3.4s steps(17) infinite;
  }
  @keyframes visdroop {
    0%,100% { transform: translateY(0); filter: brightness(1); }
    55% { transform: translateY(1.6px); filter: brightness(.92); }
  }
  /* 肖像枠の地。仮絵（丸バッジ）の頃は暗いほうが雰囲気が出たが、全身立ち絵に変えたら
     「暗い絵を暗い地に置く」形になって顔が沈んだ。地を上げ、灰色化も浅くする。
     ※ 立ち絵は cover で枠を埋めるので、地が見えるのは絵の透明部分（肩の外・足元）だけ */
  .card.viscard .cport { background: radial-gradient(ellipse at 50% 25%, #ccd1db, #8b93a3); filter: grayscale(.15); }
  /* 文字色は「羊皮紙の地」に合わせる。
     枠絵を入れた版で .card.framed.viscard が地を羊皮紙に戻したのに、ここだけ
     暗色カード時代の白文字(#e2e5ec)が残っていて、明るい地に白＝ほぼ読めなかった */
  .card.viscard .cname { color: #33384a; }
  .card.viscard .btag { color: #5b6272; }
  .card.viscard .cstats { color: #5b6272 !important; }   /* インラインの #8a93a6 を上書き（羊皮紙では薄すぎる） */
  .card.viscard .cost { background: #3b3f4a; color: #cfd4de; }
  #panel .visBtn {
    flex: 1; line-height: 1.4; white-space: normal; padding: 12px 6px; font-size: 13px; font-weight: 900;
    background: linear-gradient(180deg, #4c5160, #333741); color: #dfe3ea;
    border: 2px solid #6b7280; box-shadow: 0 0 10px #00000077, inset 0 1px 0 #ffffff10;
  }
  #panel .visBtn.treat { border-color: var(--gold); color: #ffe9b8; }
  /* 「追い払う」は打ち消し系なので、他のキャンセルボタンと同じ落ち着いた色にする
     （#panel .btn より詳細度を高くしないと金色に上書きされる） */
  #panel .visBtn.shoo { background: linear-gradient(180deg, #a98b5a, #8a6b3a); color: #fff; border-color: #6b5a3e; }
  #panel .visBtn:disabled { opacity: .45; }
  #panel .visBtn .vsub { font-size: 10.5px; font-weight: 700; opacity: .8; }
  /* 勇者カード。枠絵付き(.framed)の白フチ対策が2つ要る（他のカードと同じ罠を2回踏む場所）:
     ① background の一括指定は background-clip を border-box に戻す。地色(#fff8e8=ほぼ白)が
        枠絵の透明な外周1〜4pxからはみ出し、枠の周りが白く光って見える → longhand + content-box
     ② 円メダル用の s4pulse は inset 0 0 6px #fff を打つ。inset 影は背景の上・borderの下に
        描かれるので、これも枠絵の透明部分から白く漏れる。さらに box-shadow ごと差し替わるため
        .card の 0 3px 0（カードの厚み）まで消えて、勇者だけ板が薄く見えていた
        → カード用に inset 無し・厚み入りの s4pulseCard を用意する */
  .card.herocard {
    border-color: #fff;
    background-image: linear-gradient(180deg, #fff8e8, #ffe9c4);
    background-clip: content-box;
    animation: cardflip .4s cubic-bezier(.2,.8,.3,1) backwards, s4pulseCard 2.4s steps(8) infinite;
  }
  @keyframes s4pulseCard {
    0%   { box-shadow: 0 3px 0 #00000088, 0 0 13px var(--gold); }
    33%  { box-shadow: 0 3px 0 #00000088, 0 0 13px #ff7eb5; }
    66%  { box-shadow: 0 3px 0 #00000088, 0 0 13px #7ecbff; }
    100% { box-shadow: 0 3px 0 #00000088, 0 0 13px var(--gold); }
  }
  .card.herocard .cport { background: radial-gradient(ellipse at 50% 25%, #fff, #ffe9c4); }

  #shopBtns { display: flex; gap: 6px; }
  .btn {
    flex: 1;
    font-family: inherit; font-weight: 900; font-size: 13px;
    border: none; border-radius: 10px; padding: 10px 0;
    cursor: pointer; color: #fff;
    box-shadow: 0 3px 0 #00000088, inset 0 1px 0 #ffffff33;
    transition: transform .1s, box-shadow .1s, filter .2s;
  }
  .btn:active { transform: translateY(2px); box-shadow: 0 1px 0 #00000088; }
  .btn:disabled { filter: grayscale(.8); opacity: .5; pointer-events: none; }
  #rerollBtn { background: linear-gradient(180deg, #6b7fa3, #50617f); max-width: 76px; font-size: 12px; }
  #capBtn { background: linear-gradient(180deg, #5d9c6c, #41744e); max-width: 104px; font-size: 12px; }
  #fightBtn { background: linear-gradient(180deg, #d4583a, #9c3a26); letter-spacing: .08em; }
  /* 出撃ボタンは準備フェーズのあいだ常に画面に出ている＝この明滅は消えない負荷になる。
     box-shadow は合成できないので steps(12) で 24回/2.4s = 10fps に落とす（理由は s3glow 参照） */
  #fightBtn:not(:disabled):not(.speed) { animation: fightglow 2.4s steps(12) infinite; }
  #fightBtn.speed { background: linear-gradient(180deg, #8a63b8, #6a4a92); animation: none; }
  @keyframes fightglow { 50% { box-shadow: 0 3px 0 #00000088, 0 0 14px #d4583a88, inset 0 1px 0 #ffffff33; } }
  #sellBtn { background: linear-gradient(180deg, #8a6b3a, #6b5230); max-width: 116px; font-size: 12px; display: none; }

  /* ───── fx ───── */
  #fx { position: fixed; inset: 0; pointer-events: none; z-index: 45; overflow: hidden; }
  /* 攻撃の瞬間に振られる武器（fx.js weaponFx）。幅・高さは絵の実寸のまま使い、
     倍率は transform の scale で掛ける（切り出しごとに縦横比が違うため） */
  .wepfx {
    position: absolute;
    image-rendering: pixelated;
    pointer-events: none;
    z-index: 46;
    transform-origin: 50% 100%;   /* 柄の握り＝絵の下端中央を支点に振る */
    filter: drop-shadow(0 1px 2px #000000aa);
  }
  .pcl { position: absolute; border-radius: 50%; pointer-events: none; }
  /* 絵文字🪙はOS(特にiPhone)によって銀色寄りに描画されるため、金色を保証するCSS描画コインに置換 */
  .flycoin {
    position: absolute; width: 15px; height: 15px;
    border-radius: 50%; pointer-events: none;
    background: radial-gradient(circle at 35% 28%, #fff2c2 0%, #f6d275 32%, #e8b33d 62%, #b07f1e 100%);
    border: 1px solid #8a5f14;
    box-shadow: 0 1px 2px #00000088, inset 0 1px 1px #ffffffaa;
  }
  .flycard { position: absolute; pointer-events: none; z-index: 46; }
  .dmg {
    position: absolute; font-weight: 900; font-size: 19px;
    pointer-events: none;
    text-shadow: 0 0 4px #000, 0 2px 2px #000;
    z-index: 47;
  }
  .spark {
    position: absolute; width: 48px; height: 48px; border-radius: 50%;
    pointer-events: none;
    background: radial-gradient(circle, #fff 0%, #ffd66e 30%, transparent 70%);
    z-index: 46;
  }
  .slash {
    position: absolute; width: 58px; height: 4px; border-radius: 2px;
    background: linear-gradient(90deg, transparent, #fff, transparent);
    pointer-events: none; z-index: 46;
  }
  .arrowfx {
    position: absolute; width: 22px; height: 3px; border-radius: 2px;
    background: linear-gradient(90deg, transparent, #fff);
    pointer-events: none; z-index: 46;
    box-shadow: 0 0 5px #fff8;
  }

  /* ───── ★アップの見せ場（fx.js starUpFx）─────
     #fx の子＝画面座標。盤の中央に出す（画面中央だと酒場側に落ちるため）。
     操作は止めないので pointer-events:none。色は JS が --su（本体）/--sug（光）に流し込む */
  .starup {
    position: absolute; z-index: 47;
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    pointer-events: none;
    filter: drop-shadow(0 6px 16px #000a);
  }
  /* ★3以上の放射光。conic の縞をリングマスクで抜き、中心（＝駒の絵）は透かす */
  .starup .su-rays {
    position: absolute; left: 50%; top: 42%;
    width: 210px; height: 210px;
    border-radius: 50%;
    background: repeating-conic-gradient(var(--sug) 0deg 5deg, transparent 5deg 26deg);
    -webkit-mask: radial-gradient(closest-side, transparent 20%, #000 44%, transparent 100%);
            mask: radial-gradient(closest-side, transparent 20%, #000 44%, transparent 100%);
    pointer-events: none;
  }
  .starup .su-port {
    display: flex; align-items: center; justify-content: center;
    border-radius: 16px;
    background: radial-gradient(circle at 45% 34%, #ffffff55, transparent 68%);
    filter: drop-shadow(0 0 14px var(--sug));
  }
  .starup .su-port .uimg { width: 100%; height: 100%; }
  .starup .su-stars { display: flex; gap: 1px; line-height: 1; }
  .starup .su-stars i {
    font-style: normal; font-size: 23px; color: var(--su);
    text-shadow: 0 0 12px var(--sug), 0 2px 2px #000;
  }
  .starup .su-name {
    font-size: 13px; font-weight: 900; letter-spacing: .04em;
    color: var(--su); text-shadow: 0 0 9px var(--sug), 0 1px 2px #000;
  }
  /* ★4だけの画面フラッシュ。#fx が fixed/inset:0 なので inset:0 で画面全面になる。
     ※ opacity:0 を必ず持たせること。素の状態が opacity:1 だと、アニメが終わってから
       onfinish で消えるまでの1フレーム、画面が真っ白に塗り潰される */
  .suflash { position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none; }

  #slam {
    position: fixed; inset: 0;
    display: none; align-items: center; justify-content: center;
    z-index: 48; pointer-events: none;
  }
  #slam.show { display: flex; }
  #slamText {
    font-size: 58px; font-weight: 900; letter-spacing: .1em;
    text-shadow: 0 0 24px #000, 0 4px 0 #000;
    transform: rotate(-6deg);
  }

  /* ワンマンアーミー: 継承した将軍のオーラと継承時フラッシュ */
  .tok.inh .tport { animation: inhglow 1.8s steps(9) infinite; }   /* 18回/1.8s = 10fps（理由は s3glow 参照） */
  @keyframes inhglow {
    0%, 100% { filter: drop-shadow(0 0 2px #ffd66e) drop-shadow(0 1px 2px #000); }
    50%      { filter: drop-shadow(0 0 9px #ffce4f) drop-shadow(0 0 16px #ffb52999) drop-shadow(0 1px 2px #000); }
  }
  .tok.inhflash .tport { animation: inhflash .55s ease-out; }
  @keyframes inhflash {
    0%   { transform: scale(1);    filter: drop-shadow(0 0 0 #ffd66e); }
    35%  { transform: scale(1.45); filter: drop-shadow(0 0 14px #ffd66e) drop-shadow(0 0 22px #ffce4f); }
    100% { transform: scale(1);    filter: drop-shadow(0 0 2px #ffd66e); }
  }
  .dexbody .evrec { display: block; color: #8a6b3a; font-weight: 800; margin-top: 1px; }
  .card .cost.discount { background: linear-gradient(180deg, #ffd66e, #e8a93c); color: #3b2800; }
  #evChip { background: #2b2019; border: 1px solid #4a3826; border-radius: 8px; font-size: 14px;
            padding: 2px 6px; line-height: 1.2; cursor: pointer; }
  /* 戦況ゲージ: 両軍の総体力の割合。左が自軍、右が敵軍 */
  #warGauge {
    position: relative; height: 11px; margin: 2px 6px 3px; border-radius: 6px; overflow: hidden;
    background: linear-gradient(180deg, #8a3038, #5c1f24);
    border: 1px solid #00000066; box-shadow: inset 0 1px 2px #0006;
  }
  #warGauge.off { display: none; }
  #wgFill {
    position: absolute; inset: 0; width: 50%;
    background: linear-gradient(180deg, #58d68d, #2e9e5b);
    border-right: 2px solid #fff9;
    transition: width .25s ease;
  }
  /* 開戦の点呼のあいだだけ出る「陣形なしの位置」の印。
     ⚠️ **これが無いと、点呼でゲージが動いたことに気づけない。** ゲージは割合なので、
       実測で 7コンボ＝味方戦力 +6.3% でも表示は 35%→36% の**1ptしか動かない**
       （459→488 / 敵852。design/formation-spec.md §4-5）。
       印を置いて「線を追い越した」形にすると、1ptでも伸びが読める。 */
  #wgBase {
    position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; z-index: 2;
    background: #ffe08a; box-shadow: 0 0 4px #ffcf5a;
    opacity: 0; transition: opacity .2s; pointer-events: none;
  }
  #warGauge.callout #wgBase { opacity: .95; }
  #wgAllyPct, #wgFoePct {
    position: absolute; top: 0; font-size: 8px; line-height: 11px; font-weight: 800;
    text-shadow: 0 1px 1px #000; pointer-events: none;
  }
  #wgAllyPct { left: 4px; color: #eafff0; }
  #wgFoePct  { right: 4px; color: #ffecec; }
  /* ワンマンアーミー: 覚醒将軍（2回行動）の強オーラと×2バッジ */
  .tok.awk .tport { animation: awkglow 1.2s steps(6) infinite; }   /* 12回/1.2s = 10fps（理由は s3glow 参照） */
  @keyframes awkglow {
    0%, 100% { filter: drop-shadow(0 0 3px #ffd66e) drop-shadow(0 0 6px #ff7b4f88) drop-shadow(0 1px 2px #000); }
    50%      { filter: drop-shadow(0 0 10px #ffce4f) drop-shadow(0 0 20px #ff7b4f) drop-shadow(0 1px 2px #000); }
  }
  .tok .tdbl {
    position: absolute; top: -7px; right: -10px; z-index: 3;
    font-size: 9px; font-weight: 900; color: #3b2800;
    background: linear-gradient(180deg, #ffd66e, #e8a93c);
    border: 1px solid #b8801f; border-radius: 7px; padding: 0 4px;
    box-shadow: 0 1px 2px #0008;
  }
  /* 霧マップ: 盤面に薄い霧のオーバーレイ */
  #board.fog::after {
    content: '';
    position: absolute; inset: 0; z-index: 4; pointer-events: none;
    background:
      radial-gradient(ellipse 80% 40% at 30% 25%, #cfd4de22 0%, transparent 70%),
      radial-gradient(ellipse 90% 45% at 70% 60%, #cfd4de1e 0%, transparent 70%),
      linear-gradient(180deg, #aab2c218 0%, #aab2c20d 100%);
    animation: fogdrift 9s ease-in-out infinite alternate;
  }
  @keyframes fogdrift { from { opacity: .7; } to { opacity: 1; } }
  /* 山岳地帯: 岩山（移動不可マス）。セル全体を岩肌の色に */
  .rock {
    position: absolute; z-index: 1; pointer-events: none;
    display: flex; align-items: center; justify-content: center;
    background:
      radial-gradient(circle at 35% 30%, #7d6a4e66 0%, transparent 60%),
      linear-gradient(180deg, #4e4234ee, #383026ee);
    border-radius: 5px;
    box-shadow: inset 0 1px 3px #0009, inset 0 -1px 2px #00000066;
    font-size: 22px;
    filter: drop-shadow(0 1px 1px #0008);
  }
  /* チュートリアル: 置き先のマス（fx.js の tutCellFx）。
     文章で位置を説明する代わりに、置くべきマスそのものを光らせる */
  .tutcell {
    position: absolute; z-index: 2; pointer-events: none;
    border-radius: 6px;
    box-sizing: border-box;
    border: 2px dashed var(--gold);
    background: #e8b33d26;
    /* steps(6): 10×1.1÷2 = 5.5 → 6（≒11fps）。box-shadow は合成できないので素の 60fps だと塗り続ける */
    animation: tutCellPulse 1.1s steps(6) infinite;
  }
  @keyframes tutCellPulse {
    0%, 100% { opacity: .55; box-shadow: inset 0 0 8px #e8b33d55; }
    50%      { opacity: 1;   box-shadow: inset 0 0 18px #e8b33daa; }
  }
  /* ターゲット予測ライン（配置フェーズ） */
  #predictLayer {
    position: absolute; inset: 0; width: 100%; height: 100%;
    pointer-events: none;   /* z-index は上の「盤の層」でまとめて決めている */
  }
  #predictHeads {
    position: absolute; inset: 0; width: 100%; height: 100%;
    pointer-events: none;   /* z-index は上の「盤の層」でまとめて決めている */
  }
  #predictLayer line {
    stroke-dasharray: .28 .18;
    vector-effect: non-scaling-stroke;
    stroke-width: 1.6px;
    /* ── なぜ linear ではなく steps なのか（発熱対策・実測して入れた） ──
       stroke-dashoffset は GPU で合成できない。値が動くたびに線の矩形が丸ごと
       再ペイントされる。線は盤を斜めに横切るので1本あたりの矩形が大きく、
       19本の合計は実測 465,799px＝画面の約半分。これを 60fps で塗り直していた
       （待機中に何もしていないのにスマホが熱くなる主因）。
       steps(12) にすると値が変わるのは 1.2秒に12回＝10fps。見た目は「マーチング
       アント」の定番どおりで流れて見えるまま、再ペイントは約1/6になる。
       ※ dashoffset -.92 は dasharray(.28+.18=.46) のちょうど2周期。整数周期なので
         jump-end（steps の既定）でも切れ目が見えない。周期を変えるなら
         「-.92 が .46 の整数倍」を保つこと */
    animation: predFlow 1.2s steps(12) infinite;
  }
  @keyframes predFlow { to { stroke-dashoffset: -.92; } }
  #predictLayer .pA { stroke: #ffd66ecc; }
  #predictLayer .pE { stroke: #ff6b70aa; }
  #predictLayer .pC { stroke: #7fd8ffcc; } /* シーフの宝箱狙い */
  #predictHeads polygon.pA { fill: #ffd66eee; }
  #predictHeads polygon.pE { fill: #ff6b70dd; }
  #predictHeads polygon.pC { fill: #7fd8ffee; }
  /* 陣形の線（試作）。色は fx.js が stroke/fill に直接入れる（従士=水色・双璧=金・縦陣=系統色）。
     ── なぜ盤の中ではなく画面座標なのか（実際に踏んだ） ──
     #board は preserve-3d で、駒は counter-transform で立っているビルボード。
     平らな層を盤の中に入れると、**z-index をいくら上げても立っている駒の後ろに隠れる**
     （3D文脈では奥行きが z-index より優先される）。translateZ で持ち上げると今度はパースで
     視差が出て線が駒からズレる。画面座標で「見えている足元」同士を結ぶのが確実で位置も合う。
     ※ 位置は fx.js の formLineFx が getBoundingClientRect から毎回計算して入れる */
  #formLayer {
    position: fixed; inset: 0; width: 100%; height: 100%;
    pointer-events: none;
    z-index: 44;                 /* #fx(45) のすぐ下・盤より上 */
    /* この層は全画面。中の線が1px でも変わるとフィルタが全画面に掛け直される。
       下の line を steps() にして更新回数を落としてあるので、その回数ぶんしか走らない */
    filter: drop-shadow(0 1px 2px #000c);
  }
  #formLayer line {
    stroke-width: 2.5px;
    stroke-linecap: round;
    stroke-dasharray: 5 4;       /* 駒の上を通るので、塗り潰さず「糸」に見せる */
    opacity: .85;
    /* steps の理由は #predictLayer line と同じ（合成できない stroke-dashoffset の
       再ペイントを 60fps → 10fps に落とす）。-18 は dasharray(5+4=9) の2周期 */
    animation: formFlow 1.6s steps(16) infinite;
  }
  @keyframes formFlow { to { stroke-dashoffset: -18; } }
  /* ═══ 盤が止まっている間は静かになる（発熱対策 ＋ 見た目） ═══
     上の2つは steps() で 10fps まで落としてあるが、それでも「考えている数十秒」の
     あいだ回り続ける。盤が動いていないなら流れる必要はない。

     ただし**いきなり止めてはいけない**（最初そうして「不自然」と却下された）。
     流れていた線がピタリと固まると、壊れたように見える。2段階にする:

       html.hush  … 静かなほうへフェード（transition。流れたまま薄くなる）
       html.still … フェードが終わってから流れを止める（ここで再ペイントがゼロになる）

     時間は render.js の FLOW_HOLD_MS / FLOW_FADE_MS。**下の transition と必ず揃えること。**
     流れの止め方は animation: none ではなく play-state: paused。none だと
     stroke-dashoffset が 0 に戻り、破線が一瞬ガクッと飛ぶ。

     消え方を左右で変えてある:
       予測ライン … 薄くするだけ。**誰が誰を狙うかは「情報」**なので消してはいけない
       陣形の線   … 完全に消す。成立数は ⚑ コンボ表示が受け持つので、盤の上は空ける

     ※ 速度は出入りで変える。**戻るときは速く**（駒を動かした手応えなので待たされたくない）、
       **静まるときはゆっくり**（消えたことに気づかせない）。
       transition は「移った先の状態」の指定が使われるので、
       .hush 側に遅いのを、素の側に速いのを書けばそのとおりに効く */
  #predictLayer line, #predictHeads polygon,
  #formLayer line, #formLayer circle { transition: opacity .22s ease-out; }
  html.hush #predictLayer line,
  html.hush #predictHeads polygon { opacity: .42; transition: opacity 2s ease; }
  html.hush #formLayer line,
  html.hush #formLayer circle { opacity: 0; transition: opacity 2s ease; }
  html.still #predictLayer line,
  html.still #formLayer line { animation-play-state: paused; }
  @media (prefers-reduced-motion: reduce) {
    #predictLayer line, #formLayer line { animation: none; }
    #predictLayer line, #predictHeads polygon,
    #formLayer line, #formLayer circle { transition: none; }
  }
  #formLayer circle { opacity: .95; }
  /* 陣形名のポップ（#fx の中＝画面座標） */
  .formname {
    position: absolute; z-index: 47; pointer-events: none;
    font-weight: 800; font-size: 19px; white-space: nowrap;
    /* 盤の絵の上に出るので、縁取りを厚くしないと読めない（地形も駒も色が濃い） */
    text-shadow: 0 0 3px #000, 0 0 3px #000, 0 2px 4px #000, 0 0 12px #000a;
  }
  .formname.big { font-size: 25px; }   /* 開戦の点呼: 読ませたいので一段大きく */
  /* 点呼の締めの「Nコンボ！」。最後の陣形名が消えてから単独で出す（fx.js formTotalFx） */
  .formname.big.total {
    font-size: 36px; color: var(--gold);
    text-shadow: 0 0 4px #000, 0 0 4px #000, 0 2px 6px #000, 0 0 22px #d4a94799;
  }
  /* 相手軍団ぶんの締め（闘技場）。**自分の結果と取り違えさせない**のが仕事なので、
     金ではなく敵色・一回り小さく・文言も「相手 N コンボ」に変えてある */
  .formname.big.total.foe {
    font-size: 29px; color: #ff9b7a;
    text-shadow: 0 0 4px #000, 0 0 4px #000, 0 2px 6px #000, 0 0 20px #ff6a3d80;
  }
  /* ヘルプのタブ（基本／陣形／図鑑） */
  .htabs { display: flex; gap: 4px; margin: 2px 0 6px; }
  .htabs .htab {
    flex: 1; padding: 6px 4px; font: inherit; font-size: 12px; font-weight: 700;
    color: #6b4f23; background: #0000001a; border: 1px solid #00000026;
    border-radius: 8px 8px 0 0; cursor: pointer;
  }
  /* 選択中は「紙が手前にせり出している」ように見せる。前は背景の差だけで、
     どのタブを見ているのか分からないという指摘があった */
  .htabs .htab.on {
    color: #2c1c08; background: #ffffff8c;
    border-color: #00000059; border-bottom-color: transparent;
    box-shadow: inset 0 3px 0 #c08a3e, 0 -1px 3px #00000026;
  }
  /* 試作中の但し書き（陣形タブの先頭） */
  .formnote {
    background: #00000014; border-left: 3px solid #c08a3e;
    padding: 6px 8px; border-radius: 4px; margin: 0 0 8px;
  }
  /* 陣形の配置例（ヘルプ）。盤は3D変形しているので流用できないため、素直な2Dグリッドで描く。
     中身は本物のユニット画像（unitIcon）なので、絵を差し替えると例図も一緒に変わる */
  .fex { display: flex; align-items: center; gap: 6px; margin: 6px 0 2px; }
  .fex-side { font-size: 9px; line-height: 1.15; color: #6b4f23; text-align: center; flex: none; }
  .fex-grid {
    --fex: 34px;
    display: grid; gap: 2px; padding: 4px;
    background: #5d4a2e; border: 2px solid #43341e; border-radius: 6px;
  }
  .fex-cell {
    width: var(--fex); height: var(--fex); position: relative;
    display: flex; align-items: flex-end; justify-content: center;
    background: #7d8b57; border-radius: 3px; font-size: 20px; line-height: 1;
  }
  .fex-cell.on { background: #94a465; box-shadow: inset 0 0 0 2px #ffd66ec0; }
  .fex-u { width: 30px; height: 33px; object-fit: contain; image-rendering: pixelated; margin-bottom: -3px; }
  .fex-st {
    position: absolute; right: 1px; top: -2px; font-size: 8px; font-style: normal;
    color: #ffe9a8; text-shadow: 0 1px 2px #000, 0 0 3px #000;
  }
  .fex-cap { font-size: 11px; line-height: 1.45; color: #4a3a22; margin: 0 0 10px 2px; }
  /* ヘルプ⚑陣形タブの一覧行（陣形名 → 条件）。13種あるので、図を出さないものはこの1行形式でさばく。
     名前の欄は固定幅にせず max-content で詰める（「〇ギルド / 〇大ギルド」のような長い名前が折れないように） */
  .frow {
    display: grid; grid-template-columns: minmax(84px, max-content) 1fr; gap: 4px 8px;
    font-size: 11px; line-height: 1.45; color: #4a3a22;
    padding: 3px 4px; border-bottom: 1px solid #00000014;
  }
  .frow b { color: #6b4f23; }
  /* ヘルプ基本タブの折りたたみ。「いま要らないもの」を閉じておく器。
     ⚠️ <details> は素の状態だと見出しに見えないので、必ず枠と▶を付けること */
  #panel .hfold { margin: 10px 0 0; border: 1px solid #00000018; border-radius: 10px; background: #00000008; }
  #panel .hfold > summary {
    cursor: pointer; list-style: none; padding: 9px 11px;
    font-size: 13px; font-weight: 900; color: var(--gold-deep);
  }
  #panel .hfold > summary::-webkit-details-marker { display: none; }
  #panel .hfold > summary::before { content: '▶ '; font-size: 10px; }
  #panel .hfold[open] > summary::before { content: '▼ '; }
  #panel .hfold > summary + * { margin-top: 0; }
  #panel .hfold .frow:last-child { border-bottom: none; }
  #panel .hfold .frow { padding-left: 11px; padding-right: 11px; }
  /* 節の頭の1〜2行だけ地の文で置く導入。長くしないこと（表へ回す） */
  #panel .hlead { font-size: 12px; line-height: 1.7; margin: 2px 0 6px; }
  /* まだ組んで戦っていない陣形の名前（ui.js の fname）。
     読めないのではなく「これから開く枠」に見せたいので、伏せ字も名前と同じ大きさ・位置に置き、
     色だけ薄くして点線で囲む。※ 名前の欄は max-content なので、幅は中身なりに詰む */
  .fhide {
    color: #8a7550; font-weight: 700; letter-spacing: 1px;
    border: 1px dashed #8a755066; border-radius: 4px; padding: 0 4px;
    background: #00000008;
  }
  /* 効果のチップ。**条件（何を並べるか）と効果（何が伸びるか）を見た目で分ける**のが目的。
     一覧が13種あるので、地の文に混ぜると条件と効果が溶けて読めなくなる */
  .ffx {
    display: inline-block; font-size: 10px; font-weight: 800; white-space: nowrap;
    background: #6b4f2318; border: 1px solid #6b4f2333; color: #4a3a22;
    padding: 0 5px; border-radius: 999px; margin-left: 4px;
  }
  .ffx.big { background: #c08a3e28; border-color: #c08a3e66; }
  .fex-cap .ffx { margin-left: 0; margin-top: 2px; }
  /* 神話魔法使い: 燃える地面 */
  /* シナジー発動の光輪。駒(.tok)の子として絵の「後ろ」(z-index:-1)に出るので視認性を落とさない。
     左右にずらした2つの放射グラデーションを重ねることで、2系統持ちの駒を2色に染め分ける
     （--c1/--c2 と、その「同色ゼロアルファ」--c1o/--c2o は fx.js の synLitFx が入れる。
       1系統なら同じ色が入るので、少し横長の単色ハローになる）。
     ※ mask で丸く抜く実装は不可。z-index:-1 と mask を併用すると Chrome で
       まったく描画されなくなる（.tsh は見えているのに光輪だけ消える形で踏んだ）。
     ※ transparent へ向けて消さないこと。rgba(0,0,0,0) なので途中でグレーに濁る */
  .tok .synlit {
    position: absolute; left: 50%; top: 50%; z-index: -1;
    border-radius: 50%; pointer-events: none;
    background:
      radial-gradient(closest-side at 32% 50%, var(--c1) 0%, var(--c1o) 76%),
      radial-gradient(closest-side at 68% 50%, var(--c2) 0%, var(--c2o) 76%);
  }

  /* 範囲攻撃の着弾域（3×3）。#lyDecalLo（地面の層）の子なので盤の3D変形に乗り、
     地面に寝た魔法陣として出る。色は fx.js が --aoe に流し込む（術=紫 / 神話魔法=燃焼色）。
     駒より前に出ないことは層が保証する（以前は z:1 と駒の行番号が競合していた） */
  .aoecell {
    position: absolute; pointer-events: none;
    border-radius: 10px;
    border: 2px solid var(--aoe);
    background: radial-gradient(ellipse at 50% 50%, var(--aoe) 0%, var(--aoe0, transparent) 68%);
    box-shadow: 0 0 12px var(--aoe), inset 0 0 16px var(--aoe);
  }
  /* 近接の薙ぎ払いの軌跡。conic-gradient で「先端が明るい弧」を作り、
     mask でドーナツ状に抜いて刃の通り道だけを残す（塗り潰しの扇にしない）。
     mask 未対応の環境では扇形になるだけで、演出として破綻はしない。
     置き場は #lyDecalHi（駒より手前の層）。地面に寝る .aoecell と役割が違い、
     こちらは「振り抜いた刃」なのでキャラの手前を通ってよい。
     ※ かつては #board 直下の z-index:4 だった。駒の z は行番号(1〜ROWS)なので、
       自陣（下4行＝6〜9）では必ず駒に負けて軌跡が丸ごと隠れていた */
  .sweepfx {
    position: absolute; pointer-events: none;
    border-radius: 50%;
    /* --aoe0 は「同じ色の透明」（fx.js が hex+00 を入れる）。素の transparent へ補間すると
       黒を経由してグレーに濁るため、必ず同色ゼロアルファへ向かって消すこと */
    background: conic-gradient(from 0deg,
      var(--aoe0, transparent) 0deg, var(--aoe0, transparent) 190deg,
      var(--aoe) 332deg, #ffffff 353deg, var(--aoe0, transparent) 360deg);
    -webkit-mask: radial-gradient(closest-side, transparent 46%, #000 56%, #000 94%, transparent 100%);
            mask: radial-gradient(closest-side, transparent 46%, #000 56%, #000 94%, transparent 100%);
    filter: drop-shadow(0 0 6px var(--aoe));
  }
  /* 遠隔の範囲攻撃の着弾衝撃波。着弾マスを中心に、3×3（＝ダメージ範囲）の縁まで広がって消える。
     輪は border ではなく radial-gradient の帯: border だと scale で線まで太り、
     小さいうちは 1px を割って消えてしまう。帯なら縮尺に関係なく輪として見える。
     .aoecell と同じ #lyDecalLo ＝ 地面の層（駒より前には出ない）。
     --aoe0 は「同じ色の透明」。素の transparent へ補間するとグレーに濁る（fx.js の aoeFade） */
  .shockfx {
    position: absolute; pointer-events: none;
    border-radius: 50%;
    /* 塗りを mask でドーナツに抜く（.sweepfx と同じ手口）。gradient の帯だけだと
       ぼやけた円板になり「輪が走った」ように見えなかった。外へ行くほど白く熱い */
    background: radial-gradient(closest-side, var(--aoe) 40%, #ffffff 100%);
    -webkit-mask: radial-gradient(closest-side, transparent 54%, #000 74%, #000 90%, transparent 100%);
            mask: radial-gradient(closest-side, transparent 54%, #000 74%, #000 90%, transparent 100%);
    filter: drop-shadow(0 0 9px var(--aoe));
  }
  .burncell {
    position: absolute; pointer-events: none;   /* 置き場は #lyDecalLo（地面の層） */
    background: radial-gradient(circle at 50% 55%, #b06cff55 0%, #8a4fe044 45%, #6a3ab810 78%, transparent 100%);
    box-shadow: inset 0 0 10px #b06cff66;
    border-radius: 6px;
    animation: burnpulse 1.1s ease-in-out infinite;
  }
  @keyframes burnpulse {
    0%, 100% { opacity: .55; }
    50%      { opacity: .95; }
  }

  #overlay {
    position: fixed; inset: 0;
    background: #160d07d9;
    display: none; align-items: center; justify-content: center;
    z-index: 50;
    backdrop-filter: blur(3px);
  }
  #overlay.show { display: flex; }
  #panel {
    width: min(86vw, 340px);
    background: linear-gradient(180deg, var(--parchment), var(--parchment-dim));
    color: var(--ink);
    border-radius: 18px;
    padding: 20px 18px 16px;
    text-align: center;
    box-shadow: 0 14px 50px #000;
    border: 3px solid var(--gold-deep);
    animation: panelin .35s cubic-bezier(.34,1.56,.64,1);
    /* 安全網: 中身が画面を超えてもパネル内でスクロールできるようにする。
       #overlay は overflow:visible の中央寄せなので、これが無いと画面外へ出たボタンに
       一切届かない＝進行不能になる。
       通常の高さでは max-height に当たらず、従来と何も変わらない */
    max-height: calc(100dvh - 20px);
    overflow-y: auto; overscroll-behavior: contain;
  }
  @keyframes panelin { from { transform: scale(.7) translateY(20px); opacity: 0; } }
  /* ── 旅立ちイベント選択 ── */
  .evChoices { display: flex; flex-direction: column; gap: 7px; margin-bottom: 10px; }
  .evopt {
    display: flex; align-items: center; gap: 10px;
    text-align: left; width: 100%;
    padding: 8px 10px;
  }
  .evopt .evic { font-size: 24px; flex: 0 0 auto; filter: drop-shadow(0 1px 1px #00000055); }
  .evopt .evtx { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
  .evopt .evtx b { font-size: 13px; }
  .evopt .evtx small { font-size: 10px; font-weight: 700; color: #5d4322; line-height: 1.35; white-space: normal; }
  #evReroll { font-size: 12px; padding: 7px 12px; }
  #evReroll:disabled { opacity: .45; }
  /* ── 実績ページ ── */
  .recstats {
    display: flex; flex-direction: column; gap: 2px;
    font-size: 12px; font-weight: 700; color: #5d4322;
    margin-bottom: 9px;
  }
  .recstats b { color: var(--ink); font-size: 13.5px; }
  .dexwrap {
    max-height: 44vh; overflow-y: auto;
    text-align: left;
    background: #00000010;
    border-radius: 10px;
    padding: 7px 8px;
    margin-bottom: 11px;
  }
  .dexhead { font-size: 11px; font-weight: 900; color: #8a6b3a; margin: 6px 0 4px; }
  .dexhead:first-child { margin-top: 0; }
  .dexrow { display: flex; align-items: center; gap: 8px; padding: 4px 2px; border-bottom: 1px dashed #00000018; }
  .dexrow:last-child { border-bottom: none; }
  .dexrow .dexic { font-size: 18px; flex: 0 0 22px; text-align: center; }
  .dexrow .dexbody { display: flex; flex-direction: column; min-width: 0; }
  .dexrow .dexbody b { font-size: 11.5px; }
  .dexrow .dexbody small { font-size: 9.5px; font-weight: 700; color: #6b4f23; line-height: 1.35; }
  .dexrow.locked { opacity: .55; }
  .dexrow.locked .dexbody b { letter-spacing: .06em; }
  #panel h2 { font-size: 22px; font-weight: 900; margin-bottom: 6px; }
  #panel .ver { font-size: 11px; font-weight: 700; color: #8a6b3a; letter-spacing: .05em; }

  /* ───── タイトル画面（img/ui/title_menue.webp ＋ 板ボタン） ─────
     ■ 1枚絵を画面いっぱいに出し、その上にボタンを載せる。絵(128×277)は縦長スマホの
       縦横比に合わせて描かれている（実測の黒帯: 375×812=0px / 390×844=0px / 414×896=0px /
       430×932=1px / 360×640=左右32px）。
       端末差は黒帯で吸収する＝**絵は絶対に引き伸ばさない**。
     ■ .title-stage は「絵とぴったり同じ矩形」。aspect-ratio + max-width/max-height で
       contain と同じ収まり方を作っているので、中のボタンを % で置けば
       どの端末でも絵との位置関係が変わらない（絵に描かれた扉の中にボタンが並ぶ）。
     ■ タイトルだけ #panel の羊皮紙を外して全画面にする（.title-full）。
       #panel の中身は25箇所から差し替えられるので、クラスの後始末は
       ui.js の MutationObserver が自動でやる（各所に書くと必ず漏れる）。 */
  #panel.title-full {
    width: 100%; height: 100%;
    max-width: none; max-height: none;
    padding: 0; border: 0; border-radius: 0;
    background: #000;                      /* 端末差を吸収する黒帯 */
    box-shadow: none;
    overflow: hidden;
    display: flex; align-items: center; justify-content: center;
    animation: none;                       /* 全画面ではパネルの跳ねる登場は使わない */
  }
  #panel.title-full .title-stage {
    position: relative;
    aspect-ratio: 128 / 277;
    height: 100%; width: auto;
    max-width: 100%; max-height: 100%;     /* ここまでで contain 相当の収まりになる */
    line-height: 0;
  }
  /* 絵の矩形。ステージと同じ大きさ（＝ボタンを重ねる土台）。
     .title-ui と**兄弟**にしてあるのが肝で、入れ子にするとボタン幅が絵の幅に縛られる */
  #panel.title-full .title-frame { position: absolute; inset: 0; }
  #panel.title-full .title-bg {
    width: 100%; height: 100%; display: block;
    image-rendering: pixelated;
  }

  /* ───── タイトルの灯り（ランタン3灯＋店内） ─────
     絵は「全体を一段落とした base（title_menue.webp）」＋「灯った一角だけの透過レイヤー
     （title_lamp1〜3.webp・128×277）」の重ね。レイヤーの opacity を上下させると、
     その一角だけが明るくなったり base の暗さへ落ちたりする＝ランプが息をする。

     重ね順が肝: base → 暗幕(.title-dim) → 灯り(.title-lamp) → 店内(.title-inglow)。
     **暗幕を灯りより下に置くこと。** 上に置くと灯りまで一緒に沈んで、
     「暗くすると光も消える」という逆の絵になる。この順なら
     「ランプから離れた場所だけが暗くなり、灯りはそのまま光る」になる。

     調整はすべて .title-stage の変数1か所で足りる（下の --title-dim / --lamp-lo / --lamp-hi）。 */
  #panel.title-full .title-stage {
    --title-dim: .5;    /* ランプから離れた場所の暗さ（0＝暗幕なし / 1＝真っ黒） */
    --title-dim-r: 46%; /* この半径までは暗くしない（扉とランタンを含む範囲） */
    --lamp-lo: .45;     /* 灯りが最も落ちたときの濃さ（0にすると消灯に見える） */
    --lamp-hi: 1;       /* 最も強いときの濃さ */
    --glow-lo: .06;     /* 店内のゆらぎ（弱） */
    --glow-hi: .2;      /* 　　〃　　（強）。上げすぎると扉の中が「オレンジの板」になる */
  }
  #panel.title-full .title-dim {
    position: absolute; inset: 0; pointer-events: none;
    /* 中央（扉と3灯のある帯）を残して、外へ向かうほど濃くする楕円の暗幕。
       石畳の手前と空の隅が落ちて、視線が入口へ集まる */
    background:
      radial-gradient(ellipse 78% 52% at 50% 42%,
        #05030b00 0%,
        #05030b00 var(--title-dim-r),
        rgba(5, 3, 11, calc(var(--title-dim) * .55)) 74%,
        rgba(5, 3, 11, var(--title-dim)) 100%);
  }
  #panel.title-full .title-lamp {
    position: absolute; inset: 0;
    width: 100%; height: 100%;
    image-rendering: pixelated;
    pointer-events: none;
    opacity: var(--lamp-hi);
    animation: lampflicker 4s ease-in-out infinite;
    /* 周期と位相は ui.js の titleLampFlicker が1灯ずつ散らす（同期して見せない） */
  }
  /* 店内の灯り。奥行きのある扉の中だけを暖色でゆらす（専用の絵は要らない） */
  #panel.title-full .title-inglow {
    position: absolute; inset: 0; pointer-events: none;
    opacity: var(--glow-hi);
    background: radial-gradient(ellipse 26% 13% at 50% 57%, #ffb85e 0%, #ff8f3a55 46%, #ff8f3a00 100%);
    animation: inglowflicker 5s ease-in-out infinite;
  }
  /* 揺れ幅は小さく、間隔は不揃いに。等間隔の sin 波にすると「電飾」に見える。
     ガクッと落ちる一瞬（62%）を混ぜると炎らしくなる */
  @keyframes lampflicker {
    0%, 100% { opacity: var(--lamp-hi); }
    18%      { opacity: calc(var(--lamp-lo) + (var(--lamp-hi) - var(--lamp-lo)) * .74); }
    34%      { opacity: var(--lamp-hi); }
    52%      { opacity: calc(var(--lamp-lo) + (var(--lamp-hi) - var(--lamp-lo)) * .55); }
    62%      { opacity: var(--lamp-lo); }
    70%      { opacity: calc(var(--lamp-lo) + (var(--lamp-hi) - var(--lamp-lo)) * .88); }
    86%      { opacity: calc(var(--lamp-lo) + (var(--lamp-hi) - var(--lamp-lo)) * .62); }
  }
  @keyframes inglowflicker {
    0%, 100% { opacity: var(--glow-hi); }
    26%      { opacity: var(--glow-lo); }
    44%      { opacity: calc(var(--glow-lo) + (var(--glow-hi) - var(--glow-lo)) * .8); }
    58%      { opacity: calc(var(--glow-lo) + (var(--glow-hi) - var(--glow-lo)) * .3); }
    78%      { opacity: var(--glow-hi); }
  }
  /* 動きを減らす設定では止める（プロジェクト共通の約束）。
     消すのではなく「いちばん明るい状態で静止」＝絵として成立させる */
  @media (prefers-reduced-motion: reduce) {
    #panel.title-full .title-lamp,
    #panel.title-full .title-inglow { animation: none; }
  }
  /* ボタン類の入れ物。絵の上に重ねる */
  #panel.title-full .title-ui { position: absolute; inset: 0; }
  /* 文字サイズは**ステージ幅**を基準にする。vh だけで書くと画面が低いときに
     文字が 7px まで落ちて読めない（実測）。
     ステージ幅 = min(100vw, 100vh×0.4621) なので、その1% = min(1vw, 0.4621vh) */
  /* 設定は絵の右上角へ小さく重ねる（絵の題字を隠さない位置） */
  #panel.title-full .title-gear {
    position: absolute; top: 1.2%; right: 2.5%;
    background: #00000066; border: 1px solid #ffffff33; border-radius: 999px;
    width: 9%; aspect-ratio: 1;
    font-size: clamp(13px, min(4.6vw, 2.13vh), 19px); line-height: 1;
    color: #f7e8ca; cursor: pointer; padding: 0;
    backdrop-filter: blur(2px);
  }
  #panel.title-full .title-ver {
    position: absolute; right: 3%; bottom: .8%;
    font-size: clamp(8px, min(2.5vw, 1.16vh), 10px); font-weight: 700; letter-spacing: .04em;
    color: #e8d3a4; text-shadow: 0 1px 2px #000, 0 0 4px #000;
    line-height: 1.2;
  }
  /* ボタンの帯: 絵の「扉」の部分（縦 43%〜）に上から並べる。
     幅・すき間も % なので、黒帯が出る端末でも絵に対する見え方は同じ。
     ⚠️ 板の間隔に `gap: n%` を使ってはいけない。縦方向の % gap は
       flexコンテナの**高さ**基準だが、ここは高さが中身依存＝不定なので 0 に解決される
       （実測で隙間0px＝板がぴったり密着していた）。
       % マージンは常に「包む要素の幅」基準なので必ず効く → margin-bottom で開ける。 */
  #panel.title-full .start-actions {
    position: absolute; left: 50%; top: 43%;
    transform: translateX(-50%);
    width: 50%;
    display: flex; flex-direction: column;
    line-height: 1.2;
    /* 板が小さくなったぶん、載せる文字もステージ幅基準で縮める（無地板の「続きから」用） */
    font-size: clamp(9px, min(3.05vw, 1.41vh), 13px);
  }
  #panel.title-full .start-actions .mplate {
    width: 100%; margin: 0 0 6.5%;
    font-size: inherit;
  }
  #panel.title-full .start-actions .mplate:last-child { margin-bottom: 0; }
  /* 細かい導線は絵の最下部（石段）へ。読めるように薄い黒の帯を敷く */
  #panel.title-full .title-links {
    position: absolute; left: 0; right: 0; bottom: 0;
    padding: 1.4% 4% 1.8%;
    background: linear-gradient(180deg, #0000, #000000b3 45%);
    line-height: 1.25;
  }
  #panel.title-full .title-links .title-link {
    display: inline-block; width: auto;
    margin: 0 3px; padding: 3px 2px;
    font-size: clamp(10px, min(3.4vw, 1.57vh), 13px);
    color: #f0dcae; text-decoration-color: #f0dcae66;
    text-shadow: 0 1px 2px #000;
  }

  /* ■ メニューの板。原画は幅128pxで高さが 33〜36px と1枚ずつ違うので、
       aspect-ratio を板ごとに当てて縦横比を保つ（一律だと文字が伸びる）。
     ■ background の一括指定は background-image を消すので、**個別の板のルールを後に置く**
       （募兵カードの枠絵で踏んだのと同じ罠）。
     ■ url() は style.css からの相対で書く。CSS変数に入れると Chrome が
       var() を使った側を基準に解決して 404 になる（枠絵で踏んだ罠）。 */
  #panel .mplate {
    display: block;
    width: min(100%, 192px);
    margin: 0 auto 7px;
    border: 0; padding: 0;
    background: no-repeat center / 100% 100%;
    image-rendering: pixelated;
    cursor: pointer;
    filter: drop-shadow(0 2px 0 #00000055);
    transition: transform .1s, filter .1s;
    /* 無地板(other)に載せる文字。焼き込み済みの板では中身が空なので効かない */
    font-family: inherit; font-weight: 900; font-size: 12.5px; color: #f7e8ca;
    text-shadow: 0 1px 0 #000, 0 0 4px #000000aa;
    line-height: 1.2;
  }
  #panel .mplate:active { transform: translateY(2px); filter: none; }
  #panel .mplate:disabled { filter: grayscale(.7); opacity: .5; pointer-events: none; }
  #panel .mplate[data-p="nomal"]     { aspect-ratio: 128/34; background-image: url('../img/ui/bottan/botan_nomal.webp'); }
  #panel .mplate[data-p="endless"]   { aspect-ratio: 128/36; background-image: url('../img/ui/bottan/botan_endless.webp'); }
  #panel .mplate[data-p="tyutorial"] { aspect-ratio: 128/35; background-image: url('../img/ui/bottan/botan_tyutorial.webp'); }
  #panel .mplate[data-p="ranking"]   { aspect-ratio: 128/36; background-image: url('../img/ui/bottan/botan_ranking.webp'); }
  #panel .mplate[data-p="other"]     { aspect-ratio: 128/33; background-image: url('../img/ui/bottan/botan_other.webp'); }
  #panel .start-note { font-size: 10.5px; margin: 8px 0 0; color: #6b4f23; line-height: 1.5; }
  /* 細かい導線は従来どおりのテキストリンク（板にすると縦に伸びすぎる） */
  #panel .title-link {
    display: block; width: 100%;
    background: none; border: none; cursor: pointer;
    font-family: inherit; font-size: 11px; font-weight: 700;
    color: #8a6b3a; text-decoration: underline;
    margin-top: 5px; padding: 2px 0;
  }
  #panel p { font-size: 13px; line-height: 1.7; margin-bottom: 13px; white-space: pre-line; }
  #panel .btn { width: 100%; background: linear-gradient(180deg, var(--gold), var(--gold-deep)); color: #3b2800; }
  #panel .feastChoices { display: flex; gap: 10px; margin: 2px 0 4px; }
  #panel .feastChoices.four { flex-wrap: wrap; gap: 8px; } /* みんな宴好き: 2×2 */
  #panel .feastChoices.four .feastBtn { flex: 1 1 42%; padding: 10px 5px 9px; }
  #panel .feastBtn {
    flex: 1; display: flex; flex-direction: column; align-items: center; gap: 3px;
    padding: 13px 6px 11px; line-height: 1.35; white-space: normal;
    background: linear-gradient(180deg, #3a2a18, #241708);
    color: var(--parchment);
    border: 2px solid var(--fc, var(--gold));
    box-shadow: 0 0 12px #00000088, inset 0 1px 0 #ffffff14;
  }
  #panel .feastBtn:active { transform: scale(.96); }
  #panel .feastBtn .fic { font-size: 32px; filter: drop-shadow(0 2px 4px #000a); }
  #panel .feastBtn .fname { font-size: 14px; font-weight: 900; color: var(--fc, var(--gold)); text-shadow: 0 1px 2px #000; }
  #panel .feastBtn .fdesc { font-size: 10px; font-weight: 700; opacity: .8; }
  #panel .feastBtn .fnote { font-size: 10px; font-weight: 800; margin-top: 3px; padding: 2px 8px; border-radius: 8px; background: #00000055; border: 1px solid var(--fc, var(--gold)); }
  #panel .help-grid { text-align: left; font-size: 11px; line-height: 1.7; margin-bottom: 13px; }
  #panel .help-grid b { color: var(--gold-deep); }
  #panel h3 { font-size: 12.5px; font-weight: 900; margin: 11px 0 4px; color: var(--gold-deep); text-align: left; border-bottom: 1px solid #b07f1e44; padding-bottom: 2px; }
  #panel h3:first-child { margin-top: 0; }
  .help-scroll { max-height: 56vh; overflow-y: auto; text-align: left; font-size: 11px; line-height: 1.65; margin-bottom: 12px; padding-right: 5px; overscroll-behavior: contain; }
  .help-scroll p { font-size: 11px; margin-bottom: 5px; }
  /* ── ヘルプ先頭「タイトルへ戻る」「設定」 ── */
  #panel .help-top { display: flex; gap: 6px; margin-bottom: 8px; }
  #panel .htitle-back {
    font-family: inherit; font-weight: 800; font-size: 11px;
    color: #5d4322; background: #00000012;
    border: 1px solid #b07f1e55; border-radius: 8px;
    padding: 6px 10px; cursor: pointer;
  }
  #panel .htitle-back:active { transform: translateY(1px); background: #00000020; }
  /* ── パネル上部リンク（ホームの⚙️設定） ── */
  #panel .panel-top { display: flex; justify-content: flex-end; margin-bottom: 2px; }
  #panel .panel-link {
    font-family: inherit; font-weight: 800; font-size: 11px;
    color: #5d4322; background: #00000012;
    border: 1px solid #b07f1e55; border-radius: 8px;
    padding: 5px 11px; cursor: pointer;
  }
  #panel .panel-link:active { transform: translateY(1px); background: #00000020; }
  /* ── 設定ページ ── */
  #panel .settings { text-align: left; margin-bottom: 14px; }
  #panel .setrow { margin-bottom: 15px; }
  #panel .setrow label {
    display: flex; justify-content: space-between; align-items: baseline;
    font-size: 12.5px; font-weight: 800; color: #5d4322; margin-bottom: 7px;
  }
  #panel .setrow label span { color: var(--ink); font-size: 13px; }
  #panel .setrow input[type=range] {
    width: 100%; height: 24px; cursor: pointer;
    accent-color: var(--gold-deep);
  }
  /* 盤面スタイルの3択（セグメント） */
  #panel .segrow { display: flex; gap: 6px; }
  #panel .seg {
    flex: 1; font-family: inherit; font-size: 11px; font-weight: 800; color: #7a5a30;
    background: #00000012; border: 1px solid #b07f1e44; border-radius: 8px;
    padding: 8px 4px; cursor: pointer; white-space: nowrap;
  }
  #panel .seg.on {
    color: #fff; background: linear-gradient(180deg, #c98a2e, #9a651a);
    border-color: var(--gold-deep); box-shadow: 0 2px 0 #00000040;
  }
  #panel .seg:active { transform: translateY(1px); }
  #panel .sethint { font-size: 10px; color: #6b4f23; margin: 0; line-height: 1.5; }
  /* 設定の下段に置く規約への導線。2つ横並び（ボタン既定の幅100%を打ち消す） */
  #panel .setlegal { display: flex; gap: 6px; margin-top: 14px; }
  #panel .setlegal .btn { flex: 1; margin: 0; font-size: 11px; padding: 8px 4px; }

  /* ── 規約類（プライバシーポリシー・利用規約。legal.js） ──
     長文をスマホで読ませる画面。**行長より行間**が効くので line-height を厚めに取り、
     見出しの前だけ大きく空けて段落の切れ目を作る。
     ※ 単体HTML（dist/privacy.html）は build.js が自前のスタイルを持つ。
       見た目を変えるときは両方見ること（本文は共通でも器は別） */
  #panel .legal { text-align: left; font-size: 12px; line-height: 1.85; color: var(--ink); }
  #panel .legal h3 {
    font-size: 13px; color: var(--gold-deep); margin: 18px 0 6px;
    border-bottom: 1px solid #b07f1e44; padding-bottom: 3px;
  }
  #panel .legal h3:first-of-type { margin-top: 12px; }
  #panel .legal .legal-lead {
    background: #00000010; border: 1px dashed #b07f1e55; border-radius: 10px;
    padding: 9px 11px; margin: 0 0 6px; font-size: 12px;
  }
  #panel .legal .legal-p { margin: 0; }
  #panel .legal b { color: var(--gold-deep); }
  #panel .legal ul { margin: 5px 0 0; padding-left: 1.2em; }
  #panel .legal li { margin-bottom: 4px; }
  #panel .legal a { color: #2f6ea5; }
  #panel .legal .legal-updated {
    font-size: 10.5px; color: #6b4f23; text-align: right; margin: 16px 0 4px;
  }
  /* ── 問い合わせ・報告ページ ── */
  #panel .contact-body { text-align: left; font-size: 12px; line-height: 1.75; margin-bottom: 14px; }
  #panel .contact-body b { color: var(--gold-deep); }
  #panel .contact-lead {
    text-align: center; font-size: 13px; font-weight: 800; color: var(--ink);
    margin-bottom: 10px;
  }
  #panel .contact-note {
    font-size: 10.5px; color: #6b4f23; line-height: 1.5;
    background: #00000010; border: 1px dashed #b07f1e55; border-radius: 10px;
    padding: 8px 10px; margin-bottom: 13px;
  }
  #panel a.btn.contact-link {
    display: block; width: 100%; box-sizing: border-box; text-decoration: none;
    background: linear-gradient(180deg, #5d9c6c, #41744e); color: #fff;
    margin-bottom: 8px;
  }
  /* ── インストール案内（タイトル画面） ── */
  #installHint:not(:empty) { margin-top: 10px; }
  #installHint .installBtn {
    width: 100%; font-size: 12px; padding: 9px 0;
    background: linear-gradient(180deg, #4d8ec2, #3a6d99); color: #fff;
  }
  #installHint .installnote {
    font-size: 10.5px; line-height: 1.55; color: #6b4f23;
    margin: 0; padding: 8px 10px;
    background: #00000010; border: 1px dashed #b07f1e66; border-radius: 10px;
    white-space: normal;
  }
  #installHint .installnote b { color: var(--gold-deep); }
  /* ── リザルトカード（最終編成＋発動シナジー） ── */
  #panel .runcard {
    background: #00000010; border: 1px solid #b07f1e44; border-radius: 12px;
    padding: 8px 10px 9px; margin-bottom: 11px;
  }
  #panel .runcard-title { font-size: 10px; font-weight: 800; color: #8a6b3a; margin-bottom: 6px; }
  /* 駒を大きくした分、部隊が多いと段が伸びてパネルを押し出す。
     ここだけスクロールさせて、共有ボタンと「タイトルへ」は必ず画面内に残す */
  #panel .runcard-units {
    display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; margin-bottom: 7px;
    max-height: 34dvh; overflow-y: auto; overscroll-behavior: contain;
  }
  /* 走り終えた編成を「見せる」場所なので大きめに取る（旧36px幅では誰がいたのか読めなかった）。
     1行6体・部隊上限10体でも2行で収まる */
  #panel .rcu {
    display: flex; flex-direction: column; align-items: center;
    width: 52px; padding: 3px 0 2px;
    background: #ffffff2e; border: 1px solid #b07f1e33; border-radius: 8px;
  }
  #panel .rcu-ic { font-size: 30px; line-height: 1.1; filter: drop-shadow(0 1px 1px #00000033); }
  #panel .rcu-star { font-size: 9px; font-weight: 900; color: #8a6b3a; line-height: 1.2; }
  #panel .rcu-star.s2 { color: #7d8ba3; }
  #panel .rcu-star.s3 { color: #c8901f; }
  #panel .rcu-star.s4 { color: #b04ad6; }
  #panel .runcard-syn { display: flex; flex-wrap: wrap; gap: 3px; justify-content: center; }
  #panel .runcard-syn .ctrait { font-size: 9px; padding: 2px 8px; }
  /* ── 闘技場（出場登録・名簿） ── */
  #panel .arena-namebox { text-align: left; margin: 4px 0 10px; }
  #panel .arena-namebox label { display: block; font-size: 10.5px; font-weight: 800; color: #5d4322; margin-bottom: 4px; }
  #panel .arena-namebox input {
    width: 100%; box-sizing: border-box;
    font-family: inherit; font-size: 14px; font-weight: 700; color: var(--ink);
    background: #ffffff55; border: 1px solid var(--gold-deep); border-radius: 8px;
    padding: 7px 10px;
  }
  /* 見出し＋📥ボタン: float だと直下の名簿（overflow による BFC）が float を避けて細くなるので flex で並べる */
  #panel .arena-rosterhead { display: flex; align-items: center; justify-content: space-between; gap: 8px; text-align: left; }
  #panel .arena-roster {
    max-height: 30vh; overflow-y: auto; text-align: left;
    background: #00000010; border-radius: 10px; padding: 5px 8px; margin-bottom: 11px;
  }
  #panel .arena-row { display: flex; align-items: center; gap: 6px; padding: 4px 0; border-bottom: 1px dashed #00000018; }
  #panel .arena-row:last-child { border-bottom: none; }
  /* min-width は small の ellipsis を効かせるため小さく、ただし軍団名が 1 文字ずつ折り返すほど潰れない値に */
  #panel .arena-row-main { flex: 1; min-width: 72px; display: flex; flex-direction: column; }
  #panel .arena-row-main b { font-size: 12px; }
  #panel .arena-row-main small { font-size: 12px; letter-spacing: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  #panel .arena-row-date { font-size: 9.5px; color: #8a6b3a; flex: 0 0 auto; }
  #panel .arena-fight {
    flex: 0 0 auto; font-family: inherit; font-weight: 900; font-size: 10.5px;
    color: #fff; background: linear-gradient(180deg, #b8734d, #8f4f30);
    border: none; border-radius: 7px; padding: 4px 8px; cursor: pointer;
    box-shadow: 0 2px 0 #00000066;
  }
  #panel .arena-fight:active { transform: translateY(1px); box-shadow: none; }
  #panel .arena-del {
    flex: 0 0 auto; font-family: inherit; font-weight: 800; font-size: 11px;
    color: #7a5a30; background: #00000012; border: 1px solid #b07f1e44; border-radius: 6px;
    padding: 2px 7px; cursor: pointer;
  }
  #panel .arena-del.arm { color: #fff; background: var(--hp-red); border-color: #a03528; font-size: 9.5px; }
  /* ── 挑戦コード（フレンド対戦） ── */
  #panel .arena-code {
    width: 100%; box-sizing: border-box; height: 92px; resize: none;
    font-family: monospace; font-size: 10px; color: var(--ink);
    background: #ffffff55; border: 1px solid var(--gold-deep); border-radius: 8px;
    padding: 7px 9px; margin-bottom: 10px; word-break: break-all;
  }
  #panel .arena-empty { font-size: 11px; color: #6b4f23; margin: 4px 0; }
  /* ── 結果パネルの共有ボタン ── */
  #panel .shareRow { display: flex; gap: 8px; margin-bottom: 9px; }
  #panel .shareRow .btn { margin: 0; }
  #panel .shareX { background: linear-gradient(180deg, #2a2f36, #15181d); color: #fff; }
  #panel .shareCopy { flex: 0 0 92px; background: linear-gradient(180deg, #6b7fa3, #50617f); color: #fff; }
  .hu { display: flex; gap: 7px; padding: 5px 0; border-bottom: 1px dashed #b07f1e33; align-items: flex-start; }
  .hu:last-child { border-bottom: none; }
  .hu-ic { font-size: 52px; line-height: 1; margin-top: 1px; flex: 0 0 auto; width: 84px; text-align: center; }
  .hu-body { flex: 1; min-width: 0; }
  .hu-top { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
  .hu-top b { font-size: 11.5px; }
  .hu-t { font-size: 8px; font-weight: 900; padding: 1px 5px; border-radius: 999px; color: #fff; }
  .hu-t.t1 { background: var(--t1); } .hu-t.t2 { background: #8d97a8; } .hu-t.t3 { background: var(--gold-deep); }
  .hu-t.t4 { background: linear-gradient(90deg, #a06ee0, #d4583a); }
  .hu-cost { font-size: 9.5px; font-weight: 900; color: var(--gold-deep); }
  .hu-stats { font-size: 10px; font-weight: 700; color: #6b4f23; }
  .hu-desc { font-size: 10px; }
  .hs { display: flex; gap: 6px; align-items: flex-start; padding: 3px 0; font-size: 10.5px; }
  .hs .ctrait { flex: 0 0 auto; margin-top: 2px; }

  /* 地形切り替えバナー（画面中央付近） */
  #biomeBanner {
    position: fixed; left: 50%; top: 42%;
    transform: translate(-50%, -50%) scale(.92);
    background: linear-gradient(180deg, #000000d8, #1a1410d8);
    border: 1px solid #6b542f;
    color: var(--parchment);
    text-align: center;
    font-size: 16px; font-weight: 900;
    padding: 12px 22px; border-radius: 14px;
    box-shadow: 0 6px 24px #000c;
    opacity: 0; transition: opacity .35s, transform .35s;
    pointer-events: none; z-index: 61;
  }
  #biomeBanner small { display: block; font-size: 11px; font-weight: 700; margin-top: 4px; color: #d9c8a3; }
  #biomeBanner.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }

  /* ── ユニット画像（ドット絵差し替え）。定義に img: を足したユニットだけ画像になる ──
     絵文字と同じ枠（丸ポートレート等）の内側に収まるサイズで表示。
     image-rendering: pixelated でドット絵を拡大してもぼやけない */
  .uimg {
    width: 88%; height: 88%;
    object-fit: contain;
    image-rendering: pixelated;
    display: block;
    pointer-events: none;
    -webkit-user-drag: none;
  }
  .hu-ic .uimg { width: 84px; height: 84px; }   /* 図鑑: 立ち絵をしっかり見せる（本文は残り幅で折り返す） */
  /* お助けキャラ（図鑑の末尾）。通常ユニットと同じ .hu 行だが、主役ではないので一回り小さく。
     絵柄が複数ある面々（ワンコ3種・疲れた実力者5系統）は全員を**2段**に組む。
     列数は JS が --n（＝ceil(枚数/2)）で渡す＝枚数が増えても常に2段のまま収まる。
     幅は通常ユニットと同じ84pxに固定しているので、行頭の位置が上下で揃う */
  .hu.helper .hu-ic { font-size: 40px; }
  .hu.helper .hu-ic .uimg { width: 66px; height: 66px; margin: 0 auto; }
  .hu.helper .hu-ic.multi { display: grid; grid-template-columns: repeat(var(--n, 2), 1fr); gap: 2px; }
  .hu.helper .hu-ic.multi .uimg { width: 100%; height: auto; aspect-ratio: 1; }
  .hu.helper.locked { opacity: .55; }
  .uimg-mx { width: 30px; height: 30px; display: inline-block; vertical-align: middle; }  /* シナジー×Tier早見表 */
  .rcu-ic .uimg { width: 46px; height: 46px; }   /* リザルトカード: 最終編成を見せ場にする */
  .uimg-s { width: 15px; height: 15px; display: inline-block; vertical-align: -3px; }  /* 情報バー等の行内 */
  .uimg-fly { width: 30px; height: 30px; }                     /* 購入時のフライ演出 */

  /* ── ゲーム内アイコン（攻撃力・体力など）──
     マークアップは <i class="gi gi-atk">⚔</i>。中の絵文字は**画像が落ちたときのフォールバック**で、
     ふだんは color:transparent で隠し、背景画像だけを見せている（1要素で両方を賄う）。
     ※ サイズは 1em 追従。文字と同じ行に置いても行送りが変わらないよう vertical-align で微調整 */
  .gi {
    display: inline-block;
    width: 1.05em; height: 1.05em;
    vertical-align: -.16em;
    background: no-repeat center / contain;
    image-rendering: pixelated;
    color: transparent;              /* 画像モードでは中の絵文字を隠す */
    font-size: inherit; line-height: 1;
    flex: none;
  }
  .gi-atk    { background-image: url('../img/ui/atk.webp'); }
  .gi-hp     { background-image: url('../img/ui/hp.webp'); }
  .gi-gold   { background-image: url('../img/ui/gold.webp'); }
  .gi-cap    { background-image: url('../img/ui/cap.webp'); }
  .gi-forge  { background-image: url('../img/ui/forge.webp'); }
  .gi-star   { background-image: url('../img/ui/star.webp'); }
  .gi-pet    { background-image: url('../img/ui/pet.webp'); }
  .gi-threat { background-image: url('../img/ui/threat.webp'); }
  .gi-chest  { background-image: url('../img/ui/chest.webp'); }

  /* ── B方式: 全身立ち絵ユニット（img: を持つと hasimg が付く）──
     円メダルを外し、足を接地させて地形に立たせる。陣営（味方/敵）は
     メダル復活の代わりに「足元の影を薄く色づけ」して識別する。 */
  /* ■ 立ち絵のサイズと接地点の関係（変えるときは必ずこの式を守る）
       .tok は top=マス中心・margin-top=mt で置かれ、箱の高さは .tport の height だけ
       （HPバーは position:absolute で flow から外してある）。よって
           絵の下端（足元） = マス中心 + mt + height
       これを **マス中心 + 9px** に保つのが接地の約束＝ mt = 9 - height。
       クラシック(絵文字)も 25px の円に mt:-16px で同じ +9 に揃っている。
       ここを崩すと駒が地面から浮く／埋まるので、height を変えたら mt も必ず直すこと。 */
  .tok.hasimg { margin-top: -55px; }                 /* = 9 - 64（下の height と対） */
  /* ■ 箱は**正方形**にすること（原画が 96×96 の正方形なので）
       object-fit:contain は短い辺に合わせる。ここが 58×64 だった間、絵は幅58に合わせて
       58×58 で描かれ、**箱の上に 6px（高さの9.4%）が透明のまま余っていた**。
       ところが駒の倍率（render.js の updateTokenScale）は TOK_H=64＝**箱の高さ**を基準に
       「駒の高さ＝行間×1.5」を保っている。つまり 1.5 のつもりで実際は 1.36 しか無く、
       **確保済みの場所に対して駒だけが1割小さく描かれていた**。
       実測（375×812・r4→r5）: 絵の高さ 55.4px・行間 44.6px＝重なり 23%（狙いは約1/3）。
       正方形にすると 61.1px・重なり 30% で、コメントどおりの見え方になる。
     ⚠️ height は 64 のまま（margin-top:-55px = 9-64 と対）。**高さを触ると接地が崩れる。**
     ⚠️ 絵が箱いっぱいになるぶん、頭上に空いていた透明マージンが消える。
       HPバー(.thp)と★(.tstars)は下で持ち上げてある。片方だけ変えないこと。 */
  .tok.hasimg .tport {
    width: 64px; height: 64px;                        /* 通常ユニットも魔王級の大きさで見せる（足元基準） */
    background: none; border: none; box-shadow: none; border-radius: 0;
    align-items: flex-end;                            /* 足を下辺に接地 */
    overflow: visible;
  }
  #board.arena .tok.hasimg .tport { background: none; }   /* 闘技場の陣営ディスク(id付き)を無効化 */
  .tok.hasimg .uimg {
    width: 100%; height: 100%;
    object-fit: contain;
    object-position: bottom center;                   /* 足元基準で拡大縮小 */
    filter: drop-shadow(0 1px 0 #00000066);           /* 地際を軽く締める */
  }
  /* 通常ユニットを魔王級まで上げたので、ボスはさらに一段大きくして格を保つ（mt = 9 - 76） */
  .tok.hasimg.boss { margin-top: -67px; }
  .tok.hasimg.boss .tport { width: 76px; height: 76px; }   /* ボス（魔王）は一回り大きく（正方形の理由は上） */
  /* 陣営識別: 足元の影を薄く染める（円メダルは復活させない） */
  /* 影の幅は絵の幅に比例させる（絵を 58→64 に広げたので 1.10 倍した）。
     据え置くと大きくなった駒の足元だけ影が細く、浮いて見える */
  .tok.hasimg .tsh { bottom: 0; width: 43px; height: 11px; margin-left: -21.5px; }  /* 立ち絵の足元にぴたりと影を置く */
  /* 大きい駒ほど影も大きい＝重さの手がかり（.tok.hasimg .tsh が上書きしてしまうので個別に書く） */
  .tok.hasimg.s3 .tsh { width: 48px; margin-left: -24px; height: 12px; }
  .tok.hasimg.s4 .tsh { width: 53px; margin-left: -26.5px; height: 13px; }
  .tok.hasimg.boss .tsh { width: 59px; margin-left: -29.5px; height: 14px; }
  /* 陣営の染め分けは色変数だけ差し替える。background を上書きすると二層構造ごと消える */
  .tok.hasimg.foe .tsh { --shc: 176, 62, 42; }
  #board.arena .tok.hasimg:not(.foe) .tsh { --shc: 46, 100, 158; }
  #board.arena .tok.hasimg.foe .tsh { --shc: 176, 62, 42; }
  .tok.hasimg.selected .uimg { filter: drop-shadow(0 0 3px #ffe08a) drop-shadow(0 1px 0 #00000066); }

  /* ── ★・エースの光り物を「絵の形」に沿わせる ──
     円メダル時代の演出は box-shadow で光らせていたが、立ち絵の .tport は
     絵より一回り大きい"透明な矩形"なので、そのままだと絵ではなく
     矩形の外周が四角く光ってしまう（★3で特に目立つ）。
     ※ .tok.hasimg .tport の box-shadow:none だけでは消えない。
       s3pulse/s4pulse/acepulse が keyframe で box-shadow を打ち込んでおり、
       アニメーション由来の値は通常の宣言より強いため。アニメごと差し替える必要がある。
     → pulse を外し、シルエットを縁取る drop-shadow に置き換える */
  .tok.hasimg.s2 .tport,
  .tok.hasimg.s3 .tport,
  .tok.hasimg.s4 .tport,
  .tok.hasimg.ace .tport { animation: idlebob 2.6s ease-in-out infinite; box-shadow: none; }
  /* ── steps() の理由（発熱対策・#predictLayer line と同じ考え方） ──
     filter は合成できない。値が動くたびにスプライトがブラー付きで焼き直される。
     ★3/★4/エースは終盤ほど盤に増えるので、これが待機中の再ペイントの主役になる
     （実測: 予測ラインを止めたあとの残りの約半分がこの3つ）。
     ゆっくりした息づかいなので 10fps でも動いて見える。
     steps(n) は**キーフレームの区間ごと**に効くので、n × 区間数 ÷ 秒 = 10 になる数を入れる:
       s3glow  1.6s・2区間(0→50→100) → steps(8)  = 16回/1.6s = 10fps
       s4glow  2.4s・3区間(0→33→66→100) → steps(8) = 24回/2.4s = 10fps
       aceglow 1.3s・2区間 → steps(7) = 14回/1.3s ≒ 10fps */
  .tok.hasimg.s3 .uimg  { animation: s3glow 1.6s steps(8) infinite; }
  .tok.hasimg.s4 .uimg  { animation: s4glow 2.4s steps(8) infinite; }
  .tok.hasimg.ace .uimg { animation: aceglow 1.3s steps(7) infinite; }
  @keyframes s3glow {
    0%, 100% { filter: drop-shadow(0 0 2px var(--gold)) drop-shadow(0 1px 0 #00000066); }
    50%      { filter: drop-shadow(0 0 5px var(--gold)) drop-shadow(0 0 2px #fff3c4) drop-shadow(0 1px 0 #00000066); }
  }
  @keyframes s4glow {
    0%, 100% { filter: drop-shadow(0 0 4px var(--gold)) drop-shadow(0 1px 0 #00000066); }
    33%      { filter: drop-shadow(0 0 4px #ff7eb5) drop-shadow(0 1px 0 #00000066); }
    66%      { filter: drop-shadow(0 0 4px #7ecbff) drop-shadow(0 1px 0 #00000066); }
  }
  @keyframes aceglow {
    0%, 100% { filter: drop-shadow(0 0 2px #c04ad6) drop-shadow(0 1px 0 #00000066); }
    50%      { filter: drop-shadow(0 0 5px #c04ad6) drop-shadow(0 1px 0 #00000066); }
  }
  /* ★3/★4 の回転リングは頭の高さ（円メダルの位置）に合わせてあり、立ち絵だと
     宙に浮いた輪に見える。足元に敷く魔法陣（平たい楕円）へ置き換える。
     楕円は回すと歪んで見えるので、回転ではなく明滅にする */
  .tok.hasimg.s3::before,
  .tok.hasimg.s4::before {
    top: auto; bottom: -2px;
    width: 44px; height: 17px; margin-left: -22px;
    animation: ringpulse 1.8s ease-in-out infinite;
  }
  .tok.hasimg.s4::after {
    top: auto; bottom: -5px;
    width: 56px; height: 22px; margin-left: -28px;
    animation: ringpulse 2.6s ease-in-out infinite;
  }
  .tok.hasimg.boss.s3::before, .tok.hasimg.boss.s4::before { width: 56px; margin-left: -28px; }
  @keyframes ringpulse { 0%, 100% { opacity: .3; } 50% { opacity: .85; } }

  /* ── 対人戦(闘技場)の敵味方識別 ──
     両軍とも同じ味方スプライトを使うため、足元の影の色だけでは見分けにくい。
     相手側は彩度と明度を落として「くすませる」。
     ※ .uimg ではなく .tport に掛けること。.uimg に掛けると上の ★3 等の
       drop-shadow アニメーションに上書きされて効かなくなる（アニメの方が強い）。
       親要素の filter は子の filter と合成されるので、この掛け方なら共存できる */
  #board.arena .tok.hasimg.foe .tport { filter: grayscale(.75) brightness(.7) contrast(1.05); }
  #board.arena .tok.hasimg.foe .tsh { background: radial-gradient(ellipse at center, #00000088, transparent 70%); }
  #board.arena .tok.hasimg:not(.foe) .tport { filter: saturate(1.12); }   /* 自軍はほんの少し鮮やかに */

  /* ── 捕らえた魔物の敵味方識別（通常モード）──
     捕獲した魔物は「倒した敵とまったく同じ絵」のまま味方として立つ。円メダル表示なら
     緑リング＋緑肌で見分けられるが、立ち絵表示では .tok.hasimg .tport が
     border / background / box-shadow をまとめて消すので、手掛かりが 🐾 バッジだけになり、
     同じ魔物が敵にも並んでいると区別がつかない。
     → 足元に緑の魔法陣を敷き、シルエットを緑で縁取って「捕らえたもの」を示す。
     ※ filter は .uimg ではなく .tport に掛ける（.uimg 側は ★・エースの
       drop-shadow アニメーションに上書きされる。親の filter は子と合成される）
     ※ :not(.foe) は必須。闘技場の幽霊は敵側にも pet が付く
     ※ 闘技場では id 詳細度の陣営ルール（上）が勝つ＝青/灰の識別がそのまま残る */
  .tok.hasimg.pet:not(.foe) .tport {
    filter: drop-shadow(0 0 2px var(--pet)) drop-shadow(0 0 5px #4ad67e77);
  }
  .tok.hasimg.pet:not(.foe) .tsh {
    bottom: -1px; width: 30px; height: 11px; margin-left: -15px;
    background: radial-gradient(ellipse at center, #4ad67e00 32%, #4ad67ecc 56%, #4ad67e00 80%);
    animation: petring 1.9s ease-in-out infinite;
  }
  @keyframes petring { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }
  /* 🐾 は絵や地形の上に重なるため、素の絵文字だと地の色に紛れて何が描いてあるか分からない。
     明るい緑の丸バッジに乗せる（🐾 の絵文字はどの環境でも濃色なので明色の地なら必ず読める） */
  .tok.hasimg.pet:not(.foe) .tpet {
    top: -4px; right: -13px;
    font-size: 8px; line-height: 13px; width: 13px; height: 13px; text-align: center;
    background: #8ceeb0; border: 1px solid #14361f; border-radius: 50%;
    box-shadow: 0 0 5px #4ad67e99;
  }

  /* ── 情報バーの系統チップ（タップした駒のシナジー） ── */
  #infoBar .ib-traits { display: inline-flex; gap: 2px; margin: 0 3px; vertical-align: 1px; }
  #infoBar .ctrait { font-size: 8px; padding: 1px 5px; }

  /* ── 図鑑: シナジー×Tier早見表 ── */
  .dexmx { width: 100%; border-collapse: collapse; margin: 2px 0 4px; }
  .dexmx th, .dexmx td { border: 1px solid #d9c8a355; padding: 3px 2px; text-align: center; font-size: 16px; }
  .dexmx th { font-size: 10px; color: #6b4f23; }
  .dexmx td span { margin: 0 1px; }
  .dexmx-none { color: #c9b890; font-size: 10px; }
  .dexmx-note { font-size: 10px; color: #8a6b3a; margin: 0 0 4px; }

  /* ── 📊 プレイ統計 ── */
  #panel .st-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; margin: 2px 0 4px; }
  #panel .st-cell { background: #00000010; border-radius: 8px; padding: 5px 2px; text-align: center; }
  #panel .st-cell b { display: block; font-size: 14px; }
  #panel .st-cell small { font-size: 8.5px; color: #6b4f23; font-weight: 700; }
  #panel .st-row { display: flex; align-items: center; gap: 5px; padding: 1.5px 0; font-size: 11px; }
  #panel .st-ic { flex: 0 0 18px; text-align: center; }
  #panel .st-name { flex: 0 0 74px; text-align: left; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  #panel .st-bar { flex: 1; height: 9px; background: #00000015; border-radius: 999px; overflow: hidden; }
  #panel .st-bar i { display: block; height: 100%; background: linear-gradient(90deg, var(--gold), var(--gold-deep)); border-radius: 999px; }
  #panel .st-n { flex: 0 0 26px; text-align: right; font-weight: 900; font-size: 11px; }
  #panel .st-line { display: flex; flex-wrap: wrap; gap: 5px; align-items: center; }
  #panel .st-syn { font-size: 11px; font-weight: 900; display: inline-flex; align-items: center; gap: 2px; }
  #panel .st-empty { font-size: 11px; color: #8a6b3a; }
  #toast {
    position: fixed; left: 50%; bottom: 21%;
    transform: translateX(-50%);
    background: #000000cc;
    color: var(--parchment);
    font-size: 12px; font-weight: 700;
    padding: 8px 16px; border-radius: 999px;
    opacity: 0; transition: opacity .25s;
    pointer-events: none; z-index: 60;
    white-space: nowrap;
    text-align: center; line-height: 1.5;
  }
  #toast.show { opacity: 1; }
  /* 長文トースト: 画面幅で折り返し、見切れを防ぐ */
  #toast.long {
    white-space: normal;
    max-width: min(84vw, 340px);
    border-radius: 16px;
    padding: 11px 18px;
    font-size: 12.5px;
  }
  #toast.long b { color: var(--gold); font-size: 13.5px; }
  #toast.long .tsub { color: #7be3a0; font-weight: 900; }

  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after { animation: none !important; transition: none !important; }
  }
