/* ゆっくり動画風トークツール - プレイヤー画面のスタイル。
 * ステージ内部は1280x720の固定ピクセル空間で組み立て、外側だけをスケールする。
 * 将来の動画書き出し(ffmpeg/canvas)でDOMと座標を1:1で移植できるようにするため。
 */

:root {
  /* 近未来/Matrix風の緑黒テーマ。元はピンク基調だった配色をここで一括管理する
     （個別要素へのハードコードを増やさず、この変数系を拡張する方針）。 */
  --bg-0: #050b08;             /* ページ最背面・ステージの既定背景 */
  --panel-bg: #0d1f15;
  --panel-border: #1e3d2a;
  --accent: #22e07a;
  --accent-soft: rgba(34, 224, 122, .14);
  --grid-line: rgba(34, 224, 122, .14);
  --glow: rgba(34, 224, 122, .55);
  --ink: #d7ffe4;
  --muted: #7fae8f;
  --danger: #ff6b76;
  color-scheme: dark;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background: radial-gradient(ellipse 120% 60% at 50% -10%, #0f2b1c 0%, var(--bg-0) 55%, #010302 100%);
  color: var(--ink);
  font-family: "Hiragino Sans", "Yu Gothic UI", "Noto Sans JP", system-ui, sans-serif;
  padding: 24px 16px 60px;
}

.page {
  max-width: 1280px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

header.hero {
  text-align: center;
}
header.hero h1 {
  font-size: 28px;
  margin: 0 0 4px;
}
header.hero p {
  margin: 0;
  color: var(--muted);
  font-size: 14px;
}
/* 「ゆっくり動画風トークツール」(全角13文字×28px=364px)は、bodyのpadding
   (左右16pxずつ)を引いたiPhone主要機種の利用幅(343〜398px)を超えることが
   あり、2行目に最後の1文字だけが落ちる(2026-10-21、iPhone SE3/iOS 18.7.2の
   実機で確認)。430px未満の狭い画面だけ文字サイズを下げて1行に収める。 */
@media (max-width: 430px) {
  header.hero h1 {
    font-size: 22px;
  }
}

/* ---------- ステージ ---------- */

.stage-outer {
  container-type: inline-size;
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 20px;
  overflow: hidden;
  box-shadow: 0 18px 44px rgba(0, 0, 0, .6), 0 0 60px rgba(34, 224, 122, .12);
  position: relative;
}

.stage {
  position: absolute;
  top: 0;
  left: 0;
  width: 1280px;
  height: 720px;
  transform-origin: top left;
  /* scale()は<number>（無単位の比率）しか受け付けない。calc(100cqw / 1280)は
   * 長さ÷無単位数なので結果は<length>になり、scale()に渡すとこの宣言自体が
   * パース時に無効として丸ごと捨てられる（＝スケールが一切効かず、.stageが
   * 常に素の1280x720のまま.stage-outerの左上に出てしまう）。
   * tan(atan2(a, b))は2つの長さの比を無単位数として返せる数少ない方法で、
   * ここでは「外枠の幅(100cqw) ÷ 1280px」を無単位の倍率として得るために使う。
   * 通常表示で.page(max-width:1280px)により外枠がちょうど1280pxになる環境
   * （幅1312px以上のウィンドウ）では見た目はscale(1)と変わらないが、それより
   * 狭いウィンドウや録画モード（外枠が画面いっぱいに広がる）では、この修正が
   * ないとステージの右・下が切れたり左上に小さく描かれたりする。
   *
   * ⚠️ 2026-10-21: WebKit(iOS Safariを含む)ではこの式が評価に失敗し、
   * scale(0)（舞台が一点に潰れる）または宣言自体が無効化（.stageが
   * 1280x720の原寸のまま.stage-outerの左上に居座り、overflow:hiddenで
   * キャラ・字幕までクリップされる）のどちらかになる罠が実機(iPhone SE3/
   * iOS 18.7.2)で発覚した。そのため、この指定はもはや「正」ではなく
   * Chromium系ブラウザ向けのフォールバック/初期描画でしかない。実際の
   * スケールはplayer.jsのsyncStageScale()が.stage-outerの実測幅から
   * 常にインラインで上書きする（ページ初期化時・リサイズ・画面回転・
   * 全画面切替のたびに再計算）。ここを変更する場合もsyncStageScale()と
   * 同じ1280基準を保つこと。 */
  transform: scale(tan(atan2(100cqw, 1280px)));
  overflow: hidden;
}

.stage-bg {
  position: absolute;
  inset: 0;
  background-color: var(--bg-0);
  background-size: cover;
  background-position: center;
  transition: background-color .4s ease;
}

/* ---------- ステージFX（@bg matrix/grid プリセット専用の演出レイヤー） ---------- */
/* .stage-bgの直後・#charLayerより前に置くことで、z-indexを一切使わずDOM順だけで
   「背景の上・キャラの下」に挟まる（このファイルはどこにもz-indexが無い設計のため、
   ここだけ例外を作らないよう位置だけで奥行きを表現する）。
   data-bg-preset属性が無いとき(色/画像背景)は常に非表示。 */
.stage-fx {
  display: none;
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}
.stage[data-bg-preset="matrix"] .stage-fx,
.stage[data-bg-preset="grid"] .stage-fx {
  display: block;
}
/* gridは素のワイヤーフレームのみ。光の筋・地平線の発光・走査線はmatrix専用にする
   （READMEで「grid=床のグリッドのみ」と約束しているため、ここで実際にその通りにする）。 */
.stage[data-bg-preset="grid"] .fx-horizon,
.stage[data-bg-preset="grid"] .fx-rain,
.stage[data-bg-preset="grid"] .fx-scanlines {
  display: none;
}

/* @bg none = 背景なし。旧@openingが口上中に固定していた見た目
   （アイキャッチと同系のグラデーションだけ・床やFXは出さない）と同じものを、
   通常の@bgプリセットの1つとして再現する。--ec-*はeyecatch.cssの:root由来
   （index.htmlがeyecatch.cssも読み込んでいるので参照できる。カスタムプロパティは
   CSSの記述順に関係なく解決されるため、player.css→eyecatch.cssの読み込み順でも
   問題ない）。.stage-fxは上のセレクタでmatrix/grid以外は元から非表示のまま。 */
.stage[data-bg-preset="none"] .stage-bg {
  background-image: linear-gradient(180deg,
    var(--ec-bg-top) 0%, var(--ec-bg-mid) 52%, var(--ec-bg-bottom) 100%);
}

/* ---------- ステージ差し込み画像（@image。ニュース写真等を上部に差し込む） ---------- */
/* #stageFxの直後・#charLayerより前に置くことで、z-indexを一切使わずDOM順だけで
   「背景・FXの上・キャラの下」に挟まる（.stage-fxと同じ位置指定の考え方。
   このファイルはどこにもz-indexが無い設計のため、ここだけ例外を作らない）。
   キャラが画像の手前に重なる、ゆっくり実況の定番レイアウトを再現する。 */
.stage-image {
  position: absolute;
  left: 240px;
  top: 32px;
  width: 800px;
  height: 468px;
  pointer-events: none;
}
/* <img>を2枚重ねて、opacityの出し分けだけで画像の切替をクロスフェードさせる
   （1要素のbackground-imageでは瞬時切替しかできない。.stage-bgのコメント参照）。
   applyStageImage()（player.js）が空いている方に新しいsrcを入れてis-visibleを
   付け替える。img.spriteと同じ「表示はopacityのみ、transitionはopacityのみ」方針。 */
.stage-image-slot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  transition: opacity .35s ease;
  /* 黒フチ＋影で「差し込んだ写真」感を出す（参考: ゆっくり実況の定番レイアウト）。
     元は白フチだったが、挿絵生成が3:2固定でステージ枠(約1.71:1)と比率が合わず
     左右に白帯が出るのが不自然だったため黒に変更（#fff→#000。2026-10-18）。 */
  background: #000;
  border: 6px solid #000;
  border-radius: 6px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
}
.stage-image-slot.is-visible {
  opacity: 1;
}

/* ---------- アイキャッチ(@eyecatch) ---------- */
/* #charLayerの直前・.stage-imageの直後に置く（既存方針どおりDOM順だけで
   奥行きを表す。字幕バー(#subtitleBar)は#charLayerより後ろ=手前にあるため
   このレイヤーで隠れることは無い）。@eyecatchの無い台本では<div hidden>の
   ままなので、このレイヤーは常に非表示＝従来の見た目に一切影響しない。
   中身(.ec-shake以下)はeyecatch.jsのwindow.Eyecatch.create()が実行時に
   生成する（アイキャッチ単独ページ/eyecatchと同じシーン実装を再利用）。
   暗転・クロスフェードの実際の秒数はplayer.js側でel.animate()を使い
   cue.eyecatch由来の値(blackoutSec/fadeSec)を直接渡して駆動するため、
   ここには固定のtransition値を書かない（CSSの固定値とJSの値がズレて
   見た目と内部タイミングが食い違う事故を避けるため）。 */
.opening-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.opening-scene {
  position: absolute;
  inset: 0;
}

/* 0. canvasで描く本メッシュ(wireframe.js)。壁と床を1枚の連続した線として描き、
   縦線・横線の両方をサイン波でうねらせる（CSSのrepeating-linear-gradientは
   直線しか描けず、これができなかったため canvas化した。経緯はdoc/changelog.md参照）。
   canvasが使えない環境ではJS側がctxをNoneのまま返すので絵は出ず、
   代わりに下の1〜2(.fx-floor/.fx-wall)の静的フォールバックがそのまま見える。 */
#stageWireframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
/* wireframe.jsがcanvasの描画に成功すると親(.stage-fx)へこのクラスを付け、
   二重に見えないよう旧CSS格子(.fx-floor/.fx-wall)を隠す。
   display:noneにする理由（2026-10-20変更・元はopacity:0だった）: opacity:0は
   「見えない」だけで、中の@keyframesアニメーション(fx-plane-sway/fx-grid-flow/
   fx-wave-ripple、計15本・うち12本はpreserve-3d内のtranslateZ)は止まらず、
   見えないままGPUを回し続けていた。wireframe.js側は口パクの60fpsループに
   干渉しないようわざわざ30fpsに間引いているのに、置き換えたはずのCSS版が
   下で無駄に動き続けていた実害を解消する。display:noneなら要素自体が
   レンダーツリーから外れ、アニメーションも含めて完全に止まる。 */
.stage-fx.wireframe-active .fx-floor,
.stage-fx.wireframe-active .fx-wall {
  display: none;
}

/* 1. 床(CSSフォールバック): 透視の器 → ゆっくり揺れる平面 → 流れる格子 + 波打つリブ、の3層構造。
   transformは1要素につき1つの合成値しか持てない（既存の.bob/.talkを分離している
   のと同じ理由）ため、役割ごとに要素を分けている。
   動きはすべてtransform/opacityのみ＝コンポジタ合成で完結し、レイアウト/ペイントの
   再計算が発生しない（口パクの60fpsレンダーループに干渉しないための最重要制約）。 */
.fx-floor {
  position: absolute;
  left: -60%;
  right: -60%;
  bottom: 0;
  height: 62%;
  perspective: 520px;
  overflow: hidden;
}
.fx-floor-plane {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  transform-origin: bottom center;
  transform: rotateX(74deg);
  animation: fx-plane-sway 22s ease-in-out infinite;
}
@keyframes fx-plane-sway {
  0%, 100% { transform: rotateX(74deg) rotateZ(0deg); }
  50%      { transform: rotateX(76.5deg) rotateZ(.6deg); }
}
.fx-floor-grid {
  position: absolute;
  left: 0;
  right: 0;
  top: -100%;
  bottom: -100%;
  background-image:
    repeating-linear-gradient(90deg, var(--grid-line) 0 1px, transparent 1px 40px),
    repeating-linear-gradient(0deg, var(--grid-line) 0 1px, transparent 1px 40px);
  background-size: 40px 40px;
  animation: fx-grid-flow 14s linear infinite;
}
/* 40px = タイル1マス分ちょうど動かしてループさせる。パターンが40px周期なので
   40px動いた状態と動いていない状態が見分けつかず、継ぎ目なく無限スクロールになる。 */
@keyframes fx-grid-flow {
  from { transform: translateY(0); }
  to   { transform: translateY(40px); }
}
.fx-wave {
  position: absolute;
  inset: 0;
}
.fx-wave span {
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--grid-line);
  animation: fx-wave-ripple 9s ease-in-out infinite;
}
/* 12本を等間隔(0%〜100%)に配置し、位相を-0.75sずつずらして持たせる
   （既存の--bob-delayでキャラのゆらぎをずらしているのと同じ考え方）。
   1周期9s ÷ 12本 = 0.75sなので、ちょうど1周期分で全本に位相が行き渡り、
   奥から手前へ波が連続して進んでいるように見える＝「ウネウネ」の本体。 */
.fx-wave span:nth-child(1)  { top: 0%;      animation-delay: 0s; }
.fx-wave span:nth-child(2)  { top: 9.09%;   animation-delay: -.75s; }
.fx-wave span:nth-child(3)  { top: 18.18%;  animation-delay: -1.5s; }
.fx-wave span:nth-child(4)  { top: 27.27%;  animation-delay: -2.25s; }
.fx-wave span:nth-child(5)  { top: 36.36%;  animation-delay: -3s; }
.fx-wave span:nth-child(6)  { top: 45.45%;  animation-delay: -3.75s; }
.fx-wave span:nth-child(7)  { top: 54.55%;  animation-delay: -4.5s; }
.fx-wave span:nth-child(8)  { top: 63.64%;  animation-delay: -5.25s; }
.fx-wave span:nth-child(9)  { top: 72.73%;  animation-delay: -6s; }
.fx-wave span:nth-child(10) { top: 81.82%;  animation-delay: -6.75s; }
.fx-wave span:nth-child(11) { top: 90.91%;  animation-delay: -7.5s; }
.fx-wave span:nth-child(12) { top: 100%;    animation-delay: -8.25s; }
/* translateZ: rotateXされた平面の中では「床から垂直に持ち上がる」方向になるため、
   床全体が波打つ立体的な動きになる（preserve-3dが必要）。26pxは調整しやすいよう
   ここ1箇所だけに書く。 */
@keyframes fx-wave-ripple {
  0%, 100% { transform: translateZ(0); }
  50%      { transform: translateZ(26px); }
}

/* 2. 奥の壁グリッド。静的・極薄。 */
.fx-wall {
  position: absolute;
  inset: 0 0 38% 0;
  background-image:
    repeating-linear-gradient(90deg, rgba(34, 224, 122, .07) 0 1px, transparent 1px 64px),
    repeating-linear-gradient(0deg, rgba(34, 224, 122, .07) 0 1px, transparent 1px 64px);
}

/* 3. 地平線の発光ライン。wireframe.jsのCONFIG.horizonY(210px/720px=29.2%)と
   位置を揃え、メッシュの消失点に自然に重なるようにしている。
   実測(スクリーンショット)で心電図のように主張が強すぎたため、旧来の二重box-shadow
   (40px+120pxの広がり、不透明度.65〜1)から、単一で控えめな発光へ弱めた。 */
.fx-horizon {
  position: absolute;
  left: 0;
  right: 0;
  top: 29.2%;
  height: 1px;
  background: var(--accent);
  box-shadow: 0 0 18px 3px rgba(34, 224, 122, .35);
  animation: fx-horizon-pulse 8s ease-in-out infinite;
}
@keyframes fx-horizon-pulse {
  0%, 100% { opacity: .35; }
  50%      { opacity: .55; }
}

/* 4. 縦に流れる光の筋（Matrixの「降るコード」の簡易的な代役。本物のカタカナ降下は
   canvasが要るため見送った。理由: 60fpsの口パクレンダーループと描画が競合するのと、
   Phase4の動画書き出し(manifest→ffmpeg)が複雑になるため。必要になれば
   `@bg matrix-rain` のような別プリセットとして追加できる）。
   注記: このパターンは90deg(左右方向)のrepeating-linear-gradientなので、
   縦方向には変化が無い（縦の1本1本が上下一様な色の帯）。そのため縦方向へ
   動かしても見た目は変化しない。誤って「動いているつもり」にならないよう、
   ここは意図的にアニメーションを付けず静的な光の帯として扱う
   （実際に降らせるには縦方向にも濃淡が要り、マスク合成などが必要になるため
   別途取り組む）。 */
.fx-rain {
  position: absolute;
  inset: -20% 0;
  background-image: repeating-linear-gradient(90deg, rgba(34, 224, 122, .16) 0 2px, transparent 2px 46px);
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, black 18%, black 82%, transparent 100%);
  mask-image: linear-gradient(180deg, transparent 0%, black 18%, black 82%, transparent 100%);
}
.fx-rain-b {
  background-image: repeating-linear-gradient(90deg, rgba(34, 224, 122, .09) 0 1px, transparent 1px 71px);
}

/* 5. ビネット。四隅を落として中央のキャラへ視線を集める。 */
.fx-vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at 50% 60%, transparent 40%, rgba(0, 0, 0, .55) 100%);
}

/* 6. 走査線。薄いブラウン管風のテクスチャ。 */
.fx-scanlines {
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(180deg, rgba(0, 0, 0, .18) 0 1px, transparent 1px 3px);
  /* mix-blend-mode: multiply は削除した(2026-10-20)。縞模様の色が純粋な黒
     (rgba(0,0,0,.18))なので、multiplyブレンドの結果Co=Cb*(1-αs)は通常の
     source-over合成と数式上完全に同一になる＝見た目は1ピクセルも変わらない。
     一方でmix-blend-modeは「この要素より下のスタッキングコンテキスト全体を
     毎回再合成し直す」必要があり、下にあるキャラの揺れ(60fps)・ワイヤーフレーム
     canvas(30fps)が動くたびに舞台全面を再ブレンドさせていた。見た目を変えずに
     そのコストだけを無くすための削除。 */
  opacity: .5;
}

/* ---------- キャラクタースロット ---------- */
/* 1280x720内での座標。left/width/heightは登場人数に応じてplayer.jsが動的に計算し
   インラインstyleで設定する（固定クラスだと5〜6人時に矩形が重なるため）。 */
.char {
  position: absolute;
  bottom: 150px;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  /* 足元の緑のスポットライト。黒いボディのキャラ(グロッキー等、実測で本体輝度62/
   * 暗部80%と判明)が暗い背景に溶けて輪郭が消えるのを防ぐ静的な背景。
   * .char自身のbackgroundは常に全descendantより先に(奥に)描画されるため、
   * ::beforeにして重ねるより確実（.stageがtransformでstacking contextを作る影響で
   * 子孫にz-indexを使うと想定外の位置まで奥へ飛ぶ罠を避けられる）。
   * 毎フレームの再計算が発生しない静的グラデーションなのでコストはゼロ。
   */
  background: radial-gradient(ellipse 70% 55% at 50% 92%, rgba(34, 224, 122, .28), transparent 72%);
  /* @enter/@exitの登場・退場演出。.charはこのファイル内で唯一
   * transform/animation/opacityが未使用の階層なので(他は役割分担済み。
   * .char-scale=拡縮、.bob=浮遊、.talk=口の跳ね、.expression=表情の出し分け、
   * .sprite-layer=発話時の拡縮)、ここに足しても既存のどのtransformとも衝突しない。
   * data-onstage="false"へ変わるだけで登場・退場の両方を1つのtransitionで賄える
   * （属性が戻れば逆再生になる）。opacityと一緒に動くのでスポットライト
   * (このルール自身のbackground)も一緒に出入りする。
   */
  transition: opacity .5s ease, transform .5s cubic-bezier(.22, .9, .28, 1);
}
.char[data-onstage="false"] {
  opacity: 0;
  /* 34pxはstage自身の overflow:hidden より内側で切れない範囲かつ、
   * 字幕バー(bottom:28px, 高さ116px = 上端144px)の裏に潜る量。
   * .charはbottom:150pxなので、34px下げると字幕バーの裏へちょうど隠れ、
   * 「字幕の裏からせり上がってくる」見た目になる。
   */
  transform: translateY(34px) scale(.97);
  pointer-events: none;
}

.char .bob {
  width: 100%;
  height: 100%;
  transform-origin: 50% 60%;
  animation: bob var(--bob-dur, 3.6s) ease-in-out infinite;
  animation-delay: var(--bob-delay, 0s);
  /* will-change: transform は外した(2026-10-20)。transformを無限アニメーション
     させている要素はブラウザが自動でコンポジタレイヤー化するため、この指定は
     効果を生まずGPUメモリ(キャラ1体あたり概算4MB相当のテクスチャ)だけを
     常時確保し続けていた。 */
}
@keyframes bob {
  0%, 100% { transform: translateY(4px) rotate(-1deg); }
  50%      { transform: translateY(-8px) rotate(1deg); }
}

/* talkはbobの子。1要素に2アニメを重ねるとtransformが後勝ちで衝突するため分離する。 */
.char .talk {
  width: 100%;
  height: 100%;
  transition: transform 90ms ease-out;
}
.char[data-mouth-style="bounce"][data-viseme="open"] .talk  { transform: translateY(-12px); }
.char[data-mouth-style="bounce"][data-viseme="half"] .talk  { transform: translateY(-6px); }
.char[data-mouth-style="bounce"][data-viseme="closed"] .talk{ transform: translateY(0); }

/* 表情(絵柄)を切り替える要素。.talkの子として表情の数だけ並列に存在し、
 * player.jsのapplyEmotion()がdisplay:none/blockで1つだけ表示する
 * （このコメントは以前から「表情切替は.expression系のクラスを使うこと」と
 * 予告していた箇所。img.spriteのtransition:noneと同じ理由で、表情の切替も
 * クロスフェードさせず瞬時に行う）。.char[data-emotion]は将来の表情別演出
 * （発光色を変える等）のためのフックとして常に最新の表情IDを持つ。 */
.char .expression {
  width: 100%;
  height: 100%;
}

.char .sprite-layer {
  position: relative;
  width: 100%;
  height: 100%;
}
.char img.sprite {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  /* 口フレーム(closed/half/open/blink)はROI外が完全同一ピクセルになるよう作られている
   * ため、クロスフェードさせる意味がない。むしろ遷移中は新旧2枚が半透明で重なり、
   * 合成後のアルファが最大25%落ちて背景が透けて見える（チラツキの主因だった）ので、
   * 瞬時に差し替える。表情(絵柄)を切り替える要素は別途 .expression 系のクラスを使うこと。
   */
  transition: none;
}
.char img.sprite.is-visible { opacity: 1; }
.char.flip-x .sprite-layer { transform: scaleX(-1); }

/* 2026-10-20: 常時filterを使うのをやめ、opacity/transformだけにした
   (brightness/saturateのfilterをopacityで近似、drop-shadowは削除)。
   filterは要素を独立したレンダリングサーフェスにしてコンポジタの高速パスを
   外すため、祖先の.bobが常時transformアニメーション中のこのスプライトに
   毎フレームの再ラスタライズを強制していた。しかもdata-speaking="false"が
   既定なので、舞台に出ている最大6体に常時掛かっていた。opacity/transformは
   どちらもコンポジタだけで完結し、祖先の再ラスタライズを強制しない。
   saturate(.85)の彩度変化やdrop-shadowの影(アルファ.18で元々薄い)は
   再現していない近似だが、暗さ(opacity)と大きさ(scale)の差だけでも
   「喋っているキャラが際立つ」見た目は保たれる。 */
.char[data-speaking="false"] .sprite-layer {
  opacity: .82;
  transform: scale(.94);
  transition: opacity .25s ease, transform .25s ease;
}
.char[data-speaking="true"] .sprite-layer {
  opacity: 1;
  transform: scale(1);
  transition: opacity .25s ease, transform .25s ease;
}
.char.flip-x[data-speaking="false"] .sprite-layer { transform: scaleX(-1) scale(.94); }
.char.flip-x[data-speaking="true"] .sprite-layer  { transform: scaleX(-1) scale(1); }

@media (prefers-reduced-motion: reduce) {
  .char .bob { animation: none; }
  .char .talk { transition: none; }
  .char { transition: none; }
  .fx-floor-plane, .fx-floor-grid, .fx-wave span, .fx-horizon { animation: none; }
  .stage-image-slot { transition: none; }
}

/* ---------- ステージの遷移抑止（アイキャッチ明けの一瞬だけ付ける） ---------- */
/* player.jsのwithStageTransitionsOff()が「クラスを付ける→DOMを書き換える→
   強制リフローで確定させる→クラスを外す」を1タスク内で同期的に行うためのクラス。
   このクラスが付いた状態で画面が描画されることは無く、リフロー時点の
   after-change style に transition-duration:0 を見せることでトランジションの
   発火自体を抑止する（buildStage()が「生成直後の要素にはtransitionが掛からない」
   ことを利用しているのと同じ意図を、既存の要素に対して実現する手段）。

   .char .sprite-layer は .char[data-speaking="true"] .sprite-layer (0,3,0) と
   詳細度が並ぶため、.char を挟んで (0,4,0) にし、かつこのブロックを
   prefers-reduced-motion の後ろに置く（どちらか片方だけだと発話ハイライトの
   0.25秒だけが抑止されずに残る）。
   .stage-image-slot は applyStageImage() が img.onload 内で非同期に is-visible を
   付けるため、ここで抑止できるのは「画像を消す」同期側の分岐だけ。
   .talk（口パクの90ms）と img.sprite（元から transition:none）は対象外。 */
.stage.is-stage-instant .char,
.stage.is-stage-instant .char .sprite-layer,
.stage.is-stage-instant .stage-bg,
.stage.is-stage-instant .stage-image-slot {
  transition: none;
}

/* ---------- 字幕バー ---------- */

.subtitle-bar {
  position: absolute;
  left: 40px;
  right: 40px;
  bottom: 28px;
  min-height: 116px;
  padding: 16px 26px;
  border-radius: 14px;
  background: rgba(12, 10, 16, .62);
  border: 3px solid rgba(34, 224, 122, .7);
  box-shadow: 0 0 24px rgba(34, 224, 122, .35);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
}
/* アイキャッチ演出中だけ付与される(player.jsのsetSubtitleBarHidden()参照)。
   #subtitleBarは#charLayerより後ろ=常に最前面なので、テキストを空にするだけでは
   枠線・背景・影が落下する6体の上に出っぱなしになってしまう。opacityと
   visibilityの両方を0にすることで、枠ごと完全に見えなくする
   （displayは使わない。レイアウト計算への影響を避け、復帰時に即座に戻せるため）。 */
.subtitle-bar.is-hidden {
  opacity: 0;
  visibility: hidden;
}
.subtitle-name {
  font-size: 24px;
  font-weight: 800;
  color: var(--subtitle-name-color, #fff);
}
/* 文字色(--subtitle-fill)・縁色(--subtitle-outline)はplayer.jsのupdateSubtitle()が
   #subtitleBarへキャラクターごとに設定する（.char の --bob-dur と同じ既存パターン。
   このファイル自身は既定値(白文字+#12101aの縁=変更前と同じ見た目)だけを持ち、
   キャラ固有の値はJS側の責務にする）。 */
.subtitle-text {
  font-size: 36px;
  font-weight: 800;
  line-height: 1.35;
  color: var(--subtitle-fill, #fff);
  letter-spacing: .01em;
  -webkit-text-stroke: var(--subtitle-stroke-w, 7px) var(--subtitle-outline, #12101a);
  paint-order: stroke fill;
  margin: 0;
}
@supports not (paint-order: stroke) {
  .subtitle-text {
    -webkit-text-stroke: 0;
    text-shadow:
      -3px -3px 0 var(--subtitle-outline, #12101a), 3px -3px 0 var(--subtitle-outline, #12101a),
      -3px  3px 0 var(--subtitle-outline, #12101a), 3px  3px 0 var(--subtitle-outline, #12101a),
      -3px  0   0 var(--subtitle-outline, #12101a), 3px  0   0 var(--subtitle-outline, #12101a),
      0    -3px 0 var(--subtitle-outline, #12101a), 0    3px 0 var(--subtitle-outline, #12101a);
  }
}
/* ルビ(<rt>)。本文の-webkit-text-stroke:7pxをそのまま継承すると、rtの小さい
   フォントサイズでは輪郭が文字自体を塗りつぶしてしまうため、細く縮小して上書きする。
   色(--subtitle-outline)は本文から継承するので幅だけ上書きする。 */
.subtitle-text rt {
  font-size: .42em;
  font-weight: 700;
  -webkit-text-stroke-width: 2.5px;
  line-height: 1.3;
}
@supports not (paint-order: stroke) {
  .subtitle-text rt {
    text-shadow:
      -1px -1px 0 var(--subtitle-outline, #12101a), 1px -1px 0 var(--subtitle-outline, #12101a),
      -1px  1px 0 var(--subtitle-outline, #12101a), 1px  1px 0 var(--subtitle-outline, #12101a);
  }
}
.subtitle-idle {
  color: rgba(255,255,255,.55);
  font-size: 15px;
}

/* ---------- 録画用の黒幕(.stage-curtain) ---------- */
/* .stageの最後の子＝このファイルの「z-indexを使わずDOM順だけで奥行きを表す」
 * 方針上、常に最前面(#subtitleBarより手前)に来る。通常表示ではopacity:0+
 * visibility:hiddenで完全に無害（クリックも奪わないようpointer-events:noneも
 * 付けておく）。録画の準備中(共有ダイアログ〜プリロール)だけplayer.jsが
 * is-visibleを付けて下ろし、再生する最初の映像が確定してから上げる
 * （player.jsのshowStageCurtain/liftStageCurtain参照）。フェードの秒数は
 * ここでは固定せず、呼び出し元がel.animate()へ渡す（アイキャッチの
 * blackoutSec/fadeSecと同じく「秒数はJS側の1箇所にだけ持つ」方針）。 */
.stage-curtain {
  position: absolute;
  inset: 0;
  background: #000;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}
.stage-curtain.is-visible {
  opacity: 1;
  visibility: visible;
}

/* ---------- コントロール ---------- */

.panel {
  background: var(--panel-bg);
  border: 1px solid var(--panel-border);
  border-radius: 16px;
  padding: 18px 20px;
}

.controls-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}
.controls-row button {
  border: none;
  border-radius: 999px;
  padding: 10px 18px;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  background: var(--accent);
  color: #03150c;
  transition: opacity .15s ease, transform .1s ease;
}
.controls-row button:hover { opacity: .9; }
.controls-row button:active { transform: scale(.96); }
.controls-row button.secondary { background: var(--accent-soft); color: var(--ink); }
.controls-row button:disabled { opacity: .4; cursor: not-allowed; }

.controls-row select {
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
  color: var(--ink);
  font-size: 14px;
}

/* 台本ライブラリのアップロード欄。既定のUAスタイルはダークテーマに合わないため、
 * 「参照」ボタン(::file-selector-button)だけ.controls-row button.secondaryに寄せる。
 * ファイル名テキスト自体の色は継承されないブラウザがあるためcolorを明示する。 */
.controls-row input[type="file"] {
  color: var(--ink);
  font-size: 13px;
  max-width: 220px;
}
.controls-row input[type="file"]::file-selector-button {
  border: none;
  border-radius: 999px;
  padding: 8px 14px;
  margin-right: 10px;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
  background: var(--accent-soft);
  color: var(--ink);
  transition: opacity .15s ease;
}
.controls-row input[type="file"]::file-selector-button:hover { opacity: .9; }

.progress-line {
  font-size: 13px;
  color: var(--muted);
  margin-left: auto;
}

.script-editor textarea {
  width: 100%;
  min-height: 220px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 14px;
  line-height: 1.6;
  padding: 14px;
  border-radius: 12px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
  color: var(--ink);
  resize: vertical;
}

.script-list {
  max-height: 220px;
  overflow-y: auto;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
}
.script-list .cue-row {
  padding: 8px 12px;
  font-size: 13px;
  border-bottom: 1px solid var(--panel-border);
  cursor: pointer;
  display: flex;
  gap: 8px;
}
.script-list .cue-row:hover { background: rgba(34, 224, 122, .08); }
.script-list .cue-row.active { background: rgba(34, 224, 122, .18); font-weight: 700; }
.script-list .cue-row .cue-speaker { color: var(--accent); min-width: 90px; }

.status-line {
  font-size: 13px;
  color: var(--muted);
}
.status-line.error { color: var(--danger); }

.warnings {
  font-size: 12px;
  color: #ffcf7d;
  background: rgba(255, 180, 60, .12);
  border: 1px solid rgba(255, 180, 60, .35);
  border-radius: 10px;
  padding: 8px 12px;
}

.two-col {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: 16px;
}
@media (max-width: 860px) {
  .two-col { grid-template-columns: 1fr; }
}

/* ---------- 画像再生成パネル ---------- */

.image-restyle-panel textarea,
.image-restyle-panel input[type="text"] {
  width: 100%;
  font-size: 14px;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
  color: var(--ink);
  font-family: inherit;
  resize: vertical;
}
/* 既知の接尾辞を選んでいる間はプロンプトを編集不可にする（applyImageRestylePresetAutofill
 * 参照）。disabled中でも読める程度のコントラストは保ちつつ、触れないことが分かるようにする。 */
.image-restyle-panel textarea:disabled {
  opacity: .7;
  cursor: not-allowed;
}

.image-restyle-table {
  margin-top: 10px;
  max-height: 260px;
  overflow: auto;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
}
.image-restyle-table table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.image-restyle-table th,
.image-restyle-table td {
  text-align: left;
  padding: 6px 10px;
  border-bottom: 1px solid var(--panel-border);
  white-space: nowrap;
}
.image-restyle-table td:last-child {
  white-space: normal;
  color: var(--muted);
}
.image-restyle-table th {
  color: var(--muted);
  font-weight: 700;
  position: sticky;
  top: 0;
  background: var(--panel-bg);
}
.image-restyle-table tr.restyle-status-conflict td,
.image-restyle-table tr.restyle-status-unresolved td,
.image-restyle-table tr.restyle-status-unsupported_origin td {
  color: var(--danger);
}
.image-restyle-table tr.restyle-status-generate td:nth-child(3) {
  color: var(--accent);
  font-weight: 700;
}
.image-restyle-table tr.restyle-status-skip td {
  color: var(--muted);
  font-style: italic;
}

/* ---------- 挿絵の自動生成パネル ---------- */

.illust-autogen-table {
  margin-top: 10px;
  max-height: 420px;
  overflow: auto;
  border: 1px solid var(--panel-border);
  border-radius: 12px;
}
.illust-autogen-table table {
  width: 100%;
  border-collapse: collapse;
  font-size: 13px;
}
.illust-autogen-table th,
.illust-autogen-table td {
  text-align: left;
  padding: 6px 8px;
  border-bottom: 1px solid var(--panel-border);
  vertical-align: top;
}
.illust-autogen-table th {
  color: var(--muted);
  font-weight: 700;
  position: sticky;
  top: 0;
  background: var(--panel-bg);
  /* スクロール時、下の行にある<select>（登場キャラのプルダウン）がヘッダの
     背景を透かして重なって見える不具合の直し。スクロール領域内の他の要素より
     確実に手前に出すため、sticky要素にz-indexを明示する。 */
  z-index: 2;
}
.illust-autogen-table tr.illust-kind-existing td {
  color: var(--muted);
  font-style: italic;
}
.illust-autogen-table tr.illust-not-adopted td {
  opacity: .55;
}
.illust-autogen-table textarea,
.illust-autogen-table input[type="text"],
.illust-autogen-table input[type="number"] {
  width: 100%;
  box-sizing: border-box;
  font-family: inherit;
  font-size: 12px;
}
.illust-autogen-table textarea {
  min-height: 42px;
  resize: vertical;
}
.illust-autogen-overlay-textarea {
  /* 「絵に入れる文字」欄が狭く、長い文字が途中で切れて見える不具合の直し。
     折り返して複数行で全文を表示できるようにし、最小幅も確保する。 */
  min-width: 140px;
  min-height: 48px;
  white-space: pre-wrap;
  word-break: break-word;
}
.illust-autogen-char-grid {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 260px;
}
.illust-autogen-char-rows {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.illust-autogen-char-row {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
}
.illust-autogen-char-row select {
  font-size: 12px;
}
/* サイクル4.2: 実際に挿絵生成へ添付される立ち絵のサムネイル。姿未選択時は
   hidden属性で非表示（レイアウト上の高さは確保しない）。 */
.illust-autogen-char-thumb {
  width: 28px;
  height: 28px;
  object-fit: contain;
  border-radius: 4px;
  border: 1px solid var(--panel-border);
  background: #fff;
  flex-shrink: 0;
}
.illust-autogen-role-input {
  flex: 1 1 auto;
  min-width: 80px;
  font-size: 12px;
  box-sizing: border-box;
}
.illust-autogen-char-row button.secondary {
  flex: 0 0 auto;
  padding: 2px 6px;
  line-height: 1;
}
.illust-autogen-char-add-btn {
  align-self: flex-start;
  font-size: 12px;
  padding: 2px 10px;
}
/* サイクル4.2: 「参考画像N枚＝共通X＋キャラY」のサマリ行。上限超過時は
   .illust-refs-status等と同じ.errorクラスの流用で赤字にする。 */
.illust-ref-summary {
  margin-top: 2px;
}
.illust-ref-summary.error {
  color: var(--danger);
}
.illust-autogen-warnings {
  color: var(--danger);
  font-size: 11px;
  margin-top: 2px;
}

/* ---------- 挿絵の絵（サイクル3.7: サムネ・版プルダウン・作り直し） ---------- */

.illust-autogen-image-cell {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 4px;
  min-width: 96px;
}
.illust-autogen-thumb {
  width: 88px;
  height: auto;
  max-height: 88px;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid var(--border);
  display: block;
}
.illust-autogen-version-select {
  font-size: 11px;
  padding: 1px 2px;
}

/* ---------- 絵柄プリセット（サイクル4.3: 絵柄・構図・禁止事項の編集・登録・選択） ---------- */
.illust-style-panel {
  margin-top: 8px;
  padding: 10px 12px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.02);
}
.illust-style-panel select {
  padding: 6px 8px;
  border-radius: 8px;
  font-size: 13px;
}
.illust-style-panel textarea {
  width: 100%;
  box-sizing: border-box;
  font-family: inherit;
  font-size: 13px;
  resize: vertical;
  margin-top: 2px;
}
.illust-style-panel label {
  display: block;
  margin-top: 6px;
}
.illust-style-details-toggle {
  margin-top: 8px;
  font-size: 12px;
  padding: 3px 10px;
  display: inline-block;
}
#illustStyleDetails {
  margin-top: 4px;
}

/* ---------- 共通の参考画像の一覧（サイクル4.1: 追加・差し替え・外す） ---------- */
.illust-refs-list {
  margin-top: 6px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.illust-ref-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 8px;
  border: 1px solid var(--panel-border);
  border-radius: 10px;
}
.illust-ref-row.illust-ref-inactive {
  opacity: .55;
}
.illust-ref-thumb {
  width: 88px;
  height: auto;
  max-height: 88px;
  object-fit: cover;
  border-radius: 4px;
  border: 1px solid var(--panel-border);
  display: block;
  flex-shrink: 0;
}
.illust-ref-info {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  word-break: break-all;
}
.illust-ref-actions {
  display: flex;
  gap: 6px;
  flex-shrink: 0;
}
/* .controls-rowの外（行単位）に置くボタンなので、.controls-row buttonの
   見た目（角丸ピル・配色）をここで明示的に再現する（既存方針どおり、
   ハードコードせずテーマ変数を使う）。 */
.illust-ref-actions button {
  border: none;
  border-radius: 999px;
  padding: 4px 12px;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
  background: var(--accent-soft);
  color: var(--ink);
  transition: opacity .15s ease, transform .1s ease;
}
.illust-ref-actions button:hover { opacity: .9; }
.illust-ref-actions button:active { transform: scale(.96); }
.illust-autogen-image-cell button.secondary {
  font-size: 11px;
  padding: 2px 8px;
}

/* ---------- 声の切り替えパネル ---------- */

.voice-hint {
  font-weight: 400;
  font-size: 12px;
  color: var(--muted);
}

.voice-panel select {
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
  color: var(--ink);
  font-size: 14px;
}
.voice-panel #voiceSelect { min-width: 240px; }

.voice-speed-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 0 10px;
  font-size: 13px;
  color: var(--muted);
}
.voice-speed-row input[type="range"] { flex: 1; max-width: 260px; }
.voice-speed-row span { min-width: 48px; font-variant-numeric: tabular-nums; }
/* .voice-speed-rowのdisplay:flexは author-normal で [hidden] の UA-normal な
   display:none より優先度(origin)が高く、hidden属性だけでは隠れない。
   声パネルがgemini/gpt_audio選択時に話速行を隠すため、明示的に上書きする。 */
.voice-speed-row[hidden] { display: none; }

.voice-instructions-label {
  display: block;
  font-size: 13px;
  color: var(--muted);
  margin-bottom: 4px;
}
.voice-panel textarea,
.readings-panel textarea {
  width: 100%;
  font-family: inherit;
  font-size: 14px;
  line-height: 1.5;
  padding: 10px 12px;
  border-radius: 10px;
  border: 1px solid var(--panel-border);
  background: var(--panel-bg);
  color: var(--ink);
  resize: vertical;
}

/* ---------- 録画モード(is-recording) ---------- */
/* 「録画して再生」中は見出し・操作パネルを全部隠し、.stage-outerを画面いっぱいへ
 * 広げる（placeholder/eyecatch.htmlの?clean=1 と同じ考え方）。.stage自身は
 * 既存どおり「外枠の幅(100cqw) ÷ 1280px」でスケールするため(このファイル冒頭の
 * .stageのコメント参照)、ここで.stage-outerの幅・高さを変えるだけでステージ内部
 * のCSS・アニメーションには一切手を入れずに済む。録画対象はplaceholder/assets/
 * player.jsがgetDisplayMedia+Region Capture(CropTarget)で.stage-outerだけへ
 * 切り出すため、ここでの見た目の変化(UIが消える・画面いっぱいになる)がそのまま
 * 録画結果になる＝.stage-outerの矩形と中身(.stage)の矩形が常にぴったり一致して
 * いないと、録画にそのまま黒帯や余白が入ってしまう点に注意。
 */
body.is-recording {
  padding: 0;
  background: #000;
  /* 100vw/100vhが実ビューポートと一致するよう、スクロールバー分のズレを消す
   * （録画中はどうせページ内コンテンツが.stage-outer以外ぜんぶ非表示なので
   * スクロール自体不要）。 */
  overflow: hidden;
}
body.is-recording header.hero,
body.is-recording .panel,
body.is-recording .two-col {
  display: none;
}
body.is-recording .page {
  max-width: none;
  gap: 0;
}
body.is-recording .stage-outer {
  position: fixed;
  inset: 0;
  /* widthとheightを両方とも明示して初めてmargin:autoでの上下左右中央寄せが
   * 効く(inset:0だけだとtop/bottom/left/rightが全部0に固定され、heightが
   * aspect-ratioを無視して100vhまで引き伸ばされてしまう＝録画モードで中身の
   * .stageとずれて黒帯が入っていた不具合の一因)。 */
  margin: auto;
  width: min(100vw, calc(100vh * 16 / 9));
  height: min(100vh, calc(100vw * 9 / 16));
  /* 丸角・影は録画には不要な装飾で、クロップ対象(.stage-outer)にそのまま
   * 残しておくと角や縁が録画に映り込んでしまうため、録画中だけ消す。 */
  border-radius: 0;
  box-shadow: none;
}
