/* 各畫面版面。座標用設計單位 360×640。
   cut1／cut3／cut5 用客戶 2026-09-22 交付的切圖（assets/screens、assets/frames）：
   圖上已經有字，DOM 裡的同一段文字只留給螢幕閱讀器與流程測試（.vh）。
   cut2／cut4 仍是骨架期的色塊與系統字（客戶未給切圖，docs/assets-index.md §6）。 */

:root {
  --c-paper: #f3ecdc;
  --c-red: #c8322b;
  --c-green: #3f7a44;
  --c-pill: linear-gradient(90deg, #f08a3c, #e5533a);
}

#app { background: #1d2a1f; }
#stage { background: var(--c-paper); font-family: system-ui, -apple-system, "PingFang TC", "Noto Sans TC", sans-serif; color: #2b2b2b; }

.screen {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 24px 20px;
  text-align: center;
}
.screen[hidden] { display: none; }

/* 滿版背景圖：設計稿 720×1280，舞台 360×640，直接鋪滿 */
.screen__bg { position: absolute; inset: 0; width: 100%; height: 100%; z-index: -1; }
/* 圖上已經有字時，DOM 文字只留給螢幕閱讀器（不能用 display:none，那樣讀不到） */
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* ───── 佔位 ───── */
.ph {
  position: relative;
  display: grid;
  place-items: center;
  border: 1.5px dashed rgba(0, 0, 0, .35);
  background: repeating-linear-gradient(45deg, rgba(0,0,0,.04) 0 8px, rgba(0,0,0,.08) 8px 16px);
  color: rgba(0, 0, 0, .55);
  font-size: 11px;
  line-height: 1.3;
}
.ph::before { content: attr(data-label); padding: 4px 8px; }
.ph--full { position: absolute; inset: 0; z-index: -1; border: 0; }
.ph--full::before { position: absolute; left: 6px; bottom: 4px; }

/* 本機才顯示畫面代號（/audit 截圖時辨認用） */
.is-local #stage::after {
  content: attr(data-screen) " · " attr(data-cut);
  position: absolute;
  right: 4px;
  top: 4px;
  z-index: 50;
  padding: 1px 6px;
  border-radius: 4px;
  background: rgba(0, 0, 0, .55);
  color: #fff;
  font: 10px/1.5 ui-monospace, Menlo, monospace;
  pointer-events: none;
}

/* ───── 共用元件 ───── */
.pill {
  margin: 0;
  padding: 5px 14px;
  border-radius: 999px;
  background: var(--c-pill);
  color: #fff;
  font-size: 12px;
  line-height: 1.4;
}
.btn {
  min-width: 112px;
  padding: 10px 18px;
  border: 0;
  border-radius: 999px;
  background: var(--c-green);
  color: #fff;
  font: inherit;
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
}
.btn:disabled { opacity: .4; cursor: default; }
.btn--ghost { background: #fff; color: var(--c-green); box-shadow: inset 0 0 0 2px var(--c-green); }
.btn-row { display: flex; gap: 12px; justify-content: center; }

/* ───── cut1 home ─────
   位置＝客戶切圖在設計稿 cut1（720×1280）裡的比對結果 ÷2（見 docs/assets-index.md §6 的比對方式）：
   標題 (124,99)、卡片群 (0,371)、START (170,916)。 */
.screen--home { padding: 0; }
.home__title { position: absolute; left: 62px; top: 49.5px; width: 234.5px; height: 135.5px; }
.home__cards { position: absolute; left: 0; top: 185.5px; width: 360px; height: 294px; }
.btn-start {
  position: absolute;
  left: 85px;
  top: 458px;
  width: 177px;
  height: 177px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.btn-start img { display: block; width: 100%; height: 100%; }
.btn-start:active img { scale: .97; }

/* ───── cut2 assign ─────
   背景設計稿只有米色底，沒有另外給切圖。對話框膠囊用客戶 2-loading 切圖，
   位置照設計稿 cut2（720×1280）比對 ÷2。 */
.screen--assign { padding: 0; background: var(--c-paper); }
.assign__pill { position: absolute; left: 14px; top: 45px; width: 331px; height: 149.5px; }

/* ───── cut2 光球 ─────
   對照設計稿 cut2 原圖（720×1280）：半透明玻璃球、右上弧形高光、球內左下一抹藍紫霧、
   底部亮邊、左下一道光痕、深灰動物剪影。
   位置依便利貼：上移到畫面約 53% 高、放大到約 60% 寬（原稿在 78% 高、40% 寬）。
   動畫一律用獨立的 translate／scale 屬性，不用 transform，免得浮動、進場、彈跳互相覆蓋。 */
.orb {
  position: absolute;
  left: 50%;
  top: 235px;
  width: 210px;
  height: 210px;
  margin-left: -105px;
  --silhouette-color: #5b5b5d;
  animation: orb-in .5s cubic-bezier(.2, .8, .3, 1) both;
}
.orb__float { position: absolute; inset: 0; animation: orb-float 3.2s ease-in-out infinite; }

.orb__ball {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, .8);
  background:
    radial-gradient(circle at 34% 28%, rgba(255, 255, 255, .95) 0 16%, rgba(255, 255, 255, 0) 45%),
    radial-gradient(circle at 50% 52%, #f8f9fb 0%, #eef1f5 52%, #dde2ea 80%, #cdd4df 100%);
  box-shadow:
    inset 0 -16px 26px rgba(255, 255, 255, .95),
    inset 12px 14px 28px rgba(160, 172, 196, .32),
    inset -5px -3px 10px rgba(255, 255, 255, .75),
    0 18px 30px -10px rgba(110, 95, 80, .3);
}
.orb__mist {
  position: absolute;
  left: -14%;
  bottom: 4%;
  width: 96%;
  height: 56%;
  border-radius: 50%;
  background: radial-gradient(ellipse at 42% 58%, rgba(140, 152, 214, .55), rgba(182, 172, 224, .26) 46%, transparent 72%);
  filter: blur(7px);
  animation: orb-mist 7s ease-in-out infinite alternate;
}
.orb__animal {
  position: absolute;
  left: 22%;
  top: 19%;
  width: 56%;
  height: 62%;
  background: var(--silhouette-color);
  -webkit-mask: var(--silhouette) center / contain no-repeat;
          mask: var(--silhouette) center / contain no-repeat;
  opacity: 0;
}
.orb__animal.is-on { opacity: .9; }
.orb__shine {
  position: absolute;
  inset: 6%;
  border-radius: 50%;
  border: 3px solid transparent;
  border-top-color: rgba(255, 255, 255, .95);
  border-right-color: rgba(255, 255, 255, .85);
  rotate: 16deg;
  filter: blur(.7px);
  -webkit-mask: linear-gradient(215deg, #000 28%, transparent 58%);
          mask: linear-gradient(215deg, #000 28%, transparent 58%);
}
.orb__rim {
  position: absolute;
  left: 14%;
  right: 14%;
  bottom: 1%;
  height: 24%;
  border-radius: 50%;
  background: radial-gradient(ellipse at 50% 100%, rgba(255, 255, 255, .95), transparent 70%);
}
.orb__glow {
  position: absolute;
  inset: -16%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255, 255, 255, .6) 44%, rgba(255, 226, 206, .3) 60%, transparent 72%);
  opacity: .45;
}
.orb__trail {
  position: absolute;
  left: -30%;
  bottom: -6%;
  width: 74%;
  height: 22%;
  background: radial-gradient(ellipse at 82% 50%, rgba(255, 255, 255, .95), transparent 66%);
  filter: blur(2px);
  rotate: -9deg;
  opacity: .75;
}
.orb__shadow {
  position: absolute;
  left: 16%;
  right: 16%;
  bottom: -11%;
  height: 15%;
  border-radius: 50%;
  background: radial-gradient(ellipse, rgba(105, 90, 75, .3), transparent 70%);
  filter: blur(3px);
}

/* 輪換中：霧加速、外圈光暈脈動 */
.orb.is-spinning .orb__mist { animation-duration: 1.2s; }
.orb.is-spinning .orb__glow { animation: orb-pulse .45s ease-in-out infinite alternate; }
/* 揭曉：光暈閃一下、剪影彈一下 */
.orb.is-revealed .orb__glow { animation: orb-flash .8s ease-out both; }
.orb.is-revealed .orb__animal.is-on { animation: orb-pop .55s cubic-bezier(.3, 1.6, .5, 1) both; }

@keyframes orb-in    { from { opacity: 0; scale: .82; } to { opacity: 1; scale: 1; } }
@keyframes orb-float { 50% { translate: 0 -7px; } }
@keyframes orb-mist  { to { translate: 16% -8%; rotate: 14deg; } }
@keyframes orb-pulse { to { opacity: .9; scale: 1.06; } }
@keyframes orb-flash {
  0%   { opacity: 1; scale: .9; }
  35%  { opacity: 1; scale: 1.2; background: radial-gradient(circle, rgba(255, 255, 255, .95) 44%, rgba(255, 214, 180, .55) 62%, transparent 74%); }
  100% { opacity: .6; scale: 1.06; }
}
@keyframes orb-pop   { 0% { scale: .7; } 100% { scale: 1; } }

@media (prefers-reduced-motion: reduce) {
  .orb, .orb__float, .orb__mist, .orb__glow, .orb__animal.is-on { animation: none !important; }
}

/* 只給螢幕閱讀器的文字 */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.assign__dev { display: none; margin: 0; font-size: 11px; color: #777; }
.is-local .assign__dev { display: block; }

/* ───── cut3 shoot ───── */
.screen--shoot { padding: 0; }
.shoot__canvas { position: absolute; inset: 0; width: 100%; height: 100%; background: #000; }
.shoot__timer {
  position: absolute;
  top: 64px;
  margin: 0;
  padding: 2px 10px;
  border-radius: 999px;
  background: rgba(0, 0, 0, .5);
  color: #fff;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
[data-recording="recording"] .shoot__timer { background: #d22; }
.shoot__controls { position: absolute; bottom: 16px; left: 0; right: 0; display: flex; justify-content: center; gap: 16px; }
.btn--record { background: #d22; }
.btn--stop { background: #333; }

/* ───── cut4 process ───── */
.screen--process { justify-content: center; }
.process__title { margin: 0; display: flex; flex-direction: column; font-size: 32px; color: var(--c-red); }
.process__title span { font-size: 16px; letter-spacing: .12em; color: var(--c-green); }
.process__row { width: 320px; height: 48px; }
.process__loading { margin: 0; font-weight: 700; letter-spacing: .1em; }
.process__bar { --progress: 0; width: 240px; height: 10px; border-radius: 999px; background: rgba(0,0,0,.12); overflow: hidden; }
.process__bar span { display: block; height: 100%; width: calc(var(--progress) * 100%); background: #f08a3c; transition: width .2s linear; }
.process__error { padding: 12px; border-radius: 12px; background: #fff; box-shadow: 0 4px 16px rgba(0,0,0,.15); }
.process__error p { margin: 0 0 10px; color: var(--c-red); font-size: 13px; }

/* ───── cut5 result ─────
   ⚠ 這一頁沒有完整版面稿。客戶只給散圖（4-bg／4-pic-frame／4-title／4-QR／兩顆按鈕），
   新背景與簡報裡那張 cut5 的背景不同（多了上方葉框），舊稿的座標套不上去。
   目前的排法：每個元素水平置中（以不含外光暈的實心範圍計），垂直依序留間距。
   ÷4 後的元素尺寸是客戶原圖的事實；垂直座標是我排的，**要客戶確認**（docs/assets-index.md §6）。 */
.screen--result { padding: 0; }

/* 成品預覽卡：客戶的玻璃卡切圖，影片疊在它裡面。
   內框比例與錄影畫布一致（9:16），影片 cover 不會被壓扁 */
.result__card { position: absolute; left: 95.5px; top: 44px; width: 168.5px; height: 278px; }
.result__card-frame { position: absolute; inset: 0; width: 100%; height: 100%; }
.result__card-inner {
  position: absolute;
  inset: 6px;
  overflow: hidden;
  border-radius: 6px;
  background: #e4ded4;
  display: grid;
  place-items: center;
}
.result__card-inner video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* 還沒錄（例如本機直達 ?screen=result）時藏影片、顯示佔位字 */
.result__card:not(.has-video) video { display: none; }
.result__card.has-video .result__placeholder { display: none; }
.result__placeholder { font-size: 11px; color: #8a8175; text-align: center; }

.result__matched { position: absolute; left: 24px; top: 332px; width: 312.5px; height: 83px; }

/* 4-QR 切圖 = 左邊文字＋右邊白色 QR 底板；QR 畫在底板裡（底板在圖內 x155.2～238.5、y0.2～83.5） */
.result__share { position: absolute; left: 60.5px; top: 420px; width: 239px; height: 84px; }
.result__share-img { display: block; width: 100%; height: 100%; }
.result__qr { position: absolute; left: 161.5px; top: 6px; width: 71px; height: 71px; }
.result__qr svg { display: block; width: 100%; height: 100%; }

.result__btn {
  position: absolute;
  top: 509.5px;
  width: 173px;
  height: 98px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.result__btn img { display: block; width: 100%; height: 100%; }
.result__btn:active img { scale: .97; }
.result__btn--retake { left: 12.5px; }
.result__btn--home { left: 174.5px; }

/* 裝飾葉片（assets/decor/）已由客戶的 result-bg 取代，樣式先留著備用 */
.decor { position: absolute; pointer-events: none; }
.decor--leaves-bottom { left: 0; bottom: 0; width: 360px; height: 115px; z-index: 1; }

/* ═══════════ 畫面動效（2026-09-22 使用者要求：太單調，每頁都加一點） ═══════════
   調性：這是國家公園署的動物麻吉，illustration 風格，要的是「自然、俏皮、會呼吸」——
   輕搖、浮動、彈跳。**不是速度感**（那是別的案子的語彙，硬套在這裡會很怪）。

   兩層：
     1. 環境動效（idle）：一直在跑，讓機台待機時看起來是活的。這是「單調」的主因。
     2. 進場動效（.is-entering，js/screens.js 掛）：切畫面時內容錯開抵達。

   ⚠ 沿用本專案既有慣例（見上方 cut2 光球那段註解）：
     **動畫一律用獨立的 translate／scale 屬性，不用 transform**，
     免得浮動、進場、彈跳三種動畫互相覆蓋。
   ⚠ 素材是合成大圖（整片葉框、十張卡都在同一張圖裡），沒辦法讓個別葉子各自搖，
     只能整張輕輕動。所以各元素的週期刻意取不同值（4.7 / 6.1 / 2.6 秒），
     彼此不同步才像自然擺動；取整數倍會同步成「一起跳」，很假。
   ⚠ 展場機台會連續跑好幾天，idle 動畫只用 transform/opacity 類屬性走合成執行緒。 */

/* ───── cut1 home：環境 ───── */
.home__title { animation: bob-slow 4.7s ease-in-out infinite; }
.home__cards { animation: bob-soft 6.1s ease-in-out infinite; }
/* ⚠ 呼吸動畫掛在裡面的 <img>，不是按鈕本身。
   直接掛在 .btn-start 上會讓按鈕的碰撞框每一幀都在變，
   Playwright（以及任何等待元素穩定的自動化／輔助技術）會認為它永遠不穩定，點擊直接逾時。
   掛在 img 上視覺完全一樣，但按鈕自己的 box 不動。 */
.btn-start img { animation: breathe 2.6s ease-in-out infinite; }
/* 背景滿版，輕微放大不會露出邊 */
.screen--home .screen__bg { animation: drift 11s ease-in-out infinite; }

@keyframes bob-slow { 50% { translate: 0 -4px; } }
@keyframes bob-soft { 50% { translate: 0 -6px; } }
@keyframes breathe  { 50% { scale: 1.035; } }
@keyframes drift    { 50% { scale: 1.012; } }

/* START 的轉動光圈：客戶的球體切圖上本來就有一道斜向高光，這裡沿用那個語彙，
   讓一道亮弧沿著球緣繞圈，而不是另外加一個突兀的外框。
   做法：conic-gradient 畫出一段亮弧，再用 radial mask 把中間挖空只留一圈環，
   然後整個轉。用獨立的 rotate 屬性，不會跟按鈕本身的 breathe（scale）打架。
   ⚠ 這顆是機台待機時唯一的行動呼籲，光圈是要吸引人來點，所以比其他環境動效明顯一點，
     但仍然只動 rotate／opacity，不重繪。 */
.btn-start::before {
  content: '';
  position: absolute;
  /* ⚠ 這圈光要對準的是「圖裡那顆球」，不是按鈕的框——**兩者不同心**。
     量自 assets/screens/home-start.webp（354px，渲染成 177px 所以 ÷2）：
       球直徑 129.5px、球心 (95.5, 78.2)，而按鈕框中心是 (88.5, 88.5)
       → 球其實偏右 7px、偏上 10.2px（切圖在自己的畫布裡沒有置中）
     量法：球是半透明玻璃，alpha>250 只會抓到文字，要用中等門檻；
           取 alpha>60/90/120/150 四種門檻，結果都收斂在直徑 259、球心 (191,156.5)（原圖座標）才採用。
     ⚠ 不要在畫面截圖上量：按鈕上方有粉色文字帶、下方有葉子，「最寬的一列」會抓到那些東西。
     ⚠ 也不要用 inset 四邊等距，那樣會變成對齊按鈕框、相對球往左下偏（使用者實機回報過）。
     z-index 是必要的：::before 預設畫在子元素底下，而 <img> 填滿整顆按鈕，不提高就會被蓋掉。 */
  width: 133.5px;            /* 129.5 球徑 + 兩側各 2px */
  height: 133.5px;
  left: 28.75px;             /* 95.5 − 133.5/2 */
  top: 11.45px;              /* 78.2 − 133.5/2 */
  z-index: 1;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(from 0deg,
    transparent 0deg 170deg,
    rgba(255, 245, 210, .25) 206deg,
    rgba(255, 255, 255, .98) 262deg,
    rgba(255, 228, 170, .75) 290deg,
    transparent 332deg);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 4.5px), #000 calc(100% - 3px));
  mask: radial-gradient(closest-side, transparent calc(100% - 4.5px), #000 calc(100% - 3px));
  animation: ring-spin 3.8s linear infinite;
}
@keyframes ring-spin { to { rotate: 360deg; } }

/* 按壓回饋要贏過 breathe：同一個 scale 屬性，後面宣告的贏 */
.btn-start:active img { animation: none; scale: .97; }

/* ───── cut1 home：進場 ───── */
.screen--home.is-entering .home__title { animation: rise-in .62s cubic-bezier(.2, .9, .25, 1.12) both; }
.screen--home.is-entering .home__cards { animation: rise-in .62s cubic-bezier(.2, .9, .25, 1.12) both .1s; }
/* ⚠ 掛在 img 不是按鈕本身——同 .btn-start img 那段的理由：
   進場那 900ms 內按鈕的 box 會一直變，等待元素穩定的點擊會被迫多等一輪。 */
.screen--home.is-entering .btn-start img { animation: pop-in .54s cubic-bezier(.2, .9, .25, 1.35) both .24s; }

/* 進場用 opacity＋translate，環境動效用 translate／scale——
   進場播完 class 一拿掉，環境動效就自然接手。 */
@keyframes rise-in { from { opacity: 0; translate: 0 26px; } to { opacity: 1; translate: 0 0; } }
@keyframes pop-in  { from { opacity: 0; scale: .72; } to { opacity: 1; scale: 1; } }

@media (prefers-reduced-motion: reduce) {
  .home__title, .home__cards, .btn-start img, .screen--home .screen__bg,
  .screen--home.is-entering .home__title, .screen--home.is-entering .home__cards,
  .screen--home.is-entering .btn-start img { animation: none; }
  .btn-start::before { animation: none; opacity: .5; }
}

/* ═══════════ cut2–cut5 畫面動效（2026-09-22，接續上面 cut1 的語彙） ═══════════
   沿用 cut1 已經定好的字彙，不另創一套：
     環境（idle）  bob-slow／bob-soft（浮動）、breathe（呼吸）、drift（背景緩慢放大）
     進場          rise-in（往上飄進來）、pop-in（彈出來），掛在 .is-entering 底下
     光圈          ring-spin ＋ conic-gradient 亮弧 ＋ radial mask 挖空成環（START 鈕那招）
   新增的只有四個：twinkle（光點閃爍）、sweep（掃過去的亮帶）、
   orb-burst（cut2 抽中時擴散的光環）、qr-ping（cut5 的 QR 呼吸框）。

   ⚠ 週期一律取彼此除不盡的值（4.3／5.3／5.9／6.7／2.9／3.1／3.7／1.7／2.3…），
     跟 cut1 的 4.7／6.1／2.6／11 也錯開。整數倍會同步成「一起跳」，看起來很假。
   ⚠ 動畫只動 translate／scale／rotate／opacity（獨立屬性，不用 transform），
     走合成執行緒，展場機台連跑好幾天不會累積重繪成本。
     filter: blur 只出現在**靜態**的層（既有的 .orb__mist／.orb__trail），
     我加的會動的層一律沒有 blur——轉動一個帶 blur 的層每幀都要重新光柵化。
   ⚠ 進場的「最慢一個」不能超過 js/screens.js 的 ENTER_TOTAL_MS = 900ms，
     否則還在飛的元素會被拔掉 class 而跳位。目前最慢：cut5 的返回首頁鍵 .34 + .54 = .88s。 */

/* ⚠⚠ 可點擊元素（button／a）上**不要**掛會改幾何的無限迴圈動畫。
   Playwright（以及任何等元素穩定的自動化與輔助技術）點擊前會等「連續兩幀 bounding box 不變」，
   無限的 scale／translate 讓它永遠在變，點擊一路等到逾時。真人點完全正常，所以很難發現，
   錯誤訊息只說 element is not stable，看不出跟動畫有關（2026-09-22 踩過，一次弄掉 4 條測試）。
   本檔的兩種安全做法，兩種都不會動到按鈕自己的 box：
     1. 掛到裡面的子元素（<img>）——cut1 的 .btn-start img 就是這樣；
     2. 掛到偽元素 ::before／::after——偽元素不計入父層的 getClientRects。
   進場動畫雖然會結束，但測試若在那 900ms 內就要點，一樣得等；所以可點擊元素的進場
   也一律掛在子元素或只動 opacity。 */

@keyframes twinkle   { 0%, 100% { opacity: .35; } 50% { opacity: 1; } }
/* 只動 opacity 的進場：給「裡面有按鈕」的容器用（容器一位移，按鈕的碰撞框就跟著動） */
@keyframes fade-in   { from { opacity: 0; } to { opacity: 1; } }
/* 按鈕外圈的呼吸光暈：畫在偽元素上，按鈕本身的 box 不動 */
@keyframes halo-pulse { 0%, 100% { opacity: .25; scale: 1; } 50% { opacity: .7; scale: 1.09; } }
@keyframes sweep     { from { translate: -140% 0; } to { translate: 340% 0; } }
@keyframes orb-burst { from { opacity: .85; scale: .6; } to { opacity: 0; scale: 1.45; } }
@keyframes qr-ping   { 0% { opacity: 0; scale: .95; } 22% { opacity: .85; } 75%, 100% { opacity: 0; scale: 1.06; } }
/* 逆時針的光點軌道（順時針直接用 cut1 的 ring-spin） */
@keyframes ring-spin-rev { to { rotate: -360deg; } }
/* 錄影中的計時器閃爍：只動 opacity，不動版面 */
@keyframes rec-blink { 0%, 100% { opacity: 1; } 50% { opacity: .55; } }
/* 全螢幕的木框：pop-in 的 .72 對滿版元素太誇張，改成只縮一點點 */
@keyframes frame-in  { from { opacity: 0; scale: .94; } to { opacity: 1; scale: 1; } }

/* ───────────── cut2 assign：環境 ─────────────
   使用者指名這頁要「絢麗」。這裡把它解讀成**抽籤的期待感**，不是單純加亮片：
   光球外圍有一圈亮弧在繞（＝ START 鈕那招，同一套語彙），加上兩組會閃的小光點——
   一組在球前面、一組在球後面（.orb__float 的 ::after 是最後一個子元素所以在前，
   ::before 是第一個所以被球擋住），兩組反向轉，光點轉到背面會被球遮住再繞出來，
   靠遮擋做出立體的環繞感，而不是平貼在球上的貼紙。
   輪換中（.is-spinning）整組加速＝越抽越急；揭曉（.is-revealed）擴散一圈光環收尾。 */
.assign__pill { animation: bob-slow 5.3s ease-in-out infinite; }

/* ⚠ 配色：cut2 的底是米色 #f3ecdc，**白色的光點與亮弧在上面等於隱形**
   （第一版就是全白，截圖出來幾乎看不見）。所以這一頁的光效一律走
   「白色核心 + 暖金色暈」與「藍紫色」兩種——暖金取自 cut2 膠囊的橘紅，
   藍紫取自球內既有的 .orb__mist，都是設計稿already有的顏色，不是我另外發明的。 */

/* 繞著球轉的亮弧。做法同 START 鈕：conic-gradient 畫一段亮弧，
   radial mask 把中間挖空只留一圈環，然後轉。
   ⚠ 半徑是量出來的：.orb 容器是 210px，但**球連同光暈實際渲染出來是 216px**
     （量法：暫時 display:none 掉光圈與光點、把有限動畫 finish() 讓 orb-in 回到 scale 1、
      截圖後從角落取背景色再沿中線找差異範圍）。
     原本寫 -13px（＝236px）會讓亮弧浮在離球緣 10px 的地方，看起來不像繞著球轉。
     現在 -5px ＝ 220px，貼著 216px 的球緣外側 2px。
   ⚠ 不要用 .orb 的 CSS 寬度去算——球外面還疊了 glow／rim 幾層，可見邊界比容器大。 */
.orb::after {
  content: '';
  position: absolute;
  inset: -5px;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(from 0deg,
    transparent 0deg 140deg,
    rgba(236, 170, 92, .28) 182deg,
    rgba(255, 238, 198, .95) 236deg,
    rgba(246, 158, 74, .9) 262deg,
    rgba(150, 142, 214, .6) 296deg,
    transparent 338deg);
  -webkit-mask: radial-gradient(closest-side, transparent calc(100% - 7px), #000 calc(100% - 5px));
          mask: radial-gradient(closest-side, transparent calc(100% - 7px), #000 calc(100% - 5px));
  animation: ring-spin 5.9s linear infinite;
}

/* 揭曉時從球心擴散出去的一圈光環。平常 opacity: 0，只有 .is-revealed 才播。 */
.orb::before {
  content: '';
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  pointer-events: none;
  border: 3px solid rgba(250, 186, 98, .95);
  box-shadow: 0 0 14px 3px rgba(255, 206, 140, .6);
  opacity: 0;
}

/* 環繞的小光點。一個 pseudo 裡用多個 radial-gradient 放好幾顆，整個轉＝一起繞軌道；
   同時疊一個 twinkle 只動 opacity，兩個動畫動不同屬性，不會打架。 */
.orb__float::after,
.orb__float::before {
  content: '';
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
}
/* 每顆光點都是「白核心 → 暖金暈 → 透明」三段，才會在米色底上浮起來；
   純白單色會被背景吃掉。不用 filter: blur 做光暈——這層一直在轉，
   每幀重新光柵化會拖垮展場機台，改用 radial-gradient 自己畫出漸層。 */
/* 前景那組（.orb__float 的最後一個子元素 → 畫在球上面） */
.orb__float::after {
  inset: -34px;
  background:
    radial-gradient(circle 6px  at 50% 1%,  #fff 0 16%, rgba(255, 200, 112, .95) 36%, rgba(243, 150, 60, 0) 74%),
    radial-gradient(circle 4.5px at 93% 68%, #fff 0 14%, rgba(255, 190, 104, .9) 38%, rgba(243, 150, 60, 0) 76%),
    radial-gradient(circle 5px  at 8% 56%,  #fff 0 14%, rgba(168, 156, 230, .92) 38%, rgba(140, 128, 214, 0) 76%),
    radial-gradient(circle 3.4px at 74% 12%, #fff 0 18%, rgba(255, 214, 140, .85) 40%, rgba(243, 150, 60, 0) 76%),
    radial-gradient(circle 3.8px at 26% 16%, #fff 0 16%, rgba(174, 160, 232, .88) 38%, rgba(140, 128, 214, 0) 76%),
    radial-gradient(circle 3px  at 62% 95%, #fff 0 18%, rgba(255, 204, 126, .85) 40%, rgba(243, 150, 60, 0) 76%);
  animation: ring-spin 9.1s linear infinite, twinkle 2.3s ease-in-out infinite;
}
/* 背景那組（第一個子元素 → 轉到球後面會被球擋住，做出前後景深） */
.orb__float::before {
  inset: -21px;
  background:
    radial-gradient(circle 5px  at 22% 6%,  #fff 0 15%, rgba(255, 196, 108, .9) 38%, rgba(243, 150, 60, 0) 76%),
    radial-gradient(circle 4px  at 80% 93%, #fff 0 15%, rgba(158, 148, 226, .9) 38%, rgba(140, 128, 214, 0) 76%),
    radial-gradient(circle 3.2px at 4% 74%, #fff 0 18%, rgba(255, 208, 128, .85) 40%, rgba(243, 150, 60, 0) 76%),
    radial-gradient(circle 3.6px at 58% 2%,  #fff 0 16%, rgba(255, 192, 106, .88) 38%, rgba(243, 150, 60, 0) 76%),
    radial-gradient(circle 3px  at 96% 34%, #fff 0 18%, rgba(170, 158, 230, .85) 40%, rgba(140, 128, 214, 0) 76%);
  animation: ring-spin-rev 6.7s linear infinite, twinkle 1.7s ease-in-out infinite;
}
/* 第三組：半徑再大一點、轉更慢，讓光點不只是一圈，密度夠才有「絢麗」的感覺。
   掛在 .orb__glow 上（它 opacity: .45，所以這組本來就會比前兩組淡，剛好當外圈） */
.orb__glow::after {
  content: '';
  position: absolute;
  inset: -58px;
  border-radius: 50%;
  pointer-events: none;
  background:
    radial-gradient(circle 4.5px at 50% 2%,  #fff 0 14%, rgba(255, 198, 110, .9) 38%, rgba(243, 150, 60, 0) 76%),
    radial-gradient(circle 4px  at 96% 46%, #fff 0 14%, rgba(162, 150, 228, .9) 38%, rgba(140, 128, 214, 0) 76%),
    radial-gradient(circle 4px  at 12% 82%, #fff 0 14%, rgba(255, 190, 104, .9) 38%, rgba(243, 150, 60, 0) 76%);
  animation: ring-spin 13.7s linear infinite, twinkle 3.1s ease-in-out infinite;
}

/* 輪換中：整組加速（既有的霧加速、光暈脈動在上面那段） */
.orb.is-spinning::after              { animation-duration: 1.45s; }
.orb.is-spinning .orb__float::after  { animation-duration: 2.3s, .62s; }
.orb.is-spinning .orb__float::before { animation-duration: 1.9s, .48s; }
.orb.is-spinning .orb__glow::after   { animation-duration: 3.4s, .9s; }

/* 揭曉：光環擴散出去；光點慢回來，像是塵埃落定 */
.orb.is-revealed::before             { animation: orb-burst .75s cubic-bezier(.2, .72, .3, 1) both; }
.orb.is-revealed::after              { animation-duration: 4.1s; }
.orb.is-revealed .orb__float::after  { animation-duration: 7.3s, 2.9s; }
.orb.is-revealed .orb__float::before { animation-duration: 5.9s, 2.1s; }
.orb.is-revealed .orb__glow::after   { animation-duration: 11.3s, 3.7s; }

/* ───────────── cut2 assign：進場 ───────────── */
.screen--assign.is-entering .assign__pill { animation: rise-in .62s cubic-bezier(.2, .9, .25, 1.12) both; }
.screen--assign.is-entering .orb          { animation: orb-in .5s cubic-bezier(.2, .8, .3, 1) both .12s; }

/* ───────────── cut3 shoot ─────────────
   ⚠⚠ 這頁的 <canvas id="shoot-canvas"> **就是錄影來源**（recorder.js 對它呼叫
   captureStream()）。captureStream 取的是 canvas 的 backing store，也就是 ctx 畫進去的像素，
   CSS 的 scale／opacity 是合成階段才套上去的，不會進到影片裡——所以對 .shoot__canvas
   這個「元素」加 CSS 動畫，錄出來的成品不受影響（已實測，見 docs/evidence/）。
   但**預覽跟成品必須看起來一樣**，所以錄影中畫面不能在動：
     1. 只在 .is-entering（進場）動，進場 900ms 內結束，而錄影要等玩家按鈕才開始；
     2. 再加一道保險：data-recording 一進 recording／stopping 就強制歸零。
   框裡的東西（木框、資訊卡）一律不碰——那是 compositor.js 畫的，動它才會真的進到影片。 */
.screen--shoot.is-entering .shoot__canvas   { animation: frame-in .58s cubic-bezier(.2, .9, .25, 1.12) both; }
.screen--shoot.is-entering .shoot__timer    { animation: rise-in .5s cubic-bezier(.2, .9, .25, 1.12) both .12s; }
/* ⚠ 控制列**只淡入、不位移**：裡面是「錄製／結束」兩顆按鈕，容器一往上飄，
   按鈕的碰撞框就跟著動，測試在進場那 900ms 內要點就得一路等到穩定。 */
.screen--shoot.is-entering .shoot__controls { animation: fade-in .5s ease-out both .2s; }

/* 保險：錄影一開始，畫布立刻靜止並歸位，預覽＝成品 */
[data-recording="recording"] .shoot__canvas,
[data-recording="stopping"]  .shoot__canvas { animation: none; scale: 1; translate: 0 0; }

/* 待機時「錄製」是這頁唯一的行動呼籲，讓它有東西在動。
   ⚠ 這顆按鈕裡只有文字、沒有可以掛動畫的子元素，所以用**偽元素**做一圈呼吸光暈——
     偽元素不計入按鈕的 getClientRects，按鈕的碰撞框全程不動，點擊不會卡在 not stable。
     （不能像 cut1 那樣直接對按鈕 scale，那會讓測試點不到，見本段開頭的警告。） */
#btn-record { position: relative; }
#btn-record:not(:disabled)::after {
  content: '';
  position: absolute;
  inset: -4px;
  border-radius: 999px;
  pointer-events: none;
  /* 暖白色，不要用跟按鈕同樣的紅——紅圈套在紅按鈕上等於看不見 */
  box-shadow: 0 0 0 2px rgba(255, 244, 224, .9), 0 0 10px 3px rgba(255, 190, 120, .5);
  animation: halo-pulse 2.9s ease-in-out infinite;
}
/* 錄影中計時器閃爍。這是 DOM 疊層、不在 canvas 裡，錄不進影片 */
[data-recording="recording"] .shoot__timer { animation: rec-blink 1.1s ease-in-out infinite; }

/* ───────────── cut4 process ─────────────
   這是等待畫面，最需要動效讓人不焦躁——畫面一靜止，等三秒就像等三十秒。
   ⚠ 這頁客戶還沒給切圖，現在全是色塊佔位（.ph）。所以動效都做在佔位本身，
     換成真素材時把選擇器換掉就好，不要在這裡塞假圖。 */
.screen--process .ph--full    { animation: drift 13s ease-in-out infinite; }
.process__title               { animation: bob-slow 5.9s ease-in-out infinite; }
.screen--process .pill        { animation: breathe 3.7s ease-in-out infinite; }
.process__loading             { animation: twinkle 1.7s ease-in-out infinite; }

/* 「一排 10 個動物剪影」佔位：先用格線切成 10 格（320 ÷ 32 = 10），
   再讓一條亮帶掃過去＝十個剪影依序亮起。亮帶只動 translate，父層 overflow 裁掉出界的部分。 */
.process__row {
  overflow: hidden;
  background:
    repeating-linear-gradient(90deg, rgba(0, 0, 0, .10) 0 24px, transparent 24px 32px),
    repeating-linear-gradient(45deg, rgba(0, 0, 0, .04) 0 8px, rgba(0, 0, 0, .08) 8px 16px);
}
.process__row::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 42%;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(255, 243, 214, .9), transparent);
  animation: sweep 1.9s ease-in-out infinite;
}

/* 進度條上再跑一條亮帶：上傳卡住（--progress 不動）時畫面仍然是活的，
   玩家才不會以為機台當掉。進度本身照舊由 JS 的 --progress 決定，這條只是打光。 */
.process__bar { position: relative; }
.process__bar::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 38%;
  pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .75), transparent);
  animation: sweep 2.3s ease-in-out infinite;
}

/* ───────────── cut4 process：進場 ───────────── */
.screen--process.is-entering .process__title   { animation: rise-in .6s cubic-bezier(.2, .9, .25, 1.12) both; }
.screen--process.is-entering .process__row     { animation: rise-in .6s cubic-bezier(.2, .9, .25, 1.12) both .1s; }
.screen--process.is-entering .pill             { animation: rise-in .6s cubic-bezier(.2, .9, .25, 1.12) both .18s; }
.screen--process.is-entering .process__loading { animation: rise-in .6s cubic-bezier(.2, .9, .25, 1.12) both .24s; }
.screen--process.is-entering .process__bar     { animation: rise-in .6s cubic-bezier(.2, .9, .25, 1.12) both .28s; }

/* ───────────── cut5 result ─────────────
   成品揭曉的喜悅感：卡片先彈出來，膠囊、QR、兩顆鍵依序抵達，之後各自用不同週期輕輕浮著。 */
.screen--result .screen__bg { animation: drift 12.7s ease-in-out infinite; }
.result__card               { animation: bob-soft 5.3s ease-in-out infinite; }
.result__matched            { animation: bob-slow 4.3s ease-in-out infinite; }
.result__share              { animation: bob-slow 6.7s ease-in-out infinite; }
/* ⚠ 呼吸掛在裡面的 <img>，不是按鈕本身（同 cut1 的 .btn-start img）：
   掛在按鈕上會讓碰撞框每幀都在變，自動化點擊永遠等不到穩定。 */
.result__btn--retake img    { animation: breathe 3.1s ease-in-out infinite; }
.result__btn--home img      { animation: breathe 3.7s ease-in-out infinite; }
/* 按壓回饋要贏過 breathe：同一個 scale 屬性，後面宣告的贏（上面 .result__btn:active img 只寫了 scale） */
.result__btn:active img     { animation: none; scale: .97; }

/* QR 的呼吸框：一圈框從 QR 外緣擴散出去，提醒玩家「這個要用手機掃」。
   ⚠ 一定要畫在 QR **外面**（inset: -3px，最大只放大到 1.06），不能蓋到黑白格子，
     否則掃不到。放大後仍落在切圖那塊白色底板內（底板 155.2～238.5、QR 161.5～232.5）。 */
.result__qr::after {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 7px;
  border: 1.5px solid rgba(226, 132, 68, .85);
  pointer-events: none;
  animation: qr-ping 2.9s ease-out infinite;
}

/* ───────────── cut5 result：進場 ─────────────
   兩顆鍵刻意錯開 .06s 抵達，不要一起跳出來。最慢的 .34 + .54 = .88s < ENTER_TOTAL_MS。 */
.screen--result.is-entering .result__card        { animation: frame-in .6s cubic-bezier(.2, .9, .25, 1.12) both; }
.screen--result.is-entering .result__matched     { animation: rise-in .6s cubic-bezier(.2, .9, .25, 1.12) both .12s; }
.screen--result.is-entering .result__share       { animation: rise-in .6s cubic-bezier(.2, .9, .25, 1.12) both .2s; }
/* ⚠ 兩顆鍵的進場也掛在 img 上，理由同上：進場那 900ms 內按鈕的 box 不能動。 */
.screen--result.is-entering .result__btn--retake img { animation: pop-in .54s cubic-bezier(.2, .9, .25, 1.35) both .28s; }
.screen--result.is-entering .result__btn--home   img { animation: pop-in .54s cubic-bezier(.2, .9, .25, 1.35) both .34s; }

/* ───────────── prefers-reduced-motion ─────────────
   偽元素也要關掉（上面好幾個效果整個是靠 ::before／::after 做的，
   只關元素本身的話光圈、光點、亮帶、QR 框都還會繼續轉）。 */
@media (prefers-reduced-motion: reduce) {
  .assign__pill,
  .orb::before, .orb::after,
  .orb__float::before, .orb__float::after, .orb__glow::after,
  .orb.is-spinning::after,
  .orb.is-spinning .orb__float::after, .orb.is-spinning .orb__float::before,
  .orb.is-spinning .orb__glow::after,
  .orb.is-revealed::before, .orb.is-revealed::after,
  .orb.is-revealed .orb__float::after, .orb.is-revealed .orb__float::before,
  .orb.is-revealed .orb__glow::after,
  .screen--assign.is-entering .assign__pill, .screen--assign.is-entering .orb,
  .screen--shoot.is-entering .shoot__canvas,
  .screen--shoot.is-entering .shoot__timer,
  .screen--shoot.is-entering .shoot__controls,
  #btn-record:not(:disabled)::after,
  [data-recording="recording"] .shoot__timer,
  .screen--process .ph--full, .process__title, .screen--process .pill,
  .process__loading, .process__row::after, .process__bar::after,
  .screen--process.is-entering .process__title,
  .screen--process.is-entering .process__row,
  .screen--process.is-entering .pill,
  .screen--process.is-entering .process__loading,
  .screen--process.is-entering .process__bar,
  .screen--result .screen__bg, .result__card, .result__matched, .result__share,
  .result__btn--retake img, .result__btn--home img, .result__qr::after,
  .screen--result.is-entering .result__card,
  .screen--result.is-entering .result__matched,
  .screen--result.is-entering .result__share,
  .screen--result.is-entering .result__btn--retake img,
  .screen--result.is-entering .result__btn--home img {
    animation: none !important;
  }
  /* 純裝飾、靜止下來沒有意義的偽元素直接不要畫 */
  .orb::before, .process__row::after, .process__bar::after,
  .result__qr::after, #btn-record:not(:disabled)::after { content: none; }
  /* 光圈與光點靜止時留一個淡淡的樣子，畫面不會突然變空 */
  .orb::after { opacity: .45; }
  .orb__float::before, .orb__float::after, .orb__glow::after { opacity: .5; }
}
