*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  /* 跟 .map-backdrop-fallback 用同一顆「素材最外緣水域淺青色」，這樣即使
     在圖片還沒載入完、或任何 CSS 疊層意外沒生效的瞬間，也不會閃出一塊
     跟美術對不上的顏色。原本是 #b7dccf，比素材邊緣偏亮偏綠，在 .scene
     上下留白時會多一條淡淡的色階。 */
  background: #a6cbc9;
  font-family: "PingFang TC", "Noto Sans TC", "Microsoft JhengHei", sans-serif;
  -webkit-user-select: none;
  user-select: none;
  touch-action: none;
}

#app {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* ===== 全螢幕邊框（border-image 九宮格，轉角花紋來自原始邊框圖，四邊可無限延展） ===== */
.site-frame {
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none;
  border-style: solid;
  border-width: clamp(46px, 8vw, 118px);
  border-image-source: url(../assets/border.png);
  border-image-slice: 391;
  border-image-repeat: stretch;
}

/* ===== 寬螢幕背景延伸：手機橫式常見的螢幕比例（例如 19.5:9）比地圖原圖
   （6133:3452 ≈ 1.78:1）更寬，.scene 用高度撐滿時，兩側會留出空白。
   這層放在 .scene 後面（DOM 順序在前、z-index 都是 auto，所以會被畫在
   .scene 底下），用美術延伸好的 office-extend.jpg（中間 2400px 跟
   office.jpg 對齊、左右各多 600px 手繪延伸場景）墊在空白處，讓畫面
   自然銜接，不是完全空白的底色。

   對齊關鍵：.scene 的寬度＝100vh * 6133 / 3452（高度撐滿時），裡面的
   office.jpg（原始寬度 2400px）用 object-fit: cover 撐滿寬度，所以它
   實際的縮放比例＝scene 寬度 / 2400。這張延伸圖 office-extend.jpg跟
   office.jpg 是同一個像素網格（只是左右各多畫了 600px），只要用「完全
   相同」的縮放比例去顯示它，中間沒被延伸過的原圖內容就會跟 .scene 的
   地圖圖層精準疊在一起。因此這裡直接把 scene 寬度的算式乘上
   (1519 / 2400)（1519 是原圖高度，套用同一顆縮放比例換算成延伸圖該有
   的顯示高度），刻意不先手動算出一個近似常數再套用——像先前用
   「1350」這個四捨五入過的中繼值，兩顆縮放比例會有約 0.07% 的落差，
   在地圖這種像素風、色塊邊界銳利的美術上，這個誤差在接縫處會被放大成
   肉眼可見的錯位。直接讓瀏覽器用同一串完整算式算到底，兩邊縮放比例才
   會完全一致，不會有任何近似誤差。 */
/* ===== 安全網底層：.map-backdrop（精準對齊版延伸圖）不可能蓋滿所有視窗
   比例，這層負責填掉它蓋不到的地方。

   兩種蓋不到的情形：
   (1) 視窗「太寬」：延伸圖左右各只多畫了 600/2400 = 0.25 倍 .scene 寬度，
       寬高比超過約 2.67:1 時兩側會露出底層。
   (2) 視窗「太高」（寬高比小於 .scene 自己的 6133/3452 ≈ 1.777，例如
       瀏覽器視窗沒有最大化、或 4:3 / 5:4 螢幕）：此時 .scene 是被寬度撐滿、
       上下留白，而延伸圖只有「水平」延伸、垂直方向頂端跟 .scene 對齊，
       所以 .scene 上方那條 (100vh - sceneH)/2 高的帶狀區域完全沒有東西蓋。

   ！！這裡曾經有一個很明顯的瑕疵（就是使用者回報「標題下方交接處錯位」
   的真正原因）：這層原本是「同一張 office-extend.jpg + background-size:
   cover + blur(48px)」。cover 在高視窗時是以高度為基準去撐滿、水平方向
   置中裁切，於是取樣到的正好是圖片正中央「深色的辦公室建築」，糊成一大片
   灰褐色污漬，就顯示在 .scene 上方那條帶狀區域裡——也就是畫面左上角主標題
   的正下方，而且跟下方清晰地圖之間是一條完全沒有過渡的水平硬邊。視窗越高
   這條污漬越粗（實測 1000x800 露出約 39px、1280x1024 約 49px，扣掉
   .site-frame 邊框遮住的部分之後仍然清楚可見）。之前幾輪只測了 16:9 ~
   3.33:1 這些「偏寬」的比例，那些比例下 .scene 剛好被高度撐滿、上方留白為 0，
   所以完全重現不出來。

   修正做法：不再用那張圖當底，改成直接填「美術素材最外緣本來就有的水域
   淺青色」。實際取樣 office-extend.jpg 的四個邊緣得到：頂端 #a6cac8、
   左緣 #a6d0cc、右緣 #a0c7cc、底緣 #a9d0c8，全都落在同一個色調上，所以
   一個單色就能同時對上四個邊。這樣一來：
     - 不會再有深色糊塊，因為根本不再取樣圖片中央；
     - .scene 頂端那條界線兩側變成「淺青色 ↔ 素材自己的淺青色水域」，
       色差極小、肉眼看不出接縫；
     - 定義上一定滿版，任何極端比例都不會露出突兀的顏色；
     - 順便省掉整頁 blur(48px) 這個很吃 GPU 的效果。
   完全沒有動任何美術素材，只改 CSS。 */
.map-backdrop-fallback {
  position: fixed;
  inset: 0;
  /* 極輕微的垂直漸層：上下兩端分別去對齊「素材上緣」跟「素材下緣」實際
     算繪出來的顏色（延伸圖被縮小顯示時會做重新取樣，所以螢幕上量到的
     上緣約 #a7c3c4、比直接讀圖檔像素稍暗一點；下緣約 #a9d0c8），中間
     取兩者的內插。整體色差控制在同色系 5% 以內，不會製造新的可見邊界，
     只是讓大面積留白不要死板。 */
  background-image: linear-gradient(to bottom, #a7c4c4 0%, #a8cac6 50%, #a9d0c8 100%);
  pointer-events: none;
  user-select: none;
}

.map-backdrop {
  position: fixed;
  /* 垂直方向不能用「跟 .scene 一起置中」，這是造成「羽化邊界接不好、
     海岸線兩層對不齊」的真正原因：.layer.map 是用 object-position:
     center top 去裁切的（見下面說明），也就是它顯示的內容永遠是「從
     office.jpg 最上面一行開始」，多出來的高度只從底部裁掉，圖片內容的
     第 0 列一定對齊 .scene 的頂端。但這張延伸圖如果改成用「置中」對齊
     （translateY(-50%)），它自己的第 0 列會對齊在「比 .scene 頂端更高
     一點」的地方（因為這張圖整體比 .scene 高，置中會讓多出來的高度上下
     各分一半）。兩層圖片同一個內容（第 0 列、第 100 列……）就會被畫在
     不同的螢幕高度上，差距＝兩者高度差的一半，換算成實際像素常常有
     幾十 px，遠超過羽化那 44px 的容許範圍，所以邊界看起來像是兩條沒接上
     的海岸線。

     正確做法：讓這張延伸圖的第 0 列跟 .layer.map 的第 0 列對齊在同一個
     screen y 座標，也就是「跟 .scene 頂端對齊」而不是「跟 .scene 置中」。
     .scene 本身是透過 #app 的 flex 垂直置中的，它的頂端 y 座標＝
     50vh − .scene 實際高度 / 2；.scene 實際高度＝min(100vw/R, 100vh)
     （R = 6133/3452，推導見 .scene 那則註解）。把這個算式直接寫進 top，
     兩層圖片的頂端就會鎖在同一條線上，不再需要另外用 transform 做垂直
     置中。 */
  /* 這裡的高度單位必須跟 .scene 用同一種（svh），否則兩者算出來的頂端
     座標會差一截，海岸線就接不上了（見 .scene 那則 svh 的說明）。 */
  top: calc(50vh - min(100vw * 3452 / 6133, 100vh) / 2);
  top: calc(50svh - min(100vw * 3452 / 6133, 100svh) / 2);
  left: 50%;
  transform: translateX(-50%);
  /* 這張延伸圖跟 office.jpg 對齊時，該套用的縮放比例是「.scene 的實際
     寬度 / 2400」（office.jpg 在 .scene 裡是用 object-fit: cover、以寬度
     為基準去縮放並允許輕微裁切高度，詳見下面 .layer.map 的說明），跟高度
     無關。直接沿用 .scene 自己的寬度公式 min(100vw, 100vh * R)，乘上
     1519/2400 換算成延伸圖該有的高度，兩張圖才會用同一顆縮放比例。 */
  height: calc(min(100vw, 100vh * 6133 / 3452) * 1519 / 2400);
  height: calc(min(100vw, 100svh * 6133 / 3452) * 1519 / 2400);
  width: auto;
  max-width: none;
  /* 這張延伸圖套用上面的縮放比例後，高度會固定比 .scene 自己的高度多出
     約 5.53%（1519/2400 跟 3452/6133 兩個比例之間的固定差距，跟視窗大小
     無關）。因為現在兩者頂端對齊（不是置中對齊），這多出來的高度只會
     往「下」溢出，不會往上：.scene 被高度撐滿時，往下溢出的部分本來就
     超出視窗底部、天然被裁掉看不到；.scene 被寬度撐滿、下方留白時，
     往下溢出的這一小段剛好變成多畫出一點原本會被 .layer.map 裁掉的地圖
     內容，自然銜接在地圖下緣，不會跟主場景重疊或重複，反而能讓下方
     留白多少被真的美術內容填掉一些，不需要再額外用 clip-path 裁切。 */
  pointer-events: none;
  user-select: none;
  /* 讓精準版延伸圖的左右「最外側」邊緣也羽化淡出，跟下面 .map-backdrop-
     fallback（模糊氛圍底圖）自然接起來，避免極端寬高比時出現一條「銳利
     圖片突然變成模糊圖片」的生硬分界線。

     這裡的羽化範圍務必抓得非常窄，只能貼在這張延伸圖「真正的最外緣」，
     原因：這張圖總寬度＝1.5 倍 .scene 寬度，.scene 本身佔這張圖左右各
     1/6 ≈ 16.7% 之外的範圍（因為左右各延伸了 0.25 倍 scene 寬度，
     0.25/1.5 ≈ 16.7%）。但「.scene 之外、這張延伸圖之內」的這 16.7%
     並不是永遠都被裁到看不見——只有在視窗寬高比大到某個程度時，這一整條
     延伸帶才會剛好完全露在畫面上；只要視窗沒那麼極端，這條延伸帶大部分
     會被截在螢幕外，只露出貼近 .scene 邊緣的一小段。如果羽化範圍抓太寬
     （之前誤用了 12%，換算成 .scene 寬度是 18%，比延伸帶自己的 16.7%
     還大！），連這段「一般寬螢幕視窗就會露出來」的正常可視範圍都被淡出、
     疊上模糊氛圍圖，反而在完全不極端的視窗尺寸下就製造出一圈「清晰忽然
     變模糊」的可見瑕疵。

     修正做法：把羽化範圍縮到只佔延伸帶最外側一小截（約 3.3%，換算成
     .scene 寬度只有 0.2 倍 scene 寬度的延伸帶最外圍 20%），讓延伸帶裡
     貼近 .scene 的那一大段（0～80%）維持完全清晰不淡出，只有真的逼近
     這張圖物理邊緣、視窗寬高比非常極端時才會顯現淡出效果，一般寬螢幕
     視窗（甚至常見的 21:9 超寬螢幕）都不會碰到這個羽化帶，自然也不會
     看到任何清晰/模糊的分界線。 */
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    black 3.3%,
    black 96.7%,
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    black 3.3%,
    black 96.7%,
    transparent 100%
  );
}

/* ===== 視窗「比 .scene 更高」時（寬高比 < 6133/3452 ≈ 1.777，例如瀏覽器
   視窗沒最大化、或 4:3 / 5:4 螢幕）：.scene 上下會留白，而這張延伸圖只做了
   水平延伸、垂直方向是跟 .scene 頂端對齊，所以它的上緣（＝素材第 0 列）
   跟下緣會直接硬切在留白區裡，變成兩條水平硬邊——尤其上緣那條就落在畫面
   左上角主標題下方，素材上緣還剛好切過幾株草叢／水管的圖形，看起來就像
   「接縫錯位」。這裡把上下緣各羽化 26px，讓素材自然溶進底層的水域淺青色，
   硬邊就消失了。

   為什麼要用 media query 換掉上面那條水平羽化，而不是兩個方向同時做？
   同時做需要 mask-composite: intersect（Safari 要另一套 -webkit- 關鍵字，
   跨瀏覽器很不保險）。但其實兩者的適用情境是互斥的：
     - 這種「高」視窗下，延伸圖總寬＝1.5 倍視窗寬、置中，左右各有 25% 的
       寬度溢出到畫面外，那條 3.3%（換算 4.95% 視窗寬）的水平羽化帶
       整條都在畫面外，根本看不到，拿掉完全沒有差別。
     - 反過來「寬」視窗下 .scene 被高度撐滿、上下留白為 0，也就不需要
       垂直羽化。
   所以直接依寬高比二選一即可，不需要 mask-composite。

   垂直羽化只作用在 .scene 以外的區域，不會影響地圖本身：.scene 範圍內的
   地圖是 .layer.map 這個獨立圖層畫在上面、完全不透明；而下緣羽化帶整條
   都在 .scene 底部以下（延伸圖高度固定比 .scene 高 12.45%）。 */
@media (max-aspect-ratio: 6133 / 3452) {
  .map-backdrop {
    -webkit-mask-image: linear-gradient(
      to bottom,
      transparent 0,
      black 26px,
      black calc(100% - 26px),
      transparent 100%
    );
    mask-image: linear-gradient(
      to bottom,
      transparent 0,
      black 26px,
      black calc(100% - 26px),
      transparent 100%
    );
  }
}

/* ===== 場景：維持與邊框原始比例一致，讓既有的 scene-content 內縮與貼紙座標不用重新校準 ===== */
/* 高度單位用 svh 不用 vh：手機瀏覽器（尤其 iOS Safari）的 100vh 指的是
   「工具列／分頁列全部隱藏時」的高度（large viewport），但使用者實際看到
   的畫面通常是工具列「顯示中」的狀態，可見高度比 100vh 矮一截。用 100vh
   算出來的地圖會比看得到的範圍還高，超出的部分被 body 的 overflow: hidden
   裁掉——症狀就是托盤最下面那排貼紙整個不見、也拖不到。
   svh（small viewport height）指的正是「工具列顯示時」的高度，用它算出來
   的地圖保證一定塞得進可見範圍。
   刻意不用 dvh（會隨工具列收合即時變動）：那會讓整張地圖在使用者捲動或
   工具列自動收合時跟著縮放，正在拖曳貼紙的手感會被打斷；svh 則是固定值，
   工具列收起來時頂多下方多一小條留白，但畫面穩定、拖曳不會被干擾。
   前面那行 vh 是給不支援 svh 的舊瀏覽器（Safari < 15.4）的 fallback，
   不認得 svh 的瀏覽器會忽略後面那行、沿用舊行為。 */
.scene {
  position: relative;
  width: min(100vw, calc(100vh * 6133 / 3452));
  width: min(100vw, calc(100svh * 6133 / 3452));
  aspect-ratio: 6133 / 3452;
  max-height: 100vh;
  max-height: 100svh;
}

.scene-content {
  position: absolute;
  left: 5.47%;
  top: 0.06%;
  width: 89.06%;
  height: 99.88%;
  z-index: 1;
}

.layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: fill;
  pointer-events: none;
}

/* 地圖圖片跟 .scene 現在是同比例，理論上不需要裁切，用 cover 保險即可 */
.layer.map {
  z-index: 0;
  object-fit: cover;
  object-position: center top;
  /* 左右邊緣羽化：.map-backdrop 跟這張地圖圖是「兩張各自獨立縮放」的
     圖片，即使縮放比例算得再精準，瀏覽器對兩張圖各自做縮放運算時，
     取樣的相位（sub-pixel sampling phase）本來就不保證一致，在海岸線
     這種色塊邊界銳利的美術上，即使只差不到 1px，接縫處還是會被放大成
     肉眼可見的錯位——這是兩張獨立圖片合成时物理上的限制，不是計算公式
     的誤差，所以單純調整縮放公式沒辦法根治。這裡改用遮罩讓地圖圖片左右
     邊緣淡出（14px 內從完全不透明羽化到透明），露出底下 .map-backdrop
     同樣內容的畫面，兩張圖在邊界处變成「淡入淡出混合」而不是「硬切邊」，
     即使兩邊取樣結果有一點點落差，也會被羽化過渡吸收掉、不再是一條
     突兀的硬線。 */
  -webkit-mask-image: linear-gradient(
    to right,
    transparent 0,
    black 44px,
    black calc(100% - 44px),
    transparent 100%
  );
  mask-image: linear-gradient(
    to right,
    transparent 0,
    black 44px,
    black calc(100% - 44px),
    transparent 100%
  );
}

/* 「高」視窗（寬高比 < 6133/3452）時，上面 .map-backdrop 的上緣羽化只能
   把 .scene 以外的側邊區域溶進底色；.scene 範圍內因為這張 .layer.map 是
   完全不透明地畫在最上層，素材上緣還是一條硬切線（草叢／水管被切一半）。
   所以這裡也要把地圖圖層的上緣一起羽化，兩層同時淡出，底下的水域淺青色
   才透得出來、變成柔和過渡。

   同樣沿用「二選一」的邏輯，拿掉原本的左右羽化沒有副作用：這種比例下
   .scene 寬度 = 100vw，地圖左右邊緣正好落在視窗最邊上（x = 0 / 100vw），
   邊緣外面沒有畫面可以露出接縫，而且 .site-frame 邊框寬度是
   clamp(46px, 8vw, 118px)、最小 46px 也比那 44px 羽化帶寬，本來就整條蓋住。

   只羽化上緣、不動下緣：地圖下緣底下還有 .map-backdrop 繼續往下多畫
   12.45% 的同一份素材內容，本來就是連續的，沒有硬邊要處理。 */
@media (max-aspect-ratio: 6133 / 3452) {
  .layer.map {
    -webkit-mask-image: linear-gradient(
      to bottom,
      transparent 0,
      black 26px,
      black 100%
    );
    mask-image: linear-gradient(
      to bottom,
      transparent 0,
      black 26px,
      black 100%
    );
  }
}

/* ===== UI ===== */
.ui-title {
  position: fixed;
  top: 0.95vh;
  left: 0.44vw;
  width: 22vw;
  height: auto;
  z-index: 120;
  pointer-events: none;
}

.ui-logo {
  position: fixed;
  top: 1.56vh;
  left: 97.67vw;
  width: 2vw;
  height: auto;
  z-index: 120;
  pointer-events: none;
}

/* ===== 放置區 ===== */
/* pointer-events: none：這一層本身是貼齊整個地圖鋪滿的容器（inset:0），
   自己不畫任何看得見的東西，純粹是拿來裝每張貼紙的剪影提示圖
   （.sticker-hint，一般模式已經是 pointer-events: none，?calibrate
   版面編輯器裡才會被 .editable-item 規則個別開回 auto）。問題出在容器
   本身沒有跟著關掉滑鼠事件：它是 position:absolute + 明確 z-index:5，
   會自己形成一個層疊環境，這個環境會插在 .scene-content 其他同層的
   絕對定位子層中間比大小——只要旁邊有誰的 z-index 比 5 低（或沒有設
   z-index），這層容器「空的背景」本身就會先接住滑鼠事件，蓋過真正
   要點的東西，即使它自己什麼都沒畫出來。?calibrate 版面編輯器的「對話框」
   （.editable-item-dialogbox）就是這樣被擋到的：對話框本身故意不設
   z-index（原因見下面該規則的說明——設了反而會讓巢狀在裡面的尾巴跳不出來
   跟其他對話框比大小），數值上跟 auto 一樣是全部疊在最底層那一批，這層
   容器的 z-index:5 就會贏過它、把點擊攔截走。容器本身加上 pointer-events:
   none 之後，滑鼠事件會直接穿透這層空殼，去點到底下真正的對話框；
   容器「裡面」的剪影提示圖／版面編輯器項目因為各自都有自己的 pointer-events
   設定（auto 或 none），不受父層這個 none 影響，行為不變。 */
.drop-zones {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
}

/* 一般遊玩時直接標示貼紙的目標位置，降低尋找成本。
   放上貼紙本人的剪影圖（xxx-X.png）當提示，比純文字/純圓圈更直覺。
   .sticker-hint 現在同時身兼「視覺提示」跟「拖曳落點判定」兩個角色，
   不再需要另外一層看不見的放置區。 */
.sticker-hint {
  position: absolute;
  pointer-events: none;
  opacity: 1;
  transition: opacity 0.25s ease;
}

.sticker-hint-img {
  display: block;
  width: 100%;
  height: auto;
}

.sticker-hint.highlight .sticker-hint-img {
  filter: drop-shadow(0 0 clamp(6px, 1.2vw, 18px) rgba(255, 255, 255, 0.95));
}

.sticker-hint.completed {
  opacity: 0;
}

/* ===== 版面編輯器模式（網址加上 ?calibrate） ===== */

/* 通用的「可編輯」外觀：虛線框 + 標籤，滑鼠移過去看得出可以拖 */
.editable-item,
.editable-fixed {
  pointer-events: auto;
  cursor: grab;
  touch-action: none;
  box-sizing: border-box;
}

.editable-item::before,
.editable-fixed::before {
  content: attr(data-edit-label);
  position: absolute;
  left: 0;
  top: -1.6em;
  font-size: 11px;
  font-weight: 700;
  white-space: nowrap;
  background: rgba(22, 30, 53, 0.88);
  color: #fff;
  padding: 1px 6px;
  border-radius: 4px;
  pointer-events: none;
}

.editable-item-plot,
.editable-item-icon {
  outline: 2px dashed rgba(255, 236, 84, 0.9);
  outline-offset: 2px;
}

/* 土壤方塊（.tray-plot）本身有 z-index: 1，
   編輯模式下貼紙的可編輯框要蓋在土壤上面，縮放把手才點得到，
   否則把手雖然畫出來了，但滑鼠點擊會被土壤方塊擋住吃掉。 */
.editable-item-icon {
  z-index: 3;
}

.editable-fixed {
  outline: 2px dashed #ff7ac6;
  outline-offset: 2px;
}

/* 剪影提示圖的可編輯框：位置+大小都能調，用橘色跟其他項目區分。
   這裡的位置+大小同時也就是貼紙最終放上去的位置+大小，不用再另外設定一組。 */
.editable-item-hint {
  outline: 2px dashed #ff9f43;
  outline-offset: 2px;
  z-index: 4;
}

/* 拖曳大小：只能縮放、不能移動的預覽項目，位置錨定在
   托盤貼紙座標上，用粉紅色虛線框跟其他「可移動」項目做出區隔。
   因為錨點跟「貼紙・xxx」（.editable-item-icon）完全一樣、而且通常
   比貼紙圖示框更大，兩者疊在一起時這層框本來會蓋住底下貼紙圖示、
   吃掉點擊事件，導致貼紙圖示整個點不到。這裡讓框身本體跟裡面的
   預覽圖都不吃事件（pointer-events: none），只保留縮放把手本身可以
   點（下面 .resize-handle 已經有 pointer-events: auto），這樣滑鼠
   點在空白重疊區域時會直接「穿透」到底下的貼紙圖示，只有點在
   右下角把手上才會觸發縮放。 */
.editable-item-dragsize {
  outline: 2px dashed #ff5c8a;
  outline-offset: 2px;
  z-index: 4;
  pointer-events: none;
}

.editable-item-dragsize .sticker {
  pointer-events: none;
}

/* 對話框展開後的位置/大小：點擊貼紙彈出的終端機對話框最終會落在哪裡、
   多大，用綠色跟其他項目做區隔。寬高可以各自獨立拖曳調整（不是等比例縮放），
   因為它是純文字內容的方框，不像貼紙圖片有固定比例要維持。
   故意不給 z-index（原本是 7）：CSS 規則是「position 不是 static、又有
   明確 z-index 數字」的元素，會單獨形成一個新的層疊環境（stacking
   context），這個元素的所有子層都被封在這個環境「裡面」比大小，沒辦法
   跳出去跟環境外的其他兄弟元素比。這裡的 8 個貼紙對話框都是彼此獨立、
   直接掛在同一個父層（.scene-content）下的兄弟元素，每一個都各自形成
   自己的層疊環境，於是巢狀在裡面的尾巴（.editable-item-tailbox，z-index:8）
   雖然數字比對話框的 7 大，但這個「8」只在自己那個對話框的環境裡面
   算數，跳不出去跟別的對話框比——後面（DOM 順序較晚）的對話框整層
   還是會直接蓋在前面對話框的尾巴上面，導致尾巴點不到、拖不動。
   對話框拿掉 z-index 之後就不會再形成自己的層疊環境，尾巴的 z-index:8
   於是直接跟 8 個對話框（現在都是 auto，數值上等同 0）在同一層比較，
   8 > 0，尾巴永遠會贏、蓋在所有對話框上面，不管彼此位置怎麼交疊都點
   得到、拖得動。這個 class 只有 ?calibrate 版面編輯器會用到，不影響
   正式上線畫面，改這裡很安全。 */
.editable-item-dialogbox {
  outline: 2px dashed #3dff8a;
  outline-offset: 2px;
  /* 跟正式上線的 .morph-dialog 用同一組 --corner / --step 數值，
     這樣 ::before 疊出來的階梯狀缺角剪影，才會跟真正上線時邊框的
     形狀完全一致（如果之後调整 css/style.css 裡 .morph-dialog 的
     --corner，這裡也要手動改成一樣的數字，兩邊目前沒有共用同一個
     來源，純粹是編輯器預覽用）。 */
  --corner: 24px;
  --step: calc(var(--corner) / 4);
}

/* 用 ::before 疊一層跟正式上線 .morph-dialog::before 完全相同的階梯狀缺角
   剪影，這樣在編輯器裡拖曳/縮放對話框位置大小時，就能直接看到跟真正上線
   一模一樣的尖角外框，不用再憑印象腦補。刻意用 ::before、不直接把
   clip-path 切在 .editable-item-dialogbox 本身，是因為 makeEditable()
   會把縮放把手（.resize-handle）當成它的子層插進來，把手位置在
   right:2px/bottom:2px，剛好落在四個角落「被缺角切掉」的範圍內——如果
   clip-path 直接切在 .editable-item-dialogbox 上，把手會被一併裁掉、
   點不到（跟先前 .morph-dialog 直接切 clip-path 導致尾巴/關閉鈕被誤裁
   是同一個原理）。切在 ::before（沒有子層）就不會影響任何把手。 */
.editable-item-dialogbox::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(10, 19, 13, 0.4);
  pointer-events: none;
  clip-path: polygon(
    0 var(--corner),
    var(--step) var(--corner),
    var(--step) calc(var(--corner) - var(--step)),
    calc(var(--step) * 2) calc(var(--corner) - var(--step)),
    calc(var(--step) * 2) calc(var(--corner) - var(--step) * 2),
    calc(var(--step) * 3) calc(var(--corner) - var(--step) * 2),
    calc(var(--step) * 3) calc(var(--corner) - var(--step) * 3),
    var(--corner) calc(var(--corner) - var(--step) * 3),
    var(--corner) 0,
    calc(100% - var(--corner)) 0,
    calc(100% - var(--corner)) var(--step),
    calc(100% - var(--step) * 3) var(--step),
    calc(100% - var(--step) * 3) calc(var(--step) * 2),
    calc(100% - var(--step) * 2) calc(var(--step) * 2),
    calc(100% - var(--step) * 2) calc(var(--step) * 3),
    calc(100% - var(--step)) calc(var(--step) * 3),
    calc(100% - var(--step)) var(--corner),
    100% var(--corner),
    100% calc(100% - var(--corner)),
    calc(100% - var(--step)) calc(100% - var(--corner)),
    calc(100% - var(--step)) calc(100% - var(--step) * 3),
    calc(100% - var(--step) * 2) calc(100% - var(--step) * 3),
    calc(100% - var(--step) * 2) calc(100% - var(--step) * 2),
    calc(100% - var(--step) * 3) calc(100% - var(--step) * 2),
    calc(100% - var(--step) * 3) calc(100% - var(--step)),
    calc(100% - var(--corner)) calc(100% - var(--step)),
    calc(100% - var(--corner)) 100%,
    var(--corner) 100%,
    var(--corner) calc(100% - var(--step)),
    calc(var(--step) * 3) calc(100% - var(--step)),
    calc(var(--step) * 3) calc(100% - var(--step) * 2),
    calc(var(--step) * 2) calc(100% - var(--step) * 2),
    calc(var(--step) * 2) calc(100% - var(--step) * 3),
    var(--step) calc(100% - var(--step) * 3),
    var(--step) calc(100% - var(--corner)),
    0 calc(100% - var(--corner))
  );
}

.dialogbox-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  line-height: 1.4;
  color: rgba(186, 255, 208, 0.85);
  pointer-events: none;
  text-align: center;
  padding: 4px;
  white-space: pre-line;
}

/* 對話框的尖尖尾巴：巢狀在對話框的可編輯框裡面，座標系跟真正上線時
   .morph-tail 的 containing block（對話框的 padding box）完全對齊，
   用深咖啡色跟外層對話框的綠色做出區隔。可以獨立拖曳位置、調整大小、
   旋轉角度，不受外層對話框大小變動影響（除非使用者自己重新調整）。 */
.editable-item-tailbox {
  outline: 2px dashed #b5651d;
  outline-offset: 2px;
  z-index: 8;
}

/* 用 ::before 疊一層跟正式上線 .morph-tail 完全相同的階梯狀鋸齒尖角剪影，
   這樣編輯器裡拖曳/縮放/旋轉尾巴框時，看到的就是跟真正上線一模一樣的
   形狀，不用再腦補方框跟實際尖角形狀的落差。刻意用 ::before、不直接
   把 clip-path 切在 .editable-item-tailbox 本身，是因為 makeEditable()
   會把縮放把手（.resize-handle）、旋轉把手（.rotate-handle）當成它的
   子層插進來，旋轉把手還故意「伸出框外」（top: -28px）——如果 clip-path
   直接切在 .editable-item-tailbox 上，伸出框外的旋轉把手會被一併裁掉、
   點不到（跟先前 .morph-dialog 直接切 clip-path 導致尾巴/關閉鈕被誤裁
   是同一個原理）。切在 ::before（沒有子層）就不會影響任何把手。 */
.editable-item-tailbox::before {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(58, 40, 28, 0.55);
  pointer-events: none;
  clip-path: polygon(
    0 100%,
    0 75%,
    12.5% 75%,
    12.5% 50%,
    25% 50%,
    25% 25%,
    37.5% 25%,
    37.5% 0,
    50% 0,
    62.5% 0,
    62.5% 25%,
    75% 25%,
    75% 50%,
    87.5% 50%,
    87.5% 75%,
    100% 75%,
    100% 100%
  );
}

.resize-only {
  cursor: default;
}

.editable-item.editing,
.editable-fixed.editing {
  cursor: grabbing;
  z-index: 500;
  outline-color: #fff;
}

.resize-handle {
  position: absolute;
  right: 2px;
  bottom: 2px;
  width: 20px;
  height: 20px;
  border-radius: 5px;
  background: #ffec54;
  border: 2px solid #1c233e;
  cursor: nwse-resize;
  pointer-events: auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
  display: flex;
  align-items: center;
  justify-content: center;
}

.resize-handle::after {
  content: '';
  width: 8px;
  height: 8px;
  border-right: 2px solid #1c233e;
  border-bottom: 2px solid #1c233e;
}

/* 旋轉把手：浮在框框正上方的圓形把手，拖動它可以繞著元素中心點轉角度。
   把手本身的 transform 是用 JS 動態疊加「反向旋轉」讓它看起來永遠是正的，
   但它在畫面上的位置仍然會跟著框框一起繞著中心點轉。 */
.rotate-handle {
  position: absolute;
  left: 50%;
  top: -28px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #6ee7ff;
  border: 2px solid #1c233e;
  cursor: grab;
  pointer-events: auto;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
}

.editable-item.editing .rotate-handle {
  cursor: grabbing;
}

.calibration-panel {
  position: fixed;
  bottom: clamp(12px, 2vw, 28px);
  left: clamp(12px, 2vw, 28px);
  z-index: 1200;
  width: min(320px, calc(100vw - 24px));
  padding: 16px;
  border: 2px solid #ffec54;
  border-radius: 12px;
  background: rgba(22, 30, 53, 0.94);
  color: #fff;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
  font-size: 14px;
}

.calibration-panel strong { font-size: 17px; }
.calibration-panel p { margin-top: 6px; line-height: 1.45; }
.calibration-current { margin: 12px 0; color: #ffec54; font-variant-numeric: tabular-nums; }

.calibration-panel button {
  width: 100%;
  border: 0;
  border-radius: 8px;
  padding: 9px 12px;
  background: #ffec54;
  color: #252a44;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

.calibration-panel textarea {
  display: block;
  width: 100%;
  height: 108px;
  margin-top: 10px;
  padding: 8px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 6px;
  resize: vertical;
  background: #101624;
  color: #d8fff7;
  font: 11px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace;
}

.calibration-hint { color: rgba(255, 255, 255, 0.72); font-size: 12px; }

/* 種類篩選：一排小按鈕，選哪個種類，畫面上就只留那個種類的框框可以點
   （其他種類的框框全部變半透明、滑鼠事件也關掉），解決太多種類疊在
   一起、很難點到想調整的那個的問題。「全部」是唯一可以同時看到所有
   種類的選項，其他都是單選、切換時互斥。 */
.calibration-category-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 10px 0;
}

/* .calibration-panel button 那條共用規則（元素+class，優先權比這裡單一
   class 高）會蓋掉寬度/顏色，這裡選擇器故意多疊一層 .calibration-panel
   前綴，優先權才會贏過去，按鈕才會真的顯示「未選中＝半透明小按鈕」的
   樣子，不是全部都長得像預設的全寬黃色按鈕。 */
.calibration-panel .calibration-category-btn {
  width: auto;
  padding: 5px 10px;
  font-size: 12px;
  font-weight: 600;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}

.calibration-panel .calibration-category-btn.is-active {
  background: #ffec54;
  color: #252a44;
}

/* 非目前選中種類的可編輯框框：大幅降低透明度、關掉滑鼠事件，視覺上
   退到背景、也點不到，只留選中的種類可以互動。「全部」模式下（JS 那邊
   不會加這個 class）維持原本全部都能點、都清楚可見的行為。 */
.editable-item.category-dimmed,
.editable-fixed.category-dimmed {
  opacity: 0.12;
  pointer-events: none;
}

/* ===== 已放置貼紙 ===== */
.placed-stickers {
  position: absolute;
  inset: 0;
  z-index: 6;
  pointer-events: none;
}

.placed-sticker {
  position: absolute;
  /* 這張貼紙自己的旋轉角度（跟 hint.rotate 一樣），由 main.js 用 inline
     style 設定成 CSS 變數，跟拖曳/托盤時的角度保持一致，全程不會跳動。 */
  --sticker-rotate: 0deg;
  transform: translate(-50%, -50%) rotate(var(--sticker-rotate)) scale(1);
  /* 不再對 opacity 做任何淡入／淡出：完全靠「放大 → 放開恢復原本大小」
     這個單一維度的變化來表現拿放的手感，才不會有一瞬間亮起來的閃爍感。
     只有 transform 會轉場，這裡是「放開恢復原本大小」用的（平滑）；
     is-appearing / is-grabbed 兩個狀態要瞬間放大則各自把 transition
     蓋成 none（見下面）。 */
  transition: transform 0.18s ease-out;
  pointer-events: auto;
  touch-action: none;
  cursor: grab;
  filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));
}

/* is-appearing：貼紙剛落定的那一刻（延續拖曳時的放大尺寸，無淡入，
   避免瞬間亮起來的閃爍感）。
   is-grabbed：貼紙已經放好之後，使用者再次按住它時的放大回饋（單純放大，
   不能拖曳移動位置，放開後一樣平滑縮回原本大小）。
   兩者都要「瞬間」放大，所以蓋掉 transition，跟托盤貼紙拿起來的瞬間放大
   是同一種手感；放開的縮回動畫則交給上面 .placed-sticker 的 transition。 */
.placed-sticker.is-appearing,
.placed-sticker.is-grabbed {
  transform: translate(-50%, -50%) rotate(var(--sticker-rotate)) scale(1.15);
  transition: none;
  cursor: grabbing;
}

/* ===== 吉祥物走路彩蛋 =====
   全部貼紙找齊後，main.js 會觸發這個元素「從螢幕右側外走進來」的動畫，
   最後停在 MASCOT_CONFIG.stop（config.js）指定的位置，座標系統跟 hint/
   dialogBox 一樣——相對於 .scene-content 的百分比，left/top 是中心點。

   走路動畫的原理：一開始額外疊加一個只在水平方向、用 vw（相對於「整個
   瀏覽器視窗」，不是相對於地圖）表示的位移，把吉祥物推到畫面右側以外
   一定會超出視窗範圍的地方（100vw 這麼大的水平位移，不管最終停留點在
   地圖的哪個水平位置、不管視窗寬度怎麼變化，都保證起點在螢幕右側外），
   垂直方向則不額外位移，讓吉祥物全程維持在跟終點一樣的高度，看起來是
   「橫向」從右走到左，而不是斜線走進來。接著 main.js 移除這段額外位移、
   讓 transform 只剩下置中用的 translate(-50%, -50%)，靠 CSS transition
   把「移除位移」這個變化過程平滑播放出來，就會看起來像從螢幕右側走進
   畫面、最後穩穩停在校正好的定點上。走路動畫播完之後，main.js 會把 GIF
   換成 MASCOT_CONFIG.standImage 指定的靜態站姿圖，讓吉祥物看起來像走到
   定點後就站定不動，不會一直循環播放走路動作。 */
.mascot-walk {
  position: absolute;
  z-index: 10;
  width: 12%;
  /* 素材本身是 480x305 的像素風圖，但在大螢幕上會被放大到 684px 左右
     （約 1.42 倍）。瀏覽器預設用平滑內插放大，像素風的硬邊會被抹成
     漸層，看起來就是「糊糊的」。改用 pixelated（最近鄰）放大，邊緣
     維持方塊感、線條清楚，跟整個網站的像素風調性也一致。
     註：這是在現有素材下能做的最好結果；要真正銳利，最好的方式還是
     把素材本身輸出成 2 倍尺寸（960x610），詳見交接說明。 */
  image-rendering: pixelated;
  pointer-events: none;
  user-select: none;
  opacity: 0;
  display: none;
  transform: translate(-50%, -50%);
  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.25));
}

.mascot-walk.is-active {
  display: block;
  opacity: 1;
}

.mascot-walk.is-walking {
  transition: transform var(--mascot-duration, 2.4s) cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ?calibrate 版面編輯器裡的吉祥物停留位置：淺紫色虛線框，拖曳方式跟其他
   項目一致（拖框身移動位置、拖右下角把手調整大小）。 */
.editable-item-mascot {
  outline: 2px dashed #b388ff;
  z-index: 9;
}

/* ===== 貼紙托盤 ===== */
.sticker-tray {
  position: absolute;
  inset: 0;
  z-index: 15;
  pointer-events: none;
}

/* 貼紙底下的方塊土壤：直接使用提供的素材圖，維持原始比例不變形 */
.tray-plot {
  position: absolute;
  z-index: 1;
  aspect-ratio: 390 / 260;
  transform: translate(-50%, -50%);
  background-image: url(../assets/soil.png);
  background-size: 100% 100%;
  background-repeat: no-repeat;
  transition: opacity 0.3s ease;
}

.tray-plot.placed {
  opacity: 0;
}

.sticker {
  position: absolute;
  z-index: 2;
  cursor: grab;
  pointer-events: auto;
  touch-action: none;
  filter: drop-shadow(0 3px 6px rgba(0, 0, 0, 0.35));
  /* width 放進 transition：放手（不管是放到正確位置還是彈回托盤）都會
     從拖曳中的放大尺寸（cfg.drag.width）平滑縮回原本大小，
     模擬放開手上的東西的感覺。 */
  transition: filter 0.15s, transform 0.15s, width 0.18s ease-out;
  transform-origin: center center;
}

.sticker:hover:not(.placed):not(.dragging) {
  filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.45));
  z-index: 100;
}

.sticker.dragging {
  cursor: grabbing;
  z-index: 200;
  filter: drop-shadow(0 8px 16px rgba(0, 0, 0, 0.5));
  /* 拿起來的瞬間直接套用放大尺寸，不用 transition（放大要「馬上」發生，
     像手一抓起來就變大；放開才需要平滑縮回，見上面 .sticker 的 transition）。 */
  transition: filter 0.15s;
}

.sticker.placed {
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
}

.sticker.ghost {
  opacity: 0.4;
}

/* ===== 彩蛋對話框（像素筆記本風格） =====
   點擊已放置的貼紙時，對話框會直接從貼紙位置整體放大展開（transform: scale），
   開／關各自有獨立的動畫時間長度跟緩動曲線。

   邊框跟尾巴原本是美術給的 dialogbox-frame.png / dialogbox-tail.png（border-image
   九宮格 + 固定尺寸圖片），現在改成純 CSS 畫出來：用 clip-path 切出鋸齒／階梯狀的
   缺角，疊兩層顏色（外層 --ink 深咖啡、內層 --cream 米白）做出邊框粗細的視覺效果，
   不再需要圖檔，--border-w（邊框粗細）跟 --corner（缺角尺寸）都是 CSS 變數，
   要調整直接改數字即可，不用再重新輸出圖片。分隔線（.morph-divider）目前
   還是用 dialogboxline.png，先維持原樣沒有動。 */
/* 找到真正的問題了：這層背景原本完全沒有設定顏色/透明度，等於完全
   透明——對話框邊框「尖角」那個階梯缺角，設計上本來就是刻意留一個
   缺口，讓底下的東西透出來（跟正式的像素風邊框一致）。因為背景是全透明
   的，缺口透出來的就是「對話框後面剛好在那個位置的地圖畫面」，如果那個
   位置剛好是地圖裡偏亮/留白的區域，看起來就會像是「尖角變白色」——不是
   clip-path 對不齊的縫隙，是背景本來就沒有變暗，缺角透到底下真正的畫面。
   吉祥物的對話框因為底色改成搶眼的紅色，這個一直都存在、但不明顯的
   現象才被看出來（其他對話框底色跟邊框都是深色系，缺角透出來的地圖
   畫面就算偏亮也不會像紅配白這麼突兀）。
   修法：讓背景在對話框開啟時真的變暗（半透明黑），符合一般對話框
   「開啟時全螢幕變暗」的預期，尖角缺口透出來的就是變暗的畫面，不會再
   看到突兀的原始亮色。 */
.morph-backdrop {
  position: fixed;
  inset: 0;
  z-index: 940;
  pointer-events: none;
  background: rgba(20, 14, 10, 0);
  transition: background 0.25s ease;
}

/* 這層遮罩只負責「接住對話框外的點擊」（點空白處關閉對話框、也避免點擊
   穿透到地圖上誤觸別的貼紙），刻意保持完全透明、不把畫面變暗。
   原本是 rgba(20, 14, 10, 0.45)，開任何彩蛋都會讓整張地圖暗一截；地圖本身
   是這個活動的主視覺，變暗會把美術壓掉，所以改成透明。
   pointer-events: auto 一定要留著，那才是這層存在的真正理由。 */
.morph-backdrop.visible {
  pointer-events: auto;
  background: transparent;
}

.morph-dialog {
  --ink: #3a281c;
  --cream: #fefdfb;
  --teal: #3ec7c4;
  --orange: #f0954a;
  /* 從 assets/title.png（畫面左上角「Aotter's Housewarming Party」
     像素標題圖）裡實際取樣的紅色，讓對話框標題文字跟頁面主標題用
     同一組紅色，風格才會一致。 */
  --title-red: #da4d28;

  /* 邊框改用 CSS 畫（clip-path 切出階梯狀缺角），不再吃圖檔：
     --border-w 是邊框粗細、--corner 是角落缺角的尺寸，兩個都是絕對 px，
     想要邊框更粗/更細、缺角更大/更小，直接改這兩個數字就好。
     --step 是缺角切成幾階的每一階大小，固定切 4 階。 */
  --border-w: 8px;
  --corner: 24px;
  --step: calc(var(--corner) / 4);

  display: none;
  position: fixed;
  z-index: 950;
  overflow: visible;
  /* 這個「border」故意設成透明色：不是拿來畫可見的邊線，而是拿來讓瀏覽器
     把 padding box 往內縮 --border-w，這樣 .morph-content 這種 absolutely
     positioned 的子層用 inset:0 時，才會正確地「貼齊邊框內緣」而不是貼齊
     整個外框。深咖啡色的邊框本體（含階梯狀缺角）改交給 ::before 這個
     虛擬元素畫（見下方），刻意不是直接畫在 .morph-dialog 自己身上，
     是因為 clip-path 會連同子層一起裁切——如果直接切在 .morph-dialog 上，
     .morph-tail（尾巴）跟 .morph-close（關閉鈕）這些故意「伸出框外」的
     子層也會被一起切掉，只有切在 ::before（沒有子層）才不會殃及它們。 */
  border: var(--border-w) solid transparent;
  opacity: 0;
  transform: scale(0.05);
  transition: transform 0.32s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
}

/* 深咖啡色邊框本體（含階梯狀缺角）畫在 ::before 這個虛擬元素上，不是直接
   畫在 .morph-dialog 身上：因為 clip-path 會連同「所有子層」一起裁切，
   而 .morph-tail（尾巴）跟 .morph-close（關閉鈕）都故意伸出 .morph-dialog
   本身的框外，如果 clip-path 直接切在 .morph-dialog 上，這兩層伸出框外
   的部分會被一併切掉。::before 沒有子層，切在它身上就不會殃及任何人。
   inset 設成 -1 倍 --border-w：因為 ::before 的 containing block 也是
   .morph-dialog 的 padding box（已經內縮了 --border-w），這裡要「擴張」
   回去，才會剛好對齊 .morph-dialog 原本的外框（border-box）大小。 */
.morph-dialog::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: calc(-1 * var(--border-w));
  background: var(--ink);
  clip-path: polygon(
    0 var(--corner),
    var(--step) var(--corner),
    var(--step) calc(var(--corner) - var(--step)),
    calc(var(--step) * 2) calc(var(--corner) - var(--step)),
    calc(var(--step) * 2) calc(var(--corner) - var(--step) * 2),
    calc(var(--step) * 3) calc(var(--corner) - var(--step) * 2),
    calc(var(--step) * 3) calc(var(--corner) - var(--step) * 3),
    var(--corner) calc(var(--corner) - var(--step) * 3),
    var(--corner) 0,
    calc(100% - var(--corner)) 0,
    calc(100% - var(--corner)) var(--step),
    calc(100% - var(--step) * 3) var(--step),
    calc(100% - var(--step) * 3) calc(var(--step) * 2),
    calc(100% - var(--step) * 2) calc(var(--step) * 2),
    calc(100% - var(--step) * 2) calc(var(--step) * 3),
    calc(100% - var(--step)) calc(var(--step) * 3),
    calc(100% - var(--step)) var(--corner),
    100% var(--corner),
    100% calc(100% - var(--corner)),
    calc(100% - var(--step)) calc(100% - var(--corner)),
    calc(100% - var(--step)) calc(100% - var(--step) * 3),
    calc(100% - var(--step) * 2) calc(100% - var(--step) * 3),
    calc(100% - var(--step) * 2) calc(100% - var(--step) * 2),
    calc(100% - var(--step) * 3) calc(100% - var(--step) * 2),
    calc(100% - var(--step) * 3) calc(100% - var(--step)),
    calc(100% - var(--corner)) calc(100% - var(--step)),
    calc(100% - var(--corner)) 100%,
    var(--corner) 100%,
    var(--corner) calc(100% - var(--step)),
    calc(var(--step) * 3) calc(100% - var(--step)),
    calc(var(--step) * 3) calc(100% - var(--step) * 2),
    calc(var(--step) * 2) calc(100% - var(--step) * 2),
    calc(var(--step) * 2) calc(100% - var(--step) * 3),
    var(--step) calc(100% - var(--step) * 3),
    var(--step) calc(100% - var(--corner)),
    0 calc(100% - var(--corner))
  );
}

/* 影片彩蛋（.morph-dialog.is-video）採用置中的寬版「左文右片」視窗。
   外框仍維持原本的像素風雙線相框；在這個較大尺寸下改用簡潔的斜切角，
   讓左右兩欄內容有更穩定的閱讀空間。
   尾巴（.morph-tail）也一併藏起來：尾巴的作用是視覺上「指回」貼紙原本
   在地圖上的位置，但影片彩蛋現在改成螢幕正中央開啟、跟貼紙位置無關，
   繼續顯示尾巴只會變成一個指向奇怪方向的裝飾物，所以直接隱藏。 */
.morph-dialog.is-video {
  /* 邊框加粗：文字型彩蛋維持原本繼承來的 8px（--border-w 是 .morph-dialog
     共用的變數），這裡針對影片彩蛋單獨覆寫成 20px，邊框整體看起來更
     厚實。因為 .morph-close（關閉鈕）跟下面雙線邊框的 4 層都是用
     var(--border-w) 算位置/尺寸，改這一個變數就會全部連動變粗，不用
     每個地方分別調。

     雙線邊框，由外而內共 4 層疊出「咖啡色線／米白間隔／藍綠色線／米白
     間隔」，每層固定佔 --border-w 的 1/4（--v-line-step）：
       Layer A（::before，咖啡色）inset 一整圈 --border-w，鋪滿整個邊框
               範圍當底
       Layer B（.morph-video-border-gap1，米白）inset 少 1 個 step，蓋掉 A
               的內側，只留最外圈 1 個 step 厚的咖啡色露出來
       Layer C（.morph-video-border-line，藍綠色）inset 再少 1 個 step，
               蓋掉 B 的內側，露出 1 個 step 厚的米白間隔
       Layer D（.morph-video-border-gap2，米白）inset 再少 1 個 step，蓋掉
               C 的內側，露出 1 個 step 厚的藍綠色，同時把最內側補回米白
               （貼齊 .morph-content 本身的米白底色）
     每一層的切角尺寸（--v-chamfer-*）依序遞減 1 個 step，跟 inset 遞減量
     一致，疊起來的斜切線才會像相框斜接一樣對齊、平行，不會內外層切得
     不對稱。四個角落都切角，唯獨右上角（關閉鈕 .morph-close 所在的角落）
     在下面四層 clip-path 裡都刻意跳過，維持直角，避免跟關閉鈕的方形
     按鈕打架。 */
  --border-w: 20px;
  --v-line-step: calc(var(--border-w) / 4);
  --v-chamfer-a: 16px;
  --v-chamfer-b: calc(var(--v-chamfer-a) - var(--v-line-step));
  --v-chamfer-c: calc(var(--v-chamfer-b) - var(--v-line-step));
  --v-chamfer-d: calc(var(--v-chamfer-c) - var(--v-line-step));
}

/* 四層共用的切角形狀（只有 --corner-size 這個切角尺寸不同），
   套用同一個 polygon() 寫法，右上角永遠不切、維持直角。 */
.morph-dialog.is-video::before {
  clip-path: polygon(
    var(--v-chamfer-a) 0,
    100% 0,
    100% calc(100% - var(--v-chamfer-a)),
    calc(100% - var(--v-chamfer-a)) 100%,
    var(--v-chamfer-a) 100%,
    0 calc(100% - var(--v-chamfer-a)),
    0 var(--v-chamfer-a)
  );
}

.morph-video-border-gap1,
.morph-video-border-line,
.morph-video-border-gap2 {
  display: none;
  position: absolute;
  z-index: 0;
}

.morph-dialog.is-video .morph-video-border-gap1,
.morph-dialog.is-video .morph-video-border-line,
.morph-dialog.is-video .morph-video-border-gap2 {
  display: block;
}

.morph-video-border-gap1 {
  inset: calc(-1 * var(--border-w) + var(--v-line-step));
  background: var(--cream);
}

.morph-dialog.is-video .morph-video-border-gap1 {
  clip-path: polygon(
    var(--v-chamfer-b) 0,
    100% 0,
    100% calc(100% - var(--v-chamfer-b)),
    calc(100% - var(--v-chamfer-b)) 100%,
    var(--v-chamfer-b) 100%,
    0 calc(100% - var(--v-chamfer-b)),
    0 var(--v-chamfer-b)
  );
}

.morph-video-border-line {
  inset: calc(-1 * var(--border-w) + var(--v-line-step) * 2);
  background: var(--teal);
}

.morph-dialog.is-video .morph-video-border-line {
  clip-path: polygon(
    var(--v-chamfer-c) 0,
    100% 0,
    100% calc(100% - var(--v-chamfer-c)),
    calc(100% - var(--v-chamfer-c)) 100%,
    var(--v-chamfer-c) 100%,
    0 calc(100% - var(--v-chamfer-c)),
    0 var(--v-chamfer-c)
  );
}

.morph-video-border-gap2 {
  inset: calc(-1 * var(--border-w) + var(--v-line-step) * 3);
  background: var(--cream);
}

.morph-dialog.is-video .morph-video-border-gap2 {
  clip-path: polygon(
    var(--v-chamfer-d) 0,
    100% 0,
    100% calc(100% - var(--v-chamfer-d)),
    calc(100% - var(--v-chamfer-d)) 100%,
    var(--v-chamfer-d) 100%,
    0 calc(100% - var(--v-chamfer-d)),
    0 var(--v-chamfer-d)
  );
}

.morph-content.is-video {
  clip-path: none;
}

/* 要蓋過後面 .morph-dialog.is-active .morph-tail { display: block; } 那條
   規則，兩者都是兩個 class 疊加、後面宣告的原本會贏，這裡刻意多疊一個
   .is-active 讓選擇器優先權更高（三個 class），才能穩定蓋過去。 */
.morph-dialog.is-video.is-active .morph-tail {
  display: none;
}

/* 手機橫式的文字彩蛋（.morph-dialog.is-fullscreen）：跟影片彩蛋一樣改成
   螢幕正中央、盡量撐滿的大視窗（見 main.js positionMobileFullDialog()），
   同樣沒有「指回貼紙位置」的尾巴——大視窗置中展開，尾巴指向哪裡都不會
   對到貼紙原本的位置，留著只會變成一個方向錯亂的裝飾物。跟上面
   .morph-dialog.is-video.is-active 用同一招疊三個 class 提高優先權。 */
.morph-dialog.is-fullscreen.is-active .morph-tail {
  display: none;
}

.morph-dialog.is-active {
  display: block;
}

/* 開啟時加上這個 class，讓對話框從很小的比例直接放大到 100%；
   transform-origin 由 JS 依照每個貼紙的 tailBox 位置動態算出來，
   所以視覺上會像是從尖尖的尾巴那邊「長」出來的。關閉時只要拿掉
   這個 class，就會沿著同一組 transition 縮小、淡出消失。 */
.morph-dialog.is-open {
  opacity: 1;
  transform: scale(1);
}

/* 不再有「貼紙圖示 ↔ 對話框內容」互相變形的中間狀態，內容一開啟就直接顯示。 */
.morph-icon {
  display: none;
}

.morph-content {
  position: absolute;
  /* .morph-dialog 有一個透明的 border（見上方註解），containing block
     （padding box）已經自動內縮 --border-w，所以這裡維持 inset: 0
     就會剛好貼齊邊框內緣，不用手動再扣一次邊框粗細。 */
  inset: 0;
  overflow-y: auto;
  /* 留白從原本收得很緊的 8px/16px 放寬成 28px/36px，跟手機橫式滿版
     視窗（.morph-content.is-fullscreen）用同一組數值，讓桌機版文字
     到邊框的間距跟手機版一致。
     當初之所以收得很緊，是因為那時候對話框高度是寫死的
     cfg.dialogBox.height（才 15~16% 地圖高度），留白一大就會把內文
     擠到要捲動；現在高度已經改成依內文實際內容動態量出來
     （見 main.js measureTextContentHeight），對話框會自己長高去容納
     這圈留白，不會再有內文被擠掉的問題。
     注意：量測用的隱藏元素跟這裡共用同一個 .morph-content class
     （見 main.js 的 morphMeasure），所以改這裡的 padding，量出來的
     高度會自動跟著一起算，兩邊不會對不上。 */
  padding: 28px 36px;
  background: var(--cream);
  color: #4a3527;
  /* 內層底色也要切一樣階梯狀的缺角（角落尺寸 = 外層缺角 - 邊框粗細），
     不然它是方形直角，四個角落會直接方形凸出，蓋過外層裁出來的斜角缺口。
     這裡故意多留 1px（不是精準的「邊框粗細」），讓內層的缺角比外層
     「小」那 1px、稍微蓋過邊框一點點，而不是剛好對齊——兩個獨立元素
     （::before 跟 .morph-content）各自算自己的 clip-path，理論上算出來
     的座標會完全對齊，但實際上不同瀏覽器（尤其 Safari）在算 clip-path
     的次像素座標時，各自可能有極小的捨入誤差，兩層完全依賴「剛好對齊」
     時，接縫處偶爾會露出一條極細的縫隙，透出對話框後面的顏色，看起來
     像是「尖角變白色」。故意讓內層蓋過外層一點點（overlap 而不是
     gap），縫隙會被蓋住而不是露出來，两層顏色還是分得清楚，只是差了
     不到 1px，肉眼看不出來。 */
  --ic: calc(var(--corner) - var(--border-w) + 1px);
  --is: calc(var(--ic) / 4);
  clip-path: polygon(
    0 var(--ic),
    var(--is) var(--ic),
    var(--is) calc(var(--ic) - var(--is)),
    calc(var(--is) * 2) calc(var(--ic) - var(--is)),
    calc(var(--is) * 2) calc(var(--ic) - var(--is) * 2),
    calc(var(--is) * 3) calc(var(--ic) - var(--is) * 2),
    calc(var(--is) * 3) calc(var(--ic) - var(--is) * 3),
    var(--ic) calc(var(--ic) - var(--is) * 3),
    var(--ic) 0,
    calc(100% - var(--ic)) 0,
    calc(100% - var(--ic)) var(--is),
    calc(100% - var(--is) * 3) var(--is),
    calc(100% - var(--is) * 3) calc(var(--is) * 2),
    calc(100% - var(--is) * 2) calc(var(--is) * 2),
    calc(100% - var(--is) * 2) calc(var(--is) * 3),
    calc(100% - var(--is)) calc(var(--is) * 3),
    calc(100% - var(--is)) var(--ic),
    100% var(--ic),
    100% calc(100% - var(--ic)),
    calc(100% - var(--is)) calc(100% - var(--ic)),
    calc(100% - var(--is)) calc(100% - var(--is) * 3),
    calc(100% - var(--is) * 2) calc(100% - var(--is) * 3),
    calc(100% - var(--is) * 2) calc(100% - var(--is) * 2),
    calc(100% - var(--is) * 3) calc(100% - var(--is) * 2),
    calc(100% - var(--is) * 3) calc(100% - var(--is)),
    calc(100% - var(--ic)) calc(100% - var(--is)),
    calc(100% - var(--ic)) 100%,
    var(--ic) 100%,
    var(--ic) calc(100% - var(--is)),
    calc(var(--is) * 3) calc(100% - var(--is)),
    calc(var(--is) * 3) calc(100% - var(--is) * 2),
    calc(var(--is) * 2) calc(100% - var(--is) * 2),
    calc(var(--is) * 2) calc(100% - var(--is) * 3),
    var(--is) calc(100% - var(--is) * 3),
    var(--is) calc(100% - var(--ic)),
    0 calc(100% - var(--ic))
  );
  /* 原本用 ZCOOL KuaiLe（手寫感的裝飾字型）當內文字體，筆畫比較粗細不一、
     彎彎繞繞，長段文字讀起來偏吃力，而且它主要是簡體中文字型，繁體字
     很多是靠瀏覽器自動 fallback 補上的，同一段文字裡字體其實會不一致。
     改用 Noto Sans TC：一樣是 Google Fonts、免費可商用，繁體中文字完整
     覆蓋、風格乾淨俐落、間距工整，讀起來清楚很多；像素風的味道則保留在
     邊框（clip-path 畫出來的階梯缺角）、尾巴跟標題前綴／關閉按鈕
     （Press Start 2P）上，內文本身以「好讀」為主。 */
  font-family: 'Noto Sans TC', 'Microsoft JhengHei', sans-serif;
}

/* 捲軸改成跟邊框一樣的深咖啡色，直角、不做圓角，滑塊中間再切一條淺色縫線，
   讓它看起來有一點點像素方塊堆疊的感覺，而不是瀏覽器預設的灰色圓角捲軸。 */
.morph-content::-webkit-scrollbar {
  width: 10px;
}

.morph-content::-webkit-scrollbar-track {
  background: rgba(58, 40, 28, 0.12);
}

.morph-content::-webkit-scrollbar-thumb {
  background: var(--ink);
  border-radius: 0;
  background-image: linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.22) 0,
    rgba(255, 255, 255, 0.22) 3px,
    transparent 3px,
    transparent 6px
  );
}

.morph-content {
  scrollbar-width: thin;
  scrollbar-color: var(--ink) rgba(58, 40, 28, 0.12);
}

/* 關閉按鈕固定卡在右上角的階梯狀切角上：top/right 都設成 -1 倍 --border-w，
   讓按鈕左上角對齊 .morph-dialog 的 border-box（外框最外緣），寬高直接用
   --corner（切角本身的尺寸），這樣按鈕就會剛好蓋滿整個切角範圍——包含
   ::before 裁掉的斜角空白，看起來像是切角本身變成了一顆方形按鈕，
   而不是浮在框外的獨立小徽章。背景色用跟邊框（::before 的 --ink）
   一樣的顏色，文字改用淺色（--cream）才能在深色底上看得清楚。 */
.morph-close {
  position: absolute;
  top: calc(-1 * var(--border-w));
  right: calc(-1 * var(--border-w));
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--corner);
  height: var(--corner);
  padding: 0;
  background: var(--ink);
  border: none;
  font-family: 'Press Start 2P', ui-monospace, monospace;
  font-size: 0.55rem;
  line-height: 1;
  color: var(--cream);
  cursor: pointer;
}

.morph-close:hover {
  color: var(--orange);
}

/* 標題列：原本用純 CSS ::before 畫一個「> 」當前綴，現在改成每個貼紙
   自己的 8bit 風格小圖示（cfg.icon8bit），跟標題文字排在同一列。 */
.morph-title-row {
  display: flex;
  align-items: center;
  gap: 0;
  /* 原本上下各留 4px/10px，先收緊成 0/4px 讓標題列少吃一點高度，
     這裡再放寬回 2px/6px（參考 dialog-css-demo.html 案例的間距感覺），
     多出來的空間是因為下面的分隔線改用 CSS 畫的細線，比原本 12px 高
     的圖檔省下不少高度，才有餘裕讓標題列跟內文之間喘口氣
     （原因見 .morph-content 的註解）。 */
  margin: 2px 0 6px 0;
}

.morph-title-icon {
  flex: 0 0 auto;
  width: 48px;
  height: 48px;
  object-fit: contain;
  image-rendering: pixelated;
  /* 每張 8bit 圖示四周的透明留白比例都不一樣，且每張圖顯示尺寸
     （icon8bitSize）也各自不同，所以「圖示可見邊緣」到標題文字的
     實際距離沒辦法套用同一個固定 margin。實際的 margin-right 數值
     由 JS 依照 cfg.icon8bitGap（每張圖片各自算好的補償值）動態設定，
     這裡的 0px 只是還沒套用時的預設值。 */
  margin-right: 0;
}

/* 上面的 image-rendering: pixelated 是刻意為 8bit 像素風貼紙圖示保留
   鋸齒方塊感的，但 Konois 用的 Aotter logo 是平滑的線條圖案，套用
   pixelated（最近鄰縮圖）會強制把圓弧邊緣切成一格一格的鋸齒。這裡
   針對這張圖改回瀏覽器預設的平滑縮圖演算法，線條才會滑順。
   （原圖是 3600×3600，解析度本來就非常足夠，不需要向量檔。） */
.morph-title-icon[src$="aotter-logo.png"] {
  image-rendering: auto;
}

.morph-title {
  font-family: 'Noto Sans TC', 'Microsoft JhengHei', sans-serif;
  font-size: 0.95rem;
  /* 原本 700 是字重最粗的等級，改成 600（次粗，Semi Bold），
     份量感比 700 輕一點，但還是比正常字重（400）明顯粗一些。 */
  font-weight: 600;
  /* 顏色從跟邊框搭配的青色（--teal）改成 --title-red，跟
     assets/title.png 主標題圖用同一組紅色。 */
  color: var(--title-red);
  margin: 0;
  line-height: 1.45;
}

/* 分隔線原本是美術給的 dialogboxline.png（左邊橘色十字、右邊藍綠色圓圈，
   中間虛線用 border-image-repeat: round 鋪滿），現在改成跟
   dialog-css-demo.html 案例一樣、純 CSS 畫的橘色虛線：不吃圖檔、
   之後想調顏色/間距/粗細都只要改這幾個數字，不用再麻煩美術重出圖。
   做法是用 repeating-linear-gradient 沿著水平方向重複「橘色線段 + 透明
   間隔」，線段本身用 background-size 沿高度方向只畫一半（見下方 --dash
   相關寫法簡化成固定 3px 高、6px 一個週期）。 */
.morph-divider {
  /* 內文區不再跟貼紙插圖並排，改成佔滿內文欄整個寬度。原本圖檔版本
     高度要 12~16px 才能讓兩端裝飾圖示看得清楚，改成純 CSS 虛線後
     只要 3px 就夠，省下來的高度分一點回標題列跟內文之間的留白
     （原因見 .morph-content 的註解）。 */
  width: 100%;
  height: 3px;
  background-image: repeating-linear-gradient(
    to right,
    var(--orange) 0,
    var(--orange) 6px,
    transparent 6px,
    transparent 12px
  );
  /* 原本下面留 14px，先收緊成 4px，這裡放寬回 8px（參考
     dialog-css-demo.html 案例的間距感覺），因為虛線本身變薄很多，
     即使留白多一點，整體還是比圖檔版本省空間。 */
  margin: 0 0 8px;

  /* 對話框開啟時，標題文字／虛線／內文要模擬「打字機」逐字出現的動態
     （見 JS 的 typewriter 相關函式）。虛線本身沒有文字可以逐字顯示，
     改成用 clip-path 從左到右「刷出來」模擬同一種被打出來的感覺：
     預設狀態用 inset(0 100% 0 0) 把整條線從右邊裁掉、完全藏起來，
     開啟時 JS 幫 .morph-divider 加上 .is-drawn，clip-path 過渡到
     inset(0 0 0 0)，虛線就會從左邊往右邊展開。transition-timing-function
     用 steps() 而不是預設的平滑曲線，是因為虛線本身就是一格一格的橘色
     線段，用階梯式的時間函式展開，看起來才會像是「一段一段被打出來」，
     而不是像百葉窗一樣平滑拉開，跟文字逐字浮現的顆粒感比較搭。 */
  clip-path: inset(0 100% 0 0);
  /* 0.3s 收快成 0.18s：整段出場動畫縮短到 1.3 秒左右之後，分隔線這一步
     佔的比例變得太重，跟著收快才不會卡在中間拖節奏。steps() 的格數
     一併從 24 降到 14，維持「一段一段被打出來」的顆粒感（格數不變、
     時間變短的話，每一格會快到看不出階梯感，變得像平滑拉開）。 */
  transition: clip-path 0.18s steps(14, end);
}

.morph-divider.is-drawn {
  clip-path: inset(0 0 0 0);
}

/* 打字機動態進行中，插在目前正在輸出的文字後面的閃爍游標，模擬打字機／
   終端機那種一個字一個字蹦出來、後面跟著一條閃爍豎線的感覺。用
   background-color: currentColor，讓游標顏色自動跟著它所在的元素（標題
   或內文）當下的文字顏色走，標題跟內文不用分開設定顏色。每次某一段
   文字（標題、或內文其中一個段落）打完之後，JS 會把這個 span 從 DOM
   移除，不會殘留在最終畫面上。 */
.type-cursor {
  display: inline-block;
  width: 2px;
  height: 1em;
  margin-left: 1px;
  background-color: currentColor;
  vertical-align: -0.15em;
  animation: type-cursor-blink 0.8s steps(1, end) infinite;
}

@keyframes type-cursor-blink {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* 內文段落的「淡入浮現」出場動畫（見 main.js 的 revealBody）。
   原本內文跟標題一樣是逐字打出來的，但文案加長到 400 多字之後，
   光內文就要跑十幾秒（每字 26ms × 字數），時間直接跟字數成正比，
   文案再加長就會再變慢。改成「一段一段淡入」之後，總時長變成
   段數 × 間隔 + 淡入時間，跟字數完全無關，不管文案多長都是同一個
   節奏（目前 4 段約 0.55 秒）。標題仍然保留逐字打字機（字數少、
   成本低），像素風的招牌感留在最划算的地方。

   .is-revealing 是「還沒出現」的起始狀態（透明、往下位移一點點），
   JS 依序幫每段加上 .is-revealed 觸發過渡。刻意不在 .is-revealing
   上寫 transition，而是寫在 .is-revealed，這樣段落被塞進 DOM 的
   瞬間會直接就是隱藏狀態（不會先閃一下完整文字再淡出），只有
   「隱藏 → 顯示」這個方向才有動畫。

   注意 opacity / transform 都不影響版面高度，所以對話框依內文動態
   量出來的高度（見 measureTextContentHeight）不會被這個動畫影響，
   量測用的隱藏元素也不會帶到這兩個 class。 */
.morph-body p.is-revealing {
  opacity: 0;
  transform: translateY(6px);
}

.morph-body p.is-revealing.is-revealed {
  opacity: 1;
  transform: none;
  transition: opacity 0.25s ease-out, transform 0.25s ease-out;
}

.morph-body {
  font-size: 0.95rem;
  /* 原本 1.75 行高偏鬆，內文一多，連前兩行都要往下捲才看得到。
     收緊到 1.4，同樣字級下可以多塞進去一些行數，一打開對話框
     就能直接看到開頭幾行文字。 */
  line-height: 1.4;
  /* 內文原本是預設的左右對齊（靠左對齊、右邊不切齊），每一行長度不一，
     視覺上跟上面貼齊滿版寬度的橘色虛線分隔線比起來，右側會參差不齊、
     看起來比虛線「短」一截。這裡改成兩端對齊（justify），除了段落
     最後一行以外，每一行都會被拉伸到跟分隔線一樣的滿版寬度，讀起來
     整段文字的寬度就會跟橘色虛線切齊。text-justify: inter-character
     讓中文這種沒有空白字元可以伸縮的文字，改成用調整「字與字之間」的
     間距來對齊（瀏覽器對中日韓文字的標準兩端對齊方式），不會出現英文
     justify 常見的「字中間被拉出很大空白」的問題。 */
  text-align: justify;
  text-justify: inter-character;
}

.morph-body p {
  margin-bottom: 0.6em;
}

/* 影片彩蛋的左欄保留標題與文案、右欄放播放器。日後只要在 config.js
   補上 modal.title / modal.content，正式內容便會自動出現在左側。
   右欄的寬度比例（4fr）故意調得比視覺上「填滿整欄」需要的寬度窄一些：
   影片欄是依照對話框的固定高度、用 16:9 反推出寬度（見下面
   .morph-video-wrap 的 aspect-ratio），不會被拉寬去填滿整欄——正式
   影片如果有字幕，字幕通常貼在畫面下緣，如果為了填滿欄位寬度用
   object-fit: cover 硬裁，裁掉的就是上下邊緣，字幕會被裁到。這裡改成
   「先决定影片該多大，欄位比例配合它」，而不是「欄位多大，影片就撐
   多大」，剩下沒被 16:9 影片用到的寬度，就直接分給左欄文字（4fr : 5fr
   量出來的欄寬跟依比例反推的影片寬度差距很小，剩餘空白只是一圈可接受
   的置中留白，不會整欄空一大半）。 */
.morph-content.is-video {
  display: grid;
  grid-template-columns: minmax(0, 4fr) minmax(0, 5fr);
  grid-template-rows: auto auto minmax(0, 1fr);
  column-gap: 22px;
  padding: 20px 22px;
  overflow: hidden;
}

.morph-content.is-video .morph-title-row {
  grid-column: 1;
  grid-row: 1;
  margin: 0;
}

/* Trek、AotterClam 的 8-bit 圖示左側各有約 6px 的透明留白；只在
   影片版面將可見圖像往左補償，讓 logo 的實際左緣對齊下方虛線。 */
.morph-content.is-video .morph-title-icon {
  transform: translateX(-6px);
}

.morph-content.is-video .morph-title {
  color: var(--title-red);
  font-size: clamp(1rem, 1.4vw, 1.35rem);
}

.morph-content.is-video .morph-divider {
  grid-column: 1;
  grid-row: 2;
  margin: 10px 0 14px;
}

.morph-content.is-video .morph-body {
  grid-column: 1;
  grid-row: 3;
  overflow-y: auto;
  padding-right: 8px;
  font-size: clamp(0.88rem, 1.1vw, 1rem);
  line-height: 1.8;
}

/* 手機橫式的文字彩蛋（.morph-content.is-fullscreen，見 main.js 的
   isMobileFull）：視窗本身撐到接近整個螢幕大小（見
   positionMobileFullDialog()），但內容還是走一般文字模式「標題＋分隔線
   ＋內文」的單欄排版，不是 .is-video 那種左文右片兩欄——這裡沒有影片
   可以放在右邊，硬要切兩欄反而會把文字欄擠小。
   留白（28px 36px）已經上收成 .morph-content 的基礎設定、桌機版也共用
   同一組數值，所以這裡不用再重複宣告一次；只保留手機大視窗才需要的
   字級放大——螢幕整片都是對話框，字級跟著放大讀起來才不會顯得太小。 */
.morph-content.is-fullscreen .morph-title {
  font-size: clamp(1.1rem, 3vw, 1.4rem);
}

.morph-content.is-fullscreen .morph-body {
  font-size: clamp(1rem, 2.2vw, 1.1rem);
  line-height: 1.7;
}

/* 純內文模式（目前只有吉祥物的恭喜對話框在用，見 main.js 的 isPlain）：
   沒有標題可以顯示，連帶標題下面那條橘色虛線分隔線也一起藏起來，只
   留內文。額外把 padding 加大：標題列原本會佔掉一段高度、順便讓內文
   跟上緣有點距離，拿掉標題列之後如果 padding 不變，文字會幾乎貼著
   邊框，所以這裡刻意把四邊的留白都加大，讓文字跟邊框保持距離。 */
.morph-content.is-plain .morph-title-row,
.morph-content.is-plain .morph-divider {
  display: none;
}

/* 四邊留白改成單一數值（不是原本上下/左右各一個數字），文字段落
   到邊框線的距離才會四邊一致，不會左右比上下寬（或反過來）。 */
.morph-content.is-plain {
  padding: 22px;
}

/* 吉祥物主題（目前只有吉祥物的恭喜對話框在用，見 main.js 的 isDark）：
   使用黃色底與深咖啡文字。.type-cursor 用的是 currentColor，不用額外
   處理，顏色會自動跟著文字色一起變成深咖啡。

   邊框形狀也整個換掉，不再沿用其他貼紙彩蛋共用的「階梯狀缺角 + 獨立
   尾巴元素」那一套：兩層獨立的 clip-path（::before 外層、.morph-content
   內層）疊在一起，靠兩組各自算出來的座標「剛好對齊」才不會露餡，
   實際測試發現無論怎麼調整，尖角那個位置還是會透出背景色，猜測是
   複雜的 32 個點階梯缺角疊加兩層時，瀏覽器算次像素座標的捨入誤差
   長期存在、修不掉。
   改成一個更簡單、也更「一體成形」的形狀：直角矩形（完全不切角），
   右側直接用同一個 clip-path 拉出一個尖尖的尾巴，取代原本浮在對話框
   「外面」的獨立 .morph-tail 三角形（見下面 .morph-dialog.is-dark
   .morph-tail 隱藏規則）。整個對話框（含尾巴）現在是「同一塊形狀」
   的兩層（邊框層 + 內容層），只有兩層而不是「邊框+內容+獨立尾巴」
   三層互相對齊，出錯的地方少很多，尖角問題從根本上不會再發生
   （因為已經沒有尖角了）。 */
.morph-dialog.is-dark {
  /* --tail-len：尾巴尖端往右伸出去多遠。
     --tail-base：尾巴底部（連接對話框右邊緣那段）的寬度。
     尾巴的「上下位置」固定用兩層各自的 50%（垂直置中），不再用寫死的
     像素值。原本是 --tail-top-offset: 90px，但對話框高度會隨版面變動：
     全螢幕時版面變寬、文字換行變少，padding box 只剩 96px 高，尾巴中心
     還停在 90px、加上半個底寬 20px，下緣就跑到 110px——超出框底 14px，
     還撞進底部切角，clip-path 的座標點順序因此錯亂，形狀直接破掉（就是
     使用者回報「尖角沒跟對話框貼合」的原因）。
     改用 50% 之後，尾巴永遠在框的正中間，高度再怎麼變都不會溢出（要撞到
     底部切角需要 padding box 矮於 72px，但光是上下留白加一行字就超過 80px）。
     為什麼兩層都寫 50% 就會對齊：::before 的框比 content 上下各多出一個
     border-w，所以它的 50% 剛好等於「content 的 50% 再往上補一個 border-w」
     ——換算到畫面座標後兩者完全重合，不需要再手動加減 border-w。
     --corner-outer：四個角落裡，三個有切角的那三個角，切角尺寸。故意
     用「單一斜線」的簡單切角（跟影片彩蛋的雙線邊框同一種做法），不是
     其他貼紙彩蛋那種 4 階的階梯缺角——階梯缺角需要 8 個點才能畫一個角，
     疊兩層（邊框+內容）對齊出錯的機會也跟著變多，這就是當初「尖角變
     白色」查了老半天的根本原因。單一斜線只要 1 個點，兩層要對齊的
     地方少很多，出錯機會也低很多。右上角（關閉鈕所在的角）維持直角、
     不切，跟其他彩蛋的邏輯一致。 */
  --tail-len: 28px;
  --tail-base: 40px;
  --corner-outer: 16px;
}

.morph-dialog.is-dark::before {
  /* 原本 inset 是四邊都 -border-w，這裡只覆寫右邊：往右多留
     --tail-len 的空間，讓尾巴尖端有地方可以畫出來（clip-path 只能
     在元素「已經有」的範圍內裁切，裁不出超出元素本身大小的形狀）。 */
  right: calc(-1 * var(--border-w) - var(--tail-len));
  clip-path: polygon(
    var(--corner-outer) 0,
    calc(100% - var(--tail-len)) 0,
    calc(100% - var(--tail-len)) calc(50% - var(--tail-base) / 2),
    100% 50%,
    calc(100% - var(--tail-len)) calc(50% + var(--tail-base) / 2),
    calc(100% - var(--tail-len)) calc(100% - var(--corner-outer)),
    calc(100% - var(--tail-len) - var(--corner-outer)) 100%,
    var(--corner-outer) 100%,
    0 calc(100% - var(--corner-outer)),
    0 var(--corner-outer)
  );
}

.morph-content.is-dark {
  background: #e8c270;
  color: var(--ink);
  /* 內層（黃色）的尾巴/切角故意比外層（咖啡色）短一點、小一點
     （各減一個邊框粗細），邊緣才會露出咖啡色線條，跟對話框主體
     的雙色 border 邏輯一致，不會整條尾巴/切角都是同一個黃色、看不出
     邊框。切角尺寸每差一個邊框粗細，斜線角度才會對齊、平行，跟影片
     彩蛋雙線邊框的斜接邏輯一致。 */
  --content-tail-len: calc(var(--tail-len) - var(--border-w));
  --content-tail-base: calc(var(--tail-base) - var(--border-w) * 2);
  --corner-inner: calc(var(--corner-outer) - var(--border-w));
  right: calc(-1 * var(--content-tail-len));
  /* .morph-content.is-plain 原本統一四邊 22px 留白，但這裡的 box 本身
     往右多撐出了 --content-tail-len（讓尾巴有地方畫），padding-right
     如果還是原本的 22px，量的是「到尾巴尖端」的距離，不是「到矩形本體
     右邊」的距離——文字看起來會偏右、右邊留白比左邊窄一截，就是使用者
     說的「文字排版跑掉」。這裡把 padding-right 加回被尾巴吃掉的那一段
     距離，文字段落對矩形本體四邊的實際留白才會恢復一致。 */
  padding-right: calc(22px + var(--content-tail-len));
  clip-path: polygon(
    var(--corner-inner) 0,
    calc(100% - var(--content-tail-len)) 0,
    calc(100% - var(--content-tail-len)) calc(50% - var(--content-tail-base) / 2),
    100% 50%,
    calc(100% - var(--content-tail-len)) calc(50% + var(--content-tail-base) / 2),
    calc(100% - var(--content-tail-len)) calc(100% - var(--corner-inner)),
    calc(100% - var(--content-tail-len) - var(--corner-inner)) 100%,
    var(--corner-inner) 100%,
    0 calc(100% - var(--corner-inner)),
    0 var(--corner-inner)
  );
}

/* 原本浮在對話框外面、獨立拖曳/旋轉的三角形尾巴不再需要——尾巴已經
   內建在上面 ::before / .morph-content 的 clip-path 形狀裡了，兩個
   尾巴疊在一起會變成兩個尾巴黏在一起，很奇怪，所以直接把舊的隱藏掉。
   跟 .morph-dialog.is-video.is-active 那條規則一樣的優先權技巧：疊三個
   class 選擇器，才蓋得過後面 .morph-dialog.is-active .morph-tail
   { display: block; } 那條規則。 */
.morph-dialog.is-dark.is-active .morph-tail {
  display: none;
}

/* 播放器容器：預設隱藏（一般文字模式的對話框用不到），只有在 .is-video
   時才顯示。用 position: absolute + inset: 0 直接貼齊 .morph-content 的
   內緣、完全滿版（不留任何留白邊），不受 .morph-content 本身 padding
   影響。對話框本身的像素風邊框（.morph-dialog 的 clip-path）還是會
   露出來，只是播放器內容本身不再另外留一圈空白。 */
.morph-video-wrap {
  display: none;
  position: absolute;
  inset: 0;
  align-items: center;
  justify-content: center;
  background: var(--ink);
  overflow: hidden;
}

.morph-content.is-video .morph-video-wrap {
  display: flex;
  position: relative;
  inset: auto;
  grid-column: 2;
  grid-row: 1 / -1;
  /* 曾經改成「兩邊都撐滿欄位（100% x 100%）」讓影片框跟欄位一樣大，
     但代價是播放區的比例不再是 16:9，得靠 <video> 的 object-fit: cover
     硬裁成欄位的比例——cover 裁掉的是上下邊緣，如果正式影片把字幕放在
     畫面下緣（常見做法），字幕會被裁掉看不到。
     改回「用高度反推寬度」：固定 height: 100%（撐滿這欄可用的高度），
     width 交給 aspect-ratio: 16 / 9 自動算，永遠是乾淨的 16:9，不會裁到
     任何畫面內容。max-width: 100% 是保險，避免極端寬螢幕下反推出來的
     寬度意外超過欄位本身（那種狀況下才會改成受寬度限制、留白改到上
     下，但不會裁切內容，一樣安全）。
     justify-self 故意用 end（不是 center）：反推出來的寬度幾乎必然比
     欄位窄一點（16:9 跟欄位本身的比例對不太上），這段多出來的寬度該
     擺在哪一側，直接決定影片右邊緣跟外框的距離。用 end 讓影片貼齊欄位
     右緣，右邊緣跟外框的距離就固定等於 .morph-content 的左右 padding
     （22px），跟左欄文字左邊緣到外框的距離（同一個 22px padding）完全
     一致；多出來的寬度全部擠到影片「左側」（也就是文字欄跟影片之間的
     欄距那一側），不會讓左右兩側的留白長得不一樣。如果用 center，這段
     多出來的寬度會平分到影片左右兩側，右邊緣就會離外框比左欄文字更遠，
     兩側留白對不齊。 */
  align-self: center;
  justify-self: end;
  width: auto;
  height: 100%;
  max-width: 100%;
  aspect-ratio: 16 / 9;
  min-width: 0;
  min-height: 0;
  border: 4px solid #e8c270;
  box-shadow: inset 0 0 0 4px var(--ink);
}

.morph-video {
  width: 100%;
  height: 100%;
  /* 播放區（.morph-video-wrap）現在本身就是靠 aspect-ratio 算出來的
     乾淨 16:9，跟影片來源的比例一致，改回 contain：完整顯示整段畫面、
     不裁切任何內容，字幕、角落文字都不會被裁到。只有在實際影片檔案
     跟 16:9 有落差時，contain 才會在左右或上下留一點點空白邊，但那是
     「留白」不是「裁切」，不會遺失畫面內容。 */
  object-fit: contain;
  background: var(--ink);
}

/* YouTube 測試連結用的 iframe 播放器：預設隱藏，只有 JS 判斷
   cfg.modal.youtube 有值、幫 .morph-video-wrap 加上 .is-youtube 時才顯示，
   同時把 <video> 隱藏起來（見下面兩條規則）。iframe 沒有 object-fit 可用，
   直接撐滿整個 wrap，YouTube 播放器本身會自己在畫面裡把影片置中顯示。 */
.morph-video-embed {
  display: none;
  width: 100%;
  height: 100%;
  border: 0;
}

.morph-video-wrap.is-youtube .morph-video,
.morph-video-wrap.is-youtube .morph-video-fallback {
  display: none;
}

.morph-video-wrap.is-youtube .morph-video-embed {
  display: block;
}

/* 影片檔案還在製作中，或是之後上線後某次載入失敗時，用一句文字取代
   播放器畫面，避免對話框看起來像「壞掉」（黑框 + 瀏覽器預設的播放
   失敗圖示）。JS 會監聽 <video> 的 error 事件，觸發時幫 .morph-video-wrap
   加上 .has-error，這裡再靠 CSS 切換播放器/文字兩者的顯示與隱藏，
   影片準備好之後只要把 config.js 裡對應的 video 路徑換成正式檔案，
   這段 fallback 文字就不會再出現。 */
.morph-video-fallback {
  display: none;
  padding: 16px;
  text-align: center;
  font-size: 0.85rem;
  color: var(--cream);
  font-family: 'Noto Sans TC', 'Microsoft JhengHei', sans-serif;
}

.morph-video-wrap.has-error .morph-video {
  display: none;
}

.morph-video-wrap.has-error .morph-video-fallback {
  display: block;
}

/* 講話框尾巴：原本是美術裁切好的 dialogbox-tail.png，現在改成跟邊框
   一樣的做法——純 CSS clip-path 畫一個階梯狀鋸齒邊的三角形（頂點朝上、
   尖端留一小截平邊，跟邊框角落缺角同樣的「4 階樓梯」風格），外層深咖啡
   （.morph-tail 本身）+ 內層米白（::after）疊出粗細一致的邊框感，
   --border-w 直接沿用 .morph-dialog 上定義的那個 CSS 變數（custom property
   會沿著 DOM 往下繼承，.morph-tail 是 .morph-dialog 的子層，兩邊粗細
   自動保持一致，不用另外設定）。
   只有在對話框 is-active（開啟中）才會顯示。因為對話框開啟時是整體用
   transform:scale() 放大出現、不再有 grow/shrink 的位置/大小動畫，
   尾巴不會有變形的問題，可以從一開始就跟著框框一起顯示、一起縮放。 */
/* 注意：.morph-tail 的 containing block 是 .morph-dialog 的「padding box」
   （.morph-dialog 上那圈透明 border 讓 padding box 正確內縮 --border-w）。
   left/top/width/height 由 JS 依照每個貼紙自己的 cfg.tailBox（百分比，
   相對於對話框本身的 padding box）動態設定，可以在 ?calibrate 版面編輯器
   裡個別拖曳位置、調整大小、旋轉角度。

   DOM 順序很重要：.morph-tail 在 index.html 裡故意放在 .morph-content
   「前面」（雖然視覺上尾巴大部分露在框外，感覺應該疊在最上層）。原因是
   不少貼紙的 tailBox 數值會讓尾巴的底部稍微「切進」對話框本體內側
   （例如 top 是負值但沒有負到完全脫離 padding box），這種情況下尾巴
   本身的深咖啡色（外層 clip-path 三角形）就會有一小截疊在對話框米白色
   內容區上面，變成一條看得見的黑線。把 .morph-tail 排在 .morph-content
   「前面」，讓米白色內容區在疊層順序上蓋在尾巴之上，切進對話框內側的
   那一小截尾巴就會被內容區蓋掉、看不見，只留下真正伸出框外的部分
   （不會被內容區蓋到，因為內容區的範圍就是對話框本身而已），效果就是
   「尾巴跟對話框重疊的部分自動消失」，不用針對每張貼紙個別調整座標。
   z-index 都維持 auto，純粹靠 HTML 裡的先後順序決定疊層，所以順序不能
   隨意調換（.morph-close 例外，它自己設了 z-index: 5，不受這個順序
   影響，永遠蓋在最上面）。 */
.morph-tail {
  display: none;
  position: absolute;
  background: var(--ink);
  pointer-events: none;
  transform-origin: center center;
  clip-path: polygon(
    0 100%,
    0 75%,
    12.5% 75%,
    12.5% 50%,
    25% 50%,
    25% 25%,
    37.5% 25%,
    37.5% 0,
    50% 0,
    62.5% 0,
    62.5% 25%,
    75% 25%,
    75% 50%,
    87.5% 50%,
    87.5% 75%,
    100% 75%,
    100% 100%
  );
}

.morph-tail::after {
  content: "";
  position: absolute;
  /* 底邊故意不內縮（inset 的 bottom 那個值是 0）：尾巴的底邊是跟畫框
     重疊、融合在一起的接口，不需要畫出邊框粗細，只有左右兩條斜邊
     才要露出跟畫框一樣粗細的深咖啡色描邊。 */
  inset: var(--border-w) var(--border-w) 0 var(--border-w);
  background: var(--cream);
  clip-path: polygon(
    0 100%,
    0 75%,
    12.5% 75%,
    12.5% 50%,
    25% 50%,
    25% 25%,
    37.5% 25%,
    37.5% 0,
    50% 0,
    62.5% 0,
    62.5% 25%,
    75% 25%,
    75% 50%,
    87.5% 50%,
    87.5% 75%,
    100% 75%,
    100% 100%
  );
}

.morph-dialog.is-active .morph-tail {
  display: block;
}

/* ===== Toast ===== */
.toast {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(30, 30, 50, 0.9);
  color: #fff;
  padding: 10px 24px;
  border-radius: 24px;
  font-size: 0.9rem;
  z-index: 900;
  pointer-events: none;
  animation: toast-in 0.3s ease;
}

.toast[hidden] {
  display: none;
}

@keyframes toast-in {
  from { opacity: 0; transform: translateX(-50%) translateY(12px); }
  to { opacity: 1; transform: translateX(-50%) translateY(0); }
}

/* ===== 橫式瀏覽提醒（手機＋直式時自動彈出） =====
   邊框畫法直接沿用 .morph-dialog / .morph-content 那一套「clip-path 切階梯
   狀缺角」技巧（外層深色框 + 內層淺色內容，兩層各自用同一組 --corner/--step
   跟 --ic/--is 變數切出互補的斜角），這裡把顏色換成地圖水域的藍綠色系
   （深水色邊框、淺水色點綴），跟彩蛋對話框的深咖啡／米白做出區隔。
   因為不是從畫面上某個貼紙位置展開的，用固定置中的淡入＋縮放呈現，
   沒有像彩蛋對話框那樣指向定點的尾巴。顯示時機／自動關閉邏輯見
   main.js 的 initRotateHint()。 */
.rotate-hint {
  position: fixed;
  inset: 0;
  z-index: 980;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(15, 46, 43, 0.55);
}

.rotate-hint.is-visible {
  display: flex;
}

.rotate-hint-frame {
  /* 深水色（邊框底色）＋淺水色（圖示／分隔線）：取自 .morph-dialog 既有的
     --teal (#3ec7c4)，深水色是它的加深版，兩者都跟地圖上的水域色系同一
     家族，但整體視覺跟深咖啡／米白的彩蛋對話框明顯不同。 */
  --rh-border: #2c8884;
  --rh-border-light: #3ec7c4;
  --rh-cream: #fefdfb;
  --rh-ink: #204b46;

  --border-w: 8px;
  --corner: 22px;
  --step: calc(var(--corner) / 4);

  position: relative;
  width: min(84vw, 360px);
  border: var(--border-w) solid transparent;
  opacity: 0;
  transform: scale(0.85);
  transition: transform 0.28s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
}

.rotate-hint.is-visible .rotate-hint-frame {
  opacity: 1;
  transform: scale(1);
}

/* 外層深水色邊框本體，畫在 ::before 上而不是直接畫在 .rotate-hint-frame
   身上，維持跟 .morph-dialog::before 一致的畫法（純粹為了跟其他對話框
   共用同一套技巧，方便日後維護）。polygon 座標跟 .morph-dialog::before
   用的是同一組階梯狀缺角算式（僅共用 --corner/--step 變數名稱，數值
   各自獨立）。 */
.rotate-hint-frame::before {
  content: "";
  position: absolute;
  z-index: 0;
  inset: calc(-1 * var(--border-w));
  background: var(--rh-border);
  clip-path: polygon(
    0 var(--corner),
    var(--step) var(--corner),
    var(--step) calc(var(--corner) - var(--step)),
    calc(var(--step) * 2) calc(var(--corner) - var(--step)),
    calc(var(--step) * 2) calc(var(--corner) - var(--step) * 2),
    calc(var(--step) * 3) calc(var(--corner) - var(--step) * 2),
    calc(var(--step) * 3) calc(var(--corner) - var(--step) * 3),
    var(--corner) calc(var(--corner) - var(--step) * 3),
    var(--corner) 0,
    calc(100% - var(--corner)) 0,
    calc(100% - var(--corner)) var(--step),
    calc(100% - var(--step) * 3) var(--step),
    calc(100% - var(--step) * 3) calc(var(--step) * 2),
    calc(100% - var(--step) * 2) calc(var(--step) * 2),
    calc(100% - var(--step) * 2) calc(var(--step) * 3),
    calc(100% - var(--step)) calc(var(--step) * 3),
    calc(100% - var(--step)) var(--corner),
    100% var(--corner),
    100% calc(100% - var(--corner)),
    calc(100% - var(--step)) calc(100% - var(--corner)),
    calc(100% - var(--step)) calc(100% - var(--step) * 3),
    calc(100% - var(--step) * 2) calc(100% - var(--step) * 3),
    calc(100% - var(--step) * 2) calc(100% - var(--step) * 2),
    calc(100% - var(--step) * 3) calc(100% - var(--step) * 2),
    calc(100% - var(--step) * 3) calc(100% - var(--step)),
    calc(100% - var(--corner)) calc(100% - var(--step)),
    calc(100% - var(--corner)) 100%,
    var(--corner) 100%,
    var(--corner) calc(100% - var(--step)),
    calc(var(--step) * 3) calc(100% - var(--step)),
    calc(var(--step) * 3) calc(100% - var(--step) * 2),
    calc(var(--step) * 2) calc(100% - var(--step) * 2),
    calc(var(--step) * 2) calc(100% - var(--step) * 3),
    var(--step) calc(100% - var(--step) * 3),
    var(--step) calc(100% - var(--corner)),
    0 calc(100% - var(--corner))
  );
}

.rotate-hint-box {
  position: relative;
  z-index: 1;
  padding: 26px 20px 22px;
  background: var(--rh-cream);
  color: var(--rh-ink);
  text-align: center;
  font-family: 'Noto Sans TC', 'Microsoft JhengHei', sans-serif;
  /* 內層淺色內容也要切一樣階梯狀的缺角（角落尺寸 = 外層缺角 - 邊框粗細），
     算法跟 .morph-content 完全一樣，不然它是方形直角，四個角落會直接
     方形凸出，蓋過外層裁出來的斜角缺口。 */
  --ic: calc(var(--corner) - var(--border-w));
  --is: calc(var(--ic) / 4);
  clip-path: polygon(
    0 var(--ic),
    var(--is) var(--ic),
    var(--is) calc(var(--ic) - var(--is)),
    calc(var(--is) * 2) calc(var(--ic) - var(--is)),
    calc(var(--is) * 2) calc(var(--ic) - var(--is) * 2),
    calc(var(--is) * 3) calc(var(--ic) - var(--is) * 2),
    calc(var(--is) * 3) calc(var(--ic) - var(--is) * 3),
    var(--ic) calc(var(--ic) - var(--is) * 3),
    var(--ic) 0,
    calc(100% - var(--ic)) 0,
    calc(100% - var(--ic)) var(--is),
    calc(100% - var(--is) * 3) var(--is),
    calc(100% - var(--is) * 3) calc(var(--is) * 2),
    calc(100% - var(--is) * 2) calc(var(--is) * 2),
    calc(100% - var(--is) * 2) calc(var(--is) * 3),
    calc(100% - var(--is)) calc(var(--is) * 3),
    calc(100% - var(--is)) var(--ic),
    100% var(--ic),
    100% calc(100% - var(--ic)),
    calc(100% - var(--is)) calc(100% - var(--ic)),
    calc(100% - var(--is)) calc(100% - var(--is) * 3),
    calc(100% - var(--is) * 2) calc(100% - var(--is) * 3),
    calc(100% - var(--is) * 2) calc(100% - var(--is) * 2),
    calc(100% - var(--is) * 3) calc(100% - var(--is) * 2),
    calc(100% - var(--is) * 3) calc(100% - var(--is)),
    calc(100% - var(--ic)) calc(100% - var(--is)),
    calc(100% - var(--ic)) 100%,
    var(--ic) 100%,
    var(--ic) calc(100% - var(--is)),
    calc(var(--is) * 3) calc(100% - var(--is)),
    calc(var(--is) * 3) calc(100% - var(--is) * 2),
    calc(var(--is) * 2) calc(100% - var(--is) * 2),
    calc(var(--is) * 2) calc(100% - var(--is) * 3),
    var(--is) calc(100% - var(--is) * 3),
    var(--is) calc(100% - var(--ic)),
    0 calc(100% - var(--ic))
  );
}

/* 提示圖示：手機外框（.rotate-hint-icon-phone）固定傾斜一個角度，搭配
   旁邊固定不動的弧形箭頭，用靜態畫面示意「請旋轉手機」，不需要另外
   準備圖檔素材，也不會有動態效果。 */
.rotate-hint-icon {
  display: block;
  width: 56px;
  height: 50px;
  margin: 0 auto 4px;
  overflow: visible;
}

.rotate-hint-icon-arrow,
.rotate-hint-icon-phone rect,
.rotate-hint-icon-phone line {
  fill: none;
  stroke: var(--rh-border-light);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.rotate-hint-icon-arrowhead {
  fill: var(--rh-border-light);
}

.rotate-hint-icon-phone {
  transform-box: fill-box;
  transform-origin: center;
  transform: rotate(35deg);
}

.rotate-hint-title {
  /* 字級跟下面的 .rotate-hint-body 一致，顏色改成跟圖示同一組淺水色
     （--rh-border-light），跟內文的深水色文字（--rh-ink）做出區隔。 */
  font-size: 0.88rem;
  font-weight: 700;
  color: var(--rh-border-light);
  margin-bottom: 10px;
}

.rotate-hint-divider {
  width: 60%;
  height: 3px;
  margin: 0 auto;
  background-image: repeating-linear-gradient(
    to right,
    var(--rh-border-light) 0,
    var(--rh-border-light) 6px,
    transparent 6px,
    transparent 12px
  );
}

.rotate-hint-body {
  /* 按鈕拿掉之後，這段文字是對話框裡最後一個元素，margin-bottom 收掉，
     交給 .rotate-hint-box 本身的 padding-bottom 控制下緣留白就好，
     避免留白疊加、底部看起來空得太多。 */
  font-size: 0.88rem;
  line-height: 1.6;
  margin: 14px 0 0;
}
