@font-face {
  font-family: "VFX Display";
  src: local("Avenir Next Condensed"), local("DIN Condensed");
  font-display: swap;
}

:root {
  color-scheme: dark;
  /* Discord 預設深色主題的主背景色。 */
  --page: #313338;
  --ink: #f6f8ff;
  --muted: #a7adba;
  --quiet: #727987;
  --line: rgba(255, 255, 255, .14);
  /* ===== 玻璃材質 =====
     一片真的玻璃需要三件事同時成立：底下的東西看得見（半透明）、看見的東西被
     折散（backdrop blur + 提高飽和度），以及邊緣比中央亮（鏡緣的全反射）。
     少了任何一件都只會變成一塊深色壓克力板。
     這組給的是頂欄、篩選列那些真的疊在頁面上的小元件；卡片走底板那條路。 */
  --glass-tint: rgba(18, 24, 37, .26);
  /* ===== 卡片底板 =====
     iOS 的玻璃 icon 不是一扇開在桌布上的窗：它底下永遠有一層自己的、不透明的
     色塊，玻璃疊在那上面。邊緣讀得出厚度，是因為它在折那層底板——有東西可折。
     我們原本的卡片底下是近黑（整頁亮度中位數 18/255），斜邊做得再準，也只是
     一條白線浮在黑上。改成不透明的中間調底板之後，斜邊的亮線與暗帶才有對比可
     依附。代價是頁面背景不再透進卡片，所以各自的主題色要自己撐起身分（見
     .card 的兩道 radial，等同 icon 的底色層）。 */
  --card-base: #242a39;
  --glass-blur: 26px;
  --glass-sat: 185%;
  --glass-rim: rgba(255, 255, 255, .5);
  --font-display: "VFX Display", "Avenir Next", "Helvetica Neue", sans-serif;
  --font-ui: "Avenir Next", "Helvetica Neue", "PingFang TC", sans-serif;
  --font-mono: "SFMono-Regular", "IBM Plex Mono", "Roboto Mono", monospace;
}

* { box-sizing: border-box; }
html { min-width: 320px; background: var(--page); scroll-behavior: smooth; }
body {
  min-height: 100vh; margin: 0; overflow-x: hidden; color: var(--ink); background: var(--page);
  font-family: var(--font-ui); -webkit-font-smoothing: antialiased; text-rendering: geometricPrecision;
}
button, a { -webkit-tap-highlight-color: transparent; }
a { color: inherit; }

.skip-link {
  position: fixed; z-index: 100; top: 12px; left: 12px; padding: 10px 14px; color: #071019;
  background: #fff; border-radius: 999px; transform: translateY(-150%); transition: transform 160ms ease;
}
.skip-link:focus { transform: translateY(0); }

/* 背景就是一塊底色，沒有光暈、沒有漸層、沒有網格與雜訊。
   前一版是兩顆 850px 的 conic-gradient 疊 filter: blur(90px)。頁面靜止時那層
   模糊會被快取，看起來毫無代價；但只要卡片的即時預覽開始逐幀重繪，Chrome 就
   每一幀重算一次那兩個大模糊——整個分頁（不只 iframe）從 60fps 掉到 8.8fps，
   每幀 114ms，其中畫圖只佔 0.5ms。單獨關掉那道 filter 就立刻回到 60fps。
   will-change、contain: paint、isolation 全都救不了，Chrome 照樣重算。
   這個背景不是這個專案的重點，所以直接換成單色，順便把問題連根拔掉。 */

.site-shell { position: relative; z-index: 1; width: min(100% - 48px, 1540px); margin: 0 auto; }
/* 版頭只有兩件東西：左邊是身分，右邊是唯一的控制項。篩選列原本自己佔一整列，
   但它只有一排藥丸，那一列有九成是空的——收進版頭之後標題與篩選共用同一條基線，
   卡片也往上移了四十幾個像素。 */
.topbar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 14px;
  min-height: 86px; border-bottom: 1px solid rgba(255,255,255,.08);
}
.brand { display: inline-flex; align-items: center; gap: 12px; color: var(--ink); text-decoration: none; }
.brand strong { font: 700 24px/1 var(--font-display); letter-spacing: .14em; }
.brand-version { font: 500 11px/1 var(--font-display); letter-spacing: .08em; color: var(--ink); opacity: .55; padding: 3px 7px; border: 1px solid currentColor; border-radius: 999px; }
#gallery { padding-top: 22px; }
.filters {
  display: flex; gap: 4px; padding: 5px; overflow-x: auto; border: 1px solid rgba(255,255,255,.13);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(0,0,0,.08), rgba(255,255,255,.02) 64%, rgba(255,255,255,.06)), var(--glass-tint);
  box-shadow: inset 0 1px rgba(255,255,255,.2), inset 0 -1px rgba(255,255,255,.3),
    inset 0 -7px 9px -8px rgba(255,255,255,.35), 0 14px 38px rgba(0,0,0,.22);
  scrollbar-width: none;
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
}
.filters::-webkit-scrollbar { display: none; }
.filter-btn {
  flex: 0 0 auto; min-height: 34px; padding: 0 14px; color: #8e95a3; border: 0; border-radius: 999px;
  background: transparent; font: 600 9px/1 var(--font-mono); letter-spacing: .1em; text-transform: uppercase;
  cursor: pointer; transition: color 180ms ease, background-color 180ms ease, box-shadow 180ms ease;
}
.filter-btn:hover { color: #fff; }
.filter-btn[aria-pressed="true"] {
  color: #10131a; background: rgba(255,255,255,.91); box-shadow: 0 2px 12px rgba(255,255,255,.12), inset 0 1px #fff;
}

#cards {
  display: grid; grid-template-columns: repeat(10,minmax(0,1fr)); grid-auto-flow: dense;
  grid-auto-rows: 8px; align-items: start;
  gap: clamp(12px,1.25vw,20px); padding-bottom: 68px;
}
/* 整張卡片只能有一條輪廓。媒體不內縮、也不自己設圓角，直接由卡片的 overflow
   裁切 —— 這樣它的外形就「是」卡片的外形，不是另一個對得很準的形狀；兩條同心
   輪廓再怎麼對齊，看起來都還是疊了兩層。 */
.card {
  --theme-left: 90,210,255; --theme-right: 255,112,205;
  position: relative; grid-column: span 2; min-width: 0; overflow: hidden; color: var(--ink);
  border: 0; border-radius: 24px;
  /* 由下往上三層，跟 iOS icon 的組成一樣：不透明底板 → 主題色 → 玻璃表面。
     表面那層本身幾乎是平的，玻璃感全部交給邊緣去講——一道斜向的擦光會把整塊
     卡片變成一片有紋路的壓克力，真正的厚玻璃中央是乾淨的，只有邊緣在折光。
     底板不透明，所以不掛 backdrop-filter：什麼都透不過來，那層模糊只是白花
     GPU（13 張卡片同時模糊不是小數目）。 */
  background:
    linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.012) 34%,
      rgba(0,0,0,.05) 68%, rgba(255,255,255,.05)),
    radial-gradient(130% 96% at 10% -12%, rgba(var(--theme-left),.20), transparent 52%),
    radial-gradient(130% 96% at 94% 112%, rgba(var(--theme-right),.16), transparent 50%),
    var(--card-base);
  /* 卡片本身只負責「外面」：一條把玻璃跟底色分開的暗線，加上落影。
     內側的斜邊全部搬到 ::after —— 畫在這裡會被媒體蓋住，變成只有下半部看得到。 */
  box-shadow:
    0 0 0 1px rgba(0,0,0,.34),
    0 18px 44px -16px rgba(0,0,0,.6), 0 4px 10px -5px rgba(0,0,0,.45);
  text-decoration: none; transform: translateZ(0); isolation: isolate; opacity: 0;
  animation: card-in 620ms cubic-bezier(.22,.75,.22,1) forwards; animation-delay: calc(var(--order) * 48ms);
  transition: transform 380ms cubic-bezier(.2,.8,.2,1), box-shadow 380ms ease,
    background-color 240ms ease, opacity 240ms ease;
}
/* iOS 的圓角是連續曲率，不是正圓弧；支援的瀏覽器直接用，其餘退回一般圓角。
   媒體不需要跟著設 —— 它是被卡片裁出來的，形狀自動一致。
   兩個偽元素就必須自己設：corner-shape 不會繼承，border-radius: inherit 只帶
   到半徑。少了這一行，鏡緣與游標高光會用正圓弧畫在一張超橢圓的卡片上，轉角處
   兩條弧分家——白線跟底下的黑色媒體對不起來，看起來就是上下圓角不一致。 */
@supports (corner-shape: squircle) {
  .card { corner-shape: squircle; border-radius: 30px; }
  .card::after { corner-shape: squircle; }
}
.card--featured { grid-column: span 4; }
/* ===== 斜邊 =====
   一條 1px 的線讀起來是 border，不是玻璃。真正看得出厚度的邊緣是分層的：
   由外往內是「亮線 → 比本體更暗的折射帶 → 本體」，那條暗帶是玻璃把背景壓縮
   進邊界留下的痕跡，少了它就只是描邊。
   用堆疊的 inset box-shadow 而不是 mask 描邊：前面的陰影畫在後面的上面，所以
   把細的亮線排前、厚的暗帶排後，就疊出由外而內的層次；而且 box-shadow 跟著
   corner-shape 走，不像 mask 那樣一碰到偽元素混合就整條消失。
   方向性靠位移：整圈給一點底，上緣中等，下緣最亮——光穿過整塊玻璃之後會堆在
   底部，所以下面那條通常比上面亮。 */
.card::after {
  content: ""; position: absolute; z-index: 5; inset: 0; border-radius: inherit; pointer-events: none;
  box-shadow:
    inset 0 1.5px 0 -0.5px var(--glass-rim),
    inset 0 -1.6px 0 -0.5px rgba(255,255,255,.9),
    inset 0 0 0 1px rgba(255,255,255,.17),
    inset 0 0 0 2.5px rgba(0,0,0,.26),
    inset 0 9px 10px -9px rgba(255,255,255,.26),
    inset 0 -12px 13px -11px rgba(255,255,255,.4);
  transition: opacity 240ms ease, filter 240ms ease;
}
.card:hover, .card:focus-visible {
  transform: translateY(-7px) scale(1.006); outline: none;
  background-color: #2b3244;
  box-shadow: 0 34px 78px -18px rgba(0,0,0,.72), 0 8px 20px -8px rgba(0,0,0,.55),
    0 0 42px -14px rgba(var(--theme-left),.35), 0 0 42px -14px rgba(var(--theme-right),.3);
}
.card:hover::after, .card:focus-visible::after { filter: brightness(1.45); }
.card[hidden] { display: none; }
@keyframes card-in { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: translateY(0); } }

/* 沒有自己的圓角，也沒有自己的邊。上緣交給卡片的鏡緣，下緣只用一道很淡的暗影
   跟底下的玻璃分界 —— 給它一條實線就等於又畫了第二條輪廓。 */
.card-preview {
  position: relative; aspect-ratio: 4/3; overflow: hidden;
  background: radial-gradient(circle at 28% 20%, rgba(var(--theme-left),.48), transparent 30%),
    radial-gradient(circle at 73% 76%, rgba(var(--theme-right),.36), transparent 34%), #090c12;
  box-shadow: 0 10px 18px -10px rgba(0,0,0,.85), 0 2px 4px -2px rgba(0,0,0,.6);
}
.card--featured .card-preview { aspect-ratio: 16/8.9; }
.card-poster, .card-preview iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.card-poster {
  display: block; object-fit: cover; transform: scale(1.015); filter: saturate(.86) contrast(1.03) brightness(.82);
  transition: transform 700ms cubic-bezier(.2,.8,.2,1), filter 420ms ease, opacity 260ms ease;
}
.card:hover .card-poster, .card:focus-visible .card-poster {
  transform: scale(1.045); filter: saturate(1.03) contrast(1.04) brightness(.92);
}
/* fitPreview() 用 left/top 50% + translate(-50%,-50%) 置中，縮放必須繞著元素中心
   算。原本這裡是 transform-origin: left top，縮放會從左上角起算，整個 iframe 被
   往左上推掉半個未縮放的尺寸（660×570 的一半）——小卡片幾乎只剩一角落在框內，
   看起來就像「hover 了卻不播放」，大卡片則是主體位置跟海報對不上。 */
.card-preview iframe {
  z-index: 1; width: 660px; height: 570px; visibility: hidden; opacity: 0; pointer-events: none;
  transform-origin: 50% 50%; transition: opacity 260ms ease;
}
.card-preview iframe.is-ready { visibility: visible; opacity: 1; }
.preview-prism {
  position: absolute; z-index: 2; inset: 0; border-radius: inherit; pointer-events: none;
  background: linear-gradient(112deg, rgba(255,255,255,.12), transparent 18% 74%, rgba(255,255,255,.05)),
    linear-gradient(180deg, transparent 52%, rgba(3,5,9,.3)); box-shadow: inset 0 -1px rgba(255,255,255,.1);
}
/* 掃描光只有在「這張卡正在載入或正在播」時才存在。
   原本它是 display 著的、opacity: 0、外加 will-change——那三件事加起來等於 13 張
   卡各自常駐一個 GPU 圖層，而且因為 mix-blend-mode: screen，每一個都要求合成器
   把底下讀回來混一次。一張都看不見，成本卻照付。同時間最多只有一張卡在預覽，所以
   基態直接 display: none，will-change 也只在真的要動的那一瞬間才掛。 */
.preview-prism::after {
  content: ""; position: absolute; display: none; inset: -40% -65%; opacity: 0;
  transform: translate3d(-42%,0,0) rotate(12deg);
  background: linear-gradient(90deg, transparent 37%, rgba(var(--theme-left),.04) 45%, rgba(255,255,255,.28) 50%, rgba(var(--theme-right),.12) 55%, transparent 63%);
  mix-blend-mode: screen;
}
.card.is-loading-preview .preview-prism::after,
.card.is-previewing .preview-prism::after { display: block; will-change: transform, opacity; }
.card.is-loading-preview .preview-prism::after {
  opacity: .72; animation: preview-scan 1.15s cubic-bezier(.45,0,.2,1) infinite;
}
.card.is-previewing .preview-prism::after {
  opacity: .24; animation: preview-scan 3.8s ease-in-out infinite;
}
@keyframes preview-scan {
  from { transform: translate3d(-42%,0,0) rotate(12deg); }
  to { transform: translate3d(42%,0,0) rotate(12deg); }
}
.preview-state {
  position: absolute; z-index: 3; top: 15px; right: 15px; display: inline-flex; align-items: center; gap: 7px;
  height: 28px; padding: 0 10px; color: rgba(255,255,255,.82); border: 1px solid rgba(255,255,255,.18);
  border-radius: 999px;
  background-image: linear-gradient(180deg, rgba(0,0,0,.1), rgba(255,255,255,.03) 62%, rgba(255,255,255,.08));
  background-color: rgba(8,11,17,.58);
  box-shadow: inset 0 1px rgba(255,255,255,.2), inset 0 -1px rgba(255,255,255,.34);
  text-shadow: 0 1px 1px rgba(0,0,0,.6);
  font: 600 8px/1 var(--font-mono);
  letter-spacing: .13em; text-transform: uppercase;
  transition: background-color 200ms ease;
}
/* 玻璃只給游標底下那一張。徽章是 28px 高的小藥丸，疊在一張已經被壓暗到
   brightness(.82) 的海報上——模糊與不模糊的差別，靠把底色從 .42 補到 .58 就幾乎
   補平了。但成本差很多：backdrop-filter 不是一個濾鏡參數，是每幀一次獨立的
   render pass（擷取背景 → 降採樣 → 兩趟高斯 → saturate → 合回去）。13 張卡全掛
   著，就是每幀 13 次。而真的需要玻璃的只有正在播即時預覽的那一張：只有它底下的
   東西在動。 */
.card:hover .preview-state, .card:focus-visible .preview-state {
  background-color: rgba(8,11,17,.42);
  backdrop-filter: blur(14px) saturate(var(--glass-sat));
  -webkit-backdrop-filter: blur(14px) saturate(var(--glass-sat));
}
.preview-state::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: rgba(255,255,255,.42); }
.card.is-loading-preview .preview-state::before { background: #ffd67a; box-shadow: 0 0 10px rgba(255,214,122,.62); }
.card.is-loading-preview .preview-state span { font-size: 0; }
.card.is-loading-preview .preview-state span::after { content: "Loading"; font-size: 8px; }
.card.is-previewing .preview-state::before { background: #74f5c7; box-shadow: 0 0 10px rgba(116,245,199,.72); }
.card.is-previewing .preview-state span { font-size: 0; }
.card.is-previewing .preview-state span::after { content: "Live"; font-size: 8px; }

/* 文字直接躺在玻璃上，不再自己鋪一層不透明底 —— 底一補上去，整塊卡片就又變回
   壓克力板了。背景的漸層與模糊由 .card 一次負責。 */
.card-body { position: relative; z-index: 2; padding: 17px 18px 19px; background: none; }
.card-meta {
  display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-bottom: 12px;
  color: #8d94a2; font: 600 9px/1 var(--font-mono); letter-spacing: .12em; text-transform: uppercase;
  text-shadow: 0 1px 1px rgba(0,0,0,.5);
}
.card-index { color: rgb(var(--theme-left)); }
/* ===== 壓花 =====
   玻璃底下的東西不會是平貼上去的：受光的那一側有一道極細的亮邊，背光側落一層
   短而緊的暗影。位移只有 1px、透明度都壓得很低——一旦看得出「有陰影」就變成
   浮在上面的貼紙，這一層要讓人感覺得到、但指不出來。 */
.card-title {
  margin: 0 0 14px; font: 560 clamp(17px,1.4vw,23px)/1.12 var(--font-display);
  letter-spacing: -.025em; text-wrap: balance;
  text-shadow: 0 -1px 0 rgba(255,255,255,.1), 0 1px 2px rgba(0,0,0,.6);
}
.card-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.tag {
  padding: 6px 9px 5px; color: #bcc3d0; border: 1px solid rgba(255,255,255,.13); border-radius: 8px;
  background: linear-gradient(180deg, rgba(0,0,0,.08), rgba(255,255,255,.025) 60%, rgba(255,255,255,.07));
  box-shadow: inset 0 1px rgba(255,255,255,.14), inset 0 -1px rgba(255,255,255,.26);
  text-shadow: 0 1px 1px rgba(0,0,0,.55);
  font: 570 10px/1 var(--font-mono); letter-spacing: .07em; text-transform: uppercase;
}
.tag[data-tech="Three.js"] { color: #f0f3f8; border-color: rgba(var(--theme-left),.28); background: rgba(var(--theme-left),.075); }

.site-footer {
  display: flex; justify-content: space-between; gap: 30px; padding: 28px 0 44px; color: #6f7684;
  border-top: 1px solid rgba(255,255,255,.08); font: 500 9px/1.5 var(--font-mono);
  letter-spacing: .12em; text-transform: uppercase;
}
.site-footer strong { color: #aeb4c0; font-weight: 600; }
body.leaving .topbar,
body.leaving .site-footer, body.leaving .card:not(.launching) { opacity: 0; transition: opacity 260ms ease; }
.card.launching { z-index: 20; transform: scale(1.035); opacity: 0; transition: transform 360ms cubic-bezier(.4,0,.9,.35), opacity 300ms ease 60ms; }

@media (max-width: 1200px) {
  #cards { grid-template-columns: repeat(12,minmax(0,1fr)); }
  .card { grid-column: span 4; }
  .card--featured { grid-column: span 8; }
}
@media (max-width: 900px) {
  .site-shell { width: min(100% - 36px, 900px); }
  .card, .card--featured { grid-column: span 6; }
  .card--featured .card-preview { aspect-ratio: 4/3; }
}
@media (max-width: 680px) {
  .site-shell { width: min(100% - 24px, 520px); }
  /* 窄螢幕放不下「標題 + 五顆篩選」同一排，讓 topbar 換行、篩選列自己佔滿一行。 */
  .topbar { min-height: 0; padding: 16px 0; }
  #gallery { padding-top: 16px; }
  .filters { width: 100%; }
  .filter-btn { flex: 1 0 auto; }
  #cards { gap: 15px; padding-bottom: 54px; }
  .card, .card--featured { grid-column: span 12; }
  .card-preview, .card--featured .card-preview { aspect-ratio: 16/10; }
  .card-body { padding: 17px 18px 19px; }
  .card-title { font-size: 21px; }
  .site-footer { flex-direction: column; padding-bottom: 30px; }
}
/* reduced-motion 只關「自己會動的」，不關「你要它動它才動的」。
   這個查詢針對的是非自願的動態：背景光暈的漂移縮放、卡片進場的位移、無限循環的
   掃描光。hover 一張卡片是使用者主動請求，把它的過渡一起壓成 0.01ms 不會讓任何人
   比較舒服，只會讓整站的每一次互動變成硬跳——實測 59.9fps、一幀都沒掉，但讀起來
   就是卡。原本那條 `*, *::before, *::after` 的全域覆寫正是這樣，而且 Windows 只要
   關掉「動畫效果」就會整站生效。 */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  /* .card 的基態是 opacity: 0，靠 card-in 的 forwards 補到 1。拿掉動畫就必須同時
     把透明度補回來，否則 13 張卡全部隱形。 */
  .card { animation: none; opacity: 1; }
  .card.is-loading-preview .preview-prism::after,
  .card.is-previewing .preview-prism::after { display: none; }
}
@media (prefers-contrast: more) {
  .card, .card:hover, .card:focus-visible { background: #11151e; }
}
